From b05efd3d361bc39cda63f62a51357e9bbfe013dd Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 2 Jun 2022 22:45:56 +0600 Subject: [PATCH 1/3] About dialog created Settings page now scrollable Playlist Create Dialog now scrollable --- .../Playlist/PlaylistCreateDialog.dart | 4 +- lib/components/Settings/About.dart | 78 +++++++++++++++++++ lib/components/Settings/Settings.dart | 40 +--------- pubspec.lock | 2 +- 4 files changed, 84 insertions(+), 40 deletions(-) create mode 100644 lib/components/Settings/About.dart diff --git a/lib/components/Playlist/PlaylistCreateDialog.dart b/lib/components/Playlist/PlaylistCreateDialog.dart index ff3f4ea8..45ca6161 100644 --- a/lib/components/Playlist/PlaylistCreateDialog.dart +++ b/lib/components/Playlist/PlaylistCreateDialog.dart @@ -60,8 +60,8 @@ class PlaylistCreateDialog extends HookConsumerWidget { content: Container( width: MediaQuery.of(context).size.width, constraints: const BoxConstraints(maxWidth: 500), - child: Column( - mainAxisSize: MainAxisSize.min, + child: ListView( + shrinkWrap: true, children: [ TextField( controller: playlistName, diff --git a/lib/components/Settings/About.dart b/lib/components/Settings/About.dart new file mode 100644 index 00000000..48ce86bb --- /dev/null +++ b/lib/components/Settings/About.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/components/Shared/Hyperlink.dart'; +import 'package:spotube/hooks/usePackageInfo.dart'; + +const licenseText = """ +BSD-4-Clause License + +Copyright (c) 2022 Kingkor Roy Tirtho. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software must display the following acknowledgement: +This product includes software developed by Kingkor Roy Tirtho. +4. Neither the name of the Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY KINGKOR ROY TIRTHO AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KINGKOR ROY TIRTHO AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""; + +class About extends HookWidget { + const About({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final info = usePackageInfo( + appName: "Spotube", + packageName: "oss.krtirtho.Spotube", + version: "2.1.0"); + + return ListTile( + title: const Text("About Spotube"), + onTap: () { + showAboutDialog( + context: context, + applicationIcon: + CircleAvatar(child: Image.asset("assets/spotube-logo.png")), + applicationName: "Spotube", + applicationVersion: info.version, + applicationLegalese: licenseText, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text("Author: "), + Hyperlink( + "Kingkor Roy Tirtho", + "https://github.com/KRTirtho", + ), + ], + ), + const SizedBox(height: 20), + Wrap( + alignment: WrapAlignment.center, + children: const [ + Hyperlink( + "💚 Sponsor/Donate 💚", + "https://opencollective.com/spotube", + ), + Text(" • "), + Hyperlink( + "BSD-4-Clause LICENSE", + "https://github.com/KRTirtho/spotube/blob/master/LICENSE", + ), + Text(" • "), + Hyperlink( + "Bug Report", + "https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=", + ), + ], + ), + const SizedBox(height: 10), + const Center(child: Text("© Spotube 2022. All rights reserved")) + ]); + }, + ); + } +} diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart index 72f86dac..5a58657b 100644 --- a/lib/components/Settings/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -5,6 +5,7 @@ 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/Settings/About.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/SettingsHotkeyTile.dart'; import 'package:spotube/components/Shared/Hyperlink.dart'; @@ -67,7 +68,7 @@ class Settings extends HookConsumerWidget { constraints: const BoxConstraints(maxWidth: 1366), child: Padding( padding: const EdgeInsets.all(16.0), - child: Column( + child: ListView( children: [ Row( children: [ @@ -307,42 +308,7 @@ class Settings extends HookConsumerWidget { ); }), const SizedBox(height: 40), - Text( - "Spotube v${packageInfo.version}", - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Text("Author: "), - Hyperlink( - "Kingkor Roy Tirtho", - "https://github.com/KRTirtho", - ), - ], - ), - const SizedBox(height: 20), - Wrap( - alignment: WrapAlignment.center, - children: const [ - Hyperlink( - "💚 Sponsor/Donate 💚", - "https://opencollective.com/spotube", - ), - Text(" • "), - Hyperlink( - "BSD-4-Clause LICENSE", - "https://github.com/KRTirtho/spotube/blob/master/LICENSE", - ), - Text(" • "), - Hyperlink( - "Bug Report", - "https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=", - ), - ], - ), - const SizedBox(height: 10), - const Text("© Spotube 2022. All rights reserved") + const About(), ], ), ), diff --git a/pubspec.lock b/pubspec.lock index c7f4c92f..823843af 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -51,7 +51,7 @@ packages: source: hosted version: "0.1.1" audio_session: - dependency: "direct main" + dependency: transitive description: name: audio_session url: "https://pub.dartlang.org" From e3c7b83ae012114c02abacbb4fe86975497c8124 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 3 Jun 2022 11:41:21 +0600 Subject: [PATCH 2/3] share playlists & track support added --- lib/components/Playlist/PlaylistView.dart | 22 +++++++++++++++++ lib/components/Shared/TrackTile.dart | 30 +++++++++++++++++++++++ lib/themes/dark-theme.dart | 8 +++++- lib/themes/light-theme.dart | 7 ++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/lib/components/Playlist/PlaylistView.dart b/lib/components/Playlist/PlaylistView.dart index 5f7c2597..d63bc3ea 100644 --- a/lib/components/Playlist/PlaylistView.dart +++ b/lib/components/Playlist/PlaylistView.dart @@ -1,3 +1,4 @@ +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'; @@ -109,6 +110,27 @@ class PlaylistView extends HookConsumerWidget { }, ); }), + 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( diff --git a/lib/components/Shared/TrackTile.dart b/lib/components/Shared/TrackTile.dart index a42eacf1..4f5ed5d5 100644 --- a/lib/components/Shared/TrackTile.dart +++ b/lib/components/Shared/TrackTile.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -57,6 +58,22 @@ class TrackTile extends HookConsumerWidget { return await spotify.playlists.removeTrack(track.value.uri!, playlistId!); }, [playlistId, spotify, track.value.uri]); + void actionShare(Track track) { + final data = "https://open.spotify.com/track/${track.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, + ), + ), + ); + }); + } + actionAddToPlaylist() async { showDialog( context: context, @@ -252,6 +269,16 @@ class TrackTile extends HookConsumerWidget { ], ), value: "favorite", + ), + PopupMenuItem( + child: Row( + children: const [ + Icon(Icons.share_rounded), + SizedBox(width: 10), + Text("Share") + ], + ), + value: "share", ) ]; }, @@ -266,6 +293,9 @@ class TrackTile extends HookConsumerWidget { case "remove-playlist": actionRemoveFromPlaylist(); break; + case "share": + actionShare(track.value); + break; } }, ); diff --git a/lib/themes/dark-theme.dart b/lib/themes/dark-theme.dart index 968a147f..bcf96aac 100644 --- a/lib/themes/dark-theme.dart +++ b/lib/themes/dark-theme.dart @@ -13,7 +13,6 @@ ThemeData darkTheme({ scaffoldBackgroundColor: backgroundMaterialColor[900], dialogBackgroundColor: backgroundMaterialColor[800], shadowColor: Colors.black26, - popupMenuTheme: PopupMenuThemeData(color: backgroundMaterialColor[800]), buttonTheme: ButtonThemeData( buttonColor: accentMaterialColor, ), @@ -56,6 +55,13 @@ ThemeData darkTheme({ ), ), ), + popupMenuTheme: PopupMenuThemeData( + color: backgroundMaterialColor[800], + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), cardColor: backgroundMaterialColor[800], canvasColor: backgroundMaterialColor[900], ); diff --git a/lib/themes/light-theme.dart b/lib/themes/light-theme.dart index 540def04..fe642365 100644 --- a/lib/themes/light-theme.dart +++ b/lib/themes/light-theme.dart @@ -81,6 +81,13 @@ ThemeData lightTheme({ ), ), ), + popupMenuTheme: PopupMenuThemeData( + color: backgroundMaterialColor[100], + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), cardColor: backgroundMaterialColor[50], canvasColor: backgroundMaterialColor[50], ); From b3b3acdb1ed3cd98c98847d3da6db2507071fffb Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 4 Jun 2022 14:06:52 +0600 Subject: [PATCH 3/3] 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,