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_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<String>((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<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 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;
},
);
},
),
),

View File

@ -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<List<Page<dynamic>>, 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<AlbumSimple>()
.map((e) => TypeConversionUtils.simpleAlbum_X_Album(e))
.toList(),
[query.pages],
() =>
query.value?.items
.cast<AlbumSimple>()
.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),
);
}

View File

@ -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<List<Page<dynamic>>, 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<Artist>()
.toList(),
[query.pages],
);
final query = ref.watch(searchProvider(SearchType.artist));
final notifier = ref.watch(searchProvider(SearchType.artist).notifier);
final artists = query.value?.items.cast<Artist>() ?? [];
return HorizontalPlaybuttonCardView<Artist>(
isLoadingNextPage: query.isLoadingNextPage,
hasNextPage: query.hasNextPage,
hasNextPage: query.value?.hasMore == true,
items: artists,
onFetchMore: query.fetchNext,
onFetchMore: notifier.fetchMore,
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_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<List<Page<dynamic>>, 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<PlaylistSimple>()
.toList(),
[query.pages],
);
final playlistsQuery = ref.watch(searchProvider(SearchType.playlist));
final playlistsQueryNotifier =
ref.watch(searchProvider(SearchType.playlist).notifier);
final playlists = playlistsQuery.value?.items.cast<PlaylistSimple>() ?? [];
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),
);
}

View File

@ -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<List<Page<dynamic>>, 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<Track>(),
[searchTrack.pages],
);
final searchTrack = ref.watch(searchProvider(SearchType.track));
final searchTrackNotifier =
ref.watch(searchProvider(SearchType.track).notifier);
final tracks = searchTrack.value?.items.cast<Track>() ?? [];
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),

View File

@ -1,13 +1,13 @@
part of '../spotify.dart';
final searchTermStateProvider = StateProvider<String>((ref) => "");
class SearchState<Y> extends PaginatedState<Y> {
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<Y> extends PaginatedState<Y> {
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<Y>
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<Y>
@override
build(arg) async {
ref.watch(searchTermStateProvider);
ref.watch(spotifyProvider);
ref.watch(
userPreferencesProvider.select((value) => value.recommendationMarket),
@ -60,30 +59,8 @@ class SearchNotifier<Y>
offset: 0,
limit: 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 =