mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Bugfix: ColorScheme not persisting
Response Caching support in multiple components PlayerView, AlbumView, Search, Lyrics, SyncedLyrics
This commit is contained in:
parent
e3c7b83ae0
commit
b3b3acdb1e
@ -6,11 +6,11 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
class AlbumView extends HookConsumerWidget {
|
||||
final AlbumSimple album;
|
||||
@ -44,88 +44,88 @@ class AlbumView extends HookConsumerWidget {
|
||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
|
||||
final update = useForceUpdate();
|
||||
final tracksSnapshot = ref.watch(albumTracksQuery(album.id!));
|
||||
final albumSavedSnapshot =
|
||||
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: FutureBuilder<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(
|
||||
children: [
|
||||
PageWindowTitleBar(
|
||||
leading: Row(
|
||||
children: [
|
||||
// nav back
|
||||
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(),
|
||||
);
|
||||
}
|
||||
return HeartButton(
|
||||
isLiked: isSaved,
|
||||
onPressed: () {
|
||||
(isSaved
|
||||
? spotify.me.removeAlbums(
|
||||
[album.id!],
|
||||
)
|
||||
: spotify.me.saveAlbums(
|
||||
[album.id!],
|
||||
))
|
||||
.then((_) => update());
|
||||
},
|
||||
// nav back
|
||||
const BackButton(),
|
||||
// heart playlist
|
||||
if (auth.isLoggedIn)
|
||||
albumSavedSnapshot.when(
|
||||
data: (isSaved) {
|
||||
return HeartButton(
|
||||
isLiked: isSaved,
|
||||
onPressed: () {
|
||||
(isSaved
|
||||
? spotify.me.removeAlbums(
|
||||
[album.id!],
|
||||
)
|
||||
: spotify.me.saveAlbums(
|
||||
[album.id!],
|
||||
))
|
||||
.whenComplete(() {
|
||||
ref.refresh(
|
||||
albumIsSavedForCurrentUserQuery(
|
||||
album.id!,
|
||||
),
|
||||
);
|
||||
}),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: snapshot.hasData
|
||||
? () => playPlaylist(playback, tracks)
|
||||
: null,
|
||||
)
|
||||
],
|
||||
ref.refresh(currentUserAlbumsQuery);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, _) => Text("Error $error"),
|
||||
loading: () => const CircularProgressIndicator()),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(album.name!,
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
),
|
||||
snapshot.hasError
|
||||
? const Center(child: Text("Error occurred"))
|
||||
: !snapshot.hasData
|
||||
? const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive()),
|
||||
onPressed: tracksSnapshot.asData?.value != null
|
||||
? () => playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value.map((trackSmp) {
|
||||
return simpleTrackToTrack(trackSmp, album);
|
||||
}).toList(),
|
||||
)
|
||||
: TracksTableView(
|
||||
tracks,
|
||||
onTrackPlayButtonPressed: (currentTrack) =>
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracks,
|
||||
currentTrack: currentTrack,
|
||||
),
|
||||
),
|
||||
: null,
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(album.name!,
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
),
|
||||
tracksSnapshot.when(
|
||||
data: (data) {
|
||||
List<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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,13 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/helpers/artist-to-string.dart';
|
||||
import 'package:spotube/helpers/getLyrics.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
class Lyrics extends HookConsumerWidget {
|
||||
const Lyrics({Key? key}) : super(key: key);
|
||||
@ -15,74 +12,8 @@ class Lyrics extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
UserPreferences userPreferences = ref.watch(userPreferencesProvider);
|
||||
final lyrics = useState({});
|
||||
|
||||
final lyricsFuture = useMemoized(() async {
|
||||
if (playback.currentTrack == null ||
|
||||
userPreferences.geniusAccessToken.isEmpty ||
|
||||
(playback.currentTrack?.id != null &&
|
||||
playback.currentTrack?.id == lyrics.value["id"])) {
|
||||
return null;
|
||||
}
|
||||
final lyricsStr = await getLyrics(
|
||||
playback.currentTrack!.name!,
|
||||
playback.currentTrack!.artists
|
||||
?.map((s) => s.name)
|
||||
.whereNotNull()
|
||||
.toList() ??
|
||||
[],
|
||||
apiKey: userPreferences.geniusAccessToken,
|
||||
optimizeQuery: true,
|
||||
);
|
||||
if (lyricsStr == null) return Future.error("No lyrics found");
|
||||
return lyricsStr;
|
||||
}, [playback.currentTrack, userPreferences.geniusAccessToken]);
|
||||
|
||||
final lyricsSnapshot = useFuture(lyricsFuture);
|
||||
|
||||
useEffect(() {
|
||||
if (lyricsSnapshot.hasData &&
|
||||
lyricsSnapshot.data != null &&
|
||||
playback.currentTrack != null) {
|
||||
lyrics.value = {
|
||||
"lyrics": lyricsSnapshot.data,
|
||||
"id": playback.currentTrack!.id!
|
||||
};
|
||||
}
|
||||
|
||||
if (lyrics.value["lyrics"] != null && playback.currentTrack == null) {
|
||||
lyrics.value = {};
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
lyricsSnapshot.data,
|
||||
lyricsSnapshot.hasData,
|
||||
lyrics.value,
|
||||
playback.currentTrack,
|
||||
]);
|
||||
|
||||
final geniusLyricsSnapshot = ref.watch(geniusLyricsQuery);
|
||||
final breakpoint = useBreakpoints();
|
||||
|
||||
if (lyrics.value["lyrics"] == null && playback.currentTrack != null) {
|
||||
if (lyricsSnapshot.hasError) {
|
||||
return Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Sorry, no Lyrics were found for `${playback.currentTrack?.name}` :'(",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Expanded(
|
||||
@ -110,13 +41,19 @@ class Lyrics extends HookConsumerWidget {
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
lyrics.value["lyrics"] == null &&
|
||||
playback.currentTrack == null
|
||||
? "No Track being played currently"
|
||||
: lyrics.value["lyrics"]!,
|
||||
style: textTheme.headline6
|
||||
?.copyWith(color: textTheme.headline1?.color),
|
||||
child: geniusLyricsSnapshot.when(
|
||||
data: (lyrics) {
|
||||
return Text(
|
||||
lyrics == null && playback.currentTrack == null
|
||||
? "No Track being played currently"
|
||||
: lyrics!,
|
||||
style: textTheme.headline6
|
||||
?.copyWith(color: textTheme.headline1?.color),
|
||||
);
|
||||
},
|
||||
error: (error, __) => Text(
|
||||
"Sorry, no Lyrics were found for `${playback.currentTrack?.name}` :'("),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,11 +1,11 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
@ -13,6 +13,7 @@ import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
class PlaylistView extends HookConsumerWidget {
|
||||
final logger = getLogger(PlaylistView);
|
||||
@ -47,130 +48,120 @@ class PlaylistView extends HookConsumerWidget {
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
||||
playback.currentPlaylist?.id == playlist.id;
|
||||
final update = useForceUpdate();
|
||||
final getMe = useMemoized(() => spotify.me.get(), []);
|
||||
final meSnapshot = useFuture<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(
|
||||
children: [
|
||||
PageWindowTitleBar(
|
||||
leading: Row(
|
||||
children: [
|
||||
// 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;
|
||||
// nav back
|
||||
const BackButton(),
|
||||
// heart playlist
|
||||
if (auth.isLoggedIn)
|
||||
meSnapshot.when(
|
||||
data: (me) {
|
||||
final query = playlistIsFollowedQuery(jsonEncode(
|
||||
{"playlistId": playlist.id, "userId": me.id!}));
|
||||
final followingSnapshot = ref.watch(query);
|
||||
|
||||
if (!snapshot.hasData && !snapshot.hasError) {
|
||||
return const SizedBox(
|
||||
height: 25,
|
||||
width: 25,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
return followingSnapshot.when(
|
||||
data: (isFollowing) {
|
||||
return HeartButton(
|
||||
isLiked: isFollowing,
|
||||
icon: playlist.owner?.id != null &&
|
||||
me.id == playlist.owner?.id
|
||||
? Icons.delete_outline_rounded
|
||||
: null,
|
||||
onPressed: () async {
|
||||
try {
|
||||
isFollowing
|
||||
? spotify.playlists
|
||||
.unfollowPlaylist(playlist.id!)
|
||||
: spotify.playlists
|
||||
.followPlaylist(playlist.id!);
|
||||
} catch (e, stack) {
|
||||
logger.e("FollowButton.onPressed", e, stack);
|
||||
} finally {
|
||||
ref.refresh(query);
|
||||
ref.refresh(currentUserPlaylistsQuery);
|
||||
}
|
||||
return HeartButton(
|
||||
isLiked: isFollowing,
|
||||
icon: playlist.owner?.id != null &&
|
||||
meSnapshot.data?.id ==
|
||||
playlist.owner?.id
|
||||
? Icons.delete_outline_rounded
|
||||
: null,
|
||||
onPressed: () async {
|
||||
try {
|
||||
isFollowing
|
||||
? spotify.playlists
|
||||
.unfollowPlaylist(playlist.id!)
|
||||
: spotify.playlists
|
||||
.followPlaylist(playlist.id!);
|
||||
} catch (e, stack) {
|
||||
logger.e(
|
||||
"FollowButton.onPressed", e, stack);
|
||||
} finally {
|
||||
update();
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
onPressed: () {
|
||||
final data =
|
||||
"https://open.spotify.com/playlist/${playlist.id}";
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: data),
|
||||
).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
"Copied $data to clipboard",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: snapshot.hasData
|
||||
? () => playPlaylist(playback, tracks)
|
||||
: null,
|
||||
)
|
||||
],
|
||||
error: (error, _) => Text("Error $error"),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
error: (error, _) => Text("Error $error"),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(playlist.name!,
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
),
|
||||
snapshot.hasError
|
||||
? const Center(child: Text("Error occurred"))
|
||||
: !snapshot.hasData
|
||||
? const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive()),
|
||||
)
|
||||
: TracksTableView(
|
||||
tracks,
|
||||
onTrackPlayButtonPressed: (currentTrack) =>
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracks,
|
||||
currentTrack: currentTrack,
|
||||
),
|
||||
playlistId: playlist.id,
|
||||
userPlaylist: playlist.owner?.id != null &&
|
||||
playlist.owner!.id == meSnapshot.data?.id,
|
||||
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
onPressed: () {
|
||||
final data =
|
||||
"https://open.spotify.com/playlist/${playlist.id}";
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: data),
|
||||
).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
"Copied $data to clipboard",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: tracksSnapshot.asData?.value != null
|
||||
? () => playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(playlist.name!,
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
),
|
||||
tracksSnapshot.when(
|
||||
data: (tracks) {
|
||||
return TracksTableView(
|
||||
tracks,
|
||||
onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
|
||||
playback,
|
||||
tracks,
|
||||
currentTrack: currentTrack,
|
||||
),
|
||||
playlistId: playlist.id,
|
||||
userPlaylist: playlist.owner?.id != null &&
|
||||
playlist.owner!.id == meSnapshot.asData?.value.id,
|
||||
);
|
||||
},
|
||||
error: (error, _) => Text("Error $error"),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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(),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -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"),
|
||||
)
|
||||
|
@ -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,174 +52,119 @@ class Settings extends HookConsumerWidget {
|
||||
Flexible(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 1366),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ListView(
|
||||
children: [
|
||||
if (!Platform.isAndroid && !Platform.isIOS) ...[
|
||||
SettingsHotKeyTile(
|
||||
title: "Next track global shortcut",
|
||||
currentHotKey: preferences.nextTrackHotKey,
|
||||
onHotKeyRecorded: (value) {
|
||||
preferences.setNextTrackHotKey(value);
|
||||
},
|
||||
),
|
||||
SettingsHotKeyTile(
|
||||
title: "Prev track global shortcut",
|
||||
currentHotKey: preferences.prevTrackHotKey,
|
||||
onHotKeyRecorded: (value) {
|
||||
preferences.setPrevTrackHotKey(value);
|
||||
},
|
||||
),
|
||||
SettingsHotKeyTile(
|
||||
title: "Play/Pause global shortcut",
|
||||
currentHotKey: preferences.playPauseHotKey,
|
||||
onHotKeyRecorded: (value) {
|
||||
preferences.setPlayPauseHotKey(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
ListTile(
|
||||
title: const Text("Theme"),
|
||||
trailing: DropdownButton<ThemeMode>(
|
||||
value: preferences.themeMode,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
child: Text(
|
||||
"Genius Access Token",
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
"Dark",
|
||||
),
|
||||
value: ThemeMode.dark,
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: geniusTokenController,
|
||||
decoration: InputDecoration(
|
||||
hintText: preferences.geniusAccessToken,
|
||||
),
|
||||
DropdownMenuItem(
|
||||
child: Text(
|
||||
"Light",
|
||||
),
|
||||
value: ThemeMode.light,
|
||||
),
|
||||
DropdownMenuItem(
|
||||
child: Text("System"),
|
||||
value: ThemeMode.system,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: geniusAccessToken.value != null
|
||||
? () async {
|
||||
SharedPreferences localStorage =
|
||||
await SharedPreferences.getInstance();
|
||||
if (geniusAccessToken.value != null &&
|
||||
geniusAccessToken.value!.isNotEmpty) {
|
||||
preferences.setGeniusAccessToken(
|
||||
geniusAccessToken.value!,
|
||||
);
|
||||
localStorage.setString(
|
||||
LocalStorageKeys.geniusAccessToken,
|
||||
geniusAccessToken.value!);
|
||||
}
|
||||
|
||||
geniusAccessToken.value = null;
|
||||
geniusTokenController.text = "";
|
||||
}
|
||||
: null,
|
||||
child: const Text("Save"),
|
||||
),
|
||||
)
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
preferences.setThemeMode(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (!Platform.isAndroid && !Platform.isIOS) ...[
|
||||
SettingsHotKeyTile(
|
||||
title: "Next track global shortcut",
|
||||
currentHotKey: preferences.nextTrackHotKey,
|
||||
onHotKeyRecorded: (value) {
|
||||
preferences.setNextTrackHotKey(value);
|
||||
},
|
||||
),
|
||||
SettingsHotKeyTile(
|
||||
title: "Prev track global shortcut",
|
||||
currentHotKey: preferences.prevTrackHotKey,
|
||||
onHotKeyRecorded: (value) {
|
||||
preferences.setPrevTrackHotKey(value);
|
||||
},
|
||||
),
|
||||
SettingsHotKeyTile(
|
||||
title: "Play/Pause global shortcut",
|
||||
currentHotKey: preferences.playPauseHotKey,
|
||||
onHotKeyRecorded: (value) {
|
||||
preferences.setPlayPauseHotKey(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Theme"),
|
||||
DropdownButton<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),
|
||||
ListTile(
|
||||
title: const Text("Accent Color Scheme"),
|
||||
trailing: ColorTile(
|
||||
color: preferences.accentColorScheme,
|
||||
onPressed: pickColorScheme(ColorSchemeType.accent),
|
||||
isActive: true,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListTile(
|
||||
title: const Text("Accent Color Scheme"),
|
||||
trailing: ColorTile(
|
||||
color: preferences.accentColorScheme,
|
||||
onPressed: pickColorScheme(ColorSchemeType.accent),
|
||||
isActive: true,
|
||||
),
|
||||
onTap: pickColorScheme(ColorSchemeType.accent),
|
||||
onTap: pickColorScheme(ColorSchemeType.accent),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListTile(
|
||||
title: const Text("Background Color Scheme"),
|
||||
trailing: ColorTile(
|
||||
color: preferences.backgroundColorScheme,
|
||||
onPressed: pickColorScheme(ColorSchemeType.background),
|
||||
isActive: true,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListTile(
|
||||
title: const Text("Background Color Scheme"),
|
||||
trailing: ColorTile(
|
||||
color: preferences.backgroundColorScheme,
|
||||
onPressed:
|
||||
pickColorScheme(ColorSchemeType.background),
|
||||
isActive: true,
|
||||
),
|
||||
onTap: pickColorScheme(ColorSchemeType.background),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
onTap: pickColorScheme(ColorSchemeType.background),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListTile(
|
||||
title:
|
||||
const Text("Market Place (Recommendation Country)"),
|
||||
DropdownButton(
|
||||
value: preferences.recommendationMarket,
|
||||
items: spotifyMarkets
|
||||
.map((country) => (DropdownMenuItem(
|
||||
child: Text(country),
|
||||
value: country,
|
||||
)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
preferences
|
||||
.setRecommendationMarket(value as String);
|
||||
},
|
||||
),
|
||||
],
|
||||
trailing: DropdownButton(
|
||||
value: preferences.recommendationMarket,
|
||||
items: spotifyMarkets
|
||||
.map((country) => (DropdownMenuItem(
|
||||
child: Text(country),
|
||||
value: country,
|
||||
)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
preferences.setRecommendationMarket(value as String);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
),
|
||||
ListTile(
|
||||
title: const Text("Download lyrics along with the Track"),
|
||||
trailing: Switch.adaptive(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
value: preferences.saveTrackLyrics,
|
||||
onChanged: (state) {
|
||||
preferences.setSaveTrackLyrics(state);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Download lyrics along with the Track"),
|
||||
Switch.adaptive(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
value: preferences.saveTrackLyrics,
|
||||
onChanged: (state) {
|
||||
preferences.setSaveTrackLyrics(state);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Expanded(
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
"Format of the YouTube Search term (Case sensitive)"),
|
||||
"Format of the YouTube Search term (Case sensitive)",
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
@ -243,74 +174,56 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (auth.isAnonymous)
|
||||
Wrap(
|
||||
spacing: 20,
|
||||
runSpacing: 20,
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
const Text("Login with your Spotify account"),
|
||||
ElevatedButton(
|
||||
child: Text("Connect with Spotify".toUpperCase()),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).push("/login");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (auth.isAnonymous)
|
||||
ListTile(
|
||||
title: const Text("Login with your Spotify account"),
|
||||
trailing: ElevatedButton(
|
||||
child: Text("Connect with Spotify".toUpperCase()),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).push("/login");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Expanded(
|
||||
flex: 2,
|
||||
child: Text("Check for Update)"),
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
value: preferences.checkUpdate,
|
||||
onChanged: (checked) =>
|
||||
preferences.setCheckUpdate(checked),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (auth.isLoggedIn)
|
||||
Builder(builder: (context) {
|
||||
Auth auth = ref.watch(authProvider);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Log out of this account"),
|
||||
ElevatedButton(
|
||||
child: const Text("Logout"),
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all(Colors.red),
|
||||
),
|
||||
onPressed: () async {
|
||||
SharedPreferences localStorage =
|
||||
await SharedPreferences.getInstance();
|
||||
await localStorage.clear();
|
||||
auth.logout();
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 40),
|
||||
const About(),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
title: const Text("Check for Update"),
|
||||
trailing: Switch.adaptive(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
value: preferences.checkUpdate,
|
||||
onChanged: (checked) =>
|
||||
preferences.setCheckUpdate(checked),
|
||||
),
|
||||
),
|
||||
if (auth.isLoggedIn)
|
||||
Builder(builder: (context) {
|
||||
Auth auth = ref.watch(authProvider);
|
||||
return ListTile(
|
||||
title: const Text("Log out of this account"),
|
||||
trailing: ElevatedButton(
|
||||
child: const Text("Logout"),
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all(Colors.red),
|
||||
),
|
||||
onPressed: () async {
|
||||
SharedPreferences localStorage =
|
||||
await SharedPreferences.getInstance();
|
||||
await localStorage.clear();
|
||||
auth.logout();
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
const About(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -15,32 +15,26 @@ class SettingsHotKeyTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
return ListTile(
|
||||
title: Text(title),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(title),
|
||||
Row(
|
||||
children: [
|
||||
if (currentHotKey != null)
|
||||
HotKeyVirtualView(hotKey: currentHotKey!),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
child: const Text("Set Shortcut"),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return RecordHotKeyDialog(
|
||||
onHotKeyRecorded: onHotKeyRecorded,
|
||||
);
|
||||
},
|
||||
if (currentHotKey != null) HotKeyVirtualView(hotKey: currentHotKey!),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
child: const Text("Set Shortcut"),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return RecordHotKeyDialog(
|
||||
onHotKeyRecorded: onHotKeyRecorded,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user