feat: use providers in artist page

This commit is contained in:
Kingkor Roy Tirtho 2024-03-17 11:09:26 +06:00
parent 84cb8d7988
commit 786342912b
10 changed files with 132 additions and 108 deletions

View File

@ -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/header.dart';
import 'package:spotube/pages/artist/section/related_artists.dart'; import 'package:spotube/pages/artist/section/related_artists.dart';
import 'package:spotube/pages/artist/section/top_tracks.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 { class ArtistPage extends HookConsumerWidget {
final String artistId; final String artistId;
@ -24,7 +24,7 @@ class ArtistPage extends HookConsumerWidget {
final scrollController = useScrollController(); final scrollController = useScrollController();
final theme = Theme.of(context); final theme = Theme.of(context);
final artistQuery = useQueries.artist.get(ref, artistId); final artistQuery = ref.watch(artistProvider(artistId));
return SafeArea( return SafeArea(
bottom: false, bottom: false,
@ -35,11 +35,11 @@ class ArtistPage extends HookConsumerWidget {
), ),
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
body: Builder(builder: (context) { body: Builder(builder: (context) {
if (artistQuery.hasError && artistQuery.data == null) { if (artistQuery.hasError && artistQuery.value == null) {
return Center(child: Text(artistQuery.error.toString())); return Center(child: Text(artistQuery.error.toString()));
} }
return Skeletonizer( return Skeletonizer(
enabled: artistQuery.isLoading, enabled: artistQuery.isLoadingAndEmpty,
child: CustomScrollView( child: CustomScrollView(
controller: scrollController, controller: scrollController,
slivers: [ slivers: [
@ -66,11 +66,11 @@ class ArtistPage extends HookConsumerWidget {
SliverSafeArea( SliverSafeArea(
sliver: ArtistPageRelatedArtists(artistId: artistId), sliver: ArtistPageRelatedArtists(artistId: artistId),
), ),
if (artistQuery.data != null) if (artistQuery.value != null)
SliverSafeArea( SliverSafeArea(
top: false, top: false,
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: ArtistPageFooter(artist: artistQuery.data!), child: ArtistPageFooter(artist: artistQuery.value!),
), ),
), ),
], ],

View File

@ -5,11 +5,11 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; 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/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class ArtistPageFooter extends HookConsumerWidget { class ArtistPageFooter extends ConsumerWidget {
final Artist artist; final Artist artist;
const ArtistPageFooter({super.key, required this.artist}); const ArtistPageFooter({super.key, required this.artist});
@ -22,8 +22,9 @@ class ArtistPageFooter extends HookConsumerWidget {
artist.images, artist.images,
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
); );
final summary = useQueries.artist.wikipediaSummary(artist); final summary = ref.watch(artistWikipediaSummaryProvider(artist));
if (summary.hasError || !summary.hasData) return const SizedBox.shrink(); if (summary.value == null) return const SizedBox.shrink();
return Container( return Container(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
padding: mediaQuery.smAndDown padding: mediaQuery.smAndDown
@ -38,9 +39,9 @@ class ArtistPageFooter extends HookConsumerWidget {
BlendMode.darken, BlendMode.darken,
), ),
image: UniversalImage.imageProvider( image: UniversalImage.imageProvider(
summary.data!.thumbnail?.source_ ?? artistImage, summary.value!.thumbnail?.source_ ?? artistImage,
height: summary.data!.thumbnail?.height.toDouble(), height: summary.value!.thumbnail?.height.toDouble(),
width: summary.data!.thumbnail?.width.toDouble(), width: summary.value!.thumbnail?.width.toDouble(),
), ),
fit: BoxFit.cover, fit: BoxFit.cover,
alignment: Alignment.center, alignment: Alignment.center,
@ -69,7 +70,7 @@ class ArtistPageFooter extends HookConsumerWidget {
), ),
const TextSpan(text: '\n\n'), const TextSpan(text: '\n\n'),
TextSpan( TextSpan(
text: summary.data!.extract, text: summary.value!.extract,
), ),
TextSpan( TextSpan(
text: '\n...read more at wikipedia', text: '\n...read more at wikipedia',
@ -81,7 +82,7 @@ class ArtistPageFooter extends HookConsumerWidget {
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () async { ..onTap = () async {
await launchUrlString( await launchUrlString(
"http://en.wikipedia.org/wiki?curid=${summary.data?.pageid}", "http://en.wikipedia.org/wiki?curid=${summary.value?.pageid}",
); );
}, },
), ),

View File

@ -1,11 +1,8 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.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:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart'; 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';
@ -14,8 +11,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -25,9 +21,8 @@ class ArtistPageHeader extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final queryClient = useQueryClient(); final artistQuery = ref.watch(artistProvider(artistId));
final artistQuery = useQueries.artist.get(ref, artistId); final artist = artistQuery.value ?? FakeData.artist;
final artist = artistQuery.data ?? FakeData.artist;
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
@ -43,7 +38,6 @@ class ArtistPageHeader extends HookConsumerWidget {
xxl: textTheme.titleMedium, xxl: textTheme.titleMedium,
); );
final spotify = ref.read(spotifyProvider);
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final blacklist = ref.watch(BlackListNotifier.provider); final blacklist = ref.watch(BlackListNotifier.provider);
final isBlackListed = blacklist.contains( final isBlackListed = blacklist.contains(
@ -143,55 +137,43 @@ class ArtistPageHeader extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (auth != null) if (auth != null)
HookBuilder( Consumer(
builder: (context, ref, _) {
final isFollowingQuery = ref
.watch(artistIsFollowingProvider(artist.id!));
final followingArtistNotifier =
ref.watch(followedArtistsProvider.notifier);
return switch (isFollowingQuery) {
AsyncData(value: final following) => Builder(
builder: (context) { builder: (context) {
final isFollowingQuery = if (following) {
useQueries.artist.doIFollow(ref, artistId);
final followUnfollow = useCallback(() async {
try {
isFollowingQuery.data!
? await spotify.me.unfollow(
FollowingType.artist,
[artistId],
)
: await spotify.me.follow(
FollowingType.artist,
[artistId],
);
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( return OutlinedButton(
onPressed: followUnfollow, onPressed: () async {
await followingArtistNotifier
.removeArtists([artist.id!]);
},
child: Text(context.l10n.following), child: Text(context.l10n.following),
); );
} }
return FilledButton( return FilledButton(
onPressed: followUnfollow, onPressed: () async {
await followingArtistNotifier
.saveArtists([artist.id!]);
},
child: Text(context.l10n.follow), child: Text(context.l10n.follow),
); );
}, },
), ),
AsyncError() => const SizedBox(),
_ => const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(),
)
};
},
),
const SizedBox(width: 5), const SizedBox(width: 5),
IconButton( IconButton(
tooltip: context.l10n.add_artist_to_blacklist, tooltip: context.l10n.add_artist_to_blacklist,

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/artist/artist_card.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; final String artistId;
const ArtistPageRelatedArtists({ const ArtistPageRelatedArtists({
super.key, super.key,
@ -12,26 +12,13 @@ class ArtistPageRelatedArtists extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final relatedArtists = useQueries.artist.relatedArtistsOf( final relatedArtists = ref.watch(relatedArtistsProvider(artistId));
ref,
artistId,
);
if (relatedArtists.isLoading || !relatedArtists.hasData) { return switch (relatedArtists) {
return const SliverToBoxAdapter( AsyncData(value: final artists) => SliverPadding(
child: Center(child: CircularProgressIndicator()));
} else if (relatedArtists.hasError) {
return SliverToBoxAdapter(
child: Center(
child: Text(relatedArtists.error.toString()),
),
);
}
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
sliver: SliverGrid.builder( sliver: SliverGrid.builder(
itemCount: relatedArtists.data!.length, itemCount: artists.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200, maxCrossAxisExtent: 200,
mainAxisExtent: 250, mainAxisExtent: 250,
@ -40,10 +27,19 @@ class ArtistPageRelatedArtists extends HookConsumerWidget {
childAspectRatio: 0.8, childAspectRatio: 0.8,
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final artist = relatedArtists.data!.elementAt(index); final artist = artists.elementAt(index);
return ArtistCard(artist); return ArtistCard(artist);
}, },
), ),
); ),
AsyncError(:final error) => SliverToBoxAdapter(
child: Center(
child: Text(error.toString()),
),
),
_ => const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator()),
),
};
} }
} }

View File

@ -7,7 +7,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.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 { class ArtistPageTopTracks extends HookConsumerWidget {
final String artistId; final String artistId;
@ -20,13 +20,10 @@ class ArtistPageTopTracks extends HookConsumerWidget {
final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final topTracksQuery = useQueries.artist.topTracksOf( final topTracksQuery = ref.watch(artistTopTracksProvider(artistId));
ref,
artistId,
);
final isPlaylistPlaying = playlist.containsTracks( final isPlaylistPlaying = playlist.containsTracks(
topTracksQuery.data ?? <Track>[], topTracksQuery.value ?? <Track>[],
); );
if (topTracksQuery.hasError) { if (topTracksQuery.hasError) {
@ -38,7 +35,7 @@ class ArtistPageTopTracks extends HookConsumerWidget {
} }
final topTracks = final topTracks =
topTracksQuery.data ?? List.generate(10, (index) => FakeData.track); topTracksQuery.value ?? List.generate(10, (index) => FakeData.track);
void playPlaylist(List<Track> tracks, {Track? currentTrack}) async { void playPlaylist(List<Track> tracks, {Track? currentTrack}) async {
currentTrack ??= tracks.first; currentTrack ??= tracks.first;

View File

@ -64,6 +64,29 @@ class FollowedArtistsNotifier
], ],
); );
}); });
for (final id in artistIds) {
ref.invalidate(artistIsFollowingProvider(id));
}
}
Future<void> removeArtists(List<String> 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));
}
} }
} }

View File

@ -0,0 +1,9 @@
part of '../spotify.dart';
final relatedArtistsProvider =
FutureProvider.family<List<Artist>, String>((ref, artistId) async {
final spotify = ref.watch(spotifyProvider);
final artists = await spotify.artists.relatedArtists(artistId);
return artists.toList();
});

View File

@ -1,6 +1,6 @@
part of '../spotify.dart'; part of '../spotify.dart';
final artistTopTracksProvider = FutureProviderFamily<List<TrackSimple>, String>( final artistTopTracksProvider = FutureProviderFamily<List<Track>, String>(
(ref, artistId) async { (ref, artistId) async {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final market = ref final market = ref

View File

@ -0,0 +1,12 @@
part of '../spotify.dart';
final artistWikipediaSummaryProvider = FutureProvider.autoDispose
.family<Summary?, ArtistSimple>((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;
});

View File

@ -18,9 +18,11 @@ import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart'; import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
import 'package:spotube/provider/spotify_provider.dart'; 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/services/wikipedia/wikipedia.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'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:wikipedia_api/wikipedia_api.dart';
part 'album/favorite.dart'; part 'album/favorite.dart';
part 'album/tracks.dart'; part 'album/tracks.dart';
@ -32,6 +34,8 @@ part 'artist/is_following.dart';
part 'artist/following.dart'; part 'artist/following.dart';
part 'artist/top_tracks.dart'; part 'artist/top_tracks.dart';
part 'artist/albums.dart'; part 'artist/albums.dart';
part 'artist/wikipedia.dart';
part 'artist/related.dart';
part 'category/genres.dart'; part 'category/genres.dart';
part 'category/categories.dart'; part 'category/categories.dart';