feat: use provider in search page

This commit is contained in:
Kingkor Roy Tirtho 2024-03-17 12:59:50 +06:00
parent 25badae7ad
commit b76e265f23
6 changed files with 72 additions and 147 deletions

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.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/playlists.dart';
import 'package:spotube/pages/search/sections/tracks.dart'; import 'package:spotube/pages/search/sections/tracks.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/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
final searchTermStateProvider = StateProvider<String>((ref) => "");
class SearchPage extends HookConsumerWidget { class SearchPage extends HookConsumerWidget {
const SearchPage({super.key}); const SearchPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final controller = useTextEditingController();
ref.watch(AuthenticationNotifier.provider); ref.watch(AuthenticationNotifier.provider);
final authenticationNotifier = final authenticationNotifier =
ref.watch(AuthenticationNotifier.provider.notifier); ref.watch(AuthenticationNotifier.provider.notifier);
@ -36,39 +37,14 @@ class SearchPage extends HookConsumerWidget {
final searchTerm = ref.watch(searchTermStateProvider); final searchTerm = ref.watch(searchTermStateProvider);
final searchTrack = final searchTrack = ref.watch(searchProvider(SearchType.track));
useQueries.search.query(ref, searchTerm, SearchType.track); final searchAlbum = ref.watch(searchProvider(SearchType.album));
final searchAlbum = final searchPlaylist = ref.watch(searchProvider(SearchType.playlist));
useQueries.search.query(ref, searchTerm, SearchType.album); final searchArtist = ref.watch(searchProvider(SearchType.artist));
final searchPlaylist =
useQueries.search.query(ref, searchTerm, SearchType.playlist);
final searchArtist =
useQueries.search.query(ref, searchTerm, SearchType.artist);
Future<void> 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 queries = [searchTrack, searchAlbum, searchPlaylist, searchArtist]; final queries = [searchTrack, searchAlbum, searchPlaylist, searchArtist];
final isFetching = queries.every(
(s) => final isFetching = queries.every((s) => s.isLoading);
(!s.hasPageData && !s.hasPageError) ||
s.isRefreshingPage ||
!s.hasPageData,
) &&
searchTerm.isNotEmpty;
final resultWidget = HookBuilder( final resultWidget = HookBuilder(
builder: (context) { builder: (context) {
@ -78,18 +54,18 @@ class SearchPage extends HookConsumerWidget {
controller: controller, controller: controller,
child: SingleChildScrollView( child: SingleChildScrollView(
controller: controller, controller: controller,
child: Padding( child: const Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: EdgeInsets.symmetric(vertical: 8),
child: SafeArea( child: SafeArea(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SearchTracksSection(query: searchTrack), SearchTracksSection(),
SearchPlaylistsSection(query: searchPlaylist), SearchPlaylistsSection(),
const SizedBox(height: 20), Gap(20),
SearchArtistsSection(query: searchArtist), SearchArtistsSection(),
const SizedBox(height: 20), Gap(20),
SearchAlbumsSection(query: searchAlbum), SearchAlbumsSection(),
], ],
), ),
), ),
@ -114,21 +90,22 @@ class SearchPage extends HookConsumerWidget {
), ),
color: theme.scaffoldBackgroundColor, color: theme.scaffoldBackgroundColor,
child: TextField( child: TextField(
autofocus: queries controller: controller,
.none((s) => s.hasPageData && !s.hasPageError) && autofocus:
queries.none((s) => s.value != null && !s.hasError) &&
!kIsMobile, !kIsMobile,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(SpotubeIcons.search), prefixIcon: const Icon(SpotubeIcons.search),
hintText: "${context.l10n.search}...", hintText: "${context.l10n.search}...",
), ),
onSubmitted: (value) async { onSubmitted: (value) async {
Timer(
const Duration(milliseconds: 50),
() {
ref.read(searchTermStateProvider.notifier).state = ref.read(searchTermStateProvider.notifier).state =
value; value;
// Fl-Query is too fast, so we need to delay the search },
// to prevent spamming the API :) );
Timer(const Duration(milliseconds: 50), () {
onSearch();
});
}, },
), ),
), ),

View File

@ -1,5 +1,3 @@
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
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';
@ -7,33 +5,33 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class SearchAlbumsSection extends HookConsumerWidget { class SearchAlbumsSection extends HookConsumerWidget {
final InfiniteQuery<List<Page<dynamic>>, dynamic, int> query;
const SearchAlbumsSection({ const SearchAlbumsSection({
required this.query,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final query = ref.watch(searchProvider(SearchType.album));
final notifier = ref.watch(searchProvider(SearchType.album).notifier);
final albums = useMemoized( final albums = useMemoized(
() => query.pages () =>
.expand( query.value?.items
(page) => page.map((p) => p.items!).expand((element) => element), .cast<AlbumSimple>()
) .map(TypeConversionUtils.simpleAlbum_X_Album)
.whereType<AlbumSimple>() .toList() ??
.map((e) => TypeConversionUtils.simpleAlbum_X_Album(e)) [],
.toList(), [query.value],
[query.pages],
); );
return HorizontalPlaybuttonCardView( return HorizontalPlaybuttonCardView(
isLoadingNextPage: query.isLoadingNextPage, isLoadingNextPage: query.isLoadingNextPage,
hasNextPage: query.hasNextPage, hasNextPage: query.value?.hasMore == true,
items: albums, items: albums,
onFetchMore: query.fetchNext, onFetchMore: notifier.fetchMore,
title: Text(context.l10n.albums), title: Text(context.l10n.albums),
); );
} }

View File

@ -1,37 +1,28 @@
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class SearchArtistsSection extends HookConsumerWidget { class SearchArtistsSection extends HookConsumerWidget {
final InfiniteQuery<List<Page<dynamic>>, dynamic, int> query;
const SearchArtistsSection({ const SearchArtistsSection({
super.key, super.key,
required this.query,
}); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final artists = useMemoized( final query = ref.watch(searchProvider(SearchType.artist));
() => query.pages final notifier = ref.watch(searchProvider(SearchType.artist).notifier);
.expand(
(page) => page.map((p) => p.items!).expand((element) => element), final artists = query.value?.items.cast<Artist>() ?? [];
)
.whereType<Artist>()
.toList(),
[query.pages],
);
return HorizontalPlaybuttonCardView<Artist>( return HorizontalPlaybuttonCardView<Artist>(
isLoadingNextPage: query.isLoadingNextPage, isLoadingNextPage: query.isLoadingNextPage,
hasNextPage: query.hasNextPage, hasNextPage: query.value?.hasMore == true,
items: artists, items: artists,
onFetchMore: query.fetchNext, onFetchMore: notifier.fetchMore,
title: Text(context.l10n.artists), title: Text(context.l10n.artists),
); );
} }

View File

@ -1,35 +1,27 @@
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class SearchPlaylistsSection extends HookConsumerWidget { class SearchPlaylistsSection extends HookConsumerWidget {
final InfiniteQuery<List<Page<dynamic>>, dynamic, int> query;
const SearchPlaylistsSection({ const SearchPlaylistsSection({
required this.query,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlists = useMemoized( final playlistsQuery = ref.watch(searchProvider(SearchType.playlist));
() => query.pages final playlistsQueryNotifier =
.expand( ref.watch(searchProvider(SearchType.playlist).notifier);
(page) => page.map((p) => p.items!).expand((element) => element), final playlists = playlistsQuery.value?.items.cast<PlaylistSimple>() ?? [];
)
.whereType<PlaylistSimple>()
.toList(),
[query.pages],
);
return HorizontalPlaybuttonCardView( return HorizontalPlaybuttonCardView(
isLoadingNextPage: query.isLoadingNextPage, isLoadingNextPage: playlistsQuery.isLoadingNextPage,
hasNextPage: query.hasNextPage, hasNextPage: playlistsQuery.value?.hasMore == true,
items: playlists, items: playlists,
onFetchMore: query.fetchNext, onFetchMore: playlistsQueryNotifier.fetchMore,
title: Text(context.l10n.playlists), title: Text(context.l10n.playlists),
); );
} }

View File

@ -1,32 +1,26 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart'; import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class SearchTracksSection extends HookConsumerWidget { class SearchTracksSection extends HookConsumerWidget {
final InfiniteQuery<List<Page<dynamic>>, dynamic, int> query;
const SearchTracksSection({ const SearchTracksSection({
super.key, super.key,
required this.query,
}); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final searchTrack = query; final searchTrack = ref.watch(searchProvider(SearchType.track));
final tracks = useMemoized(
() => searchTrack.pages final searchTrackNotifier =
.expand( ref.watch(searchProvider(SearchType.track).notifier);
(page) => page.map((p) => p.items!).expand((element) => element),
) final tracks = searchTrack.value?.items.cast<Track>() ?? [];
.whereType<Track>(),
[searchTrack.pages],
);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.provider.notifier); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.provider.notifier);
final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final theme = Theme.of(context); final theme = Theme.of(context);
@ -43,14 +37,10 @@ class SearchTracksSection extends HookConsumerWidget {
style: theme.textTheme.titleLarge!, style: theme.textTheme.titleLarge!,
), ),
), ),
if (!searchTrack.hasPageData && if (searchTrack.isLoadingAndEmpty)
!searchTrack.hasPageError &&
!searchTrack.isLoadingNextPage)
const CircularProgressIndicator() const CircularProgressIndicator()
else if (searchTrack.hasPageError) else if (searchTrack.hasError)
Text( Text(searchTrack.error.toString())
searchTrack.errors.lastOrNull?.toString() ?? "",
)
else else
...tracks.mapIndexed((i, track) { ...tracks.mapIndexed((i, track) {
return TrackTile( return TrackTile(
@ -81,12 +71,12 @@ class SearchTracksSection extends HookConsumerWidget {
}, },
); );
}), }),
if (searchTrack.hasNextPage && tracks.isNotEmpty) if (searchTrack.value?.hasMore == true && tracks.isNotEmpty)
Center( Center(
child: TextButton( child: TextButton(
onPressed: searchTrack.isLoadingNextPage onPressed: searchTrack.isLoadingNextPage
? null ? null
: () => searchTrack.fetchNext(), : () => searchTrackNotifier.fetchMore,
child: searchTrack.isLoadingNextPage child: searchTrack.isLoadingNextPage
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: Text(context.l10n.load_more), : Text(context.l10n.load_more),

View File

@ -1,13 +1,13 @@
part of '../spotify.dart'; part of '../spotify.dart';
final searchTermStateProvider = StateProvider<String>((ref) => "");
class SearchState<Y> extends PaginatedState<Y> { class SearchState<Y> extends PaginatedState<Y> {
final String query;
SearchState({ SearchState({
required super.items, required super.items,
required super.offset, required super.offset,
required super.limit, required super.limit,
required super.hasMore, required super.hasMore,
required this.query,
}); });
@override @override
@ -16,14 +16,12 @@ class SearchState<Y> extends PaginatedState<Y> {
int? offset, int? offset,
int? limit, int? limit,
bool? hasMore, bool? hasMore,
String? query,
}) { }) {
return SearchState( return SearchState(
items: items ?? this.items, items: items ?? this.items,
offset: offset ?? this.offset, offset: offset ?? this.offset,
limit: limit ?? this.limit, limit: limit ?? this.limit,
hasMore: hasMore ?? this.hasMore, hasMore: hasMore ?? this.hasMore,
query: query ?? this.query,
); );
} }
} }
@ -37,7 +35,7 @@ class SearchNotifier<Y>
if (state.value == null) return []; if (state.value == null) return [];
final results = await spotify.search final results = await spotify.search
.get( .get(
state.value!.query, ref.read(searchTermStateProvider),
types: [arg], types: [arg],
market: ref.read(userPreferencesProvider).recommendationMarket, market: ref.read(userPreferencesProvider).recommendationMarket,
) )
@ -48,6 +46,7 @@ class SearchNotifier<Y>
@override @override
build(arg) async { build(arg) async {
ref.watch(searchTermStateProvider);
ref.watch(spotifyProvider); ref.watch(spotifyProvider);
ref.watch( ref.watch(
userPreferencesProvider.select((value) => value.recommendationMarket), userPreferencesProvider.select((value) => value.recommendationMarket),
@ -60,30 +59,8 @@ class SearchNotifier<Y>
offset: 0, offset: 0,
limit: 10, limit: 10,
hasMore: results.length == 10, hasMore: results.length == 10,
query: "",
); );
} }
Future<void> 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 = final searchProvider =