fix(flutter): player updates on track change + kebab artist nav + Start radio

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.
This commit is contained in:
2026-05-11 12:32:43 -04:00
parent 2299824ad9
commit 4dbb3190ff
6 changed files with 85 additions and 12 deletions
+27 -10
View File
@@ -165,19 +165,36 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
queue.add([...queue.value, item]);
}
MediaItem _toMediaItem(TrackRef t) => MediaItem(
id: t.id,
title: t.title,
artist: t.artistName,
album: t.albumTitle,
duration: Duration(seconds: t.durationSec),
// Stash album_id in extras so _loadArtForCurrentItem can pull it
// back without re-walking the track list.
extras: t.albumId.isEmpty ? null : {'album_id': t.albumId},
);
MediaItem _toMediaItem(TrackRef t) {
// Stash album_id + artist_id in extras so widgets reconstructing
// a TrackRef from the MediaItem (player kebab → "Go to artist",
// "Go to album") have the IDs they need to navigate. Earlier code
// only carried album_id which left "Go to artist" pushing
// /artists/ (empty id, route 404).
final extras = <String, dynamic>{};
if (t.albumId.isNotEmpty) extras['album_id'] = t.albumId;
if (t.artistId.isNotEmpty) extras['artist_id'] = t.artistId;
return MediaItem(
id: t.id,
title: t.title,
artist: t.artistName,
album: t.albumTitle,
duration: Duration(seconds: t.durationSec),
extras: extras.isEmpty ? null : extras,
);
}
void _onCurrentIndexChanged(int? idx) {
if (idx == null) return;
// Push the new track's MediaItem onto the mediaItem stream so
// the player UI rebuilds with the new title/artist/album/cover.
// Without this, the bar and full player stayed pinned to whichever
// track was passed via setQueueFromTracks(initialIndex:) regardless
// of skip/auto-advance.
final items = queue.value;
if (idx >= 0 && idx < items.length) {
mediaItem.add(items[idx]);
}
unawaited(_loadArtForCurrentItem());
}
@@ -235,7 +235,7 @@ class _TitleRow extends StatelessWidget {
title: media.title,
albumId: (media.extras?['album_id'] as String?) ?? '',
albumTitle: media.album ?? '',
artistId: '',
artistId: (media.extras?['artist_id'] as String?) ?? '',
artistName: media.artist ?? '',
durationSec: media.duration?.inSeconds ?? 0,
streamUrl: '',
+3 -1
View File
@@ -286,7 +286,9 @@ TrackRef _trackRefFromMediaItem(MediaItem media) => TrackRef(
title: media.title,
albumId: (media.extras?['album_id'] as String?) ?? '',
albumTitle: media.album ?? '',
artistId: '',
// artist_id is stashed in extras by audio_handler — without it
// the kebab's "Go to artist" pushes /artists/ (empty id) and 404s.
artistId: (media.extras?['artist_id'] as String?) ?? '',
artistName: media.artist ?? '',
durationSec: media.duration?.inSeconds ?? 0,
streamUrl: '',
@@ -1,6 +1,7 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api/endpoints/radio.dart';
import '../auth/auth_provider.dart';
import '../cache/audio_cache_manager.dart';
import '../library/library_providers.dart' show dioProvider;
@@ -102,6 +103,15 @@ class PlayerActions {
Future<void> setVolume(double v) async {
await _ref.read(audioHandlerProvider).setVolume(v);
}
/// Fetches /api/radio?seed_track=<id> and starts playing the
/// returned track list (seed at index 0 + recommended picks).
Future<void> startRadio(String trackId) async {
final dio = await _ref.read(dioProvider.future);
final tracks = await RadioApi(dio).seedTrack(trackId);
if (tracks.isEmpty) return;
await playTracks(tracks);
}
}
final playerActionsProvider = Provider<PlayerActions>((ref) => PlayerActions(ref));