Skip to content

Commit

Permalink
Delete from server button (#1004)
Browse files Browse the repository at this point in the history
* changes due to flutter pub upgrade --major-version

* Delete Button

* local delete (WIP)

* adaptive delete button for every option

* reverted back to two separate buttons

* added a new option to settings

* removed old code

* add delete from server buer to album and playlist context menu

* Added translation tokens

* delete confirm dialog for album&playlist; generalized prompts and code

* formatting

* removed now unused translations (reason: two commits before)

* delete menu inside album/playlist view

* Refactored a bit to use the new reusable delete dialogs

* removed translations for now unused strings (reason: one commit before)

* formatting

* moved canDeleteChecker to JellyfinApiHelper

* moved widget to their own file

* Changed can DeleteFromServer Function to instantly return both a initial temporary value and a Future to allow usage of cached canDelete value before the server sends final value

* format and remove now unused function

* renamed file

* automatic refresh

* attempt to pop Navigator

* Navigator pop (WIP)

* Navigator pop (WIP 2)

* Navigator pop (WIP 3)

* pop until not in a dialog or the album/playlist screen anymore

* rename server delete prompt function

* also apply pop fix to local delete prompt

* capitalization

* use enum for DeleteType

* fix problem from githubs conflict resolver via code generation

* change HiveField to 80 instead of 90 to follow correct convention

* add delay before list refresh to avoid item still being listed after delete

* removed unused function

* refresh downloads list after delete

* refresh (track list of) album after deleting a track

technically also refreshes the album if the delete dialog was canceled, but this has no effect and there was no easy way to avoid this

* refresh album lists after deleting album (both from album list or album screen)

* fix navigating to album screen after deleting track in playlist

* fixed problems from the issue resolving

* updated CONTRIBUTING.md

* removed unused file

* move delete setting to better location, refresh music tab when changing setting

---------

Co-authored-by: F-4Dev <[email protected]>
Co-authored-by: Chaphasilor <[email protected]>
  • Loading branch information
3 people authored Feb 25, 2025
1 parent 5643363 commit d7492ba
Show file tree
Hide file tree
Showing 40 changed files with 497 additions and 547 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Because Dart doesn't support macros and stuff, a few dependencies rely on code g
* Chopper - For talking to Jellyfin over HTTP
* This layer (`lib/services/jellyfin_api.dart`) is not used by the app directly. The user-facing API is located at `lib/services/jellyfin_api_helper.dart`.

To rebuild these files, run `dart run build_runner build --delete-conflicting-outputs`. This must be done when:
To rebuild these files, run `dart run build_runner build --delete-conflicting-outputs`. If you cant run (`flutter run`) finamp after generating code you may need to run `flutter clean` before running the builder. This must be done when:

* Modifying a class that is returned by Jellyfin (such as the classes in `lib/models/jellyfin_models.dart`)
* Adding fields to a database class (annotated with `@HiveType`)
Expand Down Expand Up @@ -56,4 +56,4 @@ Linux packaging might vary depending on distribution and type of installation.
Please follow the guidelines of your distribution if you would like to package Finamp for it.
The repo contains a [desktop file template](assets/finamp.desktop.m4) and
pre-generated icons following the XDG Icon Theme Specification
in the [assets folder](assets/icon/linux).
in the [assets folder](assets/icon/linux).
108 changes: 102 additions & 6 deletions lib/components/AlbumScreen/album_screen_content.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import 'package:finamp/components/MusicScreen/music_screen_tab_view.dart';
import 'package:finamp/components/delete_prompts.dart';
import 'package:finamp/services/downloads_service.dart';
import 'package:finamp/services/jellyfin_api_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
import 'package:get_it/get_it.dart';

import '../../components/favourite_button.dart';
import '../../models/finamp_models.dart';
Expand All @@ -15,12 +21,11 @@ import 'track_list_tile.dart';
typedef BaseItemDtoCallback = void Function(BaseItemDto item);

class AlbumScreenContent extends StatefulWidget {
const AlbumScreenContent({
super.key,
required this.parent,
required this.displayChildren,
required this.queueChildren,
});
const AlbumScreenContent(
{super.key,
required this.parent,
required this.displayChildren,
required this.queueChildren});

final BaseItemDto parent;
final List<BaseItemDto> displayChildren;
Expand All @@ -31,8 +36,26 @@ class AlbumScreenContent extends StatefulWidget {
}

class _AlbumScreenContentState extends State<AlbumScreenContent> {
final downloadsService = GetIt.instance<DownloadsService>();
bool canDeleteFromServer = false;
bool canDeleteGotUpdated = false;

@override
Widget build(BuildContext context) {
final downloadStub = DownloadStub.fromItem(
type: DownloadItemType.collection, item: widget.parent);
final downloadStatus = downloadsService.getStatus(downloadStub, null);

if (!canDeleteGotUpdated) {
canDeleteGotUpdated = true;
var deletable = GetIt.instance<JellyfinApiHelper>()
.canDeleteFromServer(widget.parent);
canDeleteFromServer = deletable.initialValue;
deletable.realValue?.then((canDelete) => setState(() {
canDeleteFromServer = canDelete;
}));
}

void onDelete(BaseItemDto item) {
// This is pretty inefficient (has to search through whole list) but
// SongsSliverList gets passed some weird split version of children to
Expand Down Expand Up @@ -83,6 +106,79 @@ class _AlbumScreenContentState extends State<AlbumScreenContent> {
!FinampSettingsHelper.finampSettings.isOffline)
PlaylistNameEditButton(playlist: widget.parent),
FavoriteButton(item: widget.parent),
if (downloadStatus == DownloadItemStatus.notNeeded)
DownloadButton(
item: DownloadStub.fromItem(
type: DownloadItemType.collection, item: widget.parent),
children: widget.displayChildren),
downloadStatus.isRequired && canDeleteFromServer
? PopupMenuButton<Null>(
enableFeedback: true,
icon: const Icon(TablerIcons.dots_vertical),
onOpened: () => {},
itemBuilder: (context) {
return [
PopupMenuItem(
value: null,
child: ListTile(
leading: Icon(Icons.delete_outline),
title: Text(AppLocalizations.of(context)!
.deleteFromTargetConfirmButton("")),
enabled: true,
onTap: () async {
await askBeforeDeleteDownloadFromDevice(
context, downloadStub);
Navigator.of(context).pop();
})),
PopupMenuItem(
value: null,
child: ListTile(
leading: Icon(Icons.delete_forever),
title: Text(AppLocalizations.of(context)!
.deleteFromTargetConfirmButton("server")),
enabled: true,
onTap: () async {
await askBeforeDeleteFromServerAndDevice(
context, downloadStub,
popIt: true,
refresh: () => musicScreenRefreshStream.add(
null)); // trigger a refresh of the music screen
}))
];
},
)
: downloadStatus.isRequired
? IconButton(
icon: const Icon(Icons.delete),
tooltip: AppLocalizations.of(context)!
.deleteFromTargetConfirmButton("device"),
// 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 because the user is offline.
onPressed: () {
askBeforeDeleteDownloadFromDevice(
context, downloadStub,
refresh: () => musicScreenRefreshStream.add(
null)); // trigger a refresh of the music screen

// .whenComplete(() => checkIfDownloaded());
},
)
: canDeleteFromServer
? IconButton(
icon: const Icon(Icons.delete_forever),
tooltip: AppLocalizations.of(context)!
.deleteFromTargetConfirmButton("server"),
onPressed: () {
askBeforeDeleteFromServerAndDevice(
context, downloadStub,
popIt: true,
refresh: () => musicScreenRefreshStream.add(
null)); // trigger a refresh of the music screen
},
)
: Visibility(visible: false, child: Text("")),
DownloadButton(
item: DownloadStub.fromItem(
type: DownloadItemType.collection, item: widget.parent),
Expand Down
24 changes: 3 additions & 21 deletions lib/components/AlbumScreen/download_button.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:finamp/components/delete_prompts.dart';
import 'package:finamp/services/finamp_settings_helper.dart';
import 'package:finamp/services/finamp_user_helper.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -90,32 +91,13 @@ class DownloadButton extends ConsumerWidget {
);
var deleteButton = IconButton(
icon: const Icon(Icons.delete),
tooltip: AppLocalizations.of(context)!.deleteItem,
tooltip: AppLocalizations.of(context)!.deleteFromTargetConfirmButton(""),
// 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 because the user is offline.
onPressed: () {
showDialog(
context: context,
builder: (context) => ConfirmationPromptDialog(
promptText: AppLocalizations.of(context)!.deleteDownloadsPrompt(
item.baseItem?.name ?? "", item.baseItemType.name),
confirmButtonText:
AppLocalizations.of(context)!.deleteDownloadsConfirmButtonText,
abortButtonText: AppLocalizations.of(context)!.genericCancel,
onConfirmed: () async {
try {
await downloadsService.deleteDownload(stub: item);
GlobalSnackbar.message((scaffold) =>
AppLocalizations.of(scaffold)!.downloadsDeleted);
} catch (error) {
GlobalSnackbar.error(error);
}
},
onAborted: () {},
),
);
askBeforeDeleteDownloadFromDevice(context, item);
// .whenComplete(() => checkIfDownloaded());
},
);
Expand Down
78 changes: 60 additions & 18 deletions lib/components/AlbumScreen/song_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:finamp/components/AlbumScreen/speed_menu.dart';
import 'package:finamp/components/PlayerScreen/queue_list.dart';
import 'package:finamp/components/PlayerScreen/sleep_timer_cancel_dialog.dart';
import 'package:finamp/components/PlayerScreen/sleep_timer_dialog.dart';
import 'package:finamp/components/delete_prompts.dart';
import 'package:finamp/components/themed_bottom_sheet.dart';
import 'package:finamp/models/finamp_models.dart';
import 'package:finamp/screens/artist_screen.dart';
Expand Down Expand Up @@ -132,13 +133,17 @@ class _SongMenuState extends ConsumerState<SongMenu> {
// Makes sure that widget doesn't just disappear after press while menu is visible
bool speedWidgetWasVisible = false;
bool showSpeedMenu = false;
bool canDeleteFromServer = false;
bool deletableGotUpdated = false;

double initialSheetExtent = 0.0;
double inputStep = 0.9;
double oldExtent = 0.0;

@override
void initState() {
super.initState();

initialSheetExtent = widget.showPlaybackControls ? 0.6 : 0.45;
oldExtent = initialSheetExtent;
}
Expand Down Expand Up @@ -200,7 +205,17 @@ class _SongMenuState extends ConsumerState<SongMenu> {
switch (element) { Visibility e => e.visible, _ => true })
.length *
56;

return Consumer(builder: (context, ref, child) {
if (!deletableGotUpdated) {
deletableGotUpdated = true;
var deletable = GetIt.instance<JellyfinApiHelper>()
.canDeleteFromServer(widget.item);
canDeleteFromServer = deletable.initialValue;
deletable.realValue?.then((canDelete) => setState(() {
canDeleteFromServer = canDelete;
}));
}
final metadata = ref.watch(currentTrackMetadataProvider).unwrapPrevious();
return widget.childBuilder(
stackHeight, menu(context, menuEntries, metadata.value));
Expand Down Expand Up @@ -384,24 +399,20 @@ class _SongMenuState extends ConsumerState<SongMenu> {
),
),
Visibility(
visible: downloadStatus.isRequired,
child: ListTile(
leading: Icon(
Icons.delete_outlined,
color: iconColor,
),
title: Text(AppLocalizations.of(context)!.deleteItem),
enabled: downloadStatus.isRequired,
onTap: () async {
var item = DownloadStub.fromItem(
type: DownloadItemType.song, item: widget.item);
unawaited(downloadsService.deleteDownload(stub: item));
if (mounted) {
Navigator.pop(context);
}
},
),
),
visible: downloadStatus.isRequired,
child: ListTile(
leading: Icon(
Icons.delete_outlined,
color: iconColor,
),
title: Text(AppLocalizations.of(context)!
.deleteFromTargetConfirmButton("")),
enabled: downloadStatus.isRequired,
onTap: () async {
var item = DownloadStub.fromItem(
type: DownloadItemType.song, item: widget.item);
await askBeforeDeleteDownloadFromDevice(context, item);
})),
Visibility(
visible: downloadStatus == DownloadItemStatus.notNeeded,
child: ListTile(
Expand Down Expand Up @@ -575,6 +586,37 @@ class _SongMenuState extends ConsumerState<SongMenu> {
},
),
),
Visibility(
visible: canDeleteFromServer,
child: ListTile(
leading: Icon(
Icons.delete_forever,
color: iconColor,
),
title: Text(AppLocalizations.of(context)!
.deleteFromTargetConfirmButton("server")),
enabled: canDeleteFromServer,
onTap: () async {
var item = DownloadStub.fromItem(
type: DownloadItemType.song, item: widget.item);
await askBeforeDeleteFromServerAndDevice(context, item);
final BaseItemDto newAlbumOrPlaylist =
await _jellyfinApiHelper.getItemById(widget.parentItem!.id);
if (context.mounted) {
Navigator.pop(context); // close dialog
// pop current album screen and reload with new album data
Navigator.of(context).popUntil((route) {
return route.settings.name != null // unnamed dialog
&&
route.settings.name !=
AlbumScreen.routeName; // albums screen
});
await Navigator.of(context)
.pushNamed(AlbumScreen.routeName,
arguments: newAlbumOrPlaylist);
}
},
)),
];
}

Expand Down
29 changes: 4 additions & 25 deletions lib/components/DownloadsScreen/downloaded_items_list.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import 'package:finamp/components/delete_prompts.dart';
import 'package:finamp/services/finamp_settings_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';

import '../../models/finamp_models.dart';
import '../../services/downloads_service.dart';
import '../album_image.dart';
import '../confirmation_prompt_dialog.dart';
import 'item_file_size.dart';

class DownloadedItemsList extends StatefulWidget {
Expand Down Expand Up @@ -49,29 +48,9 @@ class _DownloadedItemsListState extends State<DownloadedItemsList> {
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => showDialog(
context: context,
builder: (context) => ConfirmationPromptDialog(
promptText:
AppLocalizations.of(context)!.deleteDownloadsPrompt(
album.name,
album.baseItemType.idString ?? "",
),
confirmButtonText: AppLocalizations.of(context)!
.deleteDownloadsConfirmButtonText,
abortButtonText: AppLocalizations.of(context)!
.genericCancel,
onConfirmed: () async {
await downloadsService.deleteDownload(stub: album);
if (mounted) {
setState(() {});
}
},
onAborted: () {},
),
),
),
icon: const Icon(Icons.delete),
onPressed: () =>
askBeforeDeleteDownloadFromDevice(context, album, refresh: () {setState(() {});})),
],
),
subtitle: ItemFileSize(
Expand Down
Loading

0 comments on commit d7492ba

Please sign in to comment.