mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-15 08:35:18 +00:00
Merge branch 'master' into build
This commit is contained in:
commit
6b4c5d0427
@ -6,11 +6,11 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
|||||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/helpers/simple-track-to-track.dart';
|
import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
|
||||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/SpotifyDI.dart';
|
import 'package:spotube/provider/SpotifyDI.dart';
|
||||||
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
|
|
||||||
class AlbumView extends HookConsumerWidget {
|
class AlbumView extends HookConsumerWidget {
|
||||||
final AlbumSimple album;
|
final AlbumSimple album;
|
||||||
@ -44,88 +44,88 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||||
final Auth auth = ref.watch(authProvider);
|
final Auth auth = ref.watch(authProvider);
|
||||||
|
|
||||||
final update = useForceUpdate();
|
final tracksSnapshot = ref.watch(albumTracksQuery(album.id!));
|
||||||
|
final albumSavedSnapshot =
|
||||||
|
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: FutureBuilder<Iterable<TrackSimple>>(
|
body: Column(
|
||||||
future: spotify.albums.getTracks(album.id!).all(),
|
children: [
|
||||||
builder: (context, snapshot) {
|
PageWindowTitleBar(
|
||||||
List<Track> tracks = snapshot.data?.map((trackSmp) {
|
leading: Row(
|
||||||
return simpleTrackToTrack(trackSmp, album);
|
|
||||||
}).toList() ??
|
|
||||||
[];
|
|
||||||
return Column(
|
|
||||||
children: [
|
children: [
|
||||||
PageWindowTitleBar(
|
// nav back
|
||||||
leading: Row(
|
const BackButton(),
|
||||||
children: [
|
// heart playlist
|
||||||
// nav back
|
if (auth.isLoggedIn)
|
||||||
const BackButton(),
|
albumSavedSnapshot.when(
|
||||||
// heart playlist
|
data: (isSaved) {
|
||||||
if (auth.isLoggedIn)
|
return HeartButton(
|
||||||
FutureBuilder<List<bool>>(
|
isLiked: isSaved,
|
||||||
future: spotify.me.isSavedAlbums([album.id!]),
|
onPressed: () {
|
||||||
builder: (context, snapshot) {
|
(isSaved
|
||||||
final isSaved = snapshot.data?.first == true;
|
? spotify.me.removeAlbums(
|
||||||
if (!snapshot.hasData && !snapshot.hasError) {
|
[album.id!],
|
||||||
return const SizedBox(
|
)
|
||||||
height: 25,
|
: spotify.me.saveAlbums(
|
||||||
width: 25,
|
[album.id!],
|
||||||
child: CircularProgressIndicator.adaptive(),
|
))
|
||||||
);
|
.whenComplete(() {
|
||||||
}
|
ref.refresh(
|
||||||
return HeartButton(
|
albumIsSavedForCurrentUserQuery(
|
||||||
isLiked: isSaved,
|
album.id!,
|
||||||
onPressed: () {
|
),
|
||||||
(isSaved
|
|
||||||
? spotify.me.removeAlbums(
|
|
||||||
[album.id!],
|
|
||||||
)
|
|
||||||
: spotify.me.saveAlbums(
|
|
||||||
[album.id!],
|
|
||||||
))
|
|
||||||
.then((_) => update());
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}),
|
ref.refresh(currentUserAlbumsQuery);
|
||||||
// play playlist
|
});
|
||||||
IconButton(
|
},
|
||||||
icon: Icon(
|
);
|
||||||
isPlaylistPlaying
|
},
|
||||||
? Icons.stop_rounded
|
error: (error, _) => Text("Error $error"),
|
||||||
: Icons.play_arrow_rounded,
|
loading: () => const CircularProgressIndicator()),
|
||||||
),
|
// play playlist
|
||||||
onPressed: snapshot.hasData
|
IconButton(
|
||||||
? () => playPlaylist(playback, tracks)
|
icon: Icon(
|
||||||
: null,
|
isPlaylistPlaying
|
||||||
)
|
? Icons.stop_rounded
|
||||||
],
|
: Icons.play_arrow_rounded,
|
||||||
),
|
),
|
||||||
),
|
onPressed: tracksSnapshot.asData?.value != null
|
||||||
Center(
|
? () => playPlaylist(
|
||||||
child: Text(album.name!,
|
playback,
|
||||||
style: Theme.of(context).textTheme.headline4),
|
tracksSnapshot.asData!.value.map((trackSmp) {
|
||||||
),
|
return simpleTrackToTrack(trackSmp, album);
|
||||||
snapshot.hasError
|
}).toList(),
|
||||||
? const Center(child: Text("Error occurred"))
|
|
||||||
: !snapshot.hasData
|
|
||||||
? const Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: CircularProgressIndicator.adaptive()),
|
|
||||||
)
|
)
|
||||||
: TracksTableView(
|
: null,
|
||||||
tracks,
|
)
|
||||||
onTrackPlayButtonPressed: (currentTrack) =>
|
|
||||||
playPlaylist(
|
|
||||||
playback,
|
|
||||||
tracks,
|
|
||||||
currentTrack: currentTrack,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
}),
|
),
|
||||||
|
Center(
|
||||||
|
child: Text(album.name!,
|
||||||
|
style: Theme.of(context).textTheme.headline4),
|
||||||
|
),
|
||||||
|
tracksSnapshot.when(
|
||||||
|
data: (data) {
|
||||||
|
List<Track> tracks = data.map((trackSmp) {
|
||||||
|
return simpleTrackToTrack(trackSmp, album);
|
||||||
|
}).toList();
|
||||||
|
return TracksTableView(
|
||||||
|
tracks,
|
||||||
|
onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
|
||||||
|
playback,
|
||||||
|
tracks,
|
||||||
|
currentTrack: currentTrack,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, _) => Text("Error $error"),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/helpers/artist-to-string.dart';
|
import 'package:spotube/helpers/artist-to-string.dart';
|
||||||
import 'package:spotube/helpers/getLyrics.dart';
|
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
class Lyrics extends HookConsumerWidget {
|
class Lyrics extends HookConsumerWidget {
|
||||||
const Lyrics({Key? key}) : super(key: key);
|
const Lyrics({Key? key}) : super(key: key);
|
||||||
@ -15,74 +12,8 @@ class Lyrics extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
Playback playback = ref.watch(playbackProvider);
|
Playback playback = ref.watch(playbackProvider);
|
||||||
UserPreferences userPreferences = ref.watch(userPreferencesProvider);
|
final geniusLyricsSnapshot = ref.watch(geniusLyricsQuery);
|
||||||
final lyrics = useState({});
|
|
||||||
|
|
||||||
final lyricsFuture = useMemoized(() async {
|
|
||||||
if (playback.currentTrack == null ||
|
|
||||||
userPreferences.geniusAccessToken.isEmpty ||
|
|
||||||
(playback.currentTrack?.id != null &&
|
|
||||||
playback.currentTrack?.id == lyrics.value["id"])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final lyricsStr = await getLyrics(
|
|
||||||
playback.currentTrack!.name!,
|
|
||||||
playback.currentTrack!.artists
|
|
||||||
?.map((s) => s.name)
|
|
||||||
.whereNotNull()
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
apiKey: userPreferences.geniusAccessToken,
|
|
||||||
optimizeQuery: true,
|
|
||||||
);
|
|
||||||
if (lyricsStr == null) return Future.error("No lyrics found");
|
|
||||||
return lyricsStr;
|
|
||||||
}, [playback.currentTrack, userPreferences.geniusAccessToken]);
|
|
||||||
|
|
||||||
final lyricsSnapshot = useFuture(lyricsFuture);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
if (lyricsSnapshot.hasData &&
|
|
||||||
lyricsSnapshot.data != null &&
|
|
||||||
playback.currentTrack != null) {
|
|
||||||
lyrics.value = {
|
|
||||||
"lyrics": lyricsSnapshot.data,
|
|
||||||
"id": playback.currentTrack!.id!
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lyrics.value["lyrics"] != null && playback.currentTrack == null) {
|
|
||||||
lyrics.value = {};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [
|
|
||||||
lyricsSnapshot.data,
|
|
||||||
lyricsSnapshot.hasData,
|
|
||||||
lyrics.value,
|
|
||||||
playback.currentTrack,
|
|
||||||
]);
|
|
||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
if (lyrics.value["lyrics"] == null && playback.currentTrack != null) {
|
|
||||||
if (lyricsSnapshot.hasError) {
|
|
||||||
return Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
"Sorry, no Lyrics were found for `${playback.currentTrack?.name}` :'(",
|
|
||||||
style: Theme.of(context).textTheme.headline4,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: CircularProgressIndicator.adaptive(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
@ -110,13 +41,19 @@ class Lyrics extends HookConsumerWidget {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: geniusLyricsSnapshot.when(
|
||||||
lyrics.value["lyrics"] == null &&
|
data: (lyrics) {
|
||||||
playback.currentTrack == null
|
return Text(
|
||||||
? "No Track being played currently"
|
lyrics == null && playback.currentTrack == null
|
||||||
: lyrics.value["lyrics"]!,
|
? "No Track being played currently"
|
||||||
style: textTheme.headline6
|
: lyrics!,
|
||||||
?.copyWith(color: textTheme.headline1?.color),
|
style: textTheme.headline6
|
||||||
|
?.copyWith(color: textTheme.headline1?.color),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, __) => Text(
|
||||||
|
"Sorry, no Lyrics were found for `${playback.currentTrack?.name}` :'("),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -60,8 +60,8 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
content: Container(
|
content: Container(
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
constraints: const BoxConstraints(maxWidth: 500),
|
constraints: const BoxConstraints(maxWidth: 500),
|
||||||
child: Column(
|
child: ListView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: playlistName,
|
controller: playlistName,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/services.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';
|
||||||
@ -12,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);
|
||||||
@ -46,109 +48,120 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||||
final isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
final isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
||||||
playback.currentPlaylist?.id == playlist.id;
|
playback.currentPlaylist?.id == playlist.id;
|
||||||
final update = useForceUpdate();
|
|
||||||
final getMe = useMemoized(() => spotify.me.get(), []);
|
|
||||||
final meSnapshot = useFuture<User>(getMe);
|
|
||||||
|
|
||||||
Future<List<bool>> isFollowing(User me) {
|
final meSnapshot = ref.watch(currentUserQuery);
|
||||||
return spotify.playlists.followedBy(playlist.id!, [me.id!]);
|
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
||||||
}
|
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: FutureBuilder<Iterable<Track>>(
|
body: Column(
|
||||||
future: playlist.id != "user-liked-tracks"
|
children: [
|
||||||
? spotify.playlists.getTracksByPlaylistId(playlist.id).all()
|
PageWindowTitleBar(
|
||||||
: spotify.tracks.me.saved
|
leading: Row(
|
||||||
.all()
|
|
||||||
.then((tracks) => tracks.map((e) => e.track!)),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
List<Track> tracks = snapshot.data?.toList() ?? [];
|
|
||||||
return Column(
|
|
||||||
children: [
|
children: [
|
||||||
PageWindowTitleBar(
|
// nav back
|
||||||
leading: Row(
|
const BackButton(),
|
||||||
children: [
|
// heart playlist
|
||||||
// nav back
|
if (auth.isLoggedIn)
|
||||||
const BackButton(),
|
meSnapshot.when(
|
||||||
// heart playlist
|
data: (me) {
|
||||||
if (auth.isLoggedIn && meSnapshot.hasData)
|
final query = playlistIsFollowedQuery(jsonEncode(
|
||||||
FutureBuilder<List<bool>>(
|
{"playlistId": playlist.id, "userId": me.id!}));
|
||||||
future: isFollowing(meSnapshot.data!),
|
final followingSnapshot = ref.watch(query);
|
||||||
builder: (context, snapshot) {
|
|
||||||
final isFollowing =
|
|
||||||
snapshot.data?.first ?? false;
|
|
||||||
|
|
||||||
if (!snapshot.hasData && !snapshot.hasError) {
|
return followingSnapshot.when(
|
||||||
return const SizedBox(
|
data: (isFollowing) {
|
||||||
height: 25,
|
return HeartButton(
|
||||||
width: 25,
|
isLiked: isFollowing,
|
||||||
child: CircularProgressIndicator.adaptive(),
|
icon: playlist.owner?.id != null &&
|
||||||
);
|
me.id == playlist.owner?.id
|
||||||
|
? Icons.delete_outline_rounded
|
||||||
|
: null,
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
isFollowing
|
||||||
|
? spotify.playlists
|
||||||
|
.unfollowPlaylist(playlist.id!)
|
||||||
|
: spotify.playlists
|
||||||
|
.followPlaylist(playlist.id!);
|
||||||
|
} catch (e, stack) {
|
||||||
|
logger.e("FollowButton.onPressed", e, stack);
|
||||||
|
} finally {
|
||||||
|
ref.refresh(query);
|
||||||
|
ref.refresh(currentUserPlaylistsQuery);
|
||||||
}
|
}
|
||||||
return HeartButton(
|
},
|
||||||
isLiked: isFollowing,
|
);
|
||||||
icon: playlist.owner?.id != null &&
|
},
|
||||||
meSnapshot.data?.id ==
|
error: (error, _) => Text("Error $error"),
|
||||||
playlist.owner?.id
|
loading: () => const CircularProgressIndicator(),
|
||||||
? Icons.delete_outline_rounded
|
);
|
||||||
: null,
|
},
|
||||||
onPressed: () async {
|
error: (error, _) => Text("Error $error"),
|
||||||
try {
|
loading: () => const CircularProgressIndicator(),
|
||||||
isFollowing
|
|
||||||
? spotify.playlists
|
|
||||||
.unfollowPlaylist(playlist.id!)
|
|
||||||
: spotify.playlists
|
|
||||||
.followPlaylist(playlist.id!);
|
|
||||||
} catch (e, stack) {
|
|
||||||
logger.e(
|
|
||||||
"FollowButton.onPressed", e, stack);
|
|
||||||
} finally {
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
// play playlist
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
isPlaylistPlaying
|
|
||||||
? Icons.stop_rounded
|
|
||||||
: Icons.play_arrow_rounded,
|
|
||||||
),
|
|
||||||
onPressed: snapshot.hasData
|
|
||||||
? () => playPlaylist(playback, tracks)
|
|
||||||
: null,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Center(
|
IconButton(
|
||||||
child: Text(playlist.name!,
|
icon: const Icon(Icons.share_rounded),
|
||||||
style: Theme.of(context).textTheme.headline4),
|
onPressed: () {
|
||||||
),
|
final data =
|
||||||
snapshot.hasError
|
"https://open.spotify.com/playlist/${playlist.id}";
|
||||||
? const Center(child: Text("Error occurred"))
|
Clipboard.setData(
|
||||||
: !snapshot.hasData
|
ClipboardData(text: data),
|
||||||
? const Expanded(
|
).then((_) {
|
||||||
child: Center(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
child: CircularProgressIndicator.adaptive()),
|
SnackBar(
|
||||||
)
|
width: 300,
|
||||||
: TracksTableView(
|
behavior: SnackBarBehavior.floating,
|
||||||
tracks,
|
content: Text(
|
||||||
onTrackPlayButtonPressed: (currentTrack) =>
|
"Copied $data to clipboard",
|
||||||
playPlaylist(
|
textAlign: TextAlign.center,
|
||||||
playback,
|
|
||||||
tracks,
|
|
||||||
currentTrack: currentTrack,
|
|
||||||
),
|
|
||||||
playlistId: playlist.id,
|
|
||||||
userPlaylist: playlist.owner?.id != null &&
|
|
||||||
playlist.owner!.id == meSnapshot.data?.id,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// play playlist
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isPlaylistPlaying
|
||||||
|
? Icons.stop_rounded
|
||||||
|
: Icons.play_arrow_rounded,
|
||||||
|
),
|
||||||
|
onPressed: tracksSnapshot.asData?.value != null
|
||||||
|
? () => playPlaylist(
|
||||||
|
playback,
|
||||||
|
tracksSnapshot.asData!.value,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
}),
|
),
|
||||||
|
Center(
|
||||||
|
child: Text(playlist.name!,
|
||||||
|
style: Theme.of(context).textTheme.headline4),
|
||||||
|
),
|
||||||
|
tracksSnapshot.when(
|
||||||
|
data: (tracks) {
|
||||||
|
return TracksTableView(
|
||||||
|
tracks,
|
||||||
|
onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
|
||||||
|
playback,
|
||||||
|
tracks,
|
||||||
|
currentTrack: currentTrack,
|
||||||
|
),
|
||||||
|
playlistId: playlist.id,
|
||||||
|
userPlaylist: playlist.owner?.id != null &&
|
||||||
|
playlist.owner!.id == meSnapshot.asData?.value.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, _) => Text("Error $error"),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
78
lib/components/Settings/About.dart
Normal file
78
lib/components/Settings/About.dart
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:spotube/components/Shared/Hyperlink.dart';
|
||||||
|
import 'package:spotube/hooks/usePackageInfo.dart';
|
||||||
|
|
||||||
|
const licenseText = """
|
||||||
|
BSD-4-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Kingkor Roy Tirtho. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
|
||||||
|
This product includes software developed by Kingkor Roy Tirtho.
|
||||||
|
4. Neither the name of the Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
THIS SOFTWARE IS PROVIDED BY KINGKOR ROY TIRTHO AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KINGKOR ROY TIRTHO AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
""";
|
||||||
|
|
||||||
|
class About extends HookWidget {
|
||||||
|
const About({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final info = usePackageInfo(
|
||||||
|
appName: "Spotube",
|
||||||
|
packageName: "oss.krtirtho.Spotube",
|
||||||
|
version: "2.1.0");
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: const Text("About Spotube"),
|
||||||
|
onTap: () {
|
||||||
|
showAboutDialog(
|
||||||
|
context: context,
|
||||||
|
applicationIcon:
|
||||||
|
CircleAvatar(child: Image.asset("assets/spotube-logo.png")),
|
||||||
|
applicationName: "Spotube",
|
||||||
|
applicationVersion: info.version,
|
||||||
|
applicationLegalese: licenseText,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Text("Author: "),
|
||||||
|
Hyperlink(
|
||||||
|
"Kingkor Roy Tirtho",
|
||||||
|
"https://github.com/KRTirtho",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Hyperlink(
|
||||||
|
"💚 Sponsor/Donate 💚",
|
||||||
|
"https://opencollective.com/spotube",
|
||||||
|
),
|
||||||
|
Text(" • "),
|
||||||
|
Hyperlink(
|
||||||
|
"BSD-4-Clause LICENSE",
|
||||||
|
"https://github.com/KRTirtho/spotube/blob/master/LICENSE",
|
||||||
|
),
|
||||||
|
Text(" • "),
|
||||||
|
Hyperlink(
|
||||||
|
"Bug Report",
|
||||||
|
"https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Center(child: Text("© Spotube 2022. All rights reserved"))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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"),
|
||||||
)
|
)
|
||||||
|
@ -5,12 +5,10 @@ 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:shared_preferences/shared_preferences.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';
|
||||||
@ -22,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,
|
||||||
@ -65,174 +52,119 @@ class Settings extends HookConsumerWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 1366),
|
constraints: const BoxConstraints(maxWidth: 1366),
|
||||||
child: Padding(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
children: [
|
||||||
child: Column(
|
if (!Platform.isAndroid && !Platform.isIOS) ...[
|
||||||
children: [
|
SettingsHotKeyTile(
|
||||||
Row(
|
title: "Next track global shortcut",
|
||||||
children: [
|
currentHotKey: preferences.nextTrackHotKey,
|
||||||
Expanded(
|
onHotKeyRecorded: (value) {
|
||||||
flex: 2,
|
preferences.setNextTrackHotKey(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SettingsHotKeyTile(
|
||||||
|
title: "Prev track global shortcut",
|
||||||
|
currentHotKey: preferences.prevTrackHotKey,
|
||||||
|
onHotKeyRecorded: (value) {
|
||||||
|
preferences.setPrevTrackHotKey(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SettingsHotKeyTile(
|
||||||
|
title: "Play/Pause global shortcut",
|
||||||
|
currentHotKey: preferences.playPauseHotKey,
|
||||||
|
onHotKeyRecorded: (value) {
|
||||||
|
preferences.setPlayPauseHotKey(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ListTile(
|
||||||
|
title: const Text("Theme"),
|
||||||
|
trailing: DropdownButton<ThemeMode>(
|
||||||
|
value: preferences.themeMode,
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Genius Access Token",
|
"Dark",
|
||||||
style: Theme.of(context).textTheme.subtitle1,
|
|
||||||
),
|
),
|
||||||
|
value: ThemeMode.dark,
|
||||||
),
|
),
|
||||||
Expanded(
|
DropdownMenuItem(
|
||||||
flex: 1,
|
child: Text(
|
||||||
child: TextField(
|
"Light",
|
||||||
controller: geniusTokenController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: preferences.geniusAccessToken,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
value: ThemeMode.light,
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
child: Text("System"),
|
||||||
|
value: ThemeMode.system,
|
||||||
),
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: geniusAccessToken.value != null
|
|
||||||
? () async {
|
|
||||||
SharedPreferences localStorage =
|
|
||||||
await SharedPreferences.getInstance();
|
|
||||||
if (geniusAccessToken.value != null &&
|
|
||||||
geniusAccessToken.value!.isNotEmpty) {
|
|
||||||
preferences.setGeniusAccessToken(
|
|
||||||
geniusAccessToken.value!,
|
|
||||||
);
|
|
||||||
localStorage.setString(
|
|
||||||
LocalStorageKeys.geniusAccessToken,
|
|
||||||
geniusAccessToken.value!);
|
|
||||||
}
|
|
||||||
|
|
||||||
geniusAccessToken.value = null;
|
|
||||||
geniusTokenController.text = "";
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: const Text("Save"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
preferences.setThemeMode(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
if (!Platform.isAndroid && !Platform.isIOS) ...[
|
const SizedBox(height: 10),
|
||||||
SettingsHotKeyTile(
|
ListTile(
|
||||||
title: "Next track global shortcut",
|
title: const Text("Accent Color Scheme"),
|
||||||
currentHotKey: preferences.nextTrackHotKey,
|
trailing: ColorTile(
|
||||||
onHotKeyRecorded: (value) {
|
color: preferences.accentColorScheme,
|
||||||
preferences.setNextTrackHotKey(value);
|
onPressed: pickColorScheme(ColorSchemeType.accent),
|
||||||
},
|
isActive: true,
|
||||||
),
|
|
||||||
SettingsHotKeyTile(
|
|
||||||
title: "Prev track global shortcut",
|
|
||||||
currentHotKey: preferences.prevTrackHotKey,
|
|
||||||
onHotKeyRecorded: (value) {
|
|
||||||
preferences.setPrevTrackHotKey(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SettingsHotKeyTile(
|
|
||||||
title: "Play/Pause global shortcut",
|
|
||||||
currentHotKey: preferences.playPauseHotKey,
|
|
||||||
onHotKeyRecorded: (value) {
|
|
||||||
preferences.setPlayPauseHotKey(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Text("Theme"),
|
|
||||||
DropdownButton<ThemeMode>(
|
|
||||||
value: preferences.themeMode,
|
|
||||||
items: const [
|
|
||||||
DropdownMenuItem(
|
|
||||||
child: Text(
|
|
||||||
"Dark",
|
|
||||||
),
|
|
||||||
value: ThemeMode.dark,
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
child: Text(
|
|
||||||
"Light",
|
|
||||||
),
|
|
||||||
value: ThemeMode.light,
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
child: Text("System"),
|
|
||||||
value: ThemeMode.system,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value != null) {
|
|
||||||
preferences.setThemeMode(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
onTap: pickColorScheme(ColorSchemeType.accent),
|
||||||
ListTile(
|
),
|
||||||
title: const Text("Accent Color Scheme"),
|
const SizedBox(height: 10),
|
||||||
trailing: ColorTile(
|
ListTile(
|
||||||
color: preferences.accentColorScheme,
|
title: const Text("Background Color Scheme"),
|
||||||
onPressed: pickColorScheme(ColorSchemeType.accent),
|
trailing: ColorTile(
|
||||||
isActive: true,
|
color: preferences.backgroundColorScheme,
|
||||||
),
|
onPressed: pickColorScheme(ColorSchemeType.background),
|
||||||
onTap: pickColorScheme(ColorSchemeType.accent),
|
isActive: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
onTap: pickColorScheme(ColorSchemeType.background),
|
||||||
ListTile(
|
),
|
||||||
title: const Text("Background Color Scheme"),
|
const SizedBox(height: 10),
|
||||||
trailing: ColorTile(
|
ListTile(
|
||||||
color: preferences.backgroundColorScheme,
|
title:
|
||||||
onPressed:
|
|
||||||
pickColorScheme(ColorSchemeType.background),
|
|
||||||
isActive: true,
|
|
||||||
),
|
|
||||||
onTap: pickColorScheme(ColorSchemeType.background),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Text("Market Place (Recommendation Country)"),
|
const Text("Market Place (Recommendation Country)"),
|
||||||
DropdownButton(
|
trailing: DropdownButton(
|
||||||
value: preferences.recommendationMarket,
|
value: preferences.recommendationMarket,
|
||||||
items: spotifyMarkets
|
items: spotifyMarkets
|
||||||
.map((country) => (DropdownMenuItem(
|
.map((country) => (DropdownMenuItem(
|
||||||
child: Text(country),
|
child: Text(country),
|
||||||
value: country,
|
value: country,
|
||||||
)))
|
)))
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
preferences
|
preferences.setRecommendationMarket(value as String);
|
||||||
.setRecommendationMarket(value as String);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
Row(
|
ListTile(
|
||||||
|
title: const Text("Download lyrics along with the Track"),
|
||||||
|
trailing: Switch.adaptive(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
value: preferences.saveTrackLyrics,
|
||||||
|
onChanged: (state) {
|
||||||
|
preferences.setSaveTrackLyrics(state);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text("Download lyrics along with the Track"),
|
Expanded(
|
||||||
Switch.adaptive(
|
|
||||||
activeColor: Theme.of(context).primaryColor,
|
|
||||||
value: preferences.saveTrackLyrics,
|
|
||||||
onChanged: (state) {
|
|
||||||
preferences.setSaveTrackLyrics(state);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Expanded(
|
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Text(
|
child: Text(
|
||||||
"Format of the YouTube Search term (Case sensitive)"),
|
"Format of the YouTube Search term (Case sensitive)",
|
||||||
|
style: Theme.of(context).textTheme.bodyText1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -242,109 +174,56 @@ class Settings extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
if (auth.isAnonymous)
|
if (auth.isAnonymous)
|
||||||
Wrap(
|
ListTile(
|
||||||
spacing: 20,
|
title: const Text("Login with your Spotify account"),
|
||||||
runSpacing: 20,
|
trailing: ElevatedButton(
|
||||||
alignment: WrapAlignment.spaceBetween,
|
child: Text("Connect with Spotify".toUpperCase()),
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
onPressed: () {
|
||||||
children: [
|
GoRouter.of(context).push("/login");
|
||||||
const Text("Login with your Spotify account"),
|
},
|
||||||
ElevatedButton(
|
style: ButtonStyle(
|
||||||
child: Text("Connect with Spotify".toUpperCase()),
|
shape: MaterialStateProperty.all(
|
||||||
onPressed: () {
|
RoundedRectangleBorder(
|
||||||
GoRouter.of(context).push("/login");
|
borderRadius: BorderRadius.circular(25.0),
|
||||||
},
|
|
||||||
style: ButtonStyle(
|
|
||||||
shape: MaterialStateProperty.all(
|
|
||||||
RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(25.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: Text("Check for Update)"),
|
|
||||||
),
|
|
||||||
Switch.adaptive(
|
|
||||||
activeColor: Theme.of(context).primaryColor,
|
|
||||||
value: preferences.checkUpdate,
|
|
||||||
onChanged: (checked) =>
|
|
||||||
preferences.setCheckUpdate(checked),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
if (auth.isLoggedIn)
|
ListTile(
|
||||||
Builder(builder: (context) {
|
title: const Text("Check for Update"),
|
||||||
Auth auth = ref.watch(authProvider);
|
trailing: Switch.adaptive(
|
||||||
return Row(
|
activeColor: Theme.of(context).primaryColor,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
value: preferences.checkUpdate,
|
||||||
children: [
|
onChanged: (checked) =>
|
||||||
const Text("Log out of this account"),
|
preferences.setCheckUpdate(checked),
|
||||||
ElevatedButton(
|
|
||||||
child: const Text("Logout"),
|
|
||||||
style: ButtonStyle(
|
|
||||||
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),
|
|
||||||
Text(
|
|
||||||
"Spotube v${packageInfo.version}",
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
Row(
|
if (auth.isLoggedIn)
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Builder(builder: (context) {
|
||||||
children: const [
|
Auth auth = ref.watch(authProvider);
|
||||||
Text("Author: "),
|
return ListTile(
|
||||||
Hyperlink(
|
title: const Text("Log out of this account"),
|
||||||
"Kingkor Roy Tirtho",
|
trailing: ElevatedButton(
|
||||||
"https://github.com/KRTirtho",
|
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: 20),
|
const About(),
|
||||||
Wrap(
|
],
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
children: const [
|
|
||||||
Hyperlink(
|
|
||||||
"💚 Sponsor/Donate 💚",
|
|
||||||
"https://opencollective.com/spotube",
|
|
||||||
),
|
|
||||||
Text(" • "),
|
|
||||||
Hyperlink(
|
|
||||||
"BSD-4-Clause LICENSE",
|
|
||||||
"https://github.com/KRTirtho/spotube/blob/master/LICENSE",
|
|
||||||
),
|
|
||||||
Text(" • "),
|
|
||||||
Hyperlink(
|
|
||||||
"Bug Report",
|
|
||||||
"https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
const Text("© Spotube 2022. All rights reserved")
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -15,32 +15,26 @@ class SettingsHotKeyTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return ListTile(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
title: Text(title),
|
||||||
child: Row(
|
trailing: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(title),
|
if (currentHotKey != null) HotKeyVirtualView(hotKey: currentHotKey!),
|
||||||
Row(
|
const SizedBox(width: 10),
|
||||||
children: [
|
ElevatedButton(
|
||||||
if (currentHotKey != null)
|
child: const Text("Set Shortcut"),
|
||||||
HotKeyVirtualView(hotKey: currentHotKey!),
|
onPressed: () {
|
||||||
const SizedBox(width: 10),
|
showDialog(
|
||||||
ElevatedButton(
|
context: context,
|
||||||
child: const Text("Set Shortcut"),
|
builder: (context) {
|
||||||
onPressed: () {
|
return RecordHotKeyDialog(
|
||||||
showDialog(
|
onHotKeyRecorded: onHotKeyRecorded,
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return RecordHotKeyDialog(
|
|
||||||
onHotKeyRecorded: onHotKeyRecorded,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.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';
|
||||||
@ -57,6 +58,22 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
return await spotify.playlists.removeTrack(track.value.uri!, playlistId!);
|
return await spotify.playlists.removeTrack(track.value.uri!, playlistId!);
|
||||||
}, [playlistId, spotify, track.value.uri]);
|
}, [playlistId, spotify, track.value.uri]);
|
||||||
|
|
||||||
|
void actionShare(Track track) {
|
||||||
|
final data = "https://open.spotify.com/track/${track.id}";
|
||||||
|
Clipboard.setData(ClipboardData(text: data)).then((_) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
width: 300,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
content: Text(
|
||||||
|
"Copied $data to clipboard",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
actionAddToPlaylist() async {
|
actionAddToPlaylist() async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -252,6 +269,16 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
value: "favorite",
|
value: "favorite",
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.share_rounded),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text("Share")
|
||||||
|
],
|
||||||
|
),
|
||||||
|
value: "share",
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@ -266,6 +293,9 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
case "remove-playlist":
|
case "remove-playlist":
|
||||||
actionRemoveFromPlaylist();
|
actionRemoveFromPlaylist();
|
||||||
break;
|
break;
|
||||||
|
case "share":
|
||||||
|
actionShare(track.value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
@ -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,
|
||||||
|
@ -13,7 +13,6 @@ ThemeData darkTheme({
|
|||||||
scaffoldBackgroundColor: backgroundMaterialColor[900],
|
scaffoldBackgroundColor: backgroundMaterialColor[900],
|
||||||
dialogBackgroundColor: backgroundMaterialColor[800],
|
dialogBackgroundColor: backgroundMaterialColor[800],
|
||||||
shadowColor: Colors.black26,
|
shadowColor: Colors.black26,
|
||||||
popupMenuTheme: PopupMenuThemeData(color: backgroundMaterialColor[800]),
|
|
||||||
buttonTheme: ButtonThemeData(
|
buttonTheme: ButtonThemeData(
|
||||||
buttonColor: accentMaterialColor,
|
buttonColor: accentMaterialColor,
|
||||||
),
|
),
|
||||||
@ -56,6 +55,13 @@ ThemeData darkTheme({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
popupMenuTheme: PopupMenuThemeData(
|
||||||
|
color: backgroundMaterialColor[800],
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
cardColor: backgroundMaterialColor[800],
|
cardColor: backgroundMaterialColor[800],
|
||||||
canvasColor: backgroundMaterialColor[900],
|
canvasColor: backgroundMaterialColor[900],
|
||||||
);
|
);
|
||||||
|
@ -81,6 +81,13 @@ ThemeData lightTheme({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
popupMenuTheme: PopupMenuThemeData(
|
||||||
|
color: backgroundMaterialColor[100],
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
cardColor: backgroundMaterialColor[50],
|
cardColor: backgroundMaterialColor[50],
|
||||||
canvasColor: backgroundMaterialColor[50],
|
canvasColor: backgroundMaterialColor[50],
|
||||||
);
|
);
|
||||||
|
@ -51,7 +51,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.1"
|
version: "0.1.1"
|
||||||
audio_session:
|
audio_session:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: audio_session
|
name: audio_session
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
|
Loading…
Reference in New Issue
Block a user