Commit Graph
614 Commits
Author SHA1 Message Date
austinried 02b3ddbb98 add a song info dialog and show the encoding in the manager
The song context menus gain Info, which lists every field we hold for the song
and, when it is downloaded, the file's format, bitrate, sample rate, channels
and size read from the file itself. For the song currently playing it takes the
encoding from the media3 stream instead, so a transcoded stream reports what is
actually being played. The menu is shared, so this reaches the album, playlist,
now playing and queue menus at once.

The downloads manager shows the same format and bitrate beside the size on each
song row, read lazily for the rows on screen rather than for the whole tree.
2026-10-04 00:30:03 +00:00
austinried adb9367f18 add a download quality preference
Downloads always fetched the original file. The setting takes the same choices
as the stream quality, and when it transcodes the download uses the stream
endpoint with those parameters instead, so the stored file is already the
wanted encoding rather than the original that would be transcoded on each play.

A preference change applied before the source is ready falls back to original,
and the URI is built from the same cached source the stream URL uses.
2026-10-04 00:30:03 +00:00
austinried 26b8963205 fail a download whose payload is a server error page
A Subsonic server answers a refused download with HTTP 200 and an XML error
document, which the platform downloader saves like any payload, so the row went
Completed and playback then tried to decode the error page. No audio container
starts with markup, so the leading bytes tell them apart: a Completed download
that starts with < or { is failed instead, and a stored Completed row is
re-checked on reconcile so existing bad files get cleaned up too.
2026-10-04 00:30:03 +00:00
austinried 954d0eddd2 search the library through FTS5 trigram tables 2026-10-04 00:30:03 +00:00
austinried 46dc6bda0e select a server on tap and refuse to delete the active one 2026-10-04 00:30:03 +00:00
austinried a77ce6984e let a server be edited and move its delete into the edit screen 2026-10-04 00:30:03 +00:00
austinried e6b58b8f59 stop the poll loop and the notification on inactive waiting rows
The cross-source reconcile went too far: a source switched away from can have
waiting rows that promote will never hand over, since it resolves only while
active and downloads are only ever started for the active source. Those rows
kept the loop alive forever, so it scanned the whole table and ran every second
for the life of the process, and the notification sat there claiming to be
downloading songs that were not moving.

Both now count a row as in flight only when the engine owns it or it belongs to
the active source. Reconcile still covers every source, so a download running
for a source you switched away from keeps updating.

The downloaded-filter DAO test gains a second artist that must be excluded (its
first version passed whichever artist clause you wrote), and the preference test
covers the legacy three-field value and a downloaded round trip.
2026-10-04 00:30:03 +00:00
austinried 7d04125f58 add the Downloaded filter to the library tabs
One toggle per tab, persisted with the rest of the list query, narrowing to
entities that have at least one completed download. It rides the existing filter
SQL: every album, artist and playlist paging query takes a flag and the shared
filter constant adds an EXISTS over song_downloads, so it composes with the
sort, search and starred filter rather than being a separate mode.

The filters sheet now renders on every tab, since playlists support Downloaded
but not Starred.
2026-10-04 00:30:03 +00:00
austinried 7db6d1365f reconcile and notify downloads outside the active source
A source switched away from kept downloading on the platform while our poller
ignored it, so its rows froze as Running, the notification vanished and the only
Cancel went with it. The poll now reconciles every source's in-flight rows (the
engine reports status without a source) and the notification reads all of them,
which is also what cancelActive already acted on.

The grouped tab queries drop entities with nothing downloaded, since the UI
treats a missing entry and an all-zero one the same.

The indicator's two-value enum was used as a boolean, so it is one; and the
library screenshot now asserts the ring is drawn, rather than capturing an
image that would stay green if it disappeared.
2026-10-04 00:30:03 +00:00
austinried 47064e82f0 leave a blank line under the name in the delete prompt 2026-10-04 00:30:03 +00:00
austinried 6c09e7d786 put the name and the freed space on their own lines in the delete prompt 2026-10-04 00:30:03 +00:00
austinried 9bb2f7d8fc say space rather than sizes in the downloads settings row 2026-10-04 00:30:03 +00:00
austinried ce035f457b post our own download progress notification
The platform posts one notification per download, so queueing an album buries
the UI in them and they can be swiped away. Hiding them (VISIBILITY_HIDDEN,
which needs DOWNLOAD_WITHOUT_NOTIFICATION) and posting one aggregated
notification instead gives a single ongoing entry with the overall progress and
a Cancel action that stops everything in flight.

It is refreshed from the download state the poller already maintains, so it
tracks the engine without extra queries, and it is tied to this process: if we
are killed the downloads continue with no notification, which is the trade for
not writing a service.
2026-10-04 00:30:03 +00:00
austinried 20814a10f8 scrim the progress ring on album art only
The ring reads on the theme background of the artist and playlist rows but got
muddled over cover art, so the album tiles and the artist page's album cells put
it back on a translucent surface circle; the rows stay bare.
2026-10-04 00:30:03 +00:00
austinried b324cbb6b9 show only an in-flight ring on the library surfaces
A finished download no longer draws anything on album covers, artist rows or
playlist rows: the badge is the progress ring alone, so a fully downloaded tile
looks like any other. The ring takes the theme's primary for the arc over a
theme track, with the one soft shadow, instead of the white glyph and its
outline.
2026-10-04 00:30:03 +00:00
austinried dac56a7fcb group the downloads tree by the album's artist
The tree read the track artist off each song, so a downloaded album split into
one group per guest performer and every song without a track artist landed in
"Unknown artist". It reads the album's artist now, which is also the artist the
node's delete resolves through, so the two agree.

The badges are white glyphs with a dark outline and a progress ring with a
visible track, after a look at the rendered result; larger and clearer than the
first pass, which the art swallowed.
2026-10-04 00:30:03 +00:00
austinried 837ab9ac5d trim the play entries, calm the badge, and only watch the visible tab
The album and playlist menus lose Play and Shuffle and the song menu loses Play;
tapping the row or the detail hero buttons already covers those, so the
ItemActions play/shuffle callbacks and the artist view-model methods behind them
go too.

The badge was a filled circle that stood out over the art. It is now a white
glyph with a soft shadow in the icon's own shape, with no background behind it.

The library screen held all three grouped status flows at once, so the two tabs
off screen ran queries for badges nobody could see. It now collects only the
visible tab's map.
2026-10-04 00:30:03 +00:00
austinried b4f69affc9 put the mixed menu back to two rows, and only badge real progress
The split bar read badly; a partly downloaded list goes back to stacked
Download and Delete items.

The badge showed progress for any list with a downloaded song in it, which
made a mostly-downloaded album look like it was fetching. It now shows only
when that list is complete or has a download actually in flight; a partly
downloaded idle list shows nothing.

The album and playlist detail screens' more-menus had no download entries at
all, leaving delete reachable only via the hero's long press. They now take
the same status-driven items as everywhere else.
2026-10-04 00:30:03 +00:00
austinried 0f3dc45081 split a partly downloaded list's menu into one row
The mixed state offered two stacked rows; the spec wants one row divided
between Download and Delete. Everything else is a single full-width row.
2026-10-04 00:30:03 +00:00
austinried fedc502c3c add the downloads management screen
A Downloads screen reachable from Settings shows the active source's downloads
as an artist > album > song tree with per-node sizes and song counts, each node
collapsible. Artist and album nodes delete with a confirmation showing what they
free; song rows delete straight away, and a summary row offers Delete all.

Sizes come from the files on disk rather than the engine's reported total, so
they agree with the byte count the delete dialog already quotes. The tree is
built from one query joining the download rows to song, album and artist names.
2026-10-04 00:30:03 +00:00
austinried 7a0dd9aa5b show download status on the library tabs and offer it in their menus
Album tiles, artist rows and playlist rows now carry the badge (lower right on
tiles, trailing end on rows) and their long-press menus take the same bulk
Download / Cancel / Delete entries the hero uses, driven by the shared actions
rule so a partly downloaded item offers both rows and an in-flight one offers
Cancel. The artist page's album cells get the badge and album menu too.

The three list callbacks the artist menu used (and its artist-specific
view-model calls) collapse into one bulkDownload action over a MenuTarget, since
the same menu code now serves album, playlist and artist.
2026-10-04 00:30:03 +00:00
austinried edbcbeb380 group download status per entity for the library tabs
The detail heroes ask for one entity's status at a time. The list surfaces
need every status on screen at once, so add one grouped flow per tab that
returns a lookup of entity id to counts, which the tiles and rows read the way
song rows read the per-song map.
2026-10-04 00:30:03 +00:00
austinried d89b9e3a47 drop the concurrency dialog test 2026-10-04 00:30:03 +00:00
austinried 6f25f426ba fix the overlapping options in the concurrency dialog
ChoiceGroup emitted its header and option rows as siblings straight into the AlertDialog text slot, and they drew on top of each other, leaving only one radio visible with garbled labels. Wrap the group in its own Column so the rows stack, and cover it with a test that asserts the presets occupy separate rows.
2026-10-04 00:30:03 +00:00
austinried cf6ee00b6f move the artist header row down and the albums up
Drop the row's bottom padding and add to its top so the count and buttons sit closer to the album grid.
2026-10-04 00:30:03 +00:00
austinried 553752bf0a bump the artist album count text size
Use bodyLarge for the album count so it reads a little larger under the artist name.
2026-10-04 00:30:03 +00:00
austinried ea952e5a73 even out the artist album grid gutters
The columns were 16dp from the screen edges but 12dp apart, so the middle looked tighter than the sides. Use the same gap horizontally as vertically.
2026-10-04 00:30:03 +00:00
austinried 9f061e2a31 drop the artist biography placeholder and centre the header row
The 'Artist biography coming soon.' line is temporary and the bio work is shelved, so remove it. With only the album count left, centre it against the star and more buttons under the image (the buttons no longer need to sit below the hanging shuffle).
2026-10-04 00:30:03 +00:00
austinried 9ce7538e54 drop the cover thumbnail request to 384px to cover the common densities 2026-10-04 00:30:03 +00:00
austinried 74a91b2de0 request 512px cover thumbnails so the album grid stays sharp
main's 512px bump was reverted by bd748f5, which dropped it as a stray edit during the downloads rebase. Restore it.
2026-10-04 00:30:03 +00:00
austinried a88618421d drop two changes that are not part of this work
`git add -A` while rebasing and committing swept up another agent's edits from
this checkout: a THUMBNAIL_SIZE bump in SubsonicSource and a refreshArtistInfo
call appended to the artwork pass in DownloadRepository. Both are out of scope
here and are reverted to main's version, so they can land through their author's
own change or not at all.
2026-10-04 00:30:03 +00:00
austinried a6659f9f62 promote only the active source, and let a partial artist delete
Third review round:

- `promote` read every source's rows, so a single inactive source with waiting
  rows could fill the window and starve the source in use: its songs stayed
  queued with no engine id, for good, through every tick. It now scopes to the
  active source, which is also the only one that can resolve a URL here.
- An artist with some songs downloaded offered only Download, because the more
  menu rendered `action()` rather than the actions the state offers. It renders
  the whole list now, so a partial artist gets Download and Delete.
- The hero's long press is only advertised when there is something to delete,
  instead of announcing a Delete it would ignore, and it declares a button role
  again after losing `IconButton`.
- The delete dialog uses the platform size formatter rather than a hand-rolled
  one, which also fixes 999_999 reading as "1000.0 kB".
- The poll warms artwork through the same helper reconcile uses.
2026-10-04 00:30:03 +00:00
austinried a57ca92250 cancel a list in one go, and round the hero's touch feedback
- Cancelling a big playlist did per-row work: a binder cancel, two queries and a
  delete each, thousands of times, under the lock. The platform takes a list of
  ids in one call, so the cancel is now one binder call, one delete per chunk of
  rows and one sweep, which is both the several seconds and the stale button.
- Partial files are covered by the same sweep, which already deletes any file in
  a source directory that no row names; a test now proves a stray .part goes with
  the cancel.
- The hero's control was a plain clickable Box after the long-press work, so its
  splash was square; it clips to a circle the way an IconButton does.
2026-10-04 00:30:03 +00:00
austinried 3b6fb1341b long press the hero to delete a partially downloaded list
The hero's control now carries the whole rule: a tap does what the state offers
(Download, Cancel, Delete) and a long press asks to delete when there is anything
downloaded to delete, so a partially downloaded album can be emptied without
starting the rest first. Nothing downloaded means the long press does nothing.

The rule itself returns the actions rather than one of them, so a partially
downloaded list offers both Download and Delete; the tap takes the first. That
list is what the library tabs' context menus will read.
2026-10-04 00:30:03 +00:00
austinried 37ccfd0b17 confirm the artist delete too
Only a single song deletes without asking; the artist's delete now opens the same
dialog as the album and playlist, naming the artist and the space it frees. The
artist's download action stays in its more-menu rather than getting a visible
control, deliberately, so bulk downloading a whole artist is a considered
action.
2026-10-04 00:30:03 +00:00
austinried 475793059b keep main's setStar when resolving the rebase
The conflict resolution took my side of ItemActions.setStar, which still
suspended and returned Result; main's rework made it a plain action the menu
calls without a coroutine, with the star filled optimistically. Compiles again,
and ItemContextMenuStarTest passes.
2026-10-04 00:30:03 +00:00
austinried 4652430f73 bound the downloads handed to the platform, and confirm bulk deletes
Two things a 3k-song playlist turned up, plus the hero's downloaded state.

- The platform runs about eight downloads at a time, but a bulk action handed it
  every song at once. Three thousand pending requests meant a notification flood,
  and a poll tick that rewrote every row's bytes/total, so the whole list churned
  ten times a second. Now a bulk action only records the songs and hands over at
  most IN_FLIGHT_LIMIT, oldest first, promoting the next as each finishes, so our
  row state mirrors what is actually running and a cancel only has to reach what
  is in flight.
- Cancelling was therefore unusable at that size. It now touches at most the
  in-flight requests instead of thousands, and the rows wait behind it.
- A queued row with no engine id is waiting its turn, not lost, so reconcile no
  longer re-enqueues the whole list on every pass; a row whose engine entry
  vanished goes back to waiting and the next promote picks it up. A song that
  cannot be handed over fails instead of being retried every tick.
- Requests follow the order the screen shows: artist songs resolve through the
  same albums ordering the artist page uses, so they were the one list that did
  not.
- The hero's fully-downloaded state shows the downloaded glyph rather than a bin,
  and tapping it asks before deleting, naming the album or playlist and the space
  it frees.
2026-10-04 00:30:02 +00:00
austinried 17c1ea40a4 test the bulk cancel race, and keep cancellation working
Second review round on the bulk downloads:

- The cancel-during-bulk fix had no test, so the invariant it exists for could
  regress silently. The fake engine can now delay an enqueue, which holds the
  race window open: the queue loop is kept busy, a cancel is fired mid-queue,
  and the test asserts the whole list ends up cancelled. Reverting to per-song
  locking fails it, which I checked.
- The per-song catch around the queue loop swallowed CancellationException, so
  cancelling the caller did not stop the loop. It rethrows now.
- The artwork pass skips songs whose row a cancel has already taken.
- The lock-hold ceiling is recorded where the lock is taken: cancel latency and
  the poll tick both scale with the list length, and a per-list cancel
  generation would lift that if it ever bites.
- Tests: an empty list says so, one song failing to enqueue leaves the rest of
  the list alone, the applyAction test covers Delete as well as Download and
  Cancel, and the two playlist fixtures share a helper.
2026-10-04 00:30:02 +00:00
austinried bf561075a2 address the bulk download review
- Cancelling a list while its download loop was still enqueueing missed every
  song queued after the cancel looked. The loop now holds the lock for the whole
  queue, so a cancel waits for it to finish and then removes all of it. That
  needed the enqueue helper split into a locked entry point and a body the loop
  can call while already holding the lock — calling the locked one from inside
  the lock deadlocked, which is what hung the tests.
- An empty resolved list now says so instead of doing nothing.
- One song failing to enqueue no longer aborts the rest of the batch.
- The artist actions resolve through the albums the artist page lists, matching
  what is on screen, rather than through songs.artistId, which for a guest on a
  compilation enqueued nothing.
- Bulk actions are one `applyAction` on the repository, so the three view models
  and both screens stop repeating the same dispatch.
- Tests: the playlist and artist resolve paths, a Running row counted as
  downloading, the action dispatch, and the action rule moved to a plain unit
  test.
2026-10-04 00:30:02 +00:00
austinried b2b92a388e download a whole album, playlist or artist at once
Third block of #65: bulk downloads and the list-level actions.

- The hero's download button on albums and playlists is real now. It starts the
  songs the list is missing, cancels what is in flight, or deletes what is
  already downloaded, chosen from the list's own status. The artist has no hero
  button, so its bulk actions live in the context menu, which now carries the
  same status.
- No new storage: a bulk job is the download rows for the list, and the status is
  counted from song_downloads joined to the list's songs, so cancel and delete
  stay well defined without anything to keep in sync.
- downloadAll fetches artwork for the whole list under one shared budget, once
  per unique cover, and skips songs that are already downloaded so re-tapping
  Download resumes rather than restarting.
2026-10-04 00:30:02 +00:00
austinried 9266fc5edc request 512px cover thumbnails so the album grid stays sharp 2026-10-04 00:30:02 +00:00
austinried f8aa391717 faster star dismiss delay 2026-10-04 00:30:02 +00:00
austinried b908d90273 right align the track times within their column 2026-10-04 00:30:02 +00:00
austinried 0d39a9de72 show track times without padded minutes
Minutes read as the plain value ("4:11", "0:05") and only hours keep the
formatted MM:SS tail, so nothing is zero-padded except the seconds.
2026-10-04 00:30:02 +00:00
austinried 51ed8fdd6b address review: pre-P shadow fallback and safer notification star
- draw the play halo as an offset copy below API 28, where path
  shadow layers are ignored, and skip the inner shadow there too
- add a repository toggleStar that reads and writes under the star lock,
  so fast double taps on the notification cannot desync
- reset the star button when a track starts, so a failed source lookup
  cannot leave the previous song's state showing
2026-10-04 00:30:02 +00:00
austinried 08eace9e90 dim the playing art instead of desaturating it 2026-10-04 00:30:02 +00:00
austinried aa954ffc3e desaturate the playing row's art behind the primary indicator
Back to the primary border and glyph, and drop the playing cover's
saturation so the accent reads against its own artwork.
2026-10-04 00:30:02 +00:00
austinried 3eef4e79b6 try a white play glyph over art 2026-10-04 00:30:02 +00:00
austinried e222a76500 back to a primary play glyph with a white art border 2026-10-04 00:30:02 +00:00
austinried 077a2ced77 tint the playing indicator with the artwork's secondary 2026-10-04 00:30:02 +00:00