spotube/lib/provider/track_options/track_options_provider.dart
Kingkor Roy Tirtho 7037145519 feat: add ErrorBox and NoDefaultMetadataPlugin components
- Implemented ErrorBox for displaying error messages with retry functionality and log viewing.
- Created NoDefaultMetadataPlugin to inform users about missing default metadata providers and provide navigation to manage them.
2025-08-19 22:34:37 +06:00

312 lines
9.9 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/errors/exceptions.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();
}
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,
);
});