diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets new file mode 100644 index 00000000..c632ae66 --- /dev/null +++ b/.vscode/snippets.code-snippets @@ -0,0 +1,161 @@ +{ + "PaginatedState": { + "scope": "dart", + "prefix": "paginatedState", + "description": "Generate a PaginatedState", + "body": [ + "class ${1:Model}State extends PaginatedState<${2:Model}> {", + " ${1:Model}State({", + " required super.items,", + " required super.offset,", + " required super.limit,", + " });", + " ", + " @override", + " ${1:Model}State copyWith({", + " List<${2:Model}>? items,", + " int? offset,", + " int? limit,", + " }) {", + " return ${1:Model}State(", + " items: items ?? this.items,", + " offset: offset ?? this.offset,", + " limit: limit ?? this.limit,", + " );", + " }", + "}" + ] + }, + "PaginatedAsyncNotifier": { + "scope": "dart", + "prefix": "paginatedAsyncNotifier", + "description": "Generate a PaginatedAsyncNotifier", + "body": [ + "class ${1:NotifierName}Notifier extends PaginatedAsyncNotifier<${3:Item}, ${2:Model}State> {", + " ${1:NotifierName}Notifier() : super();", + " ", + " @override", + " fetch(int offset, int limit) async {", + " throw UnimplementedError();", + " }", + " ", + " @override", + " build() async {", + " throw UnimplementedError();", + " }", + "}" + ] + }, + "PaginaitedNotifierWithState": { + "scope": "dart", + "prefix": "paginatedNotifierWithState", + "description": "Generate a PaginatedNotifier with PaginatedState", + "body": [ + "class $1State extends PaginatedState<$2> {", + " $1State({", + " required super.items,", + " required super.offset,", + " required super.limit,", + " });", + " ", + " @override", + " $1State copyWith({", + " List<$2>? items,", + " int? offset,", + " int? limit,", + " }) {", + " return $1State(", + " items: items ?? this.items,", + " offset: offset ?? this.offset,", + " limit: limit ?? this.limit,", + " );", + " }", + "}", + " ", + "class $1Notifier", + " extends PaginatedAsyncNotifier<$2, $1State> {", + " $1Notifier() : super();", + " ", + " @override", + " fetch(int offset, int limit) async {", + " throw UnimplementedError();", + " }", + " ", + " @override", + " build() async {", + " throw UnimplementedError();", + " }", + "}", + " ", + "final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProvider<$1Notifier, $1State>(", + " ()=> $1Notifier(),", + ");" + ] + }, + "FamilyPaginatedAsyncNotifier": { + "scope": "dart", + "prefix": "familyPaginatedAsyncNotifier", + "description": "Generate a FamilyPaginatedAsyncNotifier", + "body": [ + "class ${1:NotifierName}Notifier extends FamilyPaginatedAsyncNotifier<${3:Item}, ${2:Model}State, {$4:Arg}> {", + " ${1:NotifierName}Notifier() : super();", + " ", + " @override", + " fetch(arg, offset, limit) async {", + " throw UnimplementedError();", + " }", + " ", + " @override", + " build(arg) async {", + " throw UnimplementedError();", + " }", + "}" + ] + }, + "FamilyPaginaitedNotifierWithState": { + "scope": "dart", + "prefix": "familyPaginatedNotifierWithState", + "description": "Generate a FamilyPaginatedAsyncNotifier with PaginatedState", + "body": [ + "class $1State extends PaginatedState<$2> {", + " $1State({", + " required super.items,", + " required super.offset,", + " required super.limit,", + " });", + " ", + " @override", + " $1State copyWith({", + " List<$2>? items,", + " int? offset,", + " int? limit,", + " }) {", + " return $1State(", + " items: items ?? this.items,", + " offset: offset ?? this.offset,", + " limit: limit ?? this.limit,", + " );", + " }", + "}", + " ", + "class $1Notifier", + " extends FamilyPaginatedAsyncNotifier<$2, $1State, $3> {", + " $1Notifier() : super();", + " ", + " @override", + " fetch(arg, offset, limit) async {", + " throw UnimplementedError();", + " }", + " ", + " @override", + " build(arg) async {", + " throw UnimplementedError();", + " }", + "}", + " ", + "final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProviderFamily<$1Notifier, $1State, $3>(", + " ()=> $1Notifier(),", + ");" + ] + }, +} \ No newline at end of file diff --git a/lib/provider/spotify/album/favorite.dart b/lib/provider/spotify/album/favorite.dart index af37894e..240a0b80 100644 --- a/lib/provider/spotify/album/favorite.dart +++ b/lib/provider/spotify/album/favorite.dart @@ -22,8 +22,7 @@ class FavoriteAlbumState extends PaginatedState { } class FavoriteAlbumNotifier - extends PaginatedAsyncNotifier - with SpotifyMixin { + extends PaginatedAsyncNotifier { @override Future> fetch(int offset, int limit) { return spotify.me @@ -59,3 +58,8 @@ class FavoriteAlbumNotifier }); } } + +final favoriteAlbumsProvider = + AsyncNotifierProvider( + () => FavoriteAlbumNotifier(), +); diff --git a/lib/provider/spotify/album/is_saved.dart b/lib/provider/spotify/album/is_saved.dart new file mode 100644 index 00000000..11442792 --- /dev/null +++ b/lib/provider/spotify/album/is_saved.dart @@ -0,0 +1,11 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/provider/spotify_provider.dart'; + +final albumsIsSavedProvider = FutureProvider.autoDispose.family( + (ref, albumId) async { + final spotify = ref.watch(spotifyProvider); + return spotify.me.containsSavedAlbums([albumId]).then( + (value) => value[albumId] ?? false, + ); + }, +); diff --git a/lib/provider/spotify/album/releases.dart b/lib/provider/spotify/album/releases.dart new file mode 100644 index 00000000..41897c21 --- /dev/null +++ b/lib/provider/spotify/album/releases.dart @@ -0,0 +1,55 @@ +part of '../spotify.dart'; + +class AlbumReleasesState extends PaginatedState { + AlbumReleasesState({ + required super.items, + required super.offset, + required super.limit, + }); + + @override + AlbumReleasesState copyWith({ + List? items, + int? offset, + int? limit, + }) { + return AlbumReleasesState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + ); + } +} + +class AlbumReleasesNotifier + extends PaginatedAsyncNotifier { + AlbumReleasesNotifier() : super(); + + @override + fetch(int offset, int limit) async { + final market = ref.read(userPreferencesProvider).recommendationMarket; + final albums = await spotify.browse + .newReleases(country: market) + .getPage(offset, limit); + return albums.items?.toList() ?? []; + } + + @override + build() async { + ref.watch(spotifyProvider); + ref.watch( + userPreferencesProvider.select((s) => s.recommendationMarket), + ); + final albums = await fetch(0, 20); + return AlbumReleasesState( + items: albums, + offset: 0, + limit: 20, + ); + } +} + +final albumReleasesProvider = + AsyncNotifierProvider( + () => AlbumReleasesNotifier(), +); diff --git a/lib/provider/spotify/album/tracks.dart b/lib/provider/spotify/album/tracks.dart new file mode 100644 index 00000000..cf754c60 --- /dev/null +++ b/lib/provider/spotify/album/tracks.dart @@ -0,0 +1,49 @@ +part of '../spotify.dart'; + +class AlbumTracksState extends PaginatedState { + AlbumTracksState({ + required super.items, + required super.offset, + required super.limit, + }); + + @override + AlbumTracksState copyWith({ + List? items, + int? offset, + int? limit, + }) { + return AlbumTracksState( + items: items ?? this.items, + offset: offset ?? this.offset, + limit: limit ?? this.limit, + ); + } +} + +class AlbumTracksNotifier extends FamilyPaginatedAsyncNotifier { + AlbumTracksNotifier() : super(); + + @override + fetch(arg, offset, limit) async { + final tracks = await spotify.albums.tracks(arg).getPage(offset, limit); + return tracks.items?.toList() ?? []; + } + + @override + build(arg) async { + ref.watch(spotifyProvider); + final tracks = await fetch(arg, 0, 20); + return AlbumTracksState( + items: tracks, + offset: 0, + limit: 20, + ); + } +} + +final albumTracksProvider = + AsyncNotifierProviderFamily( + () => AlbumTracksNotifier(), +); diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index 091ff897..fc8f82ab 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -2,9 +2,15 @@ library spotify; import 'package:spotify/spotify.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +// ignore: depend_on_referenced_packages, implementation_imports +import 'package:riverpod/src/async_notifier.dart'; import 'package:spotube/provider/spotify_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; part 'album/favorite.dart'; +part 'album/tracks.dart'; +part 'album/releases.dart'; + part 'utils/mixin.dart'; part 'utils/state.dart'; part 'utils/provider.dart'; diff --git a/lib/provider/spotify/utils/mixin.dart b/lib/provider/spotify/utils/mixin.dart index db41daaf..0728c207 100644 --- a/lib/provider/spotify/utils/mixin.dart +++ b/lib/provider/spotify/utils/mixin.dart @@ -1,5 +1,6 @@ part of '../spotify.dart'; -mixin SpotifyMixin on AsyncNotifier { +// ignore: invalid_use_of_internal_member +mixin SpotifyMixin on BuildlessAsyncNotifier { SpotifyApi get spotify => ref.read(spotifyProvider); } diff --git a/lib/provider/spotify/utils/provider.dart b/lib/provider/spotify/utils/provider.dart index 08cd022d..343d6c18 100644 --- a/lib/provider/spotify/utils/provider.dart +++ b/lib/provider/spotify/utils/provider.dart @@ -1,7 +1,7 @@ part of '../spotify.dart'; abstract class PaginatedAsyncNotifier> - extends AsyncNotifier { + extends AsyncNotifier with SpotifyMixin { Future> fetch(int offset, int limit); Future fetchMore() async { @@ -21,3 +21,29 @@ abstract class PaginatedAsyncNotifier> ); } } + +abstract class FamilyPaginatedAsyncNotifier, A> + extends FamilyAsyncNotifier with SpotifyMixin { + Future> fetch(A arg, int 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, + state.limit, + ); + return state.copyWith( + items: [ + ...state.items, + ...items, + ], + offset: state.offset + state.limit, + ) as T; + }, + ); + } +}