mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
refactor(playback): migration to ProxyPlaylist based playback
This commit is contained in:
parent
3ba3df7265
commit
5f70207076
@ -5,7 +5,8 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:spotube/components/player/player_controls.dart';
|
import 'package:spotube/components/player/player_controls.dart';
|
||||||
import 'package:spotube/collections/routes.dart';
|
import 'package:spotube/collections/routes.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
@ -23,19 +24,11 @@ class PlayPauseAction extends Action<PlayPauseIntent> {
|
|||||||
if (PlayerControls.focusNode.canRequestFocus) {
|
if (PlayerControls.focusNode.canRequestFocus) {
|
||||||
PlayerControls.focusNode.requestFocus();
|
PlayerControls.focusNode.requestFocus();
|
||||||
}
|
}
|
||||||
final playlist = intent.ref.read(PlaylistQueueNotifier.provider);
|
|
||||||
final playlistNotifier = intent.ref.read(PlaylistQueueNotifier.notifier);
|
if (!audioPlayer.isPlaying) {
|
||||||
if (playlist == null) {
|
await audioPlayer.resume();
|
||||||
return null;
|
|
||||||
} else if (!audioPlayer.isPlaying) {
|
|
||||||
if (audioPlayer.hasSource && !await audioPlayer.isCompleted) {
|
|
||||||
await playlistNotifier.resume();
|
|
||||||
} else {
|
} else {
|
||||||
// TODO: Implement play on start
|
await audioPlayer.pause();
|
||||||
// await playlistNotifier.play();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await playlistNotifier.pause();
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -98,9 +91,8 @@ class SeekIntent extends Intent {
|
|||||||
class SeekAction extends Action<SeekIntent> {
|
class SeekAction extends Action<SeekIntent> {
|
||||||
@override
|
@override
|
||||||
invoke(intent) async {
|
invoke(intent) async {
|
||||||
final playlist = intent.ref.read(PlaylistQueueNotifier.provider);
|
final playlist = intent.ref.read(ProxyPlaylistNotifier.provider);
|
||||||
final playlistNotifier = intent.ref.read(PlaylistQueueNotifier.notifier);
|
if (playlist.isFetching) {
|
||||||
if (playlist == null || playlist.isLoading) {
|
|
||||||
DirectionalFocusAction().invoke(
|
DirectionalFocusAction().invoke(
|
||||||
DirectionalFocusIntent(
|
DirectionalFocusIntent(
|
||||||
intent.forward ? TraversalDirection.right : TraversalDirection.left,
|
intent.forward ? TraversalDirection.right : TraversalDirection.left,
|
||||||
@ -109,7 +101,7 @@ class SeekAction extends Action<SeekIntent> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final position = (await audioPlayer.position ?? Duration.zero).inSeconds;
|
final position = (await audioPlayer.position ?? Duration.zero).inSeconds;
|
||||||
await playlistNotifier.seek(
|
await audioPlayer.seek(
|
||||||
Duration(
|
Duration(
|
||||||
seconds: intent.forward ? position + 5 : position - 5,
|
seconds: intent.forward ? position + 5 : position - 5,
|
||||||
),
|
),
|
||||||
|
@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/playbutton_card.dart';
|
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -41,16 +41,15 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final queryClient = useQueryClient();
|
final queryClient = useQueryClient();
|
||||||
final query = queryClient
|
final query = queryClient
|
||||||
.getQuery<List<TrackSimple>, dynamic>("album-tracks/${album.id}");
|
.getQuery<List<TrackSimple>, dynamic>("album-tracks/${album.id}");
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
() =>
|
() => playlist.containsTracks(query?.data ?? album.tracks ?? []),
|
||||||
playlistNotifier.isPlayingPlaylist(query?.data ?? album.tracks ?? []),
|
|
||||||
[playlistNotifier, query?.data, album.tracks],
|
[playlistNotifier, query?.data, album.tracks],
|
||||||
);
|
);
|
||||||
final int marginH =
|
final int marginH =
|
||||||
@ -66,7 +65,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading: isPlaylistPlaying && playlist?.isLoading == true,
|
isLoading: isPlaylistPlaying && playlist.isFetching == true,
|
||||||
title: album.name!,
|
title: album.name!,
|
||||||
description:
|
description:
|
||||||
"${AlbumType.from(album.albumType!).formatted} • ${TypeConversionUtils.artists_X_String<ArtistSimple>(album.artists ?? [])}",
|
"${AlbumType.from(album.albumType!).formatted} • ${TypeConversionUtils.artists_X_String<ArtistSimple>(album.artists ?? [])}",
|
||||||
@ -77,16 +76,19 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
updating.value = true;
|
updating.value = true;
|
||||||
try {
|
try {
|
||||||
if (isPlaylistPlaying && playing) {
|
if (isPlaylistPlaying && playing) {
|
||||||
return playlistNotifier.pause();
|
return audioPlayer.pause();
|
||||||
} else if (isPlaylistPlaying && !playing) {
|
} else if (isPlaylistPlaying && !playing) {
|
||||||
return playlistNotifier.resume();
|
return audioPlayer.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
await playlistNotifier.loadAndPlay(album.tracks
|
await playlistNotifier.load(
|
||||||
|
album.tracks
|
||||||
?.map((e) =>
|
?.map((e) =>
|
||||||
TypeConversionUtils.simpleTrack_X_Track(e, album))
|
TypeConversionUtils.simpleTrack_X_Track(e, album))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[]);
|
[],
|
||||||
|
autoPlay: true,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
}
|
}
|
||||||
@ -115,16 +117,15 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (fetchedTracks == null || fetchedTracks.isEmpty) return;
|
if (fetchedTracks == null || fetchedTracks.isEmpty) return;
|
||||||
playlistNotifier.add(
|
playlistNotifier.addTracks(fetchedTracks);
|
||||||
fetchedTracks,
|
|
||||||
);
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final snackbar = SnackBar(
|
final snackbar = SnackBar(
|
||||||
content: Text("Added ${album.tracks?.length} tracks to queue"),
|
content: Text("Added ${album.tracks?.length} tracks to queue"),
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: "Undo",
|
label: "Undo",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
playlistNotifier.remove(fetchedTracks);
|
playlistNotifier
|
||||||
|
.removeTracks(fetchedTracks.map((e) => e.id!));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -22,7 +22,7 @@ import 'package:spotube/components/shared/track_table/track_tile.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/use_async_effect.dart';
|
import 'package:spotube/hooks/use_async_effect.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
@ -132,33 +132,35 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
const UserLocalTracks({Key? key}) : super(key: key);
|
const UserLocalTracks({Key? key}) : super(key: key);
|
||||||
|
|
||||||
void playLocalTracks(
|
void playLocalTracks(
|
||||||
PlaylistQueueNotifier playback,
|
WidgetRef ref,
|
||||||
List<LocalTrack> tracks, {
|
List<LocalTrack> tracks, {
|
||||||
LocalTrack? currentTrack,
|
LocalTrack? currentTrack,
|
||||||
}) async {
|
}) async {
|
||||||
|
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||||
|
final playback = ref.read(ProxyPlaylistNotifier.notifier);
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
final isPlaylistPlaying = playback.isPlayingPlaylist(tracks);
|
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
await playback.loadAndPlay(
|
await playback.load(
|
||||||
tracks,
|
tracks,
|
||||||
active: tracks.indexWhere((s) => s.id == currentTrack?.id),
|
initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playback.state?.activeTrack.id) {
|
currentTrack.id != playlist.activeTrack?.id) {
|
||||||
await playback.playTrack(currentTrack);
|
await playback.jumpToTrack(currentTrack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final sortBy = useState<SortBy>(SortBy.none);
|
final sortBy = useState<SortBy>(SortBy.none);
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final trackSnapshot = ref.watch(localTracksProvider);
|
final trackSnapshot = ref.watch(localTracksProvider);
|
||||||
final isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(
|
final isPlaylistPlaying =
|
||||||
trackSnapshot.value ?? [],
|
playlist.containsTracks(trackSnapshot.value ?? []);
|
||||||
);
|
|
||||||
final isMounted = useIsMounted();
|
final isMounted = useIsMounted();
|
||||||
|
|
||||||
final searchText = useState<String>("");
|
final searchText = useState<String>("");
|
||||||
@ -194,9 +196,12 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
if (trackSnapshot.value?.isNotEmpty == true) {
|
if (trackSnapshot.value?.isNotEmpty == true) {
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playLocalTracks(
|
playLocalTracks(
|
||||||
playlistNotifier, trackSnapshot.value!);
|
ref,
|
||||||
|
trackSnapshot.value!,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
playlistNotifier.stop();
|
// TODO: Remove stop capability
|
||||||
|
// playlistNotifier.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -272,13 +277,13 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
duration:
|
duration:
|
||||||
"${track.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.duration?.inSeconds.remainder(60) ?? 0)}",
|
"${track.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.duration?.inSeconds.remainder(60) ?? 0)}",
|
||||||
track: MapEntry(index, track),
|
track: MapEntry(index, track),
|
||||||
isActive: playlist?.activeTrack.id == track.id,
|
isActive: playlist.activeTrack?.id == track.id,
|
||||||
isChecked: false,
|
isChecked: false,
|
||||||
showCheck: false,
|
showCheck: false,
|
||||||
isLocal: true,
|
isLocal: true,
|
||||||
onTrackPlayButtonPressed: (currentTrack) {
|
onTrackPlayButtonPressed: (currentTrack) {
|
||||||
return playLocalTracks(
|
return playLocalTracks(
|
||||||
playlistNotifier,
|
ref,
|
||||||
sortedTracks,
|
sortedTracks,
|
||||||
currentTrack: track,
|
currentTrack: track,
|
||||||
);
|
);
|
||||||
|
@ -13,7 +13,7 @@ import 'package:spotube/models/local_track.dart';
|
|||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/downloader_provider.dart';
|
import 'package:spotube/provider/downloader_provider.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class PlayerActions extends HookConsumerWidget {
|
class PlayerActions extends HookConsumerWidget {
|
||||||
@ -30,26 +30,26 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final isLocalTrack = playlist?.activeTrack is LocalTrack;
|
final isLocalTrack = playlist.activeTrack is LocalTrack;
|
||||||
final downloader = ref.watch(downloaderProvider);
|
final downloader = ref.watch(downloaderProvider);
|
||||||
final isInQueue = downloader.inQueue
|
final isInQueue = downloader.inQueue
|
||||||
.any((element) => element.id == playlist?.activeTrack.id);
|
.any((element) => element.id == playlist.activeTrack?.id);
|
||||||
final localTracks = [] /* ref.watch(localTracksProvider).value */;
|
final localTracks = [] /* ref.watch(localTracksProvider).value */;
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
|
|
||||||
final isDownloaded = useMemoized(() {
|
final isDownloaded = useMemoized(() {
|
||||||
return localTracks.any(
|
return localTracks.any(
|
||||||
(element) =>
|
(element) =>
|
||||||
element.name == playlist?.activeTrack.name &&
|
element.name == playlist.activeTrack?.name &&
|
||||||
element.album?.name == playlist?.activeTrack.album?.name &&
|
element.album?.name == playlist.activeTrack?.album?.name &&
|
||||||
TypeConversionUtils.artists_X_String<Artist>(
|
TypeConversionUtils.artists_X_String<Artist>(
|
||||||
element.artists ?? []) ==
|
element.artists ?? []) ==
|
||||||
TypeConversionUtils.artists_X_String<Artist>(
|
TypeConversionUtils.artists_X_String<Artist>(
|
||||||
playlist?.activeTrack.artists ?? []),
|
playlist.activeTrack?.artists ?? []),
|
||||||
) ==
|
) ==
|
||||||
true;
|
true;
|
||||||
}, [localTracks, playlist?.activeTrack]);
|
}, [localTracks, playlist.activeTrack]);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: mainAxisAlignment,
|
mainAxisAlignment: mainAxisAlignment,
|
||||||
@ -57,7 +57,7 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(SpotubeIcons.queue),
|
icon: const Icon(SpotubeIcons.queue),
|
||||||
tooltip: context.l10n.queue,
|
tooltip: context.l10n.queue,
|
||||||
onPressed: playlist != null
|
onPressed: playlist.activeTrack != null
|
||||||
? () {
|
? () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@ -83,7 +83,7 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(SpotubeIcons.alternativeRoute),
|
icon: const Icon(SpotubeIcons.alternativeRoute),
|
||||||
tooltip: context.l10n.alternative_track_sources,
|
tooltip: context.l10n.alternative_track_sources,
|
||||||
onPressed: playlist?.activeTrack != null
|
onPressed: playlist.activeTrack != null
|
||||||
? () {
|
? () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@ -120,12 +120,12 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
icon: Icon(
|
icon: Icon(
|
||||||
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,
|
||||||
),
|
),
|
||||||
onPressed: playlist?.activeTrack != null
|
onPressed: playlist.activeTrack != null
|
||||||
? () => downloader.addToQueue(playlist!.activeTrack)
|
? () => downloader.addToQueue(playlist.activeTrack!)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
if (playlist?.activeTrack != null && !isLocalTrack && auth != null)
|
if (playlist.activeTrack != null && !isLocalTrack && auth != null)
|
||||||
TrackHeartButton(track: playlist!.activeTrack),
|
TrackHeartButton(track: playlist.activeTrack!),
|
||||||
...(extraActions ?? [])
|
...(extraActions ?? [])
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ import 'package:spotube/collections/intents.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/use_progress.dart';
|
import 'package:spotube/hooks/use_progress.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
@ -43,8 +43,8 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
SeekIntent: SeekAction(),
|
SeekIntent: SeekAction(),
|
||||||
},
|
},
|
||||||
[]);
|
[]);
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
|
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
@ -145,13 +145,13 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
// than total duration. Keeping it resolved
|
// than total duration. Keeping it resolved
|
||||||
value: progress.value.toDouble(),
|
value: progress.value.toDouble(),
|
||||||
secondaryTrackValue: progressObj.item4,
|
secondaryTrackValue: progressObj.item4,
|
||||||
onChanged: playlist?.isLoading == true || buffering
|
onChanged: playlist.isFetching == true || buffering
|
||||||
? null
|
? null
|
||||||
: (v) {
|
: (v) {
|
||||||
progress.value = v;
|
progress.value = v;
|
||||||
},
|
},
|
||||||
onChangeEnd: (value) async {
|
onChangeEnd: (value) async {
|
||||||
await playlistNotifier.seek(
|
await audioPlayer.seek(
|
||||||
Duration(
|
Duration(
|
||||||
seconds: (value * duration.inSeconds).toInt(),
|
seconds: (value * duration.inSeconds).toInt(),
|
||||||
),
|
),
|
||||||
@ -186,24 +186,27 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
StreamBuilder<bool>(
|
||||||
tooltip: playlist?.shuffled == true
|
stream: audioPlayer.shuffledStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final shuffled = snapshot.data ?? false;
|
||||||
|
return IconButton(
|
||||||
|
tooltip: shuffled
|
||||||
? context.l10n.unshuffle_playlist
|
? context.l10n.unshuffle_playlist
|
||||||
: context.l10n.shuffle_playlist,
|
: context.l10n.shuffle_playlist,
|
||||||
icon: const Icon(SpotubeIcons.shuffle),
|
icon: const Icon(SpotubeIcons.shuffle),
|
||||||
style: playlist?.shuffled == true
|
style: shuffled ? activeButtonStyle : buttonStyle,
|
||||||
? activeButtonStyle
|
onPressed: playlist.isFetching
|
||||||
: buttonStyle,
|
|
||||||
onPressed: playlist == null || playlist.isLoading
|
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (playlist.shuffled == true) {
|
if (shuffled) {
|
||||||
playlistNotifier.setShuffle(false);
|
audioPlayer.setShuffle(false);
|
||||||
} else {
|
} else {
|
||||||
playlistNotifier.setShuffle(true);
|
audioPlayer.setShuffle(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: context.l10n.previous_track,
|
tooltip: context.l10n.previous_track,
|
||||||
icon: const Icon(SpotubeIcons.skipBack),
|
icon: const Icon(SpotubeIcons.skipBack),
|
||||||
@ -214,7 +217,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
tooltip: playing
|
tooltip: playing
|
||||||
? context.l10n.pause_playback
|
? context.l10n.pause_playback
|
||||||
: context.l10n.resume_playback,
|
: context.l10n.resume_playback,
|
||||||
icon: playlist?.isLoading == true
|
icon: playlist.isFetching == true
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
@ -227,7 +230,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
playing ? SpotubeIcons.pause : SpotubeIcons.play,
|
playing ? SpotubeIcons.pause : SpotubeIcons.play,
|
||||||
),
|
),
|
||||||
style: resumePauseStyle,
|
style: resumePauseStyle,
|
||||||
onPressed: playlist?.isLoading == true
|
onPressed: playlist.isFetching == true
|
||||||
? null
|
? null
|
||||||
: Actions.handler<PlayPauseIntent>(
|
: Actions.handler<PlayPauseIntent>(
|
||||||
context,
|
context,
|
||||||
@ -240,30 +243,35 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
style: buttonStyle,
|
style: buttonStyle,
|
||||||
onPressed: playlistNotifier.next,
|
onPressed: playlistNotifier.next,
|
||||||
),
|
),
|
||||||
IconButton(
|
StreamBuilder<PlaybackLoopMode>(
|
||||||
tooltip: playlist?.loopMode == PlaybackLoopMode.one
|
stream: audioPlayer.loopModeStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
|
||||||
|
return IconButton(
|
||||||
|
tooltip: loopMode == PlaybackLoopMode.one
|
||||||
? context.l10n.loop_track
|
? context.l10n.loop_track
|
||||||
: context.l10n.repeat_playlist,
|
: context.l10n.repeat_playlist,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
playlist?.loopMode == PlaybackLoopMode.one
|
loopMode == PlaybackLoopMode.one
|
||||||
? SpotubeIcons.repeatOne
|
? SpotubeIcons.repeatOne
|
||||||
: SpotubeIcons.repeat,
|
: SpotubeIcons.repeat,
|
||||||
),
|
),
|
||||||
style: playlist?.loopMode == PlaybackLoopMode.one
|
style: loopMode == PlaybackLoopMode.one
|
||||||
? activeButtonStyle
|
? activeButtonStyle
|
||||||
: buttonStyle,
|
: buttonStyle,
|
||||||
onPressed: playlist == null || playlist.isLoading
|
onPressed: playlist.isFetching
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (playlist.loopMode == PlaybackLoopMode.one) {
|
if (loopMode == PlaybackLoopMode.one) {
|
||||||
playlistNotifier
|
audioPlayer
|
||||||
.setLoopMode(PlaybackLoopMode.all);
|
.setLoopMode(PlaybackLoopMode.all);
|
||||||
} else {
|
} else {
|
||||||
playlistNotifier
|
audioPlayer
|
||||||
.setLoopMode(PlaybackLoopMode.one);
|
.setLoopMode(PlaybackLoopMode.one);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5)
|
const SizedBox(height: 5)
|
||||||
|
@ -9,7 +9,7 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/player/player_track_details.dart';
|
import 'package:spotube/components/player/player_track_details.dart';
|
||||||
import 'package:spotube/collections/intents.dart';
|
import 'package:spotube/collections/intents.dart';
|
||||||
import 'package:spotube/hooks/use_progress.dart';
|
import 'package:spotube/hooks/use_progress.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -24,10 +24,10 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final canShow = ref.watch(
|
final canShow = ref.watch(
|
||||||
PlaylistQueueNotifier.provider.select((s) => s != null),
|
ProxyPlaylistNotifier.provider.select((s) => s != null),
|
||||||
);
|
);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: playlist?.isLoading == true
|
icon: playlist.isFetching
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
|
@ -10,7 +10,7 @@ import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
|||||||
import 'package:spotube/components/shared/track_table/track_tile.dart';
|
import 'package:spotube/components/shared/track_table/track_tile.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/use_auto_scroll_controller.dart';
|
import 'package:spotube/hooks/use_auto_scroll_controller.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
|
|
||||||
class PlayerQueue extends HookConsumerWidget {
|
class PlayerQueue extends HookConsumerWidget {
|
||||||
@ -22,10 +22,10 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final controller = useAutoScrollController();
|
final controller = useAutoScrollController();
|
||||||
final tracks = playlist?.tracks ?? {};
|
final tracks = playlist.tracks;
|
||||||
|
|
||||||
if (tracks.isEmpty) {
|
if (tracks.isEmpty) {
|
||||||
return const NotFound(vertical: true);
|
return const NotFound(vertical: true);
|
||||||
@ -41,11 +41,11 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
final headlineColor = theme.textTheme.headlineSmall?.color;
|
final headlineColor = theme.textTheme.headlineSmall?.color;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (playlist == null) return null;
|
if (playlist.active == null) return null;
|
||||||
final index = playlist.active;
|
|
||||||
if (index < 0) return;
|
if (playlist.active! < 0) return;
|
||||||
controller.scrollToIndex(
|
controller.scrollToIndex(
|
||||||
index,
|
playlist.active!,
|
||||||
preferPosition: AutoScrollPosition.middle,
|
preferPosition: AutoScrollPosition.middle,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
@ -113,7 +113,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: ReorderableListView.builder(
|
child: ReorderableListView.builder(
|
||||||
onReorder: (oldIndex, newIndex) {
|
onReorder: (oldIndex, newIndex) {
|
||||||
playlistNotifier.reorder(oldIndex, newIndex);
|
playlistNotifier.moveTrack(oldIndex, newIndex);
|
||||||
},
|
},
|
||||||
scrollController: controller,
|
scrollController: controller,
|
||||||
itemCount: tracks.length,
|
itemCount: tracks.length,
|
||||||
@ -133,12 +133,12 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
playlist,
|
playlist,
|
||||||
track: track,
|
track: track,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
isActive: playlist?.activeTrack.id == track.value.id,
|
isActive: playlist.activeTrack?.id == track.value.id,
|
||||||
onTrackPlayButtonPressed: (currentTrack) async {
|
onTrackPlayButtonPressed: (currentTrack) async {
|
||||||
if (playlist?.activeTrack.id == track.value.id) {
|
if (playlist.activeTrack?.id == track.value.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await playlistNotifier.playTrack(currentTrack);
|
await playlistNotifier.jumpToTrack(currentTrack);
|
||||||
},
|
},
|
||||||
leadingActions: [
|
leadingActions: [
|
||||||
ReorderableDragStartListener(
|
ReorderableDragStartListener(
|
||||||
|
@ -5,7 +5,7 @@ import 'package:spotify/spotify.dart';
|
|||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class PlayerTrackDetails extends HookConsumerWidget {
|
class PlayerTrackDetails extends HookConsumerWidget {
|
||||||
@ -18,11 +18,11 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
final playback = ref.watch(PlaylistQueueNotifier.provider);
|
final playback = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
if (playback != null)
|
if (playback.activeTrack != null)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
@ -44,7 +44,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
playback?.activeTrack.name ?? "",
|
playback.activeTrack?.name ?? "",
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: color,
|
color: color,
|
||||||
@ -52,7 +52,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
TypeConversionUtils.artists_X_String<Artist>(
|
TypeConversionUtils.artists_X_String<Artist>(
|
||||||
playback?.activeTrack.artists ?? [],
|
playback.activeTrack?.artists ?? [],
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodySmall!.copyWith(color: color),
|
style: theme.textTheme.bodySmall!.copyWith(color: color),
|
||||||
@ -66,12 +66,12 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
playback?.activeTrack.name ?? "",
|
playback.activeTrack?.name ?? "",
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, color: color),
|
style: TextStyle(fontWeight: FontWeight.bold, color: color),
|
||||||
),
|
),
|
||||||
TypeConversionUtils.artists_X_ClickableArtists(
|
TypeConversionUtils.artists_X_ClickableArtists(
|
||||||
playback?.activeTrack.artists ?? [],
|
playback.activeTrack?.artists ?? [],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/spotube_track.dart';
|
import 'package:spotube/models/spotube_track.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
@ -21,11 +21,11 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
|
|
||||||
final siblings = playlist?.isLoading == false
|
final siblings = playlist.isFetching == false
|
||||||
? (playlist!.activeTrack as SpotubeTrack).siblings
|
? (playlist.activeTrack as SpotubeTrack).siblings
|
||||||
: <Video>[];
|
: <Video>[];
|
||||||
|
|
||||||
final borderRadius = floating
|
final borderRadius = floating
|
||||||
@ -36,12 +36,12 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (playlist?.activeTrack is SpotubeTrack &&
|
if (playlist.activeTrack is SpotubeTrack &&
|
||||||
(playlist?.activeTrack as SpotubeTrack).siblings.isEmpty) {
|
(playlist.activeTrack as SpotubeTrack).siblings.isEmpty) {
|
||||||
playlistNotifier.populateSibling();
|
playlistNotifier.populateSibling();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [playlist?.activeTrack]);
|
}, [playlist.activeTrack]);
|
||||||
|
|
||||||
return BackdropFilter(
|
return BackdropFilter(
|
||||||
filter: ImageFilter.blur(
|
filter: ImageFilter.blur(
|
||||||
@ -91,18 +91,18 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(video.author),
|
subtitle: Text(video.author),
|
||||||
enabled: playlist?.isLoading != true,
|
enabled: playlist.isFetching != true,
|
||||||
selected: playlist?.isLoading != true &&
|
selected: playlist.isFetching != true &&
|
||||||
video.id.value ==
|
video.id.value ==
|
||||||
(playlist?.activeTrack as SpotubeTrack)
|
(playlist.activeTrack as SpotubeTrack)
|
||||||
.ytTrack
|
.ytTrack
|
||||||
.id
|
.id
|
||||||
.value,
|
.value,
|
||||||
selectedTileColor: theme.popupMenuTheme.color,
|
selectedTileColor: theme.popupMenuTheme.color,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (playlist?.isLoading == false &&
|
if (playlist.isFetching == false &&
|
||||||
video.id.value !=
|
video.id.value !=
|
||||||
(playlist?.activeTrack as SpotubeTrack)
|
(playlist.activeTrack as SpotubeTrack)
|
||||||
.ytTrack
|
.ytTrack
|
||||||
.id
|
.id
|
||||||
.value) {
|
.value) {
|
||||||
|
@ -4,7 +4,7 @@ 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/components/shared/playbutton_card.dart';
|
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
@ -19,8 +19,8 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlistQueue = ref.watch(PlaylistQueueNotifier.provider);
|
final playlistQueue = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
final queryBowl = QueryClient.of(context);
|
final queryBowl = QueryClient.of(context);
|
||||||
@ -29,8 +29,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
final tracks = useState<List<TrackSimple>?>(null);
|
final tracks = useState<List<TrackSimple>?>(null);
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
() =>
|
() => playlistQueue.containsTracks(tracks.value ?? query?.data ?? []),
|
||||||
playlistNotifier.isPlayingPlaylist(tracks.value ?? query?.data ?? []),
|
|
||||||
[playlistNotifier, tracks.value, query?.data],
|
[playlistNotifier, tracks.value, query?.data],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -46,8 +45,8 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading: (isPlaylistPlaying && playlistQueue?.isLoading == true) ||
|
isLoading:
|
||||||
updating.value,
|
(isPlaylistPlaying && playlistQueue.isFetching) || updating.value,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.navigate(
|
ServiceUtils.navigate(
|
||||||
context,
|
context,
|
||||||
@ -59,9 +58,9 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
updating.value = true;
|
updating.value = true;
|
||||||
if (isPlaylistPlaying && playing) {
|
if (isPlaylistPlaying && playing) {
|
||||||
return playlistNotifier.pause();
|
return audioPlayer.pause();
|
||||||
} else if (isPlaylistPlaying && !playing) {
|
} else if (isPlaylistPlaying && !playing) {
|
||||||
return playlistNotifier.resume();
|
return audioPlayer.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Track> fetchedTracks = await queryBowl.fetchQuery(
|
List<Track> fetchedTracks = await queryBowl.fetchQuery(
|
||||||
@ -72,7 +71,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (fetchedTracks.isEmpty) return;
|
if (fetchedTracks.isEmpty) return;
|
||||||
|
|
||||||
await playlistNotifier.loadAndPlay(fetchedTracks);
|
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
||||||
tracks.value = fetchedTracks;
|
tracks.value = fetchedTracks;
|
||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
@ -90,7 +89,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (fetchedTracks.isEmpty) return;
|
if (fetchedTracks.isEmpty) return;
|
||||||
|
|
||||||
playlistNotifier.add(fetchedTracks);
|
playlistNotifier.addTracks(fetchedTracks);
|
||||||
tracks.value = fetchedTracks;
|
tracks.value = fetchedTracks;
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final snackbar = SnackBar(
|
final snackbar = SnackBar(
|
||||||
@ -98,7 +97,8 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: "Undo",
|
label: "Undo",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
playlistNotifier.remove(fetchedTracks);
|
playlistNotifier
|
||||||
|
.removeTracks(fetchedTracks.map((e) => e.id!));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -17,8 +17,9 @@ import 'package:spotube/hooks/use_breakpoints.dart';
|
|||||||
import 'package:spotube/hooks/use_brightness_value.dart';
|
import 'package:spotube/hooks/use_brightness_value.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
@ -28,21 +29,21 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
final logger = getLogger(BottomPlayer);
|
final logger = getLogger(BottomPlayer);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final layoutMode =
|
final layoutMode =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => playlist?.activeTrack.album?.images?.isNotEmpty == true
|
() => playlist.activeTrack?.album?.images?.isNotEmpty == true
|
||||||
? TypeConversionUtils.image_X_UrlString(
|
? TypeConversionUtils.image_X_UrlString(
|
||||||
playlist?.activeTrack.album?.images,
|
playlist.activeTrack?.album?.images,
|
||||||
index: (playlist?.activeTrack.album?.images?.length ?? 1) - 1,
|
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
)
|
)
|
||||||
: Assets.albumPlaceholder.path,
|
: Assets.albumPlaceholder.path,
|
||||||
[playlist?.activeTrack.album?.images],
|
[playlist.activeTrack?.album?.images],
|
||||||
);
|
);
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@ -90,9 +91,8 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
constraints: const BoxConstraints(maxWidth: 200),
|
constraints: const BoxConstraints(maxWidth: 200),
|
||||||
child: HookBuilder(builder: (context) {
|
child: HookBuilder(builder: (context) {
|
||||||
final volumeState =
|
final volumeState =
|
||||||
ref.watch(VolumeProvider.provider);
|
useStream(audioPlayer.volumeStream).data ??
|
||||||
final volumeNotifier =
|
audioPlayer.volume;
|
||||||
ref.watch(VolumeProvider.provider.notifier);
|
|
||||||
final volume = useState(volumeState);
|
final volume = useState(volumeState);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
@ -107,12 +107,10 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
if (event is PointerScrollEvent) {
|
if (event is PointerScrollEvent) {
|
||||||
if (event.scrollDelta.dy > 0) {
|
if (event.scrollDelta.dy > 0) {
|
||||||
final value = volume.value - .2;
|
final value = volume.value - .2;
|
||||||
volumeNotifier
|
audioPlayer.setVolume(value < 0 ? 0 : value);
|
||||||
.setVolume(value < 0 ? 0 : value);
|
|
||||||
} else {
|
} else {
|
||||||
final value = volume.value + .2;
|
final value = volume.value + .2;
|
||||||
volumeNotifier
|
audioPlayer.setVolume(value > 1 ? 1 : value);
|
||||||
.setVolume(value > 1 ? 1 : value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -123,7 +121,7 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
volume.value = v;
|
volume.value = v;
|
||||||
},
|
},
|
||||||
onChangeEnd: volumeNotifier.setVolume,
|
onChangeEnd: audioPlayer.setVolume,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
@ -15,14 +15,15 @@ import 'package:spotube/hooks/use_breakpoints.dart';
|
|||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/mutations/mutations.dart';
|
import 'package:spotube/services/mutations/mutations.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class TrackTile extends HookConsumerWidget {
|
class TrackTile extends HookConsumerWidget {
|
||||||
final PlaylistQueue? playlist;
|
final ProxyPlaylist playlist;
|
||||||
final MapEntry<int, Track> track;
|
final MapEntry<int, Track> track;
|
||||||
final String duration;
|
final String duration;
|
||||||
final void Function(Track currentTrack)? onTrackPlayButtonPressed;
|
final void Function(Track currentTrack)? onTrackPlayButtonPressed;
|
||||||
@ -75,7 +76,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
final playlistQueueNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
|
|
||||||
final removingTrack = useState<String?>(null);
|
final removingTrack = useState<String?>(null);
|
||||||
final removeTrack = useMutations.playlist.removeTrackOf(
|
final removeTrack = useMutations.playlist.removeTrackOf(
|
||||||
@ -238,7 +239,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
playlist?.activeTrack.id == track.value.id
|
playlist.activeTrack?.id == track.value.id
|
||||||
? SpotubeIcons.pause
|
? SpotubeIcons.pause
|
||||||
: SpotubeIcons.play,
|
: SpotubeIcons.play,
|
||||||
),
|
),
|
||||||
@ -312,11 +313,11 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
tooltip: context.l10n.more_actions,
|
tooltip: context.l10n.more_actions,
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
return [
|
||||||
if (!playlistQueueNotifier.isTrackOnQueue(track.value)) ...[
|
if (!playlist.containsTrack(track.value)) ...[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
playlistQueueNotifier.add([track.value]);
|
playback.addTrack(track.value);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
@ -334,7 +335,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
playlistQueueNotifier.playNext([track.value]);
|
playback.addTracksAtFirst([track.value]);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
@ -352,10 +353,10 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
] else
|
] else
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onTap: playlist?.activeTrack.id == track.value.id
|
onTap: playlist.activeTrack?.id == track.value.id
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
playlistQueueNotifier.remove([track.value]);
|
playback.removeTrack(track.value.id!);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
@ -366,7 +367,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
enabled: playlist?.activeTrack.id != track.value.id,
|
enabled: playlist.activeTrack?.id != track.value.id,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(SpotubeIcons.queueRemove),
|
leading: const Icon(SpotubeIcons.queueRemove),
|
||||||
title: Text(context.l10n.remove_from_queue),
|
title: Text(context.l10n.remove_from_queue),
|
||||||
|
@ -14,7 +14,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/downloader_provider.dart';
|
import 'package:spotube/provider/downloader_provider.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -41,8 +41,8 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(context, ref) {
|
Widget build(context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final downloader = ref.watch(downloaderProvider);
|
final downloader = ref.watch(downloaderProvider);
|
||||||
TextStyle tableHeadStyle =
|
TextStyle tableHeadStyle =
|
||||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
||||||
@ -229,14 +229,14 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
case "play-next":
|
case "play-next":
|
||||||
{
|
{
|
||||||
playlistNotifier.playNext(selectedTracks.toList());
|
playback.addTracksAtFirst(selectedTracks);
|
||||||
selected.value = [];
|
selected.value = [];
|
||||||
showCheck.value = false;
|
showCheck.value = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "add-to-queue":
|
case "add-to-queue":
|
||||||
{
|
{
|
||||||
playlistNotifier.add(selectedTracks.toList());
|
playback.addTracks(selectedTracks);
|
||||||
selected.value = [];
|
selected.value = [];
|
||||||
showCheck.value = false;
|
showCheck.value = false;
|
||||||
break;
|
break;
|
||||||
@ -310,7 +310,7 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
track: track,
|
track: track,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
userPlaylist: userPlaylist,
|
userPlaylist: userPlaylist,
|
||||||
isActive: playlist?.activeTrack.id == track.value.id,
|
isActive: playlist.activeTrack?.id == track.value.id,
|
||||||
onTrackPlayButtonPressed: onTrackPlayButtonPressed,
|
onTrackPlayButtonPressed: onTrackPlayButtonPressed,
|
||||||
isChecked: selected.value.contains(track.value.id),
|
isChecked: selected.value.contains(track.value.id),
|
||||||
showCheck: showCheck.value,
|
showCheck: showCheck.value,
|
||||||
|
@ -3,7 +3,8 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.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:spotube/collections/intents.dart';
|
import 'package:spotube/collections/intents.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
|
|
||||||
void useInitSysTray(WidgetRef ref) {
|
void useInitSysTray(WidgetRef ref) {
|
||||||
@ -12,15 +13,15 @@ void useInitSysTray(WidgetRef ref) {
|
|||||||
|
|
||||||
final initializeMenu = useCallback(() async {
|
final initializeMenu = useCallback(() async {
|
||||||
systemTray.value?.destroy();
|
systemTray.value?.destroy();
|
||||||
final playlistQueue = ref.read(PlaylistQueueNotifier.notifier);
|
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||||
|
final playlistQueue = ref.read(ProxyPlaylistNotifier.notifier);
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
if (!preferences.showSystemTrayIcon) {
|
if (!preferences.showSystemTrayIcon) {
|
||||||
await systemTray.value?.destroy();
|
await systemTray.value?.destroy();
|
||||||
systemTray.value = null;
|
systemTray.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final enabled =
|
final enabled = !playlist.isFetching;
|
||||||
playlistQueue.isLoaded && playlistQueue.state?.isLoading != true;
|
|
||||||
systemTray.value = await DesktopTools.createSystemTrayMenu(
|
systemTray.value = await DesktopTools.createSystemTrayMenu(
|
||||||
title: "Spotube",
|
title: "Spotube",
|
||||||
iconPath: "assets/spotube-logo.png",
|
iconPath: "assets/spotube-logo.png",
|
||||||
@ -51,7 +52,7 @@ void useInitSysTray(WidgetRef ref) {
|
|||||||
MenuItemLabel(
|
MenuItemLabel(
|
||||||
label: "Next",
|
label: "Next",
|
||||||
name: "next",
|
name: "next",
|
||||||
enabled: enabled && (playlistQueue.state?.tracks.length ?? 0) > 1,
|
enabled: enabled && (playlist.tracks.length) > 1,
|
||||||
onClicked: (p0) async {
|
onClicked: (p0) async {
|
||||||
await playlistQueue.next();
|
await playlistQueue.next();
|
||||||
},
|
},
|
||||||
@ -59,7 +60,7 @@ void useInitSysTray(WidgetRef ref) {
|
|||||||
MenuItemLabel(
|
MenuItemLabel(
|
||||||
label: "Previous",
|
label: "Previous",
|
||||||
name: "previous",
|
name: "previous",
|
||||||
enabled: enabled && (playlistQueue.state?.tracks.length ?? 0) > 1,
|
enabled: enabled && (playlist.tracks.length) > 1,
|
||||||
onClicked: (p0) async {
|
onClicked: (p0) async {
|
||||||
await playlistQueue.previous();
|
await playlistQueue.previous();
|
||||||
},
|
},
|
||||||
@ -101,8 +102,8 @@ void useInitSysTray(WidgetRef ref) {
|
|||||||
|
|
||||||
useReassemble(initializeMenu);
|
useReassemble(initializeMenu);
|
||||||
|
|
||||||
ref.listen<PlaylistQueue?>(
|
ref.listen<ProxyPlaylist?>(
|
||||||
PlaylistQueueNotifier.provider,
|
ProxyPlaylistNotifier.provider,
|
||||||
(previous, next) {
|
(previous, next) {
|
||||||
initializeMenu();
|
initializeMenu();
|
||||||
},
|
},
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
Tuple4<double, Duration, Duration, double> useProgress(WidgetRef ref) {
|
Tuple4<double, Duration, Duration, double> useProgress(WidgetRef ref) {
|
||||||
ref.watch(PlaylistQueueNotifier.provider);
|
ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
|
|
||||||
final bufferProgress =
|
final bufferProgress =
|
||||||
useStream(audioPlayer.bufferedPositionStream).data?.inSeconds ?? 0;
|
useStream(audioPlayer.bufferedPositionStream).data?.inSeconds ?? 0;
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
|
||||||
|
|
||||||
// Duration future is needed for getting the duration of the song
|
// Duration future is needed for getting the duration of the song
|
||||||
// as stream can be null when no event occurs (Mostly needed for android)
|
// as stream can be null when no event occurs (Mostly needed for android)
|
||||||
@ -32,9 +31,9 @@ Tuple4<double, Duration, Duration, double> useProgress(WidgetRef ref) {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
if (positionSnapshot.hasData && duration == Duration.zero) {
|
if (positionSnapshot.hasData && duration == Duration.zero) {
|
||||||
await Future.delayed(const Duration(milliseconds: 200));
|
await Future.delayed(const Duration(milliseconds: 200));
|
||||||
await playlistNotifier.pause();
|
await audioPlayer.pause();
|
||||||
await Future.delayed(const Duration(milliseconds: 400));
|
await Future.delayed(const Duration(milliseconds: 400));
|
||||||
await playlistNotifier.resume();
|
await audioPlayer.resume();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
|
@ -7,7 +7,7 @@ import 'package:spotube/components/shared/heart_button.dart';
|
|||||||
import 'package:spotube/components/shared/track_table/track_collection_view.dart';
|
import 'package:spotube/components/shared/track_table/track_collection_view.dart';
|
||||||
import 'package:spotube/components/shared/track_table/tracks_table_view.dart';
|
import 'package:spotube/components/shared/track_table/tracks_table_view.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
@ -17,32 +17,32 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
const AlbumPage(this.album, {Key? key}) : super(key: key);
|
const AlbumPage(this.album, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
Future<void> playPlaylist(
|
Future<void> playPlaylist(
|
||||||
PlaylistQueueNotifier playback,
|
|
||||||
List<Track> tracks,
|
List<Track> tracks,
|
||||||
WidgetRef ref, {
|
WidgetRef ref, {
|
||||||
Track? currentTrack,
|
Track? currentTrack,
|
||||||
}) async {
|
}) async {
|
||||||
final playlist = ref.read(PlaylistQueueNotifier.provider);
|
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
||||||
|
final playback = ref.read(ProxyPlaylistNotifier.notifier);
|
||||||
final sortBy = ref.read(trackCollectionSortState(album.id!));
|
final sortBy = ref.read(trackCollectionSortState(album.id!));
|
||||||
final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy);
|
final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy);
|
||||||
currentTrack ??= sortedTracks.first;
|
currentTrack ??= sortedTracks.first;
|
||||||
final isPlaylistPlaying = playback.isPlayingPlaylist(tracks);
|
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
await playback.loadAndPlay(
|
await playback.load(
|
||||||
sortedTracks,
|
sortedTracks,
|
||||||
active: sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
initialIndex: sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
);
|
);
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playlist?.activeTrack.id) {
|
currentTrack.id != playlist.activeTrack?.id) {
|
||||||
await playback.playTrack(currentTrack);
|
await playback.jumpToTrack(currentTrack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playback = ref.watch(PlaylistQueueNotifier.notifier);
|
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
|
|
||||||
final tracksSnapshot = useQueries.album.tracksOf(ref, album.id!);
|
final tracksSnapshot = useQueries.album.tracksOf(ref, album.id!);
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
final isAlbumPlaying = useMemoized(
|
final isAlbumPlaying = useMemoized(
|
||||||
() => playback.isPlayingPlaylist(tracksSnapshot.data ?? []),
|
() => playlist.containsTracks(tracksSnapshot.data ?? []),
|
||||||
[playback, tracksSnapshot.data],
|
[playback, tracksSnapshot.data],
|
||||||
);
|
);
|
||||||
return TrackCollectionView(
|
return TrackCollectionView(
|
||||||
@ -72,7 +72,6 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
if (tracksSnapshot.hasData) {
|
if (tracksSnapshot.hasData) {
|
||||||
if (!isAlbumPlaying) {
|
if (!isAlbumPlaying) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playback,
|
|
||||||
tracksSnapshot.data!
|
tracksSnapshot.data!
|
||||||
.map((track) =>
|
.map((track) =>
|
||||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||||
@ -81,7 +80,6 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
} else if (isAlbumPlaying && track != null) {
|
} else if (isAlbumPlaying && track != null) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playback,
|
|
||||||
tracksSnapshot.data!
|
tracksSnapshot.data!
|
||||||
.map((track) =>
|
.map((track) =>
|
||||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||||
@ -90,18 +88,14 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
ref,
|
ref,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
playback.remove(
|
playback
|
||||||
tracksSnapshot.data!
|
.removeTracks(tracksSnapshot.data!.map((track) => track.id!));
|
||||||
.map((track) =>
|
|
||||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onAddToQueue: () {
|
onAddToQueue: () {
|
||||||
if (tracksSnapshot.hasData && !isAlbumPlaying) {
|
if (tracksSnapshot.hasData && !isAlbumPlaying) {
|
||||||
playback.add(
|
playback.addTracks(
|
||||||
tracksSnapshot.data!
|
tracksSnapshot.data!
|
||||||
.map((track) =>
|
.map((track) =>
|
||||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||||
@ -125,19 +119,18 @@ class AlbumPage extends HookConsumerWidget {
|
|||||||
..shuffle();
|
..shuffle();
|
||||||
if (!isAlbumPlaying) {
|
if (!isAlbumPlaying) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playback,
|
|
||||||
tracks,
|
tracks,
|
||||||
ref,
|
ref,
|
||||||
);
|
);
|
||||||
} else if (isAlbumPlaying && track != null) {
|
} else if (isAlbumPlaying && track != null) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playback,
|
|
||||||
tracks,
|
tracks,
|
||||||
ref,
|
ref,
|
||||||
currentTrack: track,
|
currentTrack: track,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
playback.stop();
|
// TODO: Disable ability to stop playback from playlist/album
|
||||||
|
// playback.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -19,7 +19,7 @@ import 'package:spotube/models/logger.dart';
|
|||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
@ -57,8 +57,8 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
|
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
|
|
||||||
@ -298,8 +298,7 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
artistId,
|
artistId,
|
||||||
);
|
);
|
||||||
|
|
||||||
final isPlaylistPlaying =
|
final isPlaylistPlaying = playlist.containsTracks(
|
||||||
playlistNotifier.isPlayingPlaylist(
|
|
||||||
topTracksQuery.data ?? <Track>[],
|
topTracksQuery.data ?? <Track>[],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -318,13 +317,16 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
{Track? currentTrack}) async {
|
{Track? currentTrack}) async {
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playlistNotifier.loadAndPlay(tracks,
|
playlistNotifier.load(
|
||||||
active: tracks.indexWhere(
|
tracks,
|
||||||
(s) => s.id == currentTrack?.id));
|
initialIndex: tracks
|
||||||
|
.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
|
autoPlay: true,
|
||||||
|
);
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playlist?.activeTrack.id) {
|
currentTrack.id != playlist.activeTrack?.id) {
|
||||||
await playlistNotifier.playTrack(currentTrack);
|
await playlistNotifier.jumpToTrack(currentTrack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,7 +343,8 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
SpotubeIcons.queueAdd,
|
SpotubeIcons.queueAdd,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
playlistNotifier.add(topTracks.toList());
|
playlistNotifier
|
||||||
|
.addTracks(topTracks.toList());
|
||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
width: 300,
|
width: 300,
|
||||||
@ -380,7 +383,7 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
track: track,
|
track: track,
|
||||||
isActive:
|
isActive:
|
||||||
playlist?.activeTrack.id == track.value.id,
|
playlist.activeTrack?.id == track.value.id,
|
||||||
onTrackPlayButtonPressed: (currentTrack) =>
|
onTrackPlayButtonPressed: (currentTrack) =>
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
topTracks.toList(),
|
topTracks.toList(),
|
||||||
|
@ -16,7 +16,7 @@ import 'package:spotube/hooks/use_palette_color.dart';
|
|||||||
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||||
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
@ -26,14 +26,14 @@ class LyricsPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => TypeConversionUtils.image_X_UrlString(
|
() => TypeConversionUtils.image_X_UrlString(
|
||||||
playlist?.activeTrack.album?.images,
|
playlist.activeTrack?.album?.images,
|
||||||
index: (playlist?.activeTrack.album?.images?.length ?? 1) - 1,
|
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
[playlist?.activeTrack.album?.images],
|
[playlist.activeTrack?.album?.images],
|
||||||
);
|
);
|
||||||
final palette = usePaletteColor(albumArt, ref);
|
final palette = usePaletteColor(albumArt, ref);
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
@ -15,7 +15,7 @@ import 'package:spotube/hooks/use_force_update.dart';
|
|||||||
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||||
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class MiniLyricsPage extends HookConsumerWidget {
|
class MiniLyricsPage extends HookConsumerWidget {
|
||||||
@ -28,7 +28,7 @@ class MiniLyricsPage extends HookConsumerWidget {
|
|||||||
final prevSize = useRef<Size?>(null);
|
final prevSize = useRef<Size?>(null);
|
||||||
final wasMaximized = useRef<bool>(false);
|
final wasMaximized = useRef<bool>(false);
|
||||||
|
|
||||||
final playlistQueue = ref.watch(PlaylistQueueNotifier.provider);
|
final playlistQueue = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
|
|
||||||
final areaActive = useState(false);
|
final areaActive = useState(false);
|
||||||
final hoverMode = useState(true);
|
final hoverMode = useState(true);
|
||||||
@ -146,9 +146,9 @@ class MiniLyricsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
if (playlistQueue != null)
|
if (playlistQueue.activeTrack != null)
|
||||||
Text(
|
Text(
|
||||||
playlistQueue.activeTrack.name!,
|
playlistQueue.activeTrack!.name!,
|
||||||
style: theme.textTheme.titleMedium,
|
style: theme.textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -178,7 +178,7 @@ class MiniLyricsPage extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(SpotubeIcons.queue),
|
icon: const Icon(SpotubeIcons.queue),
|
||||||
tooltip: context.l10n.queue,
|
tooltip: context.l10n.queue,
|
||||||
onPressed: playlistQueue != null
|
onPressed: playlistQueue.activeTrack != null
|
||||||
? () {
|
? () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -7,7 +7,7 @@ import 'package:spotify/spotify.dart';
|
|||||||
import 'package:spotube/components/lyrics/zoom_controls.dart';
|
import 'package:spotube/components/lyrics/zoom_controls.dart';
|
||||||
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart';
|
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
@ -25,7 +25,7 @@ class PlainLyrics extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final lyricsQuery = useQueries.lyrics.spotifySynced(
|
final lyricsQuery = useQueries.lyrics.spotifySynced(
|
||||||
ref,
|
ref,
|
||||||
playlist?.activeTrack,
|
playlist?.activeTrack,
|
||||||
@ -43,7 +43,7 @@ class PlainLyrics extends HookConsumerWidget {
|
|||||||
if (isModal != true) ...[
|
if (isModal != true) ...[
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
playlist?.activeTrack.name ?? "",
|
playlist.activeTrack?.name ?? "",
|
||||||
style: breakpoint >= Breakpoints.md
|
style: breakpoint >= Breakpoints.md
|
||||||
? textTheme.displaySmall
|
? textTheme.displaySmall
|
||||||
: textTheme.headlineMedium?.copyWith(
|
: textTheme.headlineMedium?.copyWith(
|
||||||
@ -55,7 +55,7 @@ class PlainLyrics extends HookConsumerWidget {
|
|||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
TypeConversionUtils.artists_X_String<Artist>(
|
TypeConversionUtils.artists_X_String<Artist>(
|
||||||
playlist?.activeTrack.artists ?? []),
|
playlist.activeTrack?.artists ?? []),
|
||||||
style: (breakpoint >= Breakpoints.md
|
style: (breakpoint >= Breakpoints.md
|
||||||
? textTheme.headlineSmall
|
? textTheme.headlineSmall
|
||||||
: textTheme.titleLarge)
|
: textTheme.titleLarge)
|
||||||
@ -74,7 +74,7 @@ class PlainLyrics extends HookConsumerWidget {
|
|||||||
return const ShimmerLyrics();
|
return const ShimmerLyrics();
|
||||||
} else if (lyricsQuery.hasError) {
|
} else if (lyricsQuery.hasError) {
|
||||||
return Text(
|
return Text(
|
||||||
"Sorry, no Lyrics were found for `${playlist?.activeTrack.name}` :'(\n${lyricsQuery.error.toString()}",
|
"Sorry, no Lyrics were found for `${playlist.activeTrack?.name}` :'(\n${lyricsQuery.error.toString()}",
|
||||||
style: textTheme.bodyLarge?.copyWith(
|
style: textTheme.bodyLarge?.copyWith(
|
||||||
color: palette.bodyTextColor,
|
color: palette.bodyTextColor,
|
||||||
),
|
),
|
||||||
|
@ -10,7 +10,7 @@ import 'package:spotube/hooks/use_auto_scroll_controller.dart';
|
|||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/hooks/use_synced_lyrics.dart';
|
import 'package:spotube/hooks/use_synced_lyrics.dart';
|
||||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
@ -31,7 +31,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
final controller = useAutoScrollController();
|
final controller = useAutoScrollController();
|
||||||
@ -77,7 +77,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
if (isModal != true)
|
if (isModal != true)
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
playlist?.activeTrack.name ?? "Not Playing",
|
playlist.activeTrack?.name ?? "Not Playing",
|
||||||
style: headlineTextStyle,
|
style: headlineTextStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -85,7 +85,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
TypeConversionUtils.artists_X_String<Artist>(
|
TypeConversionUtils.artists_X_String<Artist>(
|
||||||
playlist?.activeTrack.artists ?? []),
|
playlist.activeTrack?.artists ?? []),
|
||||||
style: breakpoint >= Breakpoints.md
|
style: breakpoint >= Breakpoints.md
|
||||||
? textTheme.headlineSmall
|
? textTheme.headlineSmall
|
||||||
: textTheme.titleLarge,
|
: textTheme.titleLarge,
|
||||||
|
@ -18,7 +18,7 @@ import 'package:spotube/hooks/use_palette_color.dart';
|
|||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/pages/lyrics/lyrics.dart';
|
import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class PlayerView extends HookConsumerWidget {
|
class PlayerView extends HookConsumerWidget {
|
||||||
@ -30,10 +30,10 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
final currentTrack = ref.watch(PlaylistQueueNotifier.provider.select(
|
final currentTrack = ref.watch(ProxyPlaylistNotifier.provider.select(
|
||||||
(value) => value?.activeTrack,
|
(value) => value?.activeTrack,
|
||||||
));
|
));
|
||||||
final isLocalTrack = ref.watch(PlaylistQueueNotifier.provider.select(
|
final isLocalTrack = ref.watch(ProxyPlaylistNotifier.provider.select(
|
||||||
(value) => value?.activeTrack is LocalTrack,
|
(value) => value?.activeTrack is LocalTrack,
|
||||||
));
|
));
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
@ -8,7 +8,7 @@ import 'package:spotube/hooks/use_breakpoints.dart';
|
|||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.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/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -20,31 +20,33 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
PlaylistView(this.playlist, {Key? key}) : super(key: key);
|
PlaylistView(this.playlist, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
Future<void> playPlaylist(
|
Future<void> playPlaylist(
|
||||||
PlaylistQueueNotifier playlistNotifier,
|
|
||||||
List<Track> tracks,
|
List<Track> tracks,
|
||||||
WidgetRef ref, {
|
WidgetRef ref, {
|
||||||
Track? currentTrack,
|
Track? currentTrack,
|
||||||
}) async {
|
}) async {
|
||||||
|
final proxyPlaylist = ref.read(ProxyPlaylistNotifier.provider);
|
||||||
|
final playback = ref.read(ProxyPlaylistNotifier.notifier);
|
||||||
final sortBy = ref.read(trackCollectionSortState(playlist.id!));
|
final sortBy = ref.read(trackCollectionSortState(playlist.id!));
|
||||||
final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy);
|
final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy);
|
||||||
currentTrack ??= sortedTracks.first;
|
currentTrack ??= sortedTracks.first;
|
||||||
final isPlaylistPlaying = playlistNotifier.isPlayingPlaylist(tracks);
|
final isPlaylistPlaying = proxyPlaylist.containsTracks(tracks);
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
await playlistNotifier.loadAndPlay(
|
await playback.load(
|
||||||
sortedTracks,
|
sortedTracks,
|
||||||
active: sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
initialIndex: sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
|
autoPlay: true,
|
||||||
);
|
);
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
currentTrack.id != null &&
|
currentTrack.id != null &&
|
||||||
currentTrack.id != playlistNotifier.state?.activeTrack.id) {
|
currentTrack.id != proxyPlaylist.activeTrack?.id) {
|
||||||
await playlistNotifier.playTrack(currentTrack);
|
await playback.jumpToTrack(currentTrack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
ref.watch(PlaylistQueueNotifier.provider);
|
final proxyPlaylist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playlistNotifier = ref.watch(PlaylistQueueNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
final tracksSnapshot = useQueries.playlist.tracksOfQuery(ref, playlist.id!);
|
final tracksSnapshot = useQueries.playlist.tracksOfQuery(ref, playlist.id!);
|
||||||
|
|
||||||
final isPlaylistPlaying = useMemoized(
|
final isPlaylistPlaying = useMemoized(
|
||||||
() => playlistNotifier.isPlayingPlaylist(tracksSnapshot.data ?? []),
|
() => proxyPlaylist.containsTracks(tracksSnapshot.data ?? []),
|
||||||
[playlistNotifier, tracksSnapshot.data],
|
[playlistNotifier, tracksSnapshot.data],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -76,26 +78,25 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
if (tracksSnapshot.hasData) {
|
if (tracksSnapshot.hasData) {
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playlistNotifier,
|
|
||||||
tracksSnapshot.data!,
|
tracksSnapshot.data!,
|
||||||
ref,
|
ref,
|
||||||
currentTrack: track,
|
currentTrack: track,
|
||||||
);
|
);
|
||||||
} else if (isPlaylistPlaying && track != null) {
|
} else if (isPlaylistPlaying && track != null) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playlistNotifier,
|
|
||||||
tracksSnapshot.data!,
|
tracksSnapshot.data!,
|
||||||
ref,
|
ref,
|
||||||
currentTrack: track,
|
currentTrack: track,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
playlistNotifier.remove(tracksSnapshot.data!);
|
playlistNotifier
|
||||||
|
.removeTracks(tracksSnapshot.data!.map((e) => e.id!));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onAddToQueue: () {
|
onAddToQueue: () {
|
||||||
if (tracksSnapshot.hasData && !isPlaylistPlaying) {
|
if (tracksSnapshot.hasData && !isPlaylistPlaying) {
|
||||||
playlistNotifier.add(tracksSnapshot.data!);
|
playlistNotifier.addTracks(tracksSnapshot.data!);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bottomSpace: breakpoint.isLessThanOrEqualTo(Breakpoints.md),
|
bottomSpace: breakpoint.isLessThanOrEqualTo(Breakpoints.md),
|
||||||
@ -125,20 +126,19 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
if (tracksSnapshot.hasData) {
|
if (tracksSnapshot.hasData) {
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playlistNotifier,
|
|
||||||
tracks,
|
tracks,
|
||||||
ref,
|
ref,
|
||||||
currentTrack: track,
|
currentTrack: track,
|
||||||
);
|
);
|
||||||
} else if (isPlaylistPlaying && track != null) {
|
} else if (isPlaylistPlaying && track != null) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playlistNotifier,
|
|
||||||
tracks,
|
tracks,
|
||||||
ref,
|
ref,
|
||||||
currentTrack: track,
|
currentTrack: track,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
playlistNotifier.stop();
|
// TODO: Remove the ability to stop the playlist
|
||||||
|
// playlistNotifier.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -19,7 +19,7 @@ import 'package:spotube/components/playlist/playlist_card.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
@ -96,9 +96,9 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
HookBuilder(
|
HookBuilder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final playlist =
|
final playlist =
|
||||||
ref.watch(PlaylistQueueNotifier.provider);
|
ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playlistNotifier =
|
final playlistNotifier =
|
||||||
ref.watch(PlaylistQueueNotifier.notifier);
|
ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
List<AlbumSimple> albums = [];
|
List<AlbumSimple> albums = [];
|
||||||
List<Artist> artists = [];
|
List<Artist> artists = [];
|
||||||
List<Track> tracks = [];
|
List<Track> tracks = [];
|
||||||
@ -154,18 +154,17 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
playlist,
|
playlist,
|
||||||
track: track,
|
track: track,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
isActive: playlist?.activeTrack.id ==
|
isActive: playlist.activeTrack?.id ==
|
||||||
track.value.id,
|
track.value.id,
|
||||||
onTrackPlayButtonPressed:
|
onTrackPlayButtonPressed:
|
||||||
(currentTrack) async {
|
(currentTrack) async {
|
||||||
final isTrackPlaying =
|
final isTrackPlaying =
|
||||||
playlist?.activeTrack.id ==
|
playlist.activeTrack?.id ==
|
||||||
currentTrack.id;
|
currentTrack.id;
|
||||||
if (!isTrackPlaying &&
|
if (!isTrackPlaying &&
|
||||||
context.mounted) {
|
context.mounted) {
|
||||||
final shouldPlay =
|
final shouldPlay =
|
||||||
(playlist?.tracks.length ?? 0) >
|
(playlist.tracks.length) > 20
|
||||||
20
|
|
||||||
? await showPromptDialog(
|
? await showPromptDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: context.l10n
|
title: context.l10n
|
||||||
@ -174,16 +173,17 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
message: context.l10n
|
message: context.l10n
|
||||||
.queue_clear_alert(
|
.queue_clear_alert(
|
||||||
playlist?.tracks
|
playlist
|
||||||
.length ??
|
.tracks.length,
|
||||||
0,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
if (shouldPlay) {
|
if (shouldPlay) {
|
||||||
await playlistNotifier
|
await playlistNotifier.load(
|
||||||
.loadAndPlay([currentTrack]);
|
[currentTrack],
|
||||||
|
autoPlay: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -55,6 +55,10 @@ class BlackListNotifier
|
|||||||
state = state.difference({element});
|
state = state.difference({element});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool contains(TrackSimple track) {
|
||||||
|
return filter([track]).isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
Iterable<TrackSimple> filter(Iterable<TrackSimple> tracks) {
|
Iterable<TrackSimple> filter(Iterable<TrackSimple> tracks) {
|
||||||
return tracks.where(
|
return tracks.where(
|
||||||
(track) {
|
(track) {
|
||||||
|
@ -1,555 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
|
||||||
import 'package:spotube/models/local_track.dart';
|
|
||||||
import 'package:spotube/models/spotube_track.dart';
|
|
||||||
import 'package:spotube/extensions/track.dart';
|
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
|
||||||
import 'package:spotube/provider/palette_provider.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
|
||||||
import 'package:spotube/services/audio_services/audio_services.dart';
|
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Playlist;
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
class PlaylistQueue {
|
|
||||||
final Set<Track> tracks;
|
|
||||||
final int active;
|
|
||||||
|
|
||||||
Track get activeTrack => tracks.elementAt(active);
|
|
||||||
|
|
||||||
final bool shuffled;
|
|
||||||
final PlaybackLoopMode loopMode;
|
|
||||||
|
|
||||||
static Future<PlaylistQueue> fromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
UserPreferences preferences,
|
|
||||||
) async {
|
|
||||||
final List? tracks = json['tracks'];
|
|
||||||
return PlaylistQueue(
|
|
||||||
Set.from(
|
|
||||||
await Future.wait(
|
|
||||||
tracks?.mapIndexed(
|
|
||||||
(i, e) async {
|
|
||||||
final jsonTrack =
|
|
||||||
Map.castFrom<dynamic, dynamic, String, dynamic>(e);
|
|
||||||
|
|
||||||
if (e["path"] != null) {
|
|
||||||
return LocalTrack.fromJson(jsonTrack);
|
|
||||||
} else if (i == json["active"] && !json.containsKey("path")) {
|
|
||||||
return await SpotubeTrack.fetchFromTrack(
|
|
||||||
Track.fromJson(jsonTrack),
|
|
||||||
preferences,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Track.fromJson(jsonTrack);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) ??
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
active: json['active'],
|
|
||||||
shuffled: json['shuffled'],
|
|
||||||
loopMode: PlaybackLoopMode.fromString(json['loopMode'] ?? ''),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'tracks': tracks.map(
|
|
||||||
(e) {
|
|
||||||
if (e is SpotubeTrack) {
|
|
||||||
return e.toJson();
|
|
||||||
} else if (e is LocalTrack) {
|
|
||||||
return e.toJson();
|
|
||||||
} else {
|
|
||||||
return e.toJson();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
'active': active,
|
|
||||||
'shuffled': shuffled,
|
|
||||||
'loopMode': loopMode.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isLoading =>
|
|
||||||
activeTrack is LocalTrack ? false : activeTrack is! SpotubeTrack;
|
|
||||||
|
|
||||||
PlaylistQueue(
|
|
||||||
this.tracks, {
|
|
||||||
this.active = 0,
|
|
||||||
this.shuffled = false,
|
|
||||||
this.loopMode = PlaybackLoopMode.none,
|
|
||||||
}) : assert(active < tracks.length && active >= 0, "Invalid active index");
|
|
||||||
|
|
||||||
PlaylistQueue copyWith({
|
|
||||||
Set<Track>? tracks,
|
|
||||||
int? active,
|
|
||||||
bool? shuffled,
|
|
||||||
PlaybackLoopMode? loopMode,
|
|
||||||
}) {
|
|
||||||
return PlaylistQueue(
|
|
||||||
tracks ?? this.tracks,
|
|
||||||
active: active ?? this.active,
|
|
||||||
shuffled: shuffled ?? this.shuffled,
|
|
||||||
loopMode: loopMode ?? this.loopMode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PlaylistQueueNotifier extends PersistedStateNotifier<PlaylistQueue?> {
|
|
||||||
final Ref ref;
|
|
||||||
|
|
||||||
late AudioServices audioServices;
|
|
||||||
|
|
||||||
static final provider =
|
|
||||||
StateNotifierProvider<PlaylistQueueNotifier, PlaylistQueue?>(
|
|
||||||
(ref) => PlaylistQueueNotifier._(ref),
|
|
||||||
);
|
|
||||||
|
|
||||||
static final notifier = provider.notifier;
|
|
||||||
|
|
||||||
PlaylistQueueNotifier._(this.ref) : super(null, "playlist") {
|
|
||||||
configure();
|
|
||||||
}
|
|
||||||
|
|
||||||
void configure() async {
|
|
||||||
audioServices = await AudioServices.create(ref, this);
|
|
||||||
|
|
||||||
audioPlayer.currentIndexChangedStream.listen((index) async {
|
|
||||||
if (!isLoaded) return;
|
|
||||||
state = state!.copyWith(active: index);
|
|
||||||
await audioServices.addTrack(state!.activeTrack);
|
|
||||||
});
|
|
||||||
|
|
||||||
audioPlayer.almostCompleteStream.listen((_) async {
|
|
||||||
if (!isLoaded) return;
|
|
||||||
final nextTrack = state!.tracks.elementAtOrNull(state!.active + 1);
|
|
||||||
final sources = audioPlayer.sources;
|
|
||||||
|
|
||||||
// we don't have a next track or the next track is already loaded
|
|
||||||
// only when the next track isn't loaded we load next 3 tracks
|
|
||||||
if (nextTrack == null ||
|
|
||||||
nextTrack is SpotubeTrack && sources.contains(nextTrack.ytUri)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<SpotubeTrack> fetchedTracks = [];
|
|
||||||
|
|
||||||
// load next 3 tracks
|
|
||||||
final tracks = await Future.wait(state!.tracks
|
|
||||||
.toList()
|
|
||||||
.skip(state!.active + 1)
|
|
||||||
.take(3)
|
|
||||||
.mapIndexed((i, track) async {
|
|
||||||
if (track is LocalTrack) return Future.value(track.path);
|
|
||||||
if (track is SpotubeTrack) return Future.value(track.ytUri);
|
|
||||||
if (i == 0) {
|
|
||||||
final fetchedTrack =
|
|
||||||
await SpotubeTrack.fetchFromTrack(track, preferences);
|
|
||||||
fetchedTracks.add(fetchedTrack);
|
|
||||||
return fetchedTrack.ytUri;
|
|
||||||
}
|
|
||||||
// Adding delay to not spoof the YouTube API for IP Block
|
|
||||||
final fetchedTrack = await Future.delayed(
|
|
||||||
const Duration(milliseconds: 100),
|
|
||||||
() => SpotubeTrack.fetchFromTrack(track, preferences),
|
|
||||||
);
|
|
||||||
|
|
||||||
fetchedTracks.add(fetchedTrack);
|
|
||||||
return fetchedTrack.ytUri;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// replacing the tracks with the fetched tracks
|
|
||||||
// in proxy playlist
|
|
||||||
state = state!.copyWith(
|
|
||||||
tracks: state!.tracks.map((track) {
|
|
||||||
final fetchedTrack =
|
|
||||||
fetchedTracks.firstWhereOrNull((e) => e.id == track.id);
|
|
||||||
|
|
||||||
if (fetchedTrack != null) {
|
|
||||||
return fetchedTrack;
|
|
||||||
}
|
|
||||||
return track;
|
|
||||||
}).toSet(),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final track in tracks) {
|
|
||||||
if (sources.contains(track)) continue;
|
|
||||||
await audioPlayer.addTrack(track);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bool isPreSearching = false;
|
|
||||||
|
|
||||||
audioPlayer.positionStream.listen((pos) async {
|
|
||||||
if (!isLoaded) return;
|
|
||||||
final currentDuration = await audioPlayer.duration ?? Duration.zero;
|
|
||||||
|
|
||||||
// skip all the activeTrack.skipSegments
|
|
||||||
if (state?.isLoading != true &&
|
|
||||||
state?.activeTrack is SpotubeTrack &&
|
|
||||||
(state?.activeTrack as SpotubeTrack?)?.skipSegments.isNotEmpty ==
|
|
||||||
true &&
|
|
||||||
preferences.skipSponsorSegments) {
|
|
||||||
for (final segment
|
|
||||||
in (state!.activeTrack as SpotubeTrack).skipSegments) {
|
|
||||||
if ((pos.inSeconds >= segment["start"]! &&
|
|
||||||
pos.inSeconds < segment["end"]!)) {
|
|
||||||
await audioPlayer.pause();
|
|
||||||
await seek(Duration(seconds: segment["end"]!));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// when the track progress is above 80%, track isn't the last
|
|
||||||
// and is not already fetched and nothing is fetching currently
|
|
||||||
if (pos.inSeconds > currentDuration.inSeconds * .8 &&
|
|
||||||
state!.active != state!.tracks.length - 1 &&
|
|
||||||
state!.tracks.elementAt(state!.active + 1) is! SpotubeTrack &&
|
|
||||||
!isPreSearching) {
|
|
||||||
isPreSearching = true;
|
|
||||||
final tracks = state!.tracks.toList();
|
|
||||||
final newTrack = await SpotubeTrack.fetchFromTrack(
|
|
||||||
state!.tracks.elementAt(state!.active + 1),
|
|
||||||
preferences,
|
|
||||||
);
|
|
||||||
tracks[state!.active + 1] = newTrack;
|
|
||||||
await audioPlayer.preload(newTrack.ytUri);
|
|
||||||
state = state!.copyWith(tracks: Set.from(tracks));
|
|
||||||
isPreSearching = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// properties
|
|
||||||
|
|
||||||
// getters
|
|
||||||
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
|
||||||
BlackListNotifier get blacklist =>
|
|
||||||
ref.read(BlackListNotifier.provider.notifier);
|
|
||||||
|
|
||||||
bool get isLoaded => state != null;
|
|
||||||
|
|
||||||
List<Video> get siblings => state?.isLoading == false
|
|
||||||
? (state!.activeTrack as SpotubeTrack).siblings
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// modifiers
|
|
||||||
void add(List<Track> tracks) {
|
|
||||||
if (!isLoaded) {
|
|
||||||
loadAndPlay(tracks);
|
|
||||||
} else {
|
|
||||||
state = state?.copyWith(
|
|
||||||
tracks: state!.tracks..addAll(tracks),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void playNext(List<Track> tracks) {
|
|
||||||
if (!isLoaded) {
|
|
||||||
loadAndPlay(tracks);
|
|
||||||
} else {
|
|
||||||
final stateTracks = state!.tracks.toList();
|
|
||||||
|
|
||||||
stateTracks.insertAll(state!.active + 1, tracks);
|
|
||||||
|
|
||||||
state = state?.copyWith(tracks: Set.from(stateTracks));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Removal of track support
|
|
||||||
void remove(List<Track> tracks) {
|
|
||||||
if (!isLoaded) return;
|
|
||||||
final trackIds = tracks.map((e) => e.id!).toSet();
|
|
||||||
final newTracks = state!.tracks.whereNot(
|
|
||||||
(element) => trackIds.contains(element.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newTracks.isEmpty) {
|
|
||||||
stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state = state?.copyWith(
|
|
||||||
tracks: newTracks.toSet(),
|
|
||||||
active: !newTracks.contains(state!.activeTrack)
|
|
||||||
? state!.active > newTracks.length - 1
|
|
||||||
? newTracks.length - 1
|
|
||||||
: state!.active
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
// if (state!.isLoading) {
|
|
||||||
// play();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Swap sibling support
|
|
||||||
Future<void> swapSibling(Video video) async {
|
|
||||||
if (!isLoaded || state!.isLoading) return;
|
|
||||||
await pause();
|
|
||||||
final tracks = state!.tracks.toList();
|
|
||||||
final track = await (state!.activeTrack as SpotubeTrack)
|
|
||||||
.swappedCopy(video, preferences);
|
|
||||||
if (track == null) return;
|
|
||||||
tracks[state!.active] = track;
|
|
||||||
|
|
||||||
state = state!.copyWith(tracks: Set.from(tracks));
|
|
||||||
// await play();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> populateSibling() async {
|
|
||||||
if (!isLoaded || state!.isLoading) return;
|
|
||||||
final tracks = state!.tracks.toList();
|
|
||||||
final track = await (state!.activeTrack as SpotubeTrack).populatedCopy();
|
|
||||||
tracks[state!.active] = track;
|
|
||||||
state = state!.copyWith(tracks: Set.from(tracks));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Future<void> play() async {
|
|
||||||
// if (!isLoaded) return;
|
|
||||||
// await pause();
|
|
||||||
// await audioServices.addTrack(state!.activeTrack);
|
|
||||||
// if (state!.activeTrack is LocalTrack) {
|
|
||||||
// await audioPlayer.play((state!.activeTrack as LocalTrack).path);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (state!.activeTrack is! SpotubeTrack) {
|
|
||||||
// final tracks = state!.tracks.toList();
|
|
||||||
// tracks[state!.active] = await SpotubeTrack.fetchFromTrack(
|
|
||||||
// state!.activeTrack,
|
|
||||||
// preferences,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// state = state!.copyWith(tracks: Set.from(tracks));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// await audioServices.addTrack(state!.activeTrack);
|
|
||||||
|
|
||||||
// final cached =
|
|
||||||
// await DefaultCacheManager().getFileFromCache(state!.activeTrack.id!);
|
|
||||||
// if (preferences.predownload && cached != null) {
|
|
||||||
// await audioPlayer.play(cached.file.path);
|
|
||||||
// } else {
|
|
||||||
// await audioPlayer.play((state!.activeTrack as SpotubeTrack).ytUri);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: Implement Playtrack
|
|
||||||
Future<void> playTrack(Track track) async {
|
|
||||||
if (!isLoaded) return;
|
|
||||||
final active =
|
|
||||||
state!.tracks.toList().indexWhere((element) => element.id == track.id);
|
|
||||||
if (active == -1) return;
|
|
||||||
state = state!.copyWith(active: active);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> load(Iterable<Track> tracks, {int active = 0}) async {
|
|
||||||
final activeTrack = tracks.elementAt(active);
|
|
||||||
final filtered = Set.from(blacklist.filter(tracks));
|
|
||||||
state = PlaylistQueue(
|
|
||||||
Set.from(blacklist.filter(tracks)),
|
|
||||||
active: filtered
|
|
||||||
.toList()
|
|
||||||
.indexWhere((element) => element.id == activeTrack.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
// load 3 items first to avoid huge initial loading time
|
|
||||||
final firstTracks = await Future.wait(
|
|
||||||
filtered
|
|
||||||
.skip(active == 0 ? 0 : active - 1)
|
|
||||||
.take(3)
|
|
||||||
.mapIndexed((i, track) {
|
|
||||||
if (track is LocalTrack) return Future.value(track.path);
|
|
||||||
if (i == 0) {
|
|
||||||
return SpotubeTrack.fetchFromTrack(track, preferences).then(
|
|
||||||
(s) => s.ytUri,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Adding delay to not spoof the YouTube API for IP Block
|
|
||||||
return Future.delayed(
|
|
||||||
const Duration(milliseconds: 100),
|
|
||||||
() => SpotubeTrack.fetchFromTrack(track, preferences).then(
|
|
||||||
(s) => s.ytUri,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
final localTracks = tracks
|
|
||||||
.where(
|
|
||||||
(element) =>
|
|
||||||
element is LocalTrack && !firstTracks.contains(element.path),
|
|
||||||
)
|
|
||||||
.map((e) => (e as LocalTrack).path);
|
|
||||||
|
|
||||||
await audioPlayer.openPlaylist(
|
|
||||||
[...firstTracks, ...localTracks],
|
|
||||||
autoPlay: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadAndPlay(Iterable<Track> tracks, {int active = 0}) async {
|
|
||||||
await load(tracks, active: active);
|
|
||||||
await resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pause() {
|
|
||||||
return audioPlayer.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> resume() {
|
|
||||||
return audioPlayer.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stop() async {
|
|
||||||
audioServices.deactivateSession();
|
|
||||||
state = null;
|
|
||||||
|
|
||||||
return audioPlayer.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> next() async {
|
|
||||||
return audioPlayer.skipToNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> previous() async {
|
|
||||||
return audioPlayer.skipToPrevious();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> seek(Duration position) async {
|
|
||||||
if (!isLoaded) return;
|
|
||||||
await audioPlayer.seek(position);
|
|
||||||
await resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setShuffle(bool shuffle) async {
|
|
||||||
if (!isLoaded) return;
|
|
||||||
audioPlayer.setShuffle(shuffle);
|
|
||||||
state = state!.copyWith(shuffled: await audioPlayer.isShuffled());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setLoopMode(PlaybackLoopMode loopMode) async {
|
|
||||||
if (!isLoaded) return;
|
|
||||||
audioPlayer.setLoopMode(loopMode);
|
|
||||||
state = state!.copyWith(loopMode: loopMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// utility
|
|
||||||
bool isPlayingPlaylist(Iterable<TrackSimple> playlist) {
|
|
||||||
if (!isLoaded || playlist.isEmpty) return false;
|
|
||||||
|
|
||||||
final trackIds = state!.tracks.map((track) => track.id!);
|
|
||||||
return blacklist
|
|
||||||
.filter(playlist)
|
|
||||||
.every((track) => trackIds.contains(track.id!));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isTrackOnQueue(TrackSimple track) {
|
|
||||||
if (!isLoaded) return false;
|
|
||||||
final trackIds = state!.tracks.map((track) => track.id!);
|
|
||||||
return trackIds.contains(track.id!);
|
|
||||||
}
|
|
||||||
|
|
||||||
void reorder(int oldIndex, int newIndex) {
|
|
||||||
if (!isLoaded) return;
|
|
||||||
|
|
||||||
final tracks = state!.tracks.toList();
|
|
||||||
final track = tracks.removeAt(oldIndex);
|
|
||||||
tracks.insert(newIndex, track);
|
|
||||||
final active =
|
|
||||||
tracks.indexWhere((element) => element.id == state!.activeTrack.id);
|
|
||||||
state = state!.copyWith(tracks: Set.from(tracks), active: active);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updatePalette() {
|
|
||||||
return Future.microtask(() async {
|
|
||||||
final palette = await PaletteGenerator.fromImageProvider(
|
|
||||||
UniversalImage.imageProvider(
|
|
||||||
TypeConversionUtils.image_X_UrlString(
|
|
||||||
state?.activeTrack.album?.images,
|
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
|
||||||
),
|
|
||||||
height: 50,
|
|
||||||
width: 50,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
ref.read(paletteProvider.notifier).state = palette;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
set state(state) {
|
|
||||||
if (preferences.albumColorSync &&
|
|
||||||
state != null &&
|
|
||||||
state.active != this.state?.active) {
|
|
||||||
updatePalette();
|
|
||||||
} else if (state == null && ref.read(paletteProvider) != null) {
|
|
||||||
ref.read(paletteProvider.notifier).state = null;
|
|
||||||
}
|
|
||||||
super.state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<PlaylistQueue>? fromJson(Map<String, dynamic> json) {
|
|
||||||
if (json.isEmpty) return null;
|
|
||||||
return PlaylistQueue.fromJson(json, preferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return state?.toJson() ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
audioServices.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class VolumeProvider extends PersistedStateNotifier<double> {
|
|
||||||
VolumeProvider() : super(1, 'volume');
|
|
||||||
|
|
||||||
static final provider = StateNotifierProvider<VolumeProvider, double>((ref) {
|
|
||||||
return VolumeProvider();
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<void> setVolume(double volume) async {
|
|
||||||
if (volume > 1) {
|
|
||||||
state = 1;
|
|
||||||
} else if (volume < 0) {
|
|
||||||
state = 0;
|
|
||||||
} else {
|
|
||||||
state = volume;
|
|
||||||
}
|
|
||||||
await audioPlayer.setVolume(state);
|
|
||||||
await audioPlayer.setVolume(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
void increaseVolume() {
|
|
||||||
setVolume(state + 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void decreaseVolume() {
|
|
||||||
setVolume(state - 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double fromJson(Map<String, dynamic> json) {
|
|
||||||
return json['volume'] as double;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {'volume': state};
|
|
||||||
}
|
|
||||||
}
|
|
79
lib/provider/proxy_playlist/next_fetcher_mixin.dart
Normal file
79
lib/provider/proxy_playlist/next_fetcher_mixin.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/models/spotube_track.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
|
|
||||||
|
mixin NextFetcher on StateNotifier<ProxyPlaylist> {
|
||||||
|
Future<List<SpotubeTrack>> fetchTracks(
|
||||||
|
UserPreferences preferences, {
|
||||||
|
int count = 3,
|
||||||
|
int offset = 0,
|
||||||
|
}) async {
|
||||||
|
/// get [count] [state.tracks] that are not [SpotubeTrack] and [LocalTrack]
|
||||||
|
|
||||||
|
final bareTracks = state.tracks
|
||||||
|
.skip(offset)
|
||||||
|
.where((element) => element is! SpotubeTrack && element is! LocalTrack)
|
||||||
|
.take(count);
|
||||||
|
|
||||||
|
/// fetch [bareTracks] one by one with 100ms delay
|
||||||
|
final fetchedTracks = await Future.wait(
|
||||||
|
bareTracks.mapIndexed((i, track) async {
|
||||||
|
final future = SpotubeTrack.fetchFromTrack(track, preferences);
|
||||||
|
if (i == 0) {
|
||||||
|
return await future;
|
||||||
|
}
|
||||||
|
return await Future.delayed(
|
||||||
|
const Duration(milliseconds: 100),
|
||||||
|
() => future,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return fetchedTracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merges List of [SpotubeTrack]s with [Track]s and outputs a mixed List
|
||||||
|
Set<Track> mergeTracks(
|
||||||
|
Iterable<SpotubeTrack> fetchTracks,
|
||||||
|
Iterable<Track> tracks,
|
||||||
|
) {
|
||||||
|
return tracks.map((track) {
|
||||||
|
final fetchedTrack = fetchTracks.firstWhereOrNull(
|
||||||
|
(fetchTrack) => fetchTrack.id == track.id,
|
||||||
|
);
|
||||||
|
if (fetchedTrack != null) {
|
||||||
|
return fetchedTrack;
|
||||||
|
}
|
||||||
|
return track;
|
||||||
|
}).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if [Track] is playable
|
||||||
|
bool isUnPlayable(String source) {
|
||||||
|
return source.startsWith('https://youtube.com/unplayable.m4a?id=');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns [Track.id] from [isUnPlayable] source that is not playable
|
||||||
|
String getIdFromUnPlayable(String source) {
|
||||||
|
return source.replaceFirst('https://youtube.com/unplayable.m4a?id=', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns appropriate Media source for [Track]
|
||||||
|
///
|
||||||
|
/// * If [Track] is [SpotubeTrack] then return [SpotubeTrack.ytUri]
|
||||||
|
/// * If [Track] is [LocalTrack] then return [LocalTrack.path]
|
||||||
|
/// * If [Track] is [Track] then return [Track.id] with [isUnPlayable] source
|
||||||
|
String makeAppropriateSource(Track track) {
|
||||||
|
if (track is SpotubeTrack) {
|
||||||
|
return track.ytUri;
|
||||||
|
} else if (track is LocalTrack) {
|
||||||
|
return track.path;
|
||||||
|
} else {
|
||||||
|
return "https://youtube.com/unplayable.m4a?id=${track.id}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
74
lib/provider/proxy_playlist/proxy_playlist.dart
Normal file
74
lib/provider/proxy_playlist/proxy_playlist.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/extensions/track.dart';
|
||||||
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/models/spotube_track.dart';
|
||||||
|
|
||||||
|
class ProxyPlaylist {
|
||||||
|
final Set<Track> tracks;
|
||||||
|
final int? active;
|
||||||
|
|
||||||
|
ProxyPlaylist(this.tracks, [this.active]);
|
||||||
|
factory ProxyPlaylist.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ProxyPlaylist(
|
||||||
|
(json['tracks'] as List<Map<String, dynamic>>)
|
||||||
|
.map(_makeAppropriateTrack)
|
||||||
|
.toSet(),
|
||||||
|
json['active'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Track? get activeTrack =>
|
||||||
|
active == null ? null : tracks.elementAtOrNull(active!);
|
||||||
|
|
||||||
|
bool get isFetching =>
|
||||||
|
activeTrack != null &&
|
||||||
|
activeTrack is! SpotubeTrack &&
|
||||||
|
activeTrack is! LocalTrack;
|
||||||
|
|
||||||
|
bool containsTrack(TrackSimple track) {
|
||||||
|
return tracks.firstWhereOrNull((element) => element.id == track.id) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool containsTracks(Iterable<TrackSimple> tracks) {
|
||||||
|
if (tracks.isEmpty) return false;
|
||||||
|
return tracks.every(containsTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Track _makeAppropriateTrack(Map<String, dynamic> track) {
|
||||||
|
if (track.containsKey("ytUri")) {
|
||||||
|
return SpotubeTrack.fromJson(track);
|
||||||
|
} else if (track.containsKey("path")) {
|
||||||
|
return LocalTrack.fromJson(track);
|
||||||
|
} else {
|
||||||
|
return Track.fromJson(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic> _makeAppropriateTrackJson(Track track) {
|
||||||
|
if (track is SpotubeTrack) {
|
||||||
|
return track.toJson();
|
||||||
|
} else if (track is LocalTrack) {
|
||||||
|
return track.toJson();
|
||||||
|
} else {
|
||||||
|
return track.toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'tracks': tracks.map(_makeAppropriateTrackJson).toList(),
|
||||||
|
'active': active,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyPlaylist copyWith({
|
||||||
|
Set<Track>? tracks,
|
||||||
|
int? active,
|
||||||
|
}) {
|
||||||
|
return ProxyPlaylist(
|
||||||
|
tracks ?? this.tracks,
|
||||||
|
active ?? this.active,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
297
lib/provider/proxy_playlist/proxy_playlist_provider.dart
Normal file
297
lib/provider/proxy_playlist/proxy_playlist_provider.dart
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/models/local_track.dart';
|
||||||
|
import 'package:spotube/models/spotube_track.dart';
|
||||||
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
|
import 'package:spotube/provider/palette_provider.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/next_fetcher_mixin.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/services/audio_services/audio_services.dart';
|
||||||
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
|
/// Things to implement:
|
||||||
|
/// * [x] Sponsor-Block skip
|
||||||
|
/// * [x] Prefetch next track as [SpotubeTrack] on 80% of current track
|
||||||
|
/// * [ ] Mixed Queue containing both [SpotubeTrack] and [LocalTrack]
|
||||||
|
/// * [ ] Caching and loading of cache of tracks
|
||||||
|
///
|
||||||
|
/// Don'ts:
|
||||||
|
/// * It'll not have any proxy method for [SpotubeAudioPlayer]
|
||||||
|
/// * It'll not store any sort of player state e.g playing, paused, shuffled etc
|
||||||
|
/// * For that, use [SpotubeAudioPlayer]
|
||||||
|
|
||||||
|
class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
|
||||||
|
with NextFetcher {
|
||||||
|
final Ref ref;
|
||||||
|
late final AudioServices notificationService;
|
||||||
|
|
||||||
|
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
||||||
|
BlackListNotifier get blacklist =>
|
||||||
|
ref.read(BlackListNotifier.provider.notifier);
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
StateNotifierProvider<ProxyPlaylistNotifier, ProxyPlaylist>(
|
||||||
|
(ref) => ProxyPlaylistNotifier(ref),
|
||||||
|
);
|
||||||
|
|
||||||
|
static AlwaysAliveRefreshable<ProxyPlaylistNotifier> get notifier =>
|
||||||
|
provider.notifier;
|
||||||
|
|
||||||
|
ProxyPlaylistNotifier(this.ref) : super(ProxyPlaylist({})) {
|
||||||
|
() async {
|
||||||
|
notificationService = await AudioServices.create(ref, this);
|
||||||
|
|
||||||
|
audioPlayer.currentIndexChangedStream.listen((index) async {
|
||||||
|
if (index == -1) return;
|
||||||
|
final track = state.tracks.elementAtOrNull(index);
|
||||||
|
if (track == null) return;
|
||||||
|
notificationService.addTrack(track);
|
||||||
|
state = state.copyWith(active: index);
|
||||||
|
|
||||||
|
if (preferences.albumColorSync) {
|
||||||
|
updatePalette();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bool isPreSearching = false;
|
||||||
|
audioPlayer.percentCompletedStream(80).listen((_) async {
|
||||||
|
if (isPreSearching) return;
|
||||||
|
try {
|
||||||
|
isPreSearching = true;
|
||||||
|
|
||||||
|
// TODO: Make repeat mode sensitive changes later
|
||||||
|
final track =
|
||||||
|
await ensureNthSourcePlayable(audioPlayer.currentIndex + 1);
|
||||||
|
if (track != null) {
|
||||||
|
state = state.copyWith(tracks: mergeTracks([track], state.tracks));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sometimes fetching can take a lot of time, so we need to check
|
||||||
|
/// if next source is playable or not at 99% progress. If not, then
|
||||||
|
/// it'll be paused automatically
|
||||||
|
///
|
||||||
|
/// After fetching the nextSource and replacing it, we need to check
|
||||||
|
/// if the player is paused or not. If it is paused, then we need to
|
||||||
|
/// resume it to skip to next track
|
||||||
|
if (audioPlayer.isPaused) {
|
||||||
|
await audioPlayer.resume();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isPreSearching = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// player stops at 99% if nextSource is still not playable
|
||||||
|
audioPlayer.percentCompletedStream(99).listen((_) async {
|
||||||
|
final nextSource =
|
||||||
|
audioPlayer.sources.elementAtOrNull(audioPlayer.currentIndex + 1);
|
||||||
|
if (nextSource == null || !isUnPlayable(nextSource)) return;
|
||||||
|
await audioPlayer.pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioPlayer.positionStream.listen((pos) async {
|
||||||
|
if (audioPlayer.currentIndex == -1) return;
|
||||||
|
final activeSource =
|
||||||
|
audioPlayer.sources.elementAtOrNull(audioPlayer.currentIndex);
|
||||||
|
if (activeSource == null) return;
|
||||||
|
final activeTrack = state.tracks.firstWhereOrNull(
|
||||||
|
(element) => element is SpotubeTrack && element.ytUri == activeSource,
|
||||||
|
) as SpotubeTrack?;
|
||||||
|
if (activeTrack == null) return;
|
||||||
|
// skip all the activeTrack.skipSegments
|
||||||
|
if (activeTrack.skipSegments.isNotEmpty == true &&
|
||||||
|
preferences.skipSponsorSegments) {
|
||||||
|
for (final segment in activeTrack.skipSegments) {
|
||||||
|
if (pos.inSeconds < segment["start"]! ||
|
||||||
|
pos.inSeconds > segment["end"]!) continue;
|
||||||
|
await audioPlayer.pause();
|
||||||
|
await audioPlayer.seek(Duration(seconds: segment["end"]!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SpotubeTrack?> ensureNthSourcePlayable(int n) async {
|
||||||
|
final sources = audioPlayer.sources;
|
||||||
|
if (n < 0 || n > sources.length - 1) return null;
|
||||||
|
final nthSource = sources.elementAtOrNull(n);
|
||||||
|
if (nthSource == null || !isUnPlayable(nthSource)) return null;
|
||||||
|
|
||||||
|
final nthTrack = state.tracks.firstWhereOrNull(
|
||||||
|
(element) => element.id == getIdFromUnPlayable(nthSource),
|
||||||
|
);
|
||||||
|
if (nthTrack == null ||
|
||||||
|
nthTrack is SpotubeTrack ||
|
||||||
|
nthTrack is LocalTrack) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nthFetchedTrack =
|
||||||
|
await SpotubeTrack.fetchFromTrack(nthTrack, preferences);
|
||||||
|
|
||||||
|
await audioPlayer.replaceSource(
|
||||||
|
nthSource,
|
||||||
|
nthFetchedTrack.ytUri,
|
||||||
|
);
|
||||||
|
|
||||||
|
return nthFetchedTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic methods for adding or removing tracks to playlist
|
||||||
|
|
||||||
|
Future<void> addTrack(Track track) async {
|
||||||
|
if (blacklist.contains(track)) return;
|
||||||
|
state = state.copyWith(tracks: {...state.tracks, track});
|
||||||
|
await audioPlayer.addTrack(makeAppropriateSource(track));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addTracks(Iterable<Track> tracks) async {
|
||||||
|
tracks = blacklist.filter(tracks).toList() as List<Track>;
|
||||||
|
state = state.copyWith(tracks: {...state.tracks, ...tracks});
|
||||||
|
for (final track in tracks) {
|
||||||
|
await audioPlayer.addTrack(makeAppropriateSource(track));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Safely Remove playing tracks
|
||||||
|
|
||||||
|
void removeTrack(String trackId) {
|
||||||
|
final track =
|
||||||
|
state.tracks.firstWhereOrNull((element) => element.id == trackId);
|
||||||
|
if (track == null) return;
|
||||||
|
state = state.copyWith(tracks: {...state.tracks..remove(track)});
|
||||||
|
final index = audioPlayer.sources.indexOf(makeAppropriateSource(track));
|
||||||
|
if (index == -1) return;
|
||||||
|
audioPlayer.removeTrack(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeTracks(Iterable<String> tracksIds) {
|
||||||
|
final tracks =
|
||||||
|
state.tracks.where((element) => tracksIds.contains(element.id));
|
||||||
|
|
||||||
|
state = state.copyWith(tracks: {
|
||||||
|
...state.tracks..removeWhere((element) => tracksIds.contains(element.id))
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final track in tracks) {
|
||||||
|
final index = audioPlayer.sources.indexOf(makeAppropriateSource(track));
|
||||||
|
if (index == -1) continue;
|
||||||
|
audioPlayer.removeTrack(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> load(
|
||||||
|
List<Track> tracks, {
|
||||||
|
int initialIndex = 0,
|
||||||
|
bool autoPlay = false,
|
||||||
|
}) async {
|
||||||
|
tracks = blacklist.filter(tracks).toList() as List<Track>;
|
||||||
|
final addableTrack =
|
||||||
|
await SpotubeTrack.fetchFromTrack(tracks[initialIndex], preferences);
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
tracks: mergeTracks([addableTrack], tracks), active: initialIndex);
|
||||||
|
|
||||||
|
await audioPlayer.openPlaylist(
|
||||||
|
state.tracks.map(makeAppropriateSource).toList(),
|
||||||
|
initialIndex: initialIndex,
|
||||||
|
autoPlay: autoPlay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> jumpTo(int index) async {
|
||||||
|
final track = await ensureNthSourcePlayable(index);
|
||||||
|
if (track != null) {
|
||||||
|
state = state.copyWith(tracks: mergeTracks([track], state.tracks));
|
||||||
|
}
|
||||||
|
await audioPlayer.jumpTo(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> jumpToTrack(Track track) async {
|
||||||
|
final index =
|
||||||
|
state.tracks.toList().indexWhere((element) => element.id == track.id);
|
||||||
|
if (index == -1) return;
|
||||||
|
await jumpTo(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add safe guards for active/playing track that needs to be moved
|
||||||
|
Future<void> moveTrack(int oldIndex, int newIndex) async {
|
||||||
|
if (oldIndex == newIndex ||
|
||||||
|
newIndex < 0 ||
|
||||||
|
oldIndex < 0 ||
|
||||||
|
newIndex > state.tracks.length - 1 ||
|
||||||
|
oldIndex > state.tracks.length - 1) return;
|
||||||
|
|
||||||
|
final tracks = state.tracks.toList();
|
||||||
|
final track = tracks.removeAt(oldIndex);
|
||||||
|
tracks.insert(newIndex, track);
|
||||||
|
state = state.copyWith(tracks: {...tracks});
|
||||||
|
|
||||||
|
await audioPlayer.moveTrack(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addTracksAtFirst(Iterable<Track> track) async {}
|
||||||
|
Future<void> populateSibling() async {}
|
||||||
|
Future<void> swapSibling(Video video) async {}
|
||||||
|
|
||||||
|
Future<void> next() async {
|
||||||
|
final track = await ensureNthSourcePlayable(audioPlayer.currentIndex + 1);
|
||||||
|
if (track != null) {
|
||||||
|
state = state.copyWith(tracks: mergeTracks([track], state.tracks));
|
||||||
|
}
|
||||||
|
await audioPlayer.skipToNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> previous() async {
|
||||||
|
final track = await ensureNthSourcePlayable(audioPlayer.currentIndex - 1);
|
||||||
|
if (track != null) {
|
||||||
|
state = state.copyWith(tracks: mergeTracks([track], state.tracks));
|
||||||
|
}
|
||||||
|
await audioPlayer.skipToPrevious();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() async {
|
||||||
|
state = ProxyPlaylist({});
|
||||||
|
await audioPlayer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updatePalette() {
|
||||||
|
return Future.microtask(() async {
|
||||||
|
final activeTrack = state.tracks.firstWhereOrNull(
|
||||||
|
(track) =>
|
||||||
|
track is SpotubeTrack &&
|
||||||
|
track.ytUri ==
|
||||||
|
audioPlayer.sources.elementAtOrNull(audioPlayer.currentIndex),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeTrack == null) return;
|
||||||
|
|
||||||
|
final palette = await PaletteGenerator.fromImageProvider(
|
||||||
|
UniversalImage.imageProvider(
|
||||||
|
TypeConversionUtils.image_X_UrlString(
|
||||||
|
activeTrack.album?.images,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ref.read(paletteProvider.notifier).state = palette;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set state(state) {
|
||||||
|
super.state = state;
|
||||||
|
if (state.tracks.isEmpty && ref.read(paletteProvider) != null) {
|
||||||
|
ref.read(paletteProvider.notifier).state = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
||||||
import 'package:spotube/provider/palette_provider.dart';
|
import 'package:spotube/provider/palette_provider.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/persisted_change_notifier.dart';
|
import 'package:spotube/utils/persisted_change_notifier.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
@ -114,7 +114,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
if (!sync) {
|
if (!sync) {
|
||||||
ref.read(paletteProvider.notifier).state = null;
|
ref.read(paletteProvider.notifier).state = null;
|
||||||
} else {
|
} else {
|
||||||
ref.read(PlaylistQueueNotifier.notifier).updatePalette();
|
ref.read(ProxyPlaylistNotifier.notifier).updatePalette();
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
updatePersistence();
|
updatePersistence();
|
||||||
|
@ -63,15 +63,15 @@ class SpotubeAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stream that emits when the player is almost (80%) complete
|
/// Stream that emits when the player is almost (%) complete
|
||||||
Stream<void> get almostCompleteStream {
|
Stream<void> percentCompletedStream(double percent) {
|
||||||
return positionStream
|
return positionStream
|
||||||
.asyncMap((event) async => [event, await duration])
|
.asyncMap((event) async => [event, await duration])
|
||||||
.where((event) {
|
.where((event) {
|
||||||
final position = event[0] as Duration;
|
final position = event[0] as Duration;
|
||||||
final duration = event[1] as Duration;
|
final duration = event[1] as Duration;
|
||||||
|
|
||||||
return position.inSeconds > (duration.inSeconds * .8).toInt();
|
return position.inSeconds > duration.inSeconds * percent / 100;
|
||||||
}).asBroadcastStream();
|
}).asBroadcastStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +83,36 @@ class SpotubeAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<bool> get shuffledStream {
|
||||||
|
if (mkSupportedPlatform) {
|
||||||
|
return _mkPlayer!.shuffleStream.asBroadcastStream();
|
||||||
|
} else {
|
||||||
|
return _justAudio!.shuffleModeEnabledStream.asBroadcastStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<PlaybackLoopMode> get loopModeStream {
|
||||||
|
if (mkSupportedPlatform) {
|
||||||
|
return _mkPlayer!.loopModeStream
|
||||||
|
.map(PlaybackLoopMode.fromPlaylistMode)
|
||||||
|
.asBroadcastStream();
|
||||||
|
} else {
|
||||||
|
return _justAudio!.loopModeStream
|
||||||
|
.map(PlaybackLoopMode.fromLoopMode)
|
||||||
|
.asBroadcastStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<double> get volumeStream {
|
||||||
|
if (mkSupportedPlatform) {
|
||||||
|
return _mkPlayer!.streams.volume
|
||||||
|
.map((event) => event / 100)
|
||||||
|
.asBroadcastStream();
|
||||||
|
} else {
|
||||||
|
return _justAudio!.volumeStream.asBroadcastStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Stream<bool> get bufferingStream {
|
Stream<bool> get bufferingStream {
|
||||||
if (mkSupportedPlatform) {
|
if (mkSupportedPlatform) {
|
||||||
return Stream.value(false).asBroadcastStream();
|
return Stream.value(false).asBroadcastStream();
|
||||||
@ -191,6 +221,30 @@ class SpotubeAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> get isShuffled async {
|
||||||
|
if (mkSupportedPlatform) {
|
||||||
|
return _mkPlayer!.shuffled;
|
||||||
|
} else {
|
||||||
|
return _justAudio!.shuffleModeEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PlaybackLoopMode> get loopMode async {
|
||||||
|
if (mkSupportedPlatform) {
|
||||||
|
return PlaybackLoopMode.fromPlaylistMode(_mkPlayer!.loopMode);
|
||||||
|
} else {
|
||||||
|
return PlaybackLoopMode.fromLoopMode(_justAudio!.loopMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double get volume {
|
||||||
|
if (mkSupportedPlatform) {
|
||||||
|
return _mkPlayer!.state.volume / 100;
|
||||||
|
} else {
|
||||||
|
return _justAudio!.volume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool get isBuffering {
|
bool get isBuffering {
|
||||||
if (mkSupportedPlatform) {
|
if (mkSupportedPlatform) {
|
||||||
// audioplayers doesn't have the capability to get buffering state
|
// audioplayers doesn't have the capability to get buffering state
|
||||||
@ -257,9 +311,7 @@ class SpotubeAudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
_mkLooped = PlaybackLoopMode.none;
|
await _mkPlayer?.stop();
|
||||||
_mkShuffled = false;
|
|
||||||
await _mkPlayer?.pause();
|
|
||||||
await _justAudio?.stop();
|
await _justAudio?.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +347,7 @@ class SpotubeAudioPlayer {
|
|||||||
if (mkSupportedPlatform) {
|
if (mkSupportedPlatform) {
|
||||||
await _mkPlayer!.open(
|
await _mkPlayer!.open(
|
||||||
mk.Playlist(
|
mk.Playlist(
|
||||||
tracks.map((e) => mk.Media(e)).toList(),
|
tracks.map(mk.Media.new).toList(),
|
||||||
index: initialIndex,
|
index: initialIndex,
|
||||||
),
|
),
|
||||||
play: autoPlay,
|
play: autoPlay,
|
||||||
@ -359,7 +411,7 @@ class SpotubeAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> skipToIndex(int index) async {
|
Future<void> jumpTo(int index) async {
|
||||||
if (mkSupportedPlatform) {
|
if (mkSupportedPlatform) {
|
||||||
await _mkPlayer!.jump(index);
|
await _mkPlayer!.jump(index);
|
||||||
} else {
|
} else {
|
||||||
@ -395,6 +447,39 @@ class SpotubeAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> replaceSource(String oldSource, String newSource) async {
|
||||||
|
final willBeReplacedIndex = sources.indexOf(oldSource);
|
||||||
|
if (willBeReplacedIndex == -1) return;
|
||||||
|
|
||||||
|
if (mkSupportedPlatform) {
|
||||||
|
final sourcesCp = sources.toList();
|
||||||
|
sourcesCp[willBeReplacedIndex] = newSource;
|
||||||
|
await _mkPlayer!.open(
|
||||||
|
mk.Playlist(
|
||||||
|
sourcesCp.map(mk.Media.new).toList(),
|
||||||
|
index: currentIndex,
|
||||||
|
),
|
||||||
|
play: false,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await addTrack(newSource);
|
||||||
|
await removeTrack(willBeReplacedIndex);
|
||||||
|
|
||||||
|
int newSourceIndex = sources.indexOf(newSource);
|
||||||
|
while (newSourceIndex == -1) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
newSourceIndex = sources.indexOf(newSource);
|
||||||
|
}
|
||||||
|
await moveTrack(newSourceIndex, willBeReplacedIndex);
|
||||||
|
newSourceIndex = sources.indexOf(newSource);
|
||||||
|
while (newSourceIndex != willBeReplacedIndex) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
await moveTrack(newSourceIndex, willBeReplacedIndex);
|
||||||
|
newSourceIndex = sources.indexOf(newSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> clearPlaylist() async {
|
Future<void> clearPlaylist() async {
|
||||||
if (mkSupportedPlatform) {
|
if (mkSupportedPlatform) {
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
@ -407,41 +492,19 @@ class SpotubeAudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _mkShuffled = false;
|
|
||||||
|
|
||||||
Future<void> setShuffle(bool shuffle) async {
|
Future<void> setShuffle(bool shuffle) async {
|
||||||
if (mkSupportedPlatform) {
|
if (mkSupportedPlatform) {
|
||||||
await _mkPlayer!.setShuffle(shuffle);
|
await _mkPlayer!.setShuffle(shuffle);
|
||||||
_mkShuffled = shuffle;
|
|
||||||
} else {
|
} else {
|
||||||
await _justAudio!.setShuffleModeEnabled(shuffle);
|
await _justAudio!.setShuffleModeEnabled(shuffle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isShuffled() async {
|
|
||||||
if (mkSupportedPlatform) {
|
|
||||||
return _mkShuffled;
|
|
||||||
} else {
|
|
||||||
return _justAudio!.shuffleModeEnabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaybackLoopMode _mkLooped = PlaybackLoopMode.none;
|
|
||||||
|
|
||||||
Future<void> setLoopMode(PlaybackLoopMode loop) async {
|
Future<void> setLoopMode(PlaybackLoopMode loop) async {
|
||||||
if (mkSupportedPlatform) {
|
if (mkSupportedPlatform) {
|
||||||
await _mkPlayer!.setPlaylistMode(loop.toPlaylistMode());
|
await _mkPlayer!.setPlaylistMode(loop.toPlaylistMode());
|
||||||
_mkLooped = loop;
|
|
||||||
} else {
|
} else {
|
||||||
await _justAudio!.setLoopMode(loop.toLoopMode());
|
await _justAudio!.setLoopMode(loop.toLoopMode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PlaybackLoopMode> getLoopMode() async {
|
|
||||||
if (mkSupportedPlatform) {
|
|
||||||
return _mkLooped;
|
|
||||||
} else {
|
|
||||||
return PlaybackLoopMode.fromLoopMode(_justAudio!.loopMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,20 @@ import 'package:spotube/services/audio_player/playback_state.dart';
|
|||||||
/// This class adds a state stream to the [Player] class.
|
/// This class adds a state stream to the [Player] class.
|
||||||
class MkPlayerWithState extends Player {
|
class MkPlayerWithState extends Player {
|
||||||
final StreamController<AudioPlaybackState> _playerStateStream;
|
final StreamController<AudioPlaybackState> _playerStateStream;
|
||||||
|
final StreamController<bool> _shuffleStream;
|
||||||
|
final StreamController<PlaylistMode> _loopModeStream;
|
||||||
|
|
||||||
late final List<StreamSubscription> _subscriptions;
|
late final List<StreamSubscription> _subscriptions;
|
||||||
|
|
||||||
|
bool _shuffled;
|
||||||
|
PlaylistMode _loopMode;
|
||||||
|
|
||||||
MkPlayerWithState({super.configuration})
|
MkPlayerWithState({super.configuration})
|
||||||
: _playerStateStream = StreamController.broadcast() {
|
: _playerStateStream = StreamController.broadcast(),
|
||||||
|
_shuffleStream = StreamController.broadcast(),
|
||||||
|
_loopModeStream = StreamController.broadcast(),
|
||||||
|
_shuffled = false,
|
||||||
|
_loopMode = PlaylistMode.none {
|
||||||
_subscriptions = [
|
_subscriptions = [
|
||||||
streams.buffering.listen((event) {
|
streams.buffering.listen((event) {
|
||||||
_playerStateStream.add(AudioPlaybackState.buffering);
|
_playerStateStream.add(AudioPlaybackState.buffering);
|
||||||
@ -34,7 +43,35 @@ class MkPlayerWithState extends Player {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get shuffled => _shuffled;
|
||||||
|
PlaylistMode get loopMode => _loopMode;
|
||||||
|
|
||||||
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
|
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
|
||||||
|
Stream<bool> get shuffleStream => _shuffleStream.stream;
|
||||||
|
Stream<PlaylistMode> get loopModeStream => _loopModeStream.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setShuffle(bool shuffle) async {
|
||||||
|
_shuffled = shuffle;
|
||||||
|
await super.setShuffle(shuffle);
|
||||||
|
_shuffleStream.add(shuffle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setPlaylistMode(PlaylistMode playlistMode) async {
|
||||||
|
_loopMode = playlistMode;
|
||||||
|
await super.setPlaylistMode(playlistMode);
|
||||||
|
_loopModeStream.add(playlistMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() async {
|
||||||
|
pause();
|
||||||
|
_loopMode = PlaylistMode.none;
|
||||||
|
_shuffled = false;
|
||||||
|
for (int i = 0; i < state.playlist.medias.length; i++) {
|
||||||
|
await remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> dispose({int code = 0}) {
|
FutureOr<void> dispose({int code = 0}) {
|
||||||
|
@ -3,7 +3,7 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/spotube_track.dart';
|
import 'package:spotube/models/spotube_track.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_services/linux_audio_service.dart';
|
import 'package:spotube/services/audio_services/linux_audio_service.dart';
|
||||||
import 'package:spotube/services/audio_services/mobile_audio_service.dart';
|
import 'package:spotube/services/audio_services/mobile_audio_service.dart';
|
||||||
import 'package:spotube/services/audio_services/windows_audio_service.dart';
|
import 'package:spotube/services/audio_services/windows_audio_service.dart';
|
||||||
@ -18,15 +18,12 @@ class AudioServices {
|
|||||||
|
|
||||||
static Future<AudioServices> create(
|
static Future<AudioServices> create(
|
||||||
Ref ref,
|
Ref ref,
|
||||||
PlaylistQueueNotifier playlistQueueNotifier,
|
ProxyPlaylistNotifier playback,
|
||||||
) async {
|
) async {
|
||||||
final mobile =
|
final mobile =
|
||||||
DesktopTools.platform.isMobile || DesktopTools.platform.isMacOS
|
DesktopTools.platform.isMobile || DesktopTools.platform.isMacOS
|
||||||
? await AudioService.init(
|
? await AudioService.init(
|
||||||
builder: () => MobileAudioService(
|
builder: () => MobileAudioService(playback),
|
||||||
playlistQueueNotifier,
|
|
||||||
ref.read(VolumeProvider.provider.notifier),
|
|
||||||
),
|
|
||||||
config: const AudioServiceConfig(
|
config: const AudioServiceConfig(
|
||||||
androidNotificationChannelId: 'com.krtirtho.Spotube',
|
androidNotificationChannelId: 'com.krtirtho.Spotube',
|
||||||
androidNotificationChannelName: 'Spotube',
|
androidNotificationChannelName: 'Spotube',
|
||||||
@ -35,11 +32,10 @@ class AudioServices {
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
final smtc = DesktopTools.platform.isWindows
|
final smtc = DesktopTools.platform.isWindows
|
||||||
? WindowsAudioService(ref, playlistQueueNotifier)
|
? WindowsAudioService(ref, playback)
|
||||||
: null;
|
|
||||||
final mpris = DesktopTools.platform.isLinux
|
|
||||||
? LinuxAudioService(ref, playlistQueueNotifier)
|
|
||||||
: null;
|
: null;
|
||||||
|
final mpris =
|
||||||
|
DesktopTools.platform.isLinux ? LinuxAudioService(ref, playback) : null;
|
||||||
|
|
||||||
return AudioServices(mobile, smtc, mpris);
|
return AudioServices(mobile, smtc, mpris);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:mpris_service/mpris_service.dart';
|
import 'package:mpris_service/mpris_service.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/spotube_track.dart';
|
import 'package:spotube/models/spotube_track.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||||
@ -14,7 +14,7 @@ import 'package:spotube/utils/type_conversion_utils.dart';
|
|||||||
class LinuxAudioService {
|
class LinuxAudioService {
|
||||||
late final MPRIS mpris;
|
late final MPRIS mpris;
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
final PlaylistQueueNotifier playlistNotifier;
|
final ProxyPlaylistNotifier playlistNotifier;
|
||||||
|
|
||||||
final subscriptions = <StreamSubscription>[];
|
final subscriptions = <StreamSubscription>[];
|
||||||
|
|
||||||
@ -30,26 +30,24 @@ class LinuxAudioService {
|
|||||||
mpris.playbackStatus = MPRISPlaybackStatus.stopped;
|
mpris.playbackStatus = MPRISPlaybackStatus.stopped;
|
||||||
mpris.setEventHandler(MPRISEventHandler(
|
mpris.setEventHandler(MPRISEventHandler(
|
||||||
loopStatus: (value) async {
|
loopStatus: (value) async {
|
||||||
playlistNotifier.setLoopMode(
|
audioPlayer.setLoopMode(
|
||||||
PlaybackLoopMode.fromMPRISLoopStatus(value),
|
PlaybackLoopMode.fromMPRISLoopStatus(value),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
next: playlistNotifier.next,
|
next: playlistNotifier.next,
|
||||||
pause: playlistNotifier.pause,
|
pause: audioPlayer.pause,
|
||||||
play: playlistNotifier.resume,
|
play: audioPlayer.resume,
|
||||||
playPause: () async {
|
playPause: () async {
|
||||||
if (audioPlayer.isPlaying) {
|
if (audioPlayer.isPlaying) {
|
||||||
await playlistNotifier.pause();
|
await audioPlayer.pause();
|
||||||
} else {
|
} else {
|
||||||
await playlistNotifier.resume();
|
await audioPlayer.resume();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
seek: playlistNotifier.seek,
|
seek: audioPlayer.seek,
|
||||||
shuffle: playlistNotifier.setShuffle,
|
shuffle: audioPlayer.setShuffle,
|
||||||
stop: playlistNotifier.stop,
|
stop: playlistNotifier.stop,
|
||||||
volume: (value) async {
|
volume: audioPlayer.setVolume,
|
||||||
await ref.read(VolumeProvider.provider.notifier).setVolume(value);
|
|
||||||
},
|
|
||||||
previous: playlistNotifier.previous,
|
previous: playlistNotifier.previous,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -2,29 +2,29 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:audio_session/audio_session.dart';
|
import 'package:audio_session/audio_session.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/loop_mode.dart';
|
import 'package:spotube/services/audio_player/loop_mode.dart';
|
||||||
|
|
||||||
class MobileAudioService extends BaseAudioHandler {
|
class MobileAudioService extends BaseAudioHandler {
|
||||||
AudioSession? session;
|
AudioSession? session;
|
||||||
final PlaylistQueueNotifier playlistNotifier;
|
final ProxyPlaylistNotifier playlistNotifier;
|
||||||
final VolumeProvider volumeNotifier;
|
|
||||||
|
|
||||||
PlaylistQueue? get playlist => playlistNotifier.state;
|
ProxyPlaylist get playlist => playlistNotifier.state;
|
||||||
|
|
||||||
MobileAudioService(this.playlistNotifier, this.volumeNotifier) {
|
MobileAudioService(this.playlistNotifier) {
|
||||||
AudioSession.instance.then((s) {
|
AudioSession.instance.then((s) {
|
||||||
session = s;
|
session = s;
|
||||||
session?.configure(const AudioSessionConfiguration.music());
|
session?.configure(const AudioSessionConfiguration.music());
|
||||||
s.interruptionEventStream.listen((event) async {
|
s.interruptionEventStream.listen((event) async {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case AudioInterruptionType.duck:
|
case AudioInterruptionType.duck:
|
||||||
await volumeNotifier.setVolume(event.begin ? 0.5 : 1.0);
|
await audioPlayer.setVolume(event.begin ? 0.5 : 1.0);
|
||||||
break;
|
break;
|
||||||
case AudioInterruptionType.pause:
|
case AudioInterruptionType.pause:
|
||||||
case AudioInterruptionType.unknown:
|
case AudioInterruptionType.unknown:
|
||||||
await playlistNotifier.pause();
|
await audioPlayer.pause();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -47,25 +47,25 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> play() => playlistNotifier.resume();
|
Future<void> play() => audioPlayer.resume();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> pause() => playlistNotifier.pause();
|
Future<void> pause() => audioPlayer.pause();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> seek(Duration position) => playlistNotifier.seek(position);
|
Future<void> seek(Duration position) => audioPlayer.seek(position);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
||||||
await super.setShuffleMode(shuffleMode);
|
await super.setShuffleMode(shuffleMode);
|
||||||
|
|
||||||
playlistNotifier.setShuffle(shuffleMode == AudioServiceShuffleMode.all);
|
audioPlayer.setShuffle(shuffleMode == AudioServiceShuffleMode.all);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
|
||||||
super.setRepeatMode(repeatMode);
|
super.setRepeatMode(repeatMode);
|
||||||
playlistNotifier.setLoopMode(
|
audioPlayer.setLoopMode(
|
||||||
PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode),
|
PlaybackLoopMode.fromAudioServiceRepeatMode(repeatMode),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,12 +109,11 @@ class MobileAudioService extends BaseAudioHandler {
|
|||||||
playing: audioPlayer.isPlaying,
|
playing: audioPlayer.isPlaying,
|
||||||
updatePosition: position,
|
updatePosition: position,
|
||||||
bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero,
|
bufferedPosition: await audioPlayer.bufferedPosition ?? Duration.zero,
|
||||||
shuffleMode: playlist?.shuffled == true
|
shuffleMode: await audioPlayer.isShuffled == true
|
||||||
? AudioServiceShuffleMode.all
|
? AudioServiceShuffleMode.all
|
||||||
: AudioServiceShuffleMode.none,
|
: AudioServiceShuffleMode.none,
|
||||||
repeatMode: playlist?.loopMode.toAudioServiceRepeatMode() ??
|
repeatMode: (await audioPlayer.loopMode).toAudioServiceRepeatMode(),
|
||||||
AudioServiceRepeatMode.none,
|
processingState: playlist.isFetching == true
|
||||||
processingState: playlist?.isLoading == true
|
|
||||||
? AudioProcessingState.loading
|
? AudioProcessingState.loading
|
||||||
: AudioProcessingState.ready,
|
: AudioProcessingState.ready,
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:smtc_windows/smtc_windows.dart';
|
import 'package:smtc_windows/smtc_windows.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/audio_player/playback_state.dart';
|
import 'package:spotube/services/audio_player/playback_state.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
@ -11,7 +11,7 @@ import 'package:spotube/utils/type_conversion_utils.dart';
|
|||||||
class WindowsAudioService {
|
class WindowsAudioService {
|
||||||
final SMTCWindows smtc;
|
final SMTCWindows smtc;
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
final PlaylistQueueNotifier playlistNotifier;
|
final ProxyPlaylistNotifier playlistNotifier;
|
||||||
|
|
||||||
final subscriptions = <StreamSubscription>[];
|
final subscriptions = <StreamSubscription>[];
|
||||||
|
|
||||||
@ -21,10 +21,10 @@ class WindowsAudioService {
|
|||||||
final buttonStream = smtc.buttonPressStream.listen((event) {
|
final buttonStream = smtc.buttonPressStream.listen((event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case PressedButton.play:
|
case PressedButton.play:
|
||||||
playlistNotifier.resume();
|
audioPlayer.resume();
|
||||||
break;
|
break;
|
||||||
case PressedButton.pause:
|
case PressedButton.pause:
|
||||||
playlistNotifier.pause();
|
audioPlayer.pause();
|
||||||
break;
|
break;
|
||||||
case PressedButton.next:
|
case PressedButton.next:
|
||||||
playlistNotifier.next();
|
playlistNotifier.next();
|
||||||
|
@ -58,8 +58,8 @@ dependencies:
|
|||||||
just_audio: ^0.9.32
|
just_audio: ^0.9.32
|
||||||
logger: ^1.1.0
|
logger: ^1.1.0
|
||||||
media_kit: ^0.0.7+1
|
media_kit: ^0.0.7+1
|
||||||
media_kit_libs_linux: ^1.0.2
|
|
||||||
media_kit_libs_windows_audio: ^1.0.3
|
media_kit_libs_windows_audio: ^1.0.3
|
||||||
|
media_kit_libs_linux: ^1.0.2
|
||||||
media_kit_native_event_loop: ^1.0.3
|
media_kit_native_event_loop: ^1.0.3
|
||||||
metadata_god: ^0.4.1
|
metadata_god: ^0.4.1
|
||||||
mime: ^1.0.2
|
mime: ^1.0.2
|
||||||
|
Loading…
Reference in New Issue
Block a user