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,18 +44,13 @@ 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(),
builder: (context, snapshot) {
List<Track> tracks = snapshot.data?.map((trackSmp) {
return simpleTrackToTrack(trackSmp, album);
}).toList() ??
[];
return Column(
children: [ children: [
PageWindowTitleBar( PageWindowTitleBar(
leading: Row( leading: Row(
@ -64,17 +59,8 @@ class AlbumView extends HookConsumerWidget {
const BackButton(), const BackButton(),
// heart playlist // heart playlist
if (auth.isLoggedIn) if (auth.isLoggedIn)
FutureBuilder<List<bool>>( albumSavedSnapshot.when(
future: spotify.me.isSavedAlbums([album.id!]), data: (isSaved) {
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( return HeartButton(
isLiked: isSaved, isLiked: isSaved,
onPressed: () { onPressed: () {
@ -85,10 +71,19 @@ class AlbumView extends HookConsumerWidget {
: spotify.me.saveAlbums( : spotify.me.saveAlbums(
[album.id!], [album.id!],
)) ))
.then((_) => update()); .whenComplete(() {
ref.refresh(
albumIsSavedForCurrentUserQuery(
album.id!,
),
);
ref.refresh(currentUserAlbumsQuery);
});
}, },
); );
}), },
error: (error, _) => Text("Error $error"),
loading: () => const CircularProgressIndicator()),
// play playlist // play playlist
IconButton( IconButton(
icon: Icon( icon: Icon(
@ -96,8 +91,13 @@ class AlbumView extends HookConsumerWidget {
? Icons.stop_rounded ? Icons.stop_rounded
: Icons.play_arrow_rounded, : Icons.play_arrow_rounded,
), ),
onPressed: snapshot.hasData onPressed: tracksSnapshot.asData?.value != null
? () => playPlaylist(playback, tracks) ? () => playPlaylist(
playback,
tracksSnapshot.asData!.value.map((trackSmp) {
return simpleTrackToTrack(trackSmp, album);
}).toList(),
)
: null, : null,
) )
], ],
@ -107,25 +107,25 @@ class AlbumView extends HookConsumerWidget {
child: Text(album.name!, child: Text(album.name!,
style: Theme.of(context).textTheme.headline4), style: Theme.of(context).textTheme.headline4),
), ),
snapshot.hasError tracksSnapshot.when(
? const Center(child: Text("Error occurred")) data: (data) {
: !snapshot.hasData List<Track> tracks = data.map((trackSmp) {
? const Expanded( return simpleTrackToTrack(trackSmp, album);
child: Center( }).toList();
child: CircularProgressIndicator.adaptive()), return TracksTableView(
)
: TracksTableView(
tracks, tracks,
onTrackPlayButtonPressed: (currentTrack) => onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
playPlaylist(
playback, playback,
tracks, tracks,
currentTrack: currentTrack, 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(
lyrics == null && playback.currentTrack == null
? "No Track being played currently" ? "No Track being played currently"
: lyrics.value["lyrics"]!, : lyrics!,
style: textTheme.headline6 style: textTheme.headline6
?.copyWith(color: textTheme.headline1?.color), ?.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,25 +48,13 @@ 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"
? spotify.playlists.getTracksByPlaylistId(playlist.id).all()
: spotify.tracks.me.saved
.all()
.then((tracks) => tracks.map((e) => e.track!)),
builder: (context, snapshot) {
List<Track> tracks = snapshot.data?.toList() ?? [];
return Column(
children: [ children: [
PageWindowTitleBar( PageWindowTitleBar(
leading: Row( leading: Row(
@ -73,25 +62,19 @@ class PlaylistView extends HookConsumerWidget {
// nav back // nav back
const BackButton(), const BackButton(),
// heart playlist // heart playlist
if (auth.isLoggedIn && meSnapshot.hasData) if (auth.isLoggedIn)
FutureBuilder<List<bool>>( meSnapshot.when(
future: isFollowing(meSnapshot.data!), data: (me) {
builder: (context, snapshot) { final query = playlistIsFollowedQuery(jsonEncode(
final isFollowing = {"playlistId": playlist.id, "userId": me.id!}));
snapshot.data?.first ?? false; final followingSnapshot = ref.watch(query);
if (!snapshot.hasData && !snapshot.hasError) { return followingSnapshot.when(
return const SizedBox( data: (isFollowing) {
height: 25,
width: 25,
child: CircularProgressIndicator.adaptive(),
);
}
return HeartButton( return HeartButton(
isLiked: isFollowing, isLiked: isFollowing,
icon: playlist.owner?.id != null && icon: playlist.owner?.id != null &&
meSnapshot.data?.id == me.id == playlist.owner?.id
playlist.owner?.id
? Icons.delete_outline_rounded ? Icons.delete_outline_rounded
: null, : null,
onPressed: () async { onPressed: () async {
@ -102,14 +85,22 @@ class PlaylistView extends HookConsumerWidget {
: spotify.playlists : spotify.playlists
.followPlaylist(playlist.id!); .followPlaylist(playlist.id!);
} catch (e, stack) { } catch (e, stack) {
logger.e( logger.e("FollowButton.onPressed", e, stack);
"FollowButton.onPressed", e, stack);
} finally { } finally {
update(); ref.refresh(query);
ref.refresh(currentUserPlaylistsQuery);
} }
}, },
); );
}), },
error: (error, _) => Text("Error $error"),
loading: () => const CircularProgressIndicator(),
);
},
error: (error, _) => Text("Error $error"),
loading: () => const CircularProgressIndicator(),
),
IconButton( IconButton(
icon: const Icon(Icons.share_rounded), icon: const Icon(Icons.share_rounded),
onPressed: () { onPressed: () {
@ -138,8 +129,11 @@ class PlaylistView extends HookConsumerWidget {
? Icons.stop_rounded ? Icons.stop_rounded
: Icons.play_arrow_rounded, : Icons.play_arrow_rounded,
), ),
onPressed: snapshot.hasData onPressed: tracksSnapshot.asData?.value != null
? () => playPlaylist(playback, tracks) ? () => playPlaylist(
playback,
tracksSnapshot.asData!.value,
)
: null, : null,
) )
], ],
@ -149,28 +143,25 @@ class PlaylistView extends HookConsumerWidget {
child: Text(playlist.name!, child: Text(playlist.name!,
style: Theme.of(context).textTheme.headline4), style: Theme.of(context).textTheme.headline4),
), ),
snapshot.hasError tracksSnapshot.when(
? const Center(child: Text("Error occurred")) data: (tracks) {
: !snapshot.hasData return TracksTableView(
? const Expanded(
child: Center(
child: CircularProgressIndicator.adaptive()),
)
: TracksTableView(
tracks, tracks,
onTrackPlayButtonPressed: (currentTrack) => onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
playPlaylist(
playback, playback,
tracks, tracks,
currentTrack: currentTrack, currentTrack: currentTrack,
), ),
playlistId: playlist.id, playlistId: playlist.id,
userPlaylist: playlist.owner?.id != null && userPlaylist: playlist.owner?.id != null &&
playlist.owner!.id == meSnapshot.data?.id, 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,55 +52,8 @@ class Settings extends HookConsumerWidget {
Flexible( Flexible(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 1366), constraints: const BoxConstraints(maxWidth: 1366),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView( child: ListView(
children: [ children: [
Row(
children: [
Expanded(
flex: 2,
child: Text(
"Genius Access Token",
style: Theme.of(context).textTheme.subtitle1,
),
),
Expanded(
flex: 1,
child: TextField(
controller: geniusTokenController,
decoration: InputDecoration(
hintText: preferences.geniusAccessToken,
),
),
),
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"),
),
)
],
),
const SizedBox(height: 10),
if (!Platform.isAndroid && !Platform.isIOS) ...[ if (!Platform.isAndroid && !Platform.isIOS) ...[
SettingsHotKeyTile( SettingsHotKeyTile(
title: "Next track global shortcut", title: "Next track global shortcut",
@ -138,11 +77,9 @@ class Settings extends HookConsumerWidget {
}, },
), ),
], ],
Row( ListTile(
mainAxisAlignment: MainAxisAlignment.spaceBetween, title: const Text("Theme"),
children: [ trailing: DropdownButton<ThemeMode>(
const Text("Theme"),
DropdownButton<ThemeMode>(
value: preferences.themeMode, value: preferences.themeMode,
items: const [ items: const [
DropdownMenuItem( DropdownMenuItem(
@ -167,8 +104,7 @@ class Settings extends HookConsumerWidget {
preferences.setThemeMode(value); preferences.setThemeMode(value);
} }
}, },
) ),
],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
ListTile( ListTile(
@ -185,18 +121,16 @@ class Settings extends HookConsumerWidget {
title: const Text("Background Color Scheme"), title: const Text("Background Color Scheme"),
trailing: ColorTile( trailing: ColorTile(
color: preferences.backgroundColorScheme, color: preferences.backgroundColorScheme,
onPressed: onPressed: pickColorScheme(ColorSchemeType.background),
pickColorScheme(ColorSchemeType.background),
isActive: true, isActive: true,
), ),
onTap: pickColorScheme(ColorSchemeType.background), onTap: pickColorScheme(ColorSchemeType.background),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Row( ListTile(
mainAxisAlignment: MainAxisAlignment.spaceBetween, title:
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(
@ -206,34 +140,31 @@ class Settings extends HookConsumerWidget {
.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), ListTile(
Row( title: const Text("Download lyrics along with the Track"),
mainAxisAlignment: MainAxisAlignment.spaceBetween, trailing: Switch.adaptive(
children: [
const Text("Download lyrics along with the Track"),
Switch.adaptive(
activeColor: Theme.of(context).primaryColor, activeColor: Theme.of(context).primaryColor,
value: preferences.saveTrackLyrics, value: preferences.saveTrackLyrics,
onChanged: (state) { onChanged: (state) {
preferences.setSaveTrackLyrics(state); preferences.setSaveTrackLyrics(state);
}, },
), ),
],
), ),
const SizedBox(height: 10), Padding(
Row( padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Expanded( 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,16 +174,11 @@ 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,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
const Text("Login with your Spotify account"),
ElevatedButton(
child: Text("Connect with Spotify".toUpperCase()), child: Text("Connect with Spotify".toUpperCase()),
onPressed: () { onPressed: () {
GoRouter.of(context).push("/login"); GoRouter.of(context).push("/login");
@ -264,33 +190,23 @@ class Settings extends HookConsumerWidget {
), ),
), ),
), ),
)
],
), ),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Expanded(
flex: 2,
child: Text("Check for Update)"),
), ),
Switch.adaptive( ListTile(
title: const Text("Check for Update"),
trailing: Switch.adaptive(
activeColor: Theme.of(context).primaryColor, activeColor: Theme.of(context).primaryColor,
value: preferences.checkUpdate, value: preferences.checkUpdate,
onChanged: (checked) => onChanged: (checked) =>
preferences.setCheckUpdate(checked), preferences.setCheckUpdate(checked),
) ),
],
), ),
if (auth.isLoggedIn) if (auth.isLoggedIn)
Builder(builder: (context) { Builder(builder: (context) {
Auth auth = ref.watch(authProvider); Auth auth = ref.watch(authProvider);
return Row( return ListTile(
mainAxisAlignment: MainAxisAlignment.spaceBetween, title: const Text("Log out of this account"),
children: [ trailing: ElevatedButton(
const Text("Log out of this account"),
ElevatedButton(
child: const Text("Logout"), child: const Text("Logout"),
style: ButtonStyle( style: ButtonStyle(
backgroundColor: backgroundColor:
@ -304,16 +220,13 @@ class Settings extends HookConsumerWidget {
GoRouter.of(context).pop(); GoRouter.of(context).pop();
}, },
), ),
],
); );
}), }),
const SizedBox(height: 40),
const About(), const About(),
], ],
), ),
), ),
), ),
),
], ],
), ),
), ),

View File

@ -15,16 +15,12 @@ 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(
children: [
if (currentHotKey != null)
HotKeyVirtualView(hotKey: currentHotKey!),
const SizedBox(width: 10), const SizedBox(width: 10),
ElevatedButton( ElevatedButton(
child: const Text("Set Shortcut"), child: const Text("Set Shortcut"),
@ -40,8 +36,6 @@ class SettingsHotKeyTile extends StatelessWidget {
}, },
), ),
], ],
)
],
), ),
); );
} }

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,