Two unrelated issues, batched.
Full-player kebab → "Go to album/artist" still crashed with
_debugCheckDuplicatedPageKeys despite the prior onBeforeNavigate
fix. Cause: pop() and push() ran in the same frame, so go_router's
page-key reservation table briefly contained both /now-playing AND
the destination shell-child, tripping the duplicate-key assert.
Refactor: replace onBeforeNavigate with onNavigate(path), where the
host receives the path AFTER the sheet pops and owns the
navigation entirely. The full player wires:
onNavigate: (path) async {
await Navigator.of(context).maybePop();
if (context.mounted) GoRouter.of(context).push(path);
}
Awaiting the pop guarantees /now-playing is fully gone from the
navigator's page list before the push starts.
Mini player + track rows + everywhere else use the default (no
onNavigate) path that does context.push(path) inline — they're
already inside the ShellRoute, no race possible.
Restored alwaysRefresh: true on artistAlbumsProvider. Dropping it
in the cache-loop fix had a side effect: if drift's cachedAlbums
held only a subset of an artist's albums (user previously visited
just one album by them), the artist detail page rendered that
partial list forever — provider only fetched on empty drift, never
on partial drift. The metadata prefetcher only mass-warms
artistProvider (single row), so re-enabling SWR on artistAlbums
won't recreate the storm.
Same root cause as the /queue duplicate-page-key crash: /now-playing
is a top-level route (lives outside the ShellRoute), but
/artists/:id and /albums/:id are shell-children. Pushing a shell-
child from a top-level route makes go_router attempt to mount a
second ShellRoute on top of the active one, leaving navigation in a
broken state. The mini player works because it's already inside the
shell.
Add an optional onBeforeNavigate callback to TrackActionsSheet
(forwarded through TrackActionsButton). When set, fires after
sheet.pop() and before context.push() of the destination route.
Wire the full player's TrackActionsButton with onBeforeNavigate:
() => Navigator.of(context).maybePop() so /now-playing dismisses
itself before the detail route is pushed. Result: clean navigation
into the destination, mini player visible underneath as expected.
Mini player keeps the default (no callback) since it's already in
the shell.
Three issues, all related to the player surface:
1. Player UI didn't update on track change. audio_handler's
_onCurrentIndexChanged only kicked off the cover load — it never
pushed the new MediaItem onto the mediaItem stream. Title/artist/
cover stayed pinned to whatever setQueueFromTracks(initialIndex:)
set on first play. Now the listener pushes queue[idx] when the
index changes.
2. Player kebab "Go to artist" 404'd while the same item from
MostPlayed worked. Same TrackActionsSheet for both, but the
player's _trackRefFromMediaItem was hardcoding artistId: ''
because audio_handler's _toMediaItem never stashed it in extras.
Stash artist_id alongside album_id; player_bar +
now_playing_screen read it back. Both kebabs now navigate.
3. "Start radio" didn't exist on Flutter even though the server has
/api/radio?seed_track=<id>. New RadioApi (lib/api/endpoints/
radio.dart) wraps the endpoint; PlayerActions.startRadio(trackId)
fetches + plays the result via the existing playTracks path.
New menu item between "Add to playlist" and the divider above
"Go to album", calls startRadio with a snackbar error fallback.
Two consumer updates left out of 8a6c926: like_button.dart and
track_actions_sheet.dart still referenced the removed
likedIdsProvider.notifier API. Switched to the new
likesControllerProvider.toggle().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modal bottom sheet listing the caller's user-created playlists (system
playlists like For-You are filtered out — they're not user-mutable).
Tap a row to pop with the playlist id; consumer (TrackActionsSheet)
then calls PlaylistsApi.appendTracks.
Empty-state copy: "You haven't created any playlists yet."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modal bottom sheet with five reason chips (mirroring server vocabulary
bad_rip / wrong_file / wrong_tags / duplicate / other) plus an optional
notes field. Returns (reason, notes) on Hide, null on Cancel. Default
selection is bad_rip.
Used by TrackActionsSheet's Hide action.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 3-dot trigger and the modal-bottom-sheet menu it opens. 7 actions:
Play next, Add to queue, Like/Unlike, Add to playlist, Go to album,
Go to artist, Hide/Unhide. hideQueueActions: true suppresses the
first two for the Now Playing surface.
Sub-sheets (HideTrackSheet, AddToPlaylistSheet) land in subsequent
commits — this commit imports them speculatively, so CI between this
and the next two commits will fail. Resolved by Tasks 5 + 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>