diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 4b88bcf4..38b263cb 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -12,7 +12,7 @@ import 'package:spotube/pages/artist/section/footer.dart'; import 'package:spotube/pages/artist/section/header.dart'; import 'package:spotube/pages/artist/section/related_artists.dart'; import 'package:spotube/pages/artist/section/top_tracks.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class ArtistPage extends HookConsumerWidget { final String artistId; @@ -24,7 +24,7 @@ class ArtistPage extends HookConsumerWidget { final scrollController = useScrollController(); final theme = Theme.of(context); - final artistQuery = useQueries.artist.get(ref, artistId); + final artistQuery = ref.watch(artistProvider(artistId)); return SafeArea( bottom: false, @@ -35,11 +35,11 @@ class ArtistPage extends HookConsumerWidget { ), extendBodyBehindAppBar: true, body: Builder(builder: (context) { - if (artistQuery.hasError && artistQuery.data == null) { + if (artistQuery.hasError && artistQuery.value == null) { return Center(child: Text(artistQuery.error.toString())); } return Skeletonizer( - enabled: artistQuery.isLoading, + enabled: artistQuery.isLoadingAndEmpty, child: CustomScrollView( controller: scrollController, slivers: [ @@ -66,11 +66,11 @@ class ArtistPage extends HookConsumerWidget { SliverSafeArea( sliver: ArtistPageRelatedArtists(artistId: artistId), ), - if (artistQuery.data != null) + if (artistQuery.value != null) SliverSafeArea( top: false, sliver: SliverToBoxAdapter( - child: ArtistPageFooter(artist: artistQuery.data!), + child: ArtistPageFooter(artist: artistQuery.value!), ), ), ], diff --git a/lib/pages/artist/section/footer.dart b/lib/pages/artist/section/footer.dart index 7a5413b6..7b19e24d 100644 --- a/lib/pages/artist/section/footer.dart +++ b/lib/pages/artist/section/footer.dart @@ -5,11 +5,11 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:url_launcher/url_launcher_string.dart'; -class ArtistPageFooter extends HookConsumerWidget { +class ArtistPageFooter extends ConsumerWidget { final Artist artist; const ArtistPageFooter({super.key, required this.artist}); @@ -22,8 +22,9 @@ class ArtistPageFooter extends HookConsumerWidget { artist.images, placeholder: ImagePlaceholder.artist, ); - final summary = useQueries.artist.wikipediaSummary(artist); - if (summary.hasError || !summary.hasData) return const SizedBox.shrink(); + final summary = ref.watch(artistWikipediaSummaryProvider(artist)); + if (summary.value == null) return const SizedBox.shrink(); + return Container( margin: const EdgeInsets.all(16), padding: mediaQuery.smAndDown @@ -38,9 +39,9 @@ class ArtistPageFooter extends HookConsumerWidget { BlendMode.darken, ), image: UniversalImage.imageProvider( - summary.data!.thumbnail?.source_ ?? artistImage, - height: summary.data!.thumbnail?.height.toDouble(), - width: summary.data!.thumbnail?.width.toDouble(), + summary.value!.thumbnail?.source_ ?? artistImage, + height: summary.value!.thumbnail?.height.toDouble(), + width: summary.value!.thumbnail?.width.toDouble(), ), fit: BoxFit.cover, alignment: Alignment.center, @@ -69,7 +70,7 @@ class ArtistPageFooter extends HookConsumerWidget { ), const TextSpan(text: '\n\n'), TextSpan( - text: summary.data!.extract, + text: summary.value!.extract, ), TextSpan( text: '\n...read more at wikipedia', @@ -81,7 +82,7 @@ class ArtistPageFooter extends HookConsumerWidget { recognizer: TapGestureRecognizer() ..onTap = () async { await launchUrlString( - "http://en.wikipedia.org/wiki?curid=${summary.data?.pageid}", + "http://en.wikipedia.org/wiki?curid=${summary.value?.pageid}", ); }, ), diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index e472ddc0..7756da15 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -1,11 +1,8 @@ -import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; @@ -14,8 +11,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; -import 'package:spotube/provider/spotify_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -25,9 +21,8 @@ class ArtistPageHeader extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final queryClient = useQueryClient(); - final artistQuery = useQueries.artist.get(ref, artistId); - final artist = artistQuery.data ?? FakeData.artist; + final artistQuery = ref.watch(artistProvider(artistId)); + final artist = artistQuery.value ?? FakeData.artist; final scaffoldMessenger = ScaffoldMessenger.of(context); final mediaQuery = MediaQuery.of(context); @@ -43,7 +38,6 @@ class ArtistPageHeader extends HookConsumerWidget { xxl: textTheme.titleMedium, ); - final spotify = ref.read(spotifyProvider); final auth = ref.watch(AuthenticationNotifier.provider); final blacklist = ref.watch(BlackListNotifier.provider); final isBlackListed = blacklist.contains( @@ -143,53 +137,41 @@ class ArtistPageHeader extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ if (auth != null) - HookBuilder( - builder: (context) { - final isFollowingQuery = - useQueries.artist.doIFollow(ref, artistId); + Consumer( + builder: (context, ref, _) { + final isFollowingQuery = ref + .watch(artistIsFollowingProvider(artist.id!)); + final followingArtistNotifier = + ref.watch(followedArtistsProvider.notifier); - final followUnfollow = useCallback(() async { - try { - isFollowingQuery.data! - ? await spotify.me.unfollow( - FollowingType.artist, - [artistId], - ) - : await spotify.me.follow( - FollowingType.artist, - [artistId], + return switch (isFollowingQuery) { + AsyncData(value: final following) => Builder( + builder: (context) { + if (following) { + return OutlinedButton( + onPressed: () async { + await followingArtistNotifier + .removeArtists([artist.id!]); + }, + child: Text(context.l10n.following), ); - await isFollowingQuery.refresh(); + } - queryClient.refreshInfiniteQueryAllPages( - "user-following-artists"); - } finally { - queryClient.refreshQuery( - "user-follows-artists-query/$artistId", - ); - } - }, [isFollowingQuery]); - - if (isFollowingQuery.isLoading || - !isFollowingQuery.hasData) { - return const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(), - ); - } - - if (isFollowingQuery.data!) { - return OutlinedButton( - onPressed: followUnfollow, - child: Text(context.l10n.following), - ); - } - - return FilledButton( - onPressed: followUnfollow, - child: Text(context.l10n.follow), - ); + return FilledButton( + onPressed: () async { + await followingArtistNotifier + .saveArtists([artist.id!]); + }, + child: Text(context.l10n.follow), + ); + }, + ), + AsyncError() => const SizedBox(), + _ => const SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(), + ) + }; }, ), const SizedBox(width: 5), diff --git a/lib/pages/artist/section/related_artists.dart b/lib/pages/artist/section/related_artists.dart index 1efcd692..7fc48ded 100644 --- a/lib/pages/artist/section/related_artists.dart +++ b/lib/pages/artist/section/related_artists.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/artist/artist_card.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; -class ArtistPageRelatedArtists extends HookConsumerWidget { +class ArtistPageRelatedArtists extends ConsumerWidget { final String artistId; const ArtistPageRelatedArtists({ super.key, @@ -12,38 +12,34 @@ class ArtistPageRelatedArtists extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final relatedArtists = useQueries.artist.relatedArtistsOf( - ref, - artistId, - ); + final relatedArtists = ref.watch(relatedArtistsProvider(artistId)); - if (relatedArtists.isLoading || !relatedArtists.hasData) { - return const SliverToBoxAdapter( - child: Center(child: CircularProgressIndicator())); - } else if (relatedArtists.hasError) { - return SliverToBoxAdapter( - child: Center( - child: Text(relatedArtists.error.toString()), + return switch (relatedArtists) { + AsyncData(value: final artists) => SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + sliver: SliverGrid.builder( + itemCount: artists.length, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: 250, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 0.8, + ), + itemBuilder: (context, index) { + final artist = artists.elementAt(index); + return ArtistCard(artist); + }, + ), ), - ); - } - - return SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - sliver: SliverGrid.builder( - itemCount: relatedArtists.data!.length, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisExtent: 250, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: 0.8, + AsyncError(:final error) => SliverToBoxAdapter( + child: Center( + child: Text(error.toString()), + ), ), - itemBuilder: (context, index) { - final artist = relatedArtists.data!.elementAt(index); - return ArtistCard(artist); - }, - ), - ); + _ => const SliverToBoxAdapter( + child: Center(child: CircularProgressIndicator()), + ), + }; } } diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart index 1209daca..9ad2b0db 100644 --- a/lib/pages/artist/section/top_tracks.dart +++ b/lib/pages/artist/section/top_tracks.dart @@ -7,7 +7,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/services/queries/queries.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; class ArtistPageTopTracks extends HookConsumerWidget { final String artistId; @@ -20,13 +20,10 @@ class ArtistPageTopTracks extends HookConsumerWidget { final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); - final topTracksQuery = useQueries.artist.topTracksOf( - ref, - artistId, - ); + final topTracksQuery = ref.watch(artistTopTracksProvider(artistId)); final isPlaylistPlaying = playlist.containsTracks( - topTracksQuery.data ?? [], + topTracksQuery.value ?? [], ); if (topTracksQuery.hasError) { @@ -38,7 +35,7 @@ class ArtistPageTopTracks extends HookConsumerWidget { } final topTracks = - topTracksQuery.data ?? List.generate(10, (index) => FakeData.track); + topTracksQuery.value ?? List.generate(10, (index) => FakeData.track); void playPlaylist(List tracks, {Track? currentTrack}) async { currentTrack ??= tracks.first; diff --git a/lib/provider/spotify/artist/following.dart b/lib/provider/spotify/artist/following.dart index 57552720..c75a3c47 100644 --- a/lib/provider/spotify/artist/following.dart +++ b/lib/provider/spotify/artist/following.dart @@ -64,6 +64,29 @@ class FollowedArtistsNotifier ], ); }); + + for (final id in artistIds) { + ref.invalidate(artistIsFollowingProvider(id)); + } + } + + Future removeArtists(List artistIds) async { + if (state.value == null) return; + await spotify.me.unfollow(FollowingType.artist, artistIds); + + state = await AsyncValue.guard(() async { + final artists = state.value!.items.where((artist) { + return !artistIds.contains(artist.id); + }).toList(); + + return state.value!.copyWith( + items: artists, + ); + }); + + for (final id in artistIds) { + ref.invalidate(artistIsFollowingProvider(id)); + } } } diff --git a/lib/provider/spotify/artist/related.dart b/lib/provider/spotify/artist/related.dart new file mode 100644 index 00000000..6782f56e --- /dev/null +++ b/lib/provider/spotify/artist/related.dart @@ -0,0 +1,9 @@ +part of '../spotify.dart'; + +final relatedArtistsProvider = + FutureProvider.family, String>((ref, artistId) async { + final spotify = ref.watch(spotifyProvider); + final artists = await spotify.artists.relatedArtists(artistId); + + return artists.toList(); +}); diff --git a/lib/provider/spotify/artist/top_tracks.dart b/lib/provider/spotify/artist/top_tracks.dart index e6e1b9dd..77186689 100644 --- a/lib/provider/spotify/artist/top_tracks.dart +++ b/lib/provider/spotify/artist/top_tracks.dart @@ -1,6 +1,6 @@ part of '../spotify.dart'; -final artistTopTracksProvider = FutureProviderFamily, String>( +final artistTopTracksProvider = FutureProviderFamily, String>( (ref, artistId) async { final spotify = ref.watch(spotifyProvider); final market = ref diff --git a/lib/provider/spotify/artist/wikipedia.dart b/lib/provider/spotify/artist/wikipedia.dart new file mode 100644 index 00000000..b2e2e6dc --- /dev/null +++ b/lib/provider/spotify/artist/wikipedia.dart @@ -0,0 +1,12 @@ +part of '../spotify.dart'; + +final artistWikipediaSummaryProvider = FutureProvider.autoDispose + .family((ref, artist) async { + final query = artist.name!.replaceAll(" ", "_"); + final res = await wikipedia.pageContent.pageSummaryTitleGet(query); + + if (res?.type != "standard") { + return await wikipedia.pageContent.pageSummaryTitleGet("${query}_(singer)"); + } + return res; +}); diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index d7fc91aa..fdea3915 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -18,9 +18,11 @@ import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/wikipedia/wikipedia.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:http/http.dart' as http; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:wikipedia_api/wikipedia_api.dart'; part 'album/favorite.dart'; part 'album/tracks.dart'; @@ -32,6 +34,8 @@ part 'artist/is_following.dart'; part 'artist/following.dart'; part 'artist/top_tracks.dart'; part 'artist/albums.dart'; +part 'artist/wikipedia.dart'; +part 'artist/related.dart'; part 'category/genres.dart'; part 'category/categories.dart';