feat: paginated user albums

This commit is contained in:
Kingkor Roy Tirtho 2023-08-25 18:54:54 +06:00
parent 6ced0a0fad
commit d239d641ff
6 changed files with 100 additions and 64 deletions

View File

@ -50,8 +50,13 @@ class AlbumCard extends HookConsumerWidget {
() => playlist.containsCollection(album.id!), () => playlist.containsCollection(album.id!),
[playlist, album.id], [playlist, album.id],
); );
final int marginH =
useBreakpointValue(xs: 10, sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); final marginH = useBreakpointValue<int>(
xs: 10,
sm: 10,
md: 15,
others: 20,
);
final updating = useState(false); final updating = useState(false);
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);

View File

@ -3,13 +3,14 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.dart'; import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -23,80 +24,90 @@ class UserAlbums extends HookConsumerWidget {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final albumsQuery = useQueries.album.ofMine(ref); final albumsQuery = useQueries.album.ofMine(ref);
final spacing = useBreakpointValue<double>( final controller = useScrollController();
xs: 0,
sm: 0,
others: 20,
);
final searchText = useState(''); final searchText = useState('');
final allAlbums = useMemoized(
() => albumsQuery.pages
.expand((element) => element.items ?? <AlbumSimple>[]),
[albumsQuery.pages],
);
final albums = useMemoized(() { final albums = useMemoized(() {
if (searchText.value.isEmpty) { if (searchText.value.isEmpty) {
return albumsQuery.data?.toList() ?? []; return allAlbums;
} }
return albumsQuery.data return allAlbums
?.map((e) => ( .map((e) => (
weightedRatio(e.name!, searchText.value), weightedRatio(e.name!, searchText.value),
e, e,
)) ))
.sorted((a, b) => b.$1.compareTo(a.$1)) .sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.$1 > 50) .where((e) => e.$1 > 50)
.map((e) => e.$2) .map((e) => e.$2)
.toList() ?? .toList();
[]; }, [allAlbums, searchText.value]);
}, [albumsQuery.data, searchText.value]);
if (auth == null) { if (auth == null) {
return const AnonymousFallback(); return const AnonymousFallback();
} }
final theme = Theme.of(context);
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await albumsQuery.refresh(); await albumsQuery.refresh();
}, },
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SafeArea( child: SafeArea(
child: Column( child: Scaffold(
crossAxisAlignment: CrossAxisAlignment.start, appBar: PreferredSize(
children: [ preferredSize: const Size.fromHeight(50),
SearchBar( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ColoredBox(
color: theme.scaffoldBackgroundColor,
child: SearchBar(
onChanged: (value) => searchText.value = value, onChanged: (value) => searchText.value = value,
leading: const Icon(SpotubeIcons.filter), leading: const Icon(SpotubeIcons.filter),
hintText: context.l10n.filter_albums, hintText: context.l10n.filter_artist,
), ),
const SizedBox(height: 20), ),
AnimatedCrossFade( ),
duration: const Duration(milliseconds: 300), ),
firstChild: Container( body: SizedBox.expand(
child: SingleChildScrollView(
padding: const EdgeInsets.all(8.0),
controller: controller,
child: Wrap(
runSpacing: 20,
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (albums.isEmpty)
Container(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: const ShimmerPlaybuttonCard(count: 7), child: const ShimmerPlaybuttonCard(count: 4),
), ),
secondChild: Wrap( for (final album in albums)
spacing: spacing, // gap between adjacent chips AlbumCard(
runSpacing: 20, // gap between lines
alignment: WrapAlignment.center,
children: albums
.map((album) => AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(album), TypeConversionUtils.simpleAlbum_X_Album(album),
))
.toList(),
),
crossFadeState: albumsQuery.isLoading ||
!albumsQuery.hasData ||
searchText.value.isNotEmpty
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
), ),
if (albumsQuery.hasNextPage)
Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: albumsQuery.fetchNext,
child: const ShimmerPlaybuttonCard(count: 1),
)
], ],
), ),
), ),
), ),
), ),
),
); );
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -188,6 +189,7 @@ class AlbumHeartButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final client = useQueryClient();
final me = useQueries.user.me(ref); final me = useQueries.user.me(ref);
final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!); final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!);
@ -196,10 +198,10 @@ class AlbumHeartButton extends HookConsumerWidget {
final toggleAlbumLike = useMutations.album.toggleFavorite( final toggleAlbumLike = useMutations.album.toggleFavorite(
ref, ref,
album.id!, album.id!,
refreshQueries: [ refreshQueries: [albumIsSaved.key],
albumIsSaved.key, onData: (_, __) async {
"current-user-albums", await client.refreshInfiniteQueryAllPages("current-user-albums");
], },
); );
if (me.isLoading || !me.hasData) { if (me.isLoading || !me.hasData) {

View File

@ -532,7 +532,13 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
final cached = await SkipSegment.box.get(id); final cached = await SkipSegment.box.get(id);
if (cached != null && cached.isNotEmpty) { if (cached != null && cached.isNotEmpty) {
return List.castFrom<dynamic, SkipSegment>( return List.castFrom<dynamic, SkipSegment>(
(cached as List).map((json) => SkipSegment.fromJson(json)).toList(), (cached as List)
.map(
(json) => SkipSegment.fromJson(
Map.castFrom<dynamic, dynamic, String, dynamic>(json),
),
)
.toList(),
); );
} }

View File

@ -9,6 +9,8 @@ class AlbumMutations {
WidgetRef ref, WidgetRef ref,
String albumId, { String albumId, {
List<String>? refreshQueries, List<String>? refreshQueries,
List<String>? refreshInfiniteQueries,
MutationOnDataFn<bool, dynamic>? onData,
}) { }) {
return useSpotifyMutation<bool, dynamic, bool, dynamic>( return useSpotifyMutation<bool, dynamic, bool, dynamic>(
"toggle-album-like/$albumId", "toggle-album-like/$albumId",
@ -22,6 +24,8 @@ class AlbumMutations {
}, },
ref: ref, ref: ref,
refreshQueries: refreshQueries, refreshQueries: refreshQueries,
refreshInfiniteQueries: refreshInfiniteQueries,
onData: onData,
); );
} }
} }

View File

@ -9,12 +9,20 @@ import 'package:spotube/provider/user_preferences_provider.dart';
class AlbumQueries { class AlbumQueries {
const AlbumQueries(); const AlbumQueries();
Query<Iterable<AlbumSimple>, dynamic> ofMine(WidgetRef ref) { InfiniteQuery<Page<AlbumSimple>, dynamic, int> ofMine(WidgetRef ref) {
return useSpotifyQuery<Iterable<AlbumSimple>, dynamic>( return useSpotifyInfiniteQuery<Page<AlbumSimple>, dynamic, int>(
"current-user-albums", "current-user-albums",
(spotify) { (page, spotify) {
return spotify.me.savedAlbums().all(); return spotify.me.savedAlbums().getPage(
20,
page * 20,
);
}, },
initialPage: 0,
nextPage: (lastPage, lastPageData) =>
(lastPageData.items?.length ?? 0) < 20 || lastPageData.isLast
? null
: lastPage + 1,
ref: ref, ref: ref,
); );
} }