Skip to content

Fix navigation back-stack, dead Library taps, and playlist UX gaps - #1

Open
4bit33 wants to merge 4 commits into
masterfrom
feature/phase-0-ux-fixes
Open

4bit33 wants to merge 4 commits into
masterfrom
feature/phase-0-ux-fixes

Conversation

@4bit33

@4bit33 4bit33 commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

Targeted correctness fixes for the "unfinished" feel reported when testing on-device, found via a code audit (three parallel exploration passes over navigation, theming, and animation) rather than guesswork.

  • Navigation back-stack bug: the single navigate() closure applied popUpTo(Home){saveState=true} to every call, including drill-downs into Player/Queue/Search/PlaylistDetail/Favorites — so pressing back from any of those always landed on Home instead of the screen you actually came from. Split into navigateToTab() (dock/rail tab switches only) and a plain-push navigate() for everything else.
  • Dead taps in Library: Artist/Genre/Folder rows had no onClick at all (Albums worked, the other three didn't). Wired playArtist/playGenre/playFolder, adding a new GetFolderSongsUseCase + getSongsOfFolder DAO query that mirrors the existing album/artist/genre pattern (no schema migration — reuses the existing relativePath column).
  • Playlist UX gaps:
    • Delete confirmation added to the Playlists list overflow (it deleted instantly before; the detail screen already confirmed — now both match).
    • New "Add songs" entry point inside PlaylistDetailScreen via a new searchable multi-select SongPickerSheet component — previously you could only remove songs, never add, while viewing a playlist.
    • "Add to playlist" extended from Library-only to Home, Search, and Favorites song rows (reusing the existing SongOverflowSheet/PlaylistPickerSheet).
    • Playing an empty playlist silently no-opped with zero feedback (the app had no Toast/snackbar anywhere despite a ready-made but unused ResonanceSnackbar component). Wired a shared SnackbarHost into the app shell.
  • collectAsState()collectAsStateWithLifecycle() across every screen, so StateFlow collection pauses while the app is backgrounded instead of running continuously (added the lifecycle-runtime-compose dependency).

Test plan

Gradle can't reach loopback sockets in the sandbox this was developed in (confirmed via stacktrace — UnixDomainSockets pipe init failure), so this could not be compiled or run in that environment. Every touched file was manually re-read end-to-end to verify import/brace/constructor consistency. Please build in Android Studio and smoke-test before merging:

  • Project builds clean
  • Open a song from Library → Player → back → lands back on Library (not Home); same for Search→Player, Home→Favorites→Player, Playlists→PlaylistDetail→Player
  • Tap an artist/genre/folder row in Library → plays and opens Queue
  • Delete a playlist from the Playlists list → confirmation dialog appears
  • Open a playlist → "Add songs" → picker appears, adds tracks
  • "Add to playlist" works from Home/Search/Favorites song row overflow menus
  • Tap Play on an empty playlist → snackbar shown instead of nothing happening

This is Phase 0 of a larger rebrand/UX plan (name change to "Crate", new app icon, accent-palette picker, motion pass) — those are separate, larger follow-up changes not included here.

🤖 Generated with Claude Code

4bit33 and others added 4 commits September 14, 2026 21:39
The app felt unfinished due to a handful of concrete bugs rather than
vague roughness:

- navigate() applied popUpTo(Home) to every push, including drill-downs
  into Player/Queue/Search/PlaylistDetail/Favorites, so back from those
  screens always landed on Home instead of the screen the user came
  from. Split into navigateToTab() (dock/rail only) and a plain-push
  navigate() for everything else.
- Library's Artist/Genre/Folder rows had no onClick at all, unlike
  Albums. Wired playArtist/playGenre/playFolder (new
  GetFolderSongsUseCase + getSongsOfFolder query, mirroring the
  existing album/artist/genre pattern) so tapping any of them plays
  and opens the queue.
- Playlists list deleted with no confirmation while the detail screen
  did confirm; unified both on the same ResonanceDialog flow.
- Playlist detail had no way to add songs while viewing it; added a
  SongPickerSheet (searchable multi-select) wired to a new
  addSongs() on PlaylistDetailViewModel.
- "Add to playlist" only existed on Library's Songs tab; extended the
  same SongOverflowSheet/PlaylistPickerSheet wiring to Home, Search,
  and Favorites song rows.
- Playing an empty playlist silently no-opped with zero feedback (the
  app had no Toast/snackbar anywhere despite a ready-made
  ResonanceSnackbar component). Wired a shared SnackbarHost into the
  app shell and surfaced it there.

Also switched every collectAsState() to collectAsStateWithLifecycle()
across the app so StateFlow collection pauses while the app is
backgrounded instead of running continuously.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
User-facing rename only — app_name, permission rationale string,
window theme name, manifest label, root project name, ARCHITECTURE.md
title. Kotlin package, applicationId, and internal Resonance* symbol
names are unchanged (deferred, no user-facing benefit for the churn).

Replaced the launcher icon foreground: it was still the unmodified
stock Android Studio template (purple circle, default glyph), never
actually designed. New mark is a simple rounded-square outline with a
play triangle, flat accent color, no gradients — matches the
"simpler/mainstream" direction from the plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Accent color is now a persisted hue (DataStore, same pattern as
themeMode) instead of hardcoded copper. stitchDarkColors(accentHue)
derives accent/accentGlow/accentDim/onPrimaryContainer from the hue
via android.graphics.Color.HSVToColor (no new dependency); surfaces
and the secondary/cyan format-badge color stay fixed per DESIGN.md's
existing "only the primary accent may adapt" rule.

Settings > Appearance gets a swatch row (6 curated hues) plus a hue
slider for a fully custom accent.

Also deleted the vinyl-groove Canvas fallback (WaveformFallback) and
its ArtworkFallbackStyle enum — no caller ever selected it, every
ArtworkImage use already fell through to the plain note-glyph
fallback (NoteFallback), so the etched-groove path and the unused
legacy ArtworkFallback() wrapper were dead code once collapsed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All via native Compose animation APIs already on the classpath, no new
dependency:

- NavHost gets a default fade enter/exit (tab switches and drill-down
  pushes alike — one crossfade avoids picking wrong directionality for
  the two different kinds of navigation happening through one graph).
- Mini player now mounts/unmounts through AnimatedVisibility (slide +
  fade) instead of a hard if, and is suppressed on the Player/Queue
  routes (route-aware, not just song-null-aware) so it stops doubling
  up with the full transport controls already visible there.
- LazyColumn/LazyRow items across Library (all 5 tabs), Search,
  Favorites, Home's recently-played row, and Playlists get
  Modifier.animateItem() so inserts/removes/filtering slide instead of
  jump-cutting.
- ReorderableLazyColumn (shared by Queue and playlist detail) gets the
  same animateItem() on its item wrapper — this is what actually
  answers the "instant snap-back on drop" and "siblings jump instead
  of sliding" complaints, for free, since animateItem()'s placement
  animation is spring-based by default. zIndex (raise dragged row
  above siblings) moved from the row to the wrapper, since it now
  needs to apply at the LazyColumn-item level, not inside it.
- Play/pause icon crossfades (mini player + full player's
  ResonancePlaybackButton) instead of an instant swap; nav dock icon
  tint animates and the dot/label indicator crossfades instead of
  snapping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant