Adds androidx.palette dependency and a rememberDominantColor()
helper that pulls the cover bitmap via Coil's singleton loader
(shares the cache with the on-screen AsyncImage), runs Palette
extraction on Dispatchers.Default, and animates the resulting
color with animateColorAsState so track changes tween smoothly.
NowPlayingScreen wraps the body in a Box with a vertical gradient
(0% dominant @ 55%α → 45% dominant @ 18%α → 100% scheme.background)
and lets the Scaffold's containerColor go transparent so the
gradient shows through. Falls back to a near-transparent gradient
on bitmap-load failure so the screen never sits on flat black.
Hero transition (MiniPlayer → NowPlaying cover) and cover
preload still pending — those land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cleanups around the noise at the top of every CI log:
* CI workflow: JAVA_TOOL_OPTIONS=--enable-native-access=ALL-UNNAMED
at the workflow env level so both build + release jobs apply it to
the launcher JVM (not just the daemon). The launcher is the one
loading native-platform.jar via System.load.
* Kotlin compiler: -Xannotation-default-target=param-property in
kotlin.compilerOptions.freeCompilerArgs. Opts every @Inject /
@ApplicationContext / @ApplicationScope constructor-parameter
annotation into the future Kotlin 2.3 behavior (apply to both
param AND property), clearing the 11 warnings on AuthController /
AuthStore / MutationReplayer / SyncController / EventsStream /
LiveEventsDispatcher / PlayEventsReporter / PlayerController /
PlayerFactory / ResumeController.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Observes PlayerController.uiState and runs the (current track,
playing) state machine. Fires play_started on track-begin, then
on close emits play_ended (within 3s of duration) or play_skipped
through the live EventsApi. Failures and offline-start plays fall
through to the MutationQueue PLAY_OFFLINE kind for durable replay.
App-background (ProcessLifecycleOwner onStop) closes the current
play via the offline path so a process kill mid-listen still
records a play.
Wires into MinstrelApplication via @Inject so the singleton
constructs at app start (same pattern as MutationReplayer /
SyncController / ResumeController). client_id is a stable
device-install UUID resolved through AuthStore (added in 415200d).
Adds androidx.lifecycle:lifecycle-process dependency for the
ProcessLifecycleOwner background-event hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First user-visible UI. LibraryScreen renders the LibraryViewModel
UiState into horizontally-scrolling Artist / Album rows; Loading
shows a centered spinner, Empty / Error fall back to shared widgets.
Files:
- library/ui/LibraryScreen.kt — top-level screen, hiltViewModel
+ collectAsStateWithLifecycle, exhaustive when(state)
- library/widgets/ArtistCard.kt — circular cover + name beneath
- library/widgets/AlbumCard.kt — 144dp square cover + title +
artist beneath, matches Flutter spec (~176dp tile width)
- shared/widgets/EmptyState.kt — generic empty-state widget
(Lucide Inbox by default), used by Library + reusable for
Quarantine / search etc.
- shared/widgets/ErrorRetry.kt — error message + retry button
(uses LocalActionColors.primary = Moss per design system rule)
Audit-deferred items now triggered:
- MinstrelApplication implements SingletonImageLoader.Factory and
wires OkHttpNetworkFetcherFactory(callFactory = { okHttpClient })
so Coil cover-art requests reuse the shared auth-bearing OkHttp
- Lucide icons via com.composables:icons-lucide-android:2.2.1 for
placeholder / decorative iconography
MainActivity now renders LibraryScreen inside a Scaffold (not the
"phase 1" text placeholder). Nav-graph wiring deferred to Phase 5.5
— onArtistClick / onAlbumClick are no-op for now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes uncovered by the 2026-05-22 build-config audit + the Gradle 9 +
JUnit Platform launcher requirement that just surfaced in CI:
- testRuntimeOnly junit-platform-launcher: Gradle 9 no longer auto-
injects it; tests fail with "Failed to load JUnit Platform" without
an explicit dep.
- compileSdk + targetSdk 35 -> 36: AGP 9 defaults to 36 and warns on
lower values; CI was also auto-downloading build-tools 36 at
runtime (now pre-installed in ci-android:36).
- container.image bumped to ci-android:36 to match.
- configuration-cache.problems=warn in gradle.properties: detekt 2.0-
alpha + ktlint Gradle plugin have CC compat holes; warn rather
than fail.
- androidTest dep parity: kotlin("test") + kotlinx-coroutines-test
added (was on testImplementation only).
- CI: actions/cache@v4 for ~/.gradle/{caches,wrapper} + ~/.kotlin,
keyed on gradle-wrapper.properties + libs.versions.toml + the
*.gradle.kts files. Saves ~3 min per run after warm-up.
Deferred (with trigger conditions, will land when needed): Hilt
testing artifacts + HiltTestRunner (first @HiltAndroidTest), Room
Gradle plugin + schemaDirectory (Phase 4.2), Coil 3 ImageLoader
factory sharing OkHttp (Phase 5.4), MainDispatcherRule test utility
(Phase 5.3), JUnit-5/4 split for instrumented (Phase 5+),
NetworkSecurityConfig (pre-cutover Phase 14).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AuthCookieInterceptorTest imports `kotlin.test.assertEquals` /
`kotlin.test.assertNull` which weren't resolving without the explicit
kotlin-test dep. `kotlin("test")` is sourced from the applied Kotlin
plugin (built-in via AGP 9), so no version pin needed.
Hilt + KSP code generation worked correctly in the prior run —
hiltAggregateDepsDebug succeeded; the failure was purely test-side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three breaking changes I missed when bumping to 2.0.0-alpha.3:
1. Gradle plugin id changed: io.gitlab.arturbosch.detekt -> dev.detekt
2. Task FQN changed: io.gitlab.arturbosch.detekt.Detekt
-> dev.detekt.gradle.Detekt
(same for DetektCreateBaselineTask)
3. jvmTarget is now a Property API (.set("17")) instead of var assignment
Also dropped `autoCorrect` from the detekt {} block — it's not in the
2.0 options list per the official getting-started docs.
Per the 2.0 release notes: "the workaround of disabling the new DSL
and built-in Kotlin via gradle.properties for AGP 9.x projects is no
longer required" — so our existing AGP 9 + built-in-Kotlin setup is
expected to work cleanly with detekt 2.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
detekt 1.23.7 bundles kotlin-compiler-embeddable 1.9.10 whose
--jvm-target validator only accepts up to 22. Detekt auto-detected
the runner's JDK 25 and choked. Pin to 17 (matches our
compileOptions.targetCompatibility + kotlin.compilerOptions.jvmTarget).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two assignments had a multi-line RHS sitting on the same line as the
`=`. ktlint's multiline-expression-wrapping rule requires the
multi-line expression to start on a new line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous attempt opted out of AGP 9's built-in Kotlin (via
android.builtInKotlin=false + explicit kotlin-android plugin) because
the message from Gradle suggested it. But Kotlin 2.2.21's
kotlin-android plugin can't cast AGP 9's new ApplicationExtension to
the removed BaseExtension:
class ApplicationExtensionImpl$AgpDecorated_Decorated
cannot be cast to class com.android.build.gradle.BaseExtension
That suggestion is for projects with an older Kotlin toolchain. The
real fix:
- Kotlin 2.3.21 (latest stable; first line where kotlin-android also
supports AGP 9, but more importantly the built-in path works)
- KSP 2.3.8 — KSP PR #2674 (merged Oct 2025) added AGP 9 built-in
Kotlin support. KSP 1.x and pre-2.3 don't work with built-in Kotlin.
- Re-drop the kotlin-android plugin from both build.gradle.kts files;
AGP 9 enables built-in Kotlin by default and KSP 2.3 cooperates.
- Remove android.builtInKotlin=false from gradle.properties.
compose-compiler plugin tracks the Kotlin version via version.ref, so
no separate bump there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AGP 9 enables built-in Kotlin by default (`android.builtInKotlin=true`),
which we initially adopted by dropping the `kotlin-android` plugin
alias. But KSP isn't compatible with built-in Kotlin yet — Gradle
errors out with:
> KSP is not compatible with Android Gradle Plugin's built-in Kotlin.
> Please disable by adding android.builtInKotlin=false to gradle.properties
> and apply kotlin("android") plugin
Restored the explicit kotlin-android plugin (root + :app) and added
`android.builtInKotlin=false` to gradle.properties. Revisit when KSP
gains built-in-Kotlin support.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`kotlinOptions { jvmTarget = "17" }` inside the `android { }` block was
removed in newer Kotlin tooling; replaced with the modern top-level
`kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } }` form.
Required after the Kotlin 2.2 + AGP 9 bump; the old DSL was tolerated
through AGP 8.7 + Kotlin 2.0 but not through AGP 9's built-in Kotlin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original 8.x toolchain choked on the ci-android image's JDK 25 with an
opaque "25.0.3" error in `ktlintCheck`; Gradle 8.10's JDK compat matrix
caps at 23. Modern chain:
- Gradle 9.1.0 (first to support JDK 25)
- AGP 9.0.1 (requires Gradle 9.1+, requires Kotlin 2.2.10+)
- Kotlin 2.2.21 / KSP 2.2.21-2.0.5 (latest 2.2.x line)
- Compose BOM 2026.05.01 (current; pulls ui-text-google-fonts at the
BOM-managed version, so the explicit pin was dropped)
AGP 9.0 breaking changes that affected us:
- `kotlin-android` plugin no longer needed — AGP 9 auto-enables via
`android.builtInKotlin=true` default. Removed alias from both the
root build.gradle.kts and :app/build.gradle.kts.
- `applicationVariants` API removed; we don't use it.
- Other defaults flipped (useAndroidx, uniquePackageNames, etc.) but
we already set them explicitly or weren't relying on the old defaults.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M8 phase 1.4. Mirrors flutter_client/lib/theme/. Source of truth for hex
values is flutter_client/shared/fabledsword.tokens.json (manual sync until
cross-language codegen lands; ports the dark-surface + flat cohort).
Material 3 ColorScheme takes accent as primary; action colors
(Moss/Bronze/Oxblood) live in LocalActionColors as semantic roles per the
project_design_system rule "NEVER use accent for action buttons".
Typography uses androidx.compose.ui.text.googlefonts to fetch Fraunces /
Inter / JetBrains Mono at runtime via Play Services Fonts — matches the
Flutter client's `google_fonts` package (no bundled .ttf files in either
tree). Weights restricted to 400/500. Fraunces is reserved for ≥18sp
display/headline slots per the design-system rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M8 phase 1.2. AndroidManifest declares FGS mediaPlayback +
POST_NOTIFICATIONS permissions ahead of the player phase. Activity
hosts a single Compose Scaffold for now; nav graph lands in phase 5.
Launcher icons reused from flutter_client/ (same applicationId means
same brand at cutover). MinstrelApplication referenced in manifest
but the class itself lands in Task 1.3 — manifest class names are
resolved at install time, not build time, so the intermediate commit
still builds.
@AndroidEntryPoint deferred to Task 1.3 alongside @HiltAndroidApp on
MinstrelApplication (Hilt KSP errors without an annotated Application).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>