From 9da3607932ac4d2ed7ec74985e88cb4e30795e74 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 27 Jul 2026 01:24:14 +0700 Subject: [PATCH] feat(queue): long-press menu with move up/down for queued items --- lib/l10n/app_localizations.dart | 12 ++++++ lib/l10n/app_localizations_de.dart | 6 +++ lib/l10n/app_localizations_en.dart | 6 +++ lib/l10n/app_localizations_es.dart | 6 +++ lib/l10n/app_localizations_fr.dart | 6 +++ lib/l10n/app_localizations_id.dart | 6 +++ lib/l10n/app_localizations_ja.dart | 6 +++ lib/l10n/app_localizations_ko.dart | 6 +++ lib/l10n/app_localizations_pt.dart | 6 +++ lib/l10n/app_localizations_ru.dart | 6 +++ lib/l10n/app_localizations_tr.dart | 6 +++ lib/l10n/app_localizations_uk.dart | 6 +++ lib/l10n/arb/app_en.arb | 8 ++++ lib/providers/download_queue_provider.dart | 25 ++++++++++++ lib/screens/queue_tab_item_widgets.dart | 45 +++++++++++++++++++++- 15 files changed, 155 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a87c0981..5968b9c0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -6306,6 +6306,18 @@ abstract class AppLocalizations { /// **'Download next'** String get queueDownloadNext; + /// Queue item menu action - move the queued item one position earlier + /// + /// In en, this message translates to: + /// **'Move up'** + String get queueMoveUp; + + /// Queue item menu action - move the queued item one position later + /// + /// In en, this message translates to: + /// **'Move down'** + String get queueMoveDown; + /// Tag editor button that fills genre and album artist from MusicBrainz by ISRC /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ed35e7e2..e9af1365 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -3841,6 +3841,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 3ff91144..04e48e85 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3795,6 +3795,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 9bff40cc..894a26c4 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -3789,6 +3789,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 405a0c20..81c94e7b 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -3896,6 +3896,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 9036e7ff..63864856 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -3798,6 +3798,12 @@ class AppLocalizationsId extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 6903820d..c6af13a6 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -3784,6 +3784,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index e08bf5d8..c258d825 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -3685,6 +3685,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index dc142aa0..0dbf7153 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -3789,6 +3789,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 931e30a8..813e2050 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -3826,6 +3826,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 1b06a401..bd531119 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -3825,6 +3825,12 @@ class AppLocalizationsTr extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index cff78cd4..e7ea5f89 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -3843,6 +3843,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get queueMoveUp => 'Move up'; + + @override + String get queueMoveDown => 'Move down'; + @override String get editMetadataMusicBrainzButton => 'Fetch from MusicBrainz'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6b1f94f2..717b1917 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -4930,6 +4930,14 @@ "@queueDownloadNext": { "description": "Tooltip on a queued download row; moves the item to the front of the queue so the next free slot downloads it" }, + "queueMoveUp": "Move up", + "@queueMoveUp": { + "description": "Queue item menu action - move the queued item one position earlier" + }, + "queueMoveDown": "Move down", + "@queueMoveDown": { + "description": "Queue item menu action - move the queued item one position later" + }, "editMetadataMusicBrainzButton": "Fetch from MusicBrainz", "@editMetadataMusicBrainzButton": { "description": "Tag editor button that fills genre and album artist from MusicBrainz by ISRC" diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index cc104f5e..cdfec7ea 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1014,6 +1014,31 @@ class DownloadQueueNotifier extends Notifier { _saveQueueToStorage(); } + /// Moves a queued item [offset] positions up (negative) or down (positive) + /// among the queued items, leaving non-queued rows in place. Same native + /// worker caveat as [downloadNext]: an in-flight batch keeps its order. + void moveQueuedItem(String id, int offset) { + if (offset == 0) return; + final items = state.items; + final index = items.indexWhere((item) => item.id == id); + if (index == -1) return; + final item = items[index]; + if (item.status != DownloadStatus.queued) return; + final queuedIndices = [ + for (var i = 0; i < items.length; i++) + if (items[i].status == DownloadStatus.queued) i, + ]; + final position = queuedIndices.indexOf(index); + final targetPosition = position + offset; + if (targetPosition < 0 || targetPosition >= queuedIndices.length) return; + final reordered = List.from(items)..removeAt(index); + // Inserting at the target's pre-removal index lands the item directly + // before it when moving up and directly after it when moving down. + reordered.insert(queuedIndices[targetPosition], item); + state = state.copyWith(items: reordered); + _saveQueueToStorage(); + } + void removeItem(String id) { final removedItem = state.items.where((item) => item.id == id).firstOrNull; _locallyCancelledItemIds.remove(id); diff --git a/lib/screens/queue_tab_item_widgets.dart b/lib/screens/queue_tab_item_widgets.dart index 6b3f6a3e..5ebba720 100644 --- a/lib/screens/queue_tab_item_widgets.dart +++ b/lib/screens/queue_tab_item_widgets.dart @@ -194,9 +194,13 @@ extension _QueueTabItemWidgets on _QueueTabState { child: InkWell( onTap: isCompleted ? () => _navigateToMetadataScreen(item) - : item.status == DownloadStatus.failed + : item.status == DownloadStatus.failed || + item.status == DownloadStatus.skipped ? () => _showDownloadErrorDialog(context, item) : null, + onLongPress: item.status == DownloadStatus.queued + ? () => _showQueuedItemMenu(context, item) + : null, borderRadius: BorderRadius.circular(12), child: Stack( children: [ @@ -479,6 +483,45 @@ extension _QueueTabItemWidgets on _QueueTabState { ); } + Future _showQueuedItemMenu(BuildContext context, DownloadItem item) { + final notifier = ref.read(downloadQueueProvider.notifier); + return showModalBottomSheet( + context: context, + showDragHandle: true, + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.skip_next), + title: Text(context.l10n.queueDownloadNext), + onTap: () { + Navigator.of(ctx).pop(); + notifier.downloadNext(item.id); + }, + ), + ListTile( + leading: const Icon(Icons.arrow_upward), + title: Text(context.l10n.queueMoveUp), + onTap: () { + Navigator.of(ctx).pop(); + notifier.moveQueuedItem(item.id, -1); + }, + ), + ListTile( + leading: const Icon(Icons.arrow_downward), + title: Text(context.l10n.queueMoveDown), + onTap: () { + Navigator.of(ctx).pop(); + notifier.moveQueuedItem(item.id, 1); + }, + ), + ], + ), + ), + ); + } + Widget _buildActionButtons( BuildContext context, DownloadItem item,