mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: replace playAt with playTrack for safe element access and property based track looping
This commit is contained in:
parent
1d82bb0987
commit
24d7b5b851
@ -148,9 +148,7 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playback.state?.activeTrack.id) {
|
currentTrack.id != playback.state?.activeTrack.id) {
|
||||||
await playback.playAt(
|
await playback.playTrack(currentTrack);
|
||||||
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
||||||
|
final playlistNotifier = ref.watch(PlaylistQueueNotifier.provider.notifier);
|
||||||
final isLocalTrack = playlist?.activeTrack is LocalTrack;
|
final isLocalTrack = playlist?.activeTrack is LocalTrack;
|
||||||
final downloader = ref.watch(downloaderProvider);
|
final downloader = ref.watch(downloaderProvider);
|
||||||
final isInQueue = downloader.inQueue
|
final isInQueue = downloader.inQueue
|
||||||
|
@ -146,19 +146,19 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
PlatformIconButton(
|
PlatformIconButton(
|
||||||
tooltip: playlistNotifier.isShuffled
|
tooltip: playlist?.isShuffled == true
|
||||||
? "Unshuffle playlist"
|
? "Unshuffle playlist"
|
||||||
: "Shuffle playlist",
|
: "Shuffle playlist",
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
SpotubeIcons.shuffle,
|
SpotubeIcons.shuffle,
|
||||||
color: playlistNotifier.isShuffled
|
color: playlist?.isShuffled == true
|
||||||
? PlatformTheme.of(context).primaryColor
|
? PlatformTheme.of(context).primaryColor
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
onPressed: playlist == null
|
onPressed: playlist == null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (playlistNotifier.isShuffled) {
|
if (playlist.isShuffled == true) {
|
||||||
playlistNotifier.unshuffle();
|
playlistNotifier.unshuffle();
|
||||||
} else {
|
} else {
|
||||||
playlistNotifier.shuffle();
|
playlistNotifier.shuffle();
|
||||||
@ -206,21 +206,28 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: playlist != null ? playlistNotifier.stop : null,
|
onPressed: playlist != null ? playlistNotifier.stop : null,
|
||||||
),
|
),
|
||||||
// PlatformIconButton(
|
PlatformIconButton(
|
||||||
// tooltip:
|
tooltip: playlist?.isLooping != true
|
||||||
// !playlist.isLoop ? "Loop Track" : "Repeat playlist",
|
? "Loop Track"
|
||||||
// icon: Icon(
|
: "Repeat playlist",
|
||||||
// playlist.isLoop
|
icon: Icon(
|
||||||
// ? SpotubeIcons.repeatOne
|
playlist?.isLooping == true
|
||||||
// : SpotubeIcons.repeat,
|
? SpotubeIcons.repeatOne
|
||||||
// ),
|
: SpotubeIcons.repeat,
|
||||||
// onPressed:
|
color: playlist?.isLooping == true
|
||||||
// playlist.track == null || playlist.playlist == null
|
? PlatformTheme.of(context).primaryColor
|
||||||
// ? null
|
: null,
|
||||||
// : () {
|
),
|
||||||
// playlist.setIsLoop(!playlist.isLoop);
|
onPressed: playlist == null || playlist.isLoading
|
||||||
// },
|
? null
|
||||||
// ),
|
: () {
|
||||||
|
if (playlist.isLooping == true) {
|
||||||
|
playlistNotifier.unloop();
|
||||||
|
} else {
|
||||||
|
playlistNotifier.loop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5)
|
const SizedBox(height: 5)
|
||||||
|
@ -102,7 +102,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
if (playlist?.activeTrack.id == track.value.id) {
|
if (playlist?.activeTrack.id == track.value.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await playlistNotifier.playAt(i);
|
await playlistNotifier.playTrack(currentTrack);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -38,9 +38,7 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playlist?.activeTrack.id) {
|
currentTrack.id != playlist?.activeTrack.id) {
|
||||||
await playback.playAt(
|
await playback.playTrack(currentTrack);
|
||||||
sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,9 +309,7 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playlist?.activeTrack.id) {
|
currentTrack.id != playlist?.activeTrack.id) {
|
||||||
await playlistNotifier.playAt(
|
await playlistNotifier.playTrack(currentTrack);
|
||||||
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,10 +39,7 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playlistNotifier.state?.activeTrack.id) {
|
currentTrack.id != playlistNotifier.state?.activeTrack.id) {
|
||||||
await playlistNotifier.loadAndPlay(
|
await playlistNotifier.playTrack(currentTrack);
|
||||||
sortedTracks,
|
|
||||||
active: sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ final youtube = YoutubeExplode();
|
|||||||
|
|
||||||
class PlaylistQueue {
|
class PlaylistQueue {
|
||||||
final Set<Track> tracks;
|
final Set<Track> tracks;
|
||||||
|
final Set<Track> tempTracks;
|
||||||
|
final bool loop;
|
||||||
final int active;
|
final int active;
|
||||||
|
|
||||||
Track get activeTrack => tracks.elementAt(active);
|
Track get activeTrack => tracks.elementAt(active);
|
||||||
@ -28,6 +30,7 @@ class PlaylistQueue {
|
|||||||
static Future<PlaylistQueue> fromJson(
|
static Future<PlaylistQueue> fromJson(
|
||||||
Map<String, dynamic> json, UserPreferences preferences) async {
|
Map<String, dynamic> json, UserPreferences preferences) async {
|
||||||
final List? tracks = json['tracks'];
|
final List? tracks = json['tracks'];
|
||||||
|
final List? tempTracks = json['tempTracks'];
|
||||||
return PlaylistQueue(
|
return PlaylistQueue(
|
||||||
Set.from(
|
Set.from(
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
@ -52,6 +55,28 @@ class PlaylistQueue {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
active: json['active'],
|
active: json['active'],
|
||||||
|
tempTracks: Set.from(
|
||||||
|
await Future.wait(
|
||||||
|
tempTracks?.mapIndexed(
|
||||||
|
(i, e) async {
|
||||||
|
final jsonTrack =
|
||||||
|
Map.castFrom<dynamic, dynamic, String, dynamic>(e);
|
||||||
|
|
||||||
|
if (e["path"] != null) {
|
||||||
|
return LocalTrack.fromJson(jsonTrack);
|
||||||
|
} else if (i == json["active"] && !json.containsKey("path")) {
|
||||||
|
return await SpotubeTrack.fromFetchTrack(
|
||||||
|
Track.fromJson(jsonTrack),
|
||||||
|
preferences,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Track.fromJson(jsonTrack);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) ??
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,24 +94,43 @@ class PlaylistQueue {
|
|||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
'active': active,
|
'active': active,
|
||||||
|
'tempTracks': tempTracks.map(
|
||||||
|
(e) {
|
||||||
|
if (e is SpotubeTrack) {
|
||||||
|
return e.toJson();
|
||||||
|
} else if (e is LocalTrack) {
|
||||||
|
return e.toJson();
|
||||||
|
} else {
|
||||||
|
return e.toJson();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isLoading =>
|
bool get isLoading =>
|
||||||
activeTrack is LocalTrack ? false : activeTrack is! SpotubeTrack;
|
activeTrack is LocalTrack ? false : activeTrack is! SpotubeTrack;
|
||||||
|
bool get isShuffled => tempTracks.isNotEmpty;
|
||||||
|
bool get isLooping => loop;
|
||||||
|
|
||||||
PlaylistQueue(
|
PlaylistQueue(
|
||||||
this.tracks, {
|
this.tracks, {
|
||||||
|
required this.tempTracks,
|
||||||
this.active = 0,
|
this.active = 0,
|
||||||
});
|
this.loop = false,
|
||||||
|
}) : assert(active < tracks.length && active >= 0, "Invalid active index");
|
||||||
|
|
||||||
PlaylistQueue copyWith({
|
PlaylistQueue copyWith({
|
||||||
Set<Track>? tracks,
|
Set<Track>? tracks,
|
||||||
|
Set<Track>? tempTracks,
|
||||||
int? active,
|
int? active,
|
||||||
|
bool? loop,
|
||||||
}) {
|
}) {
|
||||||
return PlaylistQueue(
|
return PlaylistQueue(
|
||||||
tracks ?? this.tracks,
|
tracks ?? this.tracks,
|
||||||
active: active ?? this.active,
|
active: active ?? this.active,
|
||||||
|
tempTracks: tempTracks ?? this.tempTracks,
|
||||||
|
loop: loop ?? this.loop,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,7 +173,15 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
linuxService?.player.updateProperties();
|
linuxService?.player.updateProperties();
|
||||||
});
|
});
|
||||||
|
|
||||||
audioPlayer.onPlayerComplete.listen((event) => next());
|
audioPlayer.onPlayerComplete.listen((event) async {
|
||||||
|
if (!isLoaded) return;
|
||||||
|
if (state!.isLooping) {
|
||||||
|
await audioPlayer.seek(Duration.zero);
|
||||||
|
await audioPlayer.resume();
|
||||||
|
} else {
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
bool isPreSearching = false;
|
bool isPreSearching = false;
|
||||||
|
|
||||||
@ -158,15 +210,12 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
|
|
||||||
// properties
|
// properties
|
||||||
|
|
||||||
Set<Track> _tempTracks = {};
|
|
||||||
|
|
||||||
// getters
|
// getters
|
||||||
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
||||||
BlackListNotifier get blacklist =>
|
BlackListNotifier get blacklist =>
|
||||||
ref.read(BlackListNotifier.provider.notifier);
|
ref.read(BlackListNotifier.provider.notifier);
|
||||||
|
|
||||||
bool get isLoaded => state != null;
|
bool get isLoaded => state != null;
|
||||||
bool get isShuffled => _tempTracks.isNotEmpty;
|
|
||||||
|
|
||||||
// redirectors
|
// redirectors
|
||||||
static bool get isPlaying => audioPlayer.state == PlayerState.playing;
|
static bool get isPlaying => audioPlayer.state == PlayerState.playing;
|
||||||
@ -199,12 +248,12 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void shuffle() {
|
void shuffle() {
|
||||||
if (isShuffled || !isLoaded) return;
|
if (!isLoaded || state!.isShuffled) return;
|
||||||
_tempTracks = state?.tracks ?? _tempTracks;
|
|
||||||
state = state?.copyWith(
|
state = state?.copyWith(
|
||||||
|
tempTracks: state!.tracks,
|
||||||
tracks: {
|
tracks: {
|
||||||
state!.activeTrack,
|
state!.activeTrack,
|
||||||
..._tempTracks.toList()
|
...state!.tracks.toList()
|
||||||
..removeAt(state!.active)
|
..removeAt(state!.active)
|
||||||
..shuffle()
|
..shuffle()
|
||||||
},
|
},
|
||||||
@ -213,12 +262,28 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void unshuffle() {
|
void unshuffle() {
|
||||||
if (!isShuffled || !isLoaded) return;
|
if (!isLoaded || !state!.isShuffled) return;
|
||||||
state = state?.copyWith(
|
state = state?.copyWith(
|
||||||
tracks: _tempTracks,
|
tracks: state!.tempTracks,
|
||||||
active: _tempTracks.toList().indexOf(state!.activeTrack),
|
active: state!.tempTracks
|
||||||
|
.toList()
|
||||||
|
.indexWhere((element) => element.id == state!.activeTrack.id),
|
||||||
|
tempTracks: {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (!isLoaded || state!.isLooping) return;
|
||||||
|
state = state?.copyWith(
|
||||||
|
loop: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void unloop() {
|
||||||
|
if (!isLoaded || !state!.isLooping) return;
|
||||||
|
state = state?.copyWith(
|
||||||
|
loop: false,
|
||||||
);
|
);
|
||||||
_tempTracks = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> swapSibling(Video video) async {
|
Future<void> swapSibling(Video video) async {
|
||||||
@ -274,7 +339,15 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
state!.activeTrack,
|
state!.activeTrack,
|
||||||
preferences,
|
preferences,
|
||||||
);
|
);
|
||||||
state = state!.copyWith(tracks: Set.from(tracks));
|
final tempTracks = state!.tempTracks
|
||||||
|
.map((e) =>
|
||||||
|
e.id == tracks[state!.active].id ? tracks[state!.active] : e)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
state = state!.copyWith(
|
||||||
|
tracks: Set.from(tracks),
|
||||||
|
tempTracks: Set.from(tempTracks),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mobileService?.addItem(mediaItem.copyWith(
|
mobileService?.addItem(mediaItem.copyWith(
|
||||||
@ -296,18 +369,19 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> playAt(int index) async {
|
Future<void> playTrack(Track track) async {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
state = PlaylistQueue(
|
final active =
|
||||||
state!.tracks,
|
state!.tracks.toList().indexWhere((element) => element.id == track.id);
|
||||||
active: index,
|
if (active == -1) return;
|
||||||
);
|
state = state!.copyWith(active: active);
|
||||||
return play();
|
return play();
|
||||||
}
|
}
|
||||||
|
|
||||||
void load(Iterable<Track> tracks, {int active = 0}) {
|
void load(Iterable<Track> tracks, {int active = 0}) {
|
||||||
state = PlaylistQueue(
|
state = PlaylistQueue(
|
||||||
Set.from(blacklist.filter(tracks)),
|
Set.from(blacklist.filter(tracks)),
|
||||||
|
tempTracks: {},
|
||||||
active: active,
|
active: active,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -328,20 +402,18 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
(mobileService)?.session?.setActive(false);
|
(mobileService)?.session?.setActive(false);
|
||||||
state = null;
|
state = null;
|
||||||
_tempTracks = {};
|
|
||||||
return audioPlayer.stop();
|
return audioPlayer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> next() async {
|
Future<void> next() async {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
if (state!.active == state!.tracks.length - 1) {
|
if (state!.active == state!.tracks.length - 1) {
|
||||||
state = PlaylistQueue(
|
state = state!.copyWith(
|
||||||
state!.tracks,
|
|
||||||
active: 0,
|
active: 0,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
state = PlaylistQueue(
|
state = state!.copyWith(
|
||||||
state!.tracks,
|
|
||||||
active: state!.active + 1,
|
active: state!.active + 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -351,13 +423,11 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
Future<void> previous() async {
|
Future<void> previous() async {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
if (state!.active == 0) {
|
if (state!.active == 0) {
|
||||||
state = PlaylistQueue(
|
state = state!.copyWith(
|
||||||
state!.tracks,
|
|
||||||
active: state!.tracks.length - 1,
|
active: state!.tracks.length - 1,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
state = PlaylistQueue(
|
state = state!.copyWith(
|
||||||
state!.tracks,
|
|
||||||
active: state!.active - 1,
|
active: state!.active - 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -373,8 +443,8 @@ class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|||||||
// utility
|
// utility
|
||||||
bool isPlayingPlaylist(Iterable<TrackSimple> playlist) {
|
bool isPlayingPlaylist(Iterable<TrackSimple> playlist) {
|
||||||
if (!isLoaded || playlist.isEmpty) return false;
|
if (!isLoaded || playlist.isEmpty) return false;
|
||||||
if (isShuffled) {
|
if (state!.isShuffled) {
|
||||||
final trackIds = _tempTracks.map((track) => track.id!);
|
final trackIds = state!.tempTracks.map((track) => track.id!);
|
||||||
return blacklist
|
return blacklist
|
||||||
.filter(playlist)
|
.filter(playlist)
|
||||||
.every((track) => trackIds.contains(track.id!));
|
.every((track) => trackIds.contains(track.id!));
|
||||||
|
@ -283,7 +283,7 @@ class _MprisMediaPlayer2Player extends DBusObject {
|
|||||||
/// Gets value of property org.mpris.MediaPlayer2.Player.Shuffle
|
/// Gets value of property org.mpris.MediaPlayer2.Player.Shuffle
|
||||||
Future<DBusMethodResponse> getShuffle() async {
|
Future<DBusMethodResponse> getShuffle() async {
|
||||||
return DBusMethodSuccessResponse(
|
return DBusMethodSuccessResponse(
|
||||||
[DBusBoolean(playlistNotifier.isShuffled)]);
|
[DBusBoolean(playlist?.isShuffled ?? false)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets property org.mpris.MediaPlayer2.Player.Shuffle
|
/// Sets property org.mpris.MediaPlayer2.Player.Shuffle
|
||||||
|
Loading…
Reference in New Issue
Block a user