mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45: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 '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/services/audio_player/loop_mode.dart';
|
||||
|
||||
part 'connect.freezed.dart';
|
||||
part 'connect.g.dart';
|
||||
|
@ -183,7 +183,7 @@ class WebSocketEvent<T> {
|
||||
if (type == WsEvent.loop) {
|
||||
await callback(
|
||||
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> {
|
||||
WebSocketLoopEvent(PlaybackLoopMode data) : super(WsEvent.loop, data);
|
||||
class WebSocketLoopEvent extends WebSocketEvent<PlaylistMode> {
|
||||
WebSocketLoopEvent(PlaylistMode data) : super(WsEvent.loop, data);
|
||||
|
||||
WebSocketLoopEvent.fromJson(Map<String, dynamic> json)
|
||||
: super(
|
||||
WsEvent.loop, PlaybackLoopMode.fromString(json["data"] as String));
|
||||
WsEvent.loop,
|
||||
PlaylistMode.values.firstWhere(
|
||||
(e) => e.name == json["data"] as String,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
String toJson() {
|
||||
|
@ -5,6 +5,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:encrypt/encrypt.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -25,11 +26,13 @@ part 'tables/preferences.dart';
|
||||
part 'tables/scrobbler.dart';
|
||||
part 'tables/skip_segment.dart';
|
||||
part 'tables/source_match.dart';
|
||||
part 'tables/audio_player_state.dart';
|
||||
|
||||
part 'typeconverters/color.dart';
|
||||
part 'typeconverters/locale.dart';
|
||||
part 'typeconverters/string_list.dart';
|
||||
part 'typeconverters/encrypted_text.dart';
|
||||
part 'typeconverters/map.dart';
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
@ -39,6 +42,9 @@ part 'typeconverters/encrypted_text.dart';
|
||||
ScrobblerTable,
|
||||
SkipSegmentTable,
|
||||
SourceMatchTable,
|
||||
AudioPlayerStateTable,
|
||||
PlaylistTable,
|
||||
PlaylistMediaTable,
|
||||
],
|
||||
)
|
||||
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_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:palette_generator/palette_generator.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/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||
|
||||
class PlayerControls extends HookConsumerWidget {
|
||||
final PaletteGenerator? palette;
|
||||
@ -234,38 +234,29 @@ class PlayerControls extends HookConsumerWidget {
|
||||
? null
|
||||
: playlistNotifier.next,
|
||||
),
|
||||
StreamBuilder<PlaybackLoopMode>(
|
||||
StreamBuilder<PlaylistMode>(
|
||||
stream: audioPlayer.loopModeStream,
|
||||
builder: (context, snapshot) {
|
||||
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
|
||||
final loopMode = snapshot.data ?? PlaylistMode.none;
|
||||
return IconButton(
|
||||
tooltip: loopMode == PlaybackLoopMode.one
|
||||
tooltip: loopMode == PlaylistMode.single
|
||||
? context.l10n.loop_track
|
||||
: loopMode == PlaybackLoopMode.all
|
||||
: loopMode == PlaylistMode.loop
|
||||
? context.l10n.repeat_playlist
|
||||
: null,
|
||||
icon: Icon(
|
||||
loopMode == PlaybackLoopMode.one
|
||||
loopMode == PlaylistMode.single
|
||||
? SpotubeIcons.repeatOne
|
||||
: SpotubeIcons.repeat,
|
||||
),
|
||||
style: loopMode == PlaybackLoopMode.one ||
|
||||
loopMode == PlaybackLoopMode.all
|
||||
style: loopMode == PlaylistMode.single ||
|
||||
loopMode == PlaylistMode.loop
|
||||
? activeButtonStyle
|
||||
: buttonStyle,
|
||||
onPressed: playlist.isFetching == true
|
||||
? null
|
||||
: () async {
|
||||
audioPlayer.setLoopMode(
|
||||
switch (loopMode) {
|
||||
PlaybackLoopMode.all =>
|
||||
PlaybackLoopMode.one,
|
||||
PlaybackLoopMode.one =>
|
||||
PlaybackLoopMode.none,
|
||||
PlaybackLoopMode.none =>
|
||||
PlaybackLoopMode.all,
|
||||
},
|
||||
);
|
||||
await audioPlayer.setLoopMode(loopMode);
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:async/async.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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;
|
||||
|
||||
useEffect(() {
|
||||
final durationOperation =
|
||||
CancelableOperation.fromFuture(audioPlayer.duration);
|
||||
durationOperation.then((value) {
|
||||
if (value != null) {
|
||||
duration.value = value;
|
||||
}
|
||||
});
|
||||
duration.value = audioPlayer.duration;
|
||||
|
||||
final durationSubscription = audioPlayer.durationStream.listen((event) {
|
||||
duration.value = event;
|
||||
});
|
||||
|
||||
final positionOperation =
|
||||
CancelableOperation.fromFuture(audioPlayer.position);
|
||||
|
||||
positionOperation.then((value) {
|
||||
if (value != null) {
|
||||
position.value = value;
|
||||
}
|
||||
});
|
||||
position.value = audioPlayer.position;
|
||||
|
||||
var lastPosition = position.value;
|
||||
|
||||
@ -54,9 +40,7 @@ import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
});
|
||||
|
||||
return () {
|
||||
positionOperation.cancel();
|
||||
positionSubscription.cancel();
|
||||
durationOperation.cancel();
|
||||
durationSubscription.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
@ -16,7 +16,7 @@ import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/pages/track/track.dart';
|
||||
import 'package:spotube/provider/connect/clients.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';
|
||||
|
||||
class RemotePlayerQueue extends ConsumerWidget {
|
||||
@ -244,18 +244,18 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
: connectNotifier.next,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: loopMode == PlaybackLoopMode.one
|
||||
tooltip: loopMode == PlaylistMode.single
|
||||
? context.l10n.loop_track
|
||||
: loopMode == PlaybackLoopMode.all
|
||||
: loopMode == PlaylistMode.loop
|
||||
? context.l10n.repeat_playlist
|
||||
: null,
|
||||
icon: Icon(
|
||||
loopMode == PlaybackLoopMode.one
|
||||
loopMode == PlaylistMode.single
|
||||
? SpotubeIcons.repeatOne
|
||||
: SpotubeIcons.repeat,
|
||||
),
|
||||
style: loopMode == PlaybackLoopMode.one ||
|
||||
loopMode == PlaybackLoopMode.all
|
||||
style: loopMode == PlaylistMode.single ||
|
||||
loopMode == PlaylistMode.loop
|
||||
? activeButtonStyle
|
||||
: buttonStyle,
|
||||
onPressed: playlist.activeTrack == null
|
||||
@ -263,12 +263,11 @@ class ConnectControlPage extends HookConsumerWidget {
|
||||
: () async {
|
||||
connectNotifier.setLoopMode(
|
||||
switch (loopMode) {
|
||||
PlaybackLoopMode.all =>
|
||||
PlaybackLoopMode.one,
|
||||
PlaybackLoopMode.one =>
|
||||
PlaybackLoopMode.none,
|
||||
PlaybackLoopMode.none =>
|
||||
PlaybackLoopMode.all,
|
||||
PlaylistMode.loop =>
|
||||
PlaylistMode.single,
|
||||
PlaylistMode.single =>
|
||||
PlaylistMode.none,
|
||||
PlaylistMode.none => PlaylistMode.loop,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
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 'package:media_kit/media_kit.dart' hide Track;
|
||||
import 'package:spotube/services/logger/logger.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/logger.dart';
|
||||
import 'package:spotube/provider/connect/clients.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/status.dart' as status;
|
||||
|
||||
@ -27,8 +27,8 @@ final shuffleProvider = StateProvider<bool>(
|
||||
(ref) => false,
|
||||
);
|
||||
|
||||
final loopModeProvider = StateProvider<PlaybackLoopMode>(
|
||||
(ref) => PlaybackLoopMode.none,
|
||||
final loopModeProvider = StateProvider<PlaylistMode>(
|
||||
(ref) => PlaylistMode.none,
|
||||
);
|
||||
|
||||
final queueProvider = StateProvider<ProxyPlaylist>(
|
||||
@ -158,7 +158,7 @@ class ConnectNotifier extends AsyncNotifier<WebSocketChannel?> {
|
||||
emit(WebSocketShuffleEvent(value));
|
||||
}
|
||||
|
||||
Future<void> setLoopMode(PlaybackLoopMode value) async {
|
||||
Future<void> setLoopMode(PlaylistMode value) async {
|
||||
emit(WebSocketLoopEvent(value));
|
||||
}
|
||||
|
||||
|
@ -3,11 +3,11 @@ import 'dart:io';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/loop_mode.dart';
|
||||
import 'package:media_kit/media_kit.dart' hide Track;
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final audioPlayerLoopMode = StreamProvider<PlaybackLoopMode>((ref) {
|
||||
final audioPlayerLoopMode = StreamProvider<PlaylistMode>((ref) {
|
||||
return audioPlayer.loopModeStream;
|
||||
});
|
||||
|
||||
@ -23,7 +23,7 @@ final trayMenuProvider = Provider((ref) {
|
||||
final isPlaybackPlaying =
|
||||
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack != null));
|
||||
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 isPlaying = ref.watch(audioPlayerPlaying).asData?.value ?? false;
|
||||
|
||||
@ -75,7 +75,7 @@ final trayMenuProvider = Provider((ref) {
|
||||
checked: isLoopOne,
|
||||
onClick: (menuItem) {
|
||||
audioPlayer.setLoopMode(
|
||||
isLoopOne ? PlaybackLoopMode.none : PlaybackLoopMode.one,
|
||||
isLoopOne ? PlaylistMode.none : PlaylistMode.single,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -1,16 +1,16 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:media_kit/media_kit.dart' hide Track;
|
||||
import 'package:spotube/provider/server/server.dart';
|
||||
import 'package:spotube/services/logger/logger.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/services/audio_player/custom_player.dart';
|
||||
import 'dart:async';
|
||||
|
||||
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/sourced_track/sourced_track.dart';
|
||||
|
||||
@ -66,15 +66,19 @@ abstract class AudioPlayerInterface {
|
||||
|
||||
bool get mkSupportedPlatform => _mkSupportedPlatform;
|
||||
|
||||
Future<Duration?> get duration async {
|
||||
Duration get duration {
|
||||
return _mkPlayer.state.duration;
|
||||
}
|
||||
|
||||
Future<Duration?> get position async {
|
||||
Playlist get playlist {
|
||||
return _mkPlayer.state.playlist;
|
||||
}
|
||||
|
||||
Duration get position {
|
||||
return _mkPlayer.state.position;
|
||||
}
|
||||
|
||||
Future<Duration?> get bufferedPosition async {
|
||||
Duration get bufferedPosition {
|
||||
return _mkPlayer.state.buffer;
|
||||
}
|
||||
|
||||
@ -111,8 +115,8 @@ abstract class AudioPlayerInterface {
|
||||
return _mkPlayer.shuffled;
|
||||
}
|
||||
|
||||
PlaybackLoopMode get loopMode {
|
||||
return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.state.playlistMode);
|
||||
PlaylistMode get loopMode {
|
||||
return _mkPlayer.state.playlistMode;
|
||||
}
|
||||
|
||||
/// Returns the current volume of the player, between 0 and 1
|
||||
|
@ -65,7 +65,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
||||
}
|
||||
|
||||
String? get nextSource {
|
||||
if (loopMode == PlaybackLoopMode.all &&
|
||||
if (loopMode == PlaylistMode.loop &&
|
||||
_mkPlayer.state.playlist.index ==
|
||||
_mkPlayer.state.playlist.medias.length - 1) {
|
||||
return sources.first;
|
||||
@ -77,8 +77,7 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
||||
}
|
||||
|
||||
String? get previousSource {
|
||||
if (loopMode == PlaybackLoopMode.all &&
|
||||
_mkPlayer.state.playlist.index == 0) {
|
||||
if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == 0) {
|
||||
return sources.last;
|
||||
}
|
||||
|
||||
@ -125,8 +124,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
||||
await _mkPlayer.setShuffle(shuffle);
|
||||
}
|
||||
|
||||
Future<void> setLoopMode(PlaybackLoopMode loop) async {
|
||||
await _mkPlayer.setPlaylistMode(loop.toPlaylistMode());
|
||||
Future<void> setLoopMode(PlaylistMode loop) async {
|
||||
await _mkPlayer.setPlaylistMode(loop);
|
||||
}
|
||||
|
||||
Future<void> setAudioNormalization(bool normalize) async {
|
||||
|
@ -71,12 +71,12 @@ mixin SpotubeAudioPlayersStreams on AudioPlayerInterface {
|
||||
// }
|
||||
}
|
||||
|
||||
Stream<PlaybackLoopMode> get loopModeStream {
|
||||
Stream<PlaylistMode> get loopModeStream {
|
||||
// if (mkSupportedPlatform) {
|
||||
return _mkPlayer.stream.playlistMode.map(PlaybackLoopMode.fromPlaylistMode);
|
||||
return _mkPlayer.stream.playlistMode;
|
||||
// } else {
|
||||
// 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_provider.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 {
|
||||
AudioSession? session;
|
||||
@ -91,9 +91,13 @@ class MobileAudioService extends BaseAudioHandler {
|
||||
@override
|
||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
||||
super.setRepeatMode(repeatMode);
|
||||
audioPlayer.setLoopMode(
|
||||
PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode),
|
||||
);
|
||||
audioPlayer.setLoopMode(switch (repeatMode) {
|
||||
AudioServiceRepeatMode.all ||
|
||||
AudioServiceRepeatMode.group =>
|
||||
PlaylistMode.loop,
|
||||
AudioServiceRepeatMode.one => PlaylistMode.single,
|
||||
_ => PlaylistMode.none,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -120,7 +124,6 @@ class MobileAudioService extends BaseAudioHandler {
|
||||
}
|
||||
|
||||
Future<PlaybackState> _transformEvent() async {
|
||||
final position = (await audioPlayer.position) ?? Duration.zero;
|
||||
return PlaybackState(
|
||||
controls: [
|
||||
MediaControl.skipToPrevious,
|
||||
@ -133,12 +136,16 @@ class MobileAudioService extends BaseAudioHandler {
|
||||
},
|
||||
androidCompactActionIndices: const [0, 1, 2],
|
||||
playing: audioPlayer.isPlaying,
|
||||
updatePosition: position,
|
||||
bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero,
|
||||
updatePosition: audioPlayer.position,
|
||||
bufferedPosition: audioPlayer.bufferedPosition,
|
||||
shuffleMode: audioPlayer.isShuffled == true
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
repeatMode: (audioPlayer.loopMode).toAudioServiceRepeatMode(),
|
||||
repeatMode: switch (audioPlayer.loopMode) {
|
||||
PlaylistMode.loop => AudioServiceRepeatMode.all,
|
||||
PlaylistMode.single => AudioServiceRepeatMode.one,
|
||||
_ => AudioServiceRepeatMode.none,
|
||||
},
|
||||
processingState: playlist.isFetching == true
|
||||
? AudioProcessingState.loading
|
||||
: AudioProcessingState.ready,
|
||||
|
Loading…
Reference in New Issue
Block a user