Commit Graph
100 Commits
Author SHA1 Message Date
austinried d76f236f59 downloads: recover reset observation and skip progress work 2026-11-14 12:05:29 +00:00
austinried 50c4dfce03 sync: batch playlist origin lookups 2026-11-14 12:05:29 +00:00
austinried 746446be86 queue: make reset remapping origin-safe and bounded 2026-11-14 12:05:29 +00:00
austinried df8cf73385 ui: preserve pending older queue loads 2026-11-14 12:05:29 +00:00
austinried 97e165e8c2 ui: preserve queue windows during reconciliation 2026-11-14 12:05:29 +00:00
austinried eb4d537437 test: drop reverse queue debug output 2026-11-14 12:05:29 +00:00
austinried 31e4596860 ui: reconcile queue edits against current rows 2026-11-14 12:05:29 +00:00
austinried b95b8c3569 ui: validate queue moves against content revisions 2026-11-14 12:05:29 +00:00
austinried e4549f5fa1 test: combine queue removal coverage 2026-11-14 12:05:29 +00:00
austinried e2c3f31c26 ui: serialize queue moves with row updates 2026-11-14 12:05:29 +00:00
austinried 2766affa65 ui: roll back rejected multi-step queue edits 2026-11-14 12:05:29 +00:00
austinried 4d5328d8a0 playback: report whether queue edits were applied 2026-11-14 12:05:29 +00:00
austinried 97e700a1ab test: cover rejected queue edit snackbars 2026-11-14 12:05:29 +00:00
austinried 5fadc21455 ui: roll back rejected queue edits 2026-11-14 12:05:29 +00:00
austinried 64ec3265b7 ui: reject stale and mixed-generation queue edits 2026-11-14 12:05:29 +00:00
austinried 9514bc501a downloads: reconcile bulk deletion as one batch 2026-11-14 12:05:29 +00:00
austinried 35f4f9178b downloads: adapt reset tests to prepared publication 2026-11-14 12:05:29 +00:00
austinried e4d97f7aba downloads: harden queue reset reconciliation
Refresh per-source rows under the reconciliation mutex before reset decisions and chunk queue-visibility checks to respect SQLite bind limits.

Cover a blocked pre-completion emission and source deletion at the bundled SQLite variable limit.
2026-11-14 12:05:29 +00:00
austinried cc94dfc458 downloads: reconcile visible queue reset baselines
Consume captured reset points on the first collector emission and leave pending sources uninitialized during reconciliation.

Build ordered baselines from queue-visible completed downloads and ignore pruned-row transitions while retaining all-row state and recovery.
2026-11-14 12:05:28 +00:00
austinried 79915d6c7a library: skip queue resets for missing songs 2026-11-14 12:05:28 +00:00
austinried 4e72b1f38d downloads: preserve completed reset baselines 2026-11-14 12:05:28 +00:00
austinried ed19ca2ece library: reconcile queues after starred and download changes 2026-11-14 12:05:28 +00:00
austinried 481f12c892 sync: preserve legacy identity during queue resets 2026-11-14 12:05:28 +00:00
austinried 544b442f4e sync: make queue reset publication recoverable 2026-11-14 12:05:28 +00:00
austinried 705c93053f sync: publish queue resets after commit
Prepare queue and playback reconciliation inside the reset transaction without publishing listener, cache, or revision state. Publish through a non-suspending, non-cancellable post-commit step so committed resets are never retried over later edits.

Keep the pending-origin clear under the cursor lock without a redundant single-statement transaction, and cover listener and post-clear write failures.
2026-11-14 12:05:28 +00:00
austinried ffc61bdbd2 sync: use playlist deletion count 2026-11-14 12:05:28 +00:00
austinried 3de80dacdd sync: reuse playlist song deletion helper 2026-11-14 12:05:28 +00:00
austinried 95c8a2a915 sync: preserve queue lock order during resets
Move pending-origin clearing into the queue mutation lock, acquire the cursor lock before Room, and clear pending rows before cache, revision, or listener side effects. Keep sync listener reconciliation transactional for retries while preserving direct reset commit visibility.
2026-11-14 12:05:28 +00:00
austinried 92e5bb45c9 sync: simplify library deletion 2026-11-14 12:05:28 +00:00
austinried 2afad4fa0d sync: verify updates recreate pending origins 2026-11-14 12:05:28 +00:00
austinried 4e51f906fe sync: consume pending origins with queue resets 2026-11-14 12:05:28 +00:00
austinried 548bf39136 sync: reset queues only for successfully changed origins 2026-11-14 12:05:28 +00:00
austinried acc820e263 playback: reconcile detached items during offline transitions 2026-11-14 12:05:28 +00:00
austinried 4b5d02f17b playback: keep detached fallback out of published position 2026-11-14 12:05:28 +00:00
austinried fd0eb0a9a0 playback: retain durable reset identity through Media3 2026-11-14 12:05:28 +00:00
austinried 01ccc6475d playback: resolve song tap behavior inside queue mutation 2026-11-14 12:05:28 +00:00
austinried a9738c6311 playback: preserve detached fallback queue state 2026-11-14 12:05:28 +00:00
austinried 767c3db1ae playback: serialize transitions and preserve detached playback
Run transition bookkeeping under the queue mutation lock, retain detached playback when its final survivor is removed, and restore offline repeat wrapping without a forward survivor. Centralize detached survivor rebuilding and add regressions for each defect.
2026-11-14 12:05:28 +00:00
austinried 018f30a3d8 playback: preserve playback across canonical queue resets 2026-11-14 12:05:28 +00:00
austinried 762639cab5 queue: invalidate keyset cursors on edits 2026-11-14 12:03:42 +00:00
austinried 94c7fba536 queue: preserve ambiguous legacy reset identity 2026-11-14 12:03:42 +00:00
austinried ddc6811c34 queue: suppress shuffled context removal 2026-11-14 12:03:42 +00:00
austinried 04dba5ef76 queue: leave ambiguous migrated fragments unreset 2026-11-14 12:03:42 +00:00
austinried 0448842f60 queue: retain reset identity across duplicate edits 2026-11-14 12:03:42 +00:00
austinried b9085af44c queue: preserve fragmented origin entry identity 2026-11-14 12:03:42 +00:00
austinried 44e2eb7ae8 queue: add serialized, origin-aware mutations 2026-11-14 12:03:42 +00:00
austinried f43bc877f0 Revert "brand: lift the icon background off black"
This reverts commit d9af47323a.
2026-11-14 09:22:54 +00:00
austinried d9af47323a brand: lift the icon background off black
The near-black #120C12 behind the icon reads as black on dark home
screens, and the same fill backs the store icon, feature graphic and
README logo. Raise it to a plum #2D1C2D (L* 4.6 -> 13.1) so the
background is visible without costing the #F862F8 foreground its
contrast. The amber nightly background moves from #1A1200 to #2E2000 by
the same lightness step, so it stays amber instead of drifting to
brown-black.

Raster assets were recoloured from the committed originals; the vector
sources in images/ have no background and are unchanged.
2026-11-12 13:07:46 +00:00
austinried 02404ef562 ui: rename the song tap preference 2026-11-12 13:03:41 +00:00
austinried d05fb6fc07 playback: resolve the pending tap preference inside the start lock 2026-11-12 07:53:06 +00:00
austinried 2bbff7e477 playback: take the start lock before reading the tap preference 2026-11-12 07:39:14 +00:00
austinried e94b365495 playback: make the song tap shuffle behaviour a preference 2026-11-12 07:05:20 +00:00
austinried d62bff5ace playback: simplify the context start paths 2026-11-12 05:02:49 +00:00
austinried 29335f7ddf playback: turn off shuffle when a song is tapped 2026-11-12 04:31:58 +00:00
austinried 11276c2e1f playback: revert the restored session prepare
Preparing the restored window on launch was meant to make the paused
session reachable from Bluetooth and Automate. The follow-up on the issue
shows the notification appears, but the car's play command still does not
reach the app until the phone is unlocked, and the speculative prepare
buffered up to 50 seconds of the restored track on every launch while
claiming the media controls whenever the app opened.

Removing the prepare also removes the error gate it needed, which could
hide genuine playback errors while paused.

Reverts #230.
2026-11-05 13:13:22 +00:00
austinried eb55858cc4 playback: preserve null keys and ranged duplicate ranks 2026-11-04 13:15:54 +00:00
austinried 95ad993996 playback: resolve shuffled windows without flat ID lists 2026-11-04 13:06:45 +00:00
austinried 11abd4afbd ui: share bulk download deletion confirmation 2026-11-04 12:16:06 +00:00
austinried 69f41b3459 playback: collapse the restore guard into one null chain 2026-11-04 11:12:07 +00:00
austinried 09baaeb77d playback: keep speculative prepare failures quiet
A restore now prepares the saved window before any play gesture, so a
network failure at launch reached the error listener and showed a toast
even though the user had not asked to play. Only report playback errors
when playback was requested.

The new tests now wait for the prepare operation rather than the window
contents, which can be observed a poll before the prepare lands.
2026-11-04 11:12:07 +00:00
austinried 484cc55385 playback: prepare the restored queue so the session starts on launch
Restoring the saved queue left the player idle, so the media session had no
notification and Bluetooth or Automate saw no active player until playback
began. Prepare the restored window without autoplaying, which surfaces the
paused session when the app opens. A queue from another source stays stopped,
and an offline queue without downloads has no window to prepare.

Closes austinried/subtracks#202
2026-11-04 11:12:07 +00:00
austinried 599924bdd0 test: share Room database and repository fixtures 2026-11-04 11:08:31 +00:00
austinried d6ff3ee086 build: fetch Robolectric's Android SDK jar from the mirror
Robolectric resolves android-all-instrumented itself at test time with
MavenArtifactFetcher, whose default repository is repo1.maven.org. The
Gradle mirror in settings.gradle.kts never applied to that fetch, so the
CI egress IP still got HTTP 429 there.

That fetcher also stores the jar in ~/.m2/repository, outside the
~/.gradle/caches directory CI restores, so every ephemeral runner
downloaded it again. Point robolectric.dependency.repo.url at the same
mirror for every Test task and cache the Robolectric Maven repository in
both workflows, keyed by the Robolectric version and the configured SDK.
2026-11-04 10:21:49 +00:00
austinried b2a1716be2 library: generate artist credit queries from one definition 2026-11-04 10:11:17 +00:00
austinried 51792f836b test: wait for the repository's active source in artist art tests
awaitActiveSourceId() watched the DAO's activeSourceId flow, which turns
non-null as soon as the source row is written. SourceRepository sets its
in-memory active source in a separate collector, so artistImage() could
still see null and the online URL assertion failed. Wait on the
repository's own activeSource readiness signal instead.
2026-11-03 09:08:54 +00:00
austinried 446a35d26e sync: declare upsert columns and values together 2026-11-03 01:21:28 +00:00
austinried b18e0891f6 db: tighten the paging observer and filter comments 2026-11-03 01:03:45 +00:00
austinried c53f1aef06 db: correct the paging filter comment
The downloaded statements are no longer the old hand-written text, and
RawQuery invalidation comes from observedEntities, not the filter text.
2026-11-03 01:03:45 +00:00
austinried 87e0609c05 db: drop the constant downloaded filter flag
The downloaded statements only run with the filter on, so the
(? = 0 OR EXISTS (...)) guards and the CASE around the downloaded counts
were always true. Keep those statements direct and remove the unused bind.
2026-11-03 01:03:45 +00:00
austinried c3a5382c27 db: bind named parameters directly and cover the repository dispatch
SQLite gives a repeated named parameter one index, so rawQuery can pass the
SQL through unchanged and bind the distinct names in first-appearance order
instead of rewriting them to positional placeholders.

Use named arguments for the repository dispatch, since the adjacent
descending/downloaded booleans were easy to transpose, and add a repository
test that collects the downloaded album grid. Fix the query-timing labels so
the unqualified rows measure the same default path as before.
2026-11-03 01:03:45 +00:00
austinried e8cbb93714 db: drop order constants the paging builders replaced
The deleted order methods and the old plan tests were the last users of the
plain name/artist order constants and the reverse sort-mode variants.
2026-11-03 01:03:45 +00:00
austinried 6f1e05d15a db: carry the sort-name mode through the paging builders
Rebase follow-up for the new SortNameMode preference: pass it into the
shared album/artist order functions so downloaded pages honour it too,
cover the new _plain indexes in the downloaded artist plan test, and add a
downloaded-mode ordering test.
2026-11-03 01:03:45 +00:00
austinried c165a6cc88 db: pin the downloaded artist count access path in the plan test
Assert the sourceId index as well as the in-memory sort for the downloaded
Artist album count order, and fold the top-level plan helper into plan().
2026-11-03 01:03:45 +00:00
austinried 18c24ca566 db: cover downloaded paging invalidation and filters
Add tests for the downloaded artist and playlist paging sources now that
their observed entities are hand-picked, and assert the downloaded
search/starred filters directly. The downloaded artist plan test uses the
real select list and documents the computed album count's in-memory sort,
which the hand-written query also had.
2026-11-03 01:03:45 +00:00
austinried 8d6f7e2341 db: collapse paged library queries into one builder per entity
Replace the thirty hand-written order variants with one query builder per
entity plus a default and a downloaded @RawQuery entry point. The
repository's sort dispatch trees and the duplicated positional filters in
LibraryPaging are gone, and the SQL now lives in multi-line strings with
named parameters.

The generated statements are identical to the previous ones modulo
whitespace: the downloaded variants keep the same CASE/download conditions,
and the observed entities match what Room inferred for the old queries. A
small helper rewrites the named parameters to RoomRawQuery's positional
form, including the repeated :sourceId bindings.
2026-11-03 01:03:45 +00:00
austinried 29935b7bee build: mirror Maven Central for dependency resolution
The CI runner's egress IP is rate-limited by repo.maven.apache.org (HTTP 429 on a Robolectric native runtime jar), and a missing Gradle cache then breaks the run. Try Google's Maven Central mirror first and fall back to the canonical repository.
2026-11-01 05:45:25 +00:00
austinried 4bfe67ec19 build: add the nightly res through the directories set
AGP 9 deprecates AndroidSourceDirectorySet.srcDir() in favour of the directories mutable set, which was the last source-set deprecation warning in the build.
2026-11-01 05:35:04 +00:00
austinried 060a5f036f ci: key the test music cache by the demo album ids
The album id is the server id, so use it for the download, the local folder and the prune fixture instead of maintaining a separate remoteId mapping. The cache key moves to v3 because v2 may already hold folders named after the old local ids.
2026-11-01 03:19:17 +00:00
austinried c169549ef4 ci: take integration test music from the Navidrome demo
demo.subsonic.org no longer accepts the guest1 account, so the harness downloaded an XML error page and unzip failed before Gradle ran. Download the same three albums from demo.navidrome.org with the demo account, fetch each album's cover art into folder.jpg, and match the album title the files actually carry (Retroconnaissance, without the EP suffix). Bump the music cache key so stale copies from the old source are not reused.
2026-11-01 03:12:01 +00:00
austinried 3bd29f75e6 release: cover the playlist info dialog 2026-10-24 02:16:45 +00:00
austinried d7a3a41ab2 metadata: refresh the artist screenshot with the new biography
Regenerate the store screenshots from the demo library. Only the artist
page changed meaningfully, so keep that capture, using Nine Inch Nails,
which has a biography and three albums.

The demo test now fetches the artist biography and passes artist art to
the home, library and search screens, so a regeneration keeps their art
instead of falling back to letter placeholders.

The captures now come from a 393x786 dp, 440 dpi phone: Pixel XL's
640x360 dp renders text far larger than a real phone shows it. That
phone renders 1080x2161, one pixel over the 2:1 limit both stores
enforce, so captures are written out at 1080x2160.
2026-10-24 02:16:45 +00:00
austinried 05e318f9ab docs: add ReplayGain to the README features 2026-10-24 02:16:45 +00:00
austinried e8fb0ac564 release: split the GitHub notes from the store changelog
The GitHub release body was the 500-char store changelog. Give the
GitHub release its own detailed notes under docs/releases/<tag>.md and
point release.yml at it, leaving the store changelog to be the trimmed
version plus a link back to the GitHub release.

The notes cover everything since 3.0.0, including ReplayGain, the artist
page work, the sort name preference and the fixes.

The store changelog still must exist; the workflow now checks both
files.
2026-10-24 02:16:45 +00:00
austinried 7379f3d93e release: bump to 3.1.0 and add its changelog
Bump versionCode to 14 and versionName to 3.1.0, and add the
changelogs/14.txt release notes covering ReplayGain, artist pages,
sort names, keep-screen-on and the fixes.

No tag: this only prepares the release.
2026-10-24 02:16:45 +00:00
austinried f9151843a6 Revert "ui: show the singular and plural artist rows separately"
This reverts commit 2f95a89682.
2026-10-21 14:24:48 +00:00
austinried 2f95a89682 ui: show the singular and plural artist rows separately
Track info now lists the Artist field and the plural Artists field as
their own rows, hiding Artists when it would repeat the Artist value.
Keep one Artist sort row, also hidden when it matches the artist name.
2026-10-21 14:24:18 +00:00
austinried 319ff4b645 test: pin independent artist and artist sort rows
The info rows do not depend on each other: a track with artists but no
artist sort keeps the artists row, and a track with an artist sort but
no artist name still shows the sort row. Pin both so a later label or
value change cannot start requiring both.
2026-10-21 14:15:40 +00:00
austinried d8ac7fefc7 db: keep playlist owner and sharing in filtered lists
The hand-written playlist projection used by the Downloaded filter and
offline mode did not select the new owner and shared columns, so the
playlist Info dialog lost them there while unfiltered lists had them.
Select both columns and pin the filtered path in LibraryDaoTest.

Also share the artist-name extraction between the subtitle and the
plural label so duplicate or blank refs cannot make them disagree,
accept the XML boolean form "1" for the public flag, and strengthen the
date-adjacency and pluralisation tests.
2026-10-21 14:04:46 +00:00
austinried f46f43b6cd ui: finish the library info dialogs and add playlist info
Sort names and titles are only shown when they differ from the regular
name, so artists without a distinct sort name no longer repeat the name.

Track info uses plural labels for both the artists and their sort name
when the track has more than one artist, and singular labels otherwise.
Added and last played sit together in album info.

Playlists gain an Info dialog with owner, sharing, song count, length,
creation and update dates. Owner and the public flag are synced from
getPlaylists, so a schema migration adds both columns.
2026-10-21 13:54:37 +00:00
austinried 8052e1db76 ui: sit the filter dot on the top bar's centre line
Place the dot's ring so its centre lands on the top bar (8dp into the
28dp glyph box), with the dot's outer edge kept at the bar's end.
2026-10-21 10:14:10 +00:00
austinried edc8782302 ui: round out the floating indicators
Size the offline button to 48dp and lift the cloud-off glyph by its
own off-centre so it sits in the middle of the circle. Give the active
filter dot the old FAB treatment: a 7dp dot with a ring in the button
colour, moved onto the top bar of the filter glyph.
2026-10-21 10:14:10 +00:00
austinried 3ffe38d988 ui: float the offline indicator opposite the library FAB
Drop the strip above the tab bar, which kept the bottom of the library
busy, and go back to a floating tonal cloud-off button at the bottom
start, level with the FAB on the other side. Tapping it still leaves
offline mode, and the tab bar goes back to just tabs and the gear.
2026-10-21 10:14:10 +00:00
austinried b76328fc26 ui: hold the offline strip to the 48dp touch target
Keep the strip's own tint and the clickable row, but give it the
standard minimum interactive height now that the Surface is no longer
padding it invisibly.
2026-10-21 10:14:10 +00:00
austinried 9feaf54cd2 ui: stop the offline strip reserving a 48dp touch target
The clickable Surface enforced Material's minimum interactive size, so
trimming its row only moved the text inside a band that stayed 48dp
tall and kept the gap to the tabs. Make the strip itself clickable
instead, so it sizes from its padding, and pin it at 36dp.
2026-10-21 10:14:10 +00:00
austinried 72e1b61bff ui: tint the offline strip and make it slimmer
Drop the divider and move the strip onto a lightly primary-tinted
version of the bar colour, so it reads as its own band without a rule.
Trim the vertical padding, leaving the strip at 28dp.
2026-10-21 10:14:10 +00:00
austinried bb3abe39b6 ui: tighten the offline strip and separate it from the tabs
Drop the strip's fixed 44dp height so it wraps its content, and draw a
divider between the strip and the tab bar. The label now sits 6dp above
the rule instead of 12dp above the tab icons, which read as one gap.
2026-10-21 10:14:10 +00:00
austinried be08e50314 ui: keep a gap before the first library tab
Put an 8dp spacer at the start of the scrollable content and let the
reveal leave that gap when scrolling back towards the first tab, so the
selected pill no longer sits flush against the screen edge. The spacer
scrolls away, so a tab that is half off is still cut at the edge.
2026-10-21 10:14:10 +00:00
austinried b9a3ae5ed0 ui: tighten the library tab bar and simplify the offline strip
Drop the strip's divider and shorten its label to "Offline mode". Take
the bar's horizontal inset off the scroll container so a tab that is
scrolled half off is cut at the screen edge instead of stopping inside
the bar's gutter; the gear keeps its end padding.
2026-10-21 10:14:10 +00:00
austinried 47787182d0 ui: scroll the library tab strip and explain offline mode
The tab bar measured the selected tab's full label before the trailing
controls, so a long translation or a large font scale squeezed the
settings gear and eventually removed it, and an indicator in the bar
made that happen sooner. Put the tabs in a weighted, horizontally
scrollable strip and keep the gear outside it, then scroll the selected
tab into view when the pager settles, in either layout direction.

Move the offline indicator out of the bar into a strip above the tabs
that names the mode and offers to go online. Offline mode limits every
list, and Home already swaps to downloaded content without saying why;
an icon in the bar could not explain that and competed for the same
width.
2026-10-21 10:14:10 +00:00
austinried aca2bd4be2 library: filter the artist cover fallback before ranking 2026-10-21 10:14:01 +00:00