mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
feat: use provider in search page
This commit is contained in:
parent
25badae7ad
commit
b76e265f23
@ -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;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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 =
|
||||
|
||||
Loading…
Reference in New Issue
Block a user