mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
feat(player): add repeat off/all/one modes
The internal player handles setRepeatMode: repeat-one replays the current track, repeat-all wraps the queue (and keeps a single-track shuffle queue alive). The mode is broadcast in playback state, persisted with the playback session, and toggleable from the Now Playing transport row and the up-next sheet; a small shuffle toggle joins the transport row for symmetry.
This commit is contained in:
@@ -102,6 +102,9 @@ class MusicPlayerController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setRepeatMode(AudioServiceRepeatMode mode) async =>
|
||||
_handler?.setRepeatMode(mode);
|
||||
|
||||
Future<void> playNext(PlayableMedia item) async =>
|
||||
(await ensureInitialized())?.enqueue(item, playNext: true);
|
||||
|
||||
|
||||
@@ -662,6 +662,11 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
(s) => s.value?.shuffleMode == AudioServiceShuffleMode.all,
|
||||
),
|
||||
);
|
||||
final repeatMode = ref.watch(
|
||||
playbackStateProvider.select(
|
||||
(s) => s.value?.repeatMode ?? AudioServiceRepeatMode.none,
|
||||
),
|
||||
);
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Column(
|
||||
@@ -678,6 +683,33 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: switch (repeatMode) {
|
||||
AudioServiceRepeatMode.one =>
|
||||
context.l10n.nowPlayingRepeatOne,
|
||||
AudioServiceRepeatMode.none =>
|
||||
context.l10n.nowPlayingRepeatOff,
|
||||
_ => context.l10n.nowPlayingRepeatAll,
|
||||
},
|
||||
isSelected:
|
||||
repeatMode != AudioServiceRepeatMode.none,
|
||||
icon: Icon(
|
||||
repeatMode == AudioServiceRepeatMode.one
|
||||
? Icons.repeat_one
|
||||
: Icons.repeat,
|
||||
),
|
||||
color: repeatMode != AudioServiceRepeatMode.none
|
||||
? colorScheme.primary
|
||||
: null,
|
||||
onPressed: () =>
|
||||
controller.setRepeatMode(switch (repeatMode) {
|
||||
AudioServiceRepeatMode.none =>
|
||||
AudioServiceRepeatMode.all,
|
||||
AudioServiceRepeatMode.all =>
|
||||
AudioServiceRepeatMode.one,
|
||||
_ => AudioServiceRepeatMode.none,
|
||||
}),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: shuffleOn
|
||||
? context.l10n.nowPlayingShuffleOn
|
||||
@@ -846,6 +878,16 @@ class _PlaybackControls extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final position = ref.watch(playbackPositionProvider);
|
||||
final isPlaying = ref.watch(playbackPlayingProvider);
|
||||
final shuffleOn = ref.watch(
|
||||
playbackStateProvider.select(
|
||||
(s) => s.value?.shuffleMode == AudioServiceShuffleMode.all,
|
||||
),
|
||||
);
|
||||
final repeatMode = ref.watch(
|
||||
playbackStateProvider.select(
|
||||
(s) => s.value?.repeatMode ?? AudioServiceRepeatMode.none,
|
||||
),
|
||||
);
|
||||
final maxMs = duration.inMilliseconds > 0
|
||||
? duration.inMilliseconds.toDouble()
|
||||
: 1.0;
|
||||
@@ -918,6 +960,18 @@ class _PlaybackControls extends ConsumerWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
iconSize: 24,
|
||||
tooltip: shuffleOn
|
||||
? context.l10n.nowPlayingShuffleOn
|
||||
: context.l10n.nowPlayingPlayInOrder,
|
||||
color: shuffleOn
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
icon: const Icon(Icons.shuffle),
|
||||
onPressed: () => controller.setShuffle(!shuffleOn),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
iconSize: 44,
|
||||
icon: const Icon(Icons.skip_previous),
|
||||
@@ -943,6 +997,28 @@ class _PlaybackControls extends ConsumerWidget {
|
||||
icon: const Icon(Icons.skip_next),
|
||||
onPressed: controller.next,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
iconSize: 24,
|
||||
tooltip: switch (repeatMode) {
|
||||
AudioServiceRepeatMode.one => context.l10n.nowPlayingRepeatOne,
|
||||
AudioServiceRepeatMode.none => context.l10n.nowPlayingRepeatOff,
|
||||
_ => context.l10n.nowPlayingRepeatAll,
|
||||
},
|
||||
color: repeatMode == AudioServiceRepeatMode.none
|
||||
? colorScheme.onSurfaceVariant
|
||||
: colorScheme.primary,
|
||||
icon: Icon(
|
||||
repeatMode == AudioServiceRepeatMode.one
|
||||
? Icons.repeat_one
|
||||
: Icons.repeat,
|
||||
),
|
||||
onPressed: () => controller.setRepeatMode(switch (repeatMode) {
|
||||
AudioServiceRepeatMode.none => AudioServiceRepeatMode.all,
|
||||
AudioServiceRepeatMode.all => AudioServiceRepeatMode.one,
|
||||
_ => AudioServiceRepeatMode.none,
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -131,6 +131,7 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
bool _initialized = false;
|
||||
|
||||
bool _shuffle = false;
|
||||
AudioServiceRepeatMode _repeatMode = AudioServiceRepeatMode.none;
|
||||
final Random _random = Random();
|
||||
final List<int> _recent = [];
|
||||
final List<int> _playHistory = [];
|
||||
@@ -320,6 +321,7 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
shuffleMode: _shuffle
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
repeatMode: _repeatMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -489,6 +491,7 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
'index': _index,
|
||||
'positionMs': (position ?? Duration.zero).inMilliseconds,
|
||||
'shuffle': _shuffle,
|
||||
'repeat': _repeatMode.name,
|
||||
};
|
||||
unawaited(
|
||||
AppStateDatabase.instance.savePlaybackSession(session).catchError((
|
||||
@@ -515,6 +518,7 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
required int index,
|
||||
required Duration position,
|
||||
required bool shuffle,
|
||||
AudioServiceRepeatMode repeatMode = AudioServiceRepeatMode.none,
|
||||
}) async {
|
||||
if (items.isEmpty) return;
|
||||
// Never clobber a session the user already started this launch.
|
||||
@@ -529,6 +533,7 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
..addAll(items.map((m) => m.toMediaItem()));
|
||||
_index = index.clamp(0, items.length - 1);
|
||||
_shuffle = shuffle;
|
||||
_repeatMode = repeatMode;
|
||||
_pendingRestorePosition = position > Duration.zero ? position : null;
|
||||
queue.add(List<MediaItem>.unmodifiable(_queueItems));
|
||||
mediaItem.add(_media[_index].toMediaItem());
|
||||
@@ -774,9 +779,18 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
}
|
||||
|
||||
Future<void> _onComplete() async {
|
||||
if (_repeatMode == AudioServiceRepeatMode.one &&
|
||||
_index >= 0 &&
|
||||
_index < _media.length) {
|
||||
await _playIndex(_index, recordHistory: false);
|
||||
return;
|
||||
}
|
||||
if (_shuffle) {
|
||||
if (_media.length > 1) {
|
||||
await _playIndex(_pickNextShuffle());
|
||||
} else if (_repeatMode == AudioServiceRepeatMode.all &&
|
||||
_media.isNotEmpty) {
|
||||
await _playIndex(_index, recordHistory: false);
|
||||
} else {
|
||||
_broadcastState(playerState: PlayerState.completed);
|
||||
}
|
||||
@@ -784,6 +798,9 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
}
|
||||
if (_index >= 0 && _index < _media.length - 1) {
|
||||
await _playIndex(_index + 1);
|
||||
} else if (_repeatMode == AudioServiceRepeatMode.all &&
|
||||
_media.isNotEmpty) {
|
||||
await _playIndex(0);
|
||||
} else {
|
||||
_broadcastState(playerState: PlayerState.completed);
|
||||
}
|
||||
@@ -854,6 +871,18 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
||||
// Group repeat has no meaning for a flat queue; treat it as all.
|
||||
_repeatMode = repeatMode == AudioServiceRepeatMode.group
|
||||
? AudioServiceRepeatMode.all
|
||||
: repeatMode;
|
||||
_broadcastState();
|
||||
if (_media.isNotEmpty && _index >= 0) {
|
||||
_persistSession(position: playbackState.value.position);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
_playRequestGeneration++;
|
||||
@@ -1114,6 +1143,10 @@ Future<void> restorePersistedPlaybackSession() async {
|
||||
index: index,
|
||||
position: position,
|
||||
shuffle: session['shuffle'] == true,
|
||||
repeatMode: AudioServiceRepeatMode.values.firstWhere(
|
||||
(mode) => mode.name == session['repeat'],
|
||||
orElse: () => AudioServiceRepeatMode.none,
|
||||
),
|
||||
);
|
||||
_log.i(
|
||||
'Restored playback session: ${items.length} track(s), paused at '
|
||||
|
||||
Reference in New Issue
Block a user