Bugfix: ColorScheme not persisting

Response Caching support in multiple components PlayerView, AlbumView, Search, Lyrics, SyncedLyrics
This commit is contained in:
Kingkor Roy Tirtho 2022-06-04 14:06:52 +06:00
parent e3c7b83ae0
commit b3b3acdb1e
11 changed files with 489 additions and 608 deletions

View File

@ -6,11 +6,11 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/simple-track-to-track.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/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
class AlbumView extends HookConsumerWidget { class AlbumView extends HookConsumerWidget {
final AlbumSimple album; final AlbumSimple album;
@ -44,88 +44,88 @@ class AlbumView extends HookConsumerWidget {
final SpotifyApi spotify = ref.watch(spotifyProvider); final SpotifyApi spotify = ref.watch(spotifyProvider);
final Auth auth = ref.watch(authProvider); final Auth auth = ref.watch(authProvider);
final update = useForceUpdate(); final tracksSnapshot = ref.watch(albumTracksQuery(album.id!));
final albumSavedSnapshot =
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
body: FutureBuilder<Iterable<TrackSimple>>( body: Column(
future: spotify.albums.getTracks(album.id!).all(), children: [
builder: (context, snapshot) { PageWindowTitleBar(
List<Track> tracks = snapshot.data?.map((trackSmp) { leading: Row(
return simpleTrackToTrack(trackSmp, album);
}).toList() ??
[];
return Column(
children: [ children: [
PageWindowTitleBar( // nav back
leading: Row( const BackButton(),
children: [ // heart playlist
// nav back if (auth.isLoggedIn)
const BackButton(), albumSavedSnapshot.when(
// heart playlist data: (isSaved) {
if (auth.isLoggedIn) return HeartButton(
FutureBuilder<List<bool>>( isLiked: isSaved,
future: spotify.me.isSavedAlbums([album.id!]), onPressed: () {
builder: (context, snapshot) { (isSaved
final isSaved = snapshot.data?.first == true; ? spotify.me.removeAlbums(
if (!snapshot.hasData && !snapshot.hasError) { [album.id!],
return const SizedBox( )
height: 25, : spotify.me.saveAlbums(
width: 25, [album.id!],
child: CircularProgressIndicator.adaptive(), ))
); .whenComplete(() {
} ref.refresh(
return HeartButton( albumIsSavedForCurrentUserQuery(
isLiked: isSaved, album.id!,
onPressed: () { ),
(isSaved
? spotify.me.removeAlbums(
[album.id!],
)
: spotify.me.saveAlbums(
[album.id!],
))
.then((_) => update());
},
); );
}), ref.refresh(currentUserAlbumsQuery);
// play playlist });
IconButton( },
icon: Icon( );
isPlaylistPlaying },
? Icons.stop_rounded error: (error, _) => Text("Error $error"),
: Icons.play_arrow_rounded, loading: () => const CircularProgressIndicator()),
), // play playlist
onPressed: snapshot.hasData IconButton(
? () => playPlaylist(playback, tracks) icon: Icon(
: null, isPlaylistPlaying
) ? Icons.stop_rounded
], : Icons.play_arrow_rounded,
), ),
), onPressed: tracksSnapshot.asData?.value != null
Center( ? () => playPlaylist(
child: Text(album.name!, playback,
style: Theme.of(context).textTheme.headline4), tracksSnapshot.asData!.value.map((trackSmp) {
), return simpleTrackToTrack(trackSmp, album);
snapshot.hasError }).toList(),
? const Center(child: Text("Error occurred"))
: !snapshot.hasData
? const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive()),
) )
: TracksTableView( : null,
tracks, )
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
playback,
tracks,
currentTrack: currentTrack,
),
),
], ],
); ),
}), ),
Center(
child: Text(album.name!,
style: Theme.of(context).textTheme.headline4),
),
tracksSnapshot.when(
data: (data) {
List<Track> 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(),
),
],
),
), ),
); );
} }

View File

@ -1,13 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/getLyrics.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:collection/collection.dart';
class Lyrics extends HookConsumerWidget { class Lyrics extends HookConsumerWidget {
const Lyrics({Key? key}) : super(key: key); const Lyrics({Key? key}) : super(key: key);
@ -15,74 +12,8 @@ class Lyrics extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
UserPreferences userPreferences = ref.watch(userPreferencesProvider); final geniusLyricsSnapshot = ref.watch(geniusLyricsQuery);
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 breakpoint = useBreakpoints(); 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; final textTheme = Theme.of(context).textTheme;
return Expanded( return Expanded(
@ -110,13 +41,19 @@ class Lyrics extends HookConsumerWidget {
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: geniusLyricsSnapshot.when(
lyrics.value["lyrics"] == null && data: (lyrics) {
playback.currentTrack == null return Text(
? "No Track being played currently" lyrics == null && playback.currentTrack == null
: lyrics.value["lyrics"]!, ? "No Track being played currently"
style: textTheme.headline6 : lyrics!,
?.copyWith(color: textTheme.headline1?.color), style: textTheme.headline6
?.copyWith(color: textTheme.headline1?.color),
);
},
error: (error, __) => Text(
"Sorry, no Lyrics were found for `${playback.currentTrack?.name}` :'("),
loading: () => const CircularProgressIndicator(),
), ),
), ),
), ),

View File

@ -5,47 +5,33 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/Lyrics/Lyrics.dart'; import 'package:spotube/components/Lyrics/Lyrics.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
import 'package:spotube/helpers/artist-to-string.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/useAutoScrollController.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/useSyncedLyrics.dart'; import 'package:spotube/hooks/useSyncedLyrics.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
class SyncedLyrics extends HookConsumerWidget { class SyncedLyrics extends HookConsumerWidget {
const SyncedLyrics({Key? key}) : super(key: key); const SyncedLyrics({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final timedLyricsSnapshot = ref.watch(rentanadviserLyricsQuery);
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final controller = useAutoScrollController(); final controller = useAutoScrollController();
final failed = useState(false); final failed = useState(false);
final timedLyrics = useMemoized(() async { final lyricValue = timedLyricsSnapshot.asData?.value;
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 lyricsMap = useMemoized( final lyricsMap = useMemoized(
() => () =>
lyricsSnapshot.data?.lyrics lyricValue?.lyrics
.map((lyric) => {lyric.time.inSeconds: lyric.text}) .map((lyric) => {lyric.time.inSeconds: lyric.text})
.reduce((accumulator, lyricSlice) => .reduce((accumulator, lyricSlice) =>
{...accumulator, ...lyricSlice}) ?? {...accumulator, ...lyricSlice}) ??
{}, {},
[lyricsSnapshot.data], [lyricValue],
); );
final currentTime = useSyncedLyrics(ref, lyricsMap); final currentTime = useSyncedLyrics(ref, lyricsMap);
@ -54,11 +40,12 @@ class SyncedLyrics extends HookConsumerWidget {
useEffect(() { useEffect(() {
controller.scrollToIndex(0); controller.scrollToIndex(0);
failed.value = false;
return null; return null;
}, [playback.currentTrack]); }, [playback.currentTrack]);
useEffect(() { useEffect(() {
if (lyricsSnapshot.data != null && lyricsSnapshot.data!.rating <= 2) { if (lyricValue != null && lyricValue.rating <= 2) {
Future.delayed(const Duration(seconds: 5), () { Future.delayed(const Duration(seconds: 5), () {
showDialog( showDialog(
context: context, context: context,
@ -97,7 +84,7 @@ class SyncedLyrics extends HookConsumerWidget {
}); });
} }
return null; return null;
}, [lyricsSnapshot.data]); }, [lyricValue]);
// when synced lyrics not found, fallback to GeniusLyrics // when synced lyrics not found, fallback to GeniusLyrics
if (failed.value) return const Lyrics(); if (failed.value) return const Lyrics();
@ -130,12 +117,12 @@ class SyncedLyrics extends HookConsumerWidget {
: textTheme.headline6, : textTheme.headline6,
), ),
), ),
if (lyricsSnapshot.hasData) if (lyricValue != null)
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
controller: controller, controller: controller,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final lyricSlice = lyricsSnapshot.data!.lyrics[index]; final lyricSlice = lyricValue.lyrics[index];
final isActive = lyricSlice.time.inSeconds == currentTime; final isActive = lyricSlice.time.inSeconds == currentTime;
if (isActive) { if (isActive) {
controller.scrollToIndex( controller.scrollToIndex(
@ -164,7 +151,7 @@ class SyncedLyrics extends HookConsumerWidget {
), ),
); );
}, },
itemCount: lyricsSnapshot.data!.lyrics.length, itemCount: lyricValue.lyrics.length,
), ),
), ),
], ],

View File

@ -1,11 +1,11 @@
import 'dart:convert';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/components/Shared/HeartButton.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/helpers/image-to-url-string.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/CurrentPlaylist.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
@ -13,6 +13,7 @@ import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
class PlaylistView extends HookConsumerWidget { class PlaylistView extends HookConsumerWidget {
final logger = getLogger(PlaylistView); final logger = getLogger(PlaylistView);
@ -47,130 +48,120 @@ class PlaylistView extends HookConsumerWidget {
SpotifyApi spotify = ref.watch(spotifyProvider); SpotifyApi spotify = ref.watch(spotifyProvider);
final isPlaylistPlaying = playback.currentPlaylist?.id != null && final isPlaylistPlaying = playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == playlist.id; playback.currentPlaylist?.id == playlist.id;
final update = useForceUpdate();
final getMe = useMemoized(() => spotify.me.get(), []);
final meSnapshot = useFuture<User>(getMe);
Future<List<bool>> isFollowing(User me) { final meSnapshot = ref.watch(currentUserQuery);
return spotify.playlists.followedBy(playlist.id!, [me.id!]); final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
}
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
body: FutureBuilder<Iterable<Track>>( body: Column(
future: playlist.id != "user-liked-tracks" children: [
? spotify.playlists.getTracksByPlaylistId(playlist.id).all() PageWindowTitleBar(
: spotify.tracks.me.saved leading: Row(
.all()
.then((tracks) => tracks.map((e) => e.track!)),
builder: (context, snapshot) {
List<Track> tracks = snapshot.data?.toList() ?? [];
return Column(
children: [ children: [
PageWindowTitleBar( // nav back
leading: Row( const BackButton(),
children: [ // heart playlist
// nav back if (auth.isLoggedIn)
const BackButton(), meSnapshot.when(
// heart playlist data: (me) {
if (auth.isLoggedIn && meSnapshot.hasData) final query = playlistIsFollowedQuery(jsonEncode(
FutureBuilder<List<bool>>( {"playlistId": playlist.id, "userId": me.id!}));
future: isFollowing(meSnapshot.data!), final followingSnapshot = ref.watch(query);
builder: (context, snapshot) {
final isFollowing =
snapshot.data?.first ?? false;
if (!snapshot.hasData && !snapshot.hasError) { return followingSnapshot.when(
return const SizedBox( data: (isFollowing) {
height: 25, return HeartButton(
width: 25, isLiked: isFollowing,
child: CircularProgressIndicator.adaptive(), 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,
),
),
);
});
}, },
), error: (error, _) => Text("Error $error"),
// play playlist loading: () => const CircularProgressIndicator(),
IconButton( );
icon: Icon( },
isPlaylistPlaying error: (error, _) => Text("Error $error"),
? Icons.stop_rounded loading: () => const CircularProgressIndicator(),
: Icons.play_arrow_rounded,
),
onPressed: snapshot.hasData
? () => playPlaylist(playback, tracks)
: null,
)
],
), ),
),
Center( IconButton(
child: Text(playlist.name!, icon: const Icon(Icons.share_rounded),
style: Theme.of(context).textTheme.headline4), onPressed: () {
), final data =
snapshot.hasError "https://open.spotify.com/playlist/${playlist.id}";
? const Center(child: Text("Error occurred")) Clipboard.setData(
: !snapshot.hasData ClipboardData(text: data),
? const Expanded( ).then((_) {
child: Center( ScaffoldMessenger.of(context).showSnackBar(
child: CircularProgressIndicator.adaptive()), SnackBar(
) width: 300,
: TracksTableView( behavior: SnackBarBehavior.floating,
tracks, content: Text(
onTrackPlayButtonPressed: (currentTrack) => "Copied $data to clipboard",
playPlaylist( textAlign: TextAlign.center,
playback,
tracks,
currentTrack: currentTrack,
),
playlistId: playlist.id,
userPlaylist: playlist.owner?.id != null &&
playlist.owner!.id == meSnapshot.data?.id,
), ),
),
);
});
},
),
// 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(),
),
],
),
), ),
); );
} }

View File

@ -14,17 +14,19 @@ import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
final searchTermStateProvider = StateProvider<String>((ref) => "");
class Search extends HookConsumerWidget { class Search extends HookConsumerWidget {
const Search({Key? key}) : super(key: key); const Search({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final SpotifyApi spotify = ref.watch(spotifyProvider);
final Auth auth = ref.watch(authProvider); final Auth auth = ref.watch(authProvider);
final controller = useTextEditingController(); final searchTerm = ref.watch(searchTermStateProvider);
final searchTerm = useState(""); final controller =
useTextEditingController(text: ref.read(searchTermStateProvider));
final albumController = useScrollController(); final albumController = useScrollController();
final playlistController = useScrollController(); final playlistController = useScrollController();
final artistController = useScrollController(); final artistController = useScrollController();
@ -33,6 +35,7 @@ class Search extends HookConsumerWidget {
if (auth.isAnonymous) { if (auth.isAnonymous) {
return const Expanded(child: AnonymousFallback()); return const Expanded(child: AnonymousFallback());
} }
final searchSnapshot = ref.watch(searchQuery(searchTerm));
return Expanded( return Expanded(
child: Container( child: Container(
@ -48,7 +51,8 @@ class Search extends HookConsumerWidget {
decoration: const InputDecoration(hintText: "Search..."), decoration: const InputDecoration(hintText: "Search..."),
controller: controller, controller: controller,
onSubmitted: (value) { 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, textColor: Colors.white,
child: const Icon(Icons.search_rounded), child: const Icon(Icons.search_rounded),
onPressed: () { onPressed: () {
searchTerm.value = controller.value.text; ref.read(searchTermStateProvider.notifier).state =
controller.value.text;
}, },
), ),
], ],
), ),
), ),
FutureBuilder<List<Page>>( searchSnapshot.when(
future: searchTerm.value.isNotEmpty data: (data) {
? 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();
}
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
List<AlbumSimple> albums = []; List<AlbumSimple> albums = [];
List<Artist> artists = []; List<Artist> artists = [];
List<Track> tracks = []; List<Track> tracks = [];
List<PlaylistSimple> playlists = []; List<PlaylistSimple> playlists = [];
for (MapEntry<int, Page> page for (MapEntry<int, Page> page in data.asMap().entries) {
in snapshot.data?.asMap().entries ?? []) {
for (var item in page.value.items ?? []) { for (var item in page.value.items ?? []) {
if (item is AlbumSimple) { if (item is AlbumSimple) {
albums.add(item); albums.add(item);
@ -217,6 +211,8 @@ class Search extends HookConsumerWidget {
), ),
); );
}, },
error: (error, __) => Text("Error $error"),
loading: () => const CircularProgressIndicator(),
) )
], ],
), ),

View File

@ -2,15 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/helpers/oauth-login.dart'; import 'package:spotube/helpers/oauth-login.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/UserPreferences.dart';
class Login extends HookConsumerWidget { class Login extends HookConsumerWidget {
Login({Key? key}) : super(key: key); Login({Key? key}) : super(key: key);
@ -21,7 +18,6 @@ class Login extends HookConsumerWidget {
Auth authState = ref.watch(authProvider); Auth authState = ref.watch(authProvider);
final clientIdController = useTextEditingController(); final clientIdController = useTextEditingController();
final clientSecretController = useTextEditingController(); final clientSecretController = useTextEditingController();
final accessTokenController = useTextEditingController();
final fieldError = useState(false); final fieldError = useState(false);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
@ -90,31 +86,10 @@ class Login extends HookConsumerWidget {
), ),
controller: clientSecretController, controller: clientSecretController,
), ),
const SizedBox(height: 10), const SizedBox(height: 20),
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,
),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
await handleLogin(authState); 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"), child: const Text("Submit"),
) )

View File

@ -8,10 +8,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Settings/About.dart'; import 'package:spotube/components/Settings/About.dart';
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
import 'package:spotube/components/Settings/SettingsHotkeyTile.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/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/models/SpotifyMarkets.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
@ -23,24 +20,13 @@ class Settings extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final UserPreferences preferences = ref.watch(userPreferencesProvider); final UserPreferences preferences = ref.watch(userPreferencesProvider);
final Auth auth = ref.watch(authProvider); final Auth auth = ref.watch(authProvider);
final geniusAccessToken = useState<String?>(null);
TextEditingController geniusTokenController = useTextEditingController();
final ytSearchFormatController = final ytSearchFormatController =
useTextEditingController(text: preferences.ytSearchFormat); useTextEditingController(text: preferences.ytSearchFormat);
geniusTokenController.addListener(() {
geniusAccessToken.value = geniusTokenController.value.text;
});
ytSearchFormatController.addListener(() { ytSearchFormatController.addListener(() {
preferences.setYtSearchFormat(ytSearchFormatController.value.text); preferences.setYtSearchFormat(ytSearchFormatController.value.text);
}); });
final packageInfo = usePackageInfo(
appName: 'Spotube',
packageName: 'spotube',
);
final pickColorScheme = useCallback((ColorSchemeType schemeType) { final pickColorScheme = useCallback((ColorSchemeType schemeType) {
return () => showDialog( return () => showDialog(
context: context, context: context,
@ -66,174 +52,119 @@ class Settings extends HookConsumerWidget {
Flexible( Flexible(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 1366), constraints: const BoxConstraints(maxWidth: 1366),
child: Padding( child: ListView(
padding: const EdgeInsets.all(16.0), children: [
child: ListView( if (!Platform.isAndroid && !Platform.isIOS) ...[
children: [ SettingsHotKeyTile(
Row( title: "Next track global shortcut",
children: [ currentHotKey: preferences.nextTrackHotKey,
Expanded( onHotKeyRecorded: (value) {
flex: 2, 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<ThemeMode>(
value: preferences.themeMode,
items: const [
DropdownMenuItem(
child: Text( child: Text(
"Genius Access Token", "Dark",
style: Theme.of(context).textTheme.subtitle1,
), ),
value: ThemeMode.dark,
), ),
Expanded( DropdownMenuItem(
flex: 1, child: Text(
child: TextField( "Light",
controller: geniusTokenController,
decoration: InputDecoration(
hintText: preferences.geniusAccessToken,
),
), ),
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) ...[ const SizedBox(height: 10),
SettingsHotKeyTile( ListTile(
title: "Next track global shortcut", title: const Text("Accent Color Scheme"),
currentHotKey: preferences.nextTrackHotKey, trailing: ColorTile(
onHotKeyRecorded: (value) { color: preferences.accentColorScheme,
preferences.setNextTrackHotKey(value); onPressed: pickColorScheme(ColorSchemeType.accent),
}, isActive: true,
),
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<ThemeMode>(
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), onTap: pickColorScheme(ColorSchemeType.accent),
ListTile( ),
title: const Text("Accent Color Scheme"), const SizedBox(height: 10),
trailing: ColorTile( ListTile(
color: preferences.accentColorScheme, title: const Text("Background Color Scheme"),
onPressed: pickColorScheme(ColorSchemeType.accent), trailing: ColorTile(
isActive: true, color: preferences.backgroundColorScheme,
), onPressed: pickColorScheme(ColorSchemeType.background),
onTap: pickColorScheme(ColorSchemeType.accent), isActive: true,
), ),
const SizedBox(height: 10), onTap: pickColorScheme(ColorSchemeType.background),
ListTile( ),
title: const Text("Background Color Scheme"), const SizedBox(height: 10),
trailing: ColorTile( ListTile(
color: preferences.backgroundColorScheme, title:
onPressed:
pickColorScheme(ColorSchemeType.background),
isActive: true,
),
onTap: pickColorScheme(ColorSchemeType.background),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Market Place (Recommendation Country)"), const Text("Market Place (Recommendation Country)"),
DropdownButton( trailing: DropdownButton(
value: preferences.recommendationMarket, value: preferences.recommendationMarket,
items: spotifyMarkets items: spotifyMarkets
.map((country) => (DropdownMenuItem( .map((country) => (DropdownMenuItem(
child: Text(country), child: Text(country),
value: country, value: country,
))) )))
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
preferences preferences.setRecommendationMarket(value as String);
.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, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text("Download lyrics along with the Track"), Expanded(
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(
flex: 2, flex: 2,
child: Text( 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( Expanded(
flex: 1, flex: 1,
@ -243,74 +174,56 @@ class Settings extends HookConsumerWidget {
), ),
], ],
), ),
const SizedBox(height: 10), ),
if (auth.isAnonymous) if (auth.isAnonymous)
Wrap( ListTile(
spacing: 20, title: const Text("Login with your Spotify account"),
runSpacing: 20, trailing: ElevatedButton(
alignment: WrapAlignment.spaceBetween, child: Text("Connect with Spotify".toUpperCase()),
crossAxisAlignment: WrapCrossAlignment.center, onPressed: () {
children: [ GoRouter.of(context).push("/login");
const Text("Login with your Spotify account"), },
ElevatedButton( style: ButtonStyle(
child: Text("Connect with Spotify".toUpperCase()), shape: MaterialStateProperty.all(
onPressed: () { RoundedRectangleBorder(
GoRouter.of(context).push("/login"); borderRadius: BorderRadius.circular(25.0),
},
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) ListTile(
Builder(builder: (context) { title: const Text("Check for Update"),
Auth auth = ref.watch(authProvider); trailing: Switch.adaptive(
return Row( activeColor: Theme.of(context).primaryColor,
mainAxisAlignment: MainAxisAlignment.spaceBetween, value: preferences.checkUpdate,
children: [ onChanged: (checked) =>
const Text("Log out of this account"), preferences.setCheckUpdate(checked),
ElevatedButton( ),
child: const Text("Logout"), ),
style: ButtonStyle( if (auth.isLoggedIn)
backgroundColor: Builder(builder: (context) {
MaterialStateProperty.all(Colors.red), Auth auth = ref.watch(authProvider);
), return ListTile(
onPressed: () async { title: const Text("Log out of this account"),
SharedPreferences localStorage = trailing: ElevatedButton(
await SharedPreferences.getInstance(); child: const Text("Logout"),
await localStorage.clear(); style: ButtonStyle(
auth.logout(); backgroundColor:
GoRouter.of(context).pop(); MaterialStateProperty.all(Colors.red),
}, ),
), onPressed: () async {
], SharedPreferences localStorage =
); await SharedPreferences.getInstance();
}), await localStorage.clear();
const SizedBox(height: 40), auth.logout();
const About(), GoRouter.of(context).pop();
], },
), ),
);
}),
const About(),
],
), ),
), ),
), ),

View File

@ -15,32 +15,26 @@ class SettingsHotKeyTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return ListTile(
padding: const EdgeInsets.symmetric(vertical: 10), title: Text(title),
child: Row( trailing: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(title), if (currentHotKey != null) HotKeyVirtualView(hotKey: currentHotKey!),
Row( const SizedBox(width: 10),
children: [ ElevatedButton(
if (currentHotKey != null) child: const Text("Set Shortcut"),
HotKeyVirtualView(hotKey: currentHotKey!), onPressed: () {
const SizedBox(width: 10), showDialog(
ElevatedButton( context: context,
child: const Text("Set Shortcut"), builder: (context) {
onPressed: () { return RecordHotKeyDialog(
showDialog( onHotKeyRecorded: onHotKeyRecorded,
context: context,
builder: (context) {
return RecordHotKeyDialog(
onHotKeyRecorded: onHotKeyRecorded,
);
},
); );
}, },
), );
], },
) ),
], ],
), ),
); );

View File

@ -4,7 +4,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:spotube/hooks/usePagingController.dart'; import 'package:spotube/hooks/usePagingController.dart';
PagingController<P, ItemType> usePaginatedFutureProvider<T, P, ItemType>( PagingController<P, ItemType> usePaginatedFutureProvider<T, P, ItemType>(
AutoDisposeFutureProvider<T> Function(P pageKey) createSnapshot, { FutureProvider<T> Function(P pageKey) createSnapshot, {
required P firstPageKey, required P firstPageKey,
required WidgetRef ref, required WidgetRef ref,
void Function( void Function(

View File

@ -1,9 +1,16 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart'; 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:spotube/provider/SpotifyDI.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:collection/collection.dart';
final categoriesQuery = FutureProvider.autoDispose.family<Page<Category>, int>( final categoriesQuery = FutureProvider.family<Page<Category>, int>(
(ref, pageKey) { (ref, pageKey) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final recommendationMarket = ref.watch( final recommendationMarket = ref.watch(
@ -16,7 +23,7 @@ final categoriesQuery = FutureProvider.autoDispose.family<Page<Category>, int>(
); );
final categoryPlaylistsQuery = final categoryPlaylistsQuery =
FutureProvider.autoDispose.family<Page<PlaylistSimple>, String>( FutureProvider.family<Page<PlaylistSimple>, String>(
(ref, value) { (ref, value) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final List data = value.split("/"); final List data = value.split("/");
@ -44,22 +51,21 @@ final currentUserAlbumsQuery = FutureProvider<Iterable<AlbumSimple>>(
); );
final currentUserFollowingArtistsQuery = final currentUserFollowingArtistsQuery =
FutureProvider.autoDispose.family<CursorPage<Artist>, String>( FutureProvider.family<CursorPage<Artist>, String>(
(ref, pageKey) { (ref, pageKey) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.me.following(FollowingType.artist).getPage(15, pageKey); return spotify.me.following(FollowingType.artist).getPage(15, pageKey);
}, },
); );
final artistProfileQuery = FutureProvider.autoDispose.family<Artist, String>( final artistProfileQuery = FutureProvider.family<Artist, String>(
(ref, id) { (ref, id) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.artists.get(id); return spotify.artists.get(id);
}, },
); );
final currentUserFollowsArtistQuery = final currentUserFollowsArtistQuery = FutureProvider.family<bool, String>(
FutureProvider.autoDispose.family<bool, String>(
(ref, artistId) async { (ref, artistId) async {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final result = await spotify.me.isFollowing( final result = await spotify.me.isFollowing(
@ -71,13 +77,12 @@ final currentUserFollowsArtistQuery =
); );
final artistTopTracksQuery = final artistTopTracksQuery =
FutureProvider.autoDispose.family<Iterable<Track>, String>((ref, id) { FutureProvider.family<Iterable<Track>, String>((ref, id) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.artists.getTopTracks(id, "US"); return spotify.artists.getTopTracks(id, "US");
}); });
final artistAlbumsQuery = final artistAlbumsQuery = FutureProvider.family<Page<Album>, String>(
FutureProvider.autoDispose.family<Page<Album>, String>(
(ref, id) { (ref, id) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.artists.albums(id).getPage(5, 0); return spotify.artists.albums(id).getPage(5, 0);
@ -85,9 +90,86 @@ final artistAlbumsQuery =
); );
final artistRelatedArtistsQuery = final artistRelatedArtistsQuery =
FutureProvider.autoDispose.family<Iterable<Artist>, String>( FutureProvider.family<Iterable<Artist>, String>(
(ref, id) { (ref, id) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return spotify.artists.getRelatedArtists(id); return spotify.artists.getRelatedArtists(id);
}, },
); );
final playlistTracksQuery = FutureProvider.family<List<Track>, 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<List<TrackSimple>, String>(
(ref, id) {
final spotify = ref.watch(spotifyProvider);
return spotify.albums.getTracks(id).all().then((value) => value.toList());
},
);
final currentUserQuery = FutureProvider<User>(
(ref) {
final spotify = ref.watch(spotifyProvider);
return spotify.me.get();
},
);
final playlistIsFollowedQuery = FutureProvider.family<bool, String>(
(ref, raw) {
final data = jsonDecode(raw);
final playlistId = data["playlistId"] as String;
final userId = data["userId"] as String;
final spotify = ref.watch(spotifyProvider);
return spotify.playlists
.followedBy(playlistId, [userId]).then((value) => value.first);
},
);
final albumIsSavedForCurrentUserQuery =
FutureProvider.family<bool, String>((ref, albumId) {
final spotify = ref.watch(spotifyProvider);
return spotify.me.isSavedAlbums([albumId]).then((value) => value.first);
});
final searchQuery = FutureProvider.family<List<Page>, String>((ref, term) {
final spotify = ref.watch(spotifyProvider);
if (term.isEmpty) return [];
return spotify.search.get(term).first(10);
});
final geniusLyricsQuery = FutureProvider<String?>(
(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<SubtitleSimple?>(
(ref) {
final currentTrack =
ref.watch(playbackProvider.select((s) => s.currentTrack));
if (currentTrack == null) return null;
return getTimedLyrics(currentTrack as SpotubeTrack);
},
);

View File

@ -136,9 +136,15 @@ class UserPreferences extends PersistedChangeNotifier {
"saveTrackLyrics": saveTrackLyrics, "saveTrackLyrics": saveTrackLyrics,
"recommendationMarket": recommendationMarket, "recommendationMarket": recommendationMarket,
"geniusAccessToken": geniusAccessToken, "geniusAccessToken": geniusAccessToken,
"nextTrackHotKey": jsonEncode(nextTrackHotKey?.toJson() ?? {}), "nextTrackHotKey": nextTrackHotKey != null
"prevTrackHotKey": jsonEncode(prevTrackHotKey?.toJson() ?? {}), ? jsonEncode(nextTrackHotKey?.toJson())
"playPauseHotKey": jsonEncode(playPauseHotKey?.toJson() ?? {}), : null,
"prevTrackHotKey": prevTrackHotKey != null
? jsonEncode(prevTrackHotKey?.toJson())
: null,
"playPauseHotKey": playPauseHotKey != null
? jsonEncode(playPauseHotKey?.toJson())
: null,
"ytSearchFormat": ytSearchFormat, "ytSearchFormat": ytSearchFormat,
"themeMode": themeMode.index, "themeMode": themeMode.index,
"backgroundColorScheme": backgroundColorScheme.value, "backgroundColorScheme": backgroundColorScheme.value,