From 78b7759f10dc3cd581b31a1119292673d08f2ce6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 13 Mar 2024 19:42:31 +0600 Subject: [PATCH] feat: add artist related providers --- .vscode/snippets.code-snippets | 9 +++ lib/provider/spotify/album/favorite.dart | 12 +-- lib/provider/spotify/album/is_saved.dart | 3 +- lib/provider/spotify/album/releases.dart | 4 + lib/provider/spotify/album/tracks.dart | 4 + lib/provider/spotify/artist/albums.dart | 60 ++++++++++++++ lib/provider/spotify/artist/artist.dart | 7 ++ lib/provider/spotify/artist/following.dart | 81 +++++++++++++++++++ lib/provider/spotify/artist/is_following.dart | 10 +++ lib/provider/spotify/artist/top_tracks.dart | 12 +++ lib/provider/spotify/spotify.dart | 7 ++ lib/provider/spotify/utils/provider.dart | 67 ++++++++++++++- lib/provider/spotify/utils/state.dart | 50 ++++++++++-- 13 files changed, 310 insertions(+), 16 deletions(-) create mode 100644 lib/provider/spotify/artist/albums.dart create mode 100644 lib/provider/spotify/artist/artist.dart create mode 100644 lib/provider/spotify/artist/following.dart create mode 100644 lib/provider/spotify/artist/is_following.dart create mode 100644 lib/provider/spotify/artist/top_tracks.dart diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets index c632ae66..9a18929b 100644 --- a/.vscode/snippets.code-snippets +++ b/.vscode/snippets.code-snippets @@ -9,6 +9,7 @@ " required super.items,", " required super.offset,", " required super.limit,", + " required super.hasMore,", " });", " ", " @override", @@ -16,11 +17,13 @@ " List<${2:Model}>? items,", " int? offset,", " int? limit,", + " bool? hasMore,", " }) {", " return ${1:Model}State(", " items: items ?? this.items,", " offset: offset ?? this.offset,", " limit: limit ?? this.limit,", + " hasMore: hasMore ?? this.hasMore,", " );", " }", "}" @@ -56,6 +59,7 @@ " required super.items,", " required super.offset,", " required super.limit,", + " required super.hasMore,", " });", " ", " @override", @@ -63,11 +67,13 @@ " List<$2>? items,", " int? offset,", " int? limit,", + " bool? hasMore,", " }) {", " return $1State(", " items: items ?? this.items,", " offset: offset ?? this.offset,", " limit: limit ?? this.limit,", + " hasMore: hasMore ?? this.hasMore,", " );", " }", "}", @@ -122,6 +128,7 @@ " required super.items,", " required super.offset,", " required super.limit,", + " required super.hasMore,", " });", " ", " @override", @@ -129,11 +136,13 @@ " List<$2>? items,", " int? offset,", " int? limit,", + " bool? hasMore,", " }) {", " return $1State(", " items: items ?? this.items,", " offset: offset ?? this.offset,", " limit: limit ?? this.limit,", + " hasMore: hasMore ?? this.hasMore,", " );", " }", "}", diff --git a/lib/provider/spotify/album/favorite.dart b/lib/provider/spotify/album/favorite.dart index 240a0b80..8cdda862 100644 --- a/lib/provider/spotify/album/favorite.dart +++ b/lib/provider/spotify/album/favorite.dart @@ -5,18 +5,16 @@ class FavoriteAlbumState extends PaginatedState { required super.items, required super.offset, required super.limit, + required super.hasMore, }); @override - FavoriteAlbumState copyWith({ - items, - offset, - limit, - }) { + FavoriteAlbumState copyWith({items, offset, limit, hasMore}) { return FavoriteAlbumState( items: items ?? this.items, offset: offset ?? this.offset, limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, ); } } @@ -34,10 +32,12 @@ class FavoriteAlbumNotifier @override build() async { ref.watch(spotifyProvider); + final items = await fetch(0, 20); return FavoriteAlbumState( - items: await fetch(0, 20), + items: items, offset: 0, limit: 20, + hasMore: items.length == 20, ); } diff --git a/lib/provider/spotify/album/is_saved.dart b/lib/provider/spotify/album/is_saved.dart index 11442792..987ccdf2 100644 --- a/lib/provider/spotify/album/is_saved.dart +++ b/lib/provider/spotify/album/is_saved.dart @@ -1,5 +1,4 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:spotube/provider/spotify_provider.dart'; +part of '../spotify.dart'; final albumsIsSavedProvider = FutureProvider.autoDispose.family( (ref, albumId) async { diff --git a/lib/provider/spotify/album/releases.dart b/lib/provider/spotify/album/releases.dart index 41897c21..1aec1bc2 100644 --- a/lib/provider/spotify/album/releases.dart +++ b/lib/provider/spotify/album/releases.dart @@ -5,6 +5,7 @@ class AlbumReleasesState extends PaginatedState { required super.items, required super.offset, required super.limit, + required super.hasMore, }); @override @@ -12,11 +13,13 @@ class AlbumReleasesState extends PaginatedState { List? items, int? offset, int? limit, + bool? hasMore, }) { return AlbumReleasesState( items: items ?? this.items, offset: offset ?? this.offset, limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, ); } } @@ -45,6 +48,7 @@ class AlbumReleasesNotifier items: albums, offset: 0, limit: 20, + hasMore: albums.length == 20, ); } } diff --git a/lib/provider/spotify/album/tracks.dart b/lib/provider/spotify/album/tracks.dart index cf754c60..da76eeaa 100644 --- a/lib/provider/spotify/album/tracks.dart +++ b/lib/provider/spotify/album/tracks.dart @@ -5,6 +5,7 @@ class AlbumTracksState extends PaginatedState { required super.items, required super.offset, required super.limit, + required super.hasMore, }); @override @@ -12,11 +13,13 @@ class AlbumTracksState extends PaginatedState { List? items, int? offset, int? limit, + bool? hasMore, }) { return AlbumTracksState( items: items ?? this.items, offset: offset ?? this.offset, limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, ); } } @@ -39,6 +42,7 @@ class AlbumTracksNotifier extends FamilyPaginatedAsyncNotifier { + ArtistAlbumsState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + @override + ArtistAlbumsState copyWith({ + List? items, + int? offset, + int? limit, + bool? hasMore, + }) { + return ArtistAlbumsState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, + ); + } +} + +class ArtistAlbumsNotifier extends FamilyPaginatedAsyncNotifier { + ArtistAlbumsNotifier() : super(); + + @override + fetch(arg, offset, limit) async { + final market = ref.read(userPreferencesProvider).recommendationMarket; + final albums = await spotify.artists + .albums(arg, country: market) + .getPage(offset, limit); + + return albums.items?.toList() ?? []; + } + + @override + build(arg) async { + ref.watch(spotifyProvider); + ref.watch( + userPreferencesProvider.select((s) => s.recommendationMarket), + ); + final albums = await fetch(arg, 0, 20); + return ArtistAlbumsState( + items: albums, + offset: 0, + limit: 20, + hasMore: albums.length == 20, + ); + } +} + +final artistAlbumsProvider = AsyncNotifierProviderFamily( + () => ArtistAlbumsNotifier(), +); diff --git a/lib/provider/spotify/artist/artist.dart b/lib/provider/spotify/artist/artist.dart new file mode 100644 index 00000000..5f795658 --- /dev/null +++ b/lib/provider/spotify/artist/artist.dart @@ -0,0 +1,7 @@ +part of '../spotify.dart'; + +final artistProvider = FutureProvider.family((ref, String artistId) { + final spotify = ref.watch(spotifyProvider); + + return spotify.artists.get(artistId); +}); diff --git a/lib/provider/spotify/artist/following.dart b/lib/provider/spotify/artist/following.dart new file mode 100644 index 00000000..57552720 --- /dev/null +++ b/lib/provider/spotify/artist/following.dart @@ -0,0 +1,81 @@ +part of '../spotify.dart'; + +class FollowedArtistsState extends CursorPaginatedState { + FollowedArtistsState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + @override + FollowedArtistsState copyWith({ + List? items, + String? offset, + int? limit, + bool? hasMore, + }) { + return FollowedArtistsState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + hasMore: hasMore ?? this.hasMore, + ); + } +} + +class FollowedArtistsNotifier + extends CursorPaginatedAsyncNotifier { + FollowedArtistsNotifier() : super(); + + @override + fetch(offset, limit) async { + final artists = await spotify.me.following(FollowingType.artist).getPage( + limit, + offset ?? '', + ); + + return (artists.items?.toList() ?? [], artists.after); + } + + @override + build() async { + ref.watch(spotifyProvider); + final (artists, nextCursor) = await fetch(null, 50); + return FollowedArtistsState( + items: artists, + offset: nextCursor, + limit: 50, + hasMore: artists.length == 50, + ); + } + + Future saveArtists(List artistIds) async { + if (state.value == null) return; + await spotify.me.follow(FollowingType.artist, artistIds); + + state = await AsyncValue.guard(() async { + final artists = await spotify.artists.list(artistIds); + + return state.value!.copyWith( + items: [ + ...state.value!.items, + ...artists, + ], + ); + }); + } +} + +final followedArtistsProvider = + AsyncNotifierProvider( + () => FollowedArtistsNotifier(), +); + +final allFollowedArtistsProvider = FutureProvider>( + (ref) async { + final spotify = ref.watch(spotifyProvider); + final artists = await spotify.me.following(FollowingType.artist).all(); + return artists.toList(); + }, +); diff --git a/lib/provider/spotify/artist/is_following.dart b/lib/provider/spotify/artist/is_following.dart new file mode 100644 index 00000000..db1be184 --- /dev/null +++ b/lib/provider/spotify/artist/is_following.dart @@ -0,0 +1,10 @@ +part of '../spotify.dart'; + +final artistIsFollowingProvider = FutureProvider.family( + (ref, String artistId) async { + final spotify = ref.watch(spotifyProvider); + return spotify.me.checkFollowing(FollowingType.artist, [artistId]).then( + (value) => value[artistId] ?? false, + ); + }, +); diff --git a/lib/provider/spotify/artist/top_tracks.dart b/lib/provider/spotify/artist/top_tracks.dart new file mode 100644 index 00000000..e6e1b9dd --- /dev/null +++ b/lib/provider/spotify/artist/top_tracks.dart @@ -0,0 +1,12 @@ +part of '../spotify.dart'; + +final artistTopTracksProvider = FutureProviderFamily, String>( + (ref, artistId) async { + final spotify = ref.watch(spotifyProvider); + final market = ref + .watch(userPreferencesProvider.select((s) => s.recommendationMarket)); + final tracks = await spotify.artists.topTracks(artistId, market); + + return tracks.toList(); + }, +); diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index fc8f82ab..26f900dc 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -10,6 +10,13 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart part 'album/favorite.dart'; part 'album/tracks.dart'; part 'album/releases.dart'; +part 'album/is_saved.dart'; + +part 'artist/artist.dart'; +part 'artist/is_following.dart'; +part 'artist/following.dart'; +part 'artist/top_tracks.dart'; +part 'artist/albums.dart'; part 'utils/mixin.dart'; part 'utils/state.dart'; diff --git a/lib/provider/spotify/utils/provider.dart b/lib/provider/spotify/utils/provider.dart index 343d6c18..4e748608 100644 --- a/lib/provider/spotify/utils/provider.dart +++ b/lib/provider/spotify/utils/provider.dart @@ -1,6 +1,6 @@ part of '../spotify.dart'; -abstract class PaginatedAsyncNotifier> +abstract class PaginatedAsyncNotifier> extends AsyncNotifier with SpotifyMixin { Future> fetch(int offset, int limit); @@ -11,6 +11,7 @@ abstract class PaginatedAsyncNotifier> (state) async { final items = await fetch(state.offset + state.limit, state.limit); return state.copyWith( + hasMore: items.length == state.limit, items: [ ...state.items, ...items, @@ -22,8 +23,34 @@ abstract class PaginatedAsyncNotifier> } } -abstract class FamilyPaginatedAsyncNotifier, A> - extends FamilyAsyncNotifier with SpotifyMixin { +abstract class CursorPaginatedAsyncNotifier> extends AsyncNotifier + with SpotifyMixin { + Future<(List items, String nextCursor)> fetch(String? offset, int limit); + + Future fetchMore() async { + if (state.value == null || !state.value!.hasMore) return; + + await update( + (state) async { + final items = await fetch(state.offset, state.limit); + return state.copyWith( + hasMore: items.$1.length == state.limit, + items: [ + ...state.items, + ...items.$1, + ], + offset: items.$2, + ) as T; + }, + ); + } +} + +abstract class FamilyPaginatedAsyncNotifier< + K, + T extends BasePaginatedState, + A> extends FamilyAsyncNotifier with SpotifyMixin { Future> fetch(A arg, int offset, int limit); Future fetchMore() async { @@ -37,6 +64,7 @@ abstract class FamilyPaginatedAsyncNotifier, A> state.limit, ); return state.copyWith( + hasMore: items.length == state.limit, items: [ ...state.items, ...items, @@ -47,3 +75,36 @@ abstract class FamilyPaginatedAsyncNotifier, A> ); } } + +abstract class FamilyCursorPaginatedAsyncNotifier< + K, + T extends CursorPaginatedState, + A> extends FamilyAsyncNotifier with SpotifyMixin { + Future<(List items, String nextCursor)> fetch( + A arg, + String? offset, + int limit, + ); + + Future fetchMore() async { + if (state.value == null || !state.value!.hasMore) return; + + await update( + (state) async { + final items = await fetch( + arg, + state.offset, + state.limit, + ); + return state.copyWith( + hasMore: items.$1.length == state.limit, + items: [ + ...state.items, + ...items.$1, + ], + offset: items.$2, + ) as T; + }, + ); + } +} diff --git a/lib/provider/spotify/utils/state.dart b/lib/provider/spotify/utils/state.dart index 002323f3..4b79ac7d 100644 --- a/lib/provider/spotify/utils/state.dart +++ b/lib/provider/spotify/utils/state.dart @@ -1,16 +1,56 @@ part of '../spotify.dart'; -abstract class PaginatedState { +abstract class BasePaginatedState { final List items; - final int offset; + final Cursor offset; final int limit; final bool hasMore; - PaginatedState({ + BasePaginatedState({ required this.items, required this.offset, required this.limit, - }) : hasMore = items.length >= limit; + required this.hasMore, + }); - PaginatedState copyWith({List? items, int? offset, int? limit}); + BasePaginatedState copyWith({ + List? items, + Cursor? offset, + int? limit, + bool? hasMore, + }); +} + +abstract class PaginatedState extends BasePaginatedState { + PaginatedState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + @override + PaginatedState copyWith({ + List? items, + int? offset, + int? limit, + bool? hasMore, + }); +} + +abstract class CursorPaginatedState extends BasePaginatedState { + CursorPaginatedState({ + required super.items, + required super.offset, + required super.limit, + required super.hasMore, + }); + + @override + CursorPaginatedState copyWith({ + List? items, + String? offset, + int? limit, + bool? hasMore, + }); }