feat: use providers in remaining pages and remove fl_query

This commit is contained in:
Kingkor Roy Tirtho 2024-03-17 16:56:35 +06:00
parent d2a9ff6652
commit 51710d5aee
35 changed files with 69 additions and 1600 deletions

View File

@ -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,

View File

@ -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}",

View File

@ -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,

View File

@ -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,

View File

@ -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(

View File

@ -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 &&

View File

@ -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();
}
}

View File

@ -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]);
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
},
),
),

View File

@ -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(

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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 =

View File

@ -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;
}

View File

@ -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,
);
}
}

View File

@ -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._();

View File

@ -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);
},
);
}
}

View File

@ -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,
);
}
}

View File

@ -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;
},
);
}
}

View File

@ -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;
},
);
}
}

View File

@ -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;
}
}

View File

@ -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,
);
}
}

View File

@ -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;
}
}

View File

@ -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._();

View File

@ -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;
}
}

View File

@ -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,
);
}
}

View File

@ -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,
);
}
}

View File

@ -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;
}
}

View File

@ -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:

View File

@ -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