mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
fix(auth): refresh access token timer not working
refactor: use fl_query for spotify API instead of riverpod
This commit is contained in:
parent
b5144dfa26
commit
b3ac5ca3bb
@ -1,4 +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/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@ -57,9 +58,14 @@ class AlbumView extends HookConsumerWidget {
|
||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
|
||||
final tracksSnapshot = ref.watch(albumTracksQuery(album.id!));
|
||||
final albumSavedSnapshot =
|
||||
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
||||
final tracksSnapshot = useQuery(
|
||||
job: albumTracksQueryJob(album.id!),
|
||||
externalData: spotify,
|
||||
);
|
||||
final albumSavedSnapshot = useQuery(
|
||||
job: albumIsSavedForCurrentUserQueryJob(album.id!),
|
||||
externalData: spotify,
|
||||
);
|
||||
|
||||
final albumArt = useMemoized(
|
||||
() => TypeConversionUtils.image_X_UrlString(
|
||||
@ -82,11 +88,11 @@ class AlbumView extends HookConsumerWidget {
|
||||
routePath: "/album/${album.id}",
|
||||
bottomSpace: breakpoint.isLessThanOrEqualTo(Breakpoints.md),
|
||||
onPlay: ([track]) {
|
||||
if (tracksSnapshot.asData?.value != null) {
|
||||
if (tracksSnapshot.hasData) {
|
||||
if (!isAlbumPlaying) {
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value
|
||||
tracksSnapshot.data!
|
||||
.map((track) =>
|
||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||
.toList(),
|
||||
@ -95,7 +101,7 @@ class AlbumView extends HookConsumerWidget {
|
||||
} else if (isAlbumPlaying && track != null) {
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value
|
||||
tracksSnapshot.data!
|
||||
.map((track) =>
|
||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||
.toList(),
|
||||
@ -112,35 +118,7 @@ class AlbumView extends HookConsumerWidget {
|
||||
ClipboardData(text: "https://open.spotify.com/album/${album.id}"),
|
||||
);
|
||||
},
|
||||
heartBtn: auth.isLoggedIn
|
||||
? 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,
|
||||
heartBtn: AlbumHeartButton(album: album),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@ -49,20 +51,28 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
|
||||
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(
|
||||
child: Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
leading: BackButton(),
|
||||
),
|
||||
body: artistsSnapshot.when<Widget>(
|
||||
data: (data) {
|
||||
body: HookBuilder(
|
||||
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(
|
||||
controller: parentScrollController,
|
||||
padding: const EdgeInsets.all(20),
|
||||
@ -115,42 +125,57 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
isFollowingSnapshot.when(
|
||||
data: (isFollowing) {
|
||||
return OutlinedButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
isFollowing
|
||||
? await spotify.me.unfollow(
|
||||
FollowingType.artist,
|
||||
[artistId],
|
||||
)
|
||||
: await spotify.me.follow(
|
||||
FollowingType.artist,
|
||||
[artistId],
|
||||
);
|
||||
} catch (e, stack) {
|
||||
logger.e(
|
||||
"FollowButton.onPressed",
|
||||
e,
|
||||
stack,
|
||||
);
|
||||
} finally {
|
||||
ref.refresh(
|
||||
currentUserFollowsArtistQuery(
|
||||
artistId),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
isFollowing ? "Following" : "Follow",
|
||||
),
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final isFollowingQuery = useQuery(
|
||||
job: currentUserFollowsArtistQueryJob(
|
||||
artistId),
|
||||
externalData: spotify,
|
||||
);
|
||||
|
||||
if (isFollowingQuery.isLoading ||
|
||||
!isFollowingQuery.hasData) {
|
||||
return const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) => Container(),
|
||||
loading: () =>
|
||||
const CircularProgressIndicator
|
||||
.adaptive()),
|
||||
}
|
||||
|
||||
return OutlinedButton(
|
||||
onPressed: () async {
|
||||
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(
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
onPressed: () {
|
||||
@ -180,8 +205,23 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
topTracksSnapshot.when(
|
||||
data: (topTracks) {
|
||||
HookBuilder(
|
||||
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 =
|
||||
playback.playlist?.id == data.id;
|
||||
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),
|
||||
Text(
|
||||
@ -266,28 +302,36 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
relatedArtists.when(
|
||||
data: (artists) {
|
||||
HookBuilder(
|
||||
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(
|
||||
child: Wrap(
|
||||
spacing: 20,
|
||||
runSpacing: 20,
|
||||
children: artists
|
||||
children: relatedArtists.data!
|
||||
.map((artist) => ArtistCard(artist))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error, stackTrack) =>
|
||||
Text("Failed to get Artist albums $error"),
|
||||
loading: () => const CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (_, __) => const Text("Life's miserable"),
|
||||
loading: () => const ShimmerArtistProfile(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:badges/badges.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:go_router/go_router.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/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Downloader.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
@ -42,7 +44,7 @@ class Sidebar extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final breakpoints = useBreakpoints();
|
||||
final extended = useState(false);
|
||||
final meSnapshot = ref.watch(currentUserQuery);
|
||||
|
||||
final auth = ref.watch(authProvider);
|
||||
final downloadCount = ref.watch(
|
||||
downloaderProvider.select((s) => s.currentlyRunning),
|
||||
@ -161,15 +163,28 @@ class Sidebar extends HookConsumerWidget {
|
||||
),
|
||||
SizedBox(
|
||||
width: extended.value ? 256 : 80,
|
||||
child: Builder(
|
||||
child: HookBuilder(
|
||||
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(
|
||||
data?.images,
|
||||
index: (data?.images?.length ?? 1) - 1,
|
||||
placeholder: ImagePlaceholder.artist,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (auth.isLoggedIn && !me.hasData) {
|
||||
me.setExternalData(ref.read(spotifyProvider));
|
||||
me.refetch();
|
||||
}
|
||||
return;
|
||||
}, [auth.isLoggedIn, me.hasData]);
|
||||
|
||||
if (extended.value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16).copyWith(left: 0),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class Lyrics extends HookConsumerWidget {
|
||||
final Color? titleBarForegroundColor;
|
||||
@ -17,7 +20,13 @@ class Lyrics extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
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 textTheme = Theme.of(context).textTheme;
|
||||
|
||||
@ -46,8 +55,18 @@ class Lyrics extends HookConsumerWidget {
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: geniusLyricsSnapshot.when(
|
||||
data: (lyrics) {
|
||||
child: Builder(
|
||||
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(
|
||||
lyrics == null && playback.track == null
|
||||
? "No Track being played currently"
|
||||
@ -56,9 +75,6 @@ class Lyrics extends HookConsumerWidget {
|
||||
?.copyWith(color: textTheme.headline1?.color),
|
||||
);
|
||||
},
|
||||
error: (error, __) => Text(
|
||||
"Sorry, no Lyrics were found for `${playback.track?.name}` :'("),
|
||||
loading: () => const ShimmerLyrics(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:ui';
|
||||
|
||||
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';
|
||||
@ -30,14 +31,17 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
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);
|
||||
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
final breakpoint = useBreakpoints();
|
||||
final controller = useAutoScrollController();
|
||||
final failed = useState(false);
|
||||
final lyricValue = timedLyricsSnapshot.asData?.value;
|
||||
final lyricValue = timedLyricsQuery.data;
|
||||
final lyricsMap = useMemoized(
|
||||
() =>
|
||||
lyricValue?.lyrics
|
||||
|
@ -6,13 +6,9 @@ import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
import 'package:spotube/components/Player/PlayerQueue.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Downloader.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';
|
||||
|
||||
class PlayerActions extends HookConsumerWidget {
|
||||
@ -27,11 +23,8 @@ class PlayerActions extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
final Playback playback = ref.watch(playbackProvider);
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
final downloader = ref.watch(downloaderProvider);
|
||||
final update = useForceUpdate();
|
||||
final isInQueue =
|
||||
downloader.inQueue.any((element) => element.id == playback.track?.id);
|
||||
final localTracks = ref.watch(localTracksProvider).value;
|
||||
@ -96,35 +89,7 @@ class PlayerActions extends HookConsumerWidget {
|
||||
? () => downloader.addToQueue(playback.track!)
|
||||
: null,
|
||||
),
|
||||
if (auth.isLoggedIn)
|
||||
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"),
|
||||
);
|
||||
}
|
||||
});
|
||||
}),
|
||||
if (playback.track != null) TrackHeartButton(track: playback.track!),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.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/TracksTableView.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -59,15 +55,18 @@ class PlaylistView extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final isPlaylistPlaying =
|
||||
playback.playlist?.id != null && playback.playlist?.id == playlist.id;
|
||||
|
||||
final breakpoint = useBreakpoints();
|
||||
|
||||
final meSnapshot = ref.watch(currentUserQuery);
|
||||
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
||||
final meSnapshot =
|
||||
useQuery(job: currentUserQueryJob, externalData: spotify);
|
||||
final tracksSnapshot = useQuery(
|
||||
job: playlistTracksQueryJob(playlist.id!),
|
||||
externalData: spotify,
|
||||
);
|
||||
|
||||
final titleImage = useMemoized(
|
||||
() => TypeConversionUtils.image_X_UrlString(
|
||||
@ -76,11 +75,6 @@ class PlaylistView extends HookConsumerWidget {
|
||||
),
|
||||
[playlist.images]);
|
||||
|
||||
final color = usePaletteGenerator(
|
||||
context,
|
||||
titleImage,
|
||||
).dominantColor;
|
||||
|
||||
return TrackCollectionView(
|
||||
id: playlist.id!,
|
||||
isPlaying: isPlaylistPlaying,
|
||||
@ -89,15 +83,15 @@ class PlaylistView extends HookConsumerWidget {
|
||||
tracksSnapshot: tracksSnapshot,
|
||||
description: playlist.description,
|
||||
isOwned: playlist.owner?.id != null &&
|
||||
playlist.owner!.id == meSnapshot.asData?.value.id,
|
||||
playlist.owner!.id == meSnapshot.data?.id,
|
||||
onPlay: ([track]) {
|
||||
if (tracksSnapshot.asData?.value != null) {
|
||||
if (tracksSnapshot.hasData) {
|
||||
if (!isPlaylistPlaying) {
|
||||
playPlaylist(playback, tracksSnapshot.asData!.value, ref);
|
||||
playPlaylist(playback, tracksSnapshot.data!, ref);
|
||||
} else if (isPlaylistPlaying && track != null) {
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value,
|
||||
tracksSnapshot.data!,
|
||||
ref,
|
||||
currentTrack: track,
|
||||
);
|
||||
@ -126,48 +120,7 @@ class PlaylistView extends HookConsumerWidget {
|
||||
);
|
||||
});
|
||||
},
|
||||
heartBtn: (auth.isLoggedIn && playlist.id != "user-liked-tracks"
|
||||
? 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),
|
||||
heartBtn: PlaylistHeartButton(playlist: playlist),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
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/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
final searchTermStateProvider = StateProvider<String>((ref) => "");
|
||||
|
||||
@ -24,18 +27,26 @@ class Search extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
final searchTerm = ref.watch(searchTermStateProvider);
|
||||
final controller =
|
||||
useTextEditingController(text: ref.read(searchTermStateProvider));
|
||||
final albumController = useScrollController();
|
||||
final playlistController = useScrollController();
|
||||
final artistController = useScrollController();
|
||||
final breakpoint = useBreakpoints();
|
||||
|
||||
final searchMutation = useMutation(
|
||||
job: searchMutationJob,
|
||||
);
|
||||
|
||||
if (auth.isAnonymous) {
|
||||
return const AnonymousFallback();
|
||||
}
|
||||
final searchSnapshot = ref.watch(searchQuery(searchTerm));
|
||||
|
||||
final getVariables = useCallback(
|
||||
() => Tuple2(
|
||||
ref.read(searchTermStateProvider),
|
||||
ref.read(spotifyProvider),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
child: Material(
|
||||
@ -49,14 +60,15 @@ class Search extends HookConsumerWidget {
|
||||
),
|
||||
color: Theme.of(context).backgroundColor,
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
ref.read(searchTermStateProvider.notifier).state = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
suffix: ElevatedButton(
|
||||
child: const Icon(Icons.search_rounded),
|
||||
onPressed: () {
|
||||
ref.read(searchTermStateProvider.notifier).state =
|
||||
controller.value.text;
|
||||
searchMutation.mutate(getVariables());
|
||||
},
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
@ -67,19 +79,26 @@ class Search extends HookConsumerWidget {
|
||||
hintText: "Search...",
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
ref.read(searchTermStateProvider.notifier).state =
|
||||
controller.value.text;
|
||||
searchMutation.mutate(getVariables());
|
||||
},
|
||||
),
|
||||
),
|
||||
searchSnapshot.when(
|
||||
data: (data) {
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
if (searchMutation.hasError && searchMutation.isError) {
|
||||
return Text("Alas! Error=${searchMutation.error}");
|
||||
}
|
||||
if (searchMutation.isLoading) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
List<AlbumSimple> albums = [];
|
||||
List<Artist> artists = [];
|
||||
List<Track> tracks = [];
|
||||
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 ?? []) {
|
||||
if (item is AlbumSimple) {
|
||||
albums.add(item);
|
||||
@ -241,8 +260,6 @@ class Search extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error, __) => Text("Error $error"),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -32,7 +32,7 @@ class Action extends StatelessWidget {
|
||||
}
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
primary: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
padding: const EdgeInsets.all(20),
|
||||
),
|
||||
icon: icon,
|
||||
|
@ -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_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 void Function() onPressed;
|
||||
final void Function()? onPressed;
|
||||
final IconData? icon;
|
||||
final Color? color;
|
||||
const HeartButton({
|
||||
@ -14,16 +25,219 @@ class HeartButton extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(authProvider);
|
||||
|
||||
if (!auth.isLoggedIn) return Container();
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
icon ??
|
||||
(!isLiked
|
||||
? Icons.favorite_outline_rounded
|
||||
: Icons.favorite_rounded),
|
||||
color: isLiked ? Theme.of(context).primaryColor : color,
|
||||
color: isLiked ? Colors.pink : color,
|
||||
),
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -13,12 +14,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class TrackCollectionView extends HookConsumerWidget {
|
||||
class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
final logger = getLogger(TrackCollectionView);
|
||||
final String id;
|
||||
final String title;
|
||||
final String? description;
|
||||
final AsyncValue<List<TrackSimple>> tracksSnapshot;
|
||||
final Query<List<TrackSimple>, T> tracksSnapshot;
|
||||
final String titleImage;
|
||||
final bool isPlaying;
|
||||
final void Function([Track? currentTrack]) onPlay;
|
||||
@ -78,7 +79,7 @@ class TrackCollectionView extends HookConsumerWidget {
|
||||
const CircleBorder(),
|
||||
),
|
||||
),
|
||||
onPressed: tracksSnapshot.asData?.value != null ? onPlay : null,
|
||||
onPressed: tracksSnapshot.data != null ? onPlay : null,
|
||||
child: Icon(
|
||||
isPlaying ? Icons.stop_rounded : Icons.play_arrow_rounded,
|
||||
color: Theme.of(context).backgroundColor,
|
||||
@ -175,7 +176,14 @@ class TrackCollectionView extends HookConsumerWidget {
|
||||
const BoxConstraints(maxHeight: 200),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: UniversalImage(path: titleImage),
|
||||
child: UniversalImage(
|
||||
path: titleImage,
|
||||
placeholder: (context, url) {
|
||||
return const UniversalImage(
|
||||
path: "assets/album-placeholder.png",
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
@ -220,14 +228,25 @@ class TrackCollectionView extends HookConsumerWidget {
|
||||
);
|
||||
}),
|
||||
),
|
||||
tracksSnapshot.when(
|
||||
data: (tracks) {
|
||||
HookBuilder(
|
||||
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(
|
||||
tracks is! List<Track>
|
||||
? tracks
|
||||
.map((track) =>
|
||||
TypeConversionUtils.simpleTrack_X_Track(
|
||||
track, album!))
|
||||
.map(
|
||||
(track) =>
|
||||
TypeConversionUtils.simpleTrack_X_Track(
|
||||
track, album!),
|
||||
)
|
||||
.toList()
|
||||
: tracks,
|
||||
onTrackPlayButtonPressed: onPlay,
|
||||
@ -235,10 +254,7 @@ class TrackCollectionView extends HookConsumerWidget {
|
||||
userPlaylist: isOwned,
|
||||
);
|
||||
},
|
||||
error: (error, _) =>
|
||||
SliverToBoxAdapter(child: Text("Error $error")),
|
||||
loading: () => const ShimmerTrackTile(),
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
);
|
||||
|
@ -4,16 +4,16 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart' hide Image;
|
||||
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/UniversalImage.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Auth.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:tuple/tuple.dart';
|
||||
|
||||
class TrackTile extends HookConsumerWidget {
|
||||
final Playback playback;
|
||||
@ -60,28 +60,6 @@ class TrackTile extends HookConsumerWidget {
|
||||
final breakpoint = useBreakpoints();
|
||||
final auth = ref.watch(authProvider);
|
||||
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 {
|
||||
if (playlistId == null) return;
|
||||
@ -188,6 +166,8 @@ class TrackTile extends HookConsumerWidget {
|
||||
index: track.value.album?.images?.length == 1 ? 0 : 2,
|
||||
);
|
||||
|
||||
final toggler = useTrackToggleLike(track.value, ref);
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
decoration: BoxDecoration(
|
||||
@ -291,14 +271,17 @@ class TrackTile extends HookConsumerWidget {
|
||||
if (!isReallyLocal)
|
||||
AdaptiveActions(
|
||||
actions: [
|
||||
if (auth.isLoggedIn)
|
||||
if (toggler.item3.hasData)
|
||||
Action(
|
||||
icon: Icon(isSaved
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_border_rounded),
|
||||
icon: toggler.item1
|
||||
? const Icon(
|
||||
Icons.favorite_rounded,
|
||||
color: Colors.pink,
|
||||
)
|
||||
: const Icon(Icons.favorite_border_rounded),
|
||||
text: const Text("Save as favorite"),
|
||||
onPressed: () {
|
||||
actionFavorite(isSaved);
|
||||
toggler.item2.mutate(Tuple2(spotify, toggler.item1));
|
||||
},
|
||||
),
|
||||
if (auth.isLoggedIn)
|
||||
|
@ -30,7 +30,7 @@ class Auth extends PersistedChangeNotifier {
|
||||
Duration get expiresIn =>
|
||||
_expiration?.difference(DateTime.now()) ?? Duration.zero;
|
||||
|
||||
refresh() async {
|
||||
Future<void> refresh() async {
|
||||
final data = await ServiceUtils.getAccessToken(authCookie!);
|
||||
_accessToken = data.accessToken;
|
||||
_expiration = data.expiration;
|
||||
@ -39,15 +39,17 @@ class Auth extends PersistedChangeNotifier {
|
||||
}
|
||||
|
||||
Timer? _createRefresher() {
|
||||
if (expiration == null || !isExpired || authCookie == null) {
|
||||
if (expiration == null || authCookie == null) {
|
||||
return null;
|
||||
}
|
||||
if (isExpired) {
|
||||
refresh();
|
||||
}
|
||||
_refresher?.cancel();
|
||||
return Timer(expiresIn, refresh);
|
||||
return Timer(expiresIn - const Duration(minutes: 5), refresh);
|
||||
}
|
||||
|
||||
void _restartRefresher() {
|
||||
_refresher?.cancel();
|
||||
_refresher = _createRefresher();
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
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/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/models/SpotubeTrack.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
final categoriesQueryJob =
|
||||
InfiniteQueryJob<Page<Category>, Map<String, dynamic>, int>(
|
||||
@ -47,7 +44,7 @@ final categoryPlaylistsQueryJob =
|
||||
|
||||
final currentUserPlaylistsQueryJob =
|
||||
QueryJob<Iterable<PlaylistSimple>, SpotifyApi>(
|
||||
queryKey: "current-user-query",
|
||||
queryKey: "current-user-playlists",
|
||||
task: (_, spotify) {
|
||||
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 =
|
||||
InfiniteQueryJob<CursorPage<Artist>, SpotifyApi, String>(
|
||||
queryKey: "user-following-artists",
|
||||
@ -80,34 +69,17 @@ final currentUserFollowingArtistsQueryJob =
|
||||
},
|
||||
);
|
||||
|
||||
final artistProfileQuery = FutureProvider.family<Artist, String>(
|
||||
(ref, id) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
return spotify.artists.get(id);
|
||||
},
|
||||
final artistProfileQueryJob = QueryJob.withVariableKey<Artist, SpotifyApi>(
|
||||
preQueryKey: "artist-profile",
|
||||
task: (queryKey, externalData) =>
|
||||
externalData.artists.get(getVariable(queryKey)),
|
||||
);
|
||||
|
||||
final currentUserFollowsArtistQuery = FutureProvider.family<bool, String>(
|
||||
(ref, artistId) async {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final result = await spotify.me.isFollowing(
|
||||
FollowingType.artist,
|
||||
[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);
|
||||
final artistTopTracksQueryJob =
|
||||
QueryJob.withVariableKey<Iterable<Track>, SpotifyApi>(
|
||||
preQueryKey: "artist-top-track-query",
|
||||
task: (queryKey, spotify) {
|
||||
return spotify.artists.getTopTracks(getVariable(queryKey), "US");
|
||||
},
|
||||
);
|
||||
|
||||
@ -123,53 +95,54 @@ final artistAlbumsQueryJob =
|
||||
},
|
||||
);
|
||||
|
||||
final artistRelatedArtistsQuery =
|
||||
FutureProvider.family<Iterable<Artist>, String>(
|
||||
(ref, id) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
return spotify.artists.getRelatedArtists(id);
|
||||
final artistRelatedArtistsQueryJob =
|
||||
QueryJob.withVariableKey<Iterable<Artist>, SpotifyApi>(
|
||||
preQueryKey: "artist-related-artist-query",
|
||||
task: (queryKey, spotify) {
|
||||
return spotify.artists.getRelatedArtists(getVariable(queryKey));
|
||||
},
|
||||
);
|
||||
|
||||
final currentUserSavedTracksQuery = FutureProvider<List<Track>>((ref) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
return spotify.tracks.me.saved.all().then(
|
||||
(tracks) => tracks.map((e) => e.track!).toList(),
|
||||
);
|
||||
});
|
||||
|
||||
final playlistTracksQuery = FutureProvider.family<List<Track>, String>(
|
||||
(ref, id) {
|
||||
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 currentUserFollowsArtistQueryJob =
|
||||
QueryJob.withVariableKey<bool, SpotifyApi>(
|
||||
preQueryKey: "user-follows-artists-query",
|
||||
task: (artistId, spotify) async {
|
||||
final result = await spotify.me.isFollowing(
|
||||
FollowingType.artist,
|
||||
[getVariable(artistId)],
|
||||
);
|
||||
return result.first;
|
||||
},
|
||||
);
|
||||
|
||||
final albumTracksQuery = FutureProvider.family<List<TrackSimple>, String>(
|
||||
(ref, id) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final playlistTracksQueryJob =
|
||||
QueryJob.withVariableKey<List<Track>, SpotifyApi>(
|
||||
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());
|
||||
},
|
||||
);
|
||||
|
||||
final currentUserQuery = FutureProvider<User>(
|
||||
(ref) async {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final currentUserQueryJob = QueryJob<User, SpotifyApi>(
|
||||
queryKey: "current-user",
|
||||
refetchOnExternalDataChange: true,
|
||||
task: (_, spotify) async {
|
||||
final me = await spotify.me.get();
|
||||
if (me.images == null || me.images?.isEmpty == true) {
|
||||
me.images = [
|
||||
@ -186,50 +159,112 @@ final currentUserQuery = FutureProvider<User>(
|
||||
},
|
||||
);
|
||||
|
||||
final playlistIsFollowedQuery = FutureProvider.family<bool, String>(
|
||||
(ref, raw) {
|
||||
final data = jsonDecode(raw);
|
||||
final playlistId = data["playlistId"] as String;
|
||||
final userId = data["userId"] as String;
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
return spotify.playlists
|
||||
.followedBy(playlistId, [userId]).then((value) => value.first);
|
||||
final playlistIsFollowedQueryJob = QueryJob.withVariableKey<bool, SpotifyApi>(
|
||||
preQueryKey: "playlist-is-followed",
|
||||
task: (queryKey, spotify) {
|
||||
final idMap = getVariable(queryKey).split(":");
|
||||
|
||||
return spotify.playlists.followedBy(idMap.first, [idMap.last]).then(
|
||||
(value) => value.first,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final albumIsSavedForCurrentUserQuery =
|
||||
FutureProvider.family<bool, String>((ref, albumId) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
return spotify.me.isSavedAlbums([albumId]).then((value) => value.first);
|
||||
final albumIsSavedForCurrentUserQueryJob =
|
||||
QueryJob.withVariableKey<bool, SpotifyApi>(task: (queryKey, spotify) {
|
||||
return spotify.me
|
||||
.isSavedAlbums([getVariable(queryKey)]).then((value) => value.first);
|
||||
});
|
||||
|
||||
final searchQuery = FutureProvider.family<List<Page>, String>((ref, term) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
if (term.isEmpty) return [];
|
||||
return spotify.search.get(term).first(10);
|
||||
});
|
||||
final searchMutationJob = MutationJob<List<Page>, Tuple2<String, SpotifyApi>>(
|
||||
mutationKey: "search-query",
|
||||
task: (ref, variables) {
|
||||
final queryString = variables.item1;
|
||||
final spotify = variables.item2;
|
||||
if (queryString.isEmpty) return [];
|
||||
return spotify.search.get(queryString).first(10);
|
||||
},
|
||||
);
|
||||
|
||||
final geniusLyricsQuery = FutureProvider<String?>(
|
||||
(ref) {
|
||||
final currentTrack = ref.watch(playbackProvider.select((s) => s.track));
|
||||
final geniusAccessToken =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.geniusAccessToken));
|
||||
final geniusLyricsQueryJob = QueryJob<String, Tuple2<Track?, String>>(
|
||||
queryKey: "genius-lyrics-query",
|
||||
task: (_, externalData) async {
|
||||
final currentTrack = externalData.item1;
|
||||
final geniusAccessToken = externalData.item2;
|
||||
if (currentTrack == null) {
|
||||
return "“Give this player a track to play”\n- S'Challa";
|
||||
}
|
||||
return ServiceUtils.getLyrics(
|
||||
final lyrics = await ServiceUtils.getLyrics(
|
||||
currentTrack.name!,
|
||||
currentTrack.artists?.map((s) => s.name).whereNotNull().toList() ?? [],
|
||||
apiKey: geniusAccessToken,
|
||||
optimizeQuery: true,
|
||||
);
|
||||
|
||||
if (lyrics == null) throw Exception("Unable find lyrics");
|
||||
return lyrics;
|
||||
},
|
||||
);
|
||||
|
||||
final rentanadviserLyricsQuery = FutureProvider<SubtitleSimple?>(
|
||||
(ref) {
|
||||
final currentTrack = ref.watch(playbackProvider.select((s) => s.track));
|
||||
if (currentTrack == null) return null;
|
||||
return ServiceUtils.getTimedLyrics(currentTrack);
|
||||
final rentanadviserLyricsQueryJob = QueryJob<SubtitleSimple, SpotubeTrack?>(
|
||||
queryKey: "synced-lyrics",
|
||||
task: (_, currentTrack) async {
|
||||
if (currentTrack == null) throw "No track currently";
|
||||
|
||||
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;
|
||||
},
|
||||
);
|
||||
|
@ -6,7 +6,6 @@ import 'package:flutter/widgets.dart' hide Image;
|
||||
import 'package:metadata_god/metadata_god.dart' hide Image;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:spotube/components/Shared/AnchorButton.dart';
|
||||
import 'package:spotube/components/Shared/LinkText.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/SpotubeTrack.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
|
@ -485,14 +485,14 @@ packages:
|
||||
name: fl_query
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
version: "0.3.1"
|
||||
fl_query_hooks:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_query_hooks
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
version: "0.3.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -57,8 +57,8 @@ dependencies:
|
||||
url: https://github.com/KRTirtho/metadata_god.git
|
||||
ref: 7d195fdde324b382fc12067c56391285807e6233
|
||||
visibility_detector: ^0.3.3
|
||||
fl_query: ^0.3.0
|
||||
fl_query_hooks: ^0.3.0
|
||||
fl_query: ^0.3.1
|
||||
fl_query_hooks: ^0.3.1
|
||||
flutter_inappwebview: ^5.4.3+7
|
||||
tuple: ^2.0.1
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user