feat: use providers in home screen

This commit is contained in:
Kingkor Roy Tirtho 2024-03-16 21:36:12 +06:00
parent f83d6e08e1
commit 94b3e160c2
18 changed files with 146 additions and 162 deletions

View File

@ -1,35 +1,28 @@
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:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.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/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class HomeFeaturedSection extends HookConsumerWidget { class HomeFeaturedSection extends HookConsumerWidget {
const HomeFeaturedSection({Key? key}) : super(key: key); const HomeFeaturedSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final featuredPlaylistsQuery = useQueries.playlist.featured(ref); final featuredPlaylists = ref.watch(featuredPlaylistsProvider);
final playlists = useMemoized( final featuredPlaylistsNotifier =
() => featuredPlaylistsQuery.pages ref.read(featuredPlaylistsProvider.notifier);
.whereType<Page<PlaylistSimple>>()
.expand((page) => page.items ?? const <PlaylistSimple>[]),
[featuredPlaylistsQuery.pages],
);
final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData &&
!featuredPlaylistsQuery.isLoadingNextPage;
return Skeletonizer( return Skeletonizer(
enabled: isLoadingFeaturedPlaylists, enabled: featuredPlaylists.isLoadingAndEmpty,
child: HorizontalPlaybuttonCardView<PlaylistSimple>( child: HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists.toList(), items: featuredPlaylists.value?.items ?? [],
title: Text(context.l10n.featured), title: Text(context.l10n.featured),
isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage, isLoadingNextPage: featuredPlaylists.isLoadingNextPage,
hasNextPage: featuredPlaylistsQuery.hasNextPage, hasNextPage: featuredPlaylists.value?.hasMore ?? false,
onFetchMore: featuredPlaylistsQuery.fetchNext, onFetchMore: featuredPlaylistsNotifier.fetchMore,
), ),
); );
} }

View File

@ -1,4 +1,3 @@
import 'dart:ffi';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; 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/components/home/sections/friends/friend_item.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/models/spotify_friends.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 { class HomePageFriendsSection extends HookConsumerWidget {
const HomePageFriendsSection({Key? key}) : super(key: key); const HomePageFriendsSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final friendsQuery = useQueries.user.friendActivity(ref); final friendsQuery = ref.watch(friendsProvider);
final friends = friendsQuery.data?.friends ?? FakeData.friends.friends; final friends = friendsQuery.value?.friends ?? FakeData.friends.friends;
final groupCount = useBreakpointValue( final groupCount = useBreakpointValue(
sm: 3, sm: 3,
@ -51,8 +50,7 @@ class HomePageFriendsSection extends HookConsumerWidget {
}, },
); );
if (!friendsQuery.isLoading && if (friendsQuery.isLoading || friendsQuery.value?.friends.isEmpty == true) {
(!friendsQuery.hasData || friendsQuery.data!.friends.isEmpty)) {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
child: SizedBox.shrink(), child: SizedBox.shrink(),
); );

View File

@ -13,9 +13,9 @@ import 'package:spotube/provider/spotify_provider.dart';
class FriendItem extends HookConsumerWidget { class FriendItem extends HookConsumerWidget {
final SpotifyFriendActivity friend; final SpotifyFriendActivity friend;
const FriendItem({ const FriendItem({
Key? key, super.key,
required this.friend, required this.friend,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -13,28 +13,26 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/queries/queries.dart';
class HomeGenresSection extends HookConsumerWidget { class HomeGenresSection extends HookConsumerWidget {
const HomeGenresSection({Key? key}) : super(key: key); const HomeGenresSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context); final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final recommendationMarket = ref.watch( final categoriesQuery = ref.watch(categoriesProvider);
userPreferencesProvider.select((s) => s.recommendationMarket), final categories = useMemoized(
() =>
categoriesQuery.value
?.where((c) => (c.icons?.length ?? 0) > 0)
.take(mediaQuery.mdAndDown ? 6 : 10)
.toList() ??
<Category>[],
[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() ??
<Category>[];
return SliverMainAxisGroup( return SliverMainAxisGroup(
slivers: [ slivers: [

View File

@ -2,19 +2,19 @@ import 'package:flutter/widgets.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/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class HomeMadeForUserSection extends HookConsumerWidget { class HomeMadeForUserSection extends HookConsumerWidget {
const HomeMadeForUserSection({Key? key}) : super(key: key); const HomeMadeForUserSection({super.key});
@override @override
Widget build(BuildContext context, ref) { 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( return SliverList.builder(
itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0, itemCount: madeForUser.value?["content"]?["items"]?.length ?? 0,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = madeForUser.data?["content"]?["items"]?[index]; final item = madeForUser.value?["content"]?["items"]?[index];
final playlists = item["content"]?["items"] final playlists = item["content"]?["items"]
?.where((itemL2) => itemL2["type"] == "playlist") ?.where((itemL2) => itemL2["type"] == "playlist")
.map((itemL2) => PlaylistSimple.fromJson(itemL2)) .map((itemL2) => PlaylistSimple.fromJson(itemL2))

View File

@ -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/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/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/type_conversion_utils.dart';
class HomeNewReleasesSection extends HookConsumerWidget { class HomeNewReleasesSection extends HookConsumerWidget {
const HomeNewReleasesSection({Key? key}) : super(key: key); const HomeNewReleasesSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final newReleases = useQueries.album.newReleases(ref); final newReleases = ref.watch(albumReleasesProvider);
final userArtistsQuery = useQueries.artist.followedByMeAll(ref); final newReleasesNotifier = ref.read(albumReleasesProvider.notifier);
final userArtists =
userArtistsQuery.data?.map((s) => s.id!).toList() ?? const [];
final albums = useMemoized( final albums = ref.watch(userArtistAlbumReleasesProvider);
() {
final allReleases = newReleases.pages
.whereType<Page<AlbumSimple>>()
.expand((page) => page.items ?? const <AlbumSimple>[])
.map((album) => TypeConversionUtils.simpleAlbum_X_Album(album));
final userArtistReleases = allReleases.where((album) { if (auth == null ||
return album.artists newReleases.isLoading ||
?.any((artist) => userArtists.contains(artist.id!)) == newReleases.value?.items.isEmpty == true) {
true; return const SizedBox.shrink();
}).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();
return HorizontalPlaybuttonCardView<Album>( return HorizontalPlaybuttonCardView<Album>(
items: albums, items: albums,
title: Text(context.l10n.new_releases), title: Text(context.l10n.new_releases),
isLoadingNextPage: newReleases.isLoadingNextPage, isLoadingNextPage: newReleases.isLoadingNextPage,
hasNextPage: newReleases.hasNextPage, hasNextPage: newReleases.value?.hasMore ?? false,
onFetchMore: newReleases.fetchNext, onFetchMore: newReleasesNotifier.fetchMore,
); );
} }
} }

View File

@ -24,13 +24,12 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
required this.hasNextPage, required this.hasNextPage,
required this.onFetchMore, required this.onFetchMore,
required this.isLoadingNextPage, required this.isLoadingNextPage,
Key? key, super.key,
}) : assert( }) : assert(
items is List<PlaylistSimple> || items is List<PlaylistSimple> ||
items is List<Album> || items is List<Album> ||
items is List<Artist>, items is List<Artist>,
), );
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.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/page_window_title_bar.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/extensions/constrains.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:collection/collection.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
@ -22,23 +20,10 @@ class GenrePlaylistsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { 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<PlaylistSimple>.empty();
},
).toList(),
[playlistsQuery.pages],
);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final playlists = ref.watch(categoryPlaylistsProvider(category.id!));
final playlistsNotifier =
ref.read(categoryPlaylistsProvider(category.id!).notifier);
final scrollController = useScrollController(); final scrollController = useScrollController();
return Scaffold( return Scaffold(
@ -109,7 +94,7 @@ class GenrePlaylistsPage extends HookConsumerWidget {
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: mediaQuery.mdAndDown ? 12 : 24, horizontal: mediaQuery.mdAndDown ? 12 : 24,
), ),
sliver: playlists.isEmpty sliver: playlists.value?.items.isNotEmpty != true
? Skeletonizer.sliver( ? Skeletonizer.sliver(
child: SliverToBoxAdapter( child: SliverToBoxAdapter(
child: Wrap( child: Wrap(
@ -129,12 +114,13 @@ class GenrePlaylistsPage extends HookConsumerWidget {
crossAxisSpacing: 12, crossAxisSpacing: 12,
mainAxisSpacing: 12, mainAxisSpacing: 12,
), ),
itemCount: playlists.length + 1, itemCount: (playlists.value?.items.length ?? 0) + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final playlist = playlists.elementAtOrNull(index); final playlist =
playlists.value?.items.elementAtOrNull(index);
if (playlist == null) { if (playlist == null) {
if (!playlistsQuery.hasNextPage) { if (playlists.value?.hasMore == false) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Skeletonizer( return Skeletonizer(
@ -142,11 +128,7 @@ class GenrePlaylistsPage extends HookConsumerWidget {
child: Waypoint( child: Waypoint(
controller: scrollController, controller: scrollController,
isGrid: true, isGrid: true,
onTouchEdge: () async { onTouchEdge: playlistsNotifier.fetchMore,
if (playlistsQuery.hasNextPage) {
await playlistsQuery.fetchNext();
}
},
child: PlaylistCard(FakeData.playlist), child: PlaylistCard(FakeData.playlist),
), ),
); );

View File

@ -5,14 +5,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/gradients.dart'; import 'package:spotube/collections/gradients.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/queries/queries.dart';
class GenrePage extends HookConsumerWidget { class GenrePage extends HookConsumerWidget {
const GenrePage({super.key}); const GenrePage({super.key});
@ -21,13 +18,7 @@ class GenrePage extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme) = Theme.of(context); final ThemeData(:textTheme) = Theme.of(context);
final scrollController = useScrollController(); final scrollController = useScrollController();
final recommendationMarket = ref.watch( final categories = ref.watch(categoriesProvider);
userPreferencesProvider.select((s) => s.recommendationMarket),
);
final categoriesQuery =
useQueries.category.listAll(ref, recommendationMarket);
final categories = categoriesQuery.data ?? <Category>[];
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
@ -48,9 +39,9 @@ class GenrePage extends HookConsumerWidget {
crossAxisSpacing: 12, crossAxisSpacing: 12,
mainAxisSpacing: 12, mainAxisSpacing: 12,
), ),
itemCount: categories.length, itemCount: categories.value!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final category = categories[index]; final category = categories.value![index];
final gradient = gradients[Random().nextInt(gradients.length)]; final gradient = gradients[Random().nextInt(gradients.length)];
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),

View File

@ -11,7 +11,7 @@ import 'package:spotube/components/home/sections/new_releases.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart';
class HomePage extends HookConsumerWidget { class HomePage extends HookConsumerWidget {
const HomePage({Key? key}) : super(key: key); const HomePage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -1,6 +1,6 @@
part of '../spotify.dart'; part of '../spotify.dart';
class AlbumReleasesState extends PaginatedState<AlbumSimple> { class AlbumReleasesState extends PaginatedState<Album> {
AlbumReleasesState({ AlbumReleasesState({
required super.items, required super.items,
required super.offset, required super.offset,
@ -10,7 +10,7 @@ class AlbumReleasesState extends PaginatedState<AlbumSimple> {
@override @override
AlbumReleasesState copyWith({ AlbumReleasesState copyWith({
List<AlbumSimple>? items, List<Album>? items,
int? offset, int? offset,
int? limit, int? limit,
bool? hasMore, bool? hasMore,
@ -25,16 +25,21 @@ class AlbumReleasesState extends PaginatedState<AlbumSimple> {
} }
class AlbumReleasesNotifier class AlbumReleasesNotifier
extends PaginatedAsyncNotifier<AlbumSimple, AlbumReleasesState> { extends PaginatedAsyncNotifier<Album, AlbumReleasesState> {
AlbumReleasesNotifier() : super(); AlbumReleasesNotifier() : super();
@override @override
fetch(int offset, int limit) async { fetch(int offset, int limit) async {
final market = ref.read(userPreferencesProvider).recommendationMarket; final market = ref.read(userPreferencesProvider).recommendationMarket;
final albums = await spotify.browse final albums = await spotify.browse
.newReleases(country: market) .newReleases(country: market)
.getPage(offset, limit); .getPage(limit, offset);
return albums.items?.toList() ?? [];
return albums.items
?.map(TypeConversionUtils.simpleAlbum_X_Album)
.toList() ??
[];
} }
@override @override
@ -43,7 +48,10 @@ class AlbumReleasesNotifier
ref.watch( ref.watch(
userPreferencesProvider.select((s) => s.recommendationMarket), userPreferencesProvider.select((s) => s.recommendationMarket),
); );
ref.watch(allFollowedArtistsProvider);
final albums = await fetch(0, 20); final albums = await fetch(0, 20);
return AlbumReleasesState( return AlbumReleasesState(
items: albums, items: albums,
offset: 0, offset: 0,
@ -57,3 +65,26 @@ final albumReleasesProvider =
AsyncNotifierProvider<AlbumReleasesNotifier, AlbumReleasesState>( AsyncNotifierProvider<AlbumReleasesNotifier, AlbumReleasesState>(
() => AlbumReleasesNotifier(), () => AlbumReleasesNotifier(),
); );
final userArtistAlbumReleasesProvider = Provider<List<Album>>((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 ?? [];
});

View File

@ -30,7 +30,7 @@ class AlbumTracksNotifier extends FamilyPaginatedAsyncNotifier<TrackSimple,
@override @override
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final tracks = await spotify.albums.tracks(arg).getPage(offset, limit); final tracks = await spotify.albums.tracks(arg).getPage(limit, offset);
return tracks.items?.toList() ?? []; return tracks.items?.toList() ?? [];
} }

View File

@ -33,7 +33,7 @@ class ArtistAlbumsNotifier extends FamilyPaginatedAsyncNotifier<AlbumSimple,
final market = ref.read(userPreferencesProvider).recommendationMarket; final market = ref.read(userPreferencesProvider).recommendationMarket;
final albums = await spotify.artists final albums = await spotify.artists
.albums(arg, country: market) .albums(arg, country: market)
.getPage(offset, limit); .getPage(limit, offset);
return albums.items?.toList() ?? []; return albums.items?.toList() ?? [];
} }

View File

@ -37,7 +37,7 @@ class CategoryPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
(json) => json == null ? null : PlaylistSimple.fromJson(json), (json) => json == null ? null : PlaylistSimple.fromJson(json),
'playlists', 'playlists',
(json) => PlaylistsFeatured.fromJson(json), (json) => PlaylistsFeatured.fromJson(json),
).getPage(offset, limit); ).getPage(limit, offset);
return playlists.items?.whereNotNull().toList() ?? []; return playlists.items?.whereNotNull().toList() ?? [];
} }

View File

@ -84,6 +84,6 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
} }
final playlistProvider = final playlistProvider =
AsyncNotifierProvider.family<PlaylistNotifier, String, PlaylistNotifier>( AsyncNotifierProviderFamily<PlaylistNotifier, Playlist, String>(
() => PlaylistNotifier(), () => PlaylistNotifier(),
); );

View File

@ -20,6 +20,7 @@ import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/persisted_state_notifier.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:spotube/utils/type_conversion_utils.dart';
part 'album/favorite.dart'; part 'album/favorite.dart';
part 'album/tracks.dart'; part 'album/tracks.dart';
@ -58,3 +59,4 @@ part 'utils/mixin.dart';
part 'utils/state.dart'; part 'utils/state.dart';
part 'utils/provider.dart'; part 'utils/provider.dart';
part 'utils/persistence.dart'; part 'utils/persistence.dart';
part 'utils/async.dart';

View File

@ -0,0 +1,6 @@
part of '../spotify.dart';
extension PaginationExtension<T> on AsyncValue<T> {
bool get isLoadingAndEmpty => value == null && isLoading;
bool get isLoadingNextPage => value != null && isLoading;
}

View File

@ -6,17 +6,19 @@ abstract class PaginatedAsyncNotifier<K, T extends BasePaginatedState<K, int>>
Future<void> fetchMore() async { Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return; if (state.value == null || !state.value!.hasMore) return;
state = const AsyncValue.loading();
await update( state = await AsyncValue.guard(
(state) async { () async {
final items = await fetch(state.offset + state.limit, state.limit); final items = await fetch(
return state.copyWith( state.value!.offset + state.value!.limit, state.value!.limit);
hasMore: items.length == state.limit, return state.value!.copyWith(
hasMore: items.length == state.value!.limit,
items: [ items: [
...state.items, ...state.value!.items,
...items, ...items,
], ],
offset: state.offset + state.limit, offset: state.value!.offset + state.value!.limit,
) as T; ) as T;
}, },
); );
@ -31,13 +33,15 @@ abstract class CursorPaginatedAsyncNotifier<K,
Future<void> fetchMore() async { Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return; if (state.value == null || !state.value!.hasMore) return;
await update( state = const AsyncValue.loading();
(state) async {
final items = await fetch(state.offset, state.limit); state = await AsyncValue.guard(
return state.copyWith( () async {
hasMore: items.$1.length == state.limit, final items = await fetch(state.value!.offset, state.value!.limit);
return state.value!.copyWith(
hasMore: items.$1.length == state.value!.limit,
items: [ items: [
...state.items, ...state.value!.items,
...items.$1, ...items.$1,
], ],
offset: items.$2, offset: items.$2,
@ -56,20 +60,22 @@ abstract class FamilyPaginatedAsyncNotifier<
Future<void> fetchMore() async { Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return; if (state.value == null || !state.value!.hasMore) return;
await update( state = const AsyncLoading();
(state) async {
state = await AsyncValue.guard(
() async {
final items = await fetch( final items = await fetch(
arg, arg,
state.offset + state.limit, state.value!.offset + state.value!.limit,
state.limit, state.value!.limit,
); );
return state.copyWith( return state.value!.copyWith(
hasMore: items.length == state.limit, hasMore: items.length == state.value!.limit,
items: [ items: [
...state.items, ...state.value!.items,
...items, ...items,
], ],
offset: state.offset + state.limit, offset: state.value!.offset + state.value!.limit,
) as T; ) as T;
}, },
); );
@ -89,17 +95,15 @@ abstract class FamilyCursorPaginatedAsyncNotifier<
Future<void> fetchMore() async { Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return; if (state.value == null || !state.value!.hasMore) return;
await update( state = const AsyncLoading();
(state) async {
final items = await fetch( state = await AsyncValue.guard(
arg, () async {
state.offset, final items = await fetch(arg, state.value!.offset, state.value!.limit);
state.limit, return state.value!.copyWith(
); hasMore: items.$1.length == state.value!.limit,
return state.copyWith(
hasMore: items.$1.length == state.limit,
items: [ items: [
...state.items, ...state.value!.items,
...items.$1, ...items.$1,
], ],
offset: items.$2, offset: items.$2,