diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index b1bde2e8..61b5666f 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -148,9 +148,7 @@ class UserLocalTracks extends HookConsumerWidget { } else if (isPlaylistPlaying && currentTrack.id != null && currentTrack.id != playback.state?.activeTrack.id) { - await playback.playAt( - tracks.indexWhere((s) => s.id == currentTrack?.id), - ); + await playback.playTrack(currentTrack); } } diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index 050670d9..cf0de7ac 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -30,6 +30,7 @@ class PlayerActions extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final playlist = ref.watch(PlaylistQueueNotifier.provider); + final playlistNotifier = ref.watch(PlaylistQueueNotifier.provider.notifier); final isLocalTrack = playlist?.activeTrack is LocalTrack; final downloader = ref.watch(downloaderProvider); final isInQueue = downloader.inQueue diff --git a/lib/components/player/player_controls.dart b/lib/components/player/player_controls.dart index bdba3cdc..5a23ca75 100644 --- a/lib/components/player/player_controls.dart +++ b/lib/components/player/player_controls.dart @@ -146,19 +146,19 @@ class PlayerControls extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ PlatformIconButton( - tooltip: playlistNotifier.isShuffled + tooltip: playlist?.isShuffled == true ? "Unshuffle playlist" : "Shuffle playlist", icon: Icon( SpotubeIcons.shuffle, - color: playlistNotifier.isShuffled + color: playlist?.isShuffled == true ? PlatformTheme.of(context).primaryColor : null, ), onPressed: playlist == null ? null : () { - if (playlistNotifier.isShuffled) { + if (playlist.isShuffled == true) { playlistNotifier.unshuffle(); } else { playlistNotifier.shuffle(); @@ -206,21 +206,28 @@ class PlayerControls extends HookConsumerWidget { ), onPressed: playlist != null ? playlistNotifier.stop : null, ), - // PlatformIconButton( - // tooltip: - // !playlist.isLoop ? "Loop Track" : "Repeat playlist", - // icon: Icon( - // playlist.isLoop - // ? SpotubeIcons.repeatOne - // : SpotubeIcons.repeat, - // ), - // onPressed: - // playlist.track == null || playlist.playlist == null - // ? null - // : () { - // playlist.setIsLoop(!playlist.isLoop); - // }, - // ), + PlatformIconButton( + tooltip: playlist?.isLooping != true + ? "Loop Track" + : "Repeat playlist", + icon: Icon( + playlist?.isLooping == true + ? SpotubeIcons.repeatOne + : SpotubeIcons.repeat, + color: playlist?.isLooping == true + ? PlatformTheme.of(context).primaryColor + : null, + ), + onPressed: playlist == null || playlist.isLoading + ? null + : () { + if (playlist.isLooping == true) { + playlistNotifier.unloop(); + } else { + playlistNotifier.loop(); + } + }, + ), ], ), const SizedBox(height: 5) diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 4d2413b5..af57afed 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -102,7 +102,7 @@ class PlayerQueue extends HookConsumerWidget { if (playlist?.activeTrack.id == track.value.id) { return; } - await playlistNotifier.playAt(i); + await playlistNotifier.playTrack(currentTrack); }, ), ), diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index 5eed14c3..9999c9bc 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -38,9 +38,7 @@ class AlbumPage extends HookConsumerWidget { } else if (isPlaylistPlaying && currentTrack.id != null && currentTrack.id != playlist?.activeTrack.id) { - await playback.playAt( - sortedTracks.indexWhere((s) => s.id == currentTrack?.id), - ); + await playback.playTrack(currentTrack); } } diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 2bc9f411..4b584727 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -309,9 +309,7 @@ class ArtistPage extends HookConsumerWidget { } else if (isPlaylistPlaying && currentTrack.id != null && currentTrack.id != playlist?.activeTrack.id) { - await playlistNotifier.playAt( - tracks.indexWhere((s) => s.id == currentTrack?.id), - ); + await playlistNotifier.playTrack(currentTrack); } } diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index afb0b929..3bbf1b25 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -39,10 +39,7 @@ class PlaylistView extends HookConsumerWidget { } else if (isPlaylistPlaying && currentTrack.id != null && currentTrack.id != playlistNotifier.state?.activeTrack.id) { - await playlistNotifier.loadAndPlay( - sortedTracks, - active: sortedTracks.indexWhere((s) => s.id == currentTrack?.id), - ); + await playlistNotifier.playTrack(currentTrack); } } diff --git a/lib/provider/playlist_queue_provider.dart b/lib/provider/playlist_queue_provider.dart index 78ad23f0..12b726b8 100644 --- a/lib/provider/playlist_queue_provider.dart +++ b/lib/provider/playlist_queue_provider.dart @@ -21,6 +21,8 @@ final youtube = YoutubeExplode(); class PlaylistQueue { final Set tracks; + final Set tempTracks; + final bool loop; final int active; Track get activeTrack => tracks.elementAt(active); @@ -28,6 +30,7 @@ class PlaylistQueue { static Future fromJson( Map json, UserPreferences preferences) async { final List? tracks = json['tracks']; + final List? tempTracks = json['tempTracks']; return PlaylistQueue( Set.from( await Future.wait( @@ -52,6 +55,28 @@ class PlaylistQueue { ), ), active: json['active'], + tempTracks: Set.from( + await Future.wait( + tempTracks?.mapIndexed( + (i, e) async { + final jsonTrack = + Map.castFrom(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(), '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 => activeTrack is LocalTrack ? false : activeTrack is! SpotubeTrack; + bool get isShuffled => tempTracks.isNotEmpty; + bool get isLooping => loop; PlaylistQueue( this.tracks, { + required this.tempTracks, this.active = 0, - }); + this.loop = false, + }) : assert(active < tracks.length && active >= 0, "Invalid active index"); PlaylistQueue copyWith({ Set? tracks, + Set? tempTracks, int? active, + bool? loop, }) { return PlaylistQueue( tracks ?? this.tracks, active: active ?? this.active, + tempTracks: tempTracks ?? this.tempTracks, + loop: loop ?? this.loop, ); } } @@ -129,7 +173,15 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { 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; @@ -158,15 +210,12 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { // properties - Set _tempTracks = {}; - // getters UserPreferences get preferences => ref.read(userPreferencesProvider); BlackListNotifier get blacklist => ref.read(BlackListNotifier.provider.notifier); bool get isLoaded => state != null; - bool get isShuffled => _tempTracks.isNotEmpty; // redirectors static bool get isPlaying => audioPlayer.state == PlayerState.playing; @@ -199,12 +248,12 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { } void shuffle() { - if (isShuffled || !isLoaded) return; - _tempTracks = state?.tracks ?? _tempTracks; + if (!isLoaded || state!.isShuffled) return; state = state?.copyWith( + tempTracks: state!.tracks, tracks: { state!.activeTrack, - ..._tempTracks.toList() + ...state!.tracks.toList() ..removeAt(state!.active) ..shuffle() }, @@ -213,12 +262,28 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { } void unshuffle() { - if (!isShuffled || !isLoaded) return; + if (!isLoaded || !state!.isShuffled) return; state = state?.copyWith( - tracks: _tempTracks, - active: _tempTracks.toList().indexOf(state!.activeTrack), + tracks: state!.tempTracks, + 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 swapSibling(Video video) async { @@ -274,7 +339,15 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { state!.activeTrack, 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( @@ -296,18 +369,19 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { } } - Future playAt(int index) async { + Future playTrack(Track track) async { if (!isLoaded) return; - state = PlaylistQueue( - state!.tracks, - active: index, - ); + final active = + state!.tracks.toList().indexWhere((element) => element.id == track.id); + if (active == -1) return; + state = state!.copyWith(active: active); return play(); } void load(Iterable tracks, {int active = 0}) { state = PlaylistQueue( Set.from(blacklist.filter(tracks)), + tempTracks: {}, active: active, ); } @@ -328,20 +402,18 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { Future stop() async { (mobileService)?.session?.setActive(false); state = null; - _tempTracks = {}; + return audioPlayer.stop(); } Future next() async { if (!isLoaded) return; if (state!.active == state!.tracks.length - 1) { - state = PlaylistQueue( - state!.tracks, + state = state!.copyWith( active: 0, ); } else { - state = PlaylistQueue( - state!.tracks, + state = state!.copyWith( active: state!.active + 1, ); } @@ -351,13 +423,11 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { Future previous() async { if (!isLoaded) return; if (state!.active == 0) { - state = PlaylistQueue( - state!.tracks, + state = state!.copyWith( active: state!.tracks.length - 1, ); } else { - state = PlaylistQueue( - state!.tracks, + state = state!.copyWith( active: state!.active - 1, ); } @@ -373,8 +443,8 @@ class PlaylistQueueNotifier extends PersistedStateNotifier { // utility bool isPlayingPlaylist(Iterable playlist) { if (!isLoaded || playlist.isEmpty) return false; - if (isShuffled) { - final trackIds = _tempTracks.map((track) => track.id!); + if (state!.isShuffled) { + final trackIds = state!.tempTracks.map((track) => track.id!); return blacklist .filter(playlist) .every((track) => trackIds.contains(track.id!)); diff --git a/lib/services/linux_audio_service.dart b/lib/services/linux_audio_service.dart index 93779d14..1bbd3a36 100644 --- a/lib/services/linux_audio_service.dart +++ b/lib/services/linux_audio_service.dart @@ -283,7 +283,7 @@ class _MprisMediaPlayer2Player extends DBusObject { /// Gets value of property org.mpris.MediaPlayer2.Player.Shuffle Future getShuffle() async { return DBusMethodSuccessResponse( - [DBusBoolean(playlistNotifier.isShuffled)]); + [DBusBoolean(playlist?.isShuffled ?? false)]); } /// Sets property org.mpris.MediaPlayer2.Player.Shuffle