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/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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,12 +125,27 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
isFollowingSnapshot.when(
|
HookBuilder(
|
||||||
data: (isFollowing) {
|
builder: (context) {
|
||||||
|
final isFollowingQuery = useQuery(
|
||||||
|
job: currentUserFollowsArtistQueryJob(
|
||||||
|
artistId),
|
||||||
|
externalData: spotify,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isFollowingQuery.isLoading ||
|
||||||
|
!isFollowingQuery.hasData) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return OutlinedButton(
|
return OutlinedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
isFollowing
|
isFollowingQuery.data!
|
||||||
? await spotify.me.unfollow(
|
? await spotify.me.unfollow(
|
||||||
FollowingType.artist,
|
FollowingType.artist,
|
||||||
[artistId],
|
[artistId],
|
||||||
@ -136,21 +161,21 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
stack,
|
stack,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
ref.refresh(
|
QueryBowl.of(context).refetchQueries([
|
||||||
currentUserFollowsArtistQuery(
|
currentUserFollowsArtistQueryJob(
|
||||||
artistId),
|
artistId)
|
||||||
);
|
.queryKey,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
isFollowing ? "Following" : "Follow",
|
isFollowingQuery.data!
|
||||||
|
? "Following"
|
||||||
|
: "Follow",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
error: (error, stackTrace) => Container(),
|
),
|
||||||
loading: () =>
|
|
||||||
const CircularProgressIndicator
|
|
||||||
.adaptive()),
|
|
||||||
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(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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),
|
||||||
|
@ -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(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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
|
||||||
|
@ -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"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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(
|
||||||
|
(track) =>
|
||||||
TypeConversionUtils.simpleTrack_X_Track(
|
TypeConversionUtils.simpleTrack_X_Track(
|
||||||
track, album!))
|
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(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
@ -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)
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,25 +95,31 @@ 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)],
|
||||||
|
);
|
||||||
|
return result.first;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
final playlistTracksQuery = FutureProvider.family<List<Track>, String>(
|
final playlistTracksQueryJob =
|
||||||
(ref, id) {
|
QueryJob.withVariableKey<List<Track>, SpotifyApi>(
|
||||||
try {
|
preQueryKey: "playlist-tracks",
|
||||||
final spotify = ref.watch(spotifyProvider);
|
task: (queryKey, spotify) {
|
||||||
|
final id = getVariable(queryKey);
|
||||||
return id != "user-liked-tracks"
|
return id != "user-liked-tracks"
|
||||||
? spotify.playlists.getTracksByPlaylistId(id).all().then(
|
? spotify.playlists.getTracksByPlaylistId(id).all().then(
|
||||||
(value) => value.toList(),
|
(value) => value.toList(),
|
||||||
@ -149,27 +127,22 @@ final playlistTracksQuery = FutureProvider.family<List<Track>, String>(
|
|||||||
: spotify.tracks.me.saved.all().then(
|
: spotify.tracks.me.saved.all().then(
|
||||||
(tracks) => tracks.map((e) => e.track!).toList(),
|
(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 albumTracksQueryJob =
|
||||||
(ref, id) {
|
QueryJob.withVariableKey<List<TrackSimple>, SpotifyApi>(
|
||||||
final spotify = ref.watch(spotifyProvider);
|
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;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -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';
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user