import 'package:flutter/foundation.dart'; import '../../media/lyrics.dart'; import '../../media/media_item.dart'; /// Repeat behavior of the music queue. enum MusicRepeatMode { off, all, one } /// Coarse playback state of the music session. enum MusicPlaybackStatus { idle, loading, playing, paused, error } /// What kind of container playback was started from. The player keeps /// artist/playlist/mix provenance stable, while album and ad-hoc queues use /// the active track's album for the "Playing from …" line. enum MusicPlayContextKind { album, artist, playlist, mix, tracks } /// Provenance of the current queue (album/artist/playlist/instant mix). class MusicPlayContext { /// Backend id of the source container, when it has one (instant mixes /// don't). final String? id; /// Display title of the session source. Used directly for stable /// artist/playlist/mix provenance labels. final String title; final MusicPlayContextKind kind; const MusicPlayContext({this.id, required this.title, required this.kind}); } /// Backend-neutral music playback session: owns the audio `Player`, the /// queue (shuffle/repeat), OS media-session feed, and progress reporting. /// /// UI consumes this via `context.watch()` — it is /// registered per profile session (see `profile_session_screen.dart`) so a /// profile switch tears the session down. [notifyListeners] fires only on /// discrete changes (track, status, queue shape, modes) — progress bars /// subscribe to [positionStream] instead. abstract class MusicPlaybackService extends ChangeNotifier { /// False on the stub — playback affordances should render disabled or /// fall back to a "not supported yet" notice. bool get isAvailable; MediaItem? get currentTrack; MusicPlaybackStatus get status; bool get isPlaying => status == MusicPlaybackStatus.playing; Duration? get duration; Duration get position; Stream get positionStream; /// Full queue in playback order (shuffle already applied). List get queue; /// Index of [currentTrack] within [queue]; -1 when idle. int get currentIndex; MusicPlayContext? get playContext; bool get shuffled; MusicRepeatMode get repeatMode; /// Playback failures the UI should surface (snackbar); the service already /// handles recovery (skip / stop) itself. Stream get errors; /// Claims the latest user intent to replace playback after asynchronous /// queue construction. Callers must check [isPlayIntentCurrent] before /// committing fetched tracks. int beginPlayIntent(); /// Whether [intent] is still the latest playback-replacement request. bool isPlayIntentCurrent(int intent); /// Changes whenever a queue session starts or stops. Asynchronous enqueue /// actions use this to avoid appending fetched tracks to a newer session. int get queueSessionRevision; /// Start a new queue from [tracks], optionally at [startTrack] (defaults /// to the first track). [shuffle] shuffles with the start track anchored /// first. Future playFromList({ required List tracks, MediaItem? startTrack, required MusicPlayContext playContext, bool shuffle = false, }); /// Fetch an instant mix seeded from [seed] and play it. Future playInstantMix(MediaItem seed); Future play(); Future pause(); Future togglePlayPause(); /// Advance to the next queue entry (respecting repeat mode). Future next(); /// Restart the current track when more than a few seconds in, otherwise /// step to the previous queue entry. Future previous(); Future seek(Duration position); /// Music playback volume, 0–100. Preview updates are exposed separately so /// a slider does not notify every service consumer on each drag event. double get volume; ValueListenable get volumeListenable; Future setVolume(double volume, {bool persist = true}); void setRepeatMode(MusicRepeatMode mode); void toggleShuffle(); /// Jump playback to queue index [index]. Future jumpTo(int index); void removeAt(int index); void reorder(int from, int to); /// Insert after the current track. void addNext(List tracks); void addToEnd(List tracks); /// Drop everything after the current track. void clearUpcoming(); /// Stop playback and clear the session (mini-player disappears). Future stop(); /// Whether a sleep timer (timed or end-of-track) is armed. bool get sleepTimerActive; /// When the timed sleep timer fires; null in end-of-track mode or when /// inactive. DateTime? get sleepTimerEndsAt; /// The duration the timed sleep timer was armed with (for marking the /// chosen preset); null in end-of-track mode or when inactive. Duration? get sleepTimerDuration; /// Whether the sleep timer pauses at the end of the current track instead /// of after a fixed duration. bool get sleepTimerEndOfTrack; /// Arm the sleep timer: a fixed [duration], or [endOfTrack] to pause when /// the current track finishes. Pass `null` with `endOfTrack: false` to /// cancel. Fires as a pause (session stays); cancelled by [stop]. void setSleepTimer(Duration? duration, {bool endOfTrack = false}); /// Lyrics for [track] (defaults to the current track's backend). Delegates /// to `MediaServerClient.fetchLyrics`; null = none available. Future fetchLyrics(MediaItem track); } /// No-op base for test doubles, which override only the members under test. /// Production always binds `MusicPlaybackServiceImpl`. class StubMusicPlaybackService extends MusicPlaybackService { final ValueNotifier _volumeNotifier = ValueNotifier(100); int _playIntentGeneration = 0; int _queueSessionRevision = 0; @override bool get isAvailable => false; @override MediaItem? get currentTrack => null; @override MusicPlaybackStatus get status => MusicPlaybackStatus.idle; @override Duration? get duration => null; @override Duration get position => Duration.zero; @override Stream get positionStream => const Stream.empty(); @override List get queue => const []; @override int get currentIndex => -1; @override MusicPlayContext? get playContext => null; @override bool get shuffled => false; @override MusicRepeatMode get repeatMode => MusicRepeatMode.off; @override Stream get errors => const Stream.empty(); @override int beginPlayIntent() => ++_playIntentGeneration; @override bool isPlayIntentCurrent(int intent) => intent == _playIntentGeneration; @override int get queueSessionRevision => _queueSessionRevision; @override Future playFromList({ required List tracks, MediaItem? startTrack, required MusicPlayContext playContext, bool shuffle = false, }) async { beginPlayIntent(); _queueSessionRevision++; } @override Future playInstantMix(MediaItem seed) async {} @override Future play() async {} @override Future pause() async {} @override Future togglePlayPause() async {} @override Future next() async {} @override Future previous() async {} @override Future seek(Duration position) async {} @override double get volume => 100; @override ValueListenable get volumeListenable => _volumeNotifier; @override Future setVolume(double volume, {bool persist = true}) async {} @override void setRepeatMode(MusicRepeatMode mode) {} @override void toggleShuffle() {} @override Future jumpTo(int index) async {} @override void removeAt(int index) {} @override void reorder(int from, int to) {} @override void addNext(List tracks) {} @override void addToEnd(List tracks) {} @override void clearUpcoming() {} @override Future stop() async { beginPlayIntent(); _queueSessionRevision++; } @override bool get sleepTimerActive => false; @override DateTime? get sleepTimerEndsAt => null; @override Duration? get sleepTimerDuration => null; @override bool get sleepTimerEndOfTrack => false; @override void setSleepTimer(Duration? duration, {bool endOfTrack = false}) {} @override Future fetchLyrics(MediaItem track) async => null; @override void dispose() { _volumeNotifier.dispose(); super.dispose(); } }