In-progress rows on Manage downloads now render a determinate progress bar
and a cancel button; completed and failed rows keep the delete button.
Cancelling removes only the active rows, so any completed downloads stay
and can still be deleted.
RevealNewFrontItem compared the post-anchor firstVisibleItemIndex against the
front threshold, so it only worked when the row was at index 0 and exactly one
item was prepended. Recover the pre-change position by subtracting the number
of items now before the old front, and cover the one-in and multi-prepend cases
with integration tests.
Adopt the canonical TagLib/mutagen names for two extended indices: 133 is
Worldbeat (was Negerpunk) and 148 is Abstract (was Christmas). Drop the unused
default on SongInfoDialog's genres argument.
Split genre tags on Navidrome's separators (semicolon, slash, comma) and
resolve ID3v1 numeric indices to their names, so a song's genres are one
deduped list. songs.genre is now derived data with no reader: drop the
column and its index, and read the song info dialog's genres from
song_genres.
Scroll a recently played row back to the front when a new first item
appears while it is already at/near the start; the lazy layout otherwise
anchors the old first item and hides the new one. Balance the three
genre rows by estimated width and tighten their gap.
The album/artist/playlist paging SQL named song_downloads, so Room
invalidated the pages on every download progress tick, janking the list
and resetting scroll when a download started. Route the default (filter
off) path through a raw query that only observes its own table; the
downloaded/offline path keeps the download-aware queries.
Port the Flutter app's network security config: add a base-config that trusts system and user certificate anchors and permits cleartext, and reference it from the application manifest. android:usesCleartextTraffic is now redundant and removed.
Use cancelAndJoinBlocking in the repo tests and AppModuleTest instead of hand-rolled runBlocking joins, put AppModuleTest's cleanup in a finally so a failed join cannot leave Main overridden and Koin started, and collapse the five polling await copies into one awaitUntil. upcomingItem now reads the queue snapshot under the lock but does the Room read outside it.
The repositories and the playback controller each created a hidden SupervisorJob scope and exposed close() only so tests could join it before closing the DB; production never called close(). Inject the scope instead and have each test cancel and join the scope it passes, so no test-only teardown lives in the app. Koin now provides the app/io/playback scopes.
Also read upcomingItem under the controller lock, and give the wall-clock test waits one shared 30s budget instead of 5s per file: a loaded runner could starve the controller's single thread past the old deadline.
OpenSubsonic servers return a per-song <genres> array that was dropped,
leaving every song with only its first (or joined) legacy genre. Parse it
and store the ordered list in a song_genres table, keeping the single
genre column as the fallback for base Subsonic servers.
Album genres are derived by unioning the album's song genres, falling
back to the single song genre, so album-level genre never trusts the
per-server album genres[] behaviour.
Schema 21 -> 22, with parser, sync, unit and integration coverage.
A submitted scrobble now counts locally regardless of the HTTP result
(offline already did), so a transient server error no longer drops the
play from the sorts. Last-played uses MAX(existing, at) so a backwards
clock step cannot move an album or artist back down the list.
It compared a possibly stale listQuery (the previous tab's value during a
tab switch) against the new tab's options and rewrote the new tab's query
to its default, so sorts appeared to reset. Not worth the fragility for a
hidden-option edge case.