diff --git a/lib/components/AlbumScreen/download_button.dart b/lib/components/AlbumScreen/download_button.dart index cba07060a..a077f91e0 100644 --- a/lib/components/AlbumScreen/download_button.dart +++ b/lib/components/AlbumScreen/download_button.dart @@ -10,6 +10,7 @@ import '../../models/jellyfin_models.dart'; import '../../models/finamp_models.dart'; import '../error_snackbar.dart'; import 'download_dialog.dart'; +import '../confirmation_prompt_dialog.dart'; class DownloadButton extends StatefulWidget { const DownloadButton({ @@ -57,25 +58,43 @@ class _DownloadButtonState extends State { // If offline, we don't allow the user to delete items. // If we did, we'd have to implement listeners for MusicScreenTabView so that the user can't delete a parent, go back, and select the same parent. // If they did, AlbumScreen would show an error since the item no longer exists. - // Also, the user could delete the parent and immediately redownload it, which will either cause unwanted network usage or cause more errors becuase the user is offline. + // Also, the user could delete the parent and immediately redownload it, which will either cause unwanted network usage or cause more errors because the user is offline. onPressed: isOffline ?? false ? null : () { if (isDownloaded) { - _downloadsHelper - .deleteDownloads( - jellyfinItemIds: widget.items.map((e) => e.id).toList(), - deletedFor: widget.parent.id, - ) - .then((_) { - checkIfDownloaded(); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - AppLocalizations.of(context)!.downloadsDeleted), - )); - }, - onError: (error, stackTrace) => - errorSnackbar(error, context)); + showDialog( + context: context, + builder: (context) => ConfirmationPromptDialog( + promptText: AppLocalizations.of(context)! + .deleteDownloadsPrompt( + widget.parent.name ?? "", + widget.parent.type == "Playlist" + ? "playlist" + : "album"), + confirmButtonText: AppLocalizations.of(context)! + .deleteDownloadsConfirmButtonText, + abortButtonText: AppLocalizations.of(context)! + .deleteDownloadsAbortButtonText, + onConfirmed: () async { + final messenger = ScaffoldMessenger.of(context); + try { + await _downloadsHelper.deleteDownloads( + jellyfinItemIds: + widget.items.map((e) => e.id).toList(), + deletedFor: widget.parent.id); + checkIfDownloaded(); + messenger.showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)! + .downloadsDeleted))); + } catch (error) { + errorSnackbar(error, context); + } + }, + onAborted: () {}, + ), + ); + // .whenComplete(() => checkIfDownloaded()); } else { if (FinampSettingsHelper .finampSettings.downloadLocationsMap.length == diff --git a/lib/components/ArtistScreen/artist_download_button.dart b/lib/components/ArtistScreen/artist_download_button.dart index ce701863f..e30f376a9 100644 --- a/lib/components/ArtistScreen/artist_download_button.dart +++ b/lib/components/ArtistScreen/artist_download_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; @@ -9,6 +10,7 @@ import '../../services/finamp_user_helper.dart'; import '../../services/jellyfin_api_helper.dart'; import '../../services/downloads_helper.dart'; import '../AlbumScreen/download_dialog.dart'; +import '../confirmation_prompt_dialog.dart'; import '../error_snackbar.dart'; class ArtistDownloadButton extends StatefulWidget { @@ -46,6 +48,7 @@ class _ArtistDownloadButtonState extends State { valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (context, box, _) { final isOffline = box.get("FinampSettings")?.isOffline ?? false; + bool deleteAlbums = false; if (isOffline) { return _disabledButton; @@ -62,28 +65,51 @@ class _ArtistDownloadButtonState extends State { if (snapshot.hasData) { final undownloadedAlbums = _getUndownloadedAlbums(snapshot.data!); + deleteAlbums = undownloadedAlbums.isEmpty; return IconButton( - icon: undownloadedAlbums.isEmpty + icon: deleteAlbums ? const Icon(Icons.delete) : const Icon(Icons.download), - onPressed: () async { - if (undownloadedAlbums.isEmpty) { - final deleteFutures = snapshot.data!.map((e) => - _downloadsHelper.deleteDownloads( - jellyfinItemIds: _downloadsHelper - .getDownloadedParent(e.id)! - .downloadedChildren - .keys - .toList(), - deletedFor: e.id)); - Future.wait(deleteFutures).then((_) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Downloads deleted."))); - }, - onError: (error, stackTrace) => - errorSnackbar(error, context)); + onPressed:() async { + if (deleteAlbums) { + showDialog( + context: context, + builder: (context) => ConfirmationPromptDialog( + promptText: AppLocalizations.of(context)! + .deleteDownloadsPrompt( + widget.artist.name ?? "", + widget.artist.type == "MusicArtist" ? "artist" : "genre"), + confirmButtonText: AppLocalizations.of(context)! + .deleteDownloadsConfirmButtonText, + abortButtonText: AppLocalizations.of(context)! + .deleteDownloadsAbortButtonText, + onConfirmed: () async { + try { + final deleteFutures = snapshot.data!.map((e) => + _downloadsHelper.deleteDownloads( + jellyfinItemIds: _downloadsHelper + .getDownloadedParent(e.id)! + .downloadedChildren + .keys + .toList(), + deletedFor: e.id)); + await Future.wait(deleteFutures); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Downloads deleted."))); + final undownloadedAlbums = + _getUndownloadedAlbums(snapshot.data!); + setState(() { + deleteAlbums = undownloadedAlbums.isEmpty; + }); + } catch (error) { + errorSnackbar(error, context); + } + }, + onAborted: () {}, + ), + ); } else { List?>> albumInfoFutures = []; for (var element in undownloadedAlbums) { diff --git a/lib/components/DownloadsScreen/downloaded_albums_list.dart b/lib/components/DownloadsScreen/downloaded_albums_list.dart index 4263865dd..9922386f8 100644 --- a/lib/components/DownloadsScreen/downloaded_albums_list.dart +++ b/lib/components/DownloadsScreen/downloaded_albums_list.dart @@ -1,4 +1,5 @@ import 'package:finamp/services/jellyfin_api_helper.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; @@ -6,6 +7,7 @@ import '../../models/finamp_models.dart'; import '../../services/downloads_helper.dart'; import '../../models/jellyfin_models.dart'; import '../album_image.dart'; +import '../confirmation_prompt_dialog.dart'; import 'item_media_source_info.dart'; import 'album_file_size.dart'; @@ -46,10 +48,27 @@ class _DownloadedAlbumsListState extends State { title: Text(album.item.name ?? "Unknown Name"), trailing: IconButton( icon: const Icon(Icons.delete), - onPressed: () async { - await deleteAlbum(context, album); - setState(() {}); - }), + onPressed: () => showDialog( + context: context, + builder: (context) => ConfirmationPromptDialog( + promptText: AppLocalizations.of(context)! + .deleteDownloadsPrompt( + album.item.name ?? "", + album.item.type == "Playlist" + ? "playlist" + : "album"), + confirmButtonText: AppLocalizations.of(context)! + .deleteDownloadsConfirmButtonText, + abortButtonText: AppLocalizations.of(context)! + .deleteDownloadsAbortButtonText, + onConfirmed: () async { + await deleteAlbum(context, album); + setState(() {}); + }, + onAborted: () {}, + ), + ), + ), subtitle: AlbumFileSize( downloadedParent: album, ), @@ -87,16 +106,32 @@ class _DownloadedSongsInAlbumListState @override Widget build(BuildContext context) { return Column(children: [ + //TODO use a list builder here for (final song in widget.children) ListTile( title: Text(song.name ?? "Unknown Name"), leading: AlbumImage(item: song), trailing: IconButton( icon: const Icon(Icons.delete), - onPressed: () async { - await deleteSong(context, song); - setState(() {}); - }), + onPressed: () => showDialog( + context: context, + builder: (context) => ConfirmationPromptDialog( + promptText: AppLocalizations.of(context)! + .deleteDownloadsPrompt( + song.name ?? "", + "track"), + confirmButtonText: AppLocalizations.of(context)! + .deleteDownloadsConfirmButtonText, + abortButtonText: AppLocalizations.of(context)! + .deleteDownloadsAbortButtonText, + onConfirmed: () async { + await deleteSong(context, song); + setState(() {}); + }, + onAborted: () {}, + ), + ), + ), subtitle: ItemMediaSourceInfo( songId: song.id, ), diff --git a/lib/components/confirmation_prompt_dialog.dart b/lib/components/confirmation_prompt_dialog.dart new file mode 100644 index 000000000..e1b40888b --- /dev/null +++ b/lib/components/confirmation_prompt_dialog.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; + +import '../models/jellyfin_models.dart'; +import '../services/downloads_helper.dart'; +import 'error_snackbar.dart'; + +class ConfirmationPromptDialog extends AlertDialog { + const ConfirmationPromptDialog({ + Key? key, + required this.promptText, + required this.confirmButtonText, + required this.abortButtonText, + required this.onConfirmed, + required this.onAborted, + }) : super(key: key); + + final String promptText; + final String confirmButtonText; + final String abortButtonText; + final void Function()? onConfirmed; + final void Function()? onAborted; + + + @override + Widget build(BuildContext context) { + return AlertDialog( + buttonPadding: const EdgeInsets.all(0.0), + contentPadding: const EdgeInsets.all(0.0), + insetPadding: const EdgeInsets.all(32.0), + actionsPadding: const EdgeInsets.all(0.0), + actionsAlignment: MainAxisAlignment.spaceAround, + actionsOverflowAlignment: OverflowBarAlignment.center, + actionsOverflowDirection: VerticalDirection.up, + title: Text( + promptText, + style: const TextStyle(fontSize: 18), + ), + actions: [ + Container( + constraints: const BoxConstraints( + maxWidth: 150.0, + ), + child: TextButton( + child: Text(abortButtonText, + textAlign: TextAlign.center, + ), + onPressed: () { + Navigator.of(context).pop(); + onAborted?.call(); + }, + ), + ), + Container( + constraints: const BoxConstraints( + maxWidth: 150.0, + ), + child: TextButton( + child: Text(confirmButtonText, + textAlign: TextAlign.center, + softWrap: true, + ), + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + onConfirmed?.call(); + }, + ), + ), + ], + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6d3bbeaa2..1a4f73a80 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -202,10 +202,29 @@ "@downloadErrorsTitle": {}, "noErrors": "No errors!", "@noErrors": {}, - "errorScreenError": "An error occured while getting the list of errors! At this point, you should probably just create an issue on GitHub and delete app data", + "errorScreenError": "An error occurred while getting the list of errors! At this point, you should probably just create an issue on GitHub and delete app data", "@errorScreenError": {}, "failedToGetSongFromDownloadId": "Failed to get song from download ID", "@failedToGetSongFromDownloadId": {}, + "deleteDownloadsPrompt": "Are you sure you want to delete the {itemType, select, album{album} playlist{playlist} artist{artist} genre{genre} track{song} other{}} '{itemName}' from this device?", + "@deleteDownloadsPrompt": { + "placeholders": { + "itemName": { + "type": "String", + "example": "Abandon Ship" + }, + "itemType": { + "type": "String", + "example": "album" + } + }, + "description": "Confirmation prompt shown before deleting downloaded media from the local device, destructive action, doesn't affect the media on the server." + }, + "deleteDownloadsConfirmButtonText": "Delete", + "@deleteDownloadsConfirmButtonText": { + "description": "Shown in the confirmation dialog for deleting downloaded media from the local device." + }, + "deleteDownloadsAbortButtonText": "Cancel", "error": "Error", "@error": {}, "discNumber": "Disc {number}", @@ -502,4 +521,4 @@ }, "noMusicLibrariesBody": "Finamp could not find any music libraries. Please ensure that your Jellyfin server contains at least one library with the content type set to \"Music\".", "refresh": "REFRESH" -} \ No newline at end of file +} diff --git a/lib/services/downloads_helper.dart b/lib/services/downloads_helper.dart index de467caae..54fb5cb2b 100644 --- a/lib/services/downloads_helper.dart +++ b/lib/services/downloads_helper.dart @@ -288,15 +288,15 @@ class DownloadsHelper { shouldDeleteContent: true, )); - _downloadedItemsBox.delete(jellyfinItemId); - _downloadIdsBox.delete(downloadedSong.downloadId); + await _downloadedItemsBox.delete(jellyfinItemId); + await _downloadIdsBox.delete(downloadedSong.downloadId); if (deletedFor != null) { DownloadedParent? downloadedAlbumTemp = _downloadedParentsBox.get(deletedFor); if (downloadedAlbumTemp != null) { downloadedAlbumTemp.downloadedChildren.remove(jellyfinItemId); - _downloadedParentsBox.put(deletedFor, downloadedAlbumTemp); + await _downloadedParentsBox.put(deletedFor, downloadedAlbumTemp); } downloadedImage?.requiredBy.remove(deletedFor); @@ -346,12 +346,12 @@ class DownloadsHelper { // Loop through each directory and check if it's empty. If it is, delete the directory. if (await element.list().isEmpty) { _downloadsLogger.info("${element.path} is empty, deleting"); - element.delete(); + await element.delete(); } } if (deletedFor != null) { - _downloadedParentsBox.delete(deletedFor); + await _downloadedParentsBox.delete(deletedFor); } } catch (e) { _downloadsLogger.severe(e);