From 0b58155d434f2de6359be77d7beee4484dbb7b2a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 5 Jan 2023 22:26:54 +0600 Subject: [PATCH] feat(user-library): filtering support for user albums and user artists --- lib/components/library/user_albums.dart | 68 ++++++++++--- lib/components/library/user_artists.dart | 109 ++++++++++++++------- lib/components/library/user_playlists.dart | 28 +++--- 3 files changed, 142 insertions(+), 63 deletions(-) diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 8d1d570c..e8301471 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -1,15 +1,21 @@ import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart' hide Image; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:collection/collection.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/components/album/album_card.dart'; +import 'package:spotube/components/shared/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/hooks/use_breakpoint_value.dart'; import 'package:spotube/provider/auth_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:tuple/tuple.dart'; class UserAlbums extends HookConsumerWidget { const UserAlbums({Key? key}) : super(key: key); @@ -17,14 +23,39 @@ class UserAlbums extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final auth = ref.watch(authProvider); - if (auth.isAnonymous) { - return const AnonymousFallback(); - } final albumsQuery = useQuery( job: Queries.album.ofMine, externalData: ref.watch(spotifyProvider), ); + final spacing = useBreakpointValue( + sm: 0, + others: 20, + ); + final viewType = MediaQuery.of(context).size.width < 480 + ? PlaybuttonCardViewType.list + : PlaybuttonCardViewType.square; + + final searchText = useState(''); + + final albums = useMemoized(() { + return albumsQuery.data + ?.map((e) => Tuple2( + searchText.value.isEmpty + ? 100 + : weightedRatio(e.name!, searchText.value), + e, + )) + .sorted((a, b) => b.item1.compareTo(a.item1)) + .where((e) => e.item1 > 50) + .map((e) => e.item2) + .toList() ?? + []; + }, [albumsQuery.data, searchText.value]); + + if (auth.isAnonymous) { + return const AnonymousFallback(); + } if (albumsQuery.isLoading || !albumsQuery.hasData) { return const Center(child: ShimmerPlaybuttonCard(count: 7)); } @@ -34,17 +65,28 @@ class UserAlbums extends HookConsumerWidget { type: MaterialType.transparency, textStyle: PlatformTheme.of(context).textTheme!.body!, color: PlatformTheme.of(context).scaffoldBackgroundColor, - child: Container( - width: double.infinity, + child: Padding( padding: const EdgeInsets.all(8.0), - child: Wrap( - spacing: 20, // gap between adjacent chips - runSpacing: 20, // gap between lines - alignment: WrapAlignment.center, - children: albumsQuery.data! - .map((album) => - AlbumCard(TypeConversionUtils.simpleAlbum_X_Album(album))) - .toList(), + child: Column( + children: [ + PlatformTextField( + onChanged: (value) => searchText.value = value, + prefixIcon: Icons.filter_alt_outlined, + placeholder: 'Filter Albums...', + ), + const SizedBox(height: 20), + Wrap( + spacing: spacing, // gap between adjacent chips + runSpacing: 20, // gap between lines + alignment: WrapAlignment.center, + children: albums + .map((album) => AlbumCard( + viewType: viewType, + TypeConversionUtils.simpleAlbum_X_Album(album), + )) + .toList(), + ), + ], ), ), ), diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index b78cd5f2..294470da 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -1,6 +1,8 @@ import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:collection/collection.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:platform_ui/platform_ui.dart'; import 'package:spotify/spotify.dart'; @@ -10,6 +12,7 @@ import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/provider/auth_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/services/queries/queries.dart'; +import 'package:tuple/tuple.dart'; class UserArtists extends HookConsumerWidget { const UserArtists({Key? key}) : super(key: key); @@ -17,53 +20,87 @@ class UserArtists extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final auth = ref.watch(authProvider); - if (auth.isAnonymous) { - return const AnonymousFallback(); - } + final artistQuery = useInfiniteQuery( job: Queries.artist.followedByMe, externalData: ref.watch(spotifyProvider), ); - final artists = useMemoized( - () => artistQuery.pages - .expand((page) => page?.items ?? const Iterable.empty()) - .toList(), - [artistQuery.pages]); - final hasNextPage = artistQuery.pages.isEmpty ? false : (artistQuery.pages.last?.items?.length ?? 0) == 15; - return Material( - type: MaterialType.transparency, - textStyle: PlatformTheme.of(context).textTheme!.body!, - color: PlatformTheme.of(context).scaffoldBackgroundColor, - child: GridView.builder( - itemCount: artists.length, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisExtent: 250, - crossAxisSpacing: 20, - mainAxisSpacing: 20, + final searchText = useState(''); + + final filteredArtists = useMemoized(() { + return artistQuery.pages + .expand((page) => page?.items ?? const Iterable.empty()) + .map((e) => Tuple2( + searchText.value.isEmpty + ? 100 + : weightedRatio(e.name!, searchText.value), + e, + )) + .sorted((a, b) => b.item1.compareTo(a.item1)) + .where((e) => e.item1 > 50) + .map((e) => e.item2) + .toList(); + }, [artistQuery.pages, searchText.value]); + + if (auth.isAnonymous) { + return const AnonymousFallback(); + } + + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(50), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: PlatformTextField( + onChanged: (value) => searchText.value = value, + prefixIcon: Icons.filter_alt_outlined, + placeholder: 'Filter artists...', + ), ), - padding: const EdgeInsets.all(10), - itemBuilder: (context, index) { - return HookBuilder(builder: (context) { - if (index == artists.length - 1 && hasNextPage) { - return Waypoint( - controller: useScrollController(), - isGrid: true, - onTouchEdge: () { - artistQuery.fetchNextPage(); - }, - child: ArtistCard(artists[index]), - ); - } - return ArtistCard(artists[index]); - }); - }, ), + backgroundColor: PlatformTheme.of(context).scaffoldBackgroundColor, + body: (artistQuery.isLoading || !artistQuery.hasData) + ? Padding( + padding: const EdgeInsets.all(20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + PlatformCircularProgressIndicator(), + SizedBox(width: 10), + PlatformText("Loading..."), + ], + ), + ) + : GridView.builder( + itemCount: filteredArtists.length, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: 250, + crossAxisSpacing: 20, + mainAxisSpacing: 20, + ), + padding: const EdgeInsets.all(10), + itemBuilder: (context, index) { + return HookBuilder(builder: (context) { + if (index == artistQuery.pages.length - 1 && hasNextPage) { + return Waypoint( + controller: useScrollController(), + isGrid: true, + onTouchEdge: () { + artistQuery.fetchNextPage(); + }, + child: ArtistCard(filteredArtists[index]), + ); + } + return ArtistCard(filteredArtists[index]); + }); + }, + ), ); } } diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 4472dec4..11a3646a 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -72,9 +72,6 @@ class UserPlaylists extends HookConsumerWidget { if (auth.isAnonymous) { return const AnonymousFallback(); } - if (playlistsQuery.isLoading || !playlistsQuery.hasData) { - return const Center(child: ShimmerPlaybuttonCard(count: 7)); - } final children = [ const PlaylistCreateDialog(), @@ -95,20 +92,23 @@ class UserPlaylists extends HookConsumerWidget { children: [ PlatformTextField( onChanged: (value) => searchText.value = value, - placeholder: "Search your playlists...", - prefixIcon: Icons.search, + placeholder: "Filter your playlists...", + prefixIcon: Icons.filter_alt_outlined, ), const SizedBox(height: 20), - Center( - child: Wrap( - spacing: spacing, // gap between adjacent chips - runSpacing: 20, // gap between lines - alignment: breakpoint.isSm - ? WrapAlignment.center - : WrapAlignment.start, - children: children, + if (playlistsQuery.isLoading || !playlistsQuery.hasData) + const Center(child: ShimmerPlaybuttonCard(count: 7)) + else + Center( + child: Wrap( + spacing: spacing, // gap between adjacent chips + runSpacing: 20, // gap between lines + alignment: breakpoint.isSm + ? WrapAlignment.center + : WrapAlignment.start, + children: children, + ), ), - ), ], ), ),