Hoisting the mini-player into an outer Scaffold (previous commit)
introduced a real regression: extra space above the top header, and
a dead gap between the bottom tab bar and the mini-player.
Root cause: TopAppBar and Material3's NavigationBar (used by
BottomNavBar) are self-inset-aware - they only ever looked correct
because they sat directly at the true screen edges. Now they're
nested one level deeper inside the outer Scaffold's content area:
- The outer Scaffold has no topBar, so with its default
contentWindowInsets it fell back to reserving the raw status-bar
inset itself as top padding on the NavHost - on top of MainScreen's
own TopAppBar doing the same thing a second time.
- BottomNavBar unconditionally pads itself for the navigation-bar
inset regardless of what's below it, but MiniPlayerBar (which
already self-pads for that inset) now always sits below it - so
BottomNavBar was reserving space for an inset it's no longer
adjacent to.
Fixed by zeroing contentWindowInsets on the outer Scaffold (it has
nothing of its own to protect beyond MiniPlayerBar's already-real
measured height) and zeroing BottomNavBar's own windowInsets (since
MiniPlayerBar is now what's genuinely bottom-adjacent).
Verified on-device across Home/Library/ArtistDetail/Settings: tight
header spacing restored, tab bar sits flush against the mini-player
with no gap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The user noticed the mini-player ribbon vanished whenever navigating
into an artist/album/settings screen, and asked for the full-player
transport controls to stay anchored to a fixed spot and for long
titles to marquee like Spotify instead of wrapping.
- Mini-player was owned by MainScreen's own Scaffold, so it unmounted
entirely on any sibling nav destination (ArtistDetail/AlbumDetail/
Settings). Hoisted it to DeepwaveNavHost, wrapping the whole
authenticated NavHost in an outer Scaffold whose bottomBar shows
the mini-player on every route except Login and NowPlaying itself.
MainScreen no longer needs PlayerViewModel at all as a result -
reverted it to a single previewable composable.
- That hoist exposed a real inset bug: MiniPlayerBar is a plain Row,
not a Material3 NavigationBar/BottomAppBar, so it doesn't handle
system-bar insets on its own - it only ever looked right because it
sat above the inset-aware BottomNavBar in the same Column. Standing
alone, it rendered underneath the gesture nav bar. Fixed with an
explicit navigationBarsPadding().
- NowPlayingScreen: the artwork/title/artist block now gets weight(1f)
in the Column, so the seek bar and transport controls always land
at a fixed distance from the bottom regardless of title length -
previously a long title wrapped to 2-3 lines and pushed the controls
down inconsistently.
- Long titles now use maxLines = 1 + Modifier.basicMarquee() instead
of wrapping, on both NowPlayingScreen and MiniPlayerBar's title.
Fixed navController.currentBackStackEntryAsState()-based route
comparison to use route-string equality rather than the hasRoute<T>()
generic (that API isn't available in navigation-compose 2.9.8, this
project's pinned version - only the KClass-less String overload
exists here).
Verified on-device: mini-player now persists correctly through
Home -> Album Detail -> Now Playing and back, sits above the system
nav bar, and the transport controls stay in the same position across
tracks with short and long titles.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Builds the single MediaSession-backed player that in-app playback,
offline downloads (Phase 5), and Android Auto (Phase 6) will all
share, per the Phase 2+ roadmap's Phase 4.
- DeepwavePlaybackService: ExoPlayer + MediaLibrarySession, built as
a MediaLibraryService from day one (not the plainer
MediaSessionService) so Android Auto only has to add browse-tree
content later, never rebuild the service. Browse tree is a stub
for now (MediaLibrarySession.Callback's defaults deny browsing).
- LocalOrRemoteDataSource: the entire "prefer a downloaded file over
streaming" mechanism as a ResolvingDataSource.Resolver, backed by
an in-memory LocalTrackFiles registry that stays empty until
Phase 5 populates it from Room - reused unmodified by Android Auto
later.
- ExoPlayer's OkHttpDataSource shares the same signed OkHttpClient as
Retrofit/Coil, so SubsonicRequestInterceptor signs stream requests
identically to every other Subsonic call - streamUrl() builds the
request URL the same way coverArtUrl() does for Coil.
- PlaybackController: app-facing facade over a MediaController,
exposing Flow<PlaybackUiState> - same facade convention as
ServerRepository. NowPlayingScreen (new, full-screen: artwork,
seek bar, play/pause/skip) and MiniPlayerBar (now real, replacing
Phase 2/3's static placeholder) both consume it.
- Track clicks in AlbumDetailScreen (queues the whole album from the
clicked index) and SearchScreen (single-song queue) now actually
play, via PlaybackController injected into their ViewModels.
- Scrobbling: submission=false when a track starts, submission=true
for the outgoing track on each transition - a simplified heuristic
rather than a played-percentage threshold, feeding Navidrome's
play-count data that Phase 3's getAlbumList2(frequent/recent) rows
read from.
- Runtime POST_NOTIFICATIONS request added to MainActivity for API
33+ (declared in the manifest but easy to forget the runtime half
of - without it the service still plays, but its notification
never shows).
Split MainScreen into a thin PlayerViewModel-resolving wrapper plus a
previewable MainContent, rather than having MainScreen call
hiltViewModel() directly - preserves the interactive Android Studio
preview from last session, which a direct Hilt dependency would have
broken.
media3 pinned to 1.11.1 (built against Kotlin 2.2.0, matching this
project's 2.2.10 - learned from Phase 3's Coil version conflict to
check this before picking a version this time).
Verified extensively on-device against the real Navidrome server:
real tracks play with correct metadata/artwork, pause/resume and
queue auto-advance work, the real Android MediaSession exposes
correct state (checked independently via `dumpsys media_session`,
not just the app's own UI), background playback survives
foregrounding/backgrounding, and no crashes across the whole session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces Phase 2's sample-data placeholders with real Navidrome
browsing: artists, albums, songs, and search, per the Phase 2+
roadmap's Phase 3.
- SubsonicApi grows from just ping to getArtists/getArtist/
getAlbumList2/getAlbum/search3, with DTOs split per-domain
(ArtistModels/AlbumModels/SongModels/SearchModels) and mapped to
plain domain models (data/model) by the new LibraryRepository
- Coil wired in for cover art, sharing the same signed OkHttpClient
as Retrofit via a Hilt EntryPoint (SubsonicRequestInterceptor signs
cover-art requests identically to every other Subsonic call) -
Artwork() falls back to the Phase 2 placeholder icon when no cover
art id is known
- ui/library replaces the Phase 2 placeholders: HomeScreen now shows
real Recently Added/Played and a random "Made For You" row,
ArtistsScreen is the real Library tab, ArtistDetailScreen and
AlbumDetailScreen are new drill-down screens (typed nav routes,
ViewModels resolve their id via SavedStateHandle.toRoute()), and
SearchScreen does debounced search3 across artists/albums/songs
- TrackRow gained a `circular` option so it can double as an artist
row in the Library list, not just a track row
First real test coverage in the repo: LibraryRepositoryTest (DTO to
domain-model mapping against MockWebServer) and
SubsonicRequestInterceptorTest (rewritten URL + signed query params).
CredentialsStore/ServerRepository tests are deliberately still out of
scope - CredentialsStore's Tink/Android Keystore usage isn't testable
in a plain JVM unit test without Robolectric, which felt like a
bigger side quest than this phase called for.
Coil pinned to 3.3.0 rather than the newer 3.6.x latest: newer Coil
requires Kotlin 2.4+, which conflicts with this project's Kotlin
2.2.10; 3.3.0 was the last release built against Kotlin 2.2.x.
Verified end-to-end against a real Navidrome server on-device: Home
rows, artist list/detail, album detail, and search all load real data
and real cover art.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Redirects Deepwave from a vanilla Subsonic client toward the Spotify/
SoundCloud-style product the user wants, per the Phase 2+ roadmap.
- Dark-first Material3 theme with a fixed aqua accent (no Material You
dynamic color), full type scale, rounded-corner shape system
- Typed (@Serializable) navigation routes; new bottom-tab shell
(Home/Search/Library) with a settings entry point and mini-player
placeholder bar, replacing the single placeholder Home screen
- Logout moved out of Home into a dedicated Settings screen
- Reusable component library (TrackRow, AlbumCard, ArtistCard,
ArtworkPlaceholder, BottomNavBar, MiniPlayerBar) for Phase 3 to wire
real data into
- Strings externalized to strings.xml
- Security fix: exclude the Tink keyset and encrypted credentials
DataStore from Android auto-backup (backup_rules.xml /
data_extraction_rules.xml), previously unexcluded despite
allowBackup=true
- Fix: Home screen's content column was missing verticalScroll,
making it unscrollable once content exceeds the viewport; also
added breathing room between each row's header and its cards
- Split Login/Settings into stateless content composables + thin
ViewModel-wired wrappers, and added @Preview coverage (including
interactive previews for MainScreen and BottomNavBar) across every
screen and component so the UI can be reviewed in Android Studio's
preview pane without running on a device
Added material-icons-extended as a pragmatic deviation from the
Phase 2 plan (no new deps) since later phases all need icons outside
Compose's small default set (play/pause/skip/download/playlist).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Spotify-like Navidrome client scaffold with Hilt DI, a Subsonic API
client (token auth, base-URL rewriting), encrypted single-server
credential storage (DataStore + Tink/Keystore), and a login/session
flow gating Compose Navigation between Login and a placeholder Home
screen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>