mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
314 lines
10 KiB
Dart
314 lines
10 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:auto_route/auto_route.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|
import 'package:spotube/collections/routes.dart';
|
|
import 'package:spotube/collections/routes.gr.dart';
|
|
import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
|
|
import 'package:spotube/components/dialogs/prompt_dialog.dart';
|
|
import 'package:spotube/components/dialogs/track_details_dialog.dart';
|
|
import 'package:spotube/extensions/context.dart';
|
|
import 'package:spotube/models/database/database.dart';
|
|
import 'package:spotube/models/metadata/metadata.dart';
|
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
|
import 'package:spotube/provider/blacklist_provider.dart';
|
|
import 'package:spotube/provider/download_manager_provider.dart';
|
|
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
|
|
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
|
import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
|
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
|
import 'package:spotube/services/metadata/endpoints/error.dart';
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
|
|
|
enum TrackOptionValue {
|
|
album,
|
|
share,
|
|
songlink,
|
|
addToPlaylist,
|
|
addToQueue,
|
|
removeFromPlaylist,
|
|
removeFromQueue,
|
|
blacklist,
|
|
delete,
|
|
playNext,
|
|
favorite,
|
|
details,
|
|
download,
|
|
startRadio,
|
|
}
|
|
|
|
class TrackOptionsActions {
|
|
final Ref ref;
|
|
final SpotubeTrackObject track;
|
|
|
|
TrackOptionsActions(this.ref, this.track);
|
|
|
|
AudioPlayerNotifier get playback => ref.read(audioPlayerProvider.notifier);
|
|
MetadataPluginSavedTracksNotifier get favoriteTracks =>
|
|
ref.read(metadataPluginSavedTracksProvider.notifier);
|
|
MetadataPluginSavedPlaylistsNotifier get favoritePlaylistsNotifier =>
|
|
ref.read(metadataPluginSavedPlaylistsProvider.notifier);
|
|
DownloadManagerProvider get downloadManager =>
|
|
ref.read(downloadManagerProvider.notifier);
|
|
BlackListNotifier get blacklist => ref.read(blacklistProvider.notifier);
|
|
|
|
void actionShare(BuildContext context) {
|
|
Clipboard.setData(ClipboardData(text: track.externalUri)).then((_) {
|
|
if (context.mounted) {
|
|
showToast(
|
|
context: rootNavigatorKey.currentContext!,
|
|
location: ToastLocation.topRight,
|
|
builder: (context, overlay) {
|
|
return SurfaceCard(
|
|
child: Text(
|
|
context.l10n.copied_to_clipboard(track.externalUri),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> actionAddToPlaylist(
|
|
BuildContext context,
|
|
String? playlistId,
|
|
) async {
|
|
/// showDialog doesn't work for some reason. So we have to
|
|
/// manually push a Dialog Route in the Navigator to get it working
|
|
await showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return PlaylistAddTrackDialog(
|
|
tracks: [track],
|
|
openFromPlaylist: playlistId,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> actionStartRadio(BuildContext context) async {
|
|
final playback = ref.read(audioPlayerProvider.notifier);
|
|
final playlist = ref.read(audioPlayerProvider);
|
|
final metadataPlugin = await ref.read(metadataPluginProvider.future);
|
|
|
|
if (metadataPlugin == null) {
|
|
throw MetadataPluginException.noDefaultPlugin(
|
|
"No default metadata plugin set",
|
|
);
|
|
}
|
|
|
|
final tracks = await metadataPlugin.track.radio(track.id);
|
|
|
|
bool replaceQueue = false;
|
|
|
|
if (context.mounted && playlist.tracks.isNotEmpty) {
|
|
replaceQueue = await showPromptDialog(
|
|
context: context,
|
|
title: context.l10n.how_to_start_radio,
|
|
message: context.l10n.replace_queue_question,
|
|
okText: context.l10n.replace,
|
|
cancelText: context.l10n.add_to_queue,
|
|
);
|
|
}
|
|
|
|
if (replaceQueue || playlist.tracks.isEmpty) {
|
|
await playback.stop();
|
|
await playback.load([track], autoPlay: true);
|
|
|
|
// we don't have to add those tracks as useEndlessPlayback will do it for us
|
|
return;
|
|
} else {
|
|
await playback.addTrack(track);
|
|
}
|
|
|
|
await playback.addTracks(
|
|
tracks.toList()
|
|
..removeWhere((e) {
|
|
final isDuplicate = playlist.tracks.any((t) => t.id == e.id);
|
|
return e.id == track.id || isDuplicate;
|
|
}),
|
|
);
|
|
}
|
|
|
|
Future<void> action(
|
|
BuildContext context,
|
|
TrackOptionValue value,
|
|
String? playlistId,
|
|
) async {
|
|
switch (value) {
|
|
case TrackOptionValue.album:
|
|
await context.navigateTo(
|
|
AlbumRoute(id: track.album.id, album: track.album),
|
|
);
|
|
break;
|
|
case TrackOptionValue.delete:
|
|
await File((track as SpotubeLocalTrackObject).path).delete();
|
|
ref.invalidate(localTracksProvider);
|
|
break;
|
|
case TrackOptionValue.addToQueue:
|
|
await playback.addTrack(track);
|
|
if (context.mounted) {
|
|
showToast(
|
|
context: context,
|
|
location: ToastLocation.topRight,
|
|
builder: (context, overlay) {
|
|
return SurfaceCard(
|
|
child: Text(
|
|
context.l10n.added_track_to_queue(track.name),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
break;
|
|
case TrackOptionValue.playNext:
|
|
playback.addTracksAtFirst([track]);
|
|
|
|
if (context.mounted) {
|
|
showToast(
|
|
context: context,
|
|
location: ToastLocation.topRight,
|
|
builder: (context, overlay) {
|
|
return SurfaceCard(
|
|
child: Text(
|
|
context.l10n.track_will_play_next(track.name),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
break;
|
|
case TrackOptionValue.removeFromQueue:
|
|
playback.removeTrack(track.id);
|
|
|
|
if (context.mounted) {
|
|
showToast(
|
|
context: context,
|
|
location: ToastLocation.topRight,
|
|
builder: (context, overlay) {
|
|
return SurfaceCard(
|
|
child: Text(
|
|
context.l10n.removed_track_from_queue(
|
|
track.name,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
break;
|
|
case TrackOptionValue.favorite:
|
|
final isLikedTrack = await ref.read(
|
|
metadataPluginIsSavedTrackProvider(track.id).future,
|
|
);
|
|
|
|
if (isLikedTrack) {
|
|
await favoriteTracks.removeFavorite([track]);
|
|
} else {
|
|
await favoriteTracks.addFavorite([track]);
|
|
}
|
|
break;
|
|
case TrackOptionValue.addToPlaylist:
|
|
actionAddToPlaylist(context, playlistId);
|
|
break;
|
|
case TrackOptionValue.removeFromPlaylist:
|
|
favoritePlaylistsNotifier.removeTracks(playlistId ?? "", [track.id]);
|
|
break;
|
|
case TrackOptionValue.blacklist:
|
|
final isBlacklisted = blacklist.contains(track);
|
|
if (isBlacklisted == true) {
|
|
await ref.read(blacklistProvider.notifier).remove(track.id);
|
|
} else {
|
|
await ref.read(blacklistProvider.notifier).add(
|
|
BlacklistTableCompanion.insert(
|
|
name: track.name,
|
|
elementId: track.id,
|
|
elementType: BlacklistedType.track,
|
|
),
|
|
);
|
|
}
|
|
break;
|
|
case TrackOptionValue.share:
|
|
actionShare(context);
|
|
break;
|
|
case TrackOptionValue.songlink:
|
|
final url = "https://song.link/s/${track.id}";
|
|
await launchUrlString(url);
|
|
break;
|
|
case TrackOptionValue.details:
|
|
if (track is! SpotubeFullTrackObject) break;
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 400),
|
|
child: TrackDetailsDialog(track: track as SpotubeFullTrackObject),
|
|
),
|
|
);
|
|
break;
|
|
case TrackOptionValue.download:
|
|
if (track is! SpotubeFullTrackObject) break;
|
|
await downloadManager.addToQueue(track as SpotubeFullTrackObject);
|
|
break;
|
|
case TrackOptionValue.startRadio:
|
|
actionStartRadio(context);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
typedef TrackOptionFlags = ({
|
|
bool isInQueue,
|
|
bool isBlacklisted,
|
|
bool isInDownloadQueue,
|
|
bool isActiveTrack,
|
|
bool isAuthenticated,
|
|
bool isLiked,
|
|
ValueNotifier<double>? progressNotifier,
|
|
});
|
|
|
|
final trackOptionActionsProvider =
|
|
Provider.family<TrackOptionsActions, SpotubeTrackObject>(
|
|
(ref, track) => TrackOptionsActions(ref, track),
|
|
);
|
|
|
|
final trackOptionsStateProvider =
|
|
Provider.family<TrackOptionFlags, SpotubeTrackObject>((ref, track) {
|
|
ref.watch(downloadManagerProvider);
|
|
ref.watch(blacklistProvider);
|
|
|
|
final playlist = ref.watch(audioPlayerProvider);
|
|
final authenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
|
final downloadManager = ref.watch(downloadManagerProvider.notifier);
|
|
final blacklist = ref.watch(blacklistProvider.notifier);
|
|
final isBlacklisted = blacklist.contains(track);
|
|
final isSavedTrack = ref.watch(metadataPluginIsSavedTrackProvider(track.id));
|
|
|
|
final isInDownloadQueue = playlist.activeTrack == null ||
|
|
playlist.activeTrack! is SpotubeLocalTrackObject
|
|
? false
|
|
: downloadManager
|
|
.isActive(playlist.activeTrack! as SpotubeFullTrackObject);
|
|
|
|
final progressNotifier = track is SpotubeLocalTrackObject
|
|
? null
|
|
: downloadManager.getProgressNotifier(track as SpotubeFullTrackObject);
|
|
|
|
return (
|
|
isInQueue: playlist.containsTrack(track),
|
|
isBlacklisted: isBlacklisted,
|
|
isInDownloadQueue: isInDownloadQueue,
|
|
isActiveTrack: playlist.activeTrack?.id == track.id,
|
|
isAuthenticated: authenticated.asData?.value ?? false,
|
|
isLiked: isSavedTrack.asData?.value ?? false,
|
|
progressNotifier: progressNotifier,
|
|
);
|
|
});
|