network addition
This commit is contained in:
parent
3bd713d667
commit
2099d3d64d
27 changed files with 2237 additions and 40 deletions
|
|
@ -105,11 +105,36 @@ class PlaybackState {
|
|||
}
|
||||
}
|
||||
|
||||
/// The command surface the UI drives, independent of *where* playback happens.
|
||||
///
|
||||
/// Implemented today only by [PlaybackController] (the local engine). Once
|
||||
/// cross-device control lands (updates-features.md #3), a `RemotePlaybackProxy`
|
||||
/// will also implement it, serializing each call into a LAN message to the
|
||||
/// device that is actually playing. The UI binds to this interface via
|
||||
/// `playbackCommandsProvider` so neither implementation leaks into widgets.
|
||||
abstract interface class PlaybackCommands {
|
||||
Future<void> playSongs(List<Song> songs, {int startIndex = 0});
|
||||
Future<void> jumpTo(int index);
|
||||
Future<void> playNext(Song song);
|
||||
Future<void> addToQueue(Song song);
|
||||
Future<void> removeAt(int index);
|
||||
Future<void> reorderQueue(int oldIndex, int newIndex);
|
||||
Future<void> togglePlayPause();
|
||||
Future<void> next();
|
||||
Future<void> previous();
|
||||
Future<void> seek(Duration position);
|
||||
Future<void> toggleShuffle();
|
||||
Future<void> cycleLoop();
|
||||
Future<void> retry();
|
||||
void resyncFromPlayer();
|
||||
}
|
||||
|
||||
/// Owns the just_audio player and translates Subsonic songs into a gapless
|
||||
/// queue. Playback methods no-op where audio is unsupported, but queue/current
|
||||
/// state and album-art accent extraction still run so the UI is fully alive on
|
||||
/// the Linux dev target.
|
||||
class PlaybackController extends StateNotifier<PlaybackState> {
|
||||
class PlaybackController extends StateNotifier<PlaybackState>
|
||||
implements PlaybackCommands {
|
||||
PlaybackController({
|
||||
required Uri? Function(Song) streamUriFor,
|
||||
required Uri? Function(Song) coverArtUriFor,
|
||||
|
|
@ -157,6 +182,11 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
static bool get _audioSupported =>
|
||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||
|
||||
/// The current state, for out-of-widget consumers (e.g. the remote host,
|
||||
/// which reads it to send a newly-connected remote an immediate snapshot).
|
||||
/// Widgets should watch `activePlaybackProvider` instead.
|
||||
PlaybackState get currentState => state;
|
||||
|
||||
void _wireStreams() {
|
||||
final player = _player!;
|
||||
player.currentIndexStream.listen((i) {
|
||||
|
|
@ -246,6 +276,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
/// into the player, so queue index == player source index 1:1. Every later
|
||||
/// queue mutation (`playNext` / `addToQueue` / `removeAt`) relies on that
|
||||
/// invariant to stay aligned.
|
||||
@override
|
||||
Future<void> playSongs(List<Song> songs, {int startIndex = 0}) async {
|
||||
if (songs.isEmpty) return;
|
||||
|
||||
|
|
@ -317,6 +348,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
/// Start playing the queue entry at [index] without rebuilding the queue —
|
||||
/// used by the queue list so tapping a row jumps to it and keeps the current
|
||||
/// (possibly shuffled) order intact.
|
||||
@override
|
||||
Future<void> jumpTo(int index) async {
|
||||
final q = state.queue;
|
||||
if (index < 0 || index >= q.length) return;
|
||||
|
|
@ -339,6 +371,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
|
||||
/// Insert [song] right after the current track (Timbre's "play next").
|
||||
/// Falls back to [playSongs] when nothing is playing.
|
||||
@override
|
||||
Future<void> playNext(Song song) async {
|
||||
if (_streamUriFor(song) == null) return;
|
||||
final q = state.queue;
|
||||
|
|
@ -353,6 +386,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
}
|
||||
|
||||
/// Append [song] to the end of the queue.
|
||||
@override
|
||||
Future<void> addToQueue(Song song) async {
|
||||
if (_streamUriFor(song) == null) return;
|
||||
final q = state.queue;
|
||||
|
|
@ -368,6 +402,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
/// playlist aligned. On mobile just_audio adjusts its own current index and
|
||||
/// re-emits `currentIndexStream`; the id dedupe in [_notifyCurrent] avoids a
|
||||
/// spurious re-scrobble when the playing track didn't actually change.
|
||||
@override
|
||||
Future<void> removeAt(int index) async {
|
||||
final q = state.queue;
|
||||
if (index < 0 || index >= q.length) return;
|
||||
|
|
@ -415,6 +450,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
/// among the shuffled tail; hitting shuffle afterwards re-randomizes as usual.
|
||||
/// A single targeted `moveAudioSource` keeps the current track playing without
|
||||
/// the re-buffer a full source rebuild ([_applyOrder]) would cause.
|
||||
@override
|
||||
Future<void> reorderQueue(int oldIndex, int newIndex) async {
|
||||
final q = state.queue;
|
||||
if (oldIndex < 0 || oldIndex >= q.length) return;
|
||||
|
|
@ -452,6 +488,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
await player.moveAudioSource(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> togglePlayPause() async {
|
||||
final player = _player;
|
||||
if (player == null) return;
|
||||
|
|
@ -462,10 +499,13 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> next() async => _player?.seekToNext();
|
||||
|
||||
@override
|
||||
Future<void> previous() async => _player?.seekToPrevious();
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) async => _player?.seek(position);
|
||||
|
||||
/// Toggle shuffle by physically reordering the queue.
|
||||
|
|
@ -478,6 +518,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
/// (and already-played history) in place; disabling restores the pre-shuffle
|
||||
/// order, dropping anything since removed and appending anything since added.
|
||||
/// The player's own shuffle mode is left off permanently.
|
||||
@override
|
||||
Future<void> toggleShuffle() async {
|
||||
final enabled = !state.shuffle;
|
||||
final q = state.queue;
|
||||
|
|
@ -587,6 +628,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
/// Re-attempt the current queue after an [PlaybackError] (e.g. the network
|
||||
/// came back). Rebuilds sources from the current queue — re-resolving each
|
||||
/// URI, so any now-available local download is preferred — and resumes.
|
||||
@override
|
||||
Future<void> retry() async {
|
||||
if (state.queue.isEmpty) return;
|
||||
try {
|
||||
|
|
@ -615,6 +657,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
/// frozen (often at 0:00) when the app returns to the foreground. Called from
|
||||
/// the app-lifecycle `resumed` hook. Reads only — never rebuilds sources or
|
||||
/// touches the queue order, so the queue-index==source-index invariant holds.
|
||||
@override
|
||||
void resyncFromPlayer() {
|
||||
final player = _player;
|
||||
if (player == null) return;
|
||||
|
|
@ -639,6 +682,7 @@ class PlaybackController extends StateNotifier<PlaybackState> {
|
|||
|
||||
/// Cycle off → all → one → off (Timbre's queue-loop toggle, extended with
|
||||
/// single-track repeat).
|
||||
@override
|
||||
Future<void> cycleLoop() async {
|
||||
final nextMode = switch (state.loop) {
|
||||
LoopMode.off => LoopMode.all,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue