fix(auth): refresh access token timer not working

refactor: use fl_query for spotify API instead of riverpod
This commit is contained in:
Kingkor Roy Tirtho 2022-10-23 16:06:03 +06:00
parent b5144dfa26
commit b3ac5ca3bb
17 changed files with 616 additions and 375 deletions

View File

@ -1,4 +1,5 @@
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';
@ -57,9 +58,14 @@ class AlbumView extends HookConsumerWidget {
final SpotifyApi spotify = ref.watch(spotifyProvider); final SpotifyApi spotify = ref.watch(spotifyProvider);
final Auth auth = ref.watch(authProvider); final Auth auth = ref.watch(authProvider);
final tracksSnapshot = ref.watch(albumTracksQuery(album.id!)); final tracksSnapshot = useQuery(
final albumSavedSnapshot = job: albumTracksQueryJob(album.id!),
ref.watch(albumIsSavedForCurrentUserQuery(album.id!)); externalData: spotify,
);
final albumSavedSnapshot = useQuery(
job: albumIsSavedForCurrentUserQueryJob(album.id!),
externalData: spotify,
);
final albumArt = useMemoized( final albumArt = useMemoized(
() => TypeConversionUtils.image_X_UrlString( () => TypeConversionUtils.image_X_UrlString(
@ -82,11 +88,11 @@ class AlbumView extends HookConsumerWidget {
routePath: "/album/${album.id}", routePath: "/album/${album.id}",
bottomSpace: breakpoint.isLessThanOrEqualTo(Breakpoints.md), bottomSpace: breakpoint.isLessThanOrEqualTo(Breakpoints.md),
onPlay: ([track]) { onPlay: ([track]) {
if (tracksSnapshot.asData?.value != null) { if (tracksSnapshot.hasData) {
if (!isAlbumPlaying) { if (!isAlbumPlaying) {
playPlaylist( playPlaylist(
playback, playback,
tracksSnapshot.asData!.value tracksSnapshot.data!
.map((track) => .map((track) =>
TypeConversionUtils.simpleTrack_X_Track(track, album)) TypeConversionUtils.simpleTrack_X_Track(track, album))
.toList(), .toList(),
@ -95,7 +101,7 @@ class AlbumView extends HookConsumerWidget {
} else if (isAlbumPlaying && track != null) { } else if (isAlbumPlaying && track != null) {
playPlaylist( playPlaylist(
playback, playback,
tracksSnapshot.asData!.value tracksSnapshot.data!
.map((track) => .map((track) =>
TypeConversionUtils.simpleTrack_X_Track(track, album)) TypeConversionUtils.simpleTrack_X_Track(track, album))
.toList(), .toList(),
@ -112,35 +118,7 @@ class AlbumView extends HookConsumerWidget {
ClipboardData(text: "https://open.spotify.com/album/${album.id}"), ClipboardData(text: "https://open.spotify.com/album/${album.id}"),
); );
}, },
heartBtn: auth.isLoggedIn heartBtn: AlbumHeartButton(album: album),
? albumSavedSnapshot.when(
data: (isSaved) {
return HeartButton(
isLiked: isSaved,
onPressed: () {
(isSaved
? spotify.me.removeAlbums(
[album.id!],
)
: spotify.me.saveAlbums(
[album.id!],
))
.whenComplete(() {
ref.refresh(
albumIsSavedForCurrentUserQuery(
album.id!,
),
);
QueryBowl.of(context).refetchQueries(
[currentUserAlbumsQueryJob.queryKey],
);
});
},
);
},
error: (error, _) => Text("Error $error"),
loading: () => const CircularProgressIndicator())
: null,
); );
} }
} }

View File

@ -1,3 +1,5 @@
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';
@ -49,20 +51,28 @@ class ArtistProfile extends HookConsumerWidget {
final Playback playback = ref.watch(playbackProvider); final Playback playback = ref.watch(playbackProvider);
final artistsSnapshot = ref.watch(artistProfileQuery(artistId));
final isFollowingSnapshot =
ref.watch(currentUserFollowsArtistQuery(artistId));
final topTracksSnapshot = ref.watch(artistTopTracksQuery(artistId));
final relatedArtists = ref.watch(artistRelatedArtistsQuery(artistId));
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
leading: BackButton(), leading: BackButton(),
), ),
body: artistsSnapshot.when<Widget>( body: HookBuilder(
data: (data) { builder: (context) {
final artistsQuery = useQuery(
job: artistProfileQueryJob(artistId),
externalData: spotify,
);
if (artistsQuery.isLoading || !artistsQuery.hasData) {
return const ShimmerArtistProfile();
} else if (artistsQuery.hasError) {
return Center(
child: Text(artistsQuery.error.toString()),
);
}
final data = artistsQuery.data!;
return SingleChildScrollView( return SingleChildScrollView(
controller: parentScrollController, controller: parentScrollController,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
@ -115,42 +125,57 @@ class ArtistProfile extends HookConsumerWidget {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
isFollowingSnapshot.when( HookBuilder(
data: (isFollowing) { builder: (context) {
return OutlinedButton( final isFollowingQuery = useQuery(
onPressed: () async { job: currentUserFollowsArtistQueryJob(
try { artistId),
isFollowing externalData: spotify,
? await spotify.me.unfollow( );
FollowingType.artist,
[artistId], if (isFollowingQuery.isLoading ||
) !isFollowingQuery.hasData) {
: await spotify.me.follow( return const SizedBox(
FollowingType.artist, height: 20,
[artistId], width: 20,
); child: CircularProgressIndicator(),
} catch (e, stack) {
logger.e(
"FollowButton.onPressed",
e,
stack,
);
} finally {
ref.refresh(
currentUserFollowsArtistQuery(
artistId),
);
}
},
child: Text(
isFollowing ? "Following" : "Follow",
),
); );
}, }
error: (error, stackTrace) => Container(),
loading: () => return OutlinedButton(
const CircularProgressIndicator onPressed: () async {
.adaptive()), try {
isFollowingQuery.data!
? await spotify.me.unfollow(
FollowingType.artist,
[artistId],
)
: await spotify.me.follow(
FollowingType.artist,
[artistId],
);
} catch (e, stack) {
logger.e(
"FollowButton.onPressed",
e,
stack,
);
} finally {
QueryBowl.of(context).refetchQueries([
currentUserFollowsArtistQueryJob(
artistId)
.queryKey,
]);
}
},
child: Text(
isFollowingQuery.data!
? "Following"
: "Follow",
),
);
},
),
IconButton( IconButton(
icon: const Icon(Icons.share_rounded), icon: const Icon(Icons.share_rounded),
onPressed: () { onPressed: () {
@ -180,8 +205,23 @@ class ArtistProfile extends HookConsumerWidget {
], ],
), ),
const SizedBox(height: 50), const SizedBox(height: 50),
topTracksSnapshot.when( HookBuilder(
data: (topTracks) { builder: (context) {
final topTracksQuery = useQuery(
job: artistTopTracksQueryJob(artistId),
externalData: spotify,
);
if (topTracksQuery.isLoading || !topTracksQuery.hasData) {
return const CircularProgressIndicator.adaptive();
} else if (topTracksQuery.hasError) {
return Center(
child: Text(topTracksQuery.error.toString()),
);
}
final topTracks = topTracksQuery.data!;
final isPlaylistPlaying = final isPlaylistPlaying =
playback.playlist?.id == data.id; playback.playlist?.id == data.id;
playPlaylist(List<Track> tracks, playPlaylist(List<Track> tracks,
@ -248,10 +288,6 @@ class ArtistProfile extends HookConsumerWidget {
}), }),
]); ]);
}, },
error: (error, stack) =>
Text("Failed to find top tracks $error"),
loading: () => const Center(
child: CircularProgressIndicator.adaptive()),
), ),
const SizedBox(height: 50), const SizedBox(height: 50),
Text( Text(
@ -266,28 +302,36 @@ class ArtistProfile extends HookConsumerWidget {
style: Theme.of(context).textTheme.headline4, style: Theme.of(context).textTheme.headline4,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
relatedArtists.when( HookBuilder(
data: (artists) { builder: (context) {
final relatedArtists = useQuery(
job: artistRelatedArtistsQueryJob(artistId),
externalData: spotify,
);
if (relatedArtists.isLoading || !relatedArtists.hasData) {
return const CircularProgressIndicator.adaptive();
} else if (relatedArtists.hasError) {
return Center(
child: Text(relatedArtists.error.toString()),
);
}
return Center( return Center(
child: Wrap( child: Wrap(
spacing: 20, spacing: 20,
runSpacing: 20, runSpacing: 20,
children: artists children: relatedArtists.data!
.map((artist) => ArtistCard(artist)) .map((artist) => ArtistCard(artist))
.toList(), .toList(),
), ),
); );
}, },
error: (error, stackTrack) =>
Text("Failed to get Artist albums $error"),
loading: () => const CircularProgressIndicator.adaptive(),
), ),
], ],
), ),
); );
}, },
error: (_, __) => const Text("Life's miserable"),
loading: () => const ShimmerArtistProfile(),
), ),
), ),
); );

View File

@ -1,5 +1,6 @@
import 'package:badges/badges.dart'; import 'package:badges/badges.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.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';
@ -9,6 +10,7 @@ import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/models/sideBarTiles.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -42,7 +44,7 @@ class Sidebar extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final breakpoints = useBreakpoints(); final breakpoints = useBreakpoints();
final extended = useState(false); final extended = useState(false);
final meSnapshot = ref.watch(currentUserQuery);
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
final downloadCount = ref.watch( final downloadCount = ref.watch(
downloaderProvider.select((s) => s.currentlyRunning), downloaderProvider.select((s) => s.currentlyRunning),
@ -161,15 +163,28 @@ class Sidebar extends HookConsumerWidget {
), ),
SizedBox( SizedBox(
width: extended.value ? 256 : 80, width: extended.value ? 256 : 80,
child: Builder( child: HookBuilder(
builder: (context) { builder: (context) {
final data = meSnapshot.asData?.value; final me = useQuery(
job: currentUserQueryJob,
externalData: ref.watch(spotifyProvider),
);
final data = me.data;
final avatarImg = TypeConversionUtils.image_X_UrlString( final avatarImg = TypeConversionUtils.image_X_UrlString(
data?.images, data?.images,
index: (data?.images?.length ?? 1) - 1, index: (data?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
); );
useEffect(() {
if (auth.isLoggedIn && !me.hasData) {
me.setExternalData(ref.read(spotifyProvider));
me.refetch();
}
return;
}, [auth.isLoggedIn, me.hasData]);
if (extended.value) { if (extended.value) {
return Padding( return Padding(
padding: const EdgeInsets.all(16).copyWith(left: 0), padding: const EdgeInsets.all(16).copyWith(left: 0),

View File

@ -1,3 +1,4 @@
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:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -5,7 +6,9 @@ import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
class Lyrics extends HookConsumerWidget { class Lyrics extends HookConsumerWidget {
final Color? titleBarForegroundColor; final Color? titleBarForegroundColor;
@ -17,7 +20,13 @@ class Lyrics extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final geniusLyricsSnapshot = ref.watch(geniusLyricsQuery); final geniusLyricsQuery = useQuery(
job: geniusLyricsQueryJob,
externalData: Tuple2(
playback.track,
ref.watch(userPreferencesProvider).geniusAccessToken,
),
);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
@ -46,8 +55,18 @@ class Lyrics extends HookConsumerWidget {
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: geniusLyricsSnapshot.when( child: Builder(
data: (lyrics) { builder: (context) {
if (geniusLyricsQuery.isLoading) {
return const ShimmerLyrics();
} else if (geniusLyricsQuery.hasError) {
return Text(
"Sorry, no Lyrics were found for `${playback.track?.name}` :'(\n${geniusLyricsQuery.error.toString()}",
);
}
final lyrics = geniusLyricsQuery.data;
return Text( return Text(
lyrics == null && playback.track == null lyrics == null && playback.track == null
? "No Track being played currently" ? "No Track being played currently"
@ -56,9 +75,6 @@ class Lyrics extends HookConsumerWidget {
?.copyWith(color: textTheme.headline1?.color), ?.copyWith(color: textTheme.headline1?.color),
); );
}, },
error: (error, __) => Text(
"Sorry, no Lyrics were found for `${playback.track?.name}` :'("),
loading: () => const ShimmerLyrics(),
), ),
), ),
), ),

View File

@ -1,5 +1,6 @@
import 'dart:ui'; import 'dart:ui';
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';
@ -30,14 +31,17 @@ class SyncedLyrics extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final timedLyricsSnapshot = ref.watch(rentanadviserLyricsQuery); Playback playback = ref.watch(playbackProvider);
final timedLyricsQuery = useQuery(
job: rentanadviserLyricsQueryJob,
externalData: playback.track,
);
final lyricDelay = ref.watch(lyricDelayState); final lyricDelay = ref.watch(lyricDelayState);
Playback playback = ref.watch(playbackProvider);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final controller = useAutoScrollController(); final controller = useAutoScrollController();
final failed = useState(false); final failed = useState(false);
final lyricValue = timedLyricsSnapshot.asData?.value; final lyricValue = timedLyricsQuery.data;
final lyricsMap = useMemoized( final lyricsMap = useMemoized(
() => () =>
lyricValue?.lyrics lyricValue?.lyrics

View File

@ -6,13 +6,9 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/components/Player/PlayerQueue.dart'; import 'package:spotube/components/Player/PlayerQueue.dart';
import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/components/Shared/HeartButton.dart';
import 'package:spotube/hooks/useForceUpdate.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class PlayerActions extends HookConsumerWidget { class PlayerActions extends HookConsumerWidget {
@ -27,11 +23,8 @@ class PlayerActions extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final SpotifyApi spotifyApi = ref.watch(spotifyProvider);
final Playback playback = ref.watch(playbackProvider); final Playback playback = ref.watch(playbackProvider);
final Auth auth = ref.watch(authProvider);
final downloader = ref.watch(downloaderProvider); final downloader = ref.watch(downloaderProvider);
final update = useForceUpdate();
final isInQueue = final isInQueue =
downloader.inQueue.any((element) => element.id == playback.track?.id); downloader.inQueue.any((element) => element.id == playback.track?.id);
final localTracks = ref.watch(localTracksProvider).value; final localTracks = ref.watch(localTracksProvider).value;
@ -96,35 +89,7 @@ class PlayerActions extends HookConsumerWidget {
? () => downloader.addToQueue(playback.track!) ? () => downloader.addToQueue(playback.track!)
: null, : null,
), ),
if (auth.isLoggedIn) if (playback.track != null) TrackHeartButton(track: playback.track!),
FutureBuilder<bool>(
future: playback.track?.id != null
? spotifyApi.tracks.me.containsOne(playback.track!.id!)
: Future.value(false),
initialData: false,
builder: (context, snapshot) {
bool isLiked = snapshot.data ?? false;
return HeartButton(
isLiked: isLiked,
onPressed: () async {
try {
if (playback.track?.id == null) return;
isLiked
? await spotifyApi.tracks.me
.removeOne(playback.track!.id!)
: await spotifyApi.tracks.me
.saveOne(playback.track!.id!);
} catch (e, stack) {
logger.e("FavoriteButton.onPressed", e, stack);
} finally {
update();
ref.refresh(currentUserSavedTracksQuery);
ref.refresh(
playlistTracksQuery("user-liked-tracks"),
);
}
});
}),
], ],
); );
} }

View File

@ -1,6 +1,4 @@
import 'dart:convert'; import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:fl_query/fl_query.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';
@ -8,10 +6,8 @@ import 'package:spotube/components/Shared/HeartButton.dart';
import 'package:spotube/components/Shared/TrackCollectionView.dart'; import 'package:spotube/components/Shared/TrackCollectionView.dart';
import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -59,15 +55,18 @@ class PlaylistView extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final Auth auth = ref.watch(authProvider);
SpotifyApi spotify = ref.watch(spotifyProvider); SpotifyApi spotify = ref.watch(spotifyProvider);
final isPlaylistPlaying = final isPlaylistPlaying =
playback.playlist?.id != null && playback.playlist?.id == playlist.id; playback.playlist?.id != null && playback.playlist?.id == playlist.id;
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final meSnapshot = ref.watch(currentUserQuery); final meSnapshot =
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!)); useQuery(job: currentUserQueryJob, externalData: spotify);
final tracksSnapshot = useQuery(
job: playlistTracksQueryJob(playlist.id!),
externalData: spotify,
);
final titleImage = useMemoized( final titleImage = useMemoized(
() => TypeConversionUtils.image_X_UrlString( () => TypeConversionUtils.image_X_UrlString(
@ -76,11 +75,6 @@ class PlaylistView extends HookConsumerWidget {
), ),
[playlist.images]); [playlist.images]);
final color = usePaletteGenerator(
context,
titleImage,
).dominantColor;
return TrackCollectionView( return TrackCollectionView(
id: playlist.id!, id: playlist.id!,
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
@ -89,15 +83,15 @@ class PlaylistView extends HookConsumerWidget {
tracksSnapshot: tracksSnapshot, tracksSnapshot: tracksSnapshot,
description: playlist.description, description: playlist.description,
isOwned: playlist.owner?.id != null && isOwned: playlist.owner?.id != null &&
playlist.owner!.id == meSnapshot.asData?.value.id, playlist.owner!.id == meSnapshot.data?.id,
onPlay: ([track]) { onPlay: ([track]) {
if (tracksSnapshot.asData?.value != null) { if (tracksSnapshot.hasData) {
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
playPlaylist(playback, tracksSnapshot.asData!.value, ref); playPlaylist(playback, tracksSnapshot.data!, ref);
} else if (isPlaylistPlaying && track != null) { } else if (isPlaylistPlaying && track != null) {
playPlaylist( playPlaylist(
playback, playback,
tracksSnapshot.asData!.value, tracksSnapshot.data!,
ref, ref,
currentTrack: track, currentTrack: track,
); );
@ -126,48 +120,7 @@ class PlaylistView extends HookConsumerWidget {
); );
}); });
}, },
heartBtn: (auth.isLoggedIn && playlist.id != "user-liked-tracks" heartBtn: PlaylistHeartButton(playlist: playlist),
? meSnapshot.when(
data: (me) {
final query = playlistIsFollowedQuery(
jsonEncode({"playlistId": playlist.id, "userId": me.id!}));
final followingSnapshot = ref.watch(query);
return followingSnapshot.when(
data: (isFollowing) {
return HeartButton(
isLiked: isFollowing,
color: color?.titleTextColor,
icon: playlist.owner?.id != null &&
me.id == playlist.owner?.id
? Icons.delete_outline_rounded
: null,
onPressed: () async {
try {
isFollowing
? await spotify.playlists
.unfollowPlaylist(playlist.id!)
: await spotify.playlists
.followPlaylist(playlist.id!);
} catch (e, stack) {
logger.e("FollowButton.onPressed", e, stack);
} finally {
ref.refresh(query);
QueryBowl.of(context).refetchQueries([
currentUserPlaylistsQueryJob.queryKey,
]);
}
},
);
},
error: (error, _) => Text("Error $error"),
loading: () => const CircularProgressIndicator(),
);
},
error: (error, _) => Text("Error $error"),
loading: () => const CircularProgressIndicator(),
)
: null),
); );
} }
} }

View File

@ -1,3 +1,4 @@
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';
@ -12,9 +13,11 @@ import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.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';
final searchTermStateProvider = StateProvider<String>((ref) => ""); final searchTermStateProvider = StateProvider<String>((ref) => "");
@ -24,18 +27,26 @@ class Search extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final Auth auth = ref.watch(authProvider); final Auth auth = ref.watch(authProvider);
final searchTerm = ref.watch(searchTermStateProvider);
final controller =
useTextEditingController(text: ref.read(searchTermStateProvider));
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 searchMutation = useMutation(
job: searchMutationJob,
);
if (auth.isAnonymous) { if (auth.isAnonymous) {
return const AnonymousFallback(); return const AnonymousFallback();
} }
final searchSnapshot = ref.watch(searchQuery(searchTerm));
final getVariables = useCallback(
() => Tuple2(
ref.read(searchTermStateProvider),
ref.read(spotifyProvider),
),
[],
);
return SafeArea( return SafeArea(
child: Material( child: Material(
@ -49,14 +60,15 @@ class Search extends HookConsumerWidget {
), ),
color: Theme.of(context).backgroundColor, color: Theme.of(context).backgroundColor,
child: TextField( child: TextField(
controller: controller, onChanged: (value) {
ref.read(searchTermStateProvider.notifier).state = value;
},
decoration: InputDecoration( decoration: InputDecoration(
isDense: true, isDense: true,
suffix: ElevatedButton( suffix: ElevatedButton(
child: const Icon(Icons.search_rounded), child: const Icon(Icons.search_rounded),
onPressed: () { onPressed: () {
ref.read(searchTermStateProvider.notifier).state = searchMutation.mutate(getVariables());
controller.value.text;
}, },
), ),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
@ -67,19 +79,26 @@ class Search extends HookConsumerWidget {
hintText: "Search...", hintText: "Search...",
), ),
onSubmitted: (value) { onSubmitted: (value) {
ref.read(searchTermStateProvider.notifier).state = searchMutation.mutate(getVariables());
controller.value.text;
}, },
), ),
), ),
searchSnapshot.when( HookBuilder(
data: (data) { builder: (context) {
if (searchMutation.hasError && searchMutation.isError) {
return Text("Alas! Error=${searchMutation.error}");
}
if (searchMutation.isLoading) {
return const CircularProgressIndicator();
}
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
List<AlbumSimple> albums = []; List<AlbumSimple> albums = [];
List<Artist> artists = []; List<Artist> artists = [];
List<Track> tracks = []; List<Track> tracks = [];
List<PlaylistSimple> playlists = []; List<PlaylistSimple> playlists = [];
for (MapEntry<int, Page> page in data.asMap().entries) { for (MapEntry<int, Page> page
in (searchMutation.data ?? []).asMap().entries) {
for (var item in page.value.items ?? []) { for (var item in page.value.items ?? []) {
if (item is AlbumSimple) { if (item is AlbumSimple) {
albums.add(item); albums.add(item);
@ -241,8 +260,6 @@ class Search extends HookConsumerWidget {
), ),
); );
}, },
error: (error, __) => Text("Error $error"),
loading: () => const CircularProgressIndicator(),
) )
], ],
), ),

View File

@ -32,7 +32,7 @@ class Action extends StatelessWidget {
} }
return TextButton.icon( return TextButton.icon(
style: TextButton.styleFrom( style: TextButton.styleFrom(
primary: Theme.of(context).textTheme.bodyMedium?.color, foregroundColor: Theme.of(context).textTheme.bodyMedium?.color,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
), ),
icon: icon, icon: icon,

View File

@ -1,8 +1,19 @@
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
class HeartButton extends StatelessWidget { class HeartButton extends ConsumerWidget {
final bool isLiked; final bool isLiked;
final void Function() onPressed; final void Function()? onPressed;
final IconData? icon; final IconData? icon;
final Color? color; final Color? color;
const HeartButton({ const HeartButton({
@ -14,16 +25,219 @@ class HeartButton extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider);
if (!auth.isLoggedIn) return Container();
return IconButton( return IconButton(
icon: Icon( icon: Icon(
icon ?? icon ??
(!isLiked (!isLiked
? Icons.favorite_outline_rounded ? Icons.favorite_outline_rounded
: Icons.favorite_rounded), : Icons.favorite_rounded),
color: isLiked ? Theme.of(context).primaryColor : color, color: isLiked ? Colors.pink : color,
), ),
onPressed: onPressed, onPressed: onPressed,
); );
} }
} }
Tuple3<bool, Mutation<bool, Tuple2<SpotifyApi, bool>>, Query<User, SpotifyApi>>
useTrackToggleLike(Track track, WidgetRef ref) {
final me = useQuery(
job: currentUserQueryJob, externalData: ref.watch(spotifyProvider));
final savedTracks = useQuery(
job: playlistTracksQueryJob("user-liked-tracks"),
externalData: ref.watch(spotifyProvider),
);
final isLiked =
savedTracks.data?.map((track) => track.id).contains(track.id) ?? false;
final mounted = useIsMounted();
final toggleTrackLike = useMutation<bool, Tuple2<SpotifyApi, bool>>(
job: toggleFavoriteTrackMutationJob(track.id!),
onMutate: (variable) {
savedTracks.setQueryData(
(oldData) {
if (!variable.item2) {
return [...(oldData ?? []), track];
}
return oldData
?.where(
(element) => element.id != track.id,
)
.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() ??
[];
},
);
},
);
return Tuple3(isLiked, toggleTrackLike, me);
}
class TrackHeartButton extends HookConsumerWidget {
final Track track;
const TrackHeartButton({
Key? key,
required this.track,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final savedTracks = useQuery(
job: playlistTracksQueryJob("user-liked-tracks"),
externalData: ref.watch(spotifyProvider),
);
final toggler = useTrackToggleLike(track, ref);
if (toggler.item3.isLoading || !toggler.item3.hasData) {
return const CircularProgressIndicator();
}
return HeartButton(
isLiked: toggler.item1,
onPressed: savedTracks.hasData
? () {
toggler.item2.mutate(
Tuple2(ref.read(spotifyProvider), toggler.item1),
);
}
: null,
);
}
}
class PlaylistHeartButton extends HookConsumerWidget {
final PlaylistSimple playlist;
const PlaylistHeartButton({
required this.playlist,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final me = useQuery(
job: currentUserQueryJob,
externalData: ref.watch(spotifyProvider),
);
final job = playlistIsFollowedQueryJob("${playlist.id}:${me.data?.id}");
final isLikedQuery = useQuery(
job: job,
externalData: ref.watch(spotifyProvider),
);
final togglePlaylistLike = useMutation<bool, Tuple2<SpotifyApi, bool>>(
job: toggleFavoritePlaylistMutationJob(playlist.id!),
onData: (payload, variables, queryContext) {
isLikedQuery.refetch();
QueryBowl.of(context)
.getQuery(currentUserPlaylistsQueryJob.queryKey)
?.refetch();
},
);
final titleImage = useMemoized(
() => TypeConversionUtils.image_X_UrlString(
playlist.images,
placeholder: ImagePlaceholder.collection,
),
[playlist.images]);
final color = usePaletteGenerator(
context,
titleImage,
).dominantColor;
if (me.isLoading || !me.hasData) return const CircularProgressIndicator();
return HeartButton(
isLiked: isLikedQuery.data ?? false,
color: color?.titleTextColor,
onPressed: isLikedQuery.hasData
? () {
togglePlaylistLike.mutate(
Tuple2(
ref.read(spotifyProvider),
isLikedQuery.data!,
),
);
}
: null,
);
}
}
class AlbumHeartButton extends HookConsumerWidget {
final AlbumSimple album;
const AlbumHeartButton({
required this.album,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider);
final me = useQuery(
job: currentUserQueryJob,
externalData: spotify,
);
final albumIsSaved = useQuery(
job: albumIsSavedForCurrentUserQueryJob(album.id!),
externalData: spotify,
);
final isLiked = albumIsSaved.data ?? false;
final toggleAlbumLike = useMutation<bool, Tuple2<SpotifyApi, bool>>(
job: toggleFavoriteAlbumMutationJob(album.id!),
onData: (payload, variables, queryContext) {
albumIsSaved.refetch();
QueryBowl.of(context)
.getQuery(currentUserAlbumsQueryJob.queryKey)
?.refetch();
},
);
if (me.isLoading || !me.hasData) return const CircularProgressIndicator();
return HeartButton(
isLiked: isLiked,
onPressed: albumIsSaved.hasData
? () {
toggleAlbumLike
.mutate(Tuple2(ref.read(spotifyProvider), isLiked));
}
: null,
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:fl_query/fl_query.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';
@ -13,12 +14,12 @@ import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class TrackCollectionView extends HookConsumerWidget { class TrackCollectionView<T> extends HookConsumerWidget {
final logger = getLogger(TrackCollectionView); final logger = getLogger(TrackCollectionView);
final String id; final String id;
final String title; final String title;
final String? description; final String? description;
final AsyncValue<List<TrackSimple>> tracksSnapshot; final Query<List<TrackSimple>, T> tracksSnapshot;
final String titleImage; final String titleImage;
final bool isPlaying; final bool isPlaying;
final void Function([Track? currentTrack]) onPlay; final void Function([Track? currentTrack]) onPlay;
@ -78,7 +79,7 @@ class TrackCollectionView extends HookConsumerWidget {
const CircleBorder(), const CircleBorder(),
), ),
), ),
onPressed: tracksSnapshot.asData?.value != null ? onPlay : null, onPressed: tracksSnapshot.data != null ? onPlay : null,
child: Icon( child: Icon(
isPlaying ? Icons.stop_rounded : Icons.play_arrow_rounded, isPlaying ? Icons.stop_rounded : Icons.play_arrow_rounded,
color: Theme.of(context).backgroundColor, color: Theme.of(context).backgroundColor,
@ -175,7 +176,14 @@ class TrackCollectionView extends HookConsumerWidget {
const BoxConstraints(maxHeight: 200), const BoxConstraints(maxHeight: 200),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: UniversalImage(path: titleImage), child: UniversalImage(
path: titleImage,
placeholder: (context, url) {
return const UniversalImage(
path: "assets/album-placeholder.png",
);
},
),
), ),
), ),
Column( Column(
@ -220,14 +228,25 @@ class TrackCollectionView extends HookConsumerWidget {
); );
}), }),
), ),
tracksSnapshot.when( HookBuilder(
data: (tracks) { builder: (context) {
if (tracksSnapshot.isLoading || !tracksSnapshot.hasData) {
return const ShimmerTrackTile();
} else if (tracksSnapshot.hasError &&
tracksSnapshot.isError) {
return SliverToBoxAdapter(
child: Text("Error ${tracksSnapshot.error}"));
}
final tracks = tracksSnapshot.data!;
return TracksTableView( return TracksTableView(
tracks is! List<Track> tracks is! List<Track>
? tracks ? tracks
.map((track) => .map(
TypeConversionUtils.simpleTrack_X_Track( (track) =>
track, album!)) TypeConversionUtils.simpleTrack_X_Track(
track, album!),
)
.toList() .toList()
: tracks, : tracks,
onTrackPlayButtonPressed: onPlay, onTrackPlayButtonPressed: onPlay,
@ -235,10 +254,7 @@ class TrackCollectionView extends HookConsumerWidget {
userPlaylist: isOwned, userPlaylist: isOwned,
); );
}, },
error: (error, _) => )
SliverToBoxAdapter(child: Text("Error $error")),
loading: () => const ShimmerTrackTile(),
),
], ],
)), )),
); );

View File

@ -4,16 +4,16 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Image; import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/components/Shared/AdaptivePopupMenuButton.dart'; import 'package:spotube/components/Shared/AdaptivePopupMenuButton.dart';
import 'package:spotube/components/Shared/HeartButton.dart';
import 'package:spotube/components/Shared/LinkText.dart'; import 'package:spotube/components/Shared/LinkText.dart';
import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/useForceUpdate.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.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 Playback playback; final Playback playback;
@ -60,28 +60,6 @@ class TrackTile extends HookConsumerWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final update = useForceUpdate();
final savedTracksSnapshot = ref.watch(currentUserSavedTracksQuery);
final isSaved = savedTracksSnapshot.asData?.value.any(
(e) => track.value.id == e.id,
) ??
false;
final actionFavorite = useCallback((bool isLiked) async {
try {
isLiked
? await spotify.tracks.me.removeOne(track.value.id!)
: await spotify.tracks.me.saveOne(track.value.id!);
} catch (e, stack) {
logger.e("FavoriteButton.onPressed", e, stack);
} finally {
update();
ref.refresh(currentUserSavedTracksQuery);
ref.refresh(playlistTracksQuery("user-liked-tracks"));
}
}, [track.value.id, spotify]);
final actionRemoveFromPlaylist = useCallback(() async { final actionRemoveFromPlaylist = useCallback(() async {
if (playlistId == null) return; if (playlistId == null) return;
@ -188,6 +166,8 @@ class TrackTile extends HookConsumerWidget {
index: track.value.album?.images?.length == 1 ? 0 : 2, index: track.value.album?.images?.length == 1 ? 0 : 2,
); );
final toggler = useTrackToggleLike(track.value, ref);
return AnimatedContainer( return AnimatedContainer(
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -291,14 +271,17 @@ class TrackTile extends HookConsumerWidget {
if (!isReallyLocal) if (!isReallyLocal)
AdaptiveActions( AdaptiveActions(
actions: [ actions: [
if (auth.isLoggedIn) if (toggler.item3.hasData)
Action( Action(
icon: Icon(isSaved icon: toggler.item1
? Icons.favorite_rounded ? const Icon(
: Icons.favorite_border_rounded), Icons.favorite_rounded,
color: Colors.pink,
)
: const Icon(Icons.favorite_border_rounded),
text: const Text("Save as favorite"), text: const Text("Save as favorite"),
onPressed: () { onPressed: () {
actionFavorite(isSaved); toggler.item2.mutate(Tuple2(spotify, toggler.item1));
}, },
), ),
if (auth.isLoggedIn) if (auth.isLoggedIn)

View File

@ -30,7 +30,7 @@ class Auth extends PersistedChangeNotifier {
Duration get expiresIn => Duration get expiresIn =>
_expiration?.difference(DateTime.now()) ?? Duration.zero; _expiration?.difference(DateTime.now()) ?? Duration.zero;
refresh() async { Future<void> refresh() async {
final data = await ServiceUtils.getAccessToken(authCookie!); final data = await ServiceUtils.getAccessToken(authCookie!);
_accessToken = data.accessToken; _accessToken = data.accessToken;
_expiration = data.expiration; _expiration = data.expiration;
@ -39,15 +39,17 @@ class Auth extends PersistedChangeNotifier {
} }
Timer? _createRefresher() { Timer? _createRefresher() {
if (expiration == null || !isExpired || authCookie == null) { if (expiration == null || authCookie == null) {
return null; return null;
} }
if (isExpired) {
refresh();
}
_refresher?.cancel(); _refresher?.cancel();
return Timer(expiresIn, refresh); return Timer(expiresIn - const Duration(minutes: 5), refresh);
} }
void _restartRefresher() { void _restartRefresher() {
_refresher?.cancel();
_refresher = _createRefresher(); _refresher = _createRefresher();
} }

View File

@ -1,16 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/Logger.dart';
import 'package:spotube/models/LyricsModels.dart'; import 'package:spotube/models/LyricsModels.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.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:tuple/tuple.dart';
final categoriesQueryJob = final categoriesQueryJob =
InfiniteQueryJob<Page<Category>, Map<String, dynamic>, int>( InfiniteQueryJob<Page<Category>, Map<String, dynamic>, int>(
@ -47,7 +44,7 @@ final categoryPlaylistsQueryJob =
final currentUserPlaylistsQueryJob = final currentUserPlaylistsQueryJob =
QueryJob<Iterable<PlaylistSimple>, SpotifyApi>( QueryJob<Iterable<PlaylistSimple>, SpotifyApi>(
queryKey: "current-user-query", queryKey: "current-user-playlists",
task: (_, spotify) { task: (_, spotify) {
return spotify.playlists.me.all(); return spotify.playlists.me.all();
}, },
@ -60,14 +57,6 @@ final currentUserAlbumsQueryJob = QueryJob<Iterable<AlbumSimple>, SpotifyApi>(
}, },
); );
final currentUserFollowingArtistsQuery =
FutureProvider.family<CursorPage<Artist>, String>(
(ref, pageKey) {
final spotify = ref.watch(spotifyProvider);
return spotify.me.following(FollowingType.artist).getPage(15, pageKey);
},
);
final currentUserFollowingArtistsQueryJob = final currentUserFollowingArtistsQueryJob =
InfiniteQueryJob<CursorPage<Artist>, SpotifyApi, String>( InfiniteQueryJob<CursorPage<Artist>, SpotifyApi, String>(
queryKey: "user-following-artists", queryKey: "user-following-artists",
@ -80,34 +69,17 @@ final currentUserFollowingArtistsQueryJob =
}, },
); );
final artistProfileQuery = FutureProvider.family<Artist, String>( final artistProfileQueryJob = QueryJob.withVariableKey<Artist, SpotifyApi>(
(ref, id) { preQueryKey: "artist-profile",
final spotify = ref.watch(spotifyProvider); task: (queryKey, externalData) =>
return spotify.artists.get(id); externalData.artists.get(getVariable(queryKey)),
},
); );
final currentUserFollowsArtistQuery = FutureProvider.family<bool, String>( final artistTopTracksQueryJob =
(ref, artistId) async { QueryJob.withVariableKey<Iterable<Track>, SpotifyApi>(
final spotify = ref.watch(spotifyProvider); preQueryKey: "artist-top-track-query",
final result = await spotify.me.isFollowing( task: (queryKey, spotify) {
FollowingType.artist, return spotify.artists.getTopTracks(getVariable(queryKey), "US");
[artistId],
);
return result.first;
},
);
final artistTopTracksQuery =
FutureProvider.family<Iterable<Track>, String>((ref, id) {
final spotify = ref.watch(spotifyProvider);
return spotify.artists.getTopTracks(id, "US");
});
final artistAlbumsQuery = FutureProvider.family<Page<Album>, String>(
(ref, id) {
final spotify = ref.watch(spotifyProvider);
return spotify.artists.albums(id).getPage(5, 0);
}, },
); );
@ -123,53 +95,54 @@ final artistAlbumsQueryJob =
}, },
); );
final artistRelatedArtistsQuery = final artistRelatedArtistsQueryJob =
FutureProvider.family<Iterable<Artist>, String>( QueryJob.withVariableKey<Iterable<Artist>, SpotifyApi>(
(ref, id) { preQueryKey: "artist-related-artist-query",
final spotify = ref.watch(spotifyProvider); task: (queryKey, spotify) {
return spotify.artists.getRelatedArtists(id); return spotify.artists.getRelatedArtists(getVariable(queryKey));
}, },
); );
final currentUserSavedTracksQuery = FutureProvider<List<Track>>((ref) { final currentUserFollowsArtistQueryJob =
final spotify = ref.watch(spotifyProvider); QueryJob.withVariableKey<bool, SpotifyApi>(
return spotify.tracks.me.saved.all().then( preQueryKey: "user-follows-artists-query",
(tracks) => tracks.map((e) => e.track!).toList(), task: (artistId, spotify) async {
); final result = await spotify.me.isFollowing(
}); FollowingType.artist,
[getVariable(artistId)],
final playlistTracksQuery = FutureProvider.family<List<Track>, String>( );
(ref, id) { return result.first;
try {
final spotify = ref.watch(spotifyProvider);
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(),
);
} catch (e, stack) {
getLogger("playlistTracksQuery").e(
"Fetching playlist tracks",
e,
stack,
);
return [];
}
}, },
); );
final albumTracksQuery = FutureProvider.family<List<TrackSimple>, String>( final playlistTracksQueryJob =
(ref, id) { QueryJob.withVariableKey<List<Track>, SpotifyApi>(
final spotify = ref.watch(spotifyProvider); preQueryKey: "playlist-tracks",
task: (queryKey, spotify) {
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(),
);
},
);
final albumTracksQueryJob =
QueryJob.withVariableKey<List<TrackSimple>, SpotifyApi>(
preQueryKey: "album-tracks",
task: (queryKey, spotify) {
final id = getVariable(queryKey);
return spotify.albums.getTracks(id).all().then((value) => value.toList()); return spotify.albums.getTracks(id).all().then((value) => value.toList());
}, },
); );
final currentUserQuery = FutureProvider<User>( final currentUserQueryJob = QueryJob<User, SpotifyApi>(
(ref) async { queryKey: "current-user",
final spotify = ref.watch(spotifyProvider); refetchOnExternalDataChange: true,
task: (_, 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 = [
@ -186,50 +159,112 @@ final currentUserQuery = FutureProvider<User>(
}, },
); );
final playlistIsFollowedQuery = FutureProvider.family<bool, String>( final playlistIsFollowedQueryJob = QueryJob.withVariableKey<bool, SpotifyApi>(
(ref, raw) { preQueryKey: "playlist-is-followed",
final data = jsonDecode(raw); task: (queryKey, spotify) {
final playlistId = data["playlistId"] as String; final idMap = getVariable(queryKey).split(":");
final userId = data["userId"] as String;
final spotify = ref.watch(spotifyProvider); return spotify.playlists.followedBy(idMap.first, [idMap.last]).then(
return spotify.playlists (value) => value.first,
.followedBy(playlistId, [userId]).then((value) => value.first); );
}, },
); );
final albumIsSavedForCurrentUserQuery = final albumIsSavedForCurrentUserQueryJob =
FutureProvider.family<bool, String>((ref, albumId) { QueryJob.withVariableKey<bool, SpotifyApi>(task: (queryKey, spotify) {
final spotify = ref.watch(spotifyProvider); return spotify.me
return spotify.me.isSavedAlbums([albumId]).then((value) => value.first); .isSavedAlbums([getVariable(queryKey)]).then((value) => value.first);
}); });
final searchQuery = FutureProvider.family<List<Page>, String>((ref, term) { final searchMutationJob = MutationJob<List<Page>, Tuple2<String, SpotifyApi>>(
final spotify = ref.watch(spotifyProvider); mutationKey: "search-query",
if (term.isEmpty) return []; task: (ref, variables) {
return spotify.search.get(term).first(10); final queryString = variables.item1;
}); final spotify = variables.item2;
if (queryString.isEmpty) return [];
return spotify.search.get(queryString).first(10);
},
);
final geniusLyricsQuery = FutureProvider<String?>( final geniusLyricsQueryJob = QueryJob<String, Tuple2<Track?, String>>(
(ref) { queryKey: "genius-lyrics-query",
final currentTrack = ref.watch(playbackProvider.select((s) => s.track)); task: (_, externalData) async {
final geniusAccessToken = final currentTrack = externalData.item1;
ref.watch(userPreferencesProvider.select((s) => s.geniusAccessToken)); final geniusAccessToken = externalData.item2;
if (currentTrack == null) { if (currentTrack == null) {
return "“Give this player a track to play”\n- S'Challa"; return "“Give this player a track to play”\n- S'Challa";
} }
return ServiceUtils.getLyrics( final lyrics = await ServiceUtils.getLyrics(
currentTrack.name!, currentTrack.name!,
currentTrack.artists?.map((s) => s.name).whereNotNull().toList() ?? [], currentTrack.artists?.map((s) => s.name).whereNotNull().toList() ?? [],
apiKey: geniusAccessToken, apiKey: geniusAccessToken,
optimizeQuery: true, optimizeQuery: true,
); );
if (lyrics == null) throw Exception("Unable find lyrics");
return lyrics;
}, },
); );
final rentanadviserLyricsQuery = FutureProvider<SubtitleSimple?>( final rentanadviserLyricsQueryJob = QueryJob<SubtitleSimple, SpotubeTrack?>(
(ref) { queryKey: "synced-lyrics",
final currentTrack = ref.watch(playbackProvider.select((s) => s.track)); task: (_, currentTrack) async {
if (currentTrack == null) return null; if (currentTrack == null) throw "No track currently";
return ServiceUtils.getTimedLyrics(currentTrack);
final timedLyrics = await ServiceUtils.getTimedLyrics(currentTrack);
if (timedLyrics == null) throw Exception("Unable to find lyrics");
return timedLyrics;
},
);
final toggleFavoriteTrackMutationJob =
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;
if (isLiked) {
await spotify.tracks.me.removeOne(trackId);
} else {
await spotify.tracks.me.saveOne(trackId);
}
return !isLiked;
},
);
final toggleFavoritePlaylistMutationJob =
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;
if (isLiked) {
await spotify.playlists.unfollowPlaylist(playlistId);
} else {
await spotify.playlists.followPlaylist(playlistId);
}
return !isLiked;
},
);
final toggleFavoriteAlbumMutationJob =
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;
if (isLiked) {
await spotify.me.removeAlbums([albumId]);
} else {
await spotify.me.saveAlbums([albumId]);
}
return !isLiked;
}, },
); );

View File

@ -6,7 +6,6 @@ import 'package:flutter/widgets.dart' hide Image;
import 'package:metadata_god/metadata_god.dart' hide Image; import 'package:metadata_god/metadata_god.dart' hide Image;
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:spotube/components/Shared/AnchorButton.dart'; import 'package:spotube/components/Shared/AnchorButton.dart';
import 'package:spotube/components/Shared/LinkText.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';

View File

@ -485,14 +485,14 @@ packages:
name: fl_query name: fl_query
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.0" version: "0.3.1"
fl_query_hooks: fl_query_hooks:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_query_hooks name: fl_query_hooks
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.0" version: "0.3.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter

View File

@ -57,8 +57,8 @@ dependencies:
url: https://github.com/KRTirtho/metadata_god.git url: https://github.com/KRTirtho/metadata_god.git
ref: 7d195fdde324b382fc12067c56391285807e6233 ref: 7d195fdde324b382fc12067c56391285807e6233
visibility_detector: ^0.3.3 visibility_detector: ^0.3.3
fl_query: ^0.3.0 fl_query: ^0.3.1
fl_query_hooks: ^0.3.0 fl_query_hooks: ^0.3.1
flutter_inappwebview: ^5.4.3+7 flutter_inappwebview: ^5.4.3+7
tuple: ^2.0.1 tuple: ^2.0.1