mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: add user endpoint calls in metadata and paginated async notifiers
This commit is contained in:
parent
3306f21860
commit
f8211cbcc7
@ -11,6 +11,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||
|
||||
@RoutePage()
|
||||
class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
@ -25,6 +26,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
final metadataPlugin = ref.watch(metadataPluginProvider);
|
||||
final isAuthenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
||||
|
||||
final user = ref.watch(metadataPluginUserProvider);
|
||||
|
||||
return Scaffold(
|
||||
headers: const [
|
||||
TitleBar(
|
||||
|
8
lib/provider/metadata_plugin/library/albums.dart
Normal file
8
lib/provider/metadata_plugin/library/albums.dart
Normal file
@ -0,0 +1,8 @@
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
|
||||
class LibraryAlbumsNotifier extends AsyncNotifier<void> {
|
||||
@override
|
||||
build() {
|
||||
return null;
|
||||
}
|
||||
}
|
0
lib/provider/metadata_plugin/library/artists.dart
Normal file
0
lib/provider/metadata_plugin/library/artists.dart
Normal file
121
lib/provider/metadata_plugin/library/playlists.dart
Normal file
121
lib/provider/metadata_plugin/library/playlists.dart
Normal file
@ -0,0 +1,121 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
import 'package:spotube/services/metadata/endpoints/error.dart';
|
||||
|
||||
class FavoritePlaylistsNotifier
|
||||
extends PaginatedAsyncNotifier<SpotubeSimplePlaylistObject> {
|
||||
FavoritePlaylistsNotifier() : super();
|
||||
|
||||
@override
|
||||
fetch(int offset, int limit) async {
|
||||
final playlists = await (await metadataPlugin)
|
||||
?.user
|
||||
.savedPlaylists(limit: limit, offset: offset);
|
||||
|
||||
return playlists!;
|
||||
}
|
||||
|
||||
@override
|
||||
build() async {
|
||||
ref.watch(metadataPluginProvider);
|
||||
final playlists = await fetch(0, 20);
|
||||
|
||||
return playlists;
|
||||
}
|
||||
|
||||
void updatePlaylist(SpotubeSimplePlaylistObject playlist) {
|
||||
if (state.value == null) return;
|
||||
|
||||
if (state.value!.items.none((e) => e.id == playlist.id)) return;
|
||||
|
||||
state = AsyncData(
|
||||
state.value!.copyWith(
|
||||
items: state.value!.items
|
||||
.map((element) => element.id == playlist.id ? playlist : element)
|
||||
.toList() as List<SpotubeSimplePlaylistObject>,
|
||||
) as SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>,
|
||||
);
|
||||
}
|
||||
|
||||
// Future<void> addFavorite(PlaylistSimple playlist) async {
|
||||
// await update((state) async {
|
||||
// await spotify.invoke(
|
||||
// (api) => api.playlists.followPlaylist(playlist.id!),
|
||||
// );
|
||||
// return state.copyWith(
|
||||
// items: [...state.items, playlist],
|
||||
// );
|
||||
// });
|
||||
|
||||
// ref.invalidate(isFavoritePlaylistProvider(playlist.id!));
|
||||
// }
|
||||
|
||||
// Future<void> removeFavorite(PlaylistSimple playlist) async {
|
||||
// await update((state) async {
|
||||
// await spotify.invoke(
|
||||
// (api) => api.playlists.unfollowPlaylist(playlist.id!),
|
||||
// );
|
||||
// return state.copyWith(
|
||||
// items: state.items.where((e) => e.id != playlist.id).toList(),
|
||||
// );
|
||||
// });
|
||||
|
||||
// ref.invalidate(isFavoritePlaylistProvider(playlist.id!));
|
||||
// }
|
||||
|
||||
// Future<void> addTracks(String playlistId, List<String> trackIds) async {
|
||||
// if (state.value == null) return;
|
||||
|
||||
// final spotify = ref.read(spotifyProvider);
|
||||
|
||||
// await spotify.invoke(
|
||||
// (api) => api.playlists.addTracks(
|
||||
// trackIds.map((id) => 'spotify:track:$id').toList(),
|
||||
// playlistId,
|
||||
// ),
|
||||
// );
|
||||
|
||||
// ref.invalidate(playlistTracksProvider(playlistId));
|
||||
// }
|
||||
|
||||
// Future<void> removeTracks(String playlistId, List<String> trackIds) async {
|
||||
// if (state.value == null) return;
|
||||
|
||||
// final spotify = ref.read(spotifyProvider);
|
||||
|
||||
// await spotify.invoke(
|
||||
// (api) => api.playlists.removeTracks(
|
||||
// trackIds.map((id) => 'spotify:track:$id').toList(),
|
||||
// playlistId,
|
||||
// ),
|
||||
// );
|
||||
|
||||
// ref.invalidate(playlistTracksProvider(playlistId));
|
||||
// }
|
||||
}
|
||||
|
||||
final metadataPluginSavedPlaylistsProvider = AsyncNotifierProvider<
|
||||
FavoritePlaylistsNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>>(
|
||||
() => FavoritePlaylistsNotifier(),
|
||||
);
|
||||
|
||||
final metadataPluginIsSavedPlaylistProvider =
|
||||
FutureProvider.family<bool, String>(
|
||||
(ref, id) async {
|
||||
final plugin = await ref.watch(metadataPluginProvider.future);
|
||||
|
||||
if (plugin == null) {
|
||||
throw MetadataPluginException.noDefaultPlugin(
|
||||
"Failed to get metadata plugin",
|
||||
);
|
||||
}
|
||||
|
||||
final follows = await plugin.user.isSavedPlaylist(id);
|
||||
|
||||
return follows;
|
||||
},
|
||||
);
|
98
lib/provider/metadata_plugin/utils/paginated.dart
Normal file
98
lib/provider/metadata_plugin/utils/paginated.dart
Normal file
@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:riverpod/src/async_notifier.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/metadata/metadata.dart';
|
||||
|
||||
mixin MetadataPluginMixin<K>
|
||||
// ignore: invalid_use_of_internal_member
|
||||
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
|
||||
Future<MetadataPlugin?> get metadataPlugin async =>
|
||||
await ref.read(metadataPluginProvider.future);
|
||||
}
|
||||
|
||||
// ignore: deprecated_member_use
|
||||
extension on AutoDisposeAsyncNotifierProviderRef {
|
||||
// When invoked keeps your provider alive for [duration]
|
||||
// ignore: unused_element
|
||||
void cacheFor([Duration duration = const Duration(minutes: 5)]) {
|
||||
final link = keepAlive();
|
||||
final timer = Timer(duration, () => link.close());
|
||||
onDispose(() => timer.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: deprecated_member_use
|
||||
extension on AutoDisposeRef {
|
||||
// When invoked keeps your provider alive for [duration]
|
||||
// ignore: unused_element
|
||||
void cacheFor([Duration duration = const Duration(minutes: 5)]) {
|
||||
final link = keepAlive();
|
||||
final timer = Timer(duration, () => link.close());
|
||||
onDispose(() => timer.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: subtype_of_sealed_class
|
||||
class AsyncLoadingNext<T> extends AsyncData<T> {
|
||||
const AsyncLoadingNext(super.value);
|
||||
}
|
||||
|
||||
mixin PaginatedAsyncNotifierMixin<K>
|
||||
// ignore: invalid_use_of_internal_member
|
||||
on AsyncNotifierBase<SpotubePaginationResponseObject<K>> {
|
||||
Future<SpotubePaginationResponseObject<K>> fetch(int offset, int limit);
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.value == null || !state.value!.hasMore) return;
|
||||
|
||||
state = AsyncLoadingNext(state.asData!.value);
|
||||
|
||||
state = await AsyncValue.guard(
|
||||
() async {
|
||||
final newState = await fetch(
|
||||
state.value!.nextOffset!,
|
||||
state.value!.limit,
|
||||
);
|
||||
return newState.copyWith(items: [
|
||||
...state.value!.items as List<K>,
|
||||
...newState.items as List<K>,
|
||||
]) as SpotubePaginationResponseObject<K>;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<K>> fetchAll() async {
|
||||
if (state.value == null) return [];
|
||||
if (!state.value!.hasMore) return state.value!.items as List<K>;
|
||||
|
||||
bool hasMore = true;
|
||||
while (hasMore) {
|
||||
await update((state) async {
|
||||
final newState = await fetch(
|
||||
state.nextOffset!,
|
||||
state.limit,
|
||||
);
|
||||
|
||||
hasMore = newState.hasMore;
|
||||
return newState.copyWith(items: [
|
||||
...state.items as List<K>,
|
||||
...newState.items as List<K>,
|
||||
]) as SpotubePaginationResponseObject<K>;
|
||||
});
|
||||
}
|
||||
|
||||
return state.value!.items as List<K>;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PaginatedAsyncNotifier<K>
|
||||
extends AsyncNotifier<SpotubePaginationResponseObject<K>>
|
||||
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
|
||||
|
||||
abstract class AutoDisposePaginatedAsyncNotifier<K>
|
||||
extends AutoDisposeAsyncNotifier<SpotubePaginationResponseObject<K>>
|
||||
with PaginatedAsyncNotifierMixin<K>, MetadataPluginMixin<K> {}
|
12
lib/services/metadata/endpoints/error.dart
Normal file
12
lib/services/metadata/endpoints/error.dart
Normal file
@ -0,0 +1,12 @@
|
||||
class MetadataPluginException implements Exception {
|
||||
final String exceptionType;
|
||||
final String message;
|
||||
|
||||
MetadataPluginException.noDefaultPlugin(this.message)
|
||||
: exceptionType = "NoDefault";
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "${exceptionType}MetadataPluginException: $message";
|
||||
}
|
||||
}
|
@ -1,15 +1,127 @@
|
||||
import 'package:hetu_script/hetu_script.dart';
|
||||
import 'package:hetu_script/values.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
|
||||
class MetadataPluginUserEndpoint {
|
||||
final Hetu hetu;
|
||||
MetadataPluginUserEndpoint(this.hetu);
|
||||
|
||||
HTInstance get hetuMetadataUser =>
|
||||
(hetu.fetch("metadataPlugin") as HTInstance).memberGet("user")
|
||||
as HTInstance;
|
||||
|
||||
Future<SpotubeUserObject> me() async {
|
||||
final raw = await hetu.eval("metadataPlugin.user.me()") as Map;
|
||||
final raw = await hetuMetadataUser.invoke("me") as Map;
|
||||
|
||||
return SpotubeUserObject.fromJson(
|
||||
raw.cast<String, dynamic>(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SpotubePaginationResponseObject<SpotubeFullTrackObject>> savedTracks({
|
||||
int? offset,
|
||||
int? limit,
|
||||
}) async {
|
||||
final raw = await hetuMetadataUser.invoke(
|
||||
"savedTracks",
|
||||
namedArgs: {
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
}..removeWhere((key, value) => value == null),
|
||||
) as Map;
|
||||
|
||||
return SpotubePaginationResponseObject.fromJson(
|
||||
raw.cast<String, dynamic>(),
|
||||
(Map json) =>
|
||||
SpotubeFullTrackObject.fromJson(json.cast<String, dynamic>()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SpotubePaginationResponseObject<SpotubeSimplePlaylistObject>>
|
||||
savedPlaylists({
|
||||
int? offset,
|
||||
int? limit,
|
||||
}) async {
|
||||
final raw = await hetuMetadataUser.invoke(
|
||||
"savedPlaylists",
|
||||
namedArgs: {
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
}..removeWhere((key, value) => value == null),
|
||||
) as Map;
|
||||
|
||||
return SpotubePaginationResponseObject.fromJson(
|
||||
raw.cast<String, dynamic>(),
|
||||
(Map json) =>
|
||||
SpotubeSimplePlaylistObject.fromJson(json.cast<String, dynamic>()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SpotubePaginationResponseObject<SpotubeSimpleAlbumObject>>
|
||||
savedAlbums({
|
||||
int? offset,
|
||||
int? limit,
|
||||
}) async {
|
||||
final raw = await hetuMetadataUser.invoke(
|
||||
"savedAlbums",
|
||||
namedArgs: {
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
}..removeWhere((key, value) => value == null),
|
||||
) as Map;
|
||||
|
||||
return SpotubePaginationResponseObject.fromJson(
|
||||
raw.cast<String, dynamic>(),
|
||||
(Map json) =>
|
||||
SpotubeSimpleAlbumObject.fromJson(json.cast<String, dynamic>()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SpotubePaginationResponseObject<SpotubeFullArtistObject>>
|
||||
savedArtists({
|
||||
int? offset,
|
||||
int? limit,
|
||||
}) async {
|
||||
final raw = await hetuMetadataUser.invoke(
|
||||
"savedArtists",
|
||||
namedArgs: {
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
}..removeWhere((key, value) => value == null),
|
||||
) as Map;
|
||||
|
||||
return SpotubePaginationResponseObject.fromJson(
|
||||
raw.cast<String, dynamic>(),
|
||||
(Map json) =>
|
||||
SpotubeFullArtistObject.fromJson(json.cast<String, dynamic>()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> isSavedPlaylist(String playlistId) async {
|
||||
return await hetuMetadataUser.invoke(
|
||||
"isSavedPlaylist",
|
||||
positionalArgs: [playlistId],
|
||||
) as bool;
|
||||
}
|
||||
|
||||
Future<List<bool>> isSavedTracks(List<String> ids) async {
|
||||
return await hetuMetadataUser.invoke(
|
||||
"isSavedTracks",
|
||||
positionalArgs: [ids],
|
||||
) as List<bool>;
|
||||
}
|
||||
|
||||
Future<List<bool>> isSavedAlbums(List<String> ids) async {
|
||||
return await hetuMetadataUser.invoke(
|
||||
"isSavedAlbums",
|
||||
positionalArgs: [ids],
|
||||
) as List<bool>;
|
||||
}
|
||||
|
||||
Future<List<bool>> isSavedArtists(List<String> ids) async {
|
||||
return await hetuMetadataUser.invoke(
|
||||
"isSavedArtists",
|
||||
positionalArgs: [ids],
|
||||
) as List<bool>;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user