mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
Replace the placeholder-art screenshots with renders of a real library, so the store listing shows real album and artist art and names. The generator (DemoScreenshotTest, run with `gradle :app:demoScreenshots`) syncs the public Navidrome demo (demo.navidrome.org) into an in-memory database using the normal SyncService pipeline, derives the theme colours from the fetched cover art, and renders each screen. The synced data and cover art stay in memory and are never written to the repository; only the resulting PNGs are committed. The test is excluded from the unit-test tasks and is not part of CI. Also reverts the screenshot fixture palette tweak from the previous commit, which the real art makes unnecessary.
7.3 KiB
7.3 KiB
AGENTS.md
Guidance for agents (and humans) working in this repository.
What this is
A native Android rewrite of Subtracks, a client for Subsonic-compatible servers (Navidrome, gonic, Airsonic, ...), in Kotlin and Jetpack Compose. The main history still contains the earlier Flutter app; the native app is being built fresh under app/ and ships as com.subtracks.next for side-by-side beta. The aim is a clean, F-Droid-friendly client: free/libre dependencies only, no telemetry.
Environment
Everything comes from the Nix flake devshell; do not install toolchains by hand.
- Enter it with
nix develop, or let direnv load it into your shell (.envrcrunsuse flake). - The default devshell provides JDK 21, Gradle 9, the Android SDK (platform 37, build-tools 37.0.0, platform-tools 37.0.1) and nushell.
- The integration harness needs real servers, so it lives in a separate
nix develop .#integrationshell (navidrome, gonic, lms, php, Nextcloud with the Music app, sqlite). Keeping them out of the default shell avoids pulling that closure into everyday builds. - Build with the devshell
gradle, not./gradlew. CI uses the nix Gradle pinned byflake.lock; the wrapper is kept only for people without Nix.
Commands
- Build:
gradle :app:assembleDebug - Release build:
gradle :app:assembleReleaseonly when build config, R8/ProGuard or dependencies change, and once before a merge; not part of routine verification - Unit tests:
gradle :app:testDebugUnitTest - Lint:
gradle :app:ktlintCheck :app:lintDebug - Format:
gradle :app:ktlintFormat - Integration tests:
nix develop .#integration --command nu tools/integration-test.nu(starts navidrome, gonic, lms and a Nextcloud Music instance, then runs:app:integrationTest) - Screenshots:
gradle :app:recordRoborazziDebugrenders the*Screencomposables toapp/src/test/screenshots/(gitignored) plus an HTML report inapp/build/reports/roborazzi/, for local review. No golden images are committed andverifyRoborazziDebugis not part of CI. - Demo screenshots:
gradle :app:demoScreenshotsdoes a real sync of the public Navidrome demo (https://demo.navidrome.org,demo/demo) and renders the store screenshots with that library's art and names intoapp/src/test/screenshots/demo/(gitignored). Needs network; not part of CI or the integration suite.
Testing layout
app/src/test- JVM unit tests (JUnit4, Robolectric, Roborazzi, MockWebServer). Run by:app:testDebugUnitTest.app/src/integrationTest- tests that need real servers. Compiled with the unit test sources but run by the dedicated:app:integrationTesttask, and excluded from every*UnitTesttask. The nushell harness starts the servers for you.- A test earns its place only if it fails when the behaviour it covers breaks, and passes when the behaviour works. A test that passes either way is worse than no test: it looks like coverage while hiding the gap. So when you add one, break the behaviour (or run the test against the pre-fix code), watch it fail, then restore the fix and watch it pass.
CI
- Forgejo Actions,
.forgejo/workflows/ci.yml, a singlecijob that runs the lint, unit test and integration test steps (one cache restore/save and Gradle warm-up instead of three). - Runs on the
nix-dockerrunner (thelocalhost/nix-ciimage built by the separatenix-homerepo) inside the devshell, restoring the Nix store viacache-nix-actionand caching Gradle and the integration test music. - The
nix-dockerandbookwormrunner labels, their images and the runner's networking are defined in the separatenix-homerepo (hosts-incus/git-runner.nix). - Pull requests from a fork need approval before the workflow runs.
- The GitHub mirror and the GitHub PR import live under
.forgejo/; see.forgejo/README.md.
Conventions
- Prefer self-explanatory code; do not add comments unless the reason cannot be inferred from the code itself (docstrings for functions and exports are fine).
- ktlint (
org.jlleitschuh.gradle.ktlint) enforces style and flags unused imports; rungradle :app:ktlintFormatbefore committing..editorconfigexempts@Composablefunctions from the lowerCamelCase function-naming rule. - Room 3 (
androidx.room3) on AndroidXsqlite-bundled(BundledSQLiteDriver, SQLite 3.50+). Do not reintroduce SQLDelight or the platform SQLite. - Networking uses OkHttp and DOM XML parsing; auth uses the Subsonic token scheme by default.
- Use coroutines and
Flow; library reads are exposed asFlowfrom Room. Large lists page with Paging 3 over RoomPagingSourcerather than loading the whole table. - The library is mirrored in full at sync and must work offline. Never fetch library metadata on demand for browsing, filtering or sorting; only media bytes (streams, downloads) and images are on-demand. A server-derived sort key must be synced into a local column, not requested lazily when the sort is selected.
- Playback is Media3:
PlaybackServiceis aMediaSessionServiceowning anExoPlayer, andPlaybackController(app-scoped Koin singleton) is the only thing the UI talks to. The queue is aqueue_entrieslist of references with optional ranges plus aplayback_cursorrow; keep only a bounded window in memory (never the whole queue, and never load a whole table to build one). Stream URLs are freshly salted per request, so attach artwork byCoverArtRefcache key, not by URL. - UI state lives in
androidx.lifecycle.ViewModel, wired with Koin (koinViewModel()); screens split into a stateful*Routeand a stateless*Screenfor screenshot tests. - UI and playback preferences go in DataStore (
UserPreferences), not Room. - Verify current dependency versions and their compatibility before adding or pinning anything.
Gotchas
- Robolectric runs at SDK 35 while
targetSdkis 37; raising it changes the on-demand Roborazzi renders (there are no committed goldens to re-record). - Room 3 does not map
PagingSourceautomatically: a DAO that returns it needs@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class). - Material icons are not pulled in by
material3; use the BOM-managedmaterial-icons-core/material-icons-extended(frozen at 1.7.8). Extended is large in debug builds but R8 strips unused icons from release. - Robolectric creates a fresh
Applicationper test in one JVM, soSubtracksApp.onCreatestops any running Koin beforestartKoin. applicationIdiscom.subtracks.nextfor the beta; change it tocom.subtracksbefore any store release.- The CI image provides
nix-ld,jq,sqlite,node,zstdand arunneruser thatcache-nix-actionexpects. None of that is needed locally beyond the devshell. - Integration servers: navidrome on 4533 (
admin/password), gonic on 4747 (admin/admin), lms on 5082 (admin/subtracks-lms), Nextcloud Music on 8090 (admin/subtracks-nextcloud, base URLhttp://localhost:8090/index.php/apps/music/subsonic/). The test music is cached in.integration/music(gitignored). - lms and Nextcloud Music authenticate the Subsonic API with per-user API keys instead of passwords. The harness seeds a key and most tests use
useTokenAuth = false;SubsonicAuthIntegrationTestdrivesuseTokenAuth = trueand asserts the fallback to plaintext (lms answers code 42, Nextcloud 41). nix developdoes not source your shell rc files (so tools like atuin are not active); use direnv if you want your normal interactive shell.