Skip to content

Commit

Permalink
feat: paginated playlist and album page
Browse files Browse the repository at this point in the history
  • Loading branch information
KRTirtho committed Nov 17, 2023
1 parent 14069cd commit 28a5d6b
Show file tree
Hide file tree
Showing 34 changed files with 1,372 additions and 1,297 deletions.
16 changes: 10 additions & 6 deletions lib/collections/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart' hide Search;
import 'package:spotube/pages/album/album.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
import 'package:spotube/pages/playlist/liked_playlist.dart';
import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/pages/settings/logs.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/shared/spotube_page_route.dart';
import 'package:spotube/pages/album/album.dart';
import 'package:spotube/pages/artist/artist.dart';
import 'package:spotube/pages/library/library.dart';
import 'package:spotube/pages/desktop_login/login_tutorial.dart';
import 'package:spotube/pages/desktop_login/desktop_login.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/pages/root/root_app.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart';

import '../pages/library/playlist_generate/playlist_generate_result.dart';

final rootNavigatorKey = Catcher2.navigatorKey;
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter(
Expand Down Expand Up @@ -104,7 +104,9 @@ final router = GoRouter(
path: "/album/:id",
pageBuilder: (context, state) {
assert(state.extra is AlbumSimple);
return SpotubePage(child: AlbumPage(state.extra as AlbumSimple));
return SpotubePage(
child: AlbumPage(album: state.extra as AlbumSimple),
);
},
),
GoRoute(
Expand All @@ -119,7 +121,9 @@ final router = GoRouter(
pageBuilder: (context, state) {
assert(state.extra is PlaylistSimple);
return SpotubePage(
child: PlaylistView(state.extra as PlaylistSimple),
child: state.pathParameters["id"] == "user-liked-tracks"
? LikedPlaylistPage(playlist: state.extra as PlaylistSimple)
: PlaylistPage(playlist: state.extra as PlaylistSimple),
);
},
),
Expand Down
81 changes: 49 additions & 32 deletions lib/components/album/album_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/infinite_query.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/queries/album.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';

Expand All @@ -15,7 +18,7 @@ extension FormattedAlbumType on AlbumType {
}

class AlbumCard extends HookConsumerWidget {
final Album album;
final AlbumSimple album;
const AlbumCard(
this.album, {
Key? key,
Expand All @@ -27,7 +30,9 @@ class AlbumCard extends HookConsumerWidget {
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);

final queryClient = useQueryClient();

bool isPlaylistPlaying = useMemoized(
() => playlist.containsCollection(album.id!),
[playlist, album.id],
Expand All @@ -36,6 +41,34 @@ class AlbumCard extends HookConsumerWidget {
final updating = useState(false);
final spotify = ref.watch(spotifyProvider);

final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);

Future<List<Track>> fetchAllTrack() async {
if (album.tracks != null && album.tracks!.isNotEmpty) {
return album.tracks!
.map((track) =>
TypeConversionUtils.simpleTrack_X_Track(track, album))
.toList();
}
final job = AlbumQueries.tracksOfJob(album.id!);

final query = queryClient.createInfiniteQuery(
job.queryKey,
(page) => job.task(page, (spotify: spotify, album: album)),
initialPage: 0,
nextPage: job.nextPage,
);

return await query.fetchAllTracks(
getAllTracks: () async {
final res = await spotify.albums.tracks(album.id!).all();
return res
.map((e) => TypeConversionUtils.simpleTrack_X_Track(e, album))
.toList();
},
);
}

return PlaybuttonCard(
imageUrl: TypeConversionUtils.image_X_UrlString(
album.images,
Expand All @@ -54,20 +87,15 @@ class AlbumCard extends HookConsumerWidget {
onPlaybuttonPressed: () async {
updating.value = true;
try {
if (isPlaylistPlaying && playing) {
return audioPlayer.pause();
} else if (isPlaylistPlaying && !playing) {
return audioPlayer.resume();
if (isPlaylistPlaying) {
return playing ? audioPlayer.pause() : audioPlayer.resume();
}

await playlistNotifier.load(
album.tracks
?.map((e) =>
TypeConversionUtils.simpleTrack_X_Track(e, album))
.toList() ??
[],
autoPlay: true,
);
final fetchedTracks = await fetchAllTrack();

if (fetchedTracks.isEmpty) return;

await playlistNotifier.load(fetchedTracks, autoPlay: true);
playlistNotifier.addCollection(album.id!);
} finally {
updating.value = false;
Expand All @@ -80,28 +108,16 @@ class AlbumCard extends HookConsumerWidget {

updating.value = true;
try {
final fetchedTracks =
await queryClient.fetchQuery<List<TrackSimple>, SpotifyApi>(
"album-tracks/${album.id}",
() {
return spotify.albums
.tracks(album.id!)
.all()
.then((value) => value.toList());
},
).then(
(tracks) => tracks
?.map(
(e) => TypeConversionUtils.simpleTrack_X_Track(e, album))
.toList(),
);

if (fetchedTracks == null || fetchedTracks.isEmpty) return;
final fetchedTracks = await fetchAllTrack();

if (fetchedTracks.isEmpty) return;
playlistNotifier.addTracks(fetchedTracks);
playlistNotifier.addCollection(album.id!);
if (context.mounted) {
final snackbar = SnackBar(
content: Text("Added ${album.tracks?.length} tracks to queue"),
content: Text(
context.l10n.added_to_queue(fetchedTracks.length),
),
action: SnackBarAction(
label: "Undo",
onPressed: () {
Expand All @@ -110,7 +126,8 @@ class AlbumCard extends HookConsumerWidget {
},
),
);
ScaffoldMessenger.maybeOf(context)?.showSnackBar(snackbar);

scaffoldMessenger?.showSnackBar(snackbar);
}
} finally {
updating.value = false;
Expand Down
10 changes: 6 additions & 4 deletions lib/components/library/user_local_tracks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import 'package:spotube/components/shared/expandable_search/expandable_search.da
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
Expand Down Expand Up @@ -199,7 +199,8 @@ class UserLocalTracks extends HookConsumerWidget {
),
const Spacer(),
ExpandableSearchButton(
isFiltering: isFiltering,
isFiltering: isFiltering.value,
onPressed: (value) => isFiltering.value = value,
searchFocus: searchFocus,
),
const SizedBox(width: 10),
Expand All @@ -222,7 +223,8 @@ class UserLocalTracks extends HookConsumerWidget {
ExpandableSearchField(
searchController: searchController,
searchFocus: searchFocus,
isFiltering: isFiltering,
isFiltering: isFiltering.value,
onChangeFiltering: (value) => isFiltering.value = value,
),
trackSnapshot.when(
data: (tracks) {
Expand Down Expand Up @@ -284,7 +286,7 @@ class UserLocalTracks extends HookConsumerWidget {
);
},
loading: () =>
const Expanded(child: ShimmerTrackTile(noSliver: true)),
const Expanded(child: ShimmerTrackTileGroup(noSliver: true)),
error: (error, stackTrace) =>
Text(error.toString() + stackTrace.toString()),
)
Expand Down
2 changes: 1 addition & 1 deletion lib/components/player/player_queue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
Expand Down
48 changes: 30 additions & 18 deletions lib/components/playlist/playlist_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/extensions/infinite_query.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
Expand All @@ -23,7 +24,7 @@ class PlaylistCard extends HookConsumerWidget {
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final queryBowl = QueryClient.of(context);
final queryClient = QueryClient.of(context);
final tracks = useState<List<TrackSimple>?>(null);
bool isPlaylistPlaying = useMemoized(
() => playlistQueue.containsCollection(playlist.id!),
Expand All @@ -34,6 +35,31 @@ class PlaylistCard extends HookConsumerWidget {
final spotify = ref.watch(spotifyProvider);
final me = useQueries.user.me(ref);

Future<List<Track>> fetchAllTracks() async {
if (playlist.id == 'user-liked-tracks') {
return await queryClient.fetchQuery(
"user-liked-tracks",
() => useQueries.playlist.likedTracks(spotify),
) ??
[];
}

final query = queryClient.createInfiniteQuery<List<Track>, dynamic, int>(
"playlist-tracks/${playlist.id}",
(page) => useQueries.playlist.tracksOf(page, spotify, playlist.id!),
initialPage: 0,
nextPage: useQueries.playlist.tracksOfQueryNextPage,
);

return await query.fetchAllTracks(
getAllTracks: () async {
final res =
await spotify.playlists.getTracksByPlaylistId(playlist.id!).all();
return res.toList();
},
);
}

return PlaybuttonCard(
margin: const EdgeInsets.symmetric(horizontal: 10),
title: playlist.name!,
Expand Down Expand Up @@ -62,18 +88,7 @@ class PlaylistCard extends HookConsumerWidget {
return audioPlayer.resume();
}

List<Track> fetchedTracks = playlist.id == 'user-liked-tracks'
? await queryBowl.fetchQuery(
"user-liked-tracks",
() => useQueries.playlist.likedTracks(spotify, ref),
) ??
[]
: await queryBowl.fetchQuery(
"playlist-tracks/${playlist.id}",
() => useQueries.playlist
.tracksOf(playlist.id!, spotify, ref),
) ??
[];
List<Track> fetchedTracks = await fetchAllTracks();

if (fetchedTracks.isEmpty) return;

Expand All @@ -90,11 +105,8 @@ class PlaylistCard extends HookConsumerWidget {
updating.value = true;
try {
if (isPlaylistPlaying) return;
List<Track> fetchedTracks = await queryBowl.fetchQuery(
"playlist-tracks/${playlist.id}",
() => useQueries.playlist.tracksOf(playlist.id!, spotify, ref),
) ??
[];

final fetchedTracks = await fetchAllTracks();

if (fetchedTracks.isEmpty) return;

Expand Down
21 changes: 11 additions & 10 deletions lib/components/shared/expandable_search/expandable_search.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/context.dart';

class ExpandableSearchField extends StatelessWidget {
final ValueNotifier<bool> isFiltering;
final bool isFiltering;
final ValueChanged<bool> onChangeFiltering;
final TextEditingController searchController;
final FocusNode searchFocus;

const ExpandableSearchField({
Key? key,
required this.isFiltering,
required this.onChangeFiltering,
required this.searchController,
required this.searchFocus,
}) : super(key: key);
Expand All @@ -19,17 +21,17 @@ class ExpandableSearchField extends StatelessWidget {
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: isFiltering.value ? 1 : 0,
opacity: isFiltering ? 1 : 0,
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
child: SizedBox(
height: isFiltering.value ? 50 : 0,
height: isFiltering ? 50 : 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: CallbackShortcuts(
bindings: {
LogicalKeySet(LogicalKeyboardKey.escape): () {
isFiltering.value = false;
onChangeFiltering(false);
searchController.clear();
searchFocus.unfocus();
}
Expand All @@ -52,7 +54,7 @@ class ExpandableSearchField extends StatelessWidget {
}

class ExpandableSearchButton extends StatelessWidget {
final ValueNotifier<bool> isFiltering;
final bool isFiltering;
final FocusNode searchFocus;
final Widget icon;
final ValueChanged<bool>? onPressed;
Expand All @@ -73,18 +75,17 @@ class ExpandableSearchButton extends StatelessWidget {
icon: icon,
style: IconButton.styleFrom(
backgroundColor:
isFiltering.value ? theme.colorScheme.secondaryContainer : null,
foregroundColor: isFiltering.value ? theme.colorScheme.secondary : null,
isFiltering ? theme.colorScheme.secondaryContainer : null,
foregroundColor: isFiltering ? theme.colorScheme.secondary : null,
minimumSize: const Size(25, 25),
),
onPressed: () {
isFiltering.value = !isFiltering.value;
if (isFiltering.value) {
if (isFiltering) {
searchFocus.requestFocus();
} else {
searchFocus.unfocus();
}
onPressed?.call(isFiltering.value);
onPressed?.call(!isFiltering);
},
);
}
Expand Down
Loading

0 comments on commit 28a5d6b

Please sign in to comment.