Merge pull request #422 from KRTirtho/immutable-fl-query-integration

Immutable fl query integration
This commit is contained in:
Kingkor Roy Tirtho 2023-03-02 19:10:30 +06:00 committed by GitHub
commit bd48ca44ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1136 additions and 1063 deletions

View File

@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart' hide Search; import 'package:spotify/spotify.dart' hide Search;
import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -16,7 +17,7 @@ import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/pages/player/player.dart'; import 'package:spotube/pages/player/player.dart';
import 'package:spotube/pages/playlist/playlist.dart'; import 'package:spotube/pages/playlist/playlist.dart';
import 'package:spotube/pages/root/root_app.dart'; import 'package:spotube/pages/root/root_app.dart';
import 'package:spotube/pages/search/search.dart'; // import 'package:spotube/pages/search/search.dart';
import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart'; import 'package:spotube/pages/mobile_login/mobile_login.dart';

View File

@ -1,4 +1,4 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -7,10 +7,8 @@ import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:uuid/uuid.dart';
enum AlbumType { enum AlbumType {
album, album,
@ -48,15 +46,18 @@ class AlbumCard extends HookConsumerWidget {
final playing = useStream(PlaylistQueueNotifier.playing).data ?? final playing = useStream(PlaylistQueueNotifier.playing).data ??
PlaylistQueueNotifier.isPlaying; PlaylistQueueNotifier.isPlaying;
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
final queryBowl = QueryBowl.of(context); final queryClient = useQueryClient();
final query = queryBowl.getQuery<List<TrackSimple>, SpotifyApi>( final query = queryClient
Queries.album.tracksOf(album.id!).queryKey); .getQuery<List<TrackSimple>, dynamic>("album-tracks/${album.id}");
final tracks = useState(query?.data ?? album.tracks ?? <Track>[]); final tracks = useState(query?.data ?? album.tracks ?? <Track>[]);
bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks.value); bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks.value);
final int marginH = final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
final updating = useState(false); final updating = useState(false);
final spotify = ref.watch(spotifyProvider);
final scaffold = ScaffoldMessenger.of(context);
return PlaybuttonCard( return PlaybuttonCard(
imageUrl: TypeConversionUtils.image_X_UrlString( imageUrl: TypeConversionUtils.image_X_UrlString(
@ -99,10 +100,14 @@ class AlbumCard extends HookConsumerWidget {
updating.value = true; updating.value = true;
try { try {
final fetchedTracks = final fetchedTracks =
await queryBowl.fetchQuery<List<TrackSimple>, SpotifyApi>( await queryClient.fetchQuery<List<TrackSimple>, SpotifyApi>(
Queries.album.tracksOf(album.id!), "album-tracks/${album.id}",
externalData: ref.read(spotifyProvider), () {
key: ValueKey(const Uuid().v4()), return spotify.albums
.getTracks(album.id!)
.all()
.then((value) => value.toList());
},
); );
if (fetchedTracks == null || fetchedTracks.isEmpty) return; if (fetchedTracks == null || fetchedTracks.isEmpty) return;
@ -113,7 +118,7 @@ class AlbumCard extends HookConsumerWidget {
.toList(), .toList(),
); );
tracks.value = fetchedTracks; tracks.value = fetchedTracks;
ScaffoldMessenger.of(context).showSnackBar( scaffold.showSnackBar(
SnackBar( SnackBar(
content: Text("Added ${album.tracks?.length} tracks to queue"), content: Text("Added ${album.tracks?.length} tracks to queue"),
), ),

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -8,7 +7,6 @@ import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
class ArtistAlbumList extends HookConsumerWidget { class ArtistAlbumList extends HookConsumerWidget {
@ -23,20 +21,17 @@ class ArtistAlbumList extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final scrollController = useScrollController(); final scrollController = useScrollController();
final albumsQuery = useInfiniteQuery( final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
job: Queries.artist.albumsOf(artistId),
externalData: ref.watch(spotifyProvider),
);
final albums = useMemoized(() { final albums = useMemoized(() {
return albumsQuery.pages return albumsQuery.pages
.expand<Album>((page) => page?.items ?? const Iterable.empty()) .expand<Album>((page) => page.items ?? const Iterable.empty())
.toList(); .toList();
}, [albumsQuery.pages]); }, [albumsQuery.pages]);
final hasNextPage = albumsQuery.pages.isEmpty final hasNextPage = albumsQuery.pages.isEmpty
? false ? false
: (albumsQuery.pages.last?.items?.length ?? 0) == 5; : (albumsQuery.pages.last.items?.length ?? 0) == 5;
return SizedBox( return SizedBox(
height: 300, height: 300,
@ -52,9 +47,7 @@ class ArtistAlbumList extends HookConsumerWidget {
controller: scrollController, controller: scrollController,
child: Waypoint( child: Waypoint(
controller: scrollController, controller: scrollController,
onTouchEdge: () { onTouchEdge: albumsQuery.fetchNext,
albumsQuery.fetchNextPage();
},
child: ListView.builder( child: ListView.builder(
itemCount: albums.length, itemCount: albums.length,
controller: scrollController, controller: scrollController,

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -9,7 +8,6 @@ import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
class CategoryCard extends HookConsumerWidget { class CategoryCard extends HookConsumerWidget {
@ -24,18 +22,14 @@ class CategoryCard extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final scrollController = useScrollController(); final scrollController = useScrollController();
final spotify = ref.watch(spotifyProvider); final playlistQuery = useQueries.category.playlistsOf(
final playlistQuery = useInfiniteQuery( ref,
job: Queries.category.playlistsOf(category.id!), category.id!,
externalData: spotify,
); );
final hasNextPage = playlistQuery.pages.isEmpty
? false
: (playlistQuery.pages.last?.items?.length ?? 0) == 5;
final playlists = playlistQuery.pages final playlists = playlistQuery.pages
.expand( .expand(
(page) => page?.items ?? const Iterable.empty(), (page) => page.items ?? const Iterable.empty(),
) )
.toList(); .toList();
@ -49,7 +43,7 @@ class CategoryCard extends HookConsumerWidget {
], ],
), ),
), ),
playlistQuery.hasError playlistQuery.hasPageError && !playlistQuery.hasPageData
? PlatformText( ? PlatformText(
"Something Went Wrong\n${playlistQuery.errors.first}") "Something Went Wrong\n${playlistQuery.errors.first}")
: SizedBox( : SizedBox(
@ -67,7 +61,7 @@ class CategoryCard extends HookConsumerWidget {
child: Waypoint( child: Waypoint(
controller: scrollController, controller: scrollController,
onTouchEdge: () { onTouchEdge: () {
playlistQuery.fetchNextPage(); playlistQuery.fetchNext();
}, },
child: ListView( child: ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -76,7 +70,7 @@ class CategoryCard extends HookConsumerWidget {
children: [ children: [
...playlists ...playlists
.map((playlist) => PlaylistCard(playlist)), .map((playlist) => PlaylistCard(playlist)),
if (hasNextPage) if (playlistQuery.hasNextPage)
const ShimmerPlaybuttonCard(count: 1), const ShimmerPlaybuttonCard(count: 1),
], ],
), ),

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart' hide Image; import 'package:flutter/material.dart' hide Image;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -12,7 +11,6 @@ import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -24,10 +22,7 @@ class UserAlbums extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final albumsQuery = useQuery( final albumsQuery = useQueries.album.ofMine(ref);
job: Queries.album.ofMine,
externalData: ref.watch(spotifyProvider),
);
final spacing = useBreakpointValue<double>( final spacing = useBreakpointValue<double>(
sm: 0, sm: 0,
@ -64,7 +59,7 @@ class UserAlbums extends HookConsumerWidget {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await albumsQuery.refetch(); await albumsQuery.refresh();
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -11,7 +10,6 @@ import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/components/artist/artist_card.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -22,20 +20,17 @@ class UserArtists extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final artistQuery = useInfiniteQuery( final artistQuery = useQueries.artist.followedByMe(ref);
job: Queries.artist.followedByMe,
externalData: ref.watch(spotifyProvider),
);
final hasNextPage = artistQuery.pages.isEmpty final hasNextPage = artistQuery.pages.isEmpty
? false ? false
: (artistQuery.pages.last?.items?.length ?? 0) == 15; : (artistQuery.pages.last.items?.length ?? 0) == 15;
final searchText = useState(''); final searchText = useState('');
final filteredArtists = useMemoized(() { final filteredArtists = useMemoized(() {
final artists = artistQuery.pages final artists = artistQuery.pages
.expand<Artist>((page) => page?.items ?? const Iterable.empty()); .expand<Artist>((page) => page.items ?? const Iterable.empty());
if (searchText.value.isEmpty) { if (searchText.value.isEmpty) {
return artists.toList(); return artists.toList();
@ -85,7 +80,7 @@ class UserArtists extends HookConsumerWidget {
) )
: RefreshIndicator( : RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await artistQuery.refetchPages(); await artistQuery.refreshAll();
}, },
child: GridView.builder( child: GridView.builder(
itemCount: filteredArtists.length, itemCount: filteredArtists.length,
@ -104,7 +99,7 @@ class UserArtists extends HookConsumerWidget {
controller: useScrollController(), controller: useScrollController(),
isGrid: true, isGrid: true,
onTouchEdge: () { onTouchEdge: () {
artistQuery.fetchNextPage(); artistQuery.fetchNext();
}, },
child: ArtistCard(filteredArtists[index]), child: ArtistCard(filteredArtists[index]),
); );

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart' hide Image; import 'package:flutter/material.dart' hide Image;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
@ -15,7 +14,6 @@ import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -35,22 +33,23 @@ class UserPlaylists extends HookConsumerWidget {
: PlaybuttonCardViewType.square; : PlaybuttonCardViewType.square;
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final playlistsQuery = useQuery( final playlistsQuery = useQueries.playlist.ofMine(ref);
job: Queries.playlist.ofMine,
externalData: ref.watch(spotifyProvider),
);
Image image = Image(); final likedTracksPlaylist = useMemoized(
image.height = 300; () => PlaylistSimple()
image.width = 300; ..name = "Liked Tracks"
PlaylistSimple likedTracksPlaylist = PlaylistSimple(); ..type = "playlist"
likedTracksPlaylist.name = "Liked Tracks"; ..collaborative = false
likedTracksPlaylist.type = "playlist"; ..public = false
likedTracksPlaylist.collaborative = false; ..id = "user-liked-tracks"
likedTracksPlaylist.public = false; ..images = [
likedTracksPlaylist.id = "user-liked-tracks"; Image()
image.url = "https://t.scdn.co/images/3099b3803ad9496896c43f22fe9be8c4.png"; ..height = 300
likedTracksPlaylist.images = [image]; ..width = 300
..url =
"https://t.scdn.co/images/3099b3803ad9496896c43f22fe9be8c4.png"
],
[]);
final playlists = useMemoized( final playlists = useMemoized(
() { () {
@ -90,7 +89,7 @@ class UserPlaylists extends HookConsumerWidget {
.toList(), .toList(),
]; ];
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => playlistsQuery.refetch(), onRefresh: playlistsQuery.refresh,
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
child: Material( child: Material(

View File

@ -10,7 +10,6 @@ import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:uuid/uuid.dart';
class PlaylistCard extends HookConsumerWidget { class PlaylistCard extends HookConsumerWidget {
final PlaylistSimple playlist; final PlaylistSimple playlist;
@ -26,9 +25,9 @@ class PlaylistCard extends HookConsumerWidget {
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
final playing = useStream(PlaylistQueueNotifier.playing).data ?? final playing = useStream(PlaylistQueueNotifier.playing).data ??
PlaylistQueueNotifier.isPlaying; PlaylistQueueNotifier.isPlaying;
final queryBowl = QueryBowl.of(context); final queryBowl = QueryClient.of(context);
final query = queryBowl.getQuery<List<Track>, SpotifyApi>( final query = queryBowl.getQuery<List<Track>, dynamic>(
Queries.playlist.tracksOf(playlist.id!).queryKey, "playlist-tracks/${playlist.id}",
); );
final tracks = useState(query?.data ?? []); final tracks = useState(query?.data ?? []);
bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks.value); bool isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks.value);
@ -37,6 +36,8 @@ class PlaylistCard extends HookConsumerWidget {
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
final updating = useState(false); final updating = useState(false);
final spotify = ref.watch(spotifyProvider);
final scaffold = ScaffoldMessenger.of(context);
return PlaybuttonCard( return PlaybuttonCard(
viewType: viewType, viewType: viewType,
@ -66,9 +67,8 @@ class PlaylistCard extends HookConsumerWidget {
} }
List<Track> fetchedTracks = await queryBowl.fetchQuery( List<Track> fetchedTracks = await queryBowl.fetchQuery(
key: ValueKey(const Uuid().v4()), "playlist-tracks/${playlist.id}",
Queries.playlist.tracksOf(playlist.id!), () => useQueries.playlist.tracksOf(playlist.id!, spotify),
externalData: ref.read(spotifyProvider),
) ?? ) ??
[]; [];
@ -85,9 +85,8 @@ class PlaylistCard extends HookConsumerWidget {
try { try {
if (isPlaylistPlaying) return; if (isPlaylistPlaying) return;
List<Track> fetchedTracks = await queryBowl.fetchQuery( List<Track> fetchedTracks = await queryBowl.fetchQuery(
key: ValueKey(const Uuid().v4()), "playlist-tracks/${playlist.id}",
Queries.playlist.tracksOf(playlist.id!), () => useQueries.playlist.tracksOf(playlist.id!, spotify),
externalData: ref.read(spotifyProvider),
) ?? ) ??
[]; [];
@ -95,7 +94,7 @@ class PlaylistCard extends HookConsumerWidget {
playlistNotifier.add(fetchedTracks); playlistNotifier.add(fetchedTracks);
tracks.value = fetchedTracks; tracks.value = fetchedTracks;
ScaffoldMessenger.of(context).showSnackBar( scaffold.showSnackBar(
SnackBar( SnackBar(
content: Text("Added ${fetchedTracks.length} tracks to queue"), content: Text("Added ${fetchedTracks.length} tracks to queue"),
), ),

View File

@ -1,4 +1,4 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -7,7 +7,6 @@ import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/sidebar.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart';
class PlaylistCreateDialog extends HookConsumerWidget { class PlaylistCreateDialog extends HookConsumerWidget {
const PlaylistCreateDialog({Key? key}) : super(key: key); const PlaylistCreateDialog({Key? key}) : super(key: key);
@ -28,6 +27,8 @@ class PlaylistCreateDialog extends HookConsumerWidget {
final description = useTextEditingController(); final description = useTextEditingController();
final public = useState(false); final public = useState(false);
final collaborative = useState(false); final collaborative = useState(false);
final client = useQueryClient();
final navigator = Navigator.of(context);
onCreate() async { onCreate() async {
if (playlistName.text.isEmpty) return; if (playlistName.text.isEmpty) return;
@ -39,12 +40,12 @@ class PlaylistCreateDialog extends HookConsumerWidget {
public: public.value, public: public.value,
description: description.text, description: description.text,
); );
await QueryBowl.of(context) await client
.getQuery( .getQuery(
Queries.playlist.ofMine.queryKey, "current-user-playlists",
) )
?.refetch(); ?.refresh();
Navigator.pop(context); navigator.pop();
} }
return PlatformAlertDialog( return PlatformAlertDialog(

View File

@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/provider/spotify_provider.dart';
class PlaylistGenreView extends ConsumerWidget {
final String genreId;
final String genreName;
final Iterable<PlaylistSimple>? playlists;
const PlaylistGenreView(
this.genreId,
this.genreName, {
this.playlists,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
return Scaffold(
appBar: PageWindowTitleBar(
leading: const PlatformBackButton(),
),
body: Column(
children: [
PlatformText.subheading(
genreName,
textAlign: TextAlign.center,
),
Consumer(
builder: (context, ref, child) {
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
return Expanded(
child: SingleChildScrollView(
child: FutureBuilder<Iterable<PlaylistSimple>>(
future: playlists == null
? (genreId != "user-featured-playlists"
? spotifyApi.playlists
.getByCategoryId(genreId)
.all()
: spotifyApi.playlists.featured.all())
: Future.value(playlists),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error occurred"));
}
if (!snapshot.hasData) {
return const PlatformCircularProgressIndicator();
}
return Center(
child: Wrap(
children: snapshot.data!
.map(
(playlist) => Padding(
padding: const EdgeInsets.all(8.0),
child: PlaylistCard(playlist),
),
)
.toList(),
),
);
}),
),
);
},
)
],
),
);
}
}

View File

@ -1,5 +1,4 @@
import 'package:badges/badges.dart'; import 'package:badges/badges.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -12,7 +11,6 @@ import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/downloader_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -195,11 +193,7 @@ class SidebarFooter extends HookConsumerWidget {
width: 256, width: 256,
child: HookBuilder( child: HookBuilder(
builder: (context) { builder: (context) {
var spotify = ref.watch(spotifyProvider); final me = useQueries.user.me(ref);
final me = useQuery(
job: Queries.user.me,
externalData: spotify,
);
final data = me.data; final data = me.data;
final avatarImg = TypeConversionUtils.image_X_UrlString( final avatarImg = TypeConversionUtils.image_X_UrlString(
@ -208,20 +202,8 @@ class SidebarFooter extends HookConsumerWidget {
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
); );
// TODO: Remove below code after fl-query ^0.4.0
/// Temporary fix before fl-query 0.4.0
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
useEffect(() {
if (auth != null && me.hasError) {
me.setExternalData(spotify);
me.refetch();
}
return null;
}, [auth, me.hasError]);
/// ===================================
return Padding( return Padding(
padding: const EdgeInsets.all(16).copyWith(left: 0), padding: const EdgeInsets.all(16).copyWith(left: 0),
child: Row( child: Row(

View File

@ -1,6 +1,4 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:platform_ui/platform_ui.dart';
@ -18,14 +16,8 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final userPlaylists = useQuery( final userPlaylists = useQueries.playlist.ofMine(ref);
job: Queries.playlist.ofMine, final me = useQueries.user.me(ref);
externalData: spotify,
);
final me = useQuery(
job: Queries.user.me,
externalData: spotify,
);
final filteredPlaylists = userPlaylists.data?.where( final filteredPlaylists = userPlaylists.data?.where(
(playlist) => (playlist) =>
playlist.owner?.id != null && playlist.owner!.id == me.data?.id, playlist.owner?.id != null && playlist.owner!.id == me.data?.id,

View File

@ -1,5 +1,4 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -8,7 +7,6 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/hooks/use_palette_color.dart'; import 'package:spotube/hooks/use_palette_color.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -47,59 +45,36 @@ class HeartButton extends ConsumerWidget {
} }
} }
Tuple3<bool, Mutation<bool, Tuple2<SpotifyApi, bool>>, Query<User, SpotifyApi>> Tuple3<bool, Mutation<bool, dynamic, bool>, Query<User, dynamic>>
useTrackToggleLike(Track track, WidgetRef ref) { useTrackToggleLike(Track track, WidgetRef ref) {
final me = final me = useQueries.user.me(ref);
useQuery(job: Queries.user.me, externalData: ref.watch(spotifyProvider));
final savedTracks = useQuery( final savedTracks =
job: Queries.playlist.tracksOf("user-liked-tracks"), useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks");
externalData: ref.watch(spotifyProvider),
);
final isLiked = final isLiked =
savedTracks.data?.map((track) => track.id).contains(track.id) ?? false; savedTracks.data?.map((track) => track.id).contains(track.id) ?? false;
final mounted = useIsMounted(); final mounted = useIsMounted();
final toggleTrackLike = useMutation<bool, Tuple2<SpotifyApi, bool>>( final toggleTrackLike = useMutations.track.toggleFavorite(
job: Mutations.track.toggleFavorite(track.id!), ref,
onMutate: (variable) { track.id!,
savedTracks.setQueryData( onMutate: (variables) {
(oldData) { return variables;
if (!variable.item2) { },
return [...(oldData ?? []), track]; onError: (payload, isLiked) {
} if (!mounted()) return;
return oldData savedTracks.setData(
isLiked == true
? [...(savedTracks.data ?? []), track]
: savedTracks.data
?.where( ?.where(
(element) => element.id != track.id, (element) => element.id != track.id,
) )
.toList() ?? .toList() ??
[]; [],
},
);
return track;
},
onData: (payload, variables, _) {
if (!mounted()) return;
savedTracks.refetch();
},
onError: (payload, variables, queryContext) {
if (!mounted()) return;
savedTracks.setQueryData(
(oldData) {
if (variables.item2) {
return [...(oldData ?? []), track];
}
return oldData
?.where(
(element) => element.id != track.id,
)
.toList() ??
[];
},
); );
}, },
); );
@ -116,10 +91,8 @@ class TrackHeartButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final savedTracks = useQuery( final savedTracks =
job: Queries.playlist.tracksOf("user-liked-tracks"), useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks");
externalData: ref.watch(spotifyProvider),
);
final toggler = useTrackToggleLike(track, ref); final toggler = useTrackToggleLike(track, ref);
if (toggler.item3.isLoading || !toggler.item3.hasData) { if (toggler.item3.isLoading || !toggler.item3.hasData) {
return const PlatformCircularProgressIndicator(); return const PlatformCircularProgressIndicator();
@ -130,9 +103,7 @@ class TrackHeartButton extends HookConsumerWidget {
isLiked: toggler.item1, isLiked: toggler.item1,
onPressed: savedTracks.hasData onPressed: savedTracks.hasData
? () { ? () {
toggler.item2.mutate( toggler.item2.mutate(toggler.item1);
Tuple2(ref.read(spotifyProvider), toggler.item1),
);
} }
: null, : null,
); );
@ -149,26 +120,21 @@ class PlaylistHeartButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final me = useQuery( final me = useQueries.user.me(ref);
job: Queries.user.me,
externalData: ref.watch(spotifyProvider), final isLikedQuery = useQueries.playlist.doesUserFollow(
ref,
playlist.id!,
me.data?.id ?? '',
); );
final job = final togglePlaylistLike = useMutations.playlist.toggleFavorite(
Queries.playlist.doesUserFollow("${playlist.id}:${me.data?.id}"); ref,
final isLikedQuery = useQuery( playlist.id!,
job: job, refreshQueries: [
externalData: ref.watch(spotifyProvider), isLikedQuery.key,
); "current-user-playlists",
],
final togglePlaylistLike = useMutation<bool, Tuple2<SpotifyApi, bool>>(
job: Mutations.playlist.toggleFavorite(playlist.id!),
onData: (payload, variables, queryContext) async {
await isLikedQuery.refetch();
await QueryBowl.of(context)
.getQuery(Queries.playlist.ofMine.queryKey)
?.refetch();
},
); );
final titleImage = useMemoized( final titleImage = useMemoized(
@ -195,12 +161,7 @@ class PlaylistHeartButton extends HookConsumerWidget {
color: color?.titleTextColor, color: color?.titleTextColor,
onPressed: isLikedQuery.hasData onPressed: isLikedQuery.hasData
? () { ? () {
togglePlaylistLike.mutate( togglePlaylistLike.mutate(isLikedQuery.data!);
Tuple2(
ref.read(spotifyProvider),
isLikedQuery.data!,
),
);
} }
: null, : null,
); );
@ -217,26 +178,18 @@ class AlbumHeartButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider); final me = useQueries.user.me(ref);
final me = useQuery(
job: Queries.user.me,
externalData: spotify,
);
final albumIsSaved = useQuery( final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!);
job: Queries.album.isSavedForMe(album.id!),
externalData: spotify,
);
final isLiked = albumIsSaved.data ?? false; final isLiked = albumIsSaved.data ?? false;
final toggleAlbumLike = useMutation<bool, Tuple2<SpotifyApi, bool>>( final toggleAlbumLike = useMutations.album.toggleFavorite(
job: Mutations.album.toggleFavorite(album.id!), ref,
onData: (payload, variables, queryContext) { album.id!,
albumIsSaved.refetch(); refreshQueries: [
QueryBowl.of(context) albumIsSaved.key,
.getQuery(Queries.album.ofMine.queryKey) "current-user-albums",
?.refetch(); ],
},
); );
if (me.isLoading || !me.hasData) { if (me.isLoading || !me.hasData) {
@ -248,8 +201,7 @@ class AlbumHeartButton extends HookConsumerWidget {
tooltip: isLiked ? "Remove from Favorite" : "Add to Favorite", tooltip: isLiked ? "Remove from Favorite" : "Add to Favorite",
onPressed: albumIsSaved.hasData onPressed: albumIsSaved.hasData
? () { ? () {
toggleAlbumLike toggleAlbumLike.mutate(isLiked);
.mutate(Tuple2(ref.read(spotifyProvider), isLiked));
} }
: null, : null,
); );

View File

@ -218,7 +218,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
: null, : null,
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await tracksSnapshot.refetch(); await tracksSnapshot.refresh();
}, },
child: CustomScrollView( child: CustomScrollView(
controller: controller, controller: controller,
@ -333,8 +333,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
builder: (context) { builder: (context) {
if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) { if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) {
return const ShimmerTrackTile(); return const ShimmerTrackTile();
} else if (tracksSnapshot.hasError && } else if (tracksSnapshot.hasError) {
tracksSnapshot.isError) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: PlatformText("Error ${tracksSnapshot.error}")); child: PlatformText("Error ${tracksSnapshot.error}"));
} }

View File

@ -1,6 +1,4 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart' hide Action; import 'package:flutter/material.dart' hide Action;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -21,10 +19,8 @@ import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
class TrackTile extends HookConsumerWidget { class TrackTile extends HookConsumerWidget {
final PlaylistQueue? playlist; final PlaylistQueue? playlist;
@ -77,16 +73,9 @@ class TrackTile extends HookConsumerWidget {
final playlistQueueNotifier = ref.watch(PlaylistQueueNotifier.notifier); final playlistQueueNotifier = ref.watch(PlaylistQueueNotifier.notifier);
final removingTrack = useState<String?>(null); final removingTrack = useState<String?>(null);
final removeTrack = useMutation<bool, Tuple2<SpotifyApi, String>>( final removeTrack = useMutations.playlist.removeTrackOf(
job: Mutations.playlist.removeTrackOf(playlistId ?? ""), ref,
onData: (payload, variables, ctx) { playlistId ?? "",
if (playlistId == null || !payload) return;
QueryBowl.of(context)
.getQuery(
Queries.playlist.tracksOf(playlistId!).queryKey,
)
?.refetch();
},
); );
void actionShare(Track track) { void actionShare(Track track) {
@ -359,7 +348,7 @@ class TrackTile extends HookConsumerWidget {
: const Icon(SpotubeIcons.heart), : const Icon(SpotubeIcons.heart),
text: const PlatformText("Save as favorite"), text: const PlatformText("Save as favorite"),
onPressed: () { onPressed: () {
toggler.item2.mutate(Tuple2(spotify, toggler.item1)); toggler.item2.mutate(toggler.item1);
}, },
), ),
if (auth != null) if (auth != null)
@ -370,7 +359,7 @@ class TrackTile extends HookConsumerWidget {
), ),
if (userPlaylist && auth != null) if (userPlaylist && auth != null)
Action( Action(
icon: (removeTrack.isLoading || !removeTrack.hasData) && icon: (removeTrack.isMutating || !removeTrack.hasData) &&
removingTrack.value == track.value.uri removingTrack.value == track.value.uri
? const Center( ? const Center(
child: PlatformCircularProgressIndicator(), child: PlatformCircularProgressIndicator(),
@ -379,7 +368,7 @@ class TrackTile extends HookConsumerWidget {
text: const PlatformText("Remove from playlist"), text: const PlatformText("Remove from playlist"),
onPressed: () { onPressed: () {
removingTrack.value = track.value.uri; removingTrack.value = track.value.uri;
removeTrack.mutate(Tuple2(spotify, track.value.uri!)); removeTrack.mutate(track.value.uri!);
}, },
), ),
Action( Action(

15
lib/extensions/map.dart Normal file
View File

@ -0,0 +1,15 @@
extension CastDeepMaps on Map {
Map<K2, dynamic> castKeyDeep<K2>() {
return cast<K2, dynamic>().map((key, value) {
if (value is Map) {
return MapEntry(key, value.castKeyDeep<K2>());
} else if (value is List) {
return MapEntry(
key,
value.map((e) => e is Map ? e.castKeyDeep<K2>() : e).toList(),
);
}
return MapEntry(key, value);
});
}
}

61
lib/extensions/page.dart Normal file
View File

@ -0,0 +1,61 @@
import 'package:spotify/spotify.dart';
extension CursorPageJson<T> on CursorPage<T> {
static CursorPage<T> fromJson<T>(
Map<String, dynamic> json,
T Function(dynamic json) itemFromJson,
) {
final metadata = Paging.fromJson(json["metadata"]);
final paging = CursorPaging<T>();
paging.cursors = Cursor.fromJson(json["metadata"])..after = json["after"];
paging.href = metadata.href;
paging.itemsNative = paging.itemsNative;
paging.limit = metadata.limit;
paging.next = metadata.next;
return CursorPage<T>(
paging,
itemFromJson,
);
}
Map<String, dynamic> toJson() {
return {
"after": after,
"metadata": metadata.toJson(),
};
}
}
extension PagingToJson<T> on Paging<T> {
Map<String, dynamic> toJson() {
return {
"items": itemsNative,
"total": total,
"next": next,
"previous": previous,
"limit": limit,
"offset": offset,
"href": href,
};
}
}
extension PageJson<T> on Page<T> {
static Page<T> fromJson<T>(
Map<String, dynamic> json,
T Function(dynamic json) itemFromJson,
) {
return Page<T>(
Paging<T>.fromJson(
Map.castFrom<dynamic, dynamic, String, dynamic>(json["metadata"]),
),
itemFromJson,
);
}
Map<String, dynamic> toJson() {
return {
"metadata": metadata.toJson(),
};
}
}

View File

@ -0,0 +1,53 @@
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 = DefaultConstants.retryConfig,
RefreshConfig refreshConfig = DefaultConstants.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

@ -0,0 +1,36 @@
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 = DefaultConstants.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

@ -0,0 +1,52 @@
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 = DefaultConstants.retryConfig,
final RefreshConfig refreshConfig = DefaultConstants.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

@ -31,7 +31,6 @@ import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart'; import 'package:window_size/window_size.dart';
final bowl = QueryBowl();
void main(List<String> rawArgs) async { void main(List<String> rawArgs) async {
final parser = ArgParser(); final parser = ArgParser();
@ -70,7 +69,7 @@ void main(List<String> rawArgs) async {
} }
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter(); await QueryClient.initialize(cachePrefix: "oss.krtirtho.spotube");
Hive.registerAdapter(CacheTrackAdapter()); Hive.registerAdapter(CacheTrackAdapter());
Hive.registerAdapter(CacheTrackEngagementAdapter()); Hive.registerAdapter(CacheTrackEngagementAdapter());
Hive.registerAdapter(CacheTrackSkipSegmentAdapter()); Hive.registerAdapter(CacheTrackSkipSegmentAdapter());
@ -173,8 +172,7 @@ void main(List<String> rawArgs) async {
}, },
) )
], ],
child: QueryBowlScope( child: QueryClientProvider(
bowl: bowl,
child: const Spotube(), child: const Spotube(),
), ),
); );

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -12,7 +11,6 @@ import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:spotube/provider/spotify_provider.dart';
class AlbumPage extends HookConsumerWidget { class AlbumPage extends HookConsumerWidget {
final AlbumSimple album; final AlbumSimple album;
@ -47,12 +45,7 @@ class AlbumPage extends HookConsumerWidget {
ref.watch(PlaylistQueueNotifier.provider); ref.watch(PlaylistQueueNotifier.provider);
final playback = ref.watch(PlaylistQueueNotifier.notifier); final playback = ref.watch(PlaylistQueueNotifier.notifier);
final SpotifyApi spotify = ref.watch(spotifyProvider); final tracksSnapshot = useQueries.album.tracksOf(ref, album.id!);
final tracksSnapshot = useQuery(
job: Queries.album.tracksOf(album.id!),
externalData: spotify,
);
final albumArt = useMemoized( final albumArt = useMemoized(
() => TypeConversionUtils.image_X_UrlString( () => TypeConversionUtils.image_X_UrlString(

View File

@ -1,5 +1,4 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -65,10 +64,7 @@ class ArtistPage extends HookConsumerWidget {
), ),
body: HookBuilder( body: HookBuilder(
builder: (context) { builder: (context) {
final artistsQuery = useQuery( final artistsQuery = useQueries.artist.get(ref, artistId);
job: Queries.artist.get(artistId),
externalData: spotify,
);
if (artistsQuery.isLoading || !artistsQuery.hasData) { if (artistsQuery.isLoading || !artistsQuery.hasData) {
return const ShimmerArtistProfile(); return const ShimmerArtistProfile();
@ -166,10 +162,8 @@ class ArtistPage extends HookConsumerWidget {
if (auth != null) if (auth != null)
HookBuilder( HookBuilder(
builder: (context) { builder: (context) {
final isFollowingQuery = useQuery( final isFollowingQuery = useQueries.artist
job: Queries.artist.doIFollow(artistId), .doIFollow(ref, artistId);
externalData: spotify,
);
if (isFollowingQuery.isLoading || if (isFollowingQuery.isLoading ||
!isFollowingQuery.hasData) { !isFollowingQuery.hasData) {
@ -181,7 +175,7 @@ class ArtistPage extends HookConsumerWidget {
); );
} }
final queryBowl = QueryBowl.of(context); final queryBowl = QueryClient.of(context);
return PlatformFilledButton( return PlatformFilledButton(
onPressed: () async { onPressed: () async {
@ -195,21 +189,14 @@ class ArtistPage extends HookConsumerWidget {
FollowingType.artist, FollowingType.artist,
[artistId], [artistId],
); );
await isFollowingQuery.refetch(); await isFollowingQuery.refresh();
queryBowl queryBowl
.getInfiniteQuery( .refreshInfiniteQueryAllPages(
Queries.artist.followedByMe "user-following-artists");
.queryKey,
)
?.refetch();
} finally { } finally {
QueryBowl.of(context) QueryClient.of(context).refreshQuery(
.refetchQueries([ "user-follows-artists-query/$artistId");
Queries.artist
.doIFollow(artistId)
.queryKey,
]);
} }
}, },
child: PlatformText( child: PlatformText(
@ -281,9 +268,9 @@ class ArtistPage extends HookConsumerWidget {
const SizedBox(height: 50), const SizedBox(height: 50),
HookBuilder( HookBuilder(
builder: (context) { builder: (context) {
final topTracksQuery = useQuery( final topTracksQuery = useQueries.artist.topTracksOf(
job: Queries.artist.topTracksOf(artistId), ref,
externalData: spotify, artistId,
); );
final isPlaylistPlaying = final isPlaylistPlaying =
@ -391,9 +378,9 @@ class ArtistPage extends HookConsumerWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
HookBuilder( HookBuilder(
builder: (context) { builder: (context) {
final relatedArtists = useQuery( final relatedArtists = useQueries.artist.relatedArtistsOf(
job: Queries.artist.relatedArtistsOf(artistId), ref,
externalData: spotify, artistId,
); );
if (relatedArtists.isLoading || !relatedArtists.hasData) { if (relatedArtists.isLoading || !relatedArtists.hasData) {

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -9,8 +8,6 @@ import 'package:spotube/components/genre/category_card.dart';
import 'package:spotube/components/shared/compact_search.dart'; import 'package:spotube/components/shared/compact_search.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -22,43 +19,19 @@ class GenrePage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final scrollController = useScrollController(); final scrollController = useScrollController();
final spotify = ref.watch(spotifyProvider);
final recommendationMarket = ref.watch( final recommendationMarket = ref.watch(
userPreferencesProvider.select((s) => s.recommendationMarket), userPreferencesProvider.select((s) => s.recommendationMarket),
); );
final categoriesQuery = useInfiniteQuery( final categoriesQuery = useQueries.category.list(ref, recommendationMarket);
job: Queries.category.list,
externalData: {
"spotify": spotify,
"recommendationMarket": recommendationMarket,
},
);
final isMounted = useIsMounted(); final isMounted = useIsMounted();
final auth = ref.watch(AuthenticationNotifier.provider);
/// Temporary fix before fl-query 0.4.0
useEffect(() {
if (auth != null && categoriesQuery.hasError) {
categoriesQuery.setExternalData({
"spotify": spotify,
"recommendationMarket": recommendationMarket,
});
categoriesQuery.refetchPages();
}
return null;
}, [auth, categoriesQuery.hasError]);
/// ===================================
return HookBuilder(builder: (context) {
final searchText = useState(""); final searchText = useState("");
final categories = useMemoized( final categories = useMemoized(
() { () {
final categories = categoriesQuery.pages final categories = categoriesQuery.pages
.expand<Category>( .expand<Category>(
(page) => page?.items ?? const Iterable.empty(), (page) => page.items ?? const Iterable.empty(),
) )
.toList(); .toList();
if (searchText.value.isEmpty) { if (searchText.value.isEmpty) {
@ -86,12 +59,12 @@ class GenrePage extends HookConsumerWidget {
final list = RefreshIndicator( final list = RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await categoriesQuery.refetchPages(); await categoriesQuery.refreshAll();
}, },
child: Waypoint( child: Waypoint(
onTouchEdge: () async { onTouchEdge: () async {
if (categoriesQuery.hasNextPage && isMounted()) { if (categoriesQuery.hasNextPage && isMounted()) {
await categoriesQuery.fetchNextPage(); await categoriesQuery.fetchNext();
} }
}, },
controller: scrollController, controller: scrollController,
@ -109,6 +82,7 @@ class GenrePage extends HookConsumerWidget {
), ),
), ),
); );
return Stack( return Stack(
children: [ children: [
Positioned.fill(child: list), Positioned.fill(child: list),
@ -119,6 +93,5 @@ class GenrePage extends HookConsumerWidget {
), ),
], ],
); );
});
} }
} }

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -10,7 +9,6 @@ import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -104,30 +102,9 @@ class PersonalizedPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider); final featuredPlaylistsQuery = useQueries.playlist.featured(ref);
final featuredPlaylistsQuery = useInfiniteQuery( final newReleases = useQueries.album.newReleases(ref);
job: Queries.playlist.featured,
externalData: spotify,
);
final newReleases = useInfiniteQuery(
job: Queries.album.newReleases,
externalData: spotify,
);
useEffect(() {
if (featuredPlaylistsQuery.hasError &&
featuredPlaylistsQuery.pages.first == null) {
featuredPlaylistsQuery.setExternalData(spotify);
featuredPlaylistsQuery.refetch();
}
if (newReleases.hasError && newReleases.pages.first == null) {
newReleases.setExternalData(spotify);
newReleases.refetch();
}
return null;
}, [spotify]);
return ListView( return ListView(
children: [ children: [
@ -136,13 +113,13 @@ class PersonalizedPage extends HookConsumerWidget {
featuredPlaylistsQuery.pages.whereType<Page<PlaylistSimple>>(), featuredPlaylistsQuery.pages.whereType<Page<PlaylistSimple>>(),
title: 'Featured', title: 'Featured',
hasNextPage: featuredPlaylistsQuery.hasNextPage, hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNextPage, onFetchMore: featuredPlaylistsQuery.fetchNext,
), ),
PersonalizedItemCard( PersonalizedItemCard(
albums: newReleases.pages.whereType<Page<AlbumSimple>>(), albums: newReleases.pages.whereType<Page<AlbumSimple>>(),
title: 'New Releases', title: 'New Releases',
hasNextPage: newReleases.hasNextPage, hasNextPage: newReleases.hasNextPage,
onFetchMore: newReleases.fetchNextPage, onFetchMore: newReleases.fetchNext,
), ),
], ],
); );

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
@ -10,7 +9,6 @@ import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
class GeniusLyrics extends HookConsumerWidget { class GeniusLyrics extends HookConsumerWidget {
final PaletteColor palette; final PaletteColor palette;
@ -24,12 +22,9 @@ class GeniusLyrics extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlist = ref.watch(PlaylistQueueNotifier.provider); final playlist = ref.watch(PlaylistQueueNotifier.provider);
final geniusLyricsQuery = useQuery( final geniusLyricsQuery = useQueries.lyrics.static(
job: Queries.lyrics.static(playlist?.activeTrack.id ?? ""),
externalData: Tuple2(
playlist?.activeTrack, playlist?.activeTrack,
ref.watch(userPreferencesProvider).geniusAccessToken, ref.watch(userPreferencesProvider).geniusAccessToken,
),
); );
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
@ -42,8 +37,8 @@ class GeniusLyrics extends HookConsumerWidget {
child: Text( child: Text(
playlist?.activeTrack.name ?? "", playlist?.activeTrack.name ?? "",
style: breakpoint >= Breakpoints.md style: breakpoint >= Breakpoints.md
? textTheme.headline3 ? textTheme.displaySmall
: textTheme.headline4?.copyWith( : textTheme.headlineMedium?.copyWith(
fontSize: 25, fontSize: 25,
color: palette.titleTextColor, color: palette.titleTextColor,
), ),
@ -54,8 +49,8 @@ class GeniusLyrics extends HookConsumerWidget {
TypeConversionUtils.artists_X_String<Artist>( TypeConversionUtils.artists_X_String<Artist>(
playlist?.activeTrack.artists ?? []), playlist?.activeTrack.artists ?? []),
style: (breakpoint >= Breakpoints.md style: (breakpoint >= Breakpoints.md
? textTheme.headline5 ? textTheme.headlineSmall
: textTheme.headline6) : textTheme.titleLarge)
?.copyWith(color: palette.bodyTextColor), ?.copyWith(color: palette.bodyTextColor),
), ),
) )
@ -68,12 +63,12 @@ class GeniusLyrics extends HookConsumerWidget {
child: Builder( child: Builder(
builder: (context) { builder: (context) {
if (geniusLyricsQuery.isLoading || if (geniusLyricsQuery.isLoading ||
geniusLyricsQuery.isRefetching) { geniusLyricsQuery.isRefreshing) {
return const ShimmerLyrics(); return const ShimmerLyrics();
} else if (geniusLyricsQuery.hasError) { } else if (geniusLyricsQuery.hasError) {
return Text( return Text(
"Sorry, no Lyrics were found for `${playlist?.activeTrack.name}` :'(\n${geniusLyricsQuery.error.toString()}", "Sorry, no Lyrics were found for `${playlist?.activeTrack.name}` :'(\n${geniusLyricsQuery.error.toString()}",
style: textTheme.bodyText1?.copyWith( style: textTheme.bodyLarge?.copyWith(
color: palette.bodyTextColor, color: palette.bodyTextColor,
), ),
); );

View File

@ -44,28 +44,7 @@ class SyncedLyrics extends HookConsumerWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final controller = useAutoScrollController(); final controller = useAutoScrollController();
final textTheme = Theme.of(context).textTheme; final timedLyricsQuery = useQueries.lyrics.synced(playlist?.activeTrack);
useEffect(() {
controller.scrollToIndex(0);
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(lyricDelayState.notifier).state = Duration.zero;
});
return null;
}, [playlist?.activeTrack]);
final headlineTextStyle = (breakpoint >= Breakpoints.md
? textTheme.headline3
: textTheme.headline4?.copyWith(fontSize: 25))
?.copyWith(color: palette.titleTextColor);
return QueryBuilder<SubtitleSimple, SpotubeTrack?>(
job: Queries.lyrics.synced(playlist?.activeTrack.id ?? ""),
externalData: playlist?.isLoading == true
? playlist?.activeTrack as SpotubeTrack
: null,
builder: (context, timedLyricsQuery) {
return HookBuilder(builder: (context) {
final lyricValue = timedLyricsQuery.data; final lyricValue = timedLyricsQuery.data;
final lyricsMap = useMemoized( final lyricsMap = useMemoized(
() => () =>
@ -77,6 +56,23 @@ class SyncedLyrics extends HookConsumerWidget {
[lyricValue], [lyricValue],
); );
final currentTime = useSyncedLyrics(ref, lyricsMap, lyricDelay); final currentTime = useSyncedLyrics(ref, lyricsMap, lyricDelay);
final textTheme = Theme.of(context).textTheme;
useEffect(() {
controller.scrollToIndex(0);
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(lyricDelayState.notifier).state = Duration.zero;
});
return null;
}, [playlist?.activeTrack]);
final headlineTextStyle = (breakpoint >= Breakpoints.md
? textTheme.displaySmall
: textTheme.headlineMedium?.copyWith(fontSize: 25))
?.copyWith(color: palette.titleTextColor);
return HookBuilder(builder: (context) {
return Stack( return Stack(
children: [ children: [
Column( Column(
@ -95,8 +91,8 @@ class SyncedLyrics extends HookConsumerWidget {
TypeConversionUtils.artists_X_String<Artist>( TypeConversionUtils.artists_X_String<Artist>(
playlist?.activeTrack.artists ?? []), playlist?.activeTrack.artists ?? []),
style: breakpoint >= Breakpoints.md style: breakpoint >= Breakpoints.md
? textTheme.headline5 ? textTheme.headlineSmall
: textTheme.headline6, : textTheme.titleLarge,
), ),
), ),
if (lyricValue != null && lyricValue.lyrics.isNotEmpty) if (lyricValue != null && lyricValue.lyrics.isNotEmpty)
@ -106,8 +102,7 @@ class SyncedLyrics extends HookConsumerWidget {
itemCount: lyricValue.lyrics.length, itemCount: lyricValue.lyrics.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final lyricSlice = lyricValue.lyrics[index]; final lyricSlice = lyricValue.lyrics[index];
final isActive = final isActive = lyricSlice.time.inSeconds == currentTime;
lyricSlice.time.inSeconds == currentTime;
if (isActive) { if (isActive) {
controller.scrollToIndex( controller.scrollToIndex(
@ -125,8 +120,7 @@ class SyncedLyrics extends HookConsumerWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: AnimatedDefaultTextStyle( child: AnimatedDefaultTextStyle(
duration: duration: const Duration(milliseconds: 250),
const Duration(milliseconds: 250),
style: TextStyle( style: TextStyle(
color: isActive color: isActive
? Colors.white ? Colors.white
@ -149,8 +143,7 @@ class SyncedLyrics extends HookConsumerWidget {
), ),
), ),
if (playlist?.activeTrack != null && if (playlist?.activeTrack != null &&
(lyricValue == null || (lyricValue == null || lyricValue.lyrics.isEmpty == true))
lyricValue.lyrics.isEmpty == true))
const Expanded(child: ShimmerLyrics()), const Expanded(child: ShimmerLyrics()),
], ],
), ),
@ -179,6 +172,5 @@ class SyncedLyrics extends HookConsumerWidget {
], ],
); );
}); });
});
} }
} }

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -9,7 +8,6 @@ import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -46,15 +44,11 @@ class PlaylistView extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier); final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
SpotifyApi spotify = ref.watch(spotifyProvider);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final meSnapshot = useQuery(job: Queries.user.me, externalData: spotify); final meSnapshot = useQueries.user.me(ref);
final tracksSnapshot = useQuery( final tracksSnapshot = useQueries.playlist.tracksOfQuery(ref, playlist.id!);
job: Queries.playlist.tracksOf(playlist.id!),
externalData: spotify,
);
final isPlaylistPlaying = final isPlaylistPlaying =
playlistNotifier.isPlayingPlaylist(tracksSnapshot.data ?? []); playlistNotifier.isPlayingPlaylist(tracksSnapshot.data ?? []);

View File

@ -4,13 +4,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/components/root/bottom_player.dart'; import 'package:spotube/components/root/bottom_player.dart';
import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/sidebar.dart';
import 'package:spotube/components/root/spotube_navigation_bar.dart'; import 'package:spotube/components/root/spotube_navigation_bar.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/hooks/use_update_checker.dart'; import 'package:spotube/hooks/use_update_checker.dart';
import 'package:spotube/provider/downloader_provider.dart'; import 'package:spotube/provider/downloader_provider.dart';

View File

@ -1,4 +1,5 @@
import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -17,14 +18,12 @@ import 'package:spotube/components/artist/artist_card.dart';
import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/playlist_queue_provider.dart'; import 'package:spotube/provider/playlist_queue_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
final searchTermStateProvider = StateProvider<String>((ref) => ""); final searchTermStateProvider = StateProvider<String>((ref) => "");
@ -37,49 +36,29 @@ class SearchPage extends HookConsumerWidget {
ref.watch(AuthenticationNotifier.provider); ref.watch(AuthenticationNotifier.provider);
final authenticationNotifier = final authenticationNotifier =
ref.watch(AuthenticationNotifier.provider.notifier); ref.watch(AuthenticationNotifier.provider.notifier);
final spotify = ref.watch(spotifyProvider);
final albumController = useScrollController(); final albumController = useScrollController();
final playlistController = useScrollController(); final playlistController = useScrollController();
final artistController = useScrollController(); final artistController = useScrollController();
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final getVariables = useCallback( final searchTerm = ref.watch(searchTermStateProvider);
() => Tuple2(
ref.read(searchTermStateProvider),
spotify,
),
[],
);
final searchTrack = useInfiniteQuery( final searchTrack =
job: Queries.search.get(SearchType.track.key), useQueries.search.query(ref, searchTerm, SearchType.track);
externalData: Tuple2("", spotify), final searchAlbum =
); useQueries.search.query(ref, searchTerm, SearchType.album);
final searchAlbum = useInfiniteQuery( final searchPlaylist =
job: Queries.search.get(SearchType.album.key), useQueries.search.query(ref, searchTerm, SearchType.playlist);
externalData: Tuple2("", spotify), final searchArtist =
); useQueries.search.query(ref, searchTerm, SearchType.artist);
final searchPlaylist = useInfiniteQuery(
job: Queries.search.get(SearchType.playlist.key),
externalData: Tuple2("", spotify),
);
final searchArtist = useInfiniteQuery(
job: Queries.search.get(SearchType.artist.key),
externalData: Tuple2("", spotify),
);
void onSearch() { Future<void> onSearch() async {
for (final query in [ await Future.wait([
searchTrack, searchTrack.refreshAll(),
searchAlbum, searchAlbum.refreshAll(),
searchPlaylist, searchPlaylist.refreshAll(),
searchArtist, searchArtist.refreshAll(),
]) { ]);
query.enabled = false;
query.fetched = false;
query.setExternalData(getVariables());
query.refetchPages();
}
} }
return SafeArea( return SafeArea(
@ -96,10 +75,6 @@ class SearchPage extends HookConsumerWidget {
), ),
color: PlatformTheme.of(context).scaffoldBackgroundColor, color: PlatformTheme.of(context).scaffoldBackgroundColor,
child: PlatformTextField( child: PlatformTextField(
onChanged: (value) {
ref.read(searchTermStateProvider.notifier).state =
value;
},
prefixIcon: SpotubeIcons.search, prefixIcon: SpotubeIcons.search,
prefixIconColor: PlatformProperty.only( prefixIconColor: PlatformProperty.only(
ios: ios:
@ -107,8 +82,14 @@ class SearchPage extends HookConsumerWidget {
other: null, other: null,
).resolve(platform!), ).resolve(platform!),
placeholder: "Search...", placeholder: "Search...",
onSubmitted: (value) { onSubmitted: (value) async {
ref.read(searchTermStateProvider.notifier).state =
value;
// Fl-Query is too fast, so we need to delay the search
// to prevent spamming the API :)
Timer(const Duration(milliseconds: 50), () {
onSearch(); onSearch();
});
}, },
), ),
), ),
@ -127,7 +108,7 @@ class SearchPage extends HookConsumerWidget {
...searchAlbum.pages, ...searchAlbum.pages,
...searchPlaylist.pages, ...searchPlaylist.pages,
...searchArtist.pages, ...searchArtist.pages,
].expand<Page>((page) => page ?? []).toList(); ].expand<Page>((page) => page).toList();
for (MapEntry<int, Page> page in pages.asMap().entries) { for (MapEntry<int, Page> page in pages.asMap().entries) {
for (var item in page.value.items ?? []) { for (var item in page.value.items ?? []) {
if (item is AlbumSimple) { if (item is AlbumSimple) {
@ -153,12 +134,10 @@ class SearchPage extends HookConsumerWidget {
children: [ children: [
if (tracks.isNotEmpty) if (tracks.isNotEmpty)
PlatformText.headline("Songs"), PlatformText.headline("Songs"),
if (searchTrack.isLoading && if (searchTrack.isLoadingPage)
!searchTrack.isFetchingNextPage)
const PlatformCircularProgressIndicator() const PlatformCircularProgressIndicator()
else if (searchTrack.hasError) else if (searchTrack.hasPageError)
PlatformText(searchTrack PlatformText(searchTrack.errors.lastOrNull
.error?[searchTrack.pageParams.last]
?.toString() ?? ?.toString() ??
"") "")
else else
@ -204,10 +183,10 @@ class SearchPage extends HookConsumerWidget {
tracks.isNotEmpty) tracks.isNotEmpty)
Center( Center(
child: PlatformTextButton( child: PlatformTextButton(
onPressed: searchTrack.isFetchingNextPage onPressed: searchTrack.isRefreshingPage
? null ? null
: () => searchTrack.fetchNextPage(), : () => searchTrack.fetchNext(),
child: searchTrack.isFetchingNextPage child: searchTrack.isRefreshingPage
? const PlatformCircularProgressIndicator() ? const PlatformCircularProgressIndicator()
: const PlatformText("Load more"), : const PlatformText("Load more"),
), ),
@ -231,7 +210,7 @@ class SearchPage extends HookConsumerWidget {
controller: playlistController, controller: playlistController,
child: Waypoint( child: Waypoint(
onTouchEdge: () { onTouchEdge: () {
searchPlaylist.fetchNextPage(); searchPlaylist.fetchNext();
}, },
controller: playlistController, controller: playlistController,
child: SingleChildScrollView( child: SingleChildScrollView(
@ -256,13 +235,11 @@ class SearchPage extends HookConsumerWidget {
), ),
), ),
), ),
if (searchPlaylist.isLoading && if (searchPlaylist.isLoadingPage)
!searchPlaylist.isFetchingNextPage)
const PlatformCircularProgressIndicator(), const PlatformCircularProgressIndicator(),
if (searchPlaylist.hasError) if (searchPlaylist.hasPageError)
PlatformText( PlatformText(
searchPlaylist.error?[ searchPlaylist.errors.lastOrNull
searchPlaylist.pageParams.last]
?.toString() ?? ?.toString() ??
"", "",
), ),
@ -283,7 +260,7 @@ class SearchPage extends HookConsumerWidget {
child: Waypoint( child: Waypoint(
controller: artistController, controller: artistController,
onTouchEdge: () { onTouchEdge: () {
searchArtist.fetchNextPage(); searchArtist.fetchNext();
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -311,13 +288,11 @@ class SearchPage extends HookConsumerWidget {
), ),
), ),
), ),
if (searchArtist.isLoading && if (searchArtist.isLoadingPage)
!searchArtist.isFetchingNextPage)
const PlatformCircularProgressIndicator(), const PlatformCircularProgressIndicator(),
if (searchArtist.hasError) if (searchArtist.hasPageError)
PlatformText( PlatformText(
searchArtist.error?[ searchArtist.errors.lastOrNull
searchArtist.pageParams.last]
?.toString() ?? ?.toString() ??
"", "",
), ),
@ -338,7 +313,7 @@ class SearchPage extends HookConsumerWidget {
child: Waypoint( child: Waypoint(
controller: albumController, controller: albumController,
onTouchEdge: () { onTouchEdge: () {
searchAlbum.fetchNextPage(); searchAlbum.fetchNext();
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -364,14 +339,11 @@ class SearchPage extends HookConsumerWidget {
), ),
), ),
), ),
if (searchAlbum.isLoading && if (searchAlbum.isLoadingPage)
!searchAlbum.isFetchingNextPage)
const PlatformCircularProgressIndicator(), const PlatformCircularProgressIndicator(),
if (searchAlbum.hasError) if (searchAlbum.hasPageError)
PlatformText( PlatformText(
searchAlbum searchAlbum.errors.lastOrNull?.toString() ??
.error?[searchAlbum.pageParams.last]
?.toString() ??
"", "",
), ),
], ],

View File

@ -1,16 +1,18 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:spotify/spotify.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tuple/tuple.dart'; import 'package:spotube/hooks/use_spotify_mutation.dart';
class AlbumMutations { class AlbumMutations {
final toggleFavorite = const AlbumMutations();
MutationJob.withVariableKey<bool, Tuple2<SpotifyApi, bool>>(
preMutationKey: "toggle-album-like",
task: (queryKey, externalData) async {
final albumId = getVariable(queryKey);
final spotify = externalData.item1;
final isLiked = externalData.item2;
Mutation<bool, dynamic, bool> toggleFavorite(
WidgetRef ref,
String albumId, {
List<String>? refreshQueries,
}) {
return useSpotifyMutation<bool, dynamic, bool, dynamic>(
"toggle-album-like/$albumId",
(isLiked, spotify) async {
if (isLiked) { if (isLiked) {
await spotify.me.removeAlbums([albumId]); await spotify.me.removeAlbums([albumId]);
} else { } else {
@ -18,5 +20,8 @@ class AlbumMutations {
} }
return !isLiked; return !isLiked;
}, },
ref: ref,
refreshQueries: refreshQueries,
); );
} }
}

View File

@ -2,8 +2,11 @@ import 'package:spotube/services/mutations/album.dart';
import 'package:spotube/services/mutations/playlist.dart'; import 'package:spotube/services/mutations/playlist.dart';
import 'package:spotube/services/mutations/track.dart'; import 'package:spotube/services/mutations/track.dart';
abstract class Mutations { class _UseMutations {
static final playlist = PlaylistMutations(); const _UseMutations._();
static final album = AlbumMutations(); final playlist = const PlaylistMutations();
static final track = TrackMutations(); final album = const AlbumMutations();
final track = const TrackMutations();
} }
const useMutations = _UseMutations._();

View File

@ -1,16 +1,18 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:spotify/spotify.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tuple/tuple.dart'; import 'package:spotube/hooks/use_spotify_mutation.dart';
class PlaylistMutations { class PlaylistMutations {
final toggleFavorite = const PlaylistMutations();
MutationJob.withVariableKey<bool, Tuple2<SpotifyApi, bool>>(
preMutationKey: "toggle-playlist-like",
task: (queryKey, externalData) async {
final playlistId = getVariable(queryKey);
final spotify = externalData.item1;
final isLiked = externalData.item2;
Mutation<bool, dynamic, bool> toggleFavorite(
WidgetRef ref,
String playlistId, {
List<String>? refreshQueries,
}) {
return useSpotifyMutation<bool, dynamic, bool, dynamic>(
"toggle-playlist-like/$playlistId",
(isLiked, spotify) async {
if (isLiked) { if (isLiked) {
await spotify.playlists.unfollowPlaylist(playlistId); await spotify.playlists.unfollowPlaylist(playlistId);
} else { } else {
@ -18,18 +20,23 @@ class PlaylistMutations {
} }
return !isLiked; return !isLiked;
}, },
ref: ref,
refreshQueries: refreshQueries,
); );
}
final removeTrackOf = Mutation<bool, dynamic, String> removeTrackOf(
MutationJob.withVariableKey<bool, Tuple2<SpotifyApi, String>>( WidgetRef ref,
preMutationKey: "remove-track-from-playlist", String playlistId,
task: (queryKey, externalData) async { ) {
final spotify = externalData.item1; return useSpotifyMutation<bool, dynamic, String, dynamic>(
final playlistId = getVariable(queryKey); "remove-track-from-playlist/$playlistId",
final trackId = externalData.item2; (trackId, spotify) async {
await spotify.playlists.removeTracks([trackId], playlistId); await spotify.playlists.removeTracks([trackId], playlistId);
return true; return true;
}, },
ref: ref,
refreshQueries: ["playlist-tracks/$playlistId"],
); );
} }
}

View File

@ -1,16 +1,20 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:spotify/spotify.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tuple/tuple.dart'; import 'package:spotube/hooks/use_spotify_mutation.dart';
class TrackMutations { class TrackMutations {
final toggleFavorite = const TrackMutations();
MutationJob.withVariableKey<bool, Tuple2<SpotifyApi, bool>>(
preMutationKey: "toggle-track-like",
task: (queryKey, externalData) async {
final trackId = getVariable(queryKey);
final spotify = externalData.item1;
final isLiked = externalData.item2;
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) { if (isLiked) {
await spotify.tracks.me.removeOne(trackId); await spotify.tracks.me.removeOne(trackId);
} else { } else {
@ -18,5 +22,11 @@ class TrackMutations {
} }
return !isLiked; return !isLiked;
}, },
ref: ref,
onData: onData,
onMutate: onMutate,
refreshQueries: ["playlist-tracks/user-liked-tracks"],
onError: onError,
); );
} }
}

View File

@ -1,37 +1,56 @@
import 'package:catcher/catcher.dart'; import 'package:catcher/catcher.dart';
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/hooks/use_spotify_infinite_query.dart';
import 'package:spotube/hooks/use_spotify_query.dart';
class AlbumQueries { class AlbumQueries {
final ofMine = QueryJob<Iterable<AlbumSimple>, SpotifyApi>( const AlbumQueries();
queryKey: "current-user-albums",
task: (_, spotify) { Query<Iterable<AlbumSimple>, dynamic> ofMine(WidgetRef ref) {
return useSpotifyQuery<Iterable<AlbumSimple>, dynamic>(
"current-user-albums",
(spotify) {
return spotify.me.savedAlbums().all(); return spotify.me.savedAlbums().all();
}, },
ref: ref,
); );
}
final tracksOf = QueryJob.withVariableKey<List<TrackSimple>, SpotifyApi>( Query<List<TrackSimple>, dynamic> tracksOf(
preQueryKey: "album-tracks", WidgetRef ref,
task: (queryKey, spotify) { String albumId,
final id = getVariable(queryKey); ) {
return spotify.albums.getTracks(id).all().then((value) => value.toList()); return useSpotifyQuery<List<TrackSimple>, dynamic>(
"album-tracks/$albumId",
(spotify) {
return spotify.albums
.getTracks(albumId)
.all()
.then((value) => value.toList());
}, },
ref: ref,
); );
}
final isSavedForMe = Query<bool, dynamic> isSavedForMe(
QueryJob.withVariableKey<bool, SpotifyApi>(task: (queryKey, spotify) { WidgetRef ref,
return spotify.me String album,
.isSavedAlbums([getVariable(queryKey)]).then((value) => value.first); ) {
}); return useSpotifyQuery<bool, dynamic>(
"is-saved-for-me/$album",
(spotify) {
return spotify.me.isSavedAlbums([album]).then((value) => value.first);
},
ref: ref,
);
}
final newReleases = InfiniteQueryJob<Page<AlbumSimple>, SpotifyApi, int>( InfiniteQuery<Page<AlbumSimple>, dynamic, int> newReleases(WidgetRef ref) {
queryKey: "new-releases", return useSpotifyInfiniteQuery<Page<AlbumSimple>, dynamic, int>(
initialParam: 0, "new-releases",
getNextPageParam: (lastPage, lastParam) => (pageParam, spotify) async {
lastPage.items?.length == 5 ? lastPage.nextOffset : null,
getPreviousPageParam: (firstPage, firstParam) => firstPage.nextOffset - 6,
refetchOnExternalDataChange: true,
task: (_, pageParam, spotify) async {
try { try {
final albums = await Pages( final albums = await Pages(
spotify, spotify,
@ -46,5 +65,14 @@ class AlbumQueries {
rethrow; rethrow;
} }
}, },
ref: ref,
initialPage: 0,
nextPage: (lastPage, lastPageData) {
if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) {
return null;
}
return lastPageData.nextOffset;
},
); );
} }
}

View File

@ -1,59 +1,103 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/hooks/use_spotify_infinite_query.dart';
import 'package:spotube/hooks/use_spotify_query.dart';
class ArtistQueries { class ArtistQueries {
final get = QueryJob.withVariableKey<Artist, SpotifyApi>( const ArtistQueries();
preQueryKey: "artist-profile",
task: (queryKey, externalData) =>
externalData.artists.get(getVariable(queryKey)),
);
final followedByMe = InfiniteQueryJob<CursorPage<Artist>, SpotifyApi, String>( Query<Artist, dynamic> get(
queryKey: "user-following-artists", WidgetRef ref,
initialParam: "", String artist,
getNextPageParam: (lastPage, lastParam) => lastPage.after, ) {
getPreviousPageParam: (lastPage, lastParam) => return useSpotifyQuery<Artist, dynamic>(
lastPage.metadata.previous ?? "", "artist-profile/$artist",
task: (queryKey, pageKey, spotify) { (spotify) => spotify.artists.get(artist),
return spotify.me.following(FollowingType.artist).getPage(15, pageKey); 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,
); );
}
final doIFollow = QueryJob.withVariableKey<bool, SpotifyApi>( Query<bool, dynamic> doIFollow(
preQueryKey: "user-follows-artists-query", WidgetRef ref,
task: (artistId, spotify) async { String artist,
) {
return useSpotifyQuery<bool, dynamic>(
"user-follows-artists-query/$artist",
(spotify) async {
final result = await spotify.me.isFollowing( final result = await spotify.me.isFollowing(
FollowingType.artist, FollowingType.artist,
[getVariable(artistId)], [artist],
); );
return result.first; return result.first;
}, },
); ref: ref,
final topTracksOf = QueryJob.withVariableKey<Iterable<Track>, SpotifyApi>(
preQueryKey: "artist-top-track-query",
task: (queryKey, spotify) {
return spotify.artists.getTopTracks(getVariable(queryKey), "US");
},
);
final albumsOf =
InfiniteQueryJob.withVariableKey<Page<Album>, SpotifyApi, int>(
preQueryKey: "artist-albums",
initialParam: 0,
getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset,
getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 6,
task: (queryKey, pageKey, spotify) {
final id = getVariable(queryKey);
return spotify.artists.albums(id).getPage(5, pageKey);
},
);
final relatedArtistsOf =
QueryJob.withVariableKey<Iterable<Artist>, SpotifyApi>(
preQueryKey: "artist-related-artist-query",
task: (queryKey, spotify) {
return spotify.artists.getRelatedArtists(getVariable(queryKey));
},
); );
} }
Query<Iterable<Track>, dynamic> topTracksOf(
WidgetRef ref,
String artist,
) {
return useSpotifyQuery<Iterable<Track>, dynamic>(
"artist-top-track-query/$artist",
(spotify) {
return spotify.artists.getTopTracks(artist, "US");
},
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.getRelatedArtists(artist);
},
ref: ref,
);
}
}

View File

@ -1,33 +1,65 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/map.dart';
import 'package:spotube/extensions/page.dart';
import 'package:spotube/hooks/use_spotify_infinite_query.dart';
class CategoryQueries { class CategoryQueries {
final list = InfiniteQueryJob<Page<Category>, Map<String, dynamic>, int>( const CategoryQueries();
queryKey: "categories-query",
initialParam: 0, InfiniteQuery<Page<Category>, dynamic, int> list(
getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset, WidgetRef ref, String recommendationMarket) {
getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 16, return useSpotifyInfiniteQuery<Page<Category>, dynamic, int>(
refetchOnExternalDataChange: true, "category-playlists",
task: (queryKey, pageParam, data) async { (pageParam, spotify) async {
final SpotifyApi spotify = data["spotify"] as SpotifyApi;
final String recommendationMarket = data["recommendationMarket"];
final categories = await spotify.categories final categories = await spotify.categories
.list(country: recommendationMarket) .list(country: recommendationMarket)
.getPage(15, pageParam); .getPage(15, pageParam);
return categories; return categories;
}, },
); initialPage: 0,
nextPage: (lastPage, lastPageData) {
final playlistsOf = if (lastPageData.isLast || (lastPageData.items ?? []).length < 15) {
InfiniteQueryJob.withVariableKey<Page<PlaylistSimple>, SpotifyApi, int>( return null;
preQueryKey: "category-playlists", }
initialParam: 0, return lastPageData.nextOffset;
getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset,
getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 6,
task: (queryKey, pageKey, spotify) {
final id = getVariable(queryKey);
return spotify.playlists.getByCategoryId(id).getPage(5, pageKey);
}, },
jsonConfig: JsonConfig<Page<Category>>(
toJson: (page) => page.toJson(),
fromJson: (json) => PageJson.fromJson<Category>(
json,
(json) {
return Category.fromJson((json as Map).castKeyDeep<String>());
},
),
),
ref: ref,
); );
} }
InfiniteQuery<Page<PlaylistSimple>, dynamic, int> playlistsOf(
WidgetRef ref,
String category,
) {
return useSpotifyInfiniteQuery<Page<PlaylistSimple>, dynamic, int>(
"category-playlists/$category",
(pageParam, spotify) async {
final playlists = await spotify.playlists
.getByCategoryId(category)
.getPage(5, pageParam);
return playlists;
},
initialPage: 0,
nextPage: (lastPage, lastPageData) {
if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) {
return null;
}
return lastPageData.nextOffset;
},
ref: ref,
);
}
}

View File

@ -1,24 +1,27 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/lyrics.dart'; import 'package:spotube/models/lyrics.dart';
import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:tuple/tuple.dart';
class LyricsQueries { class LyricsQueries {
final static = QueryJob.withVariableKey<String, Tuple2<Track?, String>>( const LyricsQueries();
preQueryKey: "genius-lyrics-query",
refetchOnExternalDataChange: true, Query<String, dynamic> static(
task: (queryKey, externalData) async { Track? track,
final currentTrack = externalData.item1; String geniusAccessToken,
final geniusAccessToken = externalData.item2; ) {
if (currentTrack == null || getVariable(queryKey).isEmpty) { return useQuery<String, dynamic>(
"genius-lyrics-query/${track?.id}",
() async {
if (track == null) {
return "“Give this player a track to play”\n- S'Challa"; return "“Give this player a track to play”\n- S'Challa";
} }
final lyrics = await ServiceUtils.getLyrics( final lyrics = await ServiceUtils.getLyrics(
currentTrack.name!, track.name!,
currentTrack.artists?.map((s) => s.name).whereNotNull().toList() ?? [], track.artists?.map((s) => s.name).whereNotNull().toList() ?? [],
apiKey: geniusAccessToken, apiKey: geniusAccessToken,
optimizeQuery: true, optimizeQuery: true,
); );
@ -27,18 +30,22 @@ class LyricsQueries {
return lyrics; return lyrics;
}, },
); );
final synced = QueryJob.withVariableKey<SubtitleSimple, SpotubeTrack?>(
preQueryKey: "synced-lyrics",
task: (queryKey, currentTrack) async {
if (currentTrack == null || getVariable(queryKey).isEmpty) {
throw "No track currently";
} }
final timedLyrics = await ServiceUtils.getTimedLyrics(currentTrack); Query<SubtitleSimple, dynamic> synced(
Track? track,
) {
return useQuery<SubtitleSimple, dynamic>(
"synced-lyrics/${track?.id}}",
() async {
if (track == null || track is! SpotubeTrack) {
throw "No track currently";
}
final timedLyrics = await ServiceUtils.getTimedLyrics(track);
if (timedLyrics == null) throw Exception("Unable to find lyrics"); if (timedLyrics == null) throw Exception("Unable to find lyrics");
return timedLyrics; return timedLyrics;
}, },
); );
} }
}

View File

@ -1,48 +1,80 @@
import 'package:catcher/catcher.dart'; import 'package:catcher/catcher.dart';
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/map.dart';
import 'package:spotube/extensions/track.dart';
import 'package:spotube/hooks/use_spotify_infinite_query.dart';
import 'package:spotube/hooks/use_spotify_query.dart';
class PlaylistQueries { class PlaylistQueries {
final doesUserFollow = QueryJob.withVariableKey<bool, SpotifyApi>( const PlaylistQueries();
preQueryKey: "playlist-is-followed",
task: (queryKey, spotify) {
final idMap = getVariable(queryKey).split(":");
return spotify.playlists.followedBy(idMap.first, [idMap.last]).then( Query<bool, dynamic> doesUserFollow(
(value) => value.first, WidgetRef ref,
); String playlistId,
String userId,
) {
return useSpotifyQuery<bool, dynamic>(
"playlist-is-followed/$playlistId/$userId",
(spotify) async {
final result = await spotify.playlists.followedBy(playlistId, [userId]);
return result.first;
}, },
ref: ref,
); );
}
final ofMine = QueryJob<Iterable<PlaylistSimple>, SpotifyApi>( Query<Iterable<PlaylistSimple>, dynamic> ofMine(WidgetRef ref) {
queryKey: "current-user-playlists", return useSpotifyQuery<Iterable<PlaylistSimple>, dynamic>(
task: (_, spotify) { "current-user-playlists",
(spotify) {
return spotify.playlists.me.all(); return spotify.playlists.me.all();
}, },
ref: ref,
); );
}
final tracksOf = QueryJob.withVariableKey<List<Track>, SpotifyApi>( Future<List<Track>> tracksOf(String playlistId, SpotifyApi spotify) {
preQueryKey: "playlist-tracks", if (playlistId == "user-liked-tracks") {
task: (queryKey, spotify) { return spotify.tracks.me.saved.all().then(
final id = getVariable(queryKey);
return id != "user-liked-tracks"
? spotify.playlists.getTracksByPlaylistId(id).all().then(
(value) => value.toList(),
)
: spotify.tracks.me.saved.all().then(
(tracks) => tracks.map((e) => e.track!).toList(), (tracks) => tracks.map((e) => e.track!).toList(),
); );
}, }
return spotify.playlists.getTracksByPlaylistId(playlistId).all().then(
(value) => value.toList(),
); );
}
final featured = InfiniteQueryJob<Page<PlaylistSimple>, SpotifyApi, int>( Query<List<Track>, dynamic> tracksOfQuery(
queryKey: "featured-playlists", WidgetRef ref,
initialParam: 0, String playlistId,
getNextPageParam: (lastPage, lastParam) => ) {
lastPage.items?.length == 5 ? lastPage.nextOffset : null, return useSpotifyQuery<List<Track>, dynamic>(
getPreviousPageParam: (firstPage, firstParam) => firstPage.nextOffset - 6, "playlist-tracks/$playlistId",
refetchOnExternalDataChange: true, (spotify) => tracksOf(playlistId, spotify),
task: (_, pageParam, spotify) async { jsonConfig: playlistId == "user-liked-tracks"
? 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(),
)
: null,
ref: ref,
);
}
InfiniteQuery<Page<PlaylistSimple>, dynamic, int> featured(
WidgetRef ref,
) {
return useSpotifyInfiniteQuery<Page<PlaylistSimple>, dynamic, int>(
"featured-playlists",
(pageParam, spotify) async {
try { try {
final playlists = final playlists =
await spotify.playlists.featured.getPage(5, pageParam); await spotify.playlists.featured.getPage(5, pageParam);
@ -52,5 +84,14 @@ class PlaylistQueries {
rethrow; rethrow;
} }
}, },
initialPage: 0,
nextPage: (lastPage, lastPageData) {
if (lastPageData.isLast || (lastPageData.items ?? []).length < 5) {
return null;
}
return lastPageData.nextOffset;
},
ref: ref,
); );
} }
}

View File

@ -6,12 +6,15 @@ import 'package:spotube/services/queries/playlist.dart';
import 'package:spotube/services/queries/search.dart'; import 'package:spotube/services/queries/search.dart';
import 'package:spotube/services/queries/user.dart'; import 'package:spotube/services/queries/user.dart';
abstract class Queries { class Queries {
static final album = AlbumQueries(); const Queries._();
static final artist = ArtistQueries(); final album = const AlbumQueries();
static final category = CategoryQueries(); final artist = const ArtistQueries();
static final lyrics = LyricsQueries(); final category = const CategoryQueries();
static final playlist = PlaylistQueries(); final lyrics = const LyricsQueries();
static final search = SearchQueries(); final playlist = const PlaylistQueries();
static final user = UserQueries(); final search = const SearchQueries();
final user = const UserQueries();
} }
const useQueries = Queries._();

View File

@ -1,28 +1,36 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:tuple/tuple.dart'; import 'package:spotube/hooks/use_spotify_infinite_query.dart';
class SearchQueries { class SearchQueries {
final get = InfiniteQueryJob.withVariableKey<List<Page>, const SearchQueries();
Tuple2<String, SpotifyApi>, int>( InfiniteQuery<List<Page>, dynamic, int> query(
preQueryKey: "search-query", WidgetRef ref,
refetchOnExternalDataChange: true, String query,
initialParam: 0, SearchType searchType,
enabled: false, ) {
getNextPageParam: (lastPage, lastParam) => return useSpotifyInfiniteQuery<List<Page>, dynamic, int>(
lastPage.isNotEmpty && (lastPage.first.items?.length ?? 0) < 10 "search-query/${searchType.key}",
? null (page, spotify) {
: lastParam + 10, if (query.trim().isEmpty) return [];
getPreviousPageParam: (lastPage, lastParam) => lastParam - 10, final queryString = query;
task: (queryKey, pageParam, variables) {
if (variables.item1.trim().isEmpty) return [];
final queryString = variables.item1;
final spotify = variables.item2;
final searchType = getVariable(queryKey);
return spotify.search.get( return spotify.search.get(
queryString, queryString,
types: [SearchType(searchType)], types: [searchType],
).getPage(10, pageParam); ).getPage(10, page);
},
enabled: false,
ref: ref,
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;
}, },
); );
} }
}

View File

@ -1,12 +1,15 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/hooks/use_spotify_query.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class UserQueries { class UserQueries {
final me = QueryJob<User, SpotifyApi>( const UserQueries();
queryKey: "current-user", Query<User, dynamic> me(WidgetRef ref) {
refetchOnExternalDataChange: true, return useSpotifyQuery<User, dynamic>(
task: (_, spotify) async { "current-user",
(spotify) async {
final me = await spotify.me.get(); final me = await spotify.me.get();
if (me.images == null || me.images?.isEmpty == true) { if (me.images == null || me.images?.isEmpty == true) {
me.images = [ me.images = [
@ -21,5 +24,7 @@ class UserQueries {
} }
return me; return me;
}, },
ref: ref,
); );
} }
}

View File

@ -9,7 +9,6 @@ import audio_service
import audio_session import audio_session
import audioplayers_darwin import audioplayers_darwin
import catcher import catcher
import connectivity_plus_macos
import device_info_plus import device_info_plus
import macos_ui import macos_ui
import metadata_god import metadata_god
@ -27,7 +26,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
CatcherPlugin.register(with: registry.registrar(forPlugin: "CatcherPlugin")) CatcherPlugin.register(with: registry.registrar(forPlugin: "CatcherPlugin"))
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin"))
MetadataGodPlugin.register(with: registry.registrar(forPlugin: "MetadataGodPlugin")) MetadataGodPlugin.register(with: registry.registrar(forPlugin: "MetadataGodPlugin"))

View File

@ -394,54 +394,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
connectivity_plus:
dependency: transitive
description:
name: connectivity_plus
sha256: "3f8fe4e504c2d33696dac671a54909743bc6a902a9bb0902306f7a2aed7e528e"
url: "https://pub.dev"
source: hosted
version: "2.3.9"
connectivity_plus_linux:
dependency: transitive
description:
name: connectivity_plus_linux
sha256: "3caf859d001f10407b8e48134c761483e4495ae38094ffcca97193f6c271f5e2"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
connectivity_plus_macos:
dependency: transitive
description:
name: connectivity_plus_macos
sha256: "488d2de1e47e1224ad486e501b20b088686ba1f4ee9c4420ecbc3b9824f0b920"
url: "https://pub.dev"
source: hosted
version: "1.2.6"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
url: "https://pub.dev"
source: hosted
version: "1.2.4"
connectivity_plus_web:
dependency: transitive
description:
name: connectivity_plus_web
sha256: "81332be1b4baf8898fed17bb4fdef27abb7c6fd990bf98c54fd978478adf2f1a"
url: "https://pub.dev"
source: hosted
version: "1.2.5"
connectivity_plus_windows:
dependency: transitive
description:
name: connectivity_plus_windows
sha256: "535b0404b4d5605c4dd8453d67e5d6d2ea0dd36e3b477f50f31af51b0aeab9dd"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -581,18 +533,20 @@ packages:
fl_query: fl_query:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_query path: "packages/fl_query"
sha256: "9d55b025d672aaf27766923817a7b458b5fb78631c83e6ce958faaef8c9ac61d" ref: new-architecture
url: "https://pub.dev" resolved-ref: "0c819d4e11572d592b5334280b8b4f2657f21459"
source: hosted url: "https://github.com/KRTirtho/fl-query.git"
source: git
version: "0.3.1" version: "0.3.1"
fl_query_hooks: fl_query_hooks:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_query_hooks path: "packages/fl_query_hooks"
sha256: "052b50587794ca6e0d0d4cb6591efcd91308c299a3779087632754a09282562e" ref: new-architecture
url: "https://pub.dev" resolved-ref: "0c819d4e11572d592b5334280b8b4f2657f21459"
source: hosted url: "https://github.com/KRTirtho/fl-query.git"
source: git
version: "0.3.1" version: "0.3.1"
fluent_ui: fluent_ui:
dependency: "direct main" dependency: "direct main"
@ -691,10 +645,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_hooks name: flutter_hooks
sha256: "2b202559a4ed3656bbb7aae9d8b335fb0037b23acc7ae3f377d1ba0b95c21aec" sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.18.5+1" version: "0.18.6"
flutter_inappwebview: flutter_inappwebview:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1062,14 +1016,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
nm: mutex:
dependency: transitive dependency: transitive
description: description:
name: nm name: mutex
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" sha256: "03116a4e46282a671b46c12de649d72c0ed18188ffe12a8d0fc63e83f4ad88f4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "3.0.1"
oauth2: oauth2:
dependency: transitive dependency: transitive
description: description:
@ -1776,10 +1730,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: window_manager name: window_manager
sha256: "5a7bda81b8b8e5feb733d07f4bd174e64dfb412031aff5314a375118e42b498e" sha256: "492806c69879f0d28e95472bbe5e8d5940ac8c6e99cc07052fe14946974555ba"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.8" version: "0.3.1"
window_size: window_size:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1822,5 +1776,5 @@ packages:
source: hosted source: hosted
version: "1.12.3" version: "1.12.3"
sdks: sdks:
dart: ">=2.19.0 <4.0.0" dart: ">=2.19.0 <3.0.0"
flutter: ">=3.7.0" flutter: ">=3.7.0"

View File

@ -27,8 +27,16 @@ dependencies:
cupertino_icons: ^1.0.5 cupertino_icons: ^1.0.5
dbus: ^0.7.8 dbus: ^0.7.8
file_picker: ^5.2.2 file_picker: ^5.2.2
fl_query: ^0.3.1 fl_query:
fl_query_hooks: ^0.3.1 git:
url: https://github.com/KRTirtho/fl-query.git
path: packages/fl_query
ref: new-architecture
fl_query_hooks:
git:
url: https://github.com/KRTirtho/fl-query.git
path: packages/fl_query_hooks
ref: new-architecture
fluent_ui: ^4.3.0 fluent_ui: ^4.3.0
fluentui_system_icons: ^1.1.189 fluentui_system_icons: ^1.1.189
flutter: flutter:
@ -77,7 +85,7 @@ dependencies:
uuid: ^3.0.7 uuid: ^3.0.7
version: ^3.0.2 version: ^3.0.2
visibility_detector: ^0.3.3 visibility_detector: ^0.3.3
window_manager: 0.2.8 window_manager: ^0.3.1
window_size: window_size:
git: git:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git
@ -97,6 +105,11 @@ dev_dependencies:
dependency_overrides: dependency_overrides:
package_info_plus: ^3.0.2 package_info_plus: ^3.0.2
fl_query:
git:
url: https://github.com/KRTirtho/fl-query.git
path: packages/fl_query
ref: new-architecture
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@ -8,7 +8,6 @@
#include <audioplayers_windows/audioplayers_windows_plugin.h> #include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <catcher/catcher_plugin.h> #include <catcher/catcher_plugin.h>
#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h>
#include <metadata_god/metadata_god_plugin_c_api.h> #include <metadata_god/metadata_god_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h> #include <screen_retriever/screen_retriever_plugin.h>
@ -21,8 +20,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
CatcherPluginRegisterWithRegistrar( CatcherPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CatcherPlugin")); registry->GetRegistrarForPlugin("CatcherPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
MetadataGodPluginCApiRegisterWithRegistrar( MetadataGodPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MetadataGodPluginCApi")); registry->GetRegistrarForPlugin("MetadataGodPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(

View File

@ -5,7 +5,6 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows audioplayers_windows
catcher catcher
connectivity_plus_windows
metadata_god metadata_god
permission_handler_windows permission_handler_windows
screen_retriever screen_retriever