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/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,18 +44,13 @@ 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<Iterable<TrackSimple>>(
future: spotify.albums.getTracks(album.id!).all(),
builder: (context, snapshot) {
List<Track> tracks = snapshot.data?.map((trackSmp) {
return simpleTrackToTrack(trackSmp, album);
}).toList() ??
[];
return Column(
body: Column(
children: [
PageWindowTitleBar(
leading: Row(
@ -64,17 +59,8 @@ class AlbumView extends HookConsumerWidget {
const BackButton(),
// heart playlist
if (auth.isLoggedIn)
FutureBuilder<List<bool>>(
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(),
);
}
albumSavedSnapshot.when(
data: (isSaved) {
return HeartButton(
isLiked: isSaved,
onPressed: () {
@ -85,10 +71,19 @@ class AlbumView extends HookConsumerWidget {
: spotify.me.saveAlbums(
[album.id!],
))
.then((_) => update());
.whenComplete(() {
ref.refresh(
albumIsSavedForCurrentUserQuery(
album.id!,
),
);
ref.refresh(currentUserAlbumsQuery);
});
},
);
}),
},
error: (error, _) => Text("Error $error"),
loading: () => const CircularProgressIndicator()),
// play playlist
IconButton(
icon: Icon(
@ -96,8 +91,13 @@ class AlbumView extends HookConsumerWidget {
? Icons.stop_rounded
: Icons.play_arrow_rounded,
),
onPressed: snapshot.hasData
? () => playPlaylist(playback, tracks)
onPressed: tracksSnapshot.asData?.value != null
? () => playPlaylist(
playback,
tracksSnapshot.asData!.value.map((trackSmp) {
return simpleTrackToTrack(trackSmp, album);
}).toList(),
)
: null,
)
],
@ -107,25 +107,25 @@ class AlbumView extends HookConsumerWidget {
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()),
)
: TracksTableView(
tracksSnapshot.when(
data: (data) {
List<Track> tracks = data.map((trackSmp) {
return simpleTrackToTrack(trackSmp, album);
}).toList();
return TracksTableView(
tracks,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
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_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
child: geniusLyricsSnapshot.when(
data: (lyrics) {
return Text(
lyrics == null && playback.currentTrack == null
? "No Track being played currently"
: lyrics.value["lyrics"]!,
: lyrics!,
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/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,
),
),
],

View File

@ -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,25 +48,13 @@ 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<User>(getMe);
Future<List<bool>> 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<Iterable<Track>>(
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(
body: Column(
children: [
PageWindowTitleBar(
leading: Row(
@ -73,25 +62,19 @@ class PlaylistView extends HookConsumerWidget {
// nav back
const BackButton(),
// heart playlist
if (auth.isLoggedIn && meSnapshot.hasData)
FutureBuilder<List<bool>>(
future: isFollowing(meSnapshot.data!),
builder: (context, snapshot) {
final isFollowing =
snapshot.data?.first ?? false;
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 &&
meSnapshot.data?.id ==
playlist.owner?.id
me.id == playlist.owner?.id
? Icons.delete_outline_rounded
: null,
onPressed: () async {
@ -102,14 +85,22 @@ class PlaylistView extends HookConsumerWidget {
: spotify.playlists
.followPlaylist(playlist.id!);
} catch (e, stack) {
logger.e(
"FollowButton.onPressed", e, stack);
logger.e("FollowButton.onPressed", e, stack);
} 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(
icon: const Icon(Icons.share_rounded),
onPressed: () {
@ -138,8 +129,11 @@ class PlaylistView extends HookConsumerWidget {
? Icons.stop_rounded
: Icons.play_arrow_rounded,
),
onPressed: snapshot.hasData
? () => playPlaylist(playback, tracks)
onPressed: tracksSnapshot.asData?.value != null
? () => playPlaylist(
playback,
tracksSnapshot.asData!.value,
)
: null,
)
],
@ -149,28 +143,25 @@ class PlaylistView extends HookConsumerWidget {
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(
tracksSnapshot.when(
data: (tracks) {
return TracksTableView(
tracks,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
playback,
tracks,
currentTrack: currentTrack,
),
playlistId: playlist.id,
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/provider/Auth.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 {
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<List<Page>>(
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<AlbumSimple> albums = [];
List<Artist> artists = [];
List<Track> tracks = [];
List<PlaylistSimple> playlists = [];
for (MapEntry<int, Page> page
in snapshot.data?.asMap().entries ?? []) {
for (MapEntry<int, Page> 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(),
)
],
),

View File

@ -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"),
)

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/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<String?>(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,55 +52,8 @@ 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: 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) ...[
SettingsHotKeyTile(
title: "Next track global shortcut",
@ -138,11 +77,9 @@ class Settings extends HookConsumerWidget {
},
),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Theme"),
DropdownButton<ThemeMode>(
ListTile(
title: const Text("Theme"),
trailing: DropdownButton<ThemeMode>(
value: preferences.themeMode,
items: const [
DropdownMenuItem(
@ -167,8 +104,7 @@ class Settings extends HookConsumerWidget {
preferences.setThemeMode(value);
}
},
)
],
),
),
const SizedBox(height: 10),
ListTile(
@ -185,18 +121,16 @@ class Settings extends HookConsumerWidget {
title: const Text("Background Color Scheme"),
trailing: ColorTile(
color: preferences.backgroundColorScheme,
onPressed:
pickColorScheme(ColorSchemeType.background),
onPressed: pickColorScheme(ColorSchemeType.background),
isActive: true,
),
onTap: pickColorScheme(ColorSchemeType.background),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ListTile(
title:
const Text("Market Place (Recommendation Country)"),
DropdownButton(
trailing: DropdownButton(
value: preferences.recommendationMarket,
items: spotifyMarkets
.map((country) => (DropdownMenuItem(
@ -206,34 +140,31 @@ class Settings extends HookConsumerWidget {
.toList(),
onChanged: (value) {
if (value == null) return;
preferences
.setRecommendationMarket(value as String);
preferences.setRecommendationMarket(value as String);
},
),
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Download lyrics along with the Track"),
Switch.adaptive(
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);
},
),
],
),
const SizedBox(height: 10),
Row(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: 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,16 +174,11 @@ 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(
ListTile(
title: const Text("Login with your Spotify account"),
trailing: ElevatedButton(
child: Text("Connect with Spotify".toUpperCase()),
onPressed: () {
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,
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(
return ListTile(
title: const Text("Log out of this account"),
trailing: ElevatedButton(
child: const Text("Logout"),
style: ButtonStyle(
backgroundColor:
@ -304,16 +220,13 @@ class Settings extends HookConsumerWidget {
GoRouter.of(context).pop();
},
),
],
);
}),
const SizedBox(height: 40),
const About(),
],
),
),
),
),
],
),
),

View File

@ -15,16 +15,12 @@ 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!),
if (currentHotKey != null) HotKeyVirtualView(hotKey: currentHotKey!),
const SizedBox(width: 10),
ElevatedButton(
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';
PagingController<P, ItemType> usePaginatedFutureProvider<T, P, ItemType>(
AutoDisposeFutureProvider<T> Function(P pageKey) createSnapshot, {
FutureProvider<T> Function(P pageKey) createSnapshot, {
required P firstPageKey,
required WidgetRef ref,
void Function(

View File

@ -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<Page<Category>, int>(
final categoriesQuery = FutureProvider.family<Page<Category>, int>(
(ref, pageKey) {
final spotify = ref.watch(spotifyProvider);
final recommendationMarket = ref.watch(
@ -16,7 +23,7 @@ final categoriesQuery = FutureProvider.autoDispose.family<Page<Category>, int>(
);
final categoryPlaylistsQuery =
FutureProvider.autoDispose.family<Page<PlaylistSimple>, String>(
FutureProvider.family<Page<PlaylistSimple>, String>(
(ref, value) {
final spotify = ref.watch(spotifyProvider);
final List data = value.split("/");
@ -44,22 +51,21 @@ final currentUserAlbumsQuery = FutureProvider<Iterable<AlbumSimple>>(
);
final currentUserFollowingArtistsQuery =
FutureProvider.autoDispose.family<CursorPage<Artist>, String>(
FutureProvider.family<CursorPage<Artist>, String>(
(ref, pageKey) {
final spotify = ref.watch(spotifyProvider);
return spotify.me.following(FollowingType.artist).getPage(15, pageKey);
},
);
final artistProfileQuery = FutureProvider.autoDispose.family<Artist, String>(
final artistProfileQuery = FutureProvider.family<Artist, String>(
(ref, id) {
final spotify = ref.watch(spotifyProvider);
return spotify.artists.get(id);
},
);
final currentUserFollowsArtistQuery =
FutureProvider.autoDispose.family<bool, String>(
final currentUserFollowsArtistQuery = FutureProvider.family<bool, String>(
(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<Iterable<Track>, String>((ref, id) {
FutureProvider.family<Iterable<Track>, String>((ref, id) {
final spotify = ref.watch(spotifyProvider);
return spotify.artists.getTopTracks(id, "US");
});
final artistAlbumsQuery =
FutureProvider.autoDispose.family<Page<Album>, String>(
final artistAlbumsQuery = FutureProvider.family<Page<Album>, 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<Iterable<Artist>, String>(
FutureProvider.family<Iterable<Artist>, String>(
(ref, id) {
final spotify = ref.watch(spotifyProvider);
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,
"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,