mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
feat: use providers in remaining pages and remove fl_query
This commit is contained in:
parent
d2a9ff6652
commit
51710d5aee
@ -1,11 +1,10 @@
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
|
||||
class ArtistAlbumList extends HookConsumerWidget {
|
||||
final String artistId;
|
||||
@ -18,21 +17,19 @@ class ArtistAlbumList extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
|
||||
final albumsQuery = ref.watch(artistAlbumsProvider(artistId));
|
||||
final albumsQueryNotifier =
|
||||
ref.watch(artistAlbumsProvider(artistId).notifier);
|
||||
|
||||
final albums = useMemoized(() {
|
||||
return albumsQuery.pages
|
||||
.expand<Album>((page) => page.items ?? const Iterable.empty())
|
||||
.toList();
|
||||
}, [albumsQuery.pages]);
|
||||
final albums = albumsQuery.asData?.value.items ?? [];
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return HorizontalPlaybuttonCardView<Album>(
|
||||
isLoadingNextPage: albumsQuery.isLoadingNextPage,
|
||||
hasNextPage: albumsQuery.hasNextPage,
|
||||
hasNextPage: albumsQuery.asData?.value.hasMore ?? false,
|
||||
items: albums,
|
||||
onFetchMore: albumsQuery.fetchNext,
|
||||
onFetchMore: albumsQueryNotifier.fetchMore,
|
||||
title: Text(
|
||||
context.l10n.albums,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/models/spotify_friends.dart';
|
||||
@ -24,7 +22,6 @@ class FriendItem extends HookConsumerWidget {
|
||||
colorScheme: colorScheme,
|
||||
) = Theme.of(context);
|
||||
|
||||
final queryClient = useQueryClient();
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
|
||||
return Container(
|
||||
@ -86,15 +83,11 @@ class FriendItem extends HookConsumerWidget {
|
||||
..onTap = () async {
|
||||
context.push(
|
||||
"/${friend.track.context.path}",
|
||||
extra: !friend.track.context.path
|
||||
.startsWith("album")
|
||||
? null
|
||||
: await queryClient.fetchQuery<Album, dynamic>(
|
||||
"album/${friend.track.album.id}",
|
||||
() => spotify.albums.get(
|
||||
friend.track.album.id,
|
||||
),
|
||||
),
|
||||
extra:
|
||||
!friend.track.context.path.startsWith("album")
|
||||
? null
|
||||
: await spotify.albums
|
||||
.get(friend.track.context.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -110,12 +103,7 @@ class FriendItem extends HookConsumerWidget {
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
final album =
|
||||
await queryClient.fetchQuery<Album, dynamic>(
|
||||
"album/${friend.track.album.id}",
|
||||
() => spotify.albums.get(
|
||||
friend.track.album.id,
|
||||
),
|
||||
);
|
||||
await spotify.albums.get(friend.track.album.id);
|
||||
if (context.mounted) {
|
||||
context.push(
|
||||
"/album/${friend.track.album.id}",
|
||||
|
||||
@ -16,7 +16,6 @@ import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/services/mutations/playlist.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
@ -90,7 +89,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
Future<void> onCreate() async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
|
||||
final PlaylistCRUDVariables payload = (
|
||||
final PlaylistInput payload = (
|
||||
playlistName: playlistName.text,
|
||||
collaborative: collaborative.value,
|
||||
public: public.value,
|
||||
|
||||
@ -15,10 +15,10 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
@ -241,8 +241,8 @@ class SidebarFooter extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final theme = Theme.of(context);
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final me = useQueries.user.me(ref);
|
||||
final data = me.data;
|
||||
final me = ref.watch(meProvider);
|
||||
final data = me.asData?.value;
|
||||
|
||||
final avatarImg = TypeConversionUtils.image_X_UrlString(
|
||||
data?.images,
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@ -25,8 +24,6 @@ import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/services/mutations/mutations.dart';
|
||||
import 'package:spotube/services/queries/search.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@ -100,21 +97,10 @@ class TrackOptions extends HookConsumerWidget {
|
||||
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||
final spotify = ref.read(spotifyProvider);
|
||||
final query = "${track.name} Radio";
|
||||
final pages = await QueryClient.of(context)
|
||||
.fetchInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
|
||||
job: SearchQueries.queryJob(query),
|
||||
args: (
|
||||
spotify: spotify,
|
||||
searchType: SearchType.playlist,
|
||||
query: query,
|
||||
),
|
||||
) ??
|
||||
[];
|
||||
final pages =
|
||||
await spotify.search.get(query, types: [SearchType.playlist]).first();
|
||||
|
||||
final radios = pages
|
||||
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
|
||||
.toList()
|
||||
.cast<PlaylistSimple>();
|
||||
final radios = pages.map((e) => e.items).toList().cast<PlaylistSimple>();
|
||||
|
||||
final artists = track.artists!.map((e) => e.name);
|
||||
|
||||
@ -192,10 +178,8 @@ class TrackOptions extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final removingTrack = useState<String?>(null);
|
||||
final removeTrack = useMutations.playlist.removeTrackOf(
|
||||
ref,
|
||||
playlistId ?? "",
|
||||
);
|
||||
final favoritePlaylistsNotifier =
|
||||
ref.watch(favoritePlaylistsProvider.notifier);
|
||||
|
||||
final isInQueue = useMemoized(() {
|
||||
if (playlist.activeTrack == null) return false;
|
||||
@ -266,7 +250,8 @@ class TrackOptions extends HookConsumerWidget {
|
||||
break;
|
||||
case TrackOptionValue.removeFromPlaylist:
|
||||
removingTrack.value = track.uri;
|
||||
removeTrack.mutate(track.uri!);
|
||||
favoritePlaylistsNotifier
|
||||
.removeTracks(playlistId ?? "", [track.id!]);
|
||||
break;
|
||||
case TrackOptionValue.blacklist:
|
||||
if (isBlackListed) {
|
||||
@ -330,7 +315,7 @@ class TrackOptions extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
children: switch (track.runtimeType) {
|
||||
LocalTrack => [
|
||||
LocalTrack() => [
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.delete,
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
@ -393,10 +378,7 @@ class TrackOptions extends HookConsumerWidget {
|
||||
if (userPlaylist && auth != null)
|
||||
PopSheetEntry(
|
||||
value: TrackOptionValue.removeFromPlaylist,
|
||||
leading: (removeTrack.isMutating || !removeTrack.hasData) &&
|
||||
removingTrack.value == track.uri
|
||||
? const CircularProgressIndicator()
|
||||
: const Icon(SpotubeIcons.removeFilled),
|
||||
leading: const Icon(SpotubeIcons.removeFilled),
|
||||
title: Text(context.l10n.remove_from_playlist),
|
||||
),
|
||||
PopSheetEntry(
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
@ -19,19 +18,6 @@ class PaginationProps {
|
||||
required this.onRefresh,
|
||||
});
|
||||
|
||||
factory PaginationProps.fromQuery(
|
||||
InfiniteQuery<List<Track>, dynamic, int> query, {
|
||||
required Future<List<Track>> Function() onFetchAll,
|
||||
}) {
|
||||
return PaginationProps(
|
||||
hasNextPage: query.hasNextPage,
|
||||
isLoading: query.isLoadingNextPage,
|
||||
onFetchMore: query.fetchNext,
|
||||
onFetchAll: onFetchAll,
|
||||
onRefresh: query.refreshAll,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return other is PaginationProps &&
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
extension FetchAllTracks on InfiniteQuery<List<Track>, dynamic, int> {
|
||||
Future<List<Track>> fetchAllTracks({
|
||||
required Future<List<Track>> Function() getAllTracks,
|
||||
}) async {
|
||||
if (pages.isNotEmpty && !hasNextPage) {
|
||||
return pages.expand((page) => page).toList();
|
||||
}
|
||||
final tracks = await getAllTracks();
|
||||
|
||||
final numOfPages = (tracks.length / 20).round();
|
||||
|
||||
final Map<int, List<Track>> pagedTracks = {};
|
||||
|
||||
for (var i = 0; i < numOfPages; i++) {
|
||||
if (i == numOfPages - 1) {
|
||||
final pageTracks = tracks.sublist(i * 20);
|
||||
pagedTracks[i] = pageTracks;
|
||||
break;
|
||||
}
|
||||
|
||||
final pageTracks = tracks.sublist(i * 20, (i + 1) * 20);
|
||||
pagedTracks[i] = pageTracks;
|
||||
}
|
||||
|
||||
for (final group in pagedTracks.entries) {
|
||||
setPageData(group.key, group.value);
|
||||
}
|
||||
|
||||
return tracks.toList();
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||
@ -17,8 +15,6 @@ final linkStream = appLinks.allStringLinkStream.asBroadcastStream();
|
||||
void useDeepLinking(WidgetRef ref) {
|
||||
// single instance no worries
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final queryClient = useQueryClient();
|
||||
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
useEffect(() {
|
||||
@ -32,10 +28,7 @@ void useDeepLinking(WidgetRef ref) {
|
||||
case "album":
|
||||
router.push(
|
||||
"/album/${url.pathSegments.last}",
|
||||
extra: await queryClient.fetchQuery<Album, dynamic>(
|
||||
"album/${url.pathSegments.last}",
|
||||
() => spotify.albums.get(url.pathSegments.last),
|
||||
),
|
||||
extra: await spotify.albums.get(url.pathSegments.last),
|
||||
);
|
||||
break;
|
||||
case "artist":
|
||||
@ -44,10 +37,7 @@ void useDeepLinking(WidgetRef ref) {
|
||||
case "playlist":
|
||||
router.push(
|
||||
"/playlist/${url.pathSegments.last}",
|
||||
extra: await queryClient.fetchQuery<Playlist, dynamic>(
|
||||
"playlist/${url.pathSegments.last}",
|
||||
() => spotify.playlists.get(url.pathSegments.last),
|
||||
),
|
||||
extra: await spotify.playlists.get(url.pathSegments.last),
|
||||
);
|
||||
break;
|
||||
case "track":
|
||||
@ -78,10 +68,7 @@ void useDeepLinking(WidgetRef ref) {
|
||||
case "spotify:album":
|
||||
await router.push(
|
||||
"/album/$endSegment",
|
||||
extra: await queryClient.fetchQuery<Album, dynamic>(
|
||||
"album/$endSegment",
|
||||
() => spotify.albums.get(endSegment),
|
||||
),
|
||||
extra: await spotify.albums.get(endSegment),
|
||||
);
|
||||
break;
|
||||
case "spotify:artist":
|
||||
@ -93,10 +80,7 @@ void useDeepLinking(WidgetRef ref) {
|
||||
case "spotify:playlist":
|
||||
await router.push(
|
||||
"/playlist/$endSegment",
|
||||
extra: await queryClient.fetchQuery<Playlist, dynamic>(
|
||||
"playlist/$endSegment",
|
||||
() => spotify.playlists.get(endSegment),
|
||||
),
|
||||
extra: await spotify.playlists.get(endSegment),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@ -108,5 +92,5 @@ void useDeepLinking(WidgetRef ref) {
|
||||
mediaStream?.cancel();
|
||||
subscription.cancel();
|
||||
};
|
||||
}, [spotify, queryClient]);
|
||||
}, [spotify]);
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -8,7 +7,6 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/queries/search.dart';
|
||||
|
||||
void useEndlessPlayback(WidgetRef ref) {
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
@ -18,7 +16,6 @@ void useEndlessPlayback(WidgetRef ref) {
|
||||
final endlessPlayback =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
|
||||
|
||||
final queryClient = useQueryClient();
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
@ -32,16 +29,8 @@ void useEndlessPlayback(WidgetRef ref) {
|
||||
final track = playlist.tracks.last;
|
||||
|
||||
final query = "${track.name} Radio";
|
||||
final pages = await queryClient.fetchInfiniteQueryJob<List<Page>,
|
||||
dynamic, int, SearchParams>(
|
||||
job: SearchQueries.queryJob(query),
|
||||
args: (
|
||||
spotify: spotify,
|
||||
searchType: SearchType.playlist,
|
||||
query: query
|
||||
),
|
||||
) ??
|
||||
[];
|
||||
final pages = await spotify.search
|
||||
.get(query, types: [SearchType.playlist]).first();
|
||||
|
||||
final radios = pages
|
||||
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
|
||||
@ -94,7 +83,6 @@ void useEndlessPlayback(WidgetRef ref) {
|
||||
[
|
||||
spotify,
|
||||
playback,
|
||||
queryClient,
|
||||
playlist.tracks,
|
||||
endlessPlayback,
|
||||
auth,
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
|
||||
InfiniteQuery<DataType, ErrorType, PageType>
|
||||
useSpotifyInfiniteQuery<DataType, ErrorType, PageType>(
|
||||
String queryKey,
|
||||
FutureOr<DataType?> Function(PageType page, SpotifyApi spotify) queryFn, {
|
||||
required WidgetRef ref,
|
||||
required InfiniteQueryNextPage<DataType, PageType> nextPage,
|
||||
required PageType initialPage,
|
||||
RetryConfig? retryConfig,
|
||||
RefreshConfig? refreshConfig,
|
||||
JsonConfig<DataType>? jsonConfig,
|
||||
ValueChanged<PageEvent<DataType, PageType>>? onData,
|
||||
ValueChanged<PageEvent<ErrorType, PageType>>? onError,
|
||||
bool enabled = true,
|
||||
List<Object?>? keys,
|
||||
}) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final query = useInfiniteQuery<DataType, ErrorType, PageType>(
|
||||
queryKey,
|
||||
(page) => queryFn(page, spotify),
|
||||
nextPage: nextPage,
|
||||
initialPage: initialPage,
|
||||
retryConfig: retryConfig,
|
||||
refreshConfig: refreshConfig,
|
||||
jsonConfig: jsonConfig,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
enabled: enabled,
|
||||
keys: keys,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
return ref.listenManual(
|
||||
spotifyProvider,
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
query.refreshAll();
|
||||
}
|
||||
},
|
||||
).close;
|
||||
}, [query]);
|
||||
|
||||
return query;
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
|
||||
Mutation<DataType, ErrorType, VariablesType>
|
||||
useSpotifyMutation<DataType, ErrorType, VariablesType, RecoveryType>(
|
||||
String mutationKey,
|
||||
Future<DataType> Function(VariablesType variables, SpotifyApi spotify)
|
||||
mutationFn, {
|
||||
required WidgetRef ref,
|
||||
RetryConfig? retryConfig,
|
||||
MutationOnDataFn<DataType, RecoveryType>? onData,
|
||||
MutationOnErrorFn<ErrorType, RecoveryType>? onError,
|
||||
MutationOnMutationFn<VariablesType, RecoveryType>? onMutate,
|
||||
List<String>? refreshQueries,
|
||||
List<String>? refreshInfiniteQueries,
|
||||
List<Object?>? keys,
|
||||
}) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final mutation =
|
||||
useMutation<DataType, ErrorType, VariablesType, RecoveryType>(
|
||||
mutationKey,
|
||||
(variables) => mutationFn(variables, spotify),
|
||||
retryConfig: retryConfig,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
onMutate: onMutate,
|
||||
refreshQueries: refreshQueries,
|
||||
refreshInfiniteQueries: refreshInfiniteQueries,
|
||||
keys: keys,
|
||||
);
|
||||
|
||||
return mutation;
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
|
||||
typedef SpotifyQueryFn<DataType> = FutureOr<DataType?> Function(
|
||||
SpotifyApi spotify);
|
||||
|
||||
Query<DataType, ErrorType> useSpotifyQuery<DataType, ErrorType>(
|
||||
final String queryKey,
|
||||
final SpotifyQueryFn<DataType> queryFn, {
|
||||
required WidgetRef ref,
|
||||
final DataType? initial,
|
||||
final RetryConfig? retryConfig,
|
||||
final RefreshConfig? refreshConfig,
|
||||
final JsonConfig<DataType>? jsonConfig,
|
||||
final ValueChanged<DataType>? onData,
|
||||
final ValueChanged<ErrorType>? onError,
|
||||
final bool enabled = true,
|
||||
}) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
|
||||
final query = useQuery<DataType, ErrorType>(
|
||||
queryKey,
|
||||
() => queryFn(spotify),
|
||||
initial: initial,
|
||||
retryConfig: retryConfig,
|
||||
refreshConfig: refreshConfig,
|
||||
jsonConfig: jsonConfig,
|
||||
onData: onData,
|
||||
onError: onError,
|
||||
enabled: enabled,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
return ref.listenManual(
|
||||
spotifyProvider,
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
query.refresh();
|
||||
}
|
||||
},
|
||||
).close;
|
||||
}, [query]);
|
||||
|
||||
return query;
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:dart_discord_rpc/dart_discord_rpc.dart';
|
||||
import 'package:device_preview/device_preview.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -28,7 +27,6 @@ import 'package:spotube/provider/palette_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/cli/cli.dart';
|
||||
import 'package:spotube/services/connectivity_adapter.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:spotube/themes/theme.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
@ -74,11 +72,7 @@ Future<void> main(List<String> rawArgs) async {
|
||||
final hiveCacheDir =
|
||||
kIsWeb ? null : (await getApplicationSupportDirectory()).path;
|
||||
|
||||
await QueryClient.initialize(
|
||||
cachePrefix: "oss.krtirtho.spotube",
|
||||
cacheDir: hiveCacheDir,
|
||||
connectivity: FlQueryInternetConnectionCheckerAdapter(),
|
||||
);
|
||||
Hive.init(hiveCacheDir);
|
||||
|
||||
Hive.registerAdapter(SkipSegmentAdapter());
|
||||
|
||||
@ -144,10 +138,7 @@ Future<void> main(List<String> rawArgs) async {
|
||||
orientation: Orientation.portrait,
|
||||
),
|
||||
builder: (context) {
|
||||
return QueryClientProvider(
|
||||
staleDuration: const Duration(minutes: 30),
|
||||
child: const Spotube(),
|
||||
);
|
||||
return const Spotube();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
@ -18,6 +17,7 @@ import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
||||
import 'package:spotube/hooks/configurators/use_update_checker.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/services/connectivity_adapter.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
|
||||
const rootPaths = {
|
||||
@ -53,8 +53,9 @@ class RootApp extends HookConsumerWidget {
|
||||
}
|
||||
});
|
||||
|
||||
final subscription =
|
||||
QueryClient.connectivity.onConnectivityChanged.listen((status) {
|
||||
final subscription = ConnectionCheckerService
|
||||
.instance.onConnectivityChanged
|
||||
.listen((status) {
|
||||
if (status) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
|
||||
@ -13,8 +13,8 @@ import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/shared/track_tile/track_options.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
|
||||
@ -35,9 +35,9 @@ class TrackPage extends HookConsumerWidget {
|
||||
|
||||
final isActive = playlist.activeTrack?.id == trackId;
|
||||
|
||||
final trackQuery = useQueries.tracks.track(ref, trackId);
|
||||
final trackQuery = ref.watch(trackProvider(trackId));
|
||||
|
||||
final track = trackQuery.data ?? FakeData.track;
|
||||
final track = trackQuery.asData?.value ?? FakeData.track;
|
||||
|
||||
void onPlay() async {
|
||||
if (isActive) {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
@ -52,8 +51,7 @@ class AuthenticationCredentials {
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (rootNavigatorKey?.currentContext != null &&
|
||||
await QueryClient.connectivity.isConnected) {
|
||||
if (rootNavigatorKey?.currentContext != null) {
|
||||
showPromptDialog(
|
||||
context: rootNavigatorKey!.currentContext!,
|
||||
title: rootNavigatorKey!.currentContext!.l10n
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
part of '../spotify.dart';
|
||||
|
||||
class ArtistAlbumsState extends PaginatedState<AlbumSimple> {
|
||||
class ArtistAlbumsState extends PaginatedState<Album> {
|
||||
ArtistAlbumsState({
|
||||
required super.items,
|
||||
required super.offset,
|
||||
@ -10,7 +10,7 @@ class ArtistAlbumsState extends PaginatedState<AlbumSimple> {
|
||||
|
||||
@override
|
||||
ArtistAlbumsState copyWith({
|
||||
List<AlbumSimple>? items,
|
||||
List<Album>? items,
|
||||
int? offset,
|
||||
int? limit,
|
||||
bool? hasMore,
|
||||
@ -24,8 +24,8 @@ class ArtistAlbumsState extends PaginatedState<AlbumSimple> {
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistAlbumsNotifier extends FamilyPaginatedAsyncNotifier<AlbumSimple,
|
||||
ArtistAlbumsState, String> {
|
||||
class ArtistAlbumsNotifier
|
||||
extends FamilyPaginatedAsyncNotifier<Album, ArtistAlbumsState, String> {
|
||||
ArtistAlbumsNotifier() : super();
|
||||
|
||||
@override
|
||||
|
||||
@ -85,6 +85,19 @@ class FavoritePlaylistsNotifier
|
||||
|
||||
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.playlists.removeTracks(
|
||||
trackIds.map((id) => 'spotify:track:$id').toList(),
|
||||
playlistId,
|
||||
);
|
||||
|
||||
ref.invalidate(playlistTracksProvider(playlistId));
|
||||
}
|
||||
}
|
||||
|
||||
final favoritePlaylistsProvider =
|
||||
|
||||
@ -2,17 +2,17 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class FlQueryInternetConnectionCheckerAdapter extends ConnectivityAdapter
|
||||
with WidgetsBindingObserver {
|
||||
class ConnectionCheckerService with WidgetsBindingObserver {
|
||||
final _connectionStreamController = StreamController<bool>.broadcast();
|
||||
final Dio dio;
|
||||
|
||||
FlQueryInternetConnectionCheckerAdapter()
|
||||
: dio = Dio(),
|
||||
super() {
|
||||
static final _instance = ConnectionCheckerService._();
|
||||
|
||||
static ConnectionCheckerService get instance => _instance;
|
||||
|
||||
ConnectionCheckerService._() : dio = Dio() {
|
||||
Timer? timer;
|
||||
|
||||
onConnectivityChanged.listen((connected) {
|
||||
@ -100,15 +100,16 @@ class FlQueryInternetConnectionCheckerAdapter extends ConnectivityAdapter
|
||||
await isVpnActive(); // when VPN is active that means we are connected
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnectedSync = false;
|
||||
|
||||
Future<bool> get isConnected async {
|
||||
final connected = await _isConnected();
|
||||
isConnectedSync = connected;
|
||||
if (connected != isConnectedSync /*previous value*/) {
|
||||
_connectionStreamController.add(connected);
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<bool> get onConnectivityChanged => _connectionStreamController.stream;
|
||||
}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_mutation.dart';
|
||||
|
||||
class AlbumMutations {
|
||||
const AlbumMutations();
|
||||
|
||||
Mutation<bool, dynamic, bool> toggleFavorite(
|
||||
WidgetRef ref,
|
||||
String albumId, {
|
||||
List<String>? refreshQueries,
|
||||
List<String>? refreshInfiniteQueries,
|
||||
MutationOnDataFn<bool, dynamic>? onData,
|
||||
}) {
|
||||
return useSpotifyMutation<bool, dynamic, bool, dynamic>(
|
||||
"toggle-album-like/$albumId",
|
||||
(isLiked, spotify) async {
|
||||
if (isLiked) {
|
||||
await spotify.me.removeAlbums([albumId]);
|
||||
} else {
|
||||
await spotify.me.saveAlbums([albumId]);
|
||||
}
|
||||
return !isLiked;
|
||||
},
|
||||
ref: ref,
|
||||
refreshQueries: refreshQueries,
|
||||
refreshInfiniteQueries: refreshInfiniteQueries,
|
||||
onData: onData,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import 'package:spotube/services/mutations/album.dart';
|
||||
import 'package:spotube/services/mutations/playlist.dart';
|
||||
import 'package:spotube/services/mutations/track.dart';
|
||||
|
||||
class _UseMutations {
|
||||
const _UseMutations._();
|
||||
final playlist = const PlaylistMutations();
|
||||
final album = const AlbumMutations();
|
||||
final track = const TrackMutations();
|
||||
}
|
||||
|
||||
const useMutations = _UseMutations._();
|
||||
@ -1,147 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_mutation.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
typedef PlaylistCRUDVariables = ({
|
||||
String playlistName,
|
||||
bool? public,
|
||||
bool? collaborative,
|
||||
String? description,
|
||||
String? base64Image,
|
||||
});
|
||||
|
||||
class PlaylistMutations {
|
||||
const PlaylistMutations();
|
||||
|
||||
Mutation<bool, dynamic, bool> toggleFavorite(
|
||||
WidgetRef ref,
|
||||
String playlistId, {
|
||||
List<String>? refreshQueries,
|
||||
List<String>? refreshInfiniteQueries,
|
||||
ValueChanged<bool>? onData,
|
||||
}) {
|
||||
return useSpotifyMutation<bool, dynamic, bool, dynamic>(
|
||||
"toggle-playlist-like/$playlistId",
|
||||
(isLiked, spotify) async {
|
||||
if (isLiked) {
|
||||
await spotify.playlists.unfollowPlaylist(playlistId);
|
||||
} else {
|
||||
await spotify.playlists.followPlaylist(playlistId);
|
||||
}
|
||||
return !isLiked;
|
||||
},
|
||||
ref: ref,
|
||||
refreshQueries: refreshQueries,
|
||||
refreshInfiniteQueries: [
|
||||
...?refreshInfiniteQueries,
|
||||
"current-user-playlists",
|
||||
],
|
||||
onData: (data, recoveryData) {
|
||||
onData?.call(data);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Mutation<bool, dynamic, String> removeTrackOf(
|
||||
WidgetRef ref,
|
||||
String playlistId,
|
||||
) {
|
||||
return useSpotifyMutation<bool, dynamic, String, dynamic>(
|
||||
"remove-track-from-playlist/$playlistId",
|
||||
(trackId, spotify) async {
|
||||
await spotify.playlists.removeTracks([trackId], playlistId);
|
||||
return true;
|
||||
},
|
||||
ref: ref,
|
||||
refreshQueries: ["playlist-tracks/$playlistId"],
|
||||
);
|
||||
}
|
||||
|
||||
Mutation<Playlist, dynamic, PlaylistCRUDVariables> create(
|
||||
WidgetRef ref, {
|
||||
List<String>? trackIds,
|
||||
ValueChanged<dynamic>? onError,
|
||||
ValueChanged<Playlist>? onData,
|
||||
}) {
|
||||
final me = useQueries.user.me(ref);
|
||||
return useSpotifyMutation<Playlist, dynamic, PlaylistCRUDVariables, void>(
|
||||
"create-playlist",
|
||||
(variable, spotify) async {
|
||||
final playlist = await spotify.playlists.createPlaylist(
|
||||
me.data!.id!,
|
||||
variable.playlistName,
|
||||
collaborative: variable.collaborative,
|
||||
description: variable.description,
|
||||
public: variable.public,
|
||||
);
|
||||
|
||||
if (variable.base64Image != null) {
|
||||
await spotify.playlists.updatePlaylistImage(
|
||||
playlist.id!,
|
||||
variable.base64Image!,
|
||||
);
|
||||
}
|
||||
|
||||
if (trackIds != null && trackIds.isNotEmpty) {
|
||||
await spotify.playlists.addTracks(
|
||||
trackIds.map((id) => "spotify:track:$id").toList(),
|
||||
playlist.id!,
|
||||
);
|
||||
}
|
||||
|
||||
return playlist;
|
||||
},
|
||||
refreshInfiniteQueries: ["current-user-playlists"],
|
||||
refreshQueries: ["current-user-all-playlists"],
|
||||
ref: ref,
|
||||
onError: (error, recoveryData) {
|
||||
onError?.call(error);
|
||||
},
|
||||
onData: (data, recoveryData) {
|
||||
onData?.call(data);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Mutation<void, dynamic, PlaylistCRUDVariables> update(
|
||||
WidgetRef ref, {
|
||||
String? playlistId,
|
||||
ValueChanged<dynamic>? onError,
|
||||
ValueChanged<void>? onData,
|
||||
}) {
|
||||
return useSpotifyMutation<void, dynamic, PlaylistCRUDVariables, void>(
|
||||
"update-playlist/$playlistId",
|
||||
(variable, spotify) async {
|
||||
if (playlistId == null) return;
|
||||
await spotify.playlists.updatePlaylist(
|
||||
playlistId,
|
||||
variable.playlistName,
|
||||
collaborative: variable.collaborative,
|
||||
description: variable.description,
|
||||
public: variable.public,
|
||||
);
|
||||
if (variable.base64Image != null) {
|
||||
await spotify.playlists.updatePlaylistImage(
|
||||
playlistId,
|
||||
variable.base64Image!,
|
||||
);
|
||||
}
|
||||
},
|
||||
refreshInfiniteQueries: [
|
||||
"playlist/$playlistId",
|
||||
"current-user-playlists",
|
||||
],
|
||||
refreshQueries: ["current-user-all-playlists"],
|
||||
ref: ref,
|
||||
onError: (error, recoveryData) {
|
||||
onError?.call(error);
|
||||
},
|
||||
onData: (data, recoveryData) {
|
||||
onData?.call(data);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_mutation.dart';
|
||||
|
||||
class TrackMutations {
|
||||
const TrackMutations();
|
||||
|
||||
Mutation<bool, dynamic, bool> toggleFavorite(
|
||||
WidgetRef ref,
|
||||
String trackId, {
|
||||
MutationOnMutationFn<bool, bool>? onMutate,
|
||||
MutationOnDataFn<bool, bool>? onData,
|
||||
MutationOnErrorFn<dynamic, bool>? onError,
|
||||
}) {
|
||||
return useSpotifyMutation<bool, dynamic, bool, bool>(
|
||||
'toggle-track-like/$trackId',
|
||||
(isLiked, spotify) async {
|
||||
if (isLiked) {
|
||||
await spotify.tracks.me.removeOne(trackId);
|
||||
} else {
|
||||
await spotify.tracks.me.saveOne(trackId);
|
||||
}
|
||||
return !isLiked;
|
||||
},
|
||||
ref: ref,
|
||||
onData: onData,
|
||||
onMutate: onMutate,
|
||||
refreshQueries: ["playlist-tracks/user-liked-tracks"],
|
||||
onError: onError,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,114 +0,0 @@
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class AlbumQueries {
|
||||
const AlbumQueries();
|
||||
|
||||
InfiniteQuery<Page<AlbumSimple>, dynamic, int> ofMine(WidgetRef ref) {
|
||||
return useSpotifyInfiniteQuery<Page<AlbumSimple>, dynamic, int>(
|
||||
"current-user-albums",
|
||||
(page, spotify) {
|
||||
return spotify.me.savedAlbums().getPage(
|
||||
20,
|
||||
page * 20,
|
||||
);
|
||||
},
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) =>
|
||||
(lastPageData.items?.length ?? 0) < 20 || lastPageData.isLast
|
||||
? null
|
||||
: lastPage + 1,
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
static final tracksOfJob = InfiniteQueryJob.withVariableKey<
|
||||
List<Track>,
|
||||
dynamic,
|
||||
int,
|
||||
({
|
||||
SpotifyApi spotify,
|
||||
AlbumSimple album,
|
||||
})>(
|
||||
baseQueryKey: "album-tracks",
|
||||
initialPage: 0,
|
||||
task: (albumId, page, args) async {
|
||||
final res =
|
||||
await args!.spotify.albums.tracks(albumId).getPage(20, page * 20);
|
||||
return res.items
|
||||
?.map((track) =>
|
||||
TypeConversionUtils.simpleTrack_X_Track(track, args.album))
|
||||
.toList() ??
|
||||
<Track>[];
|
||||
},
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.length < 20) {
|
||||
return null;
|
||||
}
|
||||
return lastPage + 1;
|
||||
},
|
||||
);
|
||||
|
||||
InfiniteQuery<List<Track>, dynamic, int> tracksOf(
|
||||
WidgetRef ref,
|
||||
AlbumSimple album,
|
||||
) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
|
||||
return useInfiniteQueryJob(
|
||||
job: tracksOfJob(album.id!),
|
||||
args: (spotify: spotify, album: album),
|
||||
);
|
||||
}
|
||||
|
||||
Query<bool, dynamic> isSavedForMe(
|
||||
WidgetRef ref,
|
||||
String album,
|
||||
) {
|
||||
return useSpotifyQuery<bool, dynamic>(
|
||||
"is-saved-for-me/$album",
|
||||
(spotify) {
|
||||
return spotify.me
|
||||
.containsSavedAlbums([album]).then((value) => value[album]);
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
InfiniteQuery<Page<AlbumSimple>, dynamic, int> newReleases(WidgetRef ref) {
|
||||
final market = ref
|
||||
.watch(userPreferencesProvider.select((s) => s.recommendationMarket));
|
||||
|
||||
return useSpotifyInfiniteQuery<Page<AlbumSimple>, dynamic, int>(
|
||||
"new-releases",
|
||||
(pageParam, spotify) async {
|
||||
try {
|
||||
final albums = await spotify.browse
|
||||
.newReleases(country: market)
|
||||
.getPage(50, pageParam);
|
||||
|
||||
return albums;
|
||||
} catch (e, stack) {
|
||||
Catcher2.reportCheckedError(e, stack);
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
ref: ref,
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isLast) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.nextOffset;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,151 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/wikipedia/wikipedia.dart';
|
||||
import 'package:wikipedia_api/wikipedia_api.dart';
|
||||
|
||||
class ArtistQueries {
|
||||
const ArtistQueries();
|
||||
|
||||
Query<Artist, dynamic> get(
|
||||
WidgetRef ref,
|
||||
String artist,
|
||||
) {
|
||||
return useSpotifyQuery<Artist, dynamic>(
|
||||
"artist-profile/$artist",
|
||||
(spotify) => spotify.artists.get(artist),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
InfiniteQuery<CursorPage<Artist>, dynamic, String> followedByMe(
|
||||
WidgetRef ref) {
|
||||
return useSpotifyInfiniteQuery<CursorPage<Artist>, dynamic, String>(
|
||||
"user-following-artists",
|
||||
(pageParam, spotify) async {
|
||||
return spotify.me
|
||||
.following(FollowingType.artist)
|
||||
.getPage(15, pageParam);
|
||||
},
|
||||
initialPage: "",
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isLast || (lastPageData.items ?? []).length < 15) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.after;
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<List<Artist>, dynamic> followedByMeAll(WidgetRef ref) {
|
||||
return useSpotifyQuery(
|
||||
"user-following-artists-all",
|
||||
(spotify) async {
|
||||
CursorPage<Artist>? page =
|
||||
await spotify.me.following(FollowingType.artist).getPage(50);
|
||||
|
||||
final following = <Artist>[];
|
||||
|
||||
if (page.isLast == true) {
|
||||
return page.items?.toList() ?? [];
|
||||
}
|
||||
|
||||
following.addAll(page.items ?? []);
|
||||
while (page?.isLast != true) {
|
||||
page = await spotify.me
|
||||
.following(FollowingType.artist)
|
||||
.getPage(50, page?.after ?? '');
|
||||
following.addAll(page.items ?? []);
|
||||
}
|
||||
|
||||
return following;
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<bool, dynamic> doIFollow(
|
||||
WidgetRef ref,
|
||||
String artist,
|
||||
) {
|
||||
return useSpotifyQuery<bool, dynamic>(
|
||||
"user-follows-artists-query/$artist",
|
||||
(spotify) async {
|
||||
final result = await spotify.me.checkFollowing(
|
||||
FollowingType.artist,
|
||||
[artist],
|
||||
);
|
||||
return result[artist];
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<Iterable<Track>, dynamic> topTracksOf(
|
||||
WidgetRef ref,
|
||||
String artist,
|
||||
) {
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
return useSpotifyQuery<Iterable<Track>, dynamic>(
|
||||
"artist-top-track-query/$artist",
|
||||
(spotify) {
|
||||
return spotify.artists
|
||||
.topTracks(artist, preferences.recommendationMarket);
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
InfiniteQuery<Page<Album>, dynamic, int> albumsOf(
|
||||
WidgetRef ref,
|
||||
String artist,
|
||||
) {
|
||||
return useSpotifyInfiniteQuery<Page<Album>, dynamic, int>(
|
||||
"artist-albums/$artist",
|
||||
(pageParam, spotify) async {
|
||||
return spotify.artists.albums(artist).getPage(5, pageParam);
|
||||
},
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.nextOffset;
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<Iterable<Artist>, dynamic> relatedArtistsOf(
|
||||
WidgetRef ref,
|
||||
String artist,
|
||||
) {
|
||||
return useSpotifyQuery<Iterable<Artist>, dynamic>(
|
||||
"artist-related-artist-query/$artist",
|
||||
(spotify) {
|
||||
return spotify.artists.relatedArtists(artist);
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<Summary?, dynamic> wikipediaSummary(ArtistSimple artist) {
|
||||
return useQuery<Summary?, dynamic>(
|
||||
"artist-wikipedia-query/${artist.id}",
|
||||
() 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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,120 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
||||
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
class CategoryQueries {
|
||||
const CategoryQueries();
|
||||
|
||||
Query<List<Category>, dynamic> listAll(
|
||||
WidgetRef ref,
|
||||
Market recommendationMarket,
|
||||
) {
|
||||
ref.watch(userPreferencesProvider.select((s) => s.locale));
|
||||
final locale = useContext().l10n.localeName;
|
||||
final query = useSpotifyQuery<List<Category>, dynamic>(
|
||||
"category-playlists",
|
||||
(spotify) async {
|
||||
final categories = await spotify.categories
|
||||
.list(
|
||||
country: recommendationMarket,
|
||||
locale: locale,
|
||||
)
|
||||
.all();
|
||||
|
||||
return categories.toList()..shuffle();
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
InfiniteQuery<Page<Category>, dynamic, int> list(
|
||||
WidgetRef ref,
|
||||
Market recommendationMarket,
|
||||
) {
|
||||
ref.watch(userPreferencesProvider.select((s) => s.locale));
|
||||
final locale = useContext().l10n.localeName;
|
||||
return useSpotifyInfiniteQuery<Page<Category>, dynamic, int>(
|
||||
"category-playlists",
|
||||
(pageParam, spotify) async {
|
||||
final categories = await spotify.categories
|
||||
.list(
|
||||
country: recommendationMarket,
|
||||
locale: locale,
|
||||
)
|
||||
.getPage(8, pageParam);
|
||||
|
||||
return categories;
|
||||
},
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isLast || (lastPageData.items ?? []).length < 8) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.nextOffset;
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
InfiniteQuery<Page<PlaylistSimple?>, dynamic, int> playlistsOf(
|
||||
WidgetRef ref,
|
||||
String category,
|
||||
) {
|
||||
ref.watch(userPreferencesProvider.select((s) => s.locale));
|
||||
final market = ref
|
||||
.watch(userPreferencesProvider.select((s) => s.recommendationMarket));
|
||||
final locale = useContext().l10n.localeName;
|
||||
return useSpotifyInfiniteQuery<Page<PlaylistSimple?>, dynamic, int>(
|
||||
"category-playlists/$category",
|
||||
(pageParam, spotify) async {
|
||||
final playlists = await Pages<PlaylistSimple?>(
|
||||
spotify,
|
||||
"v1/browse/categories/$category/playlists?country=${market.name}&locale=$locale",
|
||||
(json) => json == null ? null : PlaylistSimple.fromJson(json),
|
||||
'playlists',
|
||||
(json) => PlaylistsFeatured.fromJson(json),
|
||||
).getPage(5, pageParam);
|
||||
|
||||
return playlists;
|
||||
},
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.nextOffset;
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<List<String>, dynamic> genreSeeds(WidgetRef ref) {
|
||||
final customSpotify = ref.watch(customSpotifyEndpointProvider);
|
||||
final query = useQuery<List<String>, dynamic>(
|
||||
"genre-seeds",
|
||||
customSpotify.listGenreSeeds,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
return ref.listenManual(
|
||||
customSpotifyEndpointProvider,
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
query.refresh();
|
||||
}
|
||||
},
|
||||
).close;
|
||||
}, [query]);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@ -1,114 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/map.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
||||
import 'package:spotube/models/lyrics.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class LyricsQueries {
|
||||
const LyricsQueries();
|
||||
|
||||
Query<String, dynamic> static(
|
||||
Track? track,
|
||||
String geniusAccessToken,
|
||||
) {
|
||||
return useQuery<String, dynamic>(
|
||||
"genius-lyrics-query/${track?.id}",
|
||||
() async {
|
||||
if (track == null) {
|
||||
return "“Give this player a track to play”\n- S'Challa";
|
||||
}
|
||||
final lyrics = await ServiceUtils.getLyrics(
|
||||
track.name!,
|
||||
track.artists?.map((s) => s.name).whereNotNull().toList() ?? [],
|
||||
apiKey: geniusAccessToken,
|
||||
optimizeQuery: true,
|
||||
);
|
||||
|
||||
if (lyrics == null) throw Exception("Unable find lyrics");
|
||||
return lyrics;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Query<SubtitleSimple, dynamic> synced(
|
||||
Track? track,
|
||||
) {
|
||||
return useQuery<SubtitleSimple, dynamic>(
|
||||
"synced-lyrics/${track?.id}}",
|
||||
() async {
|
||||
if (track == null || track is! SourcedTrack) {
|
||||
throw "No track currently";
|
||||
}
|
||||
final timedLyrics = await ServiceUtils.getTimedLyrics(track);
|
||||
if (timedLyrics == null) throw Exception("Unable to find lyrics");
|
||||
|
||||
return timedLyrics;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// The Concept behind this method was shamelessly stolen from
|
||||
/// https://github.com/akashrchandran/spotify-lyrics-api
|
||||
///
|
||||
/// Thanks to [akashrchandran](https://github.com/akashrchandran) for the idea
|
||||
///
|
||||
/// Special thanks to [raptag](https://github.com/raptag) for discovering this
|
||||
/// jem
|
||||
|
||||
Query<SubtitleSimple, Exception> spotifySynced(WidgetRef ref, Track? track) {
|
||||
return useSpotifyQuery<SubtitleSimple, Exception>(
|
||||
"spotify-synced-lyrics/${track?.id}}",
|
||||
(spotify) async {
|
||||
if (track == null) {
|
||||
throw "No track currently";
|
||||
}
|
||||
final token = await spotify.getCredentials();
|
||||
final res = await http.get(
|
||||
Uri.parse(
|
||||
"https://spclient.wg.spotify.com/color-lyrics/v2/track/${track.id}?format=json&market=from_token",
|
||||
),
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36",
|
||||
"App-platform": "WebPlayer",
|
||||
"authorization": "Bearer ${token.accessToken}"
|
||||
});
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception("Unable to find lyrics");
|
||||
}
|
||||
final linesRaw = Map.castFrom<dynamic, dynamic, String, dynamic>(
|
||||
jsonDecode(res.body),
|
||||
)["lyrics"]?["lines"] as List?;
|
||||
|
||||
final lines = linesRaw?.map((line) {
|
||||
return LyricSlice(
|
||||
time: Duration(milliseconds: int.parse(line["startTimeMs"])),
|
||||
text: line["words"] as String,
|
||||
);
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
return SubtitleSimple(
|
||||
lyrics: lines,
|
||||
name: track.name!,
|
||||
uri: res.request!.url,
|
||||
rating: 100,
|
||||
);
|
||||
},
|
||||
jsonConfig: JsonConfig(
|
||||
fromJson: (json) => SubtitleSimple.fromJson(json.castKeyDeep<String>()),
|
||||
toJson: (data) => data.toJson(),
|
||||
),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,320 +0,0 @@
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/library/playlist_generate/recommendation_attribute_dials.dart';
|
||||
import 'package:spotube/extensions/map.dart';
|
||||
import 'package:spotube/extensions/track.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
|
||||
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
typedef RecommendationParameters = ({
|
||||
RecommendationAttribute acousticness,
|
||||
RecommendationAttribute danceability,
|
||||
// ignore: non_constant_identifier_names
|
||||
RecommendationAttribute duration_ms,
|
||||
RecommendationAttribute energy,
|
||||
RecommendationAttribute instrumentalness,
|
||||
RecommendationAttribute key,
|
||||
RecommendationAttribute liveness,
|
||||
RecommendationAttribute loudness,
|
||||
RecommendationAttribute mode,
|
||||
RecommendationAttribute popularity,
|
||||
RecommendationAttribute speechiness,
|
||||
RecommendationAttribute tempo,
|
||||
// ignore: non_constant_identifier_names
|
||||
RecommendationAttribute time_signature,
|
||||
RecommendationAttribute valence,
|
||||
});
|
||||
|
||||
Map<String, num> recommendationAttributeToMap(RecommendationAttribute attr) => {
|
||||
"min": attr.min,
|
||||
"target": attr.target,
|
||||
"max": attr.max,
|
||||
};
|
||||
|
||||
({Map<String, num> min, Map<String, num> target, Map<String, num> max})
|
||||
recommendationParametersToMap(RecommendationParameters params) {
|
||||
final maxMap = <String, num>{
|
||||
if (params.acousticness != zeroValues)
|
||||
"acousticness": params.acousticness.max,
|
||||
if (params.danceability != zeroValues)
|
||||
"danceability": params.danceability.max,
|
||||
if (params.duration_ms != zeroValues) "duration_ms": params.duration_ms.max,
|
||||
if (params.energy != zeroValues) "energy": params.energy.max,
|
||||
if (params.instrumentalness != zeroValues)
|
||||
"instrumentalness": params.instrumentalness.max,
|
||||
if (params.key != zeroValues) "key": params.key.max,
|
||||
if (params.liveness != zeroValues) "liveness": params.liveness.max,
|
||||
if (params.loudness != zeroValues) "loudness": params.loudness.max,
|
||||
if (params.mode != zeroValues) "mode": params.mode.max,
|
||||
if (params.popularity != zeroValues) "popularity": params.popularity.max,
|
||||
if (params.speechiness != zeroValues) "speechiness": params.speechiness.max,
|
||||
if (params.tempo != zeroValues) "tempo": params.tempo.max,
|
||||
if (params.time_signature != zeroValues)
|
||||
"time_signature": params.time_signature.max,
|
||||
if (params.valence != zeroValues) "valence": params.valence.max,
|
||||
};
|
||||
final minMap = <String, num>{
|
||||
if (params.acousticness != zeroValues)
|
||||
"acousticness": params.acousticness.min,
|
||||
if (params.danceability != zeroValues)
|
||||
"danceability": params.danceability.min,
|
||||
if (params.duration_ms != zeroValues) "duration_ms": params.duration_ms.min,
|
||||
if (params.energy != zeroValues) "energy": params.energy.min,
|
||||
if (params.instrumentalness != zeroValues)
|
||||
"instrumentalness": params.instrumentalness.min,
|
||||
if (params.key != zeroValues) "key": params.key.min,
|
||||
if (params.liveness != zeroValues) "liveness": params.liveness.min,
|
||||
if (params.loudness != zeroValues) "loudness": params.loudness.min,
|
||||
if (params.mode != zeroValues) "mode": params.mode.min,
|
||||
if (params.popularity != zeroValues) "popularity": params.popularity.min,
|
||||
if (params.speechiness != zeroValues) "speechiness": params.speechiness.min,
|
||||
if (params.tempo != zeroValues) "tempo": params.tempo.min,
|
||||
if (params.time_signature != zeroValues)
|
||||
"time_signature": params.time_signature.min,
|
||||
if (params.valence != zeroValues) "valence": params.valence.min,
|
||||
};
|
||||
final targetMap = <String, num>{
|
||||
if (params.acousticness != zeroValues)
|
||||
"acousticness": params.acousticness.target,
|
||||
if (params.danceability != zeroValues)
|
||||
"danceability": params.danceability.target,
|
||||
if (params.duration_ms != zeroValues)
|
||||
"duration_ms": params.duration_ms.target,
|
||||
if (params.energy != zeroValues) "energy": params.energy.target,
|
||||
if (params.instrumentalness != zeroValues)
|
||||
"instrumentalness": params.instrumentalness.target,
|
||||
if (params.key != zeroValues) "key": params.key.target,
|
||||
if (params.liveness != zeroValues) "liveness": params.liveness.target,
|
||||
if (params.loudness != zeroValues) "loudness": params.loudness.target,
|
||||
if (params.mode != zeroValues) "mode": params.mode.target,
|
||||
if (params.popularity != zeroValues) "popularity": params.popularity.target,
|
||||
if (params.speechiness != zeroValues)
|
||||
"speechiness": params.speechiness.target,
|
||||
if (params.tempo != zeroValues) "tempo": params.tempo.target,
|
||||
if (params.time_signature != zeroValues)
|
||||
"time_signature": params.time_signature.target,
|
||||
if (params.valence != zeroValues) "valence": params.valence.target,
|
||||
};
|
||||
|
||||
return (
|
||||
max: maxMap,
|
||||
min: minMap,
|
||||
target: targetMap,
|
||||
);
|
||||
}
|
||||
|
||||
class PlaylistQueries {
|
||||
const PlaylistQueries();
|
||||
|
||||
Query<bool, dynamic> doesUserFollow(
|
||||
WidgetRef ref,
|
||||
String playlistId,
|
||||
String userId,
|
||||
) {
|
||||
return useSpotifyQuery<bool, dynamic>(
|
||||
"playlist-is-followed/$playlistId/$userId",
|
||||
(spotify) async {
|
||||
final result =
|
||||
await spotify.playlists.followedByUsers(playlistId, [userId]);
|
||||
return result[userId] ?? false;
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
InfiniteQuery<Page<PlaylistSimple>, dynamic, int> ofMine(WidgetRef ref) {
|
||||
return useSpotifyInfiniteQuery<Page<PlaylistSimple>, dynamic, int>(
|
||||
"current-user-playlists",
|
||||
(page, spotify) async {
|
||||
final playlists = await spotify.playlists.me.getPage(10, page * 10);
|
||||
return playlists;
|
||||
},
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) =>
|
||||
(lastPageData.items?.length ?? 0) < 10 || lastPageData.isLast
|
||||
? null
|
||||
: lastPage + 1,
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<List<PlaylistSimple>, dynamic> ofMineAll(WidgetRef ref) {
|
||||
return useSpotifyQuery<List<PlaylistSimple>, dynamic>(
|
||||
"current-user-all-playlists",
|
||||
(spotify) async {
|
||||
var page = await spotify.playlists.me.getPage(50);
|
||||
final playlists = <PlaylistSimple>[];
|
||||
|
||||
if (page.isLast == true) {
|
||||
return page.items?.toList() ?? [];
|
||||
}
|
||||
|
||||
playlists.addAll(page.items ?? []);
|
||||
while (!page.isLast) {
|
||||
page = await spotify.playlists.me.getPage(50, page.nextOffset);
|
||||
playlists.addAll(page.items ?? []);
|
||||
}
|
||||
|
||||
return playlists;
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Track>> likedTracks(SpotifyApi spotify) async {
|
||||
final tracks = await spotify.tracks.me.saved.all();
|
||||
|
||||
return tracks.map((e) => e.track!).toList();
|
||||
}
|
||||
|
||||
Query<List<Track>, dynamic> likedTracksQuery(WidgetRef ref) {
|
||||
final query = useCallback((spotify) => likedTracks(spotify), []);
|
||||
final context = useContext();
|
||||
|
||||
return useSpotifyQuery<List<Track>, dynamic>(
|
||||
"user-liked-tracks",
|
||||
query,
|
||||
jsonConfig: JsonConfig(
|
||||
toJson: (tracks) => <String, dynamic>{
|
||||
'tracks': tracks.map((e) => e.toJson()).toList(),
|
||||
},
|
||||
fromJson: (json) => (json['tracks'] as List)
|
||||
.map(
|
||||
(e) => Track.fromJson((e as Map).castKeyDeep<String>()),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
refreshConfig: RefreshConfig.withDefaults(
|
||||
context,
|
||||
// will never make it stale
|
||||
staleDuration: const Duration(days: 60),
|
||||
),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<Playlist, dynamic> byId(WidgetRef ref, String id) {
|
||||
return useSpotifyQuery<Playlist, dynamic>(
|
||||
"playlist/$id",
|
||||
(spotify) async {
|
||||
return await spotify.playlists.get(id);
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Track>> tracksOf(
|
||||
int pageParam,
|
||||
SpotifyApi spotify,
|
||||
String playlistId,
|
||||
) async {
|
||||
try {
|
||||
final playlists = await spotify.playlists
|
||||
.getTracksByPlaylistId(playlistId)
|
||||
.getPage(20, pageParam * 20);
|
||||
return playlists.items?.toList() ?? <Track>[];
|
||||
} catch (e, stack) {
|
||||
Catcher2.reportCheckedError(e, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
int? tracksOfQueryNextPage(int lastPage, List<Track> lastPageData) {
|
||||
if (lastPageData.length < 20) {
|
||||
return null;
|
||||
}
|
||||
return lastPage + 1;
|
||||
}
|
||||
|
||||
InfiniteQuery<List<Track>, dynamic, int> tracksOfQuery(
|
||||
WidgetRef ref,
|
||||
String playlistId,
|
||||
) {
|
||||
return useSpotifyInfiniteQuery<List<Track>, dynamic, int>(
|
||||
"playlist-tracks/$playlistId",
|
||||
(page, spotify) => tracksOf(page, spotify, playlistId),
|
||||
initialPage: 0,
|
||||
nextPage: tracksOfQueryNextPage,
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
InfiniteQuery<Page<PlaylistSimple>, dynamic, int> featured(
|
||||
WidgetRef ref,
|
||||
) {
|
||||
return useSpotifyInfiniteQuery<Page<PlaylistSimple>, dynamic, int>(
|
||||
"featured-playlists",
|
||||
(pageParam, spotify) async {
|
||||
try {
|
||||
final playlists =
|
||||
await spotify.playlists.featured.getPage(5, pageParam);
|
||||
return playlists;
|
||||
} catch (e, stack) {
|
||||
Catcher2.reportCheckedError(e, stack);
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.nextOffset;
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<List<Track>, dynamic> generate(
|
||||
WidgetRef ref, {
|
||||
({List<String> tracks, List<String> artists, List<String> genres})? seeds,
|
||||
RecommendationParameters? parameters,
|
||||
int limit = 20,
|
||||
Market? market,
|
||||
}) {
|
||||
final marketOfPreference = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.recommendationMarket),
|
||||
);
|
||||
final customSpotify = ref.watch(customSpotifyEndpointProvider);
|
||||
|
||||
final parametersMap =
|
||||
parameters == null ? null : recommendationParametersToMap(parameters);
|
||||
|
||||
final query = useQuery<List<Track>, dynamic>(
|
||||
"generate-playlist",
|
||||
() async {
|
||||
final tracks = await customSpotify.getRecommendations(
|
||||
limit: limit,
|
||||
market: market ?? marketOfPreference,
|
||||
max: parametersMap?.max,
|
||||
min: parametersMap?.min,
|
||||
target: parametersMap?.target,
|
||||
seedArtists: seeds?.artists,
|
||||
seedGenres: seeds?.genres,
|
||||
seedTracks: seeds?.tracks,
|
||||
);
|
||||
return tracks;
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
return ref.listenManual(
|
||||
customSpotifyEndpointProvider,
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
query.refresh();
|
||||
}
|
||||
},
|
||||
).close;
|
||||
}, [query]);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import 'package:spotube/services/queries/album.dart';
|
||||
import 'package:spotube/services/queries/artist.dart';
|
||||
import 'package:spotube/services/queries/category.dart';
|
||||
import 'package:spotube/services/queries/lyrics.dart';
|
||||
import 'package:spotube/services/queries/playlist.dart';
|
||||
import 'package:spotube/services/queries/search.dart';
|
||||
import 'package:spotube/services/queries/tracks.dart';
|
||||
import 'package:spotube/services/queries/user.dart';
|
||||
import 'package:spotube/services/queries/views.dart';
|
||||
|
||||
class Queries {
|
||||
const Queries._();
|
||||
final album = const AlbumQueries();
|
||||
final artist = const ArtistQueries();
|
||||
final category = const CategoryQueries();
|
||||
final lyrics = const LyricsQueries();
|
||||
final playlist = const PlaylistQueries();
|
||||
final search = const SearchQueries();
|
||||
final user = const UserQueries();
|
||||
final views = const ViewsQueries();
|
||||
final tracks = const TracksQueries();
|
||||
}
|
||||
|
||||
const useQueries = Queries._();
|
||||
@ -1,60 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
|
||||
typedef SearchParams = ({
|
||||
SpotifyApi spotify,
|
||||
SearchType searchType,
|
||||
String query
|
||||
});
|
||||
|
||||
class SearchQueries {
|
||||
const SearchQueries();
|
||||
|
||||
static final queryJob =
|
||||
InfiniteQueryJob.withVariableKey<List<Page>, dynamic, int, SearchParams>(
|
||||
baseQueryKey: "search-query",
|
||||
task: (variableKey, page, args) => args!.spotify.search.get(
|
||||
args.query,
|
||||
types: [args.searchType],
|
||||
).getPage(10, page),
|
||||
initialPage: 0,
|
||||
nextPage: (lastPage, lastPageData) {
|
||||
if (lastPageData.isEmpty) return null;
|
||||
if ((lastPageData.first.isLast ||
|
||||
(lastPageData.first.items ?? []).length < 10)) {
|
||||
return null;
|
||||
}
|
||||
return lastPageData.first.nextOffset;
|
||||
},
|
||||
enabled: false,
|
||||
);
|
||||
|
||||
InfiniteQuery<List<Page>, dynamic, int> query(
|
||||
WidgetRef ref,
|
||||
String queryStr,
|
||||
SearchType searchType,
|
||||
) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final query = useInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>(
|
||||
job: queryJob(searchType.name),
|
||||
args: (spotify: spotify, searchType: searchType, query: queryStr),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
return ref.listenManual(
|
||||
spotifyProvider,
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
query.refreshAll();
|
||||
}
|
||||
},
|
||||
).close;
|
||||
}, [query]);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
||||
|
||||
class TracksQueries {
|
||||
const TracksQueries();
|
||||
|
||||
Query<Track, dynamic> track(WidgetRef ref, String id) {
|
||||
return useSpotifyQuery(
|
||||
"track/$id",
|
||||
(spotify) => spotify.tracks.get(id),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
||||
import 'package:spotube/models/spotify_friends.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class UserQueries {
|
||||
const UserQueries();
|
||||
Query<User?, dynamic> me(WidgetRef ref) {
|
||||
final context = useContext();
|
||||
|
||||
return useSpotifyQuery<User, dynamic>(
|
||||
"current-user",
|
||||
(spotify) async {
|
||||
final me = await spotify.me.get();
|
||||
if (ref.read(AuthenticationNotifier.provider) == null) return null;
|
||||
if (me.images == null || me.images?.isEmpty == true) {
|
||||
me.images = [
|
||||
Image()
|
||||
..height = 50
|
||||
..width = 50
|
||||
..url = TypeConversionUtils.image_X_UrlString(
|
||||
me.images,
|
||||
placeholder: ImagePlaceholder.artist,
|
||||
),
|
||||
];
|
||||
}
|
||||
return me;
|
||||
},
|
||||
refreshConfig: RefreshConfig.withDefaults(
|
||||
context,
|
||||
// will never make it stale
|
||||
staleDuration: const Duration(days: 60),
|
||||
),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
|
||||
Query<SpotifyFriends, dynamic> friendActivity(WidgetRef ref) {
|
||||
final customSpotify = ref.read(customSpotifyEndpointProvider);
|
||||
return useSpotifyQuery<SpotifyFriends, dynamic>(
|
||||
"friend-activity",
|
||||
(spotify) {
|
||||
return customSpotify.getFriendActivity();
|
||||
},
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
class ViewsQueries {
|
||||
const ViewsQueries();
|
||||
|
||||
Query<Map<String, dynamic>?, dynamic> get(
|
||||
WidgetRef ref,
|
||||
String view,
|
||||
) {
|
||||
final customSpotify = ref.watch(customSpotifyEndpointProvider);
|
||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||
final market = ref
|
||||
.watch(userPreferencesProvider.select((s) => s.recommendationMarket));
|
||||
|
||||
final locale = useContext().l10n.localeName;
|
||||
|
||||
final query = useQuery<Map<String, dynamic>?, dynamic>("views/$view", () {
|
||||
if (auth == null) return null;
|
||||
return customSpotify.getView(
|
||||
view,
|
||||
market: market,
|
||||
country: market,
|
||||
locale: locale,
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() {
|
||||
return ref.listenManual(
|
||||
customSpotifyEndpointProvider,
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
query.refresh();
|
||||
}
|
||||
},
|
||||
).close;
|
||||
}, [query]);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
40
pubspec.lock
40
pubspec.lock
@ -675,30 +675,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
fl_query:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_query
|
||||
sha256: daee5ab0ed8899baa201b89b5813107df5258144a9e2bcf192dbcf922c57d985
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
fl_query_devtools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_query_devtools
|
||||
sha256: "2ae8905fd4a95f1d245a1b54057c31c8d27fc961223bcb7ce13088bcf6595059"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
fl_query_hooks:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_query_hooks
|
||||
sha256: "6c88b3bfbdc3e1330931b927903929d7351f86fc63266ac93b3acb9f133a09a9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
fluentui_system_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1319,14 +1295,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.7.1"
|
||||
json_view:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_view
|
||||
sha256: "905c69f9e69d1eab5406b87ab6c10c3706c04c70c6a4959621bd2b43c2d27374"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1495,14 +1463,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
mutex:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mutex
|
||||
sha256: "03116a4e46282a671b46c12de649d72c0ed18188ffe12a8d0fc63e83f4ad88f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -32,9 +32,6 @@ dependencies:
|
||||
duration: ^3.0.12
|
||||
envied: ^0.3.0
|
||||
file_selector: ^1.0.1
|
||||
fl_query: ^1.0.0
|
||||
fl_query_hooks: ^1.0.0
|
||||
fl_query_devtools: ^0.1.0
|
||||
fluentui_system_icons: ^1.1.189
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
Loading…
Reference in New Issue
Block a user