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); + }, +);