mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
700 lines
20 KiB
Dart
700 lines
20 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:drift/drift.dart' show Value;
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:just_audio/just_audio.dart';
|
|
import 'package:pool/pool.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:rxdart/rxdart.dart';
|
|
import 'package:synchronized/synchronized.dart';
|
|
|
|
import '../cache/image_cache.dart';
|
|
import '../database/database.dart';
|
|
import '../log.dart';
|
|
import '../models/music.dart';
|
|
import '../models/query.dart';
|
|
import '../models/support.dart';
|
|
import '../sources/music_source.dart';
|
|
import '../state/settings.dart';
|
|
import 'cache_service.dart';
|
|
import 'settings_service.dart';
|
|
|
|
part 'audio_service.g.dart';
|
|
|
|
class QueueSourceItem {
|
|
final MediaItem mediaItem;
|
|
final UriAudioSource audioSource;
|
|
final QueueData queueData;
|
|
|
|
const QueueSourceItem({
|
|
required this.mediaItem,
|
|
required this.audioSource,
|
|
required this.queueData,
|
|
});
|
|
}
|
|
|
|
class QueueSlice {
|
|
final QueueSourceItem? prev;
|
|
final QueueSourceItem? current;
|
|
final QueueSourceItem? next;
|
|
|
|
const QueueSlice(this.prev, this.current, this.next);
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
FutureOr<AudioControl> audioControlInit(AudioControlInitRef ref) async {
|
|
final imageCache = ref.watch(imageCacheProvider);
|
|
|
|
return AudioService.init(
|
|
builder: () => AudioControl(AudioPlayer(), ref),
|
|
config: const AudioServiceConfig(
|
|
androidNotificationChannelId: 'com.subtracks2.channel.audio',
|
|
androidNotificationChannelName: 'Music playback',
|
|
androidNotificationIcon: 'drawable/ic_stat_name',
|
|
),
|
|
cacheManager: imageCache,
|
|
cacheKeyResolver: (mediaItem) =>
|
|
mediaItem.data.artCache?.thumbnailArtCacheKey ?? '',
|
|
);
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
AudioControl audioControl(AudioControlRef ref) {
|
|
return ref.watch(audioControlInitProvider).requireValue;
|
|
}
|
|
|
|
class AudioControl extends BaseAudioHandler with QueueHandler, SeekHandler {
|
|
static const radioLength = 10;
|
|
|
|
Stream<Duration> get position => _player.positionStream;
|
|
|
|
BehaviorSubject<QueueMode> queueMode = BehaviorSubject.seeded(QueueMode.user);
|
|
BehaviorSubject<List<int>?> shuffleIndicies = BehaviorSubject.seeded(null);
|
|
BehaviorSubject<AudioServiceRepeatMode> repeatMode =
|
|
BehaviorSubject.seeded(AudioServiceRepeatMode.none);
|
|
|
|
final Ref _ref;
|
|
final AudioPlayer _player;
|
|
|
|
int? _queueLength;
|
|
int? _previousCurrentTrackIndex;
|
|
|
|
final _syncPool = Pool(1);
|
|
final _playLock = Lock();
|
|
final _currentIndexIgnore = <int?>[null, 0];
|
|
|
|
final ConcatenatingAudioSource _audioSource =
|
|
ConcatenatingAudioSource(children: []);
|
|
|
|
SubtracksDatabase get _db => _ref.read(databaseProvider);
|
|
CacheService get _cache => _ref.read(cacheServiceProvider);
|
|
MusicSource get _source => _ref.read(musicSourceProvider);
|
|
int get _sourceId => _ref.read(sourceIdProvider);
|
|
|
|
AudioControl(this._player, this._ref) {
|
|
_player.playbackEventStream.listen((PlaybackEvent event) {
|
|
final playing = _player.playing;
|
|
playbackState.add(playbackState.value.copyWith(
|
|
controls: [
|
|
MediaControl.skipToPrevious,
|
|
if (playing) MediaControl.pause else MediaControl.play,
|
|
MediaControl.stop,
|
|
MediaControl.skipToNext,
|
|
],
|
|
systemActions: const {
|
|
MediaAction.seek,
|
|
},
|
|
androidCompactActionIndices: const [0, 1, 3],
|
|
processingState: const {
|
|
ProcessingState.idle: AudioProcessingState.idle,
|
|
ProcessingState.loading: AudioProcessingState.loading,
|
|
ProcessingState.buffering: AudioProcessingState.buffering,
|
|
ProcessingState.ready: AudioProcessingState.ready,
|
|
ProcessingState.completed: AudioProcessingState.completed,
|
|
}[_player.processingState]!,
|
|
playing: playing,
|
|
updatePosition: _player.position,
|
|
bufferedPosition: _player.bufferedPosition,
|
|
queueIndex: event.currentIndex,
|
|
));
|
|
});
|
|
|
|
shuffleIndicies.listen((value) {
|
|
playbackState.add(playbackState.value.copyWith(
|
|
shuffleMode: value != null
|
|
? AudioServiceShuffleMode.all
|
|
: AudioServiceShuffleMode.none,
|
|
));
|
|
});
|
|
|
|
repeatMode.listen((value) {
|
|
playbackState.add(playbackState.value.copyWith(repeatMode: value));
|
|
});
|
|
|
|
_player.processingStateStream.listen((event) async {
|
|
if (event == ProcessingState.completed) {
|
|
if (_audioSource.length > 0) {
|
|
log.fine('completed');
|
|
await stop();
|
|
await seek(Duration.zero);
|
|
}
|
|
}
|
|
});
|
|
|
|
_ref.listen(sourceIdProvider, (_, __) async {
|
|
await _clearAudioSource(true);
|
|
await _db.clearQueue();
|
|
});
|
|
|
|
_ref.listen(maxBitrateProvider, (prev, next) async {
|
|
if (prev?.valueOrNull != next.valueOrNull) {
|
|
await _resyncQueue(true);
|
|
}
|
|
});
|
|
|
|
_ref.listen(
|
|
settingsServiceProvider.select((value) => value.app.streamFormat),
|
|
(prev, next) async {
|
|
await _resyncQueue(true);
|
|
});
|
|
|
|
_player.durationStream.listen((duration) {
|
|
if (mediaItem.valueOrNull == null) return;
|
|
|
|
final index = queue.valueOrNull?.indexOf(mediaItem.value!);
|
|
|
|
final updated = mediaItem.value!.copyWith(duration: duration);
|
|
mediaItem.add(updated);
|
|
|
|
if (index != null) {
|
|
queue.add(queue.value..replaceRange(index, index + 1, [updated]));
|
|
}
|
|
});
|
|
|
|
_player.currentIndexStream.listen((index) async {
|
|
if (_currentIndexIgnore.contains(index)) {
|
|
_currentIndexIgnore.remove(index);
|
|
return;
|
|
}
|
|
if (index == null || index >= _audioSource.sequence.length) return;
|
|
|
|
final queueIndex = _audioSource.sequence[index].tag;
|
|
if (queueIndex != null) {
|
|
await _db.setCurrentTrack(queueIndex);
|
|
}
|
|
});
|
|
|
|
_db.currentTrackIndex().watchSingleOrNull().listen((index) {
|
|
// distict() except for when in loop one mode
|
|
if (repeatMode.value != AudioServiceRepeatMode.one &&
|
|
_previousCurrentTrackIndex == index) {
|
|
return;
|
|
}
|
|
|
|
_previousCurrentTrackIndex = index;
|
|
_syncPool.withResource(() => _syncQueue(index));
|
|
});
|
|
}
|
|
|
|
Future<void> init() async {
|
|
await _player.setAudioSource(_audioSource, preload: false);
|
|
|
|
final last = await _db.getLastAudioState().getSingleOrNull();
|
|
if (last == null) return;
|
|
|
|
_queueLength = await _db.queueLength().getSingleOrNull();
|
|
final repeat = {
|
|
RepeatMode.none: AudioServiceRepeatMode.none,
|
|
RepeatMode.all: AudioServiceRepeatMode.all,
|
|
RepeatMode.one: AudioServiceRepeatMode.one,
|
|
}[last.repeat]!;
|
|
repeatMode.add(repeat);
|
|
await repeatMode.firstWhere((e) => e == repeat);
|
|
|
|
queueMode.add(last.queueMode);
|
|
await queueMode.firstWhere((e) => e == last.queueMode);
|
|
|
|
if (last.shuffleIndicies != null) {
|
|
final indicies = last.shuffleIndicies!.unlock;
|
|
shuffleIndicies.add(indicies);
|
|
await shuffleIndicies.firstWhere((e) => e == indicies);
|
|
}
|
|
|
|
final startIndex = await _db.currentTrackIndex().getSingleOrNull();
|
|
|
|
if (startIndex != null && _queueLength != null) {
|
|
await _preparePlayer(startIndex);
|
|
}
|
|
}
|
|
|
|
Future<void> playSongs({
|
|
QueueMode mode = QueueMode.user,
|
|
required QueueContextType context,
|
|
String? contextId,
|
|
required ListQuery query,
|
|
required FutureOr<Iterable<Song>> Function(ListQuery query) getSongs,
|
|
int? startIndex,
|
|
bool? shuffle,
|
|
}) async {
|
|
if (!_playLock.locked) {
|
|
return _playLock.synchronized(
|
|
() => _playSongs(
|
|
mode: mode,
|
|
context: context,
|
|
contextId: contextId,
|
|
query: query,
|
|
getSongs: getSongs,
|
|
startIndex: startIndex,
|
|
shuffle: shuffle,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> playRadio({
|
|
required QueueContextType context,
|
|
String? contextId,
|
|
ListQuery query = const ListQuery(),
|
|
required FutureOr<Iterable<Song>> Function(ListQuery query) getSongs,
|
|
}) async {
|
|
await playSongs(
|
|
mode: QueueMode.radio,
|
|
context: QueueContextType.library,
|
|
contextId: contextId,
|
|
query: query.copyWith(
|
|
sort: SortBy(
|
|
column: 'SIN(songs.ROWID + ${Random().nextInt(10000)})',
|
|
),
|
|
),
|
|
getSongs: getSongs,
|
|
startIndex: 0,
|
|
);
|
|
}
|
|
|
|
Future<void> _playSongs({
|
|
QueueMode mode = QueueMode.user,
|
|
required QueueContextType context,
|
|
String? contextId,
|
|
required ListQuery query,
|
|
required FutureOr<Iterable<Song>> Function(ListQuery query) getSongs,
|
|
int? startIndex,
|
|
bool? shuffle,
|
|
}) async {
|
|
shuffle = shuffle ?? shuffleIndicies.valueOrNull != null;
|
|
queueMode.add(mode);
|
|
|
|
if (mode == QueueMode.radio) {
|
|
if (repeatMode.value != AudioServiceRepeatMode.none) {
|
|
await _loop(AudioServiceRepeatMode.none);
|
|
}
|
|
if (shuffleIndicies.value != null) {
|
|
await _shuffle(unshuffle: true);
|
|
}
|
|
}
|
|
|
|
await _clearAudioSource();
|
|
|
|
const limit = 500;
|
|
_queueLength = 0;
|
|
|
|
if ((startIndex == null || startIndex >= limit) && shuffle) {
|
|
startIndex = Random().nextInt(limit);
|
|
} else {
|
|
startIndex ??= 0;
|
|
}
|
|
|
|
// clear the queue and load the initial songs only up to the startIndex
|
|
await _db.transaction(() async {
|
|
await _db.clearQueue();
|
|
|
|
while (_queueLength! <= startIndex!) {
|
|
final songs = await getSongs(query.copyWith(
|
|
page: Pagination(limit: limit, offset: _queueLength!),
|
|
));
|
|
await _loadQueueSongs(songs, _queueLength!, context, contextId);
|
|
if (songs.length < limit) {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// if there are less songs than the limit and we're shuffling,
|
|
// choose a new random startIndex
|
|
if (startIndex >= _queueLength!) {
|
|
startIndex = Random().nextInt(_queueLength!);
|
|
}
|
|
|
|
await _preparePlayer(startIndex, shuffle);
|
|
await _db.setCurrentTrack(startIndex);
|
|
play();
|
|
|
|
const maxLength = 10000;
|
|
|
|
// no need to do extra loading if we've already loaded everything
|
|
if (_queueLength! < limit) return;
|
|
while (true) {
|
|
final songs = await getSongs(query.copyWith(
|
|
page: Pagination(limit: limit, offset: _queueLength!),
|
|
));
|
|
await _loadQueueSongs(songs, _queueLength!, context, contextId);
|
|
if (songs.length < limit || _queueLength! >= maxLength) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _loadQueueSongs(
|
|
Iterable<Song> songs,
|
|
int total,
|
|
QueueContextType context,
|
|
String? contextId,
|
|
) async {
|
|
await _db.insertQueue(songs.mapIndexed(
|
|
(i, song) => QueueCompanion.insert(
|
|
index: Value(i + (_queueLength ?? 0)),
|
|
sourceId: song.sourceId,
|
|
id: song.id,
|
|
context: context,
|
|
contextId: Value(contextId),
|
|
),
|
|
));
|
|
|
|
_queueLength = (_queueLength ?? 0) + songs.length;
|
|
if (shuffleIndicies.valueOrNull != null) {
|
|
await _generateShuffleIndicies(startIndex: _player.currentIndex);
|
|
}
|
|
}
|
|
|
|
Future<void> _preparePlayer(int startIndex, [bool? shuffle]) async {
|
|
if (shuffle == true) {
|
|
await _shuffle(startIndex: startIndex);
|
|
} else if (shuffle == false) {
|
|
await _shuffle(unshuffle: true);
|
|
}
|
|
|
|
final slice = await _getQueueSlice(startIndex);
|
|
if (slice == null) {
|
|
throw StateError('Could not get queue slice!');
|
|
}
|
|
|
|
final list =
|
|
[slice.prev, slice.current, slice.next].whereNotNull().toList();
|
|
|
|
mediaItem.add(slice.current!.mediaItem);
|
|
queue.add(list.map((e) => e.mediaItem).toList());
|
|
|
|
log.fine('addAll');
|
|
await _audioSource.addAll(list.map((e) => e.audioSource).toList());
|
|
await _player.seek(Duration.zero, index: list.indexOf(slice.current!));
|
|
}
|
|
|
|
Future<void> _syncQueue(int? index) async {
|
|
if (index == null || _queueLength == null) return;
|
|
|
|
final slice = await _getQueueSlice(index);
|
|
if (slice == null || slice.current == null) return;
|
|
|
|
mediaItem.add(slice.current!.mediaItem);
|
|
queue.add(
|
|
[slice.prev, slice.current, slice.next]
|
|
.map((e) => e?.mediaItem)
|
|
.whereNotNull()
|
|
.toList(),
|
|
);
|
|
|
|
final sourceIndex = _player.currentIndex;
|
|
final sourceNeedsNext = sourceIndex == _audioSource.length - 1;
|
|
final sourceNeedsPrev = sourceIndex == 0;
|
|
|
|
if (sourceNeedsNext && slice.next != null) {
|
|
log.fine('add');
|
|
await _audioSource.add(slice.next!.audioSource);
|
|
}
|
|
if (sourceNeedsPrev && slice.prev != null) {
|
|
await _insertFirstAudioSource(slice.prev!.audioSource);
|
|
}
|
|
}
|
|
|
|
Future<void> _loop(AudioServiceRepeatMode mode) async {
|
|
repeatMode.add(mode);
|
|
await repeatMode.firstWhere((e) => e == mode);
|
|
|
|
await _resyncQueue();
|
|
}
|
|
|
|
Future<void> _generateShuffleIndicies({
|
|
bool unshuffle = false,
|
|
int? startIndex,
|
|
}) async {
|
|
final indicies = unshuffle
|
|
? null
|
|
: (List.generate(_queueLength!, (i) => i + 1)
|
|
..insert(0, 0)
|
|
..removeLast()
|
|
..shuffle());
|
|
|
|
if (indicies != null && startIndex != null) {
|
|
indicies.removeAt(indicies.indexOf(startIndex));
|
|
indicies.insert(0, startIndex);
|
|
}
|
|
shuffleIndicies.add(indicies);
|
|
await shuffleIndicies.firstWhere((e) => e == indicies);
|
|
}
|
|
|
|
Future<void> _shuffle({bool unshuffle = false, int? startIndex}) async {
|
|
await _generateShuffleIndicies(
|
|
unshuffle: unshuffle,
|
|
startIndex: startIndex,
|
|
);
|
|
|
|
await _resyncQueue();
|
|
}
|
|
|
|
Future<void> _resyncQueue([bool reloadCurrent = false]) async {
|
|
return _syncPool.withResource(() async {
|
|
final currentSource =
|
|
_player.sequenceState?.currentSource as UriAudioSource?;
|
|
if (currentSource == null) return;
|
|
|
|
final currentSourceIndex = _player.sequence!.indexOf(currentSource);
|
|
await _pruneAudioSources(currentSourceIndex);
|
|
|
|
if (reloadCurrent && !currentSource.uri.isScheme('file')) {
|
|
final position = _player.position;
|
|
|
|
final item = (await _getQueueItems([currentSource.tag]))[0];
|
|
|
|
await _audioSource.clear();
|
|
await _audioSource.add(item.audioSource);
|
|
await seek(position);
|
|
}
|
|
|
|
await _syncQueue(currentSource.tag);
|
|
});
|
|
}
|
|
|
|
int _realIndex(int index) {
|
|
if (shuffleIndicies.valueOrNull == null) {
|
|
return index;
|
|
}
|
|
|
|
if (index < 0 || index >= shuffleIndicies.value!.length) {
|
|
return -1;
|
|
}
|
|
|
|
return shuffleIndicies.value![index];
|
|
}
|
|
|
|
int _effectiveIndex(int index) {
|
|
if (shuffleIndicies.valueOrNull == null) {
|
|
return index;
|
|
}
|
|
|
|
return shuffleIndicies.value!.indexOf(index);
|
|
}
|
|
|
|
Future<void> _insertFirstAudioSource(AudioSource source) {
|
|
log.fine('insert');
|
|
final wait = _audioSource.insert(0, source);
|
|
_currentIndexIgnore.add(1);
|
|
return wait;
|
|
}
|
|
|
|
Future<void> _pruneAudioSources(int keepIndex) async {
|
|
if (keepIndex > 0) {
|
|
log.fine('removeRange 0');
|
|
final wait = _audioSource.removeRange(0, keepIndex);
|
|
_currentIndexIgnore.add(0);
|
|
await wait;
|
|
}
|
|
if (_audioSource.length > 1) {
|
|
log.fine('removeRange 1');
|
|
await _audioSource.removeRange(1, _audioSource.length);
|
|
}
|
|
}
|
|
|
|
Future<void> _clearAudioSource([bool clearMetadata = false]) async {
|
|
// await _player.stop();
|
|
log.fine('_clearAudioSource');
|
|
await _audioSource.clear();
|
|
|
|
if (clearMetadata) {
|
|
mediaItem.add(null);
|
|
queue.add([]);
|
|
queueTitle.add('');
|
|
}
|
|
}
|
|
|
|
Future<QueueSlice?> _getQueueSlice(int index) async {
|
|
if (_queueLength == null) {
|
|
return null;
|
|
}
|
|
|
|
final effectiveIndex = _effectiveIndex(index);
|
|
|
|
int nextIndex;
|
|
int prevIndex;
|
|
if (repeatMode.value == AudioServiceRepeatMode.none) {
|
|
nextIndex = _realIndex(effectiveIndex + 1);
|
|
prevIndex = _realIndex(effectiveIndex - 1);
|
|
} else if (repeatMode.value == AudioServiceRepeatMode.one) {
|
|
nextIndex = index;
|
|
prevIndex = index;
|
|
} else {
|
|
nextIndex = _realIndex(
|
|
effectiveIndex + 1 >= _queueLength! ? 0 : effectiveIndex + 1,
|
|
);
|
|
prevIndex = _realIndex(
|
|
effectiveIndex - 1 < 0 ? _queueLength! - 1 : effectiveIndex - 1,
|
|
);
|
|
}
|
|
|
|
final slice = await _getQueueItems([prevIndex, index, nextIndex]);
|
|
final current = slice.firstWhereOrNull(
|
|
(e) => e.queueData.index == index,
|
|
);
|
|
final next = slice.firstWhereOrNull((e) => e.queueData.index == nextIndex);
|
|
final prev = slice.firstWhereOrNull((e) => e.queueData.index == prevIndex);
|
|
|
|
return QueueSlice(prev, current, next);
|
|
}
|
|
|
|
Future<List<QueueSourceItem>> _getQueueItems(List<int> indexes) async {
|
|
final slice = await _db.queueInIndicies(indexes).get();
|
|
final songs =
|
|
await _db.songsInIds(_sourceId, slice.map((e) => e.id).toList()).get();
|
|
final songMap = {for (var song in songs) song.id: song};
|
|
|
|
final albumIds = songs.map((e) => e.albumId).whereNotNull().toSet();
|
|
final albums = await _db.albumsInIds(_sourceId, albumIds.toList()).get();
|
|
final albumArtMap = {
|
|
for (var album in albums) album.id: _mapArtCache(album)
|
|
};
|
|
|
|
final queueItems = slice.map(
|
|
(item) => _mapSong(
|
|
songMap[item.id]!,
|
|
MediaItemData(
|
|
sourceId: item.sourceId,
|
|
contextType: item.context,
|
|
contextId: item.contextId,
|
|
artCache: albumArtMap[songMap[item.id]!.albumId],
|
|
),
|
|
item,
|
|
),
|
|
);
|
|
|
|
return queueItems.toList();
|
|
}
|
|
|
|
QueueSourceItem _mapSong(Song song, MediaItemData data, QueueData queueData) {
|
|
final item = MediaItem(
|
|
id: song.id,
|
|
title: song.title,
|
|
artist: song.artist,
|
|
album: song.album,
|
|
duration: song.duration,
|
|
artUri: data.artCache?.thumbnailArtUri ?? _cache.placeholderThumbImageUri,
|
|
extras: {},
|
|
);
|
|
item.data = data;
|
|
|
|
return QueueSourceItem(
|
|
mediaItem: item,
|
|
audioSource: song.downloadFilePath != null
|
|
? AudioSource.file(song.downloadFilePath!, tag: queueData.index)
|
|
: AudioSource.uri(_source.streamUri(song.id), tag: queueData.index),
|
|
queueData: queueData,
|
|
);
|
|
}
|
|
|
|
MediaItemArtCache _mapArtCache(Album album) {
|
|
final full = _cache.albumArt(album, thumbnail: false);
|
|
final thumbnail = _cache.albumArt(album, thumbnail: true);
|
|
return MediaItemArtCache(
|
|
fullArtUri: full.uri,
|
|
fullArtCacheKey: full.cacheKey,
|
|
thumbnailArtUri: thumbnail.uri,
|
|
thumbnailArtCacheKey: thumbnail.cacheKey,
|
|
);
|
|
}
|
|
|
|
///
|
|
/// AudioHandler
|
|
///
|
|
|
|
@override
|
|
Future<void> play() async {
|
|
await _player.play();
|
|
}
|
|
|
|
@override
|
|
Future<void> pause() async {
|
|
await _player.pause();
|
|
}
|
|
|
|
@override
|
|
Future<void> stop() async {
|
|
await _player.stop();
|
|
}
|
|
|
|
@override
|
|
Future<void> seek(Duration position) async {
|
|
await _player.seek(position);
|
|
}
|
|
|
|
@override
|
|
Future<void> skipToNext() => _player.seekToNext();
|
|
|
|
@override
|
|
Future<void> skipToPrevious() => _player.seekToPrevious();
|
|
|
|
@override
|
|
Future<void> skipToQueueItem(int index) async {
|
|
if (_player.effectiveIndices == null || _player.effectiveIndices!.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
index = _player.effectiveIndices![index];
|
|
if (index < 0 || index >= queue.value.length) {
|
|
return;
|
|
}
|
|
|
|
await _player.seek(Duration.zero, index: index);
|
|
}
|
|
|
|
@override
|
|
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
|
if (queueMode.value == QueueMode.radio) {
|
|
switch (repeatMode) {
|
|
case AudioServiceRepeatMode.all:
|
|
case AudioServiceRepeatMode.group:
|
|
case AudioServiceRepeatMode.one:
|
|
return _loop(AudioServiceRepeatMode.one);
|
|
default:
|
|
return _loop(AudioServiceRepeatMode.none);
|
|
}
|
|
}
|
|
|
|
return _loop(repeatMode);
|
|
}
|
|
|
|
@override
|
|
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
|
if (queueMode.value == QueueMode.radio) {
|
|
return;
|
|
}
|
|
|
|
switch (shuffleMode) {
|
|
case AudioServiceShuffleMode.all:
|
|
case AudioServiceShuffleMode.group:
|
|
return _shuffle(startIndex: _player.sequenceState?.currentSource?.tag);
|
|
case AudioServiceShuffleMode.none:
|
|
return _shuffle(unshuffle: true);
|
|
}
|
|
}
|
|
}
|