diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart index 1c6cb4d41..c82b8177a 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart @@ -13,6 +13,12 @@ import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; +enum PlayButtonState { + playing, + notPlaying, + loading, +} + class TrackCollectionHeading extends HookConsumerWidget { final String title; final String? description; @@ -20,7 +26,7 @@ class TrackCollectionHeading extends HookConsumerWidget { final List buttons; final AlbumSimple? album; final Query, T> tracksSnapshot; - final bool isPlaying; + final PlayButtonState playingState; final void Function([Track? currentTrack]) onPlay; final void Function([Track? currentTrack]) onShuffledPlay; final PaletteColor? color; @@ -31,7 +37,7 @@ class TrackCollectionHeading extends HookConsumerWidget { required this.titleImage, required this.buttons, required this.tracksSnapshot, - required this.isPlaying, + required this.playingState, required this.onPlay, required this.onShuffledPlay, required this.color, @@ -155,7 +161,8 @@ class TrackCollectionHeading extends HookConsumerWidget { label: Text(context.l10n.shuffle), icon: const Icon(SpotubeIcons.shuffle), onPressed: tracksSnapshot.data == null || - isPlaying + playingState == + PlayButtonState.playing ? null : onShuffledPlay, ), @@ -167,16 +174,27 @@ class TrackCollectionHeading extends HookConsumerWidget { backgroundColor: color?.color, foregroundColor: color?.bodyTextColor, ), - onPressed: tracksSnapshot.data != null + onPressed: tracksSnapshot.data != null || + playingState == + PlayButtonState.loading ? onPlay : null, - icon: Icon( - isPlaying - ? SpotubeIcons.stop - : SpotubeIcons.play, - ), + icon: switch (playingState) { + PlayButtonState.playing => + const Icon(SpotubeIcons.pause), + PlayButtonState.notPlaying => + const Icon(SpotubeIcons.play), + PlayButtonState.loading => + const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: .7, + ), + ), + }, label: Text( - isPlaying + playingState == PlayButtonState.playing ? context.l10n.stop : context.l10n.play, ), diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart index 2014370b6..b4a1314e5 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_view.dart @@ -25,7 +25,7 @@ class TrackCollectionView extends HookConsumerWidget { final String? description; final Query, T> tracksSnapshot; final String titleImage; - final bool isPlaying; + final PlayButtonState playingState; final void Function([Track? currentTrack]) onPlay; final void Function([Track? currentTrack]) onShuffledPlay; final void Function() onAddToQueue; @@ -43,7 +43,7 @@ class TrackCollectionView extends HookConsumerWidget { required this.id, required this.tracksSnapshot, required this.titleImage, - required this.isPlaying, + required this.playingState, required this.onPlay, required this.onShuffledPlay, required this.onAddToQueue, @@ -62,6 +62,7 @@ class TrackCollectionView extends HookConsumerWidget { Widget build(BuildContext context, ref) { final theme = Theme.of(context); final auth = ref.watch(AuthenticationNotifier.provider); + final color = usePaletteGenerator(titleImage).dominantColor; final List buttons = [ @@ -72,7 +73,7 @@ class TrackCollectionView extends HookConsumerWidget { ), if (heartBtn != null && auth != null) heartBtn!, IconButton( - onPressed: isPlaying + onPressed: playingState == PlayButtonState.playing ? null : tracksSnapshot.data != null ? onAddToQueue @@ -143,7 +144,9 @@ class TrackCollectionView extends HookConsumerWidget { child: IconButton( tooltip: context.l10n.shuffle, icon: const Icon(SpotubeIcons.shuffle), - onPressed: isPlaying ? null : onShuffledPlay, + onPressed: playingState == PlayButtonState.playing + ? null + : onShuffledPlay, ), ), AnimatedScale( @@ -155,8 +158,19 @@ class TrackCollectionView extends HookConsumerWidget { backgroundColor: theme.colorScheme.inversePrimary, ), onPressed: tracksSnapshot.data != null ? onPlay : null, - child: Icon( - isPlaying ? SpotubeIcons.stop : SpotubeIcons.play), + child: switch (playingState) { + PlayButtonState.playing => + const Icon(SpotubeIcons.pause), + PlayButtonState.notPlaying => + const Icon(SpotubeIcons.play), + PlayButtonState.loading => const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: .7, + ), + ), + }, ), ), ], @@ -185,7 +199,7 @@ class TrackCollectionView extends HookConsumerWidget { title: title, description: description, titleImage: titleImage, - isPlaying: isPlaying, + playingState: playingState, onPlay: onPlay, onShuffledPlay: onShuffledPlay, tracksSnapshot: tracksSnapshot, diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart index 7ec9d4460..efdd76aab 100644 --- a/lib/models/spotube_track.dart +++ b/lib/models/spotube_track.dart @@ -66,7 +66,7 @@ class SpotubeTrack extends Track { await client.search("$title - ${artists.join(", ")}").then( (res) { final siblings = res - .sorted((a, b) => a.views.compareTo(b.views)) + .sorted((a, b) => b.views.compareTo(a.views)) .where((item) { return artists.any( (artist) => diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index 0e9609d59..84e23e8b2 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -4,9 +4,11 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/heart_button.dart'; +import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart'; import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart'; import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -26,14 +28,15 @@ class AlbumPage extends HookConsumerWidget { final sortBy = ref.read(trackCollectionSortState(album.id!)); final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy); currentTrack ??= sortedTracks.first; - final isPlaylistPlaying = playlist.containsTracks(tracks); - if (!isPlaylistPlaying) { + final isAlbumPlaying = playlist.containsTracks(tracks); + if (!isAlbumPlaying) { + playback.addCollection(album.id!); // for enabling loading indicator await playback.load( sortedTracks, initialIndex: sortedTracks.indexWhere((s) => s.id == currentTrack?.id), ); playback.addCollection(album.id!); - } else if (isPlaylistPlaying && + } else if (isAlbumPlaying && currentTrack.id != null && currentTrack.id != playlist.activeTrack?.id) { await playback.jumpToTrack(currentTrack); @@ -57,12 +60,25 @@ class AlbumPage extends HookConsumerWidget { final mediaQuery = MediaQuery.of(context); final isAlbumPlaying = useMemoized( - () => playlist.containsTracks(tracksSnapshot.data ?? []), - [playback, tracksSnapshot.data], + () => playlist.collections.contains(album.id!), + [playlist, album], ); + + final albumTrackPlaying = useMemoized( + () => + tracksSnapshot.data?.any((s) => s.id! == playlist.activeTrack?.id!) == + true && + playlist.activeTrack is SpotubeTrack, + [playlist.activeTrack, tracksSnapshot.data], + ); + return TrackCollectionView( id: album.id!, - isPlaying: isAlbumPlaying, + playingState: isAlbumPlaying && albumTrackPlaying + ? PlayButtonState.playing + : isAlbumPlaying && !albumTrackPlaying + ? PlayButtonState.loading + : PlayButtonState.notPlaying, title: album.name!, titleImage: albumArt, tracksSnapshot: tracksSnapshot, diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index de89429ac..baee06698 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -2,12 +2,14 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/shared/heart_button.dart'; +import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_heading.dart'; import 'package:spotube/components/shared/track_table/track_collection_view/track_collection_view.dart'; import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -31,6 +33,7 @@ class PlaylistView extends HookConsumerWidget { currentTrack ??= sortedTracks.first; final isPlaylistPlaying = proxyPlaylist.containsTracks(tracks); if (!isPlaylistPlaying) { + playback.addCollection(playlist.id!); // for enabling loading indicator await playback.load( sortedTracks, initialIndex: sortedTracks.indexWhere((s) => s.id == currentTrack?.id), @@ -55,8 +58,8 @@ class PlaylistView extends HookConsumerWidget { final tracksSnapshot = useQueries.playlist.tracksOfQuery(ref, playlist.id!); final isPlaylistPlaying = useMemoized( - () => proxyPlaylist.containsTracks(tracksSnapshot.data ?? []), - [playlistNotifier, tracksSnapshot.data], + () => proxyPlaylist.collections.contains(playlist.id!), + [proxyPlaylist, playlist], ); final titleImage = useMemoized( @@ -66,9 +69,22 @@ class PlaylistView extends HookConsumerWidget { ), [playlist.images]); + final playlistTrackPlaying = useMemoized( + () => + tracksSnapshot.data + ?.any((s) => s.id! == proxyPlaylist.activeTrack?.id!) == + true && + proxyPlaylist.activeTrack is SpotubeTrack, + [proxyPlaylist.activeTrack, tracksSnapshot.data], + ); + return TrackCollectionView( id: playlist.id!, - isPlaying: isPlaylistPlaying, + playingState: isPlaylistPlaying && playlistTrackPlaying + ? PlayButtonState.playing + : isPlaylistPlaying && !playlistTrackPlaying + ? PlayButtonState.loading + : PlayButtonState.notPlaying, title: playlist.name!, titleImage: titleImage, tracksSnapshot: tracksSnapshot, diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 0afc7046a..b5d42fb2e 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -290,7 +290,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier final addableTrack = await SpotubeTrack.fetchFromTrack( tracks.elementAtOrNull(initialIndex) ?? tracks.first, youtube, - ); + ).catchError((e, stackTrace) { + return SpotubeTrack.fetchFromTrack( + tracks.elementAtOrNull(initialIndex + 1) ?? tracks.first, + youtube, + ); + }); state = state.copyWith( tracks: mergeTracks([addableTrack], tracks),