From b3b3acdb1ed3cd98c98847d3da6db2507071fffb Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 4 Jun 2022 14:06:52 +0600 Subject: [PATCH] Bugfix: ColorScheme not persisting Response Caching support in multiple components PlayerView, AlbumView, Search, Lyrics, SyncedLyrics --- lib/components/Album/AlbumView.dart | 150 +++---- lib/components/Lyrics/Lyrics.dart | 93 +---- lib/components/Lyrics/SyncedLyrics.dart | 37 +- lib/components/Playlist/PlaylistView.dart | 221 +++++----- lib/components/Search/Search.dart | 36 +- lib/components/Settings/Login.dart | 27 +- lib/components/Settings/Settings.dart | 377 +++++++----------- .../Settings/SettingsHotkeyTile.dart | 40 +- lib/hooks/usePaginatedFutureProvider.dart | 2 +- lib/provider/SpotifyRequests.dart | 102 ++++- lib/provider/UserPreferences.dart | 12 +- 11 files changed, 489 insertions(+), 608 deletions(-) diff --git a/lib/components/Album/AlbumView.dart b/lib/components/Album/AlbumView.dart index 489bd713..f9243009 100644 --- a/lib/components/Album/AlbumView.dart +++ b/lib/components/Album/AlbumView.dart @@ -6,11 +6,11 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/simple-track-to-track.dart'; -import 'package:spotube/hooks/useForceUpdate.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'; class AlbumView extends HookConsumerWidget { final AlbumSimple album; @@ -44,88 +44,88 @@ class AlbumView extends HookConsumerWidget { final SpotifyApi spotify = ref.watch(spotifyProvider); final Auth auth = ref.watch(authProvider); - final update = useForceUpdate(); + final tracksSnapshot = ref.watch(albumTracksQuery(album.id!)); + final albumSavedSnapshot = + ref.watch(albumIsSavedForCurrentUserQuery(album.id!)); return SafeArea( child: Scaffold( - body: FutureBuilder>( - future: spotify.albums.getTracks(album.id!).all(), - builder: (context, snapshot) { - List tracks = snapshot.data?.map((trackSmp) { - return simpleTrackToTrack(trackSmp, album); - }).toList() ?? - []; - return Column( + body: Column( + children: [ + PageWindowTitleBar( + leading: Row( children: [ - PageWindowTitleBar( - leading: Row( - children: [ - // nav back - const BackButton(), - // heart playlist - if (auth.isLoggedIn) - FutureBuilder>( - future: spotify.me.isSavedAlbums([album.id!]), - builder: (context, snapshot) { - final isSaved = snapshot.data?.first == true; - if (!snapshot.hasData && !snapshot.hasError) { - return const SizedBox( - height: 25, - width: 25, - child: CircularProgressIndicator.adaptive(), - ); - } - return HeartButton( - isLiked: isSaved, - onPressed: () { - (isSaved - ? spotify.me.removeAlbums( - [album.id!], - ) - : spotify.me.saveAlbums( - [album.id!], - )) - .then((_) => update()); - }, + // nav back + const BackButton(), + // heart playlist + if (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!, + ), ); - }), - // play playlist - IconButton( - icon: Icon( - isPlaylistPlaying - ? Icons.stop_rounded - : Icons.play_arrow_rounded, - ), - onPressed: snapshot.hasData - ? () => playPlaylist(playback, tracks) - : null, - ) - ], + ref.refresh(currentUserAlbumsQuery); + }); + }, + ); + }, + error: (error, _) => Text("Error $error"), + loading: () => const CircularProgressIndicator()), + // play playlist + IconButton( + icon: Icon( + isPlaylistPlaying + ? Icons.stop_rounded + : Icons.play_arrow_rounded, ), - ), - Center( - child: Text(album.name!, - style: Theme.of(context).textTheme.headline4), - ), - snapshot.hasError - ? const Center(child: Text("Error occurred")) - : !snapshot.hasData - ? const Expanded( - child: Center( - child: CircularProgressIndicator.adaptive()), + onPressed: tracksSnapshot.asData?.value != null + ? () => playPlaylist( + playback, + tracksSnapshot.asData!.value.map((trackSmp) { + return simpleTrackToTrack(trackSmp, album); + }).toList(), ) - : TracksTableView( - tracks, - onTrackPlayButtonPressed: (currentTrack) => - playPlaylist( - playback, - tracks, - currentTrack: currentTrack, - ), - ), + : null, + ) ], - ); - }), + ), + ), + Center( + child: Text(album.name!, + style: Theme.of(context).textTheme.headline4), + ), + tracksSnapshot.when( + data: (data) { + List tracks = data.map((trackSmp) { + return simpleTrackToTrack(trackSmp, album); + }).toList(); + return TracksTableView( + tracks, + onTrackPlayButtonPressed: (currentTrack) => playPlaylist( + playback, + tracks, + currentTrack: currentTrack, + ), + ); + }, + error: (error, _) => Text("Error $error"), + loading: () => const CircularProgressIndicator(), + ), + ], + ), ), ); } diff --git a/lib/components/Lyrics/Lyrics.dart b/lib/components/Lyrics/Lyrics.dart index 4a3a632a..706dc36d 100644 --- a/lib/components/Lyrics/Lyrics.dart +++ b/lib/components/Lyrics/Lyrics.dart @@ -1,13 +1,10 @@ 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/helpers/artist-to-string.dart'; -import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/Playback.dart'; -import 'package:spotube/provider/UserPreferences.dart'; -import 'package:collection/collection.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; class Lyrics extends HookConsumerWidget { const Lyrics({Key? key}) : super(key: key); @@ -15,74 +12,8 @@ class Lyrics extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); - UserPreferences userPreferences = ref.watch(userPreferencesProvider); - final lyrics = useState({}); - - final lyricsFuture = useMemoized(() async { - if (playback.currentTrack == null || - userPreferences.geniusAccessToken.isEmpty || - (playback.currentTrack?.id != null && - playback.currentTrack?.id == lyrics.value["id"])) { - return null; - } - final lyricsStr = await getLyrics( - playback.currentTrack!.name!, - playback.currentTrack!.artists - ?.map((s) => s.name) - .whereNotNull() - .toList() ?? - [], - apiKey: userPreferences.geniusAccessToken, - optimizeQuery: true, - ); - if (lyricsStr == null) return Future.error("No lyrics found"); - return lyricsStr; - }, [playback.currentTrack, userPreferences.geniusAccessToken]); - - final lyricsSnapshot = useFuture(lyricsFuture); - - useEffect(() { - if (lyricsSnapshot.hasData && - lyricsSnapshot.data != null && - playback.currentTrack != null) { - lyrics.value = { - "lyrics": lyricsSnapshot.data, - "id": playback.currentTrack!.id! - }; - } - - if (lyrics.value["lyrics"] != null && playback.currentTrack == null) { - lyrics.value = {}; - } - return null; - }, [ - lyricsSnapshot.data, - lyricsSnapshot.hasData, - lyrics.value, - playback.currentTrack, - ]); - + final geniusLyricsSnapshot = ref.watch(geniusLyricsQuery); final breakpoint = useBreakpoints(); - - if (lyrics.value["lyrics"] == null && playback.currentTrack != null) { - if (lyricsSnapshot.hasError) { - return Expanded( - child: Center( - child: Text( - "Sorry, no Lyrics were found for `${playback.currentTrack?.name}` :'(", - style: Theme.of(context).textTheme.headline4, - textAlign: TextAlign.center, - ), - ), - ); - } - return const Expanded( - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ); - } - final textTheme = Theme.of(context).textTheme; return Expanded( @@ -110,13 +41,19 @@ class Lyrics extends HookConsumerWidget { child: Center( child: Padding( padding: const EdgeInsets.all(8.0), - child: Text( - lyrics.value["lyrics"] == null && - playback.currentTrack == null - ? "No Track being played currently" - : lyrics.value["lyrics"]!, - style: textTheme.headline6 - ?.copyWith(color: textTheme.headline1?.color), + child: geniusLyricsSnapshot.when( + data: (lyrics) { + return Text( + lyrics == null && playback.currentTrack == null + ? "No Track being played currently" + : lyrics!, + style: textTheme.headline6 + ?.copyWith(color: textTheme.headline1?.color), + ); + }, + error: (error, __) => Text( + "Sorry, no Lyrics were found for `${playback.currentTrack?.name}` :'("), + loading: () => const CircularProgressIndicator(), ), ), ), diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart index 1713834e..74fb57bb 100644 --- a/lib/components/Lyrics/SyncedLyrics.dart +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -5,47 +5,33 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/components/Lyrics/Lyrics.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/helpers/artist-to-string.dart'; -import 'package:spotube/helpers/timed-lyrics.dart'; import 'package:spotube/hooks/useAutoScrollController.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useSyncedLyrics.dart'; -import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; class SyncedLyrics extends HookConsumerWidget { const SyncedLyrics({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { + final timedLyricsSnapshot = ref.watch(rentanadviserLyricsQuery); + Playback playback = ref.watch(playbackProvider); final breakpoint = useBreakpoints(); final controller = useAutoScrollController(); final failed = useState(false); - final timedLyrics = useMemoized(() async { - if (playback.currentTrack == null || - playback.currentTrack is! SpotubeTrack) return null; - try { - if (failed.value) failed.value = false; - final lyrics = - await getTimedLyrics(playback.currentTrack as SpotubeTrack); - if (lyrics == null) failed.value = true; - return lyrics; - } catch (e) { - if (e == "Subtitle lookup failed") { - failed.value = true; - } - } - }, [playback.currentTrack]); - final lyricsSnapshot = useFuture(timedLyrics); + final lyricValue = timedLyricsSnapshot.asData?.value; final lyricsMap = useMemoized( () => - lyricsSnapshot.data?.lyrics + lyricValue?.lyrics .map((lyric) => {lyric.time.inSeconds: lyric.text}) .reduce((accumulator, lyricSlice) => {...accumulator, ...lyricSlice}) ?? {}, - [lyricsSnapshot.data], + [lyricValue], ); final currentTime = useSyncedLyrics(ref, lyricsMap); @@ -54,11 +40,12 @@ class SyncedLyrics extends HookConsumerWidget { useEffect(() { controller.scrollToIndex(0); + failed.value = false; return null; }, [playback.currentTrack]); useEffect(() { - if (lyricsSnapshot.data != null && lyricsSnapshot.data!.rating <= 2) { + if (lyricValue != null && lyricValue.rating <= 2) { Future.delayed(const Duration(seconds: 5), () { showDialog( context: context, @@ -97,7 +84,7 @@ class SyncedLyrics extends HookConsumerWidget { }); } return null; - }, [lyricsSnapshot.data]); + }, [lyricValue]); // when synced lyrics not found, fallback to GeniusLyrics if (failed.value) return const Lyrics(); @@ -130,12 +117,12 @@ class SyncedLyrics extends HookConsumerWidget { : textTheme.headline6, ), ), - if (lyricsSnapshot.hasData) + if (lyricValue != null) Expanded( child: ListView.builder( controller: controller, itemBuilder: (context, index) { - final lyricSlice = lyricsSnapshot.data!.lyrics[index]; + final lyricSlice = lyricValue.lyrics[index]; final isActive = lyricSlice.time.inSeconds == currentTime; if (isActive) { controller.scrollToIndex( @@ -164,7 +151,7 @@ class SyncedLyrics extends HookConsumerWidget { ), ); }, - itemCount: lyricsSnapshot.data!.lyrics.length, + itemCount: lyricValue.lyrics.length, ), ), ], diff --git a/lib/components/Playlist/PlaylistView.dart b/lib/components/Playlist/PlaylistView.dart index d63bc3ea..9db0c82e 100644 --- a/lib/components/Playlist/PlaylistView.dart +++ b/lib/components/Playlist/PlaylistView.dart @@ -1,11 +1,11 @@ +import 'dart:convert'; + import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; -import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Auth.dart'; @@ -13,6 +13,7 @@ import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; class PlaylistView extends HookConsumerWidget { final logger = getLogger(PlaylistView); @@ -47,130 +48,120 @@ class PlaylistView extends HookConsumerWidget { SpotifyApi spotify = ref.watch(spotifyProvider); final isPlaylistPlaying = playback.currentPlaylist?.id != null && playback.currentPlaylist?.id == playlist.id; - final update = useForceUpdate(); - final getMe = useMemoized(() => spotify.me.get(), []); - final meSnapshot = useFuture(getMe); - Future> isFollowing(User me) { - return spotify.playlists.followedBy(playlist.id!, [me.id!]); - } + final meSnapshot = ref.watch(currentUserQuery); + final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!)); return SafeArea( child: Scaffold( - body: FutureBuilder>( - future: playlist.id != "user-liked-tracks" - ? spotify.playlists.getTracksByPlaylistId(playlist.id).all() - : spotify.tracks.me.saved - .all() - .then((tracks) => tracks.map((e) => e.track!)), - builder: (context, snapshot) { - List tracks = snapshot.data?.toList() ?? []; - return Column( + body: Column( + children: [ + PageWindowTitleBar( + leading: Row( children: [ - PageWindowTitleBar( - leading: Row( - children: [ - // nav back - const BackButton(), - // heart playlist - if (auth.isLoggedIn && meSnapshot.hasData) - FutureBuilder>( - future: isFollowing(meSnapshot.data!), - builder: (context, snapshot) { - final isFollowing = - snapshot.data?.first ?? false; + // nav back + const BackButton(), + // heart playlist + if (auth.isLoggedIn) + meSnapshot.when( + data: (me) { + final query = playlistIsFollowedQuery(jsonEncode( + {"playlistId": playlist.id, "userId": me.id!})); + final followingSnapshot = ref.watch(query); - if (!snapshot.hasData && !snapshot.hasError) { - return const SizedBox( - height: 25, - width: 25, - child: CircularProgressIndicator.adaptive(), - ); + return followingSnapshot.when( + data: (isFollowing) { + return HeartButton( + isLiked: isFollowing, + icon: playlist.owner?.id != null && + me.id == playlist.owner?.id + ? Icons.delete_outline_rounded + : null, + onPressed: () async { + try { + isFollowing + ? spotify.playlists + .unfollowPlaylist(playlist.id!) + : spotify.playlists + .followPlaylist(playlist.id!); + } catch (e, stack) { + logger.e("FollowButton.onPressed", e, stack); + } finally { + ref.refresh(query); + ref.refresh(currentUserPlaylistsQuery); } - return HeartButton( - isLiked: isFollowing, - icon: playlist.owner?.id != null && - meSnapshot.data?.id == - playlist.owner?.id - ? Icons.delete_outline_rounded - : null, - onPressed: () async { - try { - isFollowing - ? spotify.playlists - .unfollowPlaylist(playlist.id!) - : spotify.playlists - .followPlaylist(playlist.id!); - } catch (e, stack) { - logger.e( - "FollowButton.onPressed", e, stack); - } finally { - update(); - } - }, - ); - }), - IconButton( - icon: const Icon(Icons.share_rounded), - onPressed: () { - final data = - "https://open.spotify.com/playlist/${playlist.id}"; - Clipboard.setData( - ClipboardData(text: data), - ).then((_) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - "Copied $data to clipboard", - textAlign: TextAlign.center, - ), - ), - ); - }); + }, + ); }, - ), - // play playlist - IconButton( - icon: Icon( - isPlaylistPlaying - ? Icons.stop_rounded - : Icons.play_arrow_rounded, - ), - onPressed: snapshot.hasData - ? () => playPlaylist(playback, tracks) - : null, - ) - ], + error: (error, _) => Text("Error $error"), + loading: () => const CircularProgressIndicator(), + ); + }, + error: (error, _) => Text("Error $error"), + loading: () => const CircularProgressIndicator(), ), - ), - Center( - child: Text(playlist.name!, - style: Theme.of(context).textTheme.headline4), - ), - snapshot.hasError - ? const Center(child: Text("Error occurred")) - : !snapshot.hasData - ? const Expanded( - child: Center( - child: CircularProgressIndicator.adaptive()), - ) - : TracksTableView( - tracks, - onTrackPlayButtonPressed: (currentTrack) => - playPlaylist( - playback, - tracks, - currentTrack: currentTrack, - ), - playlistId: playlist.id, - userPlaylist: playlist.owner?.id != null && - playlist.owner!.id == meSnapshot.data?.id, + + IconButton( + icon: const Icon(Icons.share_rounded), + onPressed: () { + final data = + "https://open.spotify.com/playlist/${playlist.id}"; + Clipboard.setData( + ClipboardData(text: data), + ).then((_) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + "Copied $data to clipboard", + textAlign: TextAlign.center, ), + ), + ); + }); + }, + ), + // play playlist + IconButton( + icon: Icon( + isPlaylistPlaying + ? Icons.stop_rounded + : Icons.play_arrow_rounded, + ), + onPressed: tracksSnapshot.asData?.value != null + ? () => playPlaylist( + playback, + tracksSnapshot.asData!.value, + ) + : null, + ) ], - ); - }), + ), + ), + Center( + child: Text(playlist.name!, + style: Theme.of(context).textTheme.headline4), + ), + tracksSnapshot.when( + data: (tracks) { + return TracksTableView( + tracks, + onTrackPlayButtonPressed: (currentTrack) => playPlaylist( + playback, + tracks, + currentTrack: currentTrack, + ), + playlistId: playlist.id, + userPlaylist: playlist.owner?.id != null && + playlist.owner!.id == meSnapshot.asData?.value.id, + ); + }, + error: (error, _) => Text("Error $error"), + loading: () => const CircularProgressIndicator(), + ), + ], + ), ), ); } diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index acc0b724..d872b999 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -14,17 +14,19 @@ 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'; + +final searchTermStateProvider = StateProvider((ref) => ""); class Search extends HookConsumerWidget { const Search({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { - final SpotifyApi spotify = ref.watch(spotifyProvider); final Auth auth = ref.watch(authProvider); - final controller = useTextEditingController(); - final searchTerm = useState(""); + final searchTerm = ref.watch(searchTermStateProvider); + final controller = + useTextEditingController(text: ref.read(searchTermStateProvider)); final albumController = useScrollController(); final playlistController = useScrollController(); final artistController = useScrollController(); @@ -33,6 +35,7 @@ class Search extends HookConsumerWidget { if (auth.isAnonymous) { return const Expanded(child: AnonymousFallback()); } + final searchSnapshot = ref.watch(searchQuery(searchTerm)); return Expanded( child: Container( @@ -48,7 +51,8 @@ class Search extends HookConsumerWidget { decoration: const InputDecoration(hintText: "Search..."), controller: controller, onSubmitted: (value) { - searchTerm.value = controller.value.text; + ref.read(searchTermStateProvider.notifier).state = + controller.value.text; }, ), ), @@ -61,31 +65,21 @@ class Search extends HookConsumerWidget { textColor: Colors.white, child: const Icon(Icons.search_rounded), onPressed: () { - searchTerm.value = controller.value.text; + ref.read(searchTermStateProvider.notifier).state = + controller.value.text; }, ), ], ), ), - FutureBuilder>( - future: searchTerm.value.isNotEmpty - ? spotify.search.get(searchTerm.value).first(10) - : null, - builder: (context, snapshot) { - if (!snapshot.hasData && searchTerm.value.isNotEmpty) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } else if (!snapshot.hasData && searchTerm.value.isEmpty) { - return Container(); - } + searchSnapshot.when( + data: (data) { Playback playback = ref.watch(playbackProvider); List albums = []; List artists = []; List tracks = []; List playlists = []; - for (MapEntry page - in snapshot.data?.asMap().entries ?? []) { + for (MapEntry page in data.asMap().entries) { for (var item in page.value.items ?? []) { if (item is AlbumSimple) { albums.add(item); @@ -217,6 +211,8 @@ class Search extends HookConsumerWidget { ), ); }, + error: (error, __) => Text("Error $error"), + loading: () => const CircularProgressIndicator(), ) ], ), diff --git a/lib/components/Settings/Login.dart b/lib/components/Settings/Login.dart index a5b1fa88..9338374c 100644 --- a/lib/components/Settings/Login.dart +++ b/lib/components/Settings/Login.dart @@ -2,15 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/helpers/oauth-login.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; -import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Auth.dart'; -import 'package:spotube/provider/UserPreferences.dart'; class Login extends HookConsumerWidget { Login({Key? key}) : super(key: key); @@ -21,7 +18,6 @@ class Login extends HookConsumerWidget { Auth authState = ref.watch(authProvider); final clientIdController = useTextEditingController(); final clientSecretController = useTextEditingController(); - final accessTokenController = useTextEditingController(); final fieldError = useState(false); final breakpoint = useBreakpoints(); @@ -90,31 +86,10 @@ class Login extends HookConsumerWidget { ), controller: clientSecretController, ), - const SizedBox(height: 10), - const Divider(color: Colors.grey), - const SizedBox(height: 10), - TextField( - decoration: const InputDecoration( - label: Text("Genius Access Token (optional)"), - ), - controller: accessTokenController, - ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 20), ElevatedButton( onPressed: () async { await handleLogin(authState); - UserPreferences preferences = - ref.read(userPreferencesProvider); - SharedPreferences localStorage = - await SharedPreferences.getInstance(); - preferences.setGeniusAccessToken( - accessTokenController.value.text); - await localStorage.setString( - LocalStorageKeys.geniusAccessToken, - accessTokenController.value.text); - accessTokenController.text = ""; }, child: const Text("Submit"), ) diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart index 5a58657b..1fa499b4 100644 --- a/lib/components/Settings/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -8,10 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/components/Settings/About.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/SettingsHotkeyTile.dart'; -import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; -import 'package:spotube/hooks/usePackageInfo.dart'; -import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/SpotifyMarkets.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/UserPreferences.dart'; @@ -23,24 +20,13 @@ class Settings extends HookConsumerWidget { Widget build(BuildContext context, ref) { final UserPreferences preferences = ref.watch(userPreferencesProvider); final Auth auth = ref.watch(authProvider); - final geniusAccessToken = useState(null); - TextEditingController geniusTokenController = useTextEditingController(); final ytSearchFormatController = useTextEditingController(text: preferences.ytSearchFormat); - geniusTokenController.addListener(() { - geniusAccessToken.value = geniusTokenController.value.text; - }); - ytSearchFormatController.addListener(() { preferences.setYtSearchFormat(ytSearchFormatController.value.text); }); - final packageInfo = usePackageInfo( - appName: 'Spotube', - packageName: 'spotube', - ); - final pickColorScheme = useCallback((ColorSchemeType schemeType) { return () => showDialog( context: context, @@ -66,174 +52,119 @@ class Settings extends HookConsumerWidget { Flexible( child: Container( constraints: const BoxConstraints(maxWidth: 1366), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: ListView( - children: [ - Row( - children: [ - Expanded( - flex: 2, + child: ListView( + children: [ + if (!Platform.isAndroid && !Platform.isIOS) ...[ + SettingsHotKeyTile( + title: "Next track global shortcut", + currentHotKey: preferences.nextTrackHotKey, + onHotKeyRecorded: (value) { + preferences.setNextTrackHotKey(value); + }, + ), + SettingsHotKeyTile( + title: "Prev track global shortcut", + currentHotKey: preferences.prevTrackHotKey, + onHotKeyRecorded: (value) { + preferences.setPrevTrackHotKey(value); + }, + ), + SettingsHotKeyTile( + title: "Play/Pause global shortcut", + currentHotKey: preferences.playPauseHotKey, + onHotKeyRecorded: (value) { + preferences.setPlayPauseHotKey(value); + }, + ), + ], + ListTile( + title: const Text("Theme"), + trailing: DropdownButton( + value: preferences.themeMode, + items: const [ + DropdownMenuItem( child: Text( - "Genius Access Token", - style: Theme.of(context).textTheme.subtitle1, + "Dark", ), + value: ThemeMode.dark, ), - Expanded( - flex: 1, - child: TextField( - controller: geniusTokenController, - decoration: InputDecoration( - hintText: preferences.geniusAccessToken, - ), + DropdownMenuItem( + child: Text( + "Light", ), + value: ThemeMode.light, + ), + DropdownMenuItem( + child: Text("System"), + value: ThemeMode.system, ), - Padding( - padding: const EdgeInsets.all(8.0), - child: ElevatedButton( - onPressed: geniusAccessToken.value != null - ? () async { - SharedPreferences localStorage = - await SharedPreferences.getInstance(); - if (geniusAccessToken.value != null && - geniusAccessToken.value!.isNotEmpty) { - preferences.setGeniusAccessToken( - geniusAccessToken.value!, - ); - localStorage.setString( - LocalStorageKeys.geniusAccessToken, - geniusAccessToken.value!); - } - - geniusAccessToken.value = null; - geniusTokenController.text = ""; - } - : null, - child: const Text("Save"), - ), - ) ], + onChanged: (value) { + if (value != null) { + preferences.setThemeMode(value); + } + }, ), - const SizedBox(height: 10), - if (!Platform.isAndroid && !Platform.isIOS) ...[ - SettingsHotKeyTile( - title: "Next track global shortcut", - currentHotKey: preferences.nextTrackHotKey, - onHotKeyRecorded: (value) { - preferences.setNextTrackHotKey(value); - }, - ), - SettingsHotKeyTile( - title: "Prev track global shortcut", - currentHotKey: preferences.prevTrackHotKey, - onHotKeyRecorded: (value) { - preferences.setPrevTrackHotKey(value); - }, - ), - SettingsHotKeyTile( - title: "Play/Pause global shortcut", - currentHotKey: preferences.playPauseHotKey, - onHotKeyRecorded: (value) { - preferences.setPlayPauseHotKey(value); - }, - ), - ], - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("Theme"), - DropdownButton( - value: preferences.themeMode, - items: const [ - DropdownMenuItem( - child: Text( - "Dark", - ), - value: ThemeMode.dark, - ), - DropdownMenuItem( - child: Text( - "Light", - ), - value: ThemeMode.light, - ), - DropdownMenuItem( - child: Text("System"), - value: ThemeMode.system, - ), - ], - onChanged: (value) { - if (value != null) { - preferences.setThemeMode(value); - } - }, - ) - ], + ), + const SizedBox(height: 10), + ListTile( + title: const Text("Accent Color Scheme"), + trailing: ColorTile( + color: preferences.accentColorScheme, + onPressed: pickColorScheme(ColorSchemeType.accent), + isActive: true, ), - const SizedBox(height: 10), - ListTile( - title: const Text("Accent Color Scheme"), - trailing: ColorTile( - color: preferences.accentColorScheme, - onPressed: pickColorScheme(ColorSchemeType.accent), - isActive: true, - ), - onTap: pickColorScheme(ColorSchemeType.accent), + onTap: pickColorScheme(ColorSchemeType.accent), + ), + const SizedBox(height: 10), + ListTile( + title: const Text("Background Color Scheme"), + trailing: ColorTile( + color: preferences.backgroundColorScheme, + onPressed: pickColorScheme(ColorSchemeType.background), + isActive: true, ), - const SizedBox(height: 10), - ListTile( - title: const Text("Background Color Scheme"), - trailing: ColorTile( - color: preferences.backgroundColorScheme, - onPressed: - pickColorScheme(ColorSchemeType.background), - isActive: true, - ), - onTap: pickColorScheme(ColorSchemeType.background), - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + onTap: pickColorScheme(ColorSchemeType.background), + ), + const SizedBox(height: 10), + ListTile( + title: const Text("Market Place (Recommendation Country)"), - DropdownButton( - value: preferences.recommendationMarket, - items: spotifyMarkets - .map((country) => (DropdownMenuItem( - child: Text(country), - value: country, - ))) - .toList(), - onChanged: (value) { - if (value == null) return; - preferences - .setRecommendationMarket(value as String); - }, - ), - ], + trailing: DropdownButton( + value: preferences.recommendationMarket, + items: spotifyMarkets + .map((country) => (DropdownMenuItem( + child: Text(country), + value: country, + ))) + .toList(), + onChanged: (value) { + if (value == null) return; + preferences.setRecommendationMarket(value as String); + }, ), - const SizedBox(height: 10), - Row( + ), + ListTile( + title: const Text("Download lyrics along with the Track"), + trailing: Switch.adaptive( + activeColor: Theme.of(context).primaryColor, + value: preferences.saveTrackLyrics, + onChanged: (state) { + preferences.setSaveTrackLyrics(state); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text("Download lyrics along with the Track"), - Switch.adaptive( - activeColor: Theme.of(context).primaryColor, - value: preferences.saveTrackLyrics, - onChanged: (state) { - preferences.setSaveTrackLyrics(state); - }, - ), - ], - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( + Expanded( flex: 2, child: Text( - "Format of the YouTube Search term (Case sensitive)"), + "Format of the YouTube Search term (Case sensitive)", + style: Theme.of(context).textTheme.bodyText1, + ), ), Expanded( flex: 1, @@ -243,74 +174,56 @@ class Settings extends HookConsumerWidget { ), ], ), - const SizedBox(height: 10), - if (auth.isAnonymous) - Wrap( - spacing: 20, - runSpacing: 20, - alignment: WrapAlignment.spaceBetween, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - const Text("Login with your Spotify account"), - ElevatedButton( - child: Text("Connect with Spotify".toUpperCase()), - onPressed: () { - GoRouter.of(context).push("/login"); - }, - style: ButtonStyle( - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25.0), - ), - ), + ), + if (auth.isAnonymous) + ListTile( + title: const Text("Login with your Spotify account"), + trailing: ElevatedButton( + child: Text("Connect with Spotify".toUpperCase()), + onPressed: () { + GoRouter.of(context).push("/login"); + }, + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25.0), ), - ) - ], - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - flex: 2, - child: Text("Check for Update)"), + ), ), - Switch.adaptive( - activeColor: Theme.of(context).primaryColor, - value: preferences.checkUpdate, - onChanged: (checked) => - preferences.setCheckUpdate(checked), - ) - ], + ), ), - if (auth.isLoggedIn) - Builder(builder: (context) { - Auth auth = ref.watch(authProvider); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("Log out of this account"), - ElevatedButton( - child: const Text("Logout"), - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Colors.red), - ), - onPressed: () async { - SharedPreferences localStorage = - await SharedPreferences.getInstance(); - await localStorage.clear(); - auth.logout(); - GoRouter.of(context).pop(); - }, - ), - ], - ); - }), - const SizedBox(height: 40), - const About(), - ], - ), + ListTile( + title: const Text("Check for Update"), + trailing: Switch.adaptive( + activeColor: Theme.of(context).primaryColor, + value: preferences.checkUpdate, + onChanged: (checked) => + preferences.setCheckUpdate(checked), + ), + ), + if (auth.isLoggedIn) + Builder(builder: (context) { + Auth auth = ref.watch(authProvider); + return ListTile( + title: const Text("Log out of this account"), + trailing: ElevatedButton( + child: const Text("Logout"), + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.red), + ), + onPressed: () async { + SharedPreferences localStorage = + await SharedPreferences.getInstance(); + await localStorage.clear(); + auth.logout(); + GoRouter.of(context).pop(); + }, + ), + ); + }), + const About(), + ], ), ), ), diff --git a/lib/components/Settings/SettingsHotkeyTile.dart b/lib/components/Settings/SettingsHotkeyTile.dart index d3ad0a02..a1f32abf 100644 --- a/lib/components/Settings/SettingsHotkeyTile.dart +++ b/lib/components/Settings/SettingsHotkeyTile.dart @@ -15,32 +15,26 @@ class SettingsHotKeyTile extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return ListTile( + title: Text(title), + trailing: Row( + mainAxisSize: MainAxisSize.min, children: [ - Text(title), - Row( - children: [ - if (currentHotKey != null) - HotKeyVirtualView(hotKey: currentHotKey!), - const SizedBox(width: 10), - ElevatedButton( - child: const Text("Set Shortcut"), - onPressed: () { - showDialog( - context: context, - builder: (context) { - return RecordHotKeyDialog( - onHotKeyRecorded: onHotKeyRecorded, - ); - }, + if (currentHotKey != null) HotKeyVirtualView(hotKey: currentHotKey!), + const SizedBox(width: 10), + ElevatedButton( + child: const Text("Set Shortcut"), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return RecordHotKeyDialog( + onHotKeyRecorded: onHotKeyRecorded, ); }, - ), - ], - ) + ); + }, + ), ], ), ); diff --git a/lib/hooks/usePaginatedFutureProvider.dart b/lib/hooks/usePaginatedFutureProvider.dart index d180e21f..b212bc04 100644 --- a/lib/hooks/usePaginatedFutureProvider.dart +++ b/lib/hooks/usePaginatedFutureProvider.dart @@ -4,7 +4,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:spotube/hooks/usePagingController.dart'; PagingController usePaginatedFutureProvider( - AutoDisposeFutureProvider Function(P pageKey) createSnapshot, { + FutureProvider Function(P pageKey) createSnapshot, { required P firstPageKey, required WidgetRef ref, void Function( diff --git a/lib/provider/SpotifyRequests.dart b/lib/provider/SpotifyRequests.dart index 5a15932b..12af9a80 100644 --- a/lib/provider/SpotifyRequests.dart +++ b/lib/provider/SpotifyRequests.dart @@ -1,9 +1,16 @@ +import 'dart:convert'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotube/helpers/getLyrics.dart'; +import 'package:spotube/helpers/timed-lyrics.dart'; +import 'package:spotube/models/SpotubeTrack.dart'; +import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/UserPreferences.dart'; +import 'package:collection/collection.dart'; -final categoriesQuery = FutureProvider.autoDispose.family, int>( +final categoriesQuery = FutureProvider.family, int>( (ref, pageKey) { final spotify = ref.watch(spotifyProvider); final recommendationMarket = ref.watch( @@ -16,7 +23,7 @@ final categoriesQuery = FutureProvider.autoDispose.family, int>( ); final categoryPlaylistsQuery = - FutureProvider.autoDispose.family, String>( + FutureProvider.family, String>( (ref, value) { final spotify = ref.watch(spotifyProvider); final List data = value.split("/"); @@ -44,22 +51,21 @@ final currentUserAlbumsQuery = FutureProvider>( ); final currentUserFollowingArtistsQuery = - FutureProvider.autoDispose.family, String>( + FutureProvider.family, String>( (ref, pageKey) { final spotify = ref.watch(spotifyProvider); return spotify.me.following(FollowingType.artist).getPage(15, pageKey); }, ); -final artistProfileQuery = FutureProvider.autoDispose.family( +final artistProfileQuery = FutureProvider.family( (ref, id) { final spotify = ref.watch(spotifyProvider); return spotify.artists.get(id); }, ); -final currentUserFollowsArtistQuery = - FutureProvider.autoDispose.family( +final currentUserFollowsArtistQuery = FutureProvider.family( (ref, artistId) async { final spotify = ref.watch(spotifyProvider); final result = await spotify.me.isFollowing( @@ -71,13 +77,12 @@ final currentUserFollowsArtistQuery = ); final artistTopTracksQuery = - FutureProvider.autoDispose.family, String>((ref, id) { + FutureProvider.family, String>((ref, id) { final spotify = ref.watch(spotifyProvider); return spotify.artists.getTopTracks(id, "US"); }); -final artistAlbumsQuery = - FutureProvider.autoDispose.family, String>( +final artistAlbumsQuery = FutureProvider.family, String>( (ref, id) { final spotify = ref.watch(spotifyProvider); return spotify.artists.albums(id).getPage(5, 0); @@ -85,9 +90,86 @@ final artistAlbumsQuery = ); final artistRelatedArtistsQuery = - FutureProvider.autoDispose.family, String>( + FutureProvider.family, String>( (ref, id) { final spotify = ref.watch(spotifyProvider); return spotify.artists.getRelatedArtists(id); }, ); + +final playlistTracksQuery = FutureProvider.family, String>( + (ref, id) { + 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(), + ); + }, +); + +final albumTracksQuery = FutureProvider.family, String>( + (ref, id) { + final spotify = ref.watch(spotifyProvider); + return spotify.albums.getTracks(id).all().then((value) => value.toList()); + }, +); + +final currentUserQuery = FutureProvider( + (ref) { + final spotify = ref.watch(spotifyProvider); + return spotify.me.get(); + }, +); + +final playlistIsFollowedQuery = FutureProvider.family( + (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 albumIsSavedForCurrentUserQuery = + FutureProvider.family((ref, albumId) { + final spotify = ref.watch(spotifyProvider); + return spotify.me.isSavedAlbums([albumId]).then((value) => value.first); +}); + +final searchQuery = FutureProvider.family, String>((ref, term) { + final spotify = ref.watch(spotifyProvider); + if (term.isEmpty) return []; + return spotify.search.get(term).first(10); +}); + +final geniusLyricsQuery = FutureProvider( + (ref) { + final currentTrack = + ref.watch(playbackProvider.select((s) => s.currentTrack)); + final geniusAccessToken = + ref.watch(userPreferencesProvider.select((s) => s.geniusAccessToken)); + if (currentTrack == null) { + return "“Give this player a track to play”\n- S'Challa"; + } + return getLyrics( + currentTrack.name!, + currentTrack.artists?.map((s) => s.name).whereNotNull().toList() ?? [], + apiKey: geniusAccessToken, + optimizeQuery: true, + ); + }, +); + +final rentanadviserLyricsQuery = FutureProvider( + (ref) { + final currentTrack = + ref.watch(playbackProvider.select((s) => s.currentTrack)); + if (currentTrack == null) return null; + return getTimedLyrics(currentTrack as SpotubeTrack); + }, +); diff --git a/lib/provider/UserPreferences.dart b/lib/provider/UserPreferences.dart index cb0bde6b..701b8517 100644 --- a/lib/provider/UserPreferences.dart +++ b/lib/provider/UserPreferences.dart @@ -136,9 +136,15 @@ class UserPreferences extends PersistedChangeNotifier { "saveTrackLyrics": saveTrackLyrics, "recommendationMarket": recommendationMarket, "geniusAccessToken": geniusAccessToken, - "nextTrackHotKey": jsonEncode(nextTrackHotKey?.toJson() ?? {}), - "prevTrackHotKey": jsonEncode(prevTrackHotKey?.toJson() ?? {}), - "playPauseHotKey": jsonEncode(playPauseHotKey?.toJson() ?? {}), + "nextTrackHotKey": nextTrackHotKey != null + ? jsonEncode(nextTrackHotKey?.toJson()) + : null, + "prevTrackHotKey": prevTrackHotKey != null + ? jsonEncode(prevTrackHotKey?.toJson()) + : null, + "playPauseHotKey": playPauseHotKey != null + ? jsonEncode(playPauseHotKey?.toJson()) + : null, "ytSearchFormat": ytSearchFormat, "themeMode": themeMode.index, "backgroundColorScheme": backgroundColorScheme.value,