diff --git a/docs/architecture.md b/docs/architecture.md index 1b9ce587..014a567c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -7,7 +7,7 @@ Subtracks is a native Android client for Subsonic-compatible servers (Navidrome, - Talk to any Subsonic-compatible server over its REST API. - Behave well on a poor connection: a local Room mirror, paging, and (later) offline playback. - F-Droid friendly: free/libre dependencies only, no telemetry, buildable from source without proprietary services. -- `minSdk 24`. The app bundles its own SQLite so behaviour is identical across devices and FTS5 is always available. +- `minSdk 24`. The app bundles its own SQLite so behaviour is identical across devices and does not depend on the platform version. ## Stack @@ -16,7 +16,7 @@ Subtracks is a native Android client for Subsonic-compatible servers (Navidrome, | UI | Jetpack Compose, Material 3 | | Navigation | Navigation Compose | | DI | Koin, constructor injection | -| Persistence | Room 3 (`androidx.room3`) on AndroidX `sqlite-bundled` (SQLite 3.50+, FTS5) | +| Persistence | Room 3 (`androidx.room3`) on AndroidX `sqlite-bundled` (SQLite 3.50+) | | Lists | Paging 3 over Room `PagingSource` (local mirror, not remote paging) | | Networking | OkHttp + DOM XML parsing | | Async | Kotlin coroutines / `Flow` | @@ -46,7 +46,7 @@ app/src/main/kotlin/com/subtracks PlaybackService.kt MediaSessionService hosting ExoPlayer PlaybackController.kt MediaController wrapper: state, queue, transport data/ - model/Models.kt Room entities, including the search_index FTS5 table + model/Models.kt Room entities db/ SubtracksDatabase, DAOs, bundled-SQLite builder prefs/UserPreferences.kt DataStore-backed UI prefs repo/ SourceRepository, LibraryRepository @@ -92,7 +92,7 @@ The window is also the queue the media notification and Android Auto can browse ## Data model -`sources` and `subsonic_sources` hold the configured servers. Library tables are keyed by `(sourceId, id)` and cascade from `sources`: `artists`, `albums`, `playlists`, `playlist_songs`, `songs`. `search_index` is an FTS5 table (trigram tokenizer) over titles, maintained by sync. +`sources` and `subsonic_sources` hold the configured servers. Library tables are keyed by `(sourceId, id)` and cascade from `sources`: `artists`, `albums`, `playlists`, `playlist_songs`, `songs`. UI and playback preferences live in DataStore (`user_prefs`), not in SQLite: they are not relational, nothing joins against them, and keeping them out of Room avoids a schema migration per preference. Room is for the library only. @@ -106,11 +106,11 @@ Images: grids and lists request a 256 px `getCoverArt` thumbnail; the 120 dp det ## Sync -`SyncService.sync()` is serialized with a mutex. It fetches the whole library first (artists, albums, songs, playlists and their entries), then writes in a single `immediateTransaction`: upserts, deletes rows no longer present on the server (diffed against the DB), rebuilds `playlist_songs`, and rebuilds the FTS index for that source. Because fetching happens outside the transaction, a network failure never leaves a half-applied state. +`SyncService.sync()` is serialized with a mutex. It fetches the whole library first (artists, albums, songs, playlists and their entries), then writes in a single `immediateTransaction`: upserts, deletes rows no longer present on the server (diffed against the DB), and rebuilds `playlist_songs`. Because fetching happens outside the transaction, a network failure never leaves a half-applied state. ## Search -`search_index` is FTS5 with the trigram tokenizer, so `MATCH` supports substring queries. Queries shorter than three characters are rejected by `SearchDao` (below the trigram minimum). Rows are keyed by `(sourceId, type, itemId)`; the DAO scopes results to a source and orders by rank. +Search is a case-insensitive substring filter on the library queries (`instr(lower(column), lower(:search)) > 0` in `LibraryDao`), scoped to the active tab and with the matching rows ordered by the tab's sort. There is no separate search index; the filter runs directly against the library tables. ## Networking and auth @@ -122,7 +122,7 @@ No analytics or third-party telemetry. Credentials are stored locally in app dat ## Testing -- Unit tests use Robolectric, Roborazzi (screenshots) and MockWebServer (client), plus an in-memory Room database on the bundled SQLite JVM driver (Robolectric's SQLite has no FTS5). +- Unit tests use Robolectric, Roborazzi (screenshots) and MockWebServer (client), plus an in-memory Room database on the bundled SQLite JVM driver (Robolectric's SQLite differs from the bundled one). - Roborazzi screenshots exercise the stateless `*Screen` composables with fake data; paging screens are fed `PagingData.from(...)`. They are rendered on demand with `gradle :app:recordRoborazziDebug` (into the gitignored `app/src/test/screenshots/`) for local review; no images are committed and nothing verifies them in CI. - Cover art in the screenshots is generated deterministically in the test and served through Coil's `FakeImageLoaderEngine` (synchronous, and no third-party images are committed); a real `ImageLoader` is installed for the run and reset afterwards. Each image is two or three flat colours in a pattern that identifies its type — stripes for albums, dots for playlists, rings for artists. - Integration tests run `SubsonicSourceIntegrationTest` against real navidrome and gonic instances, driven by `tools/integration-test.nu`. diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index 9538f1e5..c096fbfc 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -11,10 +11,9 @@ Nix devshell (JDK, Gradle, Android SDK, nushell, navidrome, gonic), Gradle Kotli Everything needed to mirror a server library locally: - `MusicSource` abstraction with `SubsonicClient`, `SubsonicXml` and `SubsonicSource`. -- Room 3 schema on bundled SQLite, so the SQLite version and FTS5 are identical on every device. +- Room 3 schema on bundled SQLite, so the SQLite version is identical on every device. - `SyncService`: fetch first, then a single transaction of upserts with diff-based pruning. -- FTS5 trigram search index over titles. -- Unit tests (client, XML, sync, search), native integration tests against navidrome and gonic, and CI (`unit`, `lint`, `integration`) with warm caches. +- Unit tests (client, XML, sync), native integration tests against navidrome and gonic, and CI (`unit`, `lint`, `integration`) with warm caches. ## Phase 2 - UI vertical slice (done) @@ -43,7 +42,7 @@ Not done yet in this slice (deliberately): cover-art tonal colour extraction, lo ## Phase 4 - Search, playlists, offline -- Search UI over the FTS5 index (substring, ranked). +- (done) Search: a substring filter on the library queries, scoped to the active tab, with a docked search field and per-tab sort/filter controls. - Playlist browsing and editing (create/add/remove), syncing changes back to the server. - Offline mode and downloads.