Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add confirmation dialog before deleting downloaded album #197

Merged
merged 16 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions lib/components/AlbumScreen/download_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -57,25 +58,43 @@ class _DownloadButtonState extends State<DownloadButton> {
// 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 ==
Expand Down
62 changes: 44 additions & 18 deletions lib/components/ArtistScreen/artist_download_button.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 {
Expand Down Expand Up @@ -46,6 +48,7 @@ class _ArtistDownloadButtonState extends State<ArtistDownloadButton> {
valueListenable: FinampSettingsHelper.finampSettingsListener,
builder: (context, box, _) {
final isOffline = box.get("FinampSettings")?.isOffline ?? false;
bool deleteAlbums = false;

if (isOffline) {
return _disabledButton;
Expand All @@ -62,28 +65,51 @@ class _ArtistDownloadButtonState extends State<ArtistDownloadButton> {
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<Future<List<BaseItemDto>?>> albumInfoFutures = [];
for (var element in undownloadedAlbums) {
Expand Down
51 changes: 43 additions & 8 deletions lib/components/DownloadsScreen/downloaded_albums_list.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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';

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';

Expand Down Expand Up @@ -46,10 +48,27 @@ class _DownloadedAlbumsListState extends State<DownloadedAlbumsList> {
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,
),
Expand Down Expand Up @@ -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,
),
Expand Down
72 changes: 72 additions & 0 deletions lib/components/confirmation_prompt_dialog.dart
Original file line number Diff line number Diff line change
@@ -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();
},
),
),
],
);
}
}
23 changes: 21 additions & 2 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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"
}
}
10 changes: 5 additions & 5 deletions lib/services/downloads_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Loading