mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
chore: create new audio player centric playback notifier with drift persistence
This commit is contained in:
parent
59041a2948
commit
f79fedefd4
@ -4,9 +4,9 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
|
import 'package:spotify/spotify.dart' hide Playlist;
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
|
||||||
|
|
||||||
part 'connect.freezed.dart';
|
part 'connect.freezed.dart';
|
||||||
part 'connect.g.dart';
|
part 'connect.g.dart';
|
||||||
|
@ -183,7 +183,7 @@ class WebSocketEvent<T> {
|
|||||||
if (type == WsEvent.loop) {
|
if (type == WsEvent.loop) {
|
||||||
await callback(
|
await callback(
|
||||||
WebSocketLoopEvent(
|
WebSocketLoopEvent(
|
||||||
PlaybackLoopMode.fromString(data as String),
|
PlaylistMode.values.firstWhere((e) => e.name == data as String),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -224,12 +224,16 @@ class WebSocketEvent<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSocketLoopEvent extends WebSocketEvent<PlaybackLoopMode> {
|
class WebSocketLoopEvent extends WebSocketEvent<PlaylistMode> {
|
||||||
WebSocketLoopEvent(PlaybackLoopMode data) : super(WsEvent.loop, data);
|
WebSocketLoopEvent(PlaylistMode data) : super(WsEvent.loop, data);
|
||||||
|
|
||||||
WebSocketLoopEvent.fromJson(Map<String, dynamic> json)
|
WebSocketLoopEvent.fromJson(Map<String, dynamic> json)
|
||||||
: super(
|
: super(
|
||||||
WsEvent.loop, PlaybackLoopMode.fromString(json["data"] as String));
|
WsEvent.loop,
|
||||||
|
PlaylistMode.values.firstWhere(
|
||||||
|
(e) => e.name == json["data"] as String,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toJson() {
|
String toJson() {
|
||||||
|
@ -5,6 +5,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:encrypt/encrypt.dart';
|
import 'package:encrypt/encrypt.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
@ -25,11 +26,13 @@ part 'tables/preferences.dart';
|
|||||||
part 'tables/scrobbler.dart';
|
part 'tables/scrobbler.dart';
|
||||||
part 'tables/skip_segment.dart';
|
part 'tables/skip_segment.dart';
|
||||||
part 'tables/source_match.dart';
|
part 'tables/source_match.dart';
|
||||||
|
part 'tables/audio_player_state.dart';
|
||||||
|
|
||||||
part 'typeconverters/color.dart';
|
part 'typeconverters/color.dart';
|
||||||
part 'typeconverters/locale.dart';
|
part 'typeconverters/locale.dart';
|
||||||
part 'typeconverters/string_list.dart';
|
part 'typeconverters/string_list.dart';
|
||||||
part 'typeconverters/encrypted_text.dart';
|
part 'typeconverters/encrypted_text.dart';
|
||||||
|
part 'typeconverters/map.dart';
|
||||||
|
|
||||||
@DriftDatabase(
|
@DriftDatabase(
|
||||||
tables: [
|
tables: [
|
||||||
@ -39,6 +42,9 @@ part 'typeconverters/encrypted_text.dart';
|
|||||||
ScrobblerTable,
|
ScrobblerTable,
|
||||||
SkipSegmentTable,
|
SkipSegmentTable,
|
||||||
SourceMatchTable,
|
SourceMatchTable,
|
||||||
|
AudioPlayerStateTable,
|
||||||
|
PlaylistTable,
|
||||||
|
PlaylistMediaTable,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
File diff suppressed because it is too large
Load Diff
27
lib/models/database/tables/audio_player_state.dart
Normal file
27
lib/models/database/tables/audio_player_state.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
part of '../database.dart';
|
||||||
|
|
||||||
|
class AudioPlayerStateTable extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
BoolColumn get playing => boolean()();
|
||||||
|
RealColumn get volume => real()();
|
||||||
|
TextColumn get loopMode => textEnum<PlaylistMode>()();
|
||||||
|
BoolColumn get shuffled => boolean()();
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaylistTable extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
IntColumn get audioPlayerStateId =>
|
||||||
|
integer().references(AudioPlayerStateTable, #id)();
|
||||||
|
IntColumn get index => integer()();
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaylistMediaTable extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
IntColumn get playlistId => integer().references(PlaylistTable, #id)();
|
||||||
|
|
||||||
|
TextColumn get uri => text()();
|
||||||
|
TextColumn get extras =>
|
||||||
|
text().nullable().map(const MapTypeConverter<String, dynamic>())();
|
||||||
|
TextColumn get httpHeaders =>
|
||||||
|
text().nullable().map(const MapTypeConverter<String, String>())();
|
||||||
|
}
|
15
lib/models/database/typeconverters/map.dart
Normal file
15
lib/models/database/typeconverters/map.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
part of '../database.dart';
|
||||||
|
|
||||||
|
class MapTypeConverter<K, V> extends TypeConverter<Map<K, V>, String> {
|
||||||
|
const MapTypeConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
fromSql(String fromDb) {
|
||||||
|
return json.decode(fromDb) as Map<K, V>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
toSql(value) {
|
||||||
|
return json.encode(value);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
@ -12,7 +13,6 @@ import 'package:spotube/modules/player/use_progress.dart';
|
|||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
|
||||||
|
|
||||||
class PlayerControls extends HookConsumerWidget {
|
class PlayerControls extends HookConsumerWidget {
|
||||||
final PaletteGenerator? palette;
|
final PaletteGenerator? palette;
|
||||||
@ -234,38 +234,29 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
? null
|
? null
|
||||||
: playlistNotifier.next,
|
: playlistNotifier.next,
|
||||||
),
|
),
|
||||||
StreamBuilder<PlaybackLoopMode>(
|
StreamBuilder<PlaylistMode>(
|
||||||
stream: audioPlayer.loopModeStream,
|
stream: audioPlayer.loopModeStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
|
final loopMode = snapshot.data ?? PlaylistMode.none;
|
||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: loopMode == PlaybackLoopMode.one
|
tooltip: loopMode == PlaylistMode.single
|
||||||
? context.l10n.loop_track
|
? context.l10n.loop_track
|
||||||
: loopMode == PlaybackLoopMode.all
|
: loopMode == PlaylistMode.loop
|
||||||
? context.l10n.repeat_playlist
|
? context.l10n.repeat_playlist
|
||||||
: null,
|
: null,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
loopMode == PlaybackLoopMode.one
|
loopMode == PlaylistMode.single
|
||||||
? SpotubeIcons.repeatOne
|
? SpotubeIcons.repeatOne
|
||||||
: SpotubeIcons.repeat,
|
: SpotubeIcons.repeat,
|
||||||
),
|
),
|
||||||
style: loopMode == PlaybackLoopMode.one ||
|
style: loopMode == PlaylistMode.single ||
|
||||||
loopMode == PlaybackLoopMode.all
|
loopMode == PlaylistMode.loop
|
||||||
? activeButtonStyle
|
? activeButtonStyle
|
||||||
: buttonStyle,
|
: buttonStyle,
|
||||||
onPressed: playlist.isFetching == true
|
onPressed: playlist.isFetching == true
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
audioPlayer.setLoopMode(
|
await audioPlayer.setLoopMode(loopMode);
|
||||||
switch (loopMode) {
|
|
||||||
PlaybackLoopMode.all =>
|
|
||||||
PlaybackLoopMode.one,
|
|
||||||
PlaybackLoopMode.one =>
|
|
||||||
PlaybackLoopMode.none,
|
|
||||||
PlaybackLoopMode.none =>
|
|
||||||
PlaybackLoopMode.all,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:async/async.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -19,26 +18,13 @@ import 'package:spotube/services/audio_player/audio_player.dart';
|
|||||||
final sliderValue = position.value.inSeconds;
|
final sliderValue = position.value.inSeconds;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final durationOperation =
|
duration.value = audioPlayer.duration;
|
||||||
CancelableOperation.fromFuture(audioPlayer.duration);
|
|
||||||
durationOperation.then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
duration.value = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final durationSubscription = audioPlayer.durationStream.listen((event) {
|
final durationSubscription = audioPlayer.durationStream.listen((event) {
|
||||||
duration.value = event;
|
duration.value = event;
|
||||||
});
|
});
|
||||||
|
|
||||||
final positionOperation =
|
position.value = audioPlayer.position;
|
||||||
CancelableOperation.fromFuture(audioPlayer.position);
|
|
||||||
|
|
||||||
positionOperation.then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
position.value = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var lastPosition = position.value;
|
var lastPosition = position.value;
|
||||||
|
|
||||||
@ -54,9 +40,7 @@ import 'package:spotube/services/audio_player/audio_player.dart';
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () {
|
return () {
|
||||||
positionOperation.cancel();
|
|
||||||
positionSubscription.cancel();
|
positionSubscription.cancel();
|
||||||
durationOperation.cancel();
|
|
||||||
durationSubscription.cancel();
|
durationSubscription.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -16,7 +16,7 @@ import 'package:spotube/extensions/image.dart';
|
|||||||
import 'package:spotube/pages/track/track.dart';
|
import 'package:spotube/pages/track/track.dart';
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
import 'package:spotube/provider/connect/connect.dart';
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class RemotePlayerQueue extends ConsumerWidget {
|
class RemotePlayerQueue extends ConsumerWidget {
|
||||||
@ -244,18 +244,18 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
: connectNotifier.next,
|
: connectNotifier.next,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: loopMode == PlaybackLoopMode.one
|
tooltip: loopMode == PlaylistMode.single
|
||||||
? context.l10n.loop_track
|
? context.l10n.loop_track
|
||||||
: loopMode == PlaybackLoopMode.all
|
: loopMode == PlaylistMode.loop
|
||||||
? context.l10n.repeat_playlist
|
? context.l10n.repeat_playlist
|
||||||
: null,
|
: null,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
loopMode == PlaybackLoopMode.one
|
loopMode == PlaylistMode.single
|
||||||
? SpotubeIcons.repeatOne
|
? SpotubeIcons.repeatOne
|
||||||
: SpotubeIcons.repeat,
|
: SpotubeIcons.repeat,
|
||||||
),
|
),
|
||||||
style: loopMode == PlaybackLoopMode.one ||
|
style: loopMode == PlaylistMode.single ||
|
||||||
loopMode == PlaybackLoopMode.all
|
loopMode == PlaylistMode.loop
|
||||||
? activeButtonStyle
|
? activeButtonStyle
|
||||||
: buttonStyle,
|
: buttonStyle,
|
||||||
onPressed: playlist.activeTrack == null
|
onPressed: playlist.activeTrack == null
|
||||||
@ -263,12 +263,11 @@ class ConnectControlPage extends HookConsumerWidget {
|
|||||||
: () async {
|
: () async {
|
||||||
connectNotifier.setLoopMode(
|
connectNotifier.setLoopMode(
|
||||||
switch (loopMode) {
|
switch (loopMode) {
|
||||||
PlaybackLoopMode.all =>
|
PlaylistMode.loop =>
|
||||||
PlaybackLoopMode.one,
|
PlaylistMode.single,
|
||||||
PlaybackLoopMode.one =>
|
PlaylistMode.single =>
|
||||||
PlaybackLoopMode.none,
|
PlaylistMode.none,
|
||||||
PlaybackLoopMode.none =>
|
PlaylistMode.none => PlaylistMode.loop,
|
||||||
PlaybackLoopMode.all,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
225
lib/provider/audio_player/audio_player.dart
Normal file
225
lib/provider/audio_player/audio_player.dart
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
|
import 'package:spotify/spotify.dart' hide Playlist;
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
|
import 'package:spotube/provider/database/database.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
|
class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||||
|
Future<void> _syncSavedState() async {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
|
||||||
|
var playerState =
|
||||||
|
await database.select(database.audioPlayerStateTable).getSingleOrNull();
|
||||||
|
|
||||||
|
if (playerState == null) {
|
||||||
|
await database.into(database.audioPlayerStateTable).insert(
|
||||||
|
AudioPlayerStateTableCompanion.insert(
|
||||||
|
playing: audioPlayer.isPlaying,
|
||||||
|
volume: audioPlayer.volume,
|
||||||
|
loopMode: audioPlayer.loopMode,
|
||||||
|
shuffled: audioPlayer.isShuffled,
|
||||||
|
id: const Value(0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
playerState =
|
||||||
|
await database.select(database.audioPlayerStateTable).getSingle();
|
||||||
|
} else {
|
||||||
|
await audioPlayer.setVolume(playerState.volume);
|
||||||
|
await audioPlayer.setLoopMode(playerState.loopMode);
|
||||||
|
await audioPlayer.setShuffle(playerState.shuffled);
|
||||||
|
}
|
||||||
|
|
||||||
|
var playlist =
|
||||||
|
await database.select(database.playlistTable).getSingleOrNull();
|
||||||
|
var medias = await database.select(database.playlistMediaTable).get();
|
||||||
|
|
||||||
|
if (playlist == null) {
|
||||||
|
await database.into(database.playlistTable).insert(
|
||||||
|
PlaylistTableCompanion.insert(
|
||||||
|
audioPlayerStateId: 0,
|
||||||
|
index: audioPlayer.playlist.index,
|
||||||
|
id: const Value(0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
playlist = await database.select(database.playlistTable).getSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (medias.isEmpty && audioPlayer.playlist.medias.isNotEmpty) {
|
||||||
|
await database.batch((batch) {
|
||||||
|
batch.insertAll(
|
||||||
|
database.playlistMediaTable,
|
||||||
|
[
|
||||||
|
for (final media in audioPlayer.playlist.medias)
|
||||||
|
PlaylistMediaTableCompanion.insert(
|
||||||
|
playlistId: playlist!.id,
|
||||||
|
uri: media.uri,
|
||||||
|
extras: Value(media.extras),
|
||||||
|
httpHeaders: Value(media.httpHeaders),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await audioPlayer.openPlaylist(
|
||||||
|
medias
|
||||||
|
.map((media) => Media(
|
||||||
|
media.uri,
|
||||||
|
extras: media.extras,
|
||||||
|
httpHeaders: media.httpHeaders,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
initialIndex: playlist.index,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updatePlayerState(
|
||||||
|
AudioPlayerStateTableCompanion companion,
|
||||||
|
) async {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
|
||||||
|
await (database.update(database.audioPlayerStateTable)
|
||||||
|
..where((tb) => tb.id.equals(0)))
|
||||||
|
.write(companion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updatePlaylist(
|
||||||
|
Playlist playlist,
|
||||||
|
) async {
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
|
||||||
|
await database.batch((batch) {
|
||||||
|
batch.update(
|
||||||
|
database.playlistTable,
|
||||||
|
PlaylistTableCompanion(index: Value(playlist.index)),
|
||||||
|
where: (tb) => tb.id.equals(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.deleteAll(database.playlistMediaTable);
|
||||||
|
|
||||||
|
if (playlist.medias.isEmpty) return;
|
||||||
|
batch.insertAll(
|
||||||
|
database.playlistMediaTable,
|
||||||
|
[
|
||||||
|
for (final media in playlist.medias)
|
||||||
|
PlaylistMediaTableCompanion.insert(
|
||||||
|
playlistId: 0,
|
||||||
|
uri: media.uri,
|
||||||
|
extras: Value(media.extras),
|
||||||
|
httpHeaders: Value(media.httpHeaders),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
build() {
|
||||||
|
final subscriptions = [
|
||||||
|
audioPlayer.playingStream.listen((playing) async {
|
||||||
|
state = state.copyWith(playing: playing);
|
||||||
|
|
||||||
|
await _updatePlayerState(
|
||||||
|
AudioPlayerStateTableCompanion(
|
||||||
|
playing: Value(playing),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
audioPlayer.volumeStream.listen((volume) async {
|
||||||
|
state = state.copyWith(volume: volume);
|
||||||
|
|
||||||
|
await _updatePlayerState(
|
||||||
|
AudioPlayerStateTableCompanion(
|
||||||
|
volume: Value(volume),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
audioPlayer.loopModeStream.listen((loopMode) async {
|
||||||
|
state = state.copyWith(loopMode: loopMode);
|
||||||
|
|
||||||
|
await _updatePlayerState(
|
||||||
|
AudioPlayerStateTableCompanion(
|
||||||
|
loopMode: Value(loopMode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
audioPlayer.shuffledStream.listen((shuffled) async {
|
||||||
|
state = state.copyWith(shuffled: shuffled);
|
||||||
|
|
||||||
|
await _updatePlayerState(
|
||||||
|
AudioPlayerStateTableCompanion(
|
||||||
|
shuffled: Value(shuffled),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
audioPlayer.playlistStream.listen((playlist) async {
|
||||||
|
state = state.copyWith(playlist: playlist);
|
||||||
|
|
||||||
|
await _updatePlaylist(playlist);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
_syncSavedState();
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
for (final subscription in subscriptions) {
|
||||||
|
subscription.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return AudioPlayerState(
|
||||||
|
loopMode: audioPlayer.loopMode,
|
||||||
|
playing: audioPlayer.isPlaying,
|
||||||
|
playlist: audioPlayer.playlist,
|
||||||
|
shuffled: audioPlayer.isShuffled,
|
||||||
|
volume: audioPlayer.volume,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracks related methods
|
||||||
|
|
||||||
|
Future<void> addTrack(Track track) async {
|
||||||
|
await audioPlayer.addTrack(SpotubeMedia(track));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addTracks(Iterable<Track> tracks) async {
|
||||||
|
for (final track in tracks) {
|
||||||
|
await addTrack(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeTrack(Track track) async {
|
||||||
|
final index = state.tracks.indexWhere((element) => element == track);
|
||||||
|
|
||||||
|
if (index == -1) return;
|
||||||
|
|
||||||
|
await audioPlayer.removeTrack(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeTracks(Iterable<Track> tracks) async {
|
||||||
|
for (final track in tracks) {
|
||||||
|
await removeTrack(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> load(
|
||||||
|
List<Track> track, {
|
||||||
|
required int initialIndex,
|
||||||
|
bool autoPlay = false,
|
||||||
|
}) async {
|
||||||
|
await audioPlayer.openPlaylist(
|
||||||
|
track.map((t) => SpotubeMedia(t)).toList(),
|
||||||
|
initialIndex: initialIndex,
|
||||||
|
autoPlay: autoPlay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final audioPlayerProvider = NotifierProvider<AudioPlayerNotifier, AudioPlayerState>(
|
||||||
|
() => AudioPlayerNotifier(),
|
||||||
|
);
|
42
lib/provider/audio_player/state.dart
Normal file
42
lib/provider/audio_player/state.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
|
import 'package:spotify/spotify.dart' hide Playlist;
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
|
class AudioPlayerState {
|
||||||
|
final bool playing;
|
||||||
|
final double volume;
|
||||||
|
final PlaylistMode loopMode;
|
||||||
|
final bool shuffled;
|
||||||
|
final Playlist playlist;
|
||||||
|
|
||||||
|
final List<Track> tracks;
|
||||||
|
|
||||||
|
AudioPlayerState({
|
||||||
|
required this.playing,
|
||||||
|
required this.volume,
|
||||||
|
required this.loopMode,
|
||||||
|
required this.shuffled,
|
||||||
|
required this.playlist,
|
||||||
|
List<Track>? tracks,
|
||||||
|
}) : tracks = tracks ??
|
||||||
|
playlist.medias
|
||||||
|
.map((media) => SpotubeMedia.fromMedia(media).track)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
AudioPlayerState copyWith({
|
||||||
|
bool? playing,
|
||||||
|
double? volume,
|
||||||
|
PlaylistMode? loopMode,
|
||||||
|
bool? shuffled,
|
||||||
|
Playlist? playlist,
|
||||||
|
}) {
|
||||||
|
return AudioPlayerState(
|
||||||
|
playing: playing ?? this.playing,
|
||||||
|
volume: volume ?? this.volume,
|
||||||
|
loopMode: loopMode ?? this.loopMode,
|
||||||
|
shuffled: shuffled ?? this.shuffled,
|
||||||
|
playlist: playlist ?? this.playlist,
|
||||||
|
tracks: playlist == null ? tracks : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart' hide Playlist;
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
import 'package:web_socket_channel/status.dart' as status;
|
import 'package:web_socket_channel/status.dart' as status;
|
||||||
|
|
||||||
@ -27,8 +27,8 @@ final shuffleProvider = StateProvider<bool>(
|
|||||||
(ref) => false,
|
(ref) => false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final loopModeProvider = StateProvider<PlaybackLoopMode>(
|
final loopModeProvider = StateProvider<PlaylistMode>(
|
||||||
(ref) => PlaybackLoopMode.none,
|
(ref) => PlaylistMode.none,
|
||||||
);
|
);
|
||||||
|
|
||||||
final queueProvider = StateProvider<ProxyPlaylist>(
|
final queueProvider = StateProvider<ProxyPlaylist>(
|
||||||
@ -158,7 +158,7 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
|
|||||||
emit(WebSocketShuffleEvent(value));
|
emit(WebSocketShuffleEvent(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setLoopMode(PlaybackLoopMode value) async {
|
Future<void> setLoopMode(PlaylistMode value) async {
|
||||||
emit(WebSocketLoopEvent(value));
|
emit(WebSocketLoopEvent(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,11 @@ import 'dart:io';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
final audioPlayerLoopMode = StreamProvider<PlaybackLoopMode>((ref) {
|
final audioPlayerLoopMode = StreamProvider<PlaylistMode>((ref) {
|
||||||
return audioPlayer.loopModeStream;
|
return audioPlayer.loopModeStream;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ final trayMenuProvider = Provider((ref) {
|
|||||||
final isPlaybackPlaying =
|
final isPlaybackPlaying =
|
||||||
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null));
|
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null));
|
||||||
final isLoopOne =
|
final isLoopOne =
|
||||||
ref.watch(audioPlayerLoopMode).asData?.value == PlaybackLoopMode.one;
|
ref.watch(audioPlayerLoopMode).asData?.value == PlaylistMode.single;
|
||||||
final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false;
|
final isShuffled = ref.watch(audioPlayerShuffleMode).asData?.value ?? false;
|
||||||
final isPlaying = ref.watch(audioPlayerPlaying).asData?.value ?? false;
|
final isPlaying = ref.watch(audioPlayerPlaying).asData?.value ?? false;
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ final trayMenuProvider = Provider((ref) {
|
|||||||
checked: isLoopOne,
|
checked: isLoopOne,
|
||||||
onClick: (menuItem) {
|
onClick: (menuItem) {
|
||||||
audioPlayer.setLoopMode(
|
audioPlayer.setLoopMode(
|
||||||
isLoopOne ? PlaybackLoopMode.none : PlaybackLoopMode.one,
|
isLoopOne ? PlaylistMode.none : PlaylistMode.single,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:spotube/provider/server/server.dart';
|
import 'package:spotube/provider/server/server.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart' hide Playlist;
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/services/audio_player/custom_player.dart';
|
import 'package:spotube/services/audio_player/custom_player.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:media_kit/media_kit.dart' as mk;
|
import 'package:media_kit/media_kit.dart' as mk;
|
||||||
|
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
|
||||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
|
|
||||||
@ -66,15 +66,19 @@ abstract class AudioPlayerInterface {
|
|||||||
|
|
||||||
bool get mkSupportedPlatform => _mkSupportedPlatform;
|
bool get mkSupportedPlatform => _mkSupportedPlatform;
|
||||||
|
|
||||||
Future<Duration?> get duration async {
|
Duration get duration {
|
||||||
return _mkPlayer.state.duration;
|
return _mkPlayer.state.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Duration?> get position async {
|
Playlist get playlist {
|
||||||
|
return _mkPlayer.state.playlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration get position {
|
||||||
return _mkPlayer.state.position;
|
return _mkPlayer.state.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Duration?> get bufferedPosition async {
|
Duration get bufferedPosition {
|
||||||
return _mkPlayer.state.buffer;
|
return _mkPlayer.state.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,8 +115,8 @@ abstract class AudioPlayerInterface {
|
|||||||
return _mkPlayer.shuffled;
|
return _mkPlayer.shuffled;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackLoopMode get loopMode {
|
PlaylistMode get loopMode {
|
||||||
return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.state.playlistMode);
|
return _mkPlayer.state.playlistMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current volume of the player, between 0 and 1
|
/// Returns the current volume of the player, between 0 and 1
|
||||||
|
@ -65,7 +65,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? get nextSource {
|
String? get nextSource {
|
||||||
if (loopMode == PlaybackLoopMode.all &&
|
if (loopMode == PlaylistMode.loop &&
|
||||||
_mkPlayer.state.playlist.index ==
|
_mkPlayer.state.playlist.index ==
|
||||||
_mkPlayer.state.playlist.medias.length - 1) {
|
_mkPlayer.state.playlist.medias.length - 1) {
|
||||||
return sources.first;
|
return sources.first;
|
||||||
@ -77,8 +77,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? get previousSource {
|
String? get previousSource {
|
||||||
if (loopMode == PlaybackLoopMode.all &&
|
if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == 0) {
|
||||||
_mkPlayer.state.playlist.index == 0) {
|
|
||||||
return sources.last;
|
return sources.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,8 +124,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
await _mkPlayer.setShuffle(shuffle);
|
await _mkPlayer.setShuffle(shuffle);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setLoopMode(PlaybackLoopMode loop) async {
|
Future<void> setLoopMode(PlaylistMode loop) async {
|
||||||
await _mkPlayer.setPlaylistMode(loop.toPlaylistMode());
|
await _mkPlayer.setPlaylistMode(loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setAudioNormalization(bool normalize) async {
|
Future<void> setAudioNormalization(bool normalize) async {
|
||||||
|
@ -71,12 +71,12 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<PlaybackLoopMode> get loopModeStream {
|
Stream<PlaylistMode> get loopModeStream {
|
||||||
// if (mkSupportedPlatform) {
|
// if (mkSupportedPlatform) {
|
||||||
return _mkPlayer.stream.playlistMode.map(PlaybackLoopMode.fromPlaylistMode);
|
return _mkPlayer.stream.playlistMode;
|
||||||
// } else {
|
// } else {
|
||||||
// return _justAudio!.loopModeStream
|
// return _justAudio!.loopModeStream
|
||||||
// .map(PlaybackLoopMode.fromLoopMode)
|
// .map(PlaylistMode.fromLoopMode)
|
||||||
// ;
|
// ;
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
|
|
||||||
/// An unified loop mode for both [LoopMode] and [PlaylistMode]
|
|
||||||
enum PlaybackLoopMode {
|
|
||||||
all,
|
|
||||||
one,
|
|
||||||
none;
|
|
||||||
|
|
||||||
// static PlaybackLoopMode fromLoopMode(LoopMode loopMode) {
|
|
||||||
// switch (loopMode) {
|
|
||||||
// case LoopMode.all:
|
|
||||||
// return PlaybackLoopMode.all;
|
|
||||||
// case LoopMode.one:
|
|
||||||
// return PlaybackLoopMode.one;
|
|
||||||
// case LoopMode.off:
|
|
||||||
// return PlaybackLoopMode.none;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// LoopMode toLoopMode() {
|
|
||||||
// switch (this) {
|
|
||||||
// case PlaybackLoopMode.all:
|
|
||||||
// return LoopMode.all;
|
|
||||||
// case PlaybackLoopMode.one:
|
|
||||||
// return LoopMode.one;
|
|
||||||
// case PlaybackLoopMode.none:
|
|
||||||
// return LoopMode.off;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
static PlaybackLoopMode fromPlaylistMode(PlaylistMode mode) {
|
|
||||||
switch (mode) {
|
|
||||||
case PlaylistMode.single:
|
|
||||||
return PlaybackLoopMode.one;
|
|
||||||
case PlaylistMode.loop:
|
|
||||||
return PlaybackLoopMode.all;
|
|
||||||
case PlaylistMode.none:
|
|
||||||
return PlaybackLoopMode.none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaylistMode toPlaylistMode() {
|
|
||||||
switch (this) {
|
|
||||||
case PlaybackLoopMode.all:
|
|
||||||
return PlaylistMode.loop;
|
|
||||||
case PlaybackLoopMode.one:
|
|
||||||
return PlaylistMode.single;
|
|
||||||
case PlaybackLoopMode.none:
|
|
||||||
return PlaylistMode.none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static PlaybackLoopMode fromAudioServiceRepeatMode(
|
|
||||||
AudioServiceRepeatMode mode) {
|
|
||||||
switch (mode) {
|
|
||||||
case AudioServiceRepeatMode.all:
|
|
||||||
case AudioServiceRepeatMode.group:
|
|
||||||
return PlaybackLoopMode.all;
|
|
||||||
case AudioServiceRepeatMode.one:
|
|
||||||
return PlaybackLoopMode.one;
|
|
||||||
case AudioServiceRepeatMode.none:
|
|
||||||
return PlaybackLoopMode.none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioServiceRepeatMode toAudioServiceRepeatMode() {
|
|
||||||
switch (this) {
|
|
||||||
case PlaybackLoopMode.all:
|
|
||||||
return AudioServiceRepeatMode.all;
|
|
||||||
case PlaybackLoopMode.one:
|
|
||||||
return AudioServiceRepeatMode.one;
|
|
||||||
case PlaybackLoopMode.none:
|
|
||||||
return AudioServiceRepeatMode.none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static PlaybackLoopMode fromString(String? value) {
|
|
||||||
switch (value) {
|
|
||||||
case 'all':
|
|
||||||
return PlaybackLoopMode.all;
|
|
||||||
case 'one':
|
|
||||||
return PlaybackLoopMode.one;
|
|
||||||
case 'none':
|
|
||||||
return PlaybackLoopMode.none;
|
|
||||||
default:
|
|
||||||
return PlaybackLoopMode.none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ import 'package:audio_session/audio_session.dart';
|
|||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
|
|
||||||
class MobileAudioService extends BaseAudioHandler {
|
class MobileAudioService extends BaseAudioHandler {
|
||||||
AudioSession? session;
|
AudioSession? session;
|
||||||
@ -91,9 +91,13 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
@override
|
@override
|
||||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
||||||
super.setRepeatMode(repeatMode);
|
super.setRepeatMode(repeatMode);
|
||||||
audioPlayer.setLoopMode(
|
audioPlayer.setLoopMode(switch (repeatMode) {
|
||||||
PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode),
|
AudioServiceRepeatMode.all ||
|
||||||
);
|
AudioServiceRepeatMode.group =>
|
||||||
|
PlaylistMode.loop,
|
||||||
|
AudioServiceRepeatMode.one => PlaylistMode.single,
|
||||||
|
_ => PlaylistMode.none,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -120,7 +124,6 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<PlaybackState> _transformEvent() async {
|
Future<PlaybackState> _transformEvent() async {
|
||||||
final position = (await audioPlayer.position) ?? Duration.zero;
|
|
||||||
return PlaybackState(
|
return PlaybackState(
|
||||||
controls: [
|
controls: [
|
||||||
MediaControl.skipToPrevious,
|
MediaControl.skipToPrevious,
|
||||||
@ -133,12 +136,16 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
},
|
},
|
||||||
androidCompactActionIndices: const [0, 1, 2],
|
androidCompactActionIndices: const [0, 1, 2],
|
||||||
playing: audioPlayer.isPlaying,
|
playing: audioPlayer.isPlaying,
|
||||||
updatePosition: position,
|
updatePosition: audioPlayer.position,
|
||||||
bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero,
|
bufferedPosition: audioPlayer.bufferedPosition,
|
||||||
shuffleMode: audioPlayer.isShuffled == true
|
shuffleMode: audioPlayer.isShuffled == true
|
||||||
? AudioServiceShuffleMode.all
|
? AudioServiceShuffleMode.all
|
||||||
: AudioServiceShuffleMode.none,
|
: AudioServiceShuffleMode.none,
|
||||||
repeatMode: (audioPlayer.loopMode).toAudioServiceRepeatMode(),
|
repeatMode: switch (audioPlayer.loopMode) {
|
||||||
|
PlaylistMode.loop => AudioServiceRepeatMode.all,
|
||||||
|
PlaylistMode.single => AudioServiceRepeatMode.one,
|
||||||
|
_ => AudioServiceRepeatMode.none,
|
||||||
|
},
|
||||||
processingState: playlist.isFetching == true
|
processingState: playlist.isFetching == true
|
||||||
? AudioProcessingState.loading
|
? AudioProcessingState.loading
|
||||||
: AudioProcessingState.ready,
|
: AudioProcessingState.ready,
|
||||||
|
Loading…
Reference in New Issue
Block a user