From 20ada953123cf7118e63dc3a79fcb6e810d44e10 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 May 2022 22:00:02 +0600 Subject: [PATCH 1/2] Partial async caching support added using FutureProvider --- lib/components/Category/CategoryCard.dart | 116 +++++++++------------- lib/components/Home/Home.dart | 95 +++++++----------- lib/components/Library/UserArtists.dart | 101 +++++++------------ lib/components/Library/UserLibrary.dart | 6 +- lib/hooks/usePaginatedFutureProvider.dart | 48 +++++++++ lib/provider/SpotifyRequests.dart | 24 ++++- 6 files changed, 189 insertions(+), 201 deletions(-) create mode 100644 lib/hooks/usePaginatedFutureProvider.dart 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); + }, +); From 9224b4c3165de55faa209de5dbd4cade23c480af Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 29 May 2022 23:32:47 +0600 Subject: [PATCH 2/2] expanded Cached netwrok request support --- lib/components/Artist/ArtistProfile.dart | 552 +++++++++++----------- lib/components/Category/CategoryCard.dart | 3 +- lib/components/Home/Home.dart | 3 +- lib/components/Library/UserArtists.dart | 3 +- lib/hooks/usePaginatedFutureProvider.dart | 10 +- lib/provider/SpotifyRequests.dart | 41 ++ 6 files changed, 319 insertions(+), 293 deletions(-) diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index e2f17ef2..5d70f457 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -18,6 +18,7 @@ import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; class ArtistProfile extends HookConsumerWidget { final String artistId; @@ -49,296 +50,283 @@ class ArtistProfile extends HookConsumerWidget { final breakpoint = useBreakpoints(); final update = useForceUpdate(); + final Playback playback = ref.watch(playbackProvider); + + final artistsSnapshot = ref.watch(artistProfileQuery(artistId)); + final isFollowingSnapshot = + ref.watch(currentUserFollowsArtistQuery(artistId)); + final topTracksSnapshot = ref.watch(artistTopTracksQuery(artistId)); + final albums = ref.watch(artistAlbumsQuery(artistId)); + final relatedArtists = ref.watch(artistRelatedArtistsQuery(artistId)); + return SafeArea( child: Scaffold( appBar: const PageWindowTitleBar( leading: BackButton(), ), - body: FutureBuilder( - future: spotify.artists.get(artistId), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator.adaptive()); - } - - return SingleChildScrollView( - controller: parentScrollController, - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - const SizedBox(width: 50), - CircleAvatar( - radius: avatarWidth, - backgroundImage: CachedNetworkImageProvider( - imageToUrlString(snapshot.data!.images), - ), - ), - Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: Colors.blue, - borderRadius: BorderRadius.circular(50)), - child: Text(snapshot.data!.type!.toUpperCase(), - style: chipTextVariant?.copyWith( - color: Colors.white)), - ), - Text( - snapshot.data!.name!, - style: breakpoint.isSm - ? textTheme.headline4 - : textTheme.headline2, - ), - Text( - "${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers", - style: breakpoint.isSm - ? textTheme.bodyText1 - : textTheme.headline5, - ), - const SizedBox(height: 20), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - FutureBuilder>( - future: spotify.me.isFollowing( - FollowingType.artist, - [artistId], - ), - builder: (context, snapshot) { - final isFollowing = - snapshot.data?.first == true; - return OutlinedButton( - onPressed: () async { - try { - isFollowing - ? await spotify.me.unfollow( - FollowingType.artist, - [artistId], - ) - : await spotify.me.follow( - FollowingType.artist, - [artistId], - ); - } catch (e, stack) { - logger.e( - "FollowButton.onPressed", - e, - stack, - ); - } finally { - update(); - } - }, - child: snapshot.hasData - ? Text(isFollowing - ? "Following" - : "Follow") - : const CircularProgressIndicator - .adaptive(), - ); - }), - IconButton( - icon: const Icon(Icons.share_rounded), - onPressed: () { - Clipboard.setData( - ClipboardData( - text: snapshot - .data?.externalUrls?.spotify), - ).then((val) { - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - "Artist URL copied to clipboard", - textAlign: TextAlign.center, - ), - ), - ); - }); - }, - ) - ], - ) - ], - ), - ), - ], - ), - const SizedBox(height: 50), - FutureBuilder>( - future: - spotify.artists.getTopTracks(snapshot.data!.id!, "US"), - builder: (context, trackSnapshot) { - if (!trackSnapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive()); - } - Playback playback = ref.watch(playbackProvider); - var isPlaylistPlaying = - playback.currentPlaylist?.id == snapshot.data?.id; - playPlaylist(List tracks, - {Track? currentTrack}) async { - currentTrack ??= tracks.first; - if (!isPlaylistPlaying) { - playback.setCurrentPlaylist = CurrentPlaylist( - tracks: tracks, - id: snapshot.data!.id!, - name: "${snapshot.data!.name!} To Tracks", - thumbnail: imageToUrlString(snapshot.data?.images), - ); - playback.setCurrentTrack = currentTrack; - } else if (isPlaylistPlaying && - currentTrack.id != null && - currentTrack.id != playback.currentTrack?.id) { - playback.setCurrentTrack = currentTrack; - } - await playback.startPlaying(); - } - - return Column(children: [ - Row( - children: [ - Text( - "Top Tracks", - style: Theme.of(context).textTheme.headline4, - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 5), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(50), - ), - child: IconButton( - icon: Icon(isPlaylistPlaying - ? Icons.stop_rounded - : Icons.play_arrow_rounded), - color: Colors.white, - onPressed: trackSnapshot.hasData - ? () => playPlaylist( - trackSnapshot.data!.toList()) - : null, - ), - ) - ], - ), - ...trackSnapshot.data - ?.toList() - .asMap() - .entries - .map((track) { - String duration = - "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - String? thumbnailUrl = imageToUrlString( - track.value.album?.images, - index: - (track.value.album?.images?.length ?? 1) - - 1); - return TrackTile( - playback, - duration: duration, - track: track, - thumbnailUrl: thumbnailUrl, - onTrackPlayButtonPressed: (currentTrack) => - playPlaylist( - trackSnapshot.data!.toList(), - currentTrack: track.value, - ), - ); - }) ?? - [], - ]); - }, - ), - const SizedBox(height: 50), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Albums", - style: Theme.of(context).textTheme.headline4, - ), - TextButton( - child: const Text("See All"), - onPressed: () { - GoRouter.of(context).push( - "/artist-album/$artistId", - extra: snapshot.data?.name ?? "KRTX", - ); - }, - ) - ], - ), - const SizedBox(height: 10), - FutureBuilder>( - future: spotify.artists - .albums(snapshot.data!.id!) - .getPage(5, 0) - .then((al) => al.items?.toList() ?? []), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive()); - } - return Scrollbar( - controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: snapshot.data - ?.map((album) => AlbumCard(album)) - .toList() ?? - [], + body: artistsSnapshot.when( + data: (data) { + return SingleChildScrollView( + controller: parentScrollController, + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + const SizedBox(width: 50), + CircleAvatar( + radius: avatarWidth, + backgroundImage: CachedNetworkImageProvider( + imageToUrlString(data.images), ), ), - ); - }, - ), - const SizedBox(height: 20), - Text( - "Fans also likes", - style: Theme.of(context).textTheme.headline4, - ), - const SizedBox(height: 10), - FutureBuilder>( - future: spotify.artists.getRelatedArtists(artistId), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive()); - } - - return Center( - child: Wrap( - spacing: 20, - runSpacing: 20, - children: snapshot.data - ?.map((artist) => ArtistCard(artist)) - .toList() ?? - [], + Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(50)), + child: Text(data.type!.toUpperCase(), + style: chipTextVariant?.copyWith( + color: Colors.white)), + ), + Text( + data.name!, + style: breakpoint.isSm + ? textTheme.headline4 + : textTheme.headline2, + ), + Text( + "${toReadableNumber(data.followers!.total!.toDouble())} followers", + style: breakpoint.isSm + ? textTheme.bodyText1 + : textTheme.headline5, + ), + const SizedBox(height: 20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + isFollowingSnapshot.when( + data: (isFollowing) { + return OutlinedButton( + onPressed: () async { + try { + isFollowing + ? await spotify.me.unfollow( + FollowingType.artist, + [artistId], + ) + : await spotify.me.follow( + FollowingType.artist, + [artistId], + ); + } catch (e, stack) { + logger.e( + "FollowButton.onPressed", + e, + stack, + ); + } finally { + ref.refresh( + currentUserFollowsArtistQuery( + artistId), + ); + } + }, + child: Text( + isFollowing + ? "Following" + : "Follow", + ), + ); + }, + error: (error, stackTrace) => Container(), + loading: () => + const CircularProgressIndicator + .adaptive()), + IconButton( + icon: const Icon(Icons.share_rounded), + onPressed: () { + Clipboard.setData( + ClipboardData( + text: data.externalUrls?.spotify), + ).then((val) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + "Artist URL copied to clipboard", + textAlign: TextAlign.center, + ), + ), + ); + }); + }, + ) + ], + ) + ], + ), ), - ); - }, - ) - ], - ), - ); - }, - ), + ], + ), + const SizedBox(height: 50), + topTracksSnapshot.when( + data: (topTracks) { + final isPlaylistPlaying = + playback.currentPlaylist?.id == data.id; + playPlaylist(List tracks, + {Track? currentTrack}) async { + currentTrack ??= tracks.first; + if (!isPlaylistPlaying) { + playback.setCurrentPlaylist = CurrentPlaylist( + tracks: tracks, + id: data.id!, + name: "${data.name!} To Tracks", + thumbnail: imageToUrlString(data.images), + ); + playback.setCurrentTrack = currentTrack; + } else if (isPlaylistPlaying && + currentTrack.id != null && + currentTrack.id != playback.currentTrack?.id) { + playback.setCurrentTrack = currentTrack; + } + await playback.startPlaying(); + } + + return Column(children: [ + Row( + children: [ + Text( + "Top Tracks", + style: Theme.of(context).textTheme.headline4, + ), + Container( + margin: + const EdgeInsets.symmetric(horizontal: 5), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(50), + ), + child: IconButton( + icon: Icon(isPlaylistPlaying + ? Icons.stop_rounded + : Icons.play_arrow_rounded), + color: Colors.white, + onPressed: () => + playPlaylist(topTracks.toList()), + ), + ) + ], + ), + ...topTracks.toList().asMap().entries.map((track) { + String duration = + "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; + String? thumbnailUrl = imageToUrlString( + track.value.album?.images, + index: + (track.value.album?.images?.length ?? 1) - + 1); + return TrackTile( + playback, + duration: duration, + track: track, + thumbnailUrl: thumbnailUrl, + onTrackPlayButtonPressed: (currentTrack) => + playPlaylist( + topTracks.toList(), + currentTrack: track.value, + ), + ); + }), + ]); + }, + error: (error, stack) => + Text("Failed to find top tracks $error"), + loading: () => const Center( + child: CircularProgressIndicator.adaptive()), + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Albums", + style: Theme.of(context).textTheme.headline4, + ), + TextButton( + child: const Text("See All"), + onPressed: () { + GoRouter.of(context).push( + "/artist-album/$artistId", + extra: data.name ?? "KRTX", + ); + }, + ) + ], + ), + const SizedBox(height: 10), + albums.when( + data: (albums) { + return Scrollbar( + controller: scrollController, + child: SingleChildScrollView( + controller: scrollController, + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: albums.items + ?.map((album) => AlbumCard(album)) + .toList() ?? + [], + ), + ), + ); + }, + error: (error, stackTrack) => + Text("Failed to get Artist albums $error"), + loading: () => const CircularProgressIndicator.adaptive(), + ), + const SizedBox(height: 20), + Text( + "Fans also likes", + style: Theme.of(context).textTheme.headline4, + ), + const SizedBox(height: 10), + relatedArtists.when( + data: (artists) { + return Center( + child: Wrap( + spacing: 20, + runSpacing: 20, + children: artists + .map((artist) => ArtistCard(artist)) + .toList(), + ), + ); + }, + error: (error, stackTrack) => + Text("Failed to get Artist albums $error"), + loading: () => const CircularProgressIndicator.adaptive(), + ), + ], + ), + ); + }, + error: (_, __) => const Text("Life's miserable"), + loading: () => + const Center(child: CircularProgressIndicator.adaptive())), ), ); } diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index 2efc6955..8308f2cc 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -34,11 +34,10 @@ class CategoryCard extends HookConsumerWidget { ), ref: ref, firstPageKey: 0, - onData: (data, pagingController, pageKey) { + onData: (page, 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) { diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index b70735a6..26f628dc 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -114,8 +114,7 @@ class Home extends HookConsumerWidget { (pageKey) => categoriesQuery(pageKey), ref: ref, firstPageKey: 0, - onData: (data, pagingController, pageKey) { - final categories = data.value; + onData: (categories, pagingController, pageKey) { final items = categories.items?.toList(); if (pageKey == 0) { Category category = Category(); diff --git a/lib/components/Library/UserArtists.dart b/lib/components/Library/UserArtists.dart index 7c5e26f5..ef2b0d7a 100644 --- a/lib/components/Library/UserArtists.dart +++ b/lib/components/Library/UserArtists.dart @@ -18,8 +18,7 @@ class UserArtists extends HookConsumerWidget { (pageKey) => currentUserFollowingArtistsQuery(pageKey), ref: ref, firstPageKey: "", - onData: (data, pagingController, pageKey) { - final artists = data.value; + onData: (artists, pagingController, pageKey) { final items = artists.items!.toList(); if (artists.items != null && items.length < 15) { diff --git a/lib/hooks/usePaginatedFutureProvider.dart b/lib/hooks/usePaginatedFutureProvider.dart index 4a233292..d180e21f 100644 --- a/lib/hooks/usePaginatedFutureProvider.dart +++ b/lib/hooks/usePaginatedFutureProvider.dart @@ -8,13 +8,13 @@ PagingController usePaginatedFutureProvider( required P firstPageKey, required WidgetRef ref, void Function( - AsyncData, + T, PagingController pagingController, P pageKey, )? onData, - void Function(AsyncError)? onError, - void Function(AsyncLoading)? onLoading, + void Function(Object)? onError, + void Function()? onLoading, }) { final currentPageKey = useState(firstPageKey); final snapshot = ref.watch(createSnapshot(currentPageKey.value)); @@ -32,10 +32,10 @@ PagingController usePaginatedFutureProvider( }, [snapshot, currentPageKey]); useEffect(() { - snapshot.mapOrNull( + snapshot.whenOrNull( data: (data) => onData?.call(data, pagingController, currentPageKey.value), - error: (error) { + error: (error, _) { pagingController.error = error; return onError?.call(error); }, diff --git a/lib/provider/SpotifyRequests.dart b/lib/provider/SpotifyRequests.dart index 8766dd30..5a15932b 100644 --- a/lib/provider/SpotifyRequests.dart +++ b/lib/provider/SpotifyRequests.dart @@ -50,3 +50,44 @@ final currentUserFollowingArtistsQuery = return spotify.me.following(FollowingType.artist).getPage(15, pageKey); }, ); + +final artistProfileQuery = FutureProvider.autoDispose.family( + (ref, id) { + final spotify = ref.watch(spotifyProvider); + return spotify.artists.get(id); + }, +); + +final currentUserFollowsArtistQuery = + FutureProvider.autoDispose.family( + (ref, artistId) async { + final spotify = ref.watch(spotifyProvider); + final result = await spotify.me.isFollowing( + FollowingType.artist, + [artistId], + ); + return result.first; + }, +); + +final artistTopTracksQuery = + FutureProvider.autoDispose.family, String>((ref, id) { + final spotify = ref.watch(spotifyProvider); + return spotify.artists.getTopTracks(id, "US"); +}); + +final artistAlbumsQuery = + FutureProvider.autoDispose.family, String>( + (ref, id) { + final spotify = ref.watch(spotifyProvider); + return spotify.artists.albums(id).getPage(5, 0); + }, +); + +final artistRelatedArtistsQuery = + FutureProvider.autoDispose.family, String>( + (ref, id) { + final spotify = ref.watch(spotifyProvider); + return spotify.artists.getRelatedArtists(id); + }, +);