diff --git a/lib/components/home/sections/featured.dart b/lib/components/home/sections/featured.dart index 8a7c2c95..58f0b0bb 100644 --- a/lib/components/home/sections/featured.dart +++ b/lib/components/home/sections/featured.dart @@ -1,35 +1,28 @@ import 'package:flutter/material.dart' hide Page; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.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/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class HomeFeaturedSection extends HookConsumerWidget { - const HomeFeaturedSection({Key? key}) : super(key: key); + const HomeFeaturedSection({super.key}); @override Widget build(BuildContext context, ref) { - final featuredPlaylistsQuery = useQueries.playlist.featured(ref); - final playlists = useMemoized( - () => featuredPlaylistsQuery.pages - .whereType>() - .expand((page) => page.items ?? const []), - [featuredPlaylistsQuery.pages], - ); - final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData && - !featuredPlaylistsQuery.isLoadingNextPage; + final featuredPlaylists = ref.watch(featuredPlaylistsProvider); + final featuredPlaylistsNotifier = + ref.read(featuredPlaylistsProvider.notifier); return Skeletonizer( - enabled: isLoadingFeaturedPlaylists, + enabled: featuredPlaylists.isLoadingAndEmpty, child: HorizontalPlaybuttonCardView( - items: playlists.toList(), + items: featuredPlaylists.value?.items ?? [], title: Text(context.l10n.featured), - isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage, - hasNextPage: featuredPlaylistsQuery.hasNextPage, - onFetchMore: featuredPlaylistsQuery.fetchNext, + isLoadingNextPage: featuredPlaylists.isLoadingNextPage, + hasNextPage: featuredPlaylists.value?.hasMore ?? false, + onFetchMore: featuredPlaylistsNotifier.fetchMore, ), ); } diff --git a/lib/components/home/sections/friends.dart b/lib/components/home/sections/friends.dart index 6382f6fd..a4b7e663 100644 --- a/lib/components/home/sections/friends.dart +++ b/lib/components/home/sections/friends.dart @@ -1,4 +1,3 @@ -import 'dart:ffi'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -8,15 +7,15 @@ import 'package:spotube/collections/fake.dart'; import 'package:spotube/components/home/sections/friends/friend_item.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/models/spotify_friends.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class HomePageFriendsSection extends HookConsumerWidget { - const HomePageFriendsSection({Key? key}) : super(key: key); + const HomePageFriendsSection({super.key}); @override Widget build(BuildContext context, ref) { - final friendsQuery = useQueries.user.friendActivity(ref); - final friends = friendsQuery.data?.friends ?? FakeData.friends.friends; + final friendsQuery = ref.watch(friendsProvider); + final friends = friendsQuery.value?.friends ?? FakeData.friends.friends; final groupCount = useBreakpointValue( sm: 3, @@ -51,8 +50,7 @@ class HomePageFriendsSection extends HookConsumerWidget { }, ); - if (!friendsQuery.isLoading && - (!friendsQuery.hasData || friendsQuery.data!.friends.isEmpty)) { + if (friendsQuery.isLoading || friendsQuery.value?.friends.isEmpty == true) { return const SliverToBoxAdapter( child: SizedBox.shrink(), ); diff --git a/lib/components/home/sections/friends/friend_item.dart b/lib/components/home/sections/friends/friend_item.dart index fcdadab7..0bb2502a 100644 --- a/lib/components/home/sections/friends/friend_item.dart +++ b/lib/components/home/sections/friends/friend_item.dart @@ -13,9 +13,9 @@ import 'package:spotube/provider/spotify_provider.dart'; class FriendItem extends HookConsumerWidget { final SpotifyFriendActivity friend; const FriendItem({ - Key? key, + super.key, required this.friend, - }) : super(key: key); + }); @override Widget build(BuildContext context, ref) { diff --git a/lib/components/home/sections/genres.dart b/lib/components/home/sections/genres.dart index 41ba235c..87f28821 100644 --- a/lib/components/home/sections/genres.dart +++ b/lib/components/home/sections/genres.dart @@ -13,28 +13,26 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class HomeGenresSection extends HookConsumerWidget { - const HomeGenresSection({Key? key}) : super(key: key); + const HomeGenresSection({super.key}); @override Widget build(BuildContext context, ref) { final ThemeData(:textTheme, :colorScheme) = Theme.of(context); final mediaQuery = MediaQuery.of(context); - final recommendationMarket = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), + final categoriesQuery = ref.watch(categoriesProvider); + final categories = useMemoized( + () => + categoriesQuery.value + ?.where((c) => (c.icons?.length ?? 0) > 0) + .take(mediaQuery.mdAndDown ? 6 : 10) + .toList() ?? + [], + [mediaQuery.mdAndDown, categoriesQuery.value], ); - final categoriesQuery = - useQueries.category.listAll(ref, recommendationMarket); - - final categories = categoriesQuery.data - ?.where((c) => (c.icons?.length ?? 0) > 0) - .take(mediaQuery.mdAndDown ? 6 : 10) - .toList() ?? - []; return SliverMainAxisGroup( slivers: [ diff --git a/lib/components/home/sections/made_for_user.dart b/lib/components/home/sections/made_for_user.dart index a3f96899..439d9c38 100644 --- a/lib/components/home/sections/made_for_user.dart +++ b/lib/components/home/sections/made_for_user.dart @@ -2,19 +2,19 @@ import 'package:flutter/widgets.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/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class HomeMadeForUserSection extends HookConsumerWidget { - const HomeMadeForUserSection({Key? key}) : super(key: key); + const HomeMadeForUserSection({super.key}); @override Widget build(BuildContext context, ref) { - final madeForUser = useQueries.views.get(ref, "made-for-x-hub"); + final madeForUser = ref.watch(viewProvider("made-for-x-hub")); return SliverList.builder( - itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0, + itemCount: madeForUser.value?["content"]?["items"]?.length ?? 0, itemBuilder: (context, index) { - final item = madeForUser.data?["content"]?["items"]?[index]; + final item = madeForUser.value?["content"]?["items"]?[index]; final playlists = item["content"]?["items"] ?.where((itemL2) => itemL2["type"] == "playlist") .map((itemL2) => PlaylistSimple.fromJson(itemL2)) diff --git a/lib/components/home/sections/new_releases.dart b/lib/components/home/sections/new_releases.dart index 0f4a046a..b38039d5 100644 --- a/lib/components/home/sections/new_releases.dart +++ b/lib/components/home/sections/new_releases.dart @@ -5,52 +5,32 @@ 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/authentication_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class HomeNewReleasesSection extends HookConsumerWidget { - const HomeNewReleasesSection({Key? key}) : super(key: key); + const HomeNewReleasesSection({super.key}); @override Widget build(BuildContext context, ref) { final auth = ref.watch(AuthenticationNotifier.provider); - final newReleases = useQueries.album.newReleases(ref); - final userArtistsQuery = useQueries.artist.followedByMeAll(ref); - final userArtists = - userArtistsQuery.data?.map((s) => s.id!).toList() ?? const []; + final newReleases = ref.watch(albumReleasesProvider); + final newReleasesNotifier = ref.read(albumReleasesProvider.notifier); - final albums = useMemoized( - () { - final allReleases = newReleases.pages - .whereType>() - .expand((page) => page.items ?? const []) - .map((album) => TypeConversionUtils.simpleAlbum_X_Album(album)); + final albums = ref.watch(userArtistAlbumReleasesProvider); - final userArtistReleases = allReleases.where((album) { - return album.artists - ?.any((artist) => userArtists.contains(artist.id!)) == - true; - }).toList(); - - if (userArtistReleases.isEmpty) return allReleases.toList(); - return userArtistReleases; - }, - [newReleases.pages], - ); - - final hasNewReleases = newReleases.hasPageData && - userArtistsQuery.hasData && - !newReleases.isLoadingNextPage; - - if (auth == null || !hasNewReleases) return const SizedBox.shrink(); + if (auth == null || + newReleases.isLoading || + newReleases.value?.items.isEmpty == true) { + return const SizedBox.shrink(); + } return HorizontalPlaybuttonCardView( items: albums, title: Text(context.l10n.new_releases), isLoadingNextPage: newReleases.isLoadingNextPage, - hasNextPage: newReleases.hasNextPage, - onFetchMore: newReleases.fetchNext, + hasNextPage: newReleases.value?.hasMore ?? false, + onFetchMore: newReleasesNotifier.fetchMore, ); } } diff --git a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart index dc9d30da..06ba9a56 100644 --- a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart +++ b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart @@ -24,13 +24,12 @@ class HorizontalPlaybuttonCardView extends HookWidget { required this.hasNextPage, required this.onFetchMore, required this.isLoadingNextPage, - Key? key, - }) : assert( + super.key, + }) : assert( items is List || items is List || items is List, - ), - super(key: key); + ); @override Widget build(BuildContext context) { diff --git a/lib/pages/home/genres/genre_playlists.dart b/lib/pages/home/genres/genre_playlists.dart index bfb0843c..8f552073 100644 --- a/lib/pages/home/genres/genre_playlists.dart +++ b/lib/pages/home/genres/genre_playlists.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; @@ -12,7 +10,7 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:collection/collection.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; @@ -22,23 +20,10 @@ class GenrePlaylistsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlistsQuery = useQueries.category.playlistsOf( - ref, - category.id!, - ); - - final playlists = useMemoized( - () => playlistsQuery.pages.expand( - (page) { - return page.items?.whereNotNull() ?? - const Iterable.empty(); - }, - ).toList(), - [playlistsQuery.pages], - ); - final mediaQuery = MediaQuery.of(context); - + final playlists = ref.watch(categoryPlaylistsProvider(category.id!)); + final playlistsNotifier = + ref.read(categoryPlaylistsProvider(category.id!).notifier); final scrollController = useScrollController(); return Scaffold( @@ -109,7 +94,7 @@ class GenrePlaylistsPage extends HookConsumerWidget { padding: EdgeInsets.symmetric( horizontal: mediaQuery.mdAndDown ? 12 : 24, ), - sliver: playlists.isEmpty + sliver: playlists.value?.items.isNotEmpty != true ? Skeletonizer.sliver( child: SliverToBoxAdapter( child: Wrap( @@ -129,12 +114,13 @@ class GenrePlaylistsPage extends HookConsumerWidget { crossAxisSpacing: 12, mainAxisSpacing: 12, ), - itemCount: playlists.length + 1, + itemCount: (playlists.value?.items.length ?? 0) + 1, itemBuilder: (context, index) { - final playlist = playlists.elementAtOrNull(index); + final playlist = + playlists.value?.items.elementAtOrNull(index); if (playlist == null) { - if (!playlistsQuery.hasNextPage) { + if (playlists.value?.hasMore == false) { return const SizedBox.shrink(); } return Skeletonizer( @@ -142,11 +128,7 @@ class GenrePlaylistsPage extends HookConsumerWidget { child: Waypoint( controller: scrollController, isGrid: true, - onTouchEdge: () async { - if (playlistsQuery.hasNextPage) { - await playlistsQuery.fetchNext(); - } - }, + onTouchEdge: playlistsNotifier.fetchMore, child: PlaylistCard(FakeData.playlist), ), ); diff --git a/lib/pages/home/genres/genres.dart b/lib/pages/home/genres/genres.dart index 17a67beb..ed6c2835 100644 --- a/lib/pages/home/genres/genres.dart +++ b/lib/pages/home/genres/genres.dart @@ -5,14 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotify/spotify.dart' hide Offset; import 'package:spotube/collections/gradients.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; - -import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class GenrePage extends HookConsumerWidget { const GenrePage({super.key}); @@ -21,13 +18,7 @@ class GenrePage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final ThemeData(:textTheme) = Theme.of(context); final scrollController = useScrollController(); - final recommendationMarket = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), - ); - final categoriesQuery = - useQueries.category.listAll(ref, recommendationMarket); - - final categories = categoriesQuery.data ?? []; + final categories = ref.watch(categoriesProvider); final mediaQuery = MediaQuery.of(context); @@ -48,9 +39,9 @@ class GenrePage extends HookConsumerWidget { crossAxisSpacing: 12, mainAxisSpacing: 12, ), - itemCount: categories.length, + itemCount: categories.value!.length, itemBuilder: (context, index) { - final category = categories[index]; + final category = categories.value![index]; final gradient = gradients[Random().nextInt(gradients.length)]; return InkWell( borderRadius: BorderRadius.circular(8), diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 312ca7f9..ed297065 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -11,7 +11,7 @@ import 'package:spotube/components/home/sections/new_releases.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; class HomePage extends HookConsumerWidget { - const HomePage({Key? key}) : super(key: key); + const HomePage({super.key}); @override Widget build(BuildContext context, ref) { diff --git a/lib/provider/spotify/album/releases.dart b/lib/provider/spotify/album/releases.dart index 1aec1bc2..99bfbe63 100644 --- a/lib/provider/spotify/album/releases.dart +++ b/lib/provider/spotify/album/releases.dart @@ -1,6 +1,6 @@ part of '../spotify.dart'; -class AlbumReleasesState extends PaginatedState { +class AlbumReleasesState extends PaginatedState { AlbumReleasesState({ required super.items, required super.offset, @@ -10,7 +10,7 @@ class AlbumReleasesState extends PaginatedState { @override AlbumReleasesState copyWith({ - List? items, + List? items, int? offset, int? limit, bool? hasMore, @@ -25,16 +25,21 @@ class AlbumReleasesState extends PaginatedState { } class AlbumReleasesNotifier - extends PaginatedAsyncNotifier { + extends PaginatedAsyncNotifier { AlbumReleasesNotifier() : super(); @override fetch(int offset, int limit) async { final market = ref.read(userPreferencesProvider).recommendationMarket; + final albums = await spotify.browse .newReleases(country: market) - .getPage(offset, limit); - return albums.items?.toList() ?? []; + .getPage(limit, offset); + + return albums.items + ?.map(TypeConversionUtils.simpleAlbum_X_Album) + .toList() ?? + []; } @override @@ -43,7 +48,10 @@ class AlbumReleasesNotifier ref.watch( userPreferencesProvider.select((s) => s.recommendationMarket), ); + ref.watch(allFollowedArtistsProvider); + final albums = await fetch(0, 20); + return AlbumReleasesState( items: albums, offset: 0, @@ -57,3 +65,26 @@ final albumReleasesProvider = AsyncNotifierProvider( () => AlbumReleasesNotifier(), ); + +final userArtistAlbumReleasesProvider = Provider>((ref) { + final newReleases = ref.watch(albumReleasesProvider); + final userArtistsQuery = ref.watch(allFollowedArtistsProvider); + + if (newReleases.isLoading || userArtistsQuery.isLoading) { + return const []; + } + + final userArtists = + userArtistsQuery.value?.map((s) => s.id!).toList() ?? const []; + + final allReleases = newReleases.value?.items; + final userArtistReleases = allReleases?.where((album) { + return album.artists?.any((artist) => userArtists.contains(artist.id!)) == + true; + }).toList(); + + if (userArtistReleases?.isEmpty == true) { + return allReleases?.toList() ?? []; + } + return userArtistReleases ?? []; +}); diff --git a/lib/provider/spotify/album/tracks.dart b/lib/provider/spotify/album/tracks.dart index da76eeaa..4cc56331 100644 --- a/lib/provider/spotify/album/tracks.dart +++ b/lib/provider/spotify/album/tracks.dart @@ -30,7 +30,7 @@ class AlbumTracksNotifier extends FamilyPaginatedAsyncNotifier json == null ? null : PlaylistSimple.fromJson(json), 'playlists', (json) => PlaylistsFeatured.fromJson(json), - ).getPage(offset, limit); + ).getPage(limit, offset); return playlists.items?.whereNotNull().toList() ?? []; } diff --git a/lib/provider/spotify/playlist/playlist.dart b/lib/provider/spotify/playlist/playlist.dart index 06ed50f8..59226865 100644 --- a/lib/provider/spotify/playlist/playlist.dart +++ b/lib/provider/spotify/playlist/playlist.dart @@ -84,6 +84,6 @@ class PlaylistNotifier extends FamilyAsyncNotifier { } final playlistProvider = - AsyncNotifierProvider.family( + AsyncNotifierProviderFamily( () => PlaylistNotifier(), ); diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index cb340576..d7fc91aa 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -20,6 +20,7 @@ import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:http/http.dart' as http; +import 'package:spotube/utils/type_conversion_utils.dart'; part 'album/favorite.dart'; part 'album/tracks.dart'; @@ -58,3 +59,4 @@ part 'utils/mixin.dart'; part 'utils/state.dart'; part 'utils/provider.dart'; part 'utils/persistence.dart'; +part 'utils/async.dart'; diff --git a/lib/provider/spotify/utils/async.dart b/lib/provider/spotify/utils/async.dart new file mode 100644 index 00000000..277869eb --- /dev/null +++ b/lib/provider/spotify/utils/async.dart @@ -0,0 +1,6 @@ +part of '../spotify.dart'; + +extension PaginationExtension on AsyncValue { + bool get isLoadingAndEmpty => value == null && isLoading; + bool get isLoadingNextPage => value != null && isLoading; +} diff --git a/lib/provider/spotify/utils/provider.dart b/lib/provider/spotify/utils/provider.dart index 4e748608..6e6a1aee 100644 --- a/lib/provider/spotify/utils/provider.dart +++ b/lib/provider/spotify/utils/provider.dart @@ -6,17 +6,19 @@ abstract class PaginatedAsyncNotifier> Future fetchMore() async { if (state.value == null || !state.value!.hasMore) return; + state = const AsyncValue.loading(); - await update( - (state) async { - final items = await fetch(state.offset + state.limit, state.limit); - return state.copyWith( - hasMore: items.length == state.limit, + state = await AsyncValue.guard( + () async { + final items = await fetch( + state.value!.offset + state.value!.limit, state.value!.limit); + return state.value!.copyWith( + hasMore: items.length == state.value!.limit, items: [ - ...state.items, + ...state.value!.items, ...items, ], - offset: state.offset + state.limit, + offset: state.value!.offset + state.value!.limit, ) as T; }, ); @@ -31,13 +33,15 @@ abstract class CursorPaginatedAsyncNotifier fetchMore() async { if (state.value == null || !state.value!.hasMore) return; - await update( - (state) async { - final items = await fetch(state.offset, state.limit); - return state.copyWith( - hasMore: items.$1.length == state.limit, + state = const AsyncValue.loading(); + + state = await AsyncValue.guard( + () async { + final items = await fetch(state.value!.offset, state.value!.limit); + return state.value!.copyWith( + hasMore: items.$1.length == state.value!.limit, items: [ - ...state.items, + ...state.value!.items, ...items.$1, ], offset: items.$2, @@ -56,20 +60,22 @@ abstract class FamilyPaginatedAsyncNotifier< Future fetchMore() async { if (state.value == null || !state.value!.hasMore) return; - await update( - (state) async { + state = const AsyncLoading(); + + state = await AsyncValue.guard( + () async { final items = await fetch( arg, - state.offset + state.limit, - state.limit, + state.value!.offset + state.value!.limit, + state.value!.limit, ); - return state.copyWith( - hasMore: items.length == state.limit, + return state.value!.copyWith( + hasMore: items.length == state.value!.limit, items: [ - ...state.items, + ...state.value!.items, ...items, ], - offset: state.offset + state.limit, + offset: state.value!.offset + state.value!.limit, ) as T; }, ); @@ -89,17 +95,15 @@ abstract class FamilyCursorPaginatedAsyncNotifier< Future fetchMore() async { if (state.value == null || !state.value!.hasMore) return; - await update( - (state) async { - final items = await fetch( - arg, - state.offset, - state.limit, - ); - return state.copyWith( - hasMore: items.$1.length == state.limit, + state = const AsyncLoading(); + + state = await AsyncValue.guard( + () async { + final items = await fetch(arg, state.value!.offset, state.value!.limit); + return state.value!.copyWith( + hasMore: items.$1.length == state.value!.limit, items: [ - ...state.items, + ...state.value!.items, ...items.$1, ], offset: items.$2,