feat(flutter): full parity for player bar — shuffle/repeat/volume + Like + kebab + 3-column layout (#356)
Mirrors web's compact PlayerBar both visually (3-column top + full- width seek bottom) and functionally — closes parity gaps that had been hidden because the prior mini-bar surfaced none of these controls. ### audio_handler.dart - Override `setShuffleMode` → delegates to just_audio's `setShuffleModeEnabled`. - Override `setRepeatMode` → maps audio_service modes (none/one/all/ group) to just_audio's LoopMode (off/one/all). - New `setVolume(double)` + `volumeStream` getter (audio_service's PlaybackState doesn't carry volume — surface it separately). - `_broadcastState` now populates PlaybackState's shuffleMode + repeatMode fields, and re-fires on shuffle/repeat/loop stream changes so UI subscribers stay in sync. ### player_provider.dart - New `volumeProvider` (StreamProvider<double>). - `PlayerActions` gains `toggleShuffle()`, `cycleRepeat()` (none → all → one → none), and `setVolume(double)`. ### player_bar.dart Complete rebuild matching the operator-designed layout: ┌──────────┬───────────┬──────────────────┐ │ art │ ♥ ⋮ │ 🔀 🔁 ☰ │ ← short row │ title │ │ │ │ artist │ ⏮ ⏯ ⏭ │ volume slider │ ← play row, full size ├──────────┴───────────┴──────────────────┤ │ 0:42 ━━━━━●━━━━━━━━━━━━━━━━━━━━━ 3:21 │ └─────────────────────────────────────────┘ - Column 1 (35%): cover + title + artist. Tap → /now-playing. - Column 2 (intrinsic): like + kebab on top, play controls (40/48/40) on bottom. - Column 3 (30%): shuffle/repeat/queue on top, volume slider on bottom. - Bottom: full-width seek slider with mm:ss labels. `TrackActionsButton` reused for the kebab; reconstructs a minimal TrackRef from MediaItem extras (same pattern NowPlayingScreen already uses). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,10 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
||||
MinstrelAudioHandler() {
|
||||
_player.playbackEventStream.listen(_broadcastState);
|
||||
_player.currentIndexStream.listen(_onCurrentIndexChanged);
|
||||
// Re-broadcast on shuffle/repeat changes so the PlaybackState's
|
||||
// shuffleMode + repeatMode fields stay current for UI subscribers.
|
||||
_player.shuffleModeEnabledStream.listen((_) => _broadcastState(null));
|
||||
_player.loopModeStream.listen((_) => _broadcastState(null));
|
||||
}
|
||||
|
||||
final AudioPlayer _player = AudioPlayer();
|
||||
@@ -22,6 +26,11 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
||||
AlbumCoverCache? _coverCache;
|
||||
AudioCacheManager? _audioCacheManager;
|
||||
|
||||
/// Volume stream for UI subscribers. Mirrors the just_audio player's
|
||||
/// volume directly; set via setVolume(double).
|
||||
Stream<double> get volumeStream => _player.volumeStream;
|
||||
double get volume => _player.volume;
|
||||
|
||||
void configure({
|
||||
required String baseUrl,
|
||||
required String? token,
|
||||
@@ -198,7 +207,34 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
||||
@override
|
||||
Future<void> skipToPrevious() => _player.seekToPrevious();
|
||||
|
||||
void _broadcastState(PlaybackEvent event) {
|
||||
@override
|
||||
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
||||
await _player
|
||||
.setShuffleModeEnabled(shuffleMode != AudioServiceShuffleMode.none);
|
||||
// _broadcastState picks up the change via shuffleModeEnabledStream.
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
||||
final loop = switch (repeatMode) {
|
||||
AudioServiceRepeatMode.none => LoopMode.off,
|
||||
AudioServiceRepeatMode.one => LoopMode.one,
|
||||
AudioServiceRepeatMode.all || AudioServiceRepeatMode.group => LoopMode.all,
|
||||
};
|
||||
await _player.setLoopMode(loop);
|
||||
}
|
||||
|
||||
/// Sets player volume in [0.0, 1.0]. Note: most mobile browsers tie
|
||||
/// page audio to system volume — this is the in-app slider for parity
|
||||
/// with desktop/web; on mobile the system volume is the real control.
|
||||
Future<void> setVolume(double v) async {
|
||||
await _player.setVolume(v.clamp(0.0, 1.0));
|
||||
}
|
||||
|
||||
// _broadcastState accepts a nullable event because the shuffle/repeat
|
||||
// listeners don't have one — we just want to re-emit PlaybackState
|
||||
// with up-to-date shuffleMode/repeatMode fields.
|
||||
void _broadcastState(PlaybackEvent? event) {
|
||||
final playing = _player.playing;
|
||||
playbackState.add(PlaybackState(
|
||||
controls: [
|
||||
@@ -218,7 +254,15 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
||||
updatePosition: _player.position,
|
||||
bufferedPosition: _player.bufferedPosition,
|
||||
speed: _player.speed,
|
||||
queueIndex: event.currentIndex,
|
||||
queueIndex: event?.currentIndex ?? _player.currentIndex,
|
||||
shuffleMode: _player.shuffleModeEnabled
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
repeatMode: switch (_player.loopMode) {
|
||||
LoopMode.off => AudioServiceRepeatMode.none,
|
||||
LoopMode.one => AudioServiceRepeatMode.one,
|
||||
LoopMode.all => AudioServiceRepeatMode.all,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user