mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55: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/auth.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/user.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SettingsMetadataProviderPage extends HookConsumerWidget {
|
class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||||
@ -25,6 +26,8 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
|||||||
final metadataPlugin = ref.watch(metadataPluginProvider);
|
final metadataPlugin = ref.watch(metadataPluginProvider);
|
||||||
final isAuthenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
final isAuthenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
||||||
|
|
||||||
|
final user = ref.watch(metadataPluginUserProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
headers: const [
|
headers: const [
|
||||||
TitleBar(
|
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/hetu_script.dart';
|
||||||
|
import 'package:hetu_script/values.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
|
||||||
class MetadataPluginUserEndpoint {
|
class MetadataPluginUserEndpoint {
|
||||||
final Hetu hetu;
|
final Hetu hetu;
|
||||||
MetadataPluginUserEndpoint(this.hetu);
|
MetadataPluginUserEndpoint(this.hetu);
|
||||||
|
|
||||||
|
HTInstance get hetuMetadataUser =>
|
||||||
|
(hetu.fetch("metadataPlugin") as HTInstance).memberGet("user")
|
||||||
|
as HTInstance;
|
||||||
|
|
||||||
Future<SpotubeUserObject> me() async {
|
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(
|
return SpotubeUserObject.fromJson(
|
||||||
raw.cast<String, dynamic>(),
|
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