From b76e265f23f26238a48ca63202f9a9a4ebcf2e17 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 17 Mar 2024 12:59:50 +0600 Subject: [PATCH] feat: use provider in search page --- lib/pages/search/search.dart | 81 +++++++++--------------- lib/pages/search/sections/albums.dart | 26 ++++---- lib/pages/search/sections/artists.dart | 23 ++----- lib/pages/search/sections/playlists.dart | 24 +++---- lib/pages/search/sections/tracks.dart | 34 ++++------ lib/provider/spotify/search/search.dart | 31 ++------- 6 files changed, 72 insertions(+), 147 deletions(-) diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 9c19ff56..c14e8ff2 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -16,19 +17,19 @@ import 'package:spotube/pages/search/sections/artists.dart'; import 'package:spotube/pages/search/sections/playlists.dart'; import 'package:spotube/pages/search/sections/tracks.dart'; import 'package:spotube/provider/authentication_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/platform.dart'; import 'package:collection/collection.dart'; -final searchTermStateProvider = StateProvider((ref) => ""); - class SearchPage extends HookConsumerWidget { const SearchPage({super.key}); @override Widget build(BuildContext context, ref) { final theme = Theme.of(context); + final controller = useTextEditingController(); + ref.watch(AuthenticationNotifier.provider); final authenticationNotifier = ref.watch(AuthenticationNotifier.provider.notifier); @@ -36,39 +37,14 @@ class SearchPage extends HookConsumerWidget { final searchTerm = ref.watch(searchTermStateProvider); - final searchTrack = - useQueries.search.query(ref, searchTerm, SearchType.track); - final searchAlbum = - useQueries.search.query(ref, searchTerm, SearchType.album); - final searchPlaylist = - useQueries.search.query(ref, searchTerm, SearchType.playlist); - final searchArtist = - useQueries.search.query(ref, searchTerm, SearchType.artist); - - Future onSearch() async { - await Future.wait([ - searchTrack.reset(), - searchAlbum.reset(), - searchPlaylist.reset(), - searchArtist.reset(), - ]).then((_) { - return Future.wait([ - searchTrack.refreshAll(), - searchAlbum.refreshAll(), - searchPlaylist.refreshAll(), - searchArtist.refreshAll(), - ]); - }); - } + final searchTrack = ref.watch(searchProvider(SearchType.track)); + final searchAlbum = ref.watch(searchProvider(SearchType.album)); + final searchPlaylist = ref.watch(searchProvider(SearchType.playlist)); + final searchArtist = ref.watch(searchProvider(SearchType.artist)); final queries = [searchTrack, searchAlbum, searchPlaylist, searchArtist]; - final isFetching = queries.every( - (s) => - (!s.hasPageData && !s.hasPageError) || - s.isRefreshingPage || - !s.hasPageData, - ) && - searchTerm.isNotEmpty; + + final isFetching = queries.every((s) => s.isLoading); final resultWidget = HookBuilder( builder: (context) { @@ -78,18 +54,18 @@ class SearchPage extends HookConsumerWidget { controller: controller, child: SingleChildScrollView( controller: controller, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 8), child: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SearchTracksSection(query: searchTrack), - SearchPlaylistsSection(query: searchPlaylist), - const SizedBox(height: 20), - SearchArtistsSection(query: searchArtist), - const SizedBox(height: 20), - SearchAlbumsSection(query: searchAlbum), + SearchTracksSection(), + SearchPlaylistsSection(), + Gap(20), + SearchArtistsSection(), + Gap(20), + SearchAlbumsSection(), ], ), ), @@ -114,21 +90,22 @@ class SearchPage extends HookConsumerWidget { ), color: theme.scaffoldBackgroundColor, child: TextField( - autofocus: queries - .none((s) => s.hasPageData && !s.hasPageError) && - !kIsMobile, + controller: controller, + autofocus: + queries.none((s) => s.value != null && !s.hasError) && + !kIsMobile, decoration: InputDecoration( prefixIcon: const Icon(SpotubeIcons.search), hintText: "${context.l10n.search}...", ), onSubmitted: (value) async { - ref.read(searchTermStateProvider.notifier).state = - value; - // Fl-Query is too fast, so we need to delay the search - // to prevent spamming the API :) - Timer(const Duration(milliseconds: 50), () { - onSearch(); - }); + Timer( + const Duration(milliseconds: 50), + () { + ref.read(searchTermStateProvider.notifier).state = + value; + }, + ); }, ), ), diff --git a/lib/pages/search/sections/albums.dart b/lib/pages/search/sections/albums.dart index a4a06748..6aa58d46 100644 --- a/lib/pages/search/sections/albums.dart +++ b/lib/pages/search/sections/albums.dart @@ -1,5 +1,3 @@ -import 'package:fl_query/fl_query.dart'; - import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -7,33 +5,33 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class SearchAlbumsSection extends HookConsumerWidget { - final InfiniteQuery>, dynamic, int> query; const SearchAlbumsSection({ - required this.query, super.key, }); @override Widget build(BuildContext context, ref) { + final query = ref.watch(searchProvider(SearchType.album)); + final notifier = ref.watch(searchProvider(SearchType.album).notifier); final albums = useMemoized( - () => query.pages - .expand( - (page) => page.map((p) => p.items!).expand((element) => element), - ) - .whereType() - .map((e) => TypeConversionUtils.simpleAlbum_X_Album(e)) - .toList(), - [query.pages], + () => + query.value?.items + .cast() + .map(TypeConversionUtils.simpleAlbum_X_Album) + .toList() ?? + [], + [query.value], ); return HorizontalPlaybuttonCardView( isLoadingNextPage: query.isLoadingNextPage, - hasNextPage: query.hasNextPage, + hasNextPage: query.value?.hasMore == true, items: albums, - onFetchMore: query.fetchNext, + onFetchMore: notifier.fetchMore, title: Text(context.l10n.albums), ); } diff --git a/lib/pages/search/sections/artists.dart b/lib/pages/search/sections/artists.dart index 84309eec..9b755e65 100644 --- a/lib/pages/search/sections/artists.dart +++ b/lib/pages/search/sections/artists.dart @@ -1,37 +1,28 @@ -import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart' hide Page; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class SearchArtistsSection extends HookConsumerWidget { - final InfiniteQuery>, dynamic, int> query; - const SearchArtistsSection({ super.key, - required this.query, }); @override Widget build(BuildContext context, ref) { - final artists = useMemoized( - () => query.pages - .expand( - (page) => page.map((p) => p.items!).expand((element) => element), - ) - .whereType() - .toList(), - [query.pages], - ); + final query = ref.watch(searchProvider(SearchType.artist)); + final notifier = ref.watch(searchProvider(SearchType.artist).notifier); + + final artists = query.value?.items.cast() ?? []; return HorizontalPlaybuttonCardView( isLoadingNextPage: query.isLoadingNextPage, - hasNextPage: query.hasNextPage, + hasNextPage: query.value?.hasMore == true, items: artists, - onFetchMore: query.fetchNext, + onFetchMore: notifier.fetchMore, title: Text(context.l10n.artists), ); } diff --git a/lib/pages/search/sections/playlists.dart b/lib/pages/search/sections/playlists.dart index 57584ab3..895f4559 100644 --- a/lib/pages/search/sections/playlists.dart +++ b/lib/pages/search/sections/playlists.dart @@ -1,35 +1,27 @@ -import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart' hide Page; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class SearchPlaylistsSection extends HookConsumerWidget { - final InfiniteQuery>, dynamic, int> query; const SearchPlaylistsSection({ - required this.query, super.key, }); @override Widget build(BuildContext context, ref) { - final playlists = useMemoized( - () => query.pages - .expand( - (page) => page.map((p) => p.items!).expand((element) => element), - ) - .whereType() - .toList(), - [query.pages], - ); + final playlistsQuery = ref.watch(searchProvider(SearchType.playlist)); + final playlistsQueryNotifier = + ref.watch(searchProvider(SearchType.playlist).notifier); + final playlists = playlistsQuery.value?.items.cast() ?? []; return HorizontalPlaybuttonCardView( - isLoadingNextPage: query.isLoadingNextPage, - hasNextPage: query.hasNextPage, + isLoadingNextPage: playlistsQuery.isLoadingNextPage, + hasNextPage: playlistsQuery.value?.hasMore == true, items: playlists, - onFetchMore: query.fetchNext, + onFetchMore: playlistsQueryNotifier.fetchMore, title: Text(context.l10n.playlists), ); } diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart index 66bdb9d7..62889285 100644 --- a/lib/pages/search/sections/tracks.dart +++ b/lib/pages/search/sections/tracks.dart @@ -1,32 +1,26 @@ import 'package:collection/collection.dart'; -import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart' hide Page; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class SearchTracksSection extends HookConsumerWidget { - final InfiniteQuery>, dynamic, int> query; const SearchTracksSection({ super.key, - required this.query, }); @override Widget build(BuildContext context, ref) { - final searchTrack = query; - final tracks = useMemoized( - () => searchTrack.pages - .expand( - (page) => page.map((p) => p.items!).expand((element) => element), - ) - .whereType(), - [searchTrack.pages], - ); + final searchTrack = ref.watch(searchProvider(SearchType.track)); + + final searchTrackNotifier = + ref.watch(searchProvider(SearchType.track).notifier); + + final tracks = searchTrack.value?.items.cast() ?? []; final playlistNotifier = ref.watch(ProxyPlaylistNotifier.provider.notifier); final playlist = ref.watch(ProxyPlaylistNotifier.provider); final theme = Theme.of(context); @@ -43,14 +37,10 @@ class SearchTracksSection extends HookConsumerWidget { style: theme.textTheme.titleLarge!, ), ), - if (!searchTrack.hasPageData && - !searchTrack.hasPageError && - !searchTrack.isLoadingNextPage) + if (searchTrack.isLoadingAndEmpty) const CircularProgressIndicator() - else if (searchTrack.hasPageError) - Text( - searchTrack.errors.lastOrNull?.toString() ?? "", - ) + else if (searchTrack.hasError) + Text(searchTrack.error.toString()) else ...tracks.mapIndexed((i, track) { return TrackTile( @@ -81,12 +71,12 @@ class SearchTracksSection extends HookConsumerWidget { }, ); }), - if (searchTrack.hasNextPage && tracks.isNotEmpty) + if (searchTrack.value?.hasMore == true && tracks.isNotEmpty) Center( child: TextButton( onPressed: searchTrack.isLoadingNextPage ? null - : () => searchTrack.fetchNext(), + : () => searchTrackNotifier.fetchMore, child: searchTrack.isLoadingNextPage ? const CircularProgressIndicator() : Text(context.l10n.load_more), diff --git a/lib/provider/spotify/search/search.dart b/lib/provider/spotify/search/search.dart index 70b837dc..4b936df9 100644 --- a/lib/provider/spotify/search/search.dart +++ b/lib/provider/spotify/search/search.dart @@ -1,13 +1,13 @@ part of '../spotify.dart'; +final searchTermStateProvider = StateProvider((ref) => ""); + class SearchState extends PaginatedState { - final String query; SearchState({ required super.items, required super.offset, required super.limit, required super.hasMore, - required this.query, }); @override @@ -16,14 +16,12 @@ class SearchState extends PaginatedState { int? offset, int? limit, bool? hasMore, - String? query, }) { return SearchState( items: items ?? this.items, offset: offset ?? this.offset, limit: limit ?? this.limit, hasMore: hasMore ?? this.hasMore, - query: query ?? this.query, ); } } @@ -37,7 +35,7 @@ class SearchNotifier if (state.value == null) return []; final results = await spotify.search .get( - state.value!.query, + ref.read(searchTermStateProvider), types: [arg], market: ref.read(userPreferencesProvider).recommendationMarket, ) @@ -48,6 +46,7 @@ class SearchNotifier @override build(arg) async { + ref.watch(searchTermStateProvider); ref.watch(spotifyProvider); ref.watch( userPreferencesProvider.select((value) => value.recommendationMarket), @@ -60,30 +59,8 @@ class SearchNotifier offset: 0, limit: 10, hasMore: results.length == 10, - query: "", ); } - - Future search(String query) async { - if (state.value == null) return; - - state = AsyncData( - state.value!.copyWith( - query: query, - ), - ); - - await update((state) async { - final results = await fetch(arg, 0, 10); - - return state.copyWith( - items: results, - offset: 0, - limit: 10, - hasMore: results.length == 10, - ); - }); - } } final searchProvider =