mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: optimize track options and related artists
This commit is contained in:
parent
3b21b05fdc
commit
3a5ddd6214
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_undraw/flutter_undraw.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
@ -80,9 +81,12 @@ class PresentationListSection extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
itemBuilder: (context, index) => HookBuilder(builder: (context) {
|
||||
final track = state.presentationTracks[index];
|
||||
final isSelected = state.selectedTracks.any((e) => e.id == track.id);
|
||||
final isSelected = useMemoized(
|
||||
() => state.selectedTracks.any((e) => e.id == track.id),
|
||||
[track.id, state.selectedTracks],
|
||||
);
|
||||
return TrackTile(
|
||||
userPlaylist: isUserPlaylist,
|
||||
playlistId: options.collectionId,
|
||||
@ -105,7 +109,7 @@ class PresentationListSection extends HookConsumerWidget {
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -9,59 +5,22 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.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/components/heart_button/use_track_toggle_like.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/components/links/artist_link.dart';
|
||||
import 'package:spotube/components/ui/button_tile.dart';
|
||||
import 'package:spotube/extensions/constrains.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/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/audio_player/audio_player.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/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/user.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,
|
||||
}
|
||||
import 'package:spotube/provider/track_options/track_options_provider.dart';
|
||||
|
||||
/// [track] must be a [SpotubeFullTrackObject] or [SpotubeLocalTrackObject]
|
||||
class TrackOptions extends HookConsumerWidget {
|
||||
final SpotubeTrackObject track;
|
||||
final bool userPlaylist;
|
||||
final String? playlistId;
|
||||
final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef;
|
||||
final Widget? icon;
|
||||
const TrackOptions({
|
||||
super.key,
|
||||
required this.track,
|
||||
this.showMenuCbRef,
|
||||
this.userPlaylist = false,
|
||||
this.playlistId,
|
||||
this.icon,
|
||||
@ -70,302 +29,53 @@ class TrackOptions extends HookConsumerWidget {
|
||||
"Track must be a SpotubeFullTrackObject, SpotubeLocalTrackObject",
|
||||
);
|
||||
|
||||
void actionShare(BuildContext context, SpotubeTrackObject track) {
|
||||
Clipboard.setData(ClipboardData(text: track.externalUri)).then((_) {
|
||||
if (context.mounted) {
|
||||
showToast(
|
||||
context: context,
|
||||
location: ToastLocation.topRight,
|
||||
builder: (context, overlay) {
|
||||
return SurfaceCard(
|
||||
child: Text(
|
||||
context.l10n.copied_to_clipboard(track.externalUri),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void actionAddToPlaylist(
|
||||
BuildContext context,
|
||||
SpotubeTrackObject track,
|
||||
) {
|
||||
/// showDialog doesn't work for some reason. So we have to
|
||||
/// manually push a Dialog Route in the Navigator to get it working
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return PlaylistAddTrackDialog(
|
||||
tracks: [track],
|
||||
openFromPlaylist: playlistId,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void actionStartRadio(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
SpotubeTrackObject track,
|
||||
) 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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final ThemeData(:colorScheme) = Theme.of(context);
|
||||
|
||||
final playlist = ref.watch(audioPlayerProvider);
|
||||
final playback = ref.watch(audioPlayerProvider.notifier);
|
||||
final authenticated = ref.watch(metadataPluginAuthenticatedProvider);
|
||||
ref.watch(downloadManagerProvider);
|
||||
final downloadManager = ref.watch(downloadManagerProvider.notifier);
|
||||
final blacklist = ref.watch(blacklistProvider);
|
||||
final me = ref.watch(metadataPluginUserProvider);
|
||||
|
||||
final favorites = useTrackToggleLike(track, ref);
|
||||
|
||||
final isBlackListed = useMemoized(
|
||||
() => blacklist.asData?.value.any(
|
||||
(element) => element.elementId == track.id,
|
||||
),
|
||||
[blacklist, track],
|
||||
);
|
||||
|
||||
final removingTrack = useState<String?>(null);
|
||||
final favoritePlaylistsNotifier =
|
||||
ref.watch(metadataPluginSavedPlaylistsProvider.notifier);
|
||||
|
||||
final isInDownloadQueue = useMemoized(() {
|
||||
if (playlist.activeTrack == null ||
|
||||
playlist.activeTrack! is SpotubeLocalTrackObject) {
|
||||
return false;
|
||||
}
|
||||
return downloadManager.isActive(
|
||||
playlist.activeTrack! as SpotubeFullTrackObject,
|
||||
);
|
||||
}, [
|
||||
playlist.activeTrack,
|
||||
downloadManager,
|
||||
]);
|
||||
|
||||
final progressNotifier = useMemoized(() {
|
||||
if (track is SpotubeLocalTrackObject) {
|
||||
return null;
|
||||
}
|
||||
return downloadManager
|
||||
.getProgressNotifier(track as SpotubeFullTrackObject);
|
||||
}, [downloadManager, track]);
|
||||
|
||||
final trackOptionActions = ref.watch(trackOptionActionsProvider(track));
|
||||
final (
|
||||
:isBlacklisted,
|
||||
:isInDownloadQueue,
|
||||
:isInQueue,
|
||||
:isActiveTrack,
|
||||
:isAuthenticated,
|
||||
:isLiked,
|
||||
:progressNotifier
|
||||
) = ref.watch(trackOptionsStateProvider(track));
|
||||
final isLocalTrack = track is SpotubeLocalTrackObject;
|
||||
|
||||
final adaptivePopSheetList = AdaptivePopSheetList<TrackOptionValue>(
|
||||
tooltip: context.l10n.more_actions,
|
||||
onSelected: (value) 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:
|
||||
favorites.toggleTrackLike(track);
|
||||
break;
|
||||
case TrackOptionValue.addToPlaylist:
|
||||
actionAddToPlaylist(context, track);
|
||||
break;
|
||||
case TrackOptionValue.removeFromPlaylist:
|
||||
removingTrack.value = track.externalUri;
|
||||
favoritePlaylistsNotifier
|
||||
.removeTracks(playlistId ?? "", [track.id]);
|
||||
break;
|
||||
case TrackOptionValue.blacklist:
|
||||
if (isBlackListed == null) break;
|
||||
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, track);
|
||||
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, ref, track);
|
||||
break;
|
||||
}
|
||||
},
|
||||
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
|
||||
variance: ButtonVariance.ghost,
|
||||
headings: [
|
||||
Basic(
|
||||
leading: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: UniversalImage(
|
||||
path: track.album.images
|
||||
.asUrlString(placeholder: ImagePlaceholder.albumArt),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
track.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).semiBold(),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ArtistLink(
|
||||
artists: track.artists,
|
||||
onOverflowArtistClick: () => context.navigateTo(
|
||||
TrackRoute(trackId: track.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
items: (context) => [
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (isLocalTrack)
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.delete,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.delete,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.trash),
|
||||
child: Text(context.l10n.delete),
|
||||
title: Text(context.l10n.delete),
|
||||
),
|
||||
if (mediaQuery.smAndDown && !isLocalTrack)
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.album,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.album,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.album),
|
||||
child: Column(
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -377,62 +87,116 @@ class TrackOptions extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!playlist.containsTrack(track)) ...[
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.addToQueue,
|
||||
if (!isInQueue) ...[
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.addToQueue,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.queueAdd),
|
||||
child: Text(context.l10n.add_to_queue),
|
||||
title: Text(context.l10n.add_to_queue),
|
||||
),
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.playNext,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.playNext,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.lightning),
|
||||
child: Text(context.l10n.play_next),
|
||||
title: Text(context.l10n.play_next),
|
||||
),
|
||||
] else
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.removeFromQueue,
|
||||
enabled: playlist.activeTrack?.id != track.id,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.removeFromQueue,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
enabled: !isActiveTrack,
|
||||
leading: const Icon(SpotubeIcons.queueRemove),
|
||||
child: Text(context.l10n.remove_from_queue),
|
||||
title: Text(context.l10n.remove_from_queue),
|
||||
),
|
||||
if (me.asData?.value != null && !isLocalTrack)
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.favorite,
|
||||
leading: favorites.isLiked
|
||||
if (isAuthenticated && !isLocalTrack)
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.favorite,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: isLiked
|
||||
? const Icon(
|
||||
SpotubeIcons.heartFilled,
|
||||
color: Colors.pink,
|
||||
)
|
||||
: const Icon(SpotubeIcons.heart),
|
||||
child: Text(
|
||||
favorites.isLiked
|
||||
title: Text(
|
||||
isLiked
|
||||
? context.l10n.remove_from_favorites
|
||||
: context.l10n.save_as_favorite,
|
||||
),
|
||||
),
|
||||
if (authenticated.asData?.value == true && !isLocalTrack) ...[
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.startRadio,
|
||||
if (isAuthenticated && !isLocalTrack) ...[
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.startRadio,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.radio),
|
||||
child: Text(context.l10n.start_a_radio),
|
||||
title: Text(context.l10n.start_a_radio),
|
||||
),
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.addToPlaylist,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.addToPlaylist,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.playlistAdd),
|
||||
child: Text(context.l10n.add_to_playlist),
|
||||
title: Text(context.l10n.add_to_playlist),
|
||||
),
|
||||
],
|
||||
if (userPlaylist &&
|
||||
authenticated.asData?.value == true &&
|
||||
!isLocalTrack)
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.removeFromPlaylist,
|
||||
if (userPlaylist && isAuthenticated && !isLocalTrack)
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.removeFromPlaylist,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.removeFilled),
|
||||
child: Text(context.l10n.remove_from_playlist),
|
||||
title: Text(context.l10n.remove_from_playlist),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.download,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.download,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
enabled: !isInDownloadQueue,
|
||||
leading: isInDownloadQueue
|
||||
? HookBuilder(builder: (context) {
|
||||
@ -442,58 +206,75 @@ class TrackOptions extends HookConsumerWidget {
|
||||
);
|
||||
})
|
||||
: const Icon(SpotubeIcons.download),
|
||||
child: Text(context.l10n.download_track),
|
||||
title: Text(context.l10n.download_track),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.blacklist,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.blacklist,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: Icon(
|
||||
SpotubeIcons.playlistRemove,
|
||||
color: isBlackListed != true ? Colors.red[400] : null,
|
||||
color: isBlacklisted != true ? Colors.red[400] : null,
|
||||
),
|
||||
child: Text(
|
||||
isBlackListed == true
|
||||
title: Text(
|
||||
isBlacklisted == true
|
||||
? context.l10n.remove_from_blacklist
|
||||
: context.l10n.add_to_blacklist,
|
||||
style: TextStyle(
|
||||
color: isBlackListed != true ? Colors.red[400] : null,
|
||||
color: isBlacklisted != true ? Colors.red[400] : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.share,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.share,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
child: Text(context.l10n.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.songlink,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.songlink,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: Assets.logos.songlinkTransparent.image(
|
||||
width: 22,
|
||||
height: 22,
|
||||
color: colorScheme.foreground.withValues(alpha: 0.5),
|
||||
),
|
||||
child: Text(context.l10n.song_link),
|
||||
title: Text(context.l10n.song_link),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
AdaptiveMenuButton(
|
||||
value: TrackOptionValue.details,
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
context,
|
||||
TrackOptionValue.details,
|
||||
playlistId,
|
||||
);
|
||||
},
|
||||
leading: const Icon(SpotubeIcons.info),
|
||||
child: Text(context.l10n.details),
|
||||
title: Text(context.l10n.details),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
//! This is the most ANTI pattern I've ever done, but it works
|
||||
showMenuCbRef?.value = (relativeRect) {
|
||||
final offsetFromRect = Offset(
|
||||
relativeRect.left,
|
||||
relativeRect.top,
|
||||
);
|
||||
adaptivePopSheetList.showDropdownMenu(context, offsetFromRect);
|
||||
};
|
||||
|
||||
return adaptivePopSheetList;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
@ -23,6 +24,17 @@ import 'package:spotube/provider/audio_player/state.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
final isBlacklistedProvider =
|
||||
Provider.autoDispose.family<bool, SpotubeTrackObject>(
|
||||
(ref, track) {
|
||||
ref.watch(blacklistProvider);
|
||||
final blacklist = ref.read(blacklistProvider.notifier);
|
||||
return blacklist.contains(track);
|
||||
},
|
||||
);
|
||||
|
||||
final _overlay = ValueNotifier<OverlayCompleter<dynamic>?>(null);
|
||||
|
||||
class TrackTile extends HookConsumerWidget {
|
||||
/// [index] will not be shown if null
|
||||
final int? index;
|
||||
@ -51,19 +63,35 @@ class TrackTile extends HookConsumerWidget {
|
||||
this.leadingActions,
|
||||
});
|
||||
|
||||
OverlayCompleter<dynamic> showOptions(
|
||||
BuildContext context,
|
||||
Offset offset,
|
||||
) {
|
||||
return showPopover(
|
||||
context: context,
|
||||
position: offset,
|
||||
alignment: Alignment.bottomRight,
|
||||
builder: (context) {
|
||||
return SizedBox(
|
||||
width: 220 * context.theme.scaling,
|
||||
child: Card(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TrackOptions(
|
||||
track: track,
|
||||
playlistId: playlistId,
|
||||
userPlaylist: userPlaylist,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final blacklist = ref.watch(blacklistProvider);
|
||||
final blacklistNotifier = ref.watch(blacklistProvider.notifier);
|
||||
|
||||
final isBlackListed = useMemoized(
|
||||
() => blacklistNotifier.contains(track),
|
||||
[blacklist, track],
|
||||
);
|
||||
|
||||
final showOptionCbRef = useRef<ValueChanged<RelativeRect>?>(null);
|
||||
final isBlackListed = ref.watch(isBlacklistedProvider(track));
|
||||
|
||||
final isLoading = useState(false);
|
||||
|
||||
@ -82,13 +110,13 @@ class TrackTile extends HookConsumerWidget {
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
if (event.buttons != kSecondaryMouseButton) return;
|
||||
showOptionCbRef.value?.call(
|
||||
RelativeRect.fromLTRB(
|
||||
event.position.dx,
|
||||
event.position.dy,
|
||||
constrains.maxWidth - event.position.dx,
|
||||
constrains.maxHeight - event.position.dy,
|
||||
),
|
||||
if (_overlay.value != null) {
|
||||
_overlay.value?.remove();
|
||||
_overlay.value = null;
|
||||
}
|
||||
_overlay.value = showOptions(
|
||||
context,
|
||||
Offset.zero,
|
||||
);
|
||||
},
|
||||
child: HoverBuilder(
|
||||
@ -303,11 +331,91 @@ class TrackTile extends HookConsumerWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
TrackOptions(
|
||||
track: track,
|
||||
playlistId: playlistId,
|
||||
userPlaylist: userPlaylist,
|
||||
showMenuCbRef: showOptionCbRef,
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return IconButton.ghost(
|
||||
icon: const Icon(SpotubeIcons.moreHorizontal),
|
||||
onPressed: () {
|
||||
final mediaQuery = MediaQuery.sizeOf(context);
|
||||
|
||||
if (mediaQuery.lgAndUp) {
|
||||
final renderBox =
|
||||
context.findRenderObject() as RenderBox;
|
||||
final position = RelativeRect.fromRect(
|
||||
Rect.fromPoints(
|
||||
renderBox.localToGlobal(Offset.zero,
|
||||
ancestor: context.findRenderObject()),
|
||||
renderBox.localToGlobal(
|
||||
renderBox.size.bottomRight(Offset.zero),
|
||||
ancestor: context.findRenderObject()),
|
||||
),
|
||||
Offset.zero & mediaQuery,
|
||||
);
|
||||
final offset = Offset(position.left, position.top);
|
||||
showOptions(context, offset);
|
||||
} else {
|
||||
openDrawer(
|
||||
context: context,
|
||||
position: OverlayPosition.bottom,
|
||||
draggable: true,
|
||||
showDragHandle: true,
|
||||
borderRadius: context.theme.borderRadiusMd,
|
||||
transformBackdrop: false,
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Basic(
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
context.theme.borderRadiusMd,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: imageProvider,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
track.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).semiBold(),
|
||||
subtitle: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ArtistLink(
|
||||
artists: track.artists,
|
||||
onOverflowArtistClick: () =>
|
||||
context.navigateTo(
|
||||
TrackRoute(trackId: track.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
TrackOptions(
|
||||
track: track,
|
||||
userPlaylist: userPlaylist,
|
||||
playlistId: playlistId,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (kIsDesktop) const Gap(10),
|
||||
],
|
||||
|
@ -2,19 +2,22 @@ import 'package:flutter/material.dart' as material;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/components/button/back_button.dart';
|
||||
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/modules/artist/artist_album_list.dart';
|
||||
|
||||
import 'package:spotube/pages/artist/section/footer.dart';
|
||||
import 'package:spotube/pages/artist/section/header.dart';
|
||||
// import 'package:spotube/pages/artist/section/related_artists.dart';
|
||||
import 'package:spotube/pages/artist/section/related_artists.dart';
|
||||
import 'package:spotube/pages/artist/section/top_tracks.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/artist/albums.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/artist/artist.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/artist/related.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/artist/top_tracks.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/artist/wikipedia.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/artists.dart';
|
||||
@ -48,7 +51,9 @@ class ArtistPage extends HookConsumerWidget {
|
||||
child: material.RefreshIndicator.adaptive(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(metadataPluginArtistProvider(artistId));
|
||||
// ref.invalidate(relatedArtistsProvider(artistId));
|
||||
ref.invalidate(
|
||||
metadataPluginArtistRelatedArtistsProvider(artistId),
|
||||
);
|
||||
ref.invalidate(metadataPluginArtistAlbumsProvider(artistId));
|
||||
ref.invalidate(metadataPluginIsSavedArtistProvider(artistId));
|
||||
ref.invalidate(metadataPluginArtistTopTracksProvider(artistId));
|
||||
@ -67,6 +72,7 @@ class ArtistPage extends HookConsumerWidget {
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
const SliverGap(material.kToolbarHeight),
|
||||
SliverToBoxAdapter(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
@ -77,16 +83,16 @@ class ArtistPage extends HookConsumerWidget {
|
||||
ArtistPageTopTracks(artistId: artistId),
|
||||
const SliverGap(20),
|
||||
SliverToBoxAdapter(child: ArtistAlbumList(artistId)),
|
||||
// SliverPadding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// sliver: SliverToBoxAdapter(
|
||||
// child: Text(
|
||||
// context.l10n.fans_also_like,
|
||||
// style: theme.typography.h4,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ArtistPageRelatedArtists(artistId: artistId),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
context.l10n.fans_also_like,
|
||||
style: context.theme.typography.h4,
|
||||
),
|
||||
),
|
||||
),
|
||||
ArtistPageRelatedArtists(artistId: artistId),
|
||||
const SliverGap(20),
|
||||
if (artistQuery.asData?.value != null)
|
||||
SliverToBoxAdapter(
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/modules/artist/artist_card.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/artist/related.dart';
|
||||
|
||||
@Deprecated("Related artists are no longer supported by Spotube")
|
||||
class ArtistPageRelatedArtists extends ConsumerWidget {
|
||||
final String artistId;
|
||||
const ArtistPageRelatedArtists({
|
||||
@ -11,39 +12,38 @@ class ArtistPageRelatedArtists extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
return const SizedBox.shrink();
|
||||
// final relatedArtists = ref.watch(relatedArtistsProvider(artistId));
|
||||
final relatedArtists =
|
||||
ref.watch(metadataPluginArtistRelatedArtistsProvider(artistId));
|
||||
|
||||
// return switch (relatedArtists) {
|
||||
// AsyncData(value: final artists) => SliverPadding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
// sliver: SliverGrid.builder(
|
||||
// itemCount: artists.length,
|
||||
// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
// maxCrossAxisExtent: 200,
|
||||
// mainAxisExtent: 250,
|
||||
// mainAxisSpacing: 10,
|
||||
// crossAxisSpacing: 10,
|
||||
// childAspectRatio: 0.8,
|
||||
// ),
|
||||
// itemBuilder: (context, index) {
|
||||
// final artist = artists.elementAt(index);
|
||||
// return SizedBox(
|
||||
// width: 180,
|
||||
// // child: ArtistCard(artist),
|
||||
// );
|
||||
// // return ArtistCard(artist);
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// AsyncError(:final error) => SliverToBoxAdapter(
|
||||
// child: Center(
|
||||
// child: Text(error.toString()),
|
||||
// ),
|
||||
// ),
|
||||
// _ => const SliverToBoxAdapter(
|
||||
// child: Center(child: CircularProgressIndicator()),
|
||||
// ),
|
||||
// };
|
||||
return switch (relatedArtists) {
|
||||
AsyncData(value: final artists) => SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
sliver: SliverGrid.builder(
|
||||
itemCount: artists.items.length,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: 250,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 0.8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final artist = artists.items.elementAt(index);
|
||||
return SizedBox(
|
||||
width: 180,
|
||||
child: ArtistCard(artist),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
AsyncError(:final error) => SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text(error.toString()),
|
||||
),
|
||||
),
|
||||
_ => const SliverToBoxAdapter(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
32
lib/provider/metadata_plugin/artist/related.dart
Normal file
32
lib/provider/metadata_plugin/artist/related.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
||||
|
||||
class MetadataPluginArtistRelatedArtistsNotifier
|
||||
extends FamilyPaginatedAsyncNotifier<SpotubeFullArtistObject, String> {
|
||||
@override
|
||||
Future<SpotubePaginationResponseObject<SpotubeFullArtistObject>> fetch(
|
||||
int offset,
|
||||
int limit,
|
||||
) async {
|
||||
return await (await metadataPlugin).artist.related(
|
||||
arg,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
build(arg) async {
|
||||
ref.watch(metadataPluginProvider);
|
||||
return await fetch(0, 20);
|
||||
}
|
||||
}
|
||||
|
||||
final metadataPluginArtistRelatedArtistsProvider = AsyncNotifierProviderFamily<
|
||||
MetadataPluginArtistRelatedArtistsNotifier,
|
||||
SpotubePaginationResponseObject<SpotubeFullArtistObject>,
|
||||
String>(
|
||||
() => MetadataPluginArtistRelatedArtistsNotifier(),
|
||||
);
|
313
lib/provider/track_options/track_options_provider.dart
Normal file
313
lib/provider/track_options/track_options_provider.dart
Normal file
@ -0,0 +1,313 @@
|
||||
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,
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue
Block a user