diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index f2963041..2efc6955 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -4,11 +4,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; -import 'package:spotube/hooks/usePagingController.dart'; +import 'package:spotube/hooks/usePaginatedFutureProvider.dart'; import 'package:spotube/models/Logger.dart'; -import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; -class CategoryCard extends HookWidget { +class CategoryCard extends HookConsumerWidget { final Category category; final Iterable? playlists; CategoryCard( @@ -20,7 +20,33 @@ class CategoryCard extends HookWidget { final logger = getLogger(CategoryCard); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { + final scrollController = useScrollController(); + final mounted = useIsMounted(); + + final pagingController = + usePaginatedFutureProvider, int, PlaylistSimple>( + (pageKey) => categoryPlaylistsQuery( + [ + category.id, + pageKey, + ].join("/"), + ), + ref: ref, + firstPageKey: 0, + onData: (data, pagingController, pageKey) { + if (playlists != null && playlists?.isNotEmpty == true && mounted()) { + return pagingController.appendLastPage(playlists!.toList()); + } + final page = data.value; + if (page.isLast && page.items != null) { + pagingController.appendLastPage(page.items!.toList()); + } else if (page.items != null) { + pagingController.appendPage(page.items!.toList(), page.nextOffset); + } + }, + ); + return Column( children: [ Padding( @@ -34,73 +60,25 @@ class CategoryCard extends HookWidget { ], ), ), - HookConsumer( - builder: (context, ref, child) { - SpotifyApi spotifyApi = ref.watch(spotifyProvider); - final scrollController = useScrollController(); - final pagingController = - usePagingController(firstPageKey: 0); - - final _error = useState(false); - final mounted = useIsMounted(); - - useEffect(() { - listener(pageKey) async { - try { - if (playlists != null && - playlists?.isNotEmpty == true && - mounted()) { - return pagingController.appendLastPage(playlists!.toList()); - } - final Page page = await (category.id != - "user-featured-playlists" - ? spotifyApi.playlists.getByCategoryId(category.id!) - : spotifyApi.playlists.featured) - .getPage(3, pageKey); - - if (!mounted()) return; - if (page.isLast && page.items != null) { - pagingController.appendLastPage(page.items!.toList()); - } else if (page.items != null) { - pagingController.appendPage( - page.items!.toList(), page.nextOffset); - } - if (_error.value) _error.value = false; - } catch (e, stack) { - if (mounted()) { - if (!_error.value) _error.value = true; - pagingController.error = e; - } - logger.e("pagingController.addPageRequestListener", e, stack); - } - } - - pagingController.addPageRequestListener(listener); - return () { - pagingController.removePageRequestListener(listener); - }; - }, [_error]); - - if (_error.value) return const Text("Something Went Wrong"); - return SizedBox( - height: 245, - child: Scrollbar( - controller: scrollController, - child: PagedListView( - shrinkWrap: true, - pagingController: pagingController, - scrollController: scrollController, - scrollDirection: Axis.horizontal, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, playlist, index) { - return PlaylistCard(playlist); - }, + pagingController.error != null + ? const Text("Something Went Wrong") + : SizedBox( + height: 245, + child: Scrollbar( + controller: scrollController, + child: PagedListView( + shrinkWrap: true, + pagingController: pagingController, + scrollController: scrollController, + scrollDirection: Axis.horizontal, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, playlist, index) { + return PlaylistCard(playlist); + }, + ), ), ), - ), - ); - }, - ) + ) ], ); } diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index b927504b..b70735a6 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -18,11 +18,9 @@ import 'package:spotube/components/Player/Player.dart'; import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useHotKeys.dart'; -import 'package:spotube/hooks/usePagingController.dart'; -import 'package:spotube/hooks/useSharedPreferences.dart'; +import 'package:spotube/hooks/usePaginatedFutureProvider.dart'; import 'package:spotube/models/Logger.dart'; -import 'package:spotube/provider/SpotifyDI.dart'; -import 'package:spotube/provider/UserPreferences.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; List spotifyScopes = [ "playlist-modify-public", @@ -42,12 +40,6 @@ class Home extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - String recommendationMarket = ref.watch(userPreferencesProvider.select( - (value) => (value.recommendationMarket), - )); - - final pagingController = - usePagingController(firstPageKey: 0); final int titleBarDragMaxWidth = useBreakpointValue( md: 72, lg: 256, @@ -58,53 +50,9 @@ class Home extends HookConsumerWidget { final _selectedIndex = useState(0); _onSelectedIndexChanged(int index) => _selectedIndex.value = index; - final localStorage = useSharedPreferences(); - // initializing global hot keys useHotKeys(ref); - final listener = useCallback((int pageKey) async { - final spotify = ref.read(spotifyProvider); - - try { - Page categories = await spotify.categories - .list(country: recommendationMarket) - .getPage(15, pageKey); - - final items = categories.items!.toList(); - if (pageKey == 0) { - Category category = Category(); - category.id = "user-featured-playlists"; - category.name = "Featured"; - items.insert(0, category); - } - - if (categories.isLast && categories.items != null) { - pagingController.appendLastPage(items); - } else if (categories.items != null) { - pagingController.appendPage(items, categories.nextOffset); - } - } catch (e, stack) { - pagingController.error = e; - logger.e("pagingController.addPageRequestListener", e, stack); - } - }, [recommendationMarket]); - - useEffect(() { - try { - pagingController.addPageRequestListener(listener); - // the world is full of surprises and the previously working - // fine pageRequestListener now doesn't notify the listeners - // automatically after assigning a listener. So doing it manually - pagingController.notifyPageRequestListeners(0); - } catch (e, stack) { - logger.e("initState", e, stack); - } - return () { - pagingController.removePageRequestListener(listener); - }; - }, [localStorage]); - final titleBarContents = Container( color: Theme.of(context).scaffoldBackgroundColor, child: Row( @@ -160,14 +108,39 @@ class Home extends HookConsumerWidget { Expanded( child: Padding( padding: const EdgeInsets.all(8.0), - child: PagedListView( - pagingController: pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return CategoryCard(item); + child: HookBuilder(builder: (context) { + final pagingController = usePaginatedFutureProvider< + Page, int, Category>( + (pageKey) => categoriesQuery(pageKey), + ref: ref, + firstPageKey: 0, + onData: (data, pagingController, pageKey) { + final categories = data.value; + final items = categories.items?.toList(); + if (pageKey == 0) { + Category category = Category(); + category.id = "user-featured-playlists"; + category.name = "Featured"; + items?.insert(0, category); + } + if (categories.isLast && items != null) { + pagingController.appendLastPage(items); + } else if (categories.items != null) { + pagingController.appendPage( + items!, categories.nextOffset); + } }, - ), - ), + ); + return PagedListView( + pagingController: pagingController, + builderDelegate: + PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return CategoryCard(item); + }, + ), + ); + }), ), ), if (_selectedIndex.value == 1) const Search(), diff --git a/lib/components/Library/UserArtists.dart b/lib/components/Library/UserArtists.dart index f7eabc1f..7c5e26f5 100644 --- a/lib/components/Library/UserArtists.dart +++ b/lib/components/Library/UserArtists.dart @@ -1,82 +1,49 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; +import 'package:spotube/hooks/usePaginatedFutureProvider.dart'; import 'package:spotube/models/Logger.dart'; -import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; -class UserArtists extends ConsumerStatefulWidget { - const UserArtists({Key? key}) : super(key: key); - - @override - ConsumerState createState() => _UserArtistsState(); -} - -class _UserArtistsState extends ConsumerState { - final PagingController _pagingController = - PagingController(firstPageKey: ""); +class UserArtists extends HookConsumerWidget { + UserArtists({Key? key}) : super(key: key); final logger = getLogger(UserArtists); @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((timestamp) { - _pagingController.addPageRequestListener((pageKey) async { - try { - SpotifyApi spotifyApi = ref.read(spotifyProvider); - CursorPage artists = await spotifyApi.me - .following(FollowingType.artist) - .getPage(15, pageKey); + Widget build(BuildContext context, ref) { + final pagingController = + usePaginatedFutureProvider, String, Artist>( + (pageKey) => currentUserFollowingArtistsQuery(pageKey), + ref: ref, + firstPageKey: "", + onData: (data, pagingController, pageKey) { + final artists = data.value; + final items = artists.items!.toList(); - var items = artists.items!.toList(); - - if (artists.items != null && items.length < 15) { - _pagingController.appendLastPage(items); - } else if (artists.items != null) { - _pagingController.appendPage(items, items.last.id); - } - } catch (e, stack) { - _pagingController.error = e; - logger.e("pagingController", e, stack); + if (artists.items != null && items.length < 15) { + pagingController.appendLastPage(items); + } else if (artists.items != null) { + pagingController.appendPage(items, items.last.id); } - }); - }); - } - - @override - void dispose() { - _pagingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - SpotifyApi spotifyApi = ref.watch(spotifyProvider); - - return FutureBuilder>( - future: spotifyApi.me.following(FollowingType.artist).first(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator.adaptive()); - } - - return PagedGridView( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - childAspectRatio: 9 / 11, - crossAxisSpacing: 20, - mainAxisSpacing: 20, - ), - padding: const EdgeInsets.all(10), - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return ArtistCard(item); - }, - ), - ); }, ); + + return PagedGridView( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + childAspectRatio: 9 / 11, + crossAxisSpacing: 20, + mainAxisSpacing: 20, + ), + padding: const EdgeInsets.all(10), + pagingController: pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return ArtistCard(item); + }, + ), + ); } } diff --git a/lib/components/Library/UserLibrary.dart b/lib/components/Library/UserLibrary.dart index 28083adc..3db22ab4 100644 --- a/lib/components/Library/UserLibrary.dart +++ b/lib/components/Library/UserLibrary.dart @@ -27,10 +27,10 @@ class UserLibrary extends ConsumerWidget { ], ), body: auth.isLoggedIn - ? const TabBarView(children: [ - UserPlaylists(), + ? TabBarView(children: [ + const UserPlaylists(), UserArtists(), - UserAlbums(), + const UserAlbums(), ]) : const AnonymousFallback(), ), diff --git a/lib/hooks/usePaginatedFutureProvider.dart b/lib/hooks/usePaginatedFutureProvider.dart new file mode 100644 index 00000000..4a233292 --- /dev/null +++ b/lib/hooks/usePaginatedFutureProvider.dart @@ -0,0 +1,48 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:spotube/hooks/usePagingController.dart'; + +PagingController usePaginatedFutureProvider( + AutoDisposeFutureProvider Function(P pageKey) createSnapshot, { + required P firstPageKey, + required WidgetRef ref, + void Function( + AsyncData, + PagingController pagingController, + P pageKey, + )? + onData, + void Function(AsyncError)? onError, + void Function(AsyncLoading)? onLoading, +}) { + final currentPageKey = useState(firstPageKey); + final snapshot = ref.watch(createSnapshot(currentPageKey.value)); + final pagingController = + usePagingController(firstPageKey: firstPageKey); + useEffect(() { + listener(pageKey) { + if (currentPageKey.value != pageKey) { + currentPageKey.value = pageKey; + } + } + + pagingController.addPageRequestListener(listener); + return () => pagingController.removePageRequestListener(listener); + }, [snapshot, currentPageKey]); + + useEffect(() { + snapshot.mapOrNull( + data: (data) => + onData?.call(data, pagingController, currentPageKey.value), + error: (error) { + pagingController.error = error; + return onError?.call(error); + }, + loading: onLoading, + ); + return null; + }, [currentPageKey, snapshot]); + + return pagingController; +} diff --git a/lib/provider/SpotifyRequests.dart b/lib/provider/SpotifyRequests.dart index a2854f7d..8766dd30 100644 --- a/lib/provider/SpotifyRequests.dart +++ b/lib/provider/SpotifyRequests.dart @@ -3,7 +3,7 @@ import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/UserPreferences.dart'; -final categoriesQuery = FutureProvider.family, int>( +final categoriesQuery = FutureProvider.autoDispose.family, int>( (ref, pageKey) { final spotify = ref.watch(spotifyProvider); final recommendationMarket = ref.watch( @@ -15,6 +15,20 @@ final categoriesQuery = FutureProvider.family, int>( }, ); +final categoryPlaylistsQuery = + FutureProvider.autoDispose.family, String>( + (ref, value) { + final spotify = ref.watch(spotifyProvider); + final List data = value.split("/"); + final id = data.first; + final pageKey = data.last; + return (id != "user-featured-playlists" + ? spotify.playlists.getByCategoryId(id) + : spotify.playlists.featured) + .getPage(3, int.parse(pageKey)); + }, +); + final currentUserPlaylistsQuery = FutureProvider>( (ref) { final spotify = ref.watch(spotifyProvider); @@ -28,3 +42,11 @@ final currentUserAlbumsQuery = FutureProvider>( return spotify.me.savedAlbums().all(); }, ); + +final currentUserFollowingArtistsQuery = + FutureProvider.autoDispose.family, String>( + (ref, pageKey) { + final spotify = ref.watch(spotifyProvider); + return spotify.me.following(FollowingType.artist).getPage(15, pageKey); + }, +);