From 107d9ca007d6710bd9f499c4da0bdd8cabd256ec Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 2 Feb 2026 07:02:04 +0700 Subject: [PATCH] feat: export failed downloads to TXT file - Add Export button in queue when there are failed downloads - Add auto-export setting in Download Settings - Export includes track name, artist, Spotify/Deezer URL, and error message - Clear Failed action in snackbar after export --- CHANGELOG.md | 5 ++ lib/l10n/app_localizations.dart | 36 +++++++++ lib/l10n/app_localizations_de.dart | 20 +++++ lib/l10n/app_localizations_en.dart | 20 +++++ lib/l10n/app_localizations_es.dart | 20 +++++ lib/l10n/app_localizations_fr.dart | 20 +++++ lib/l10n/app_localizations_hi.dart | 20 +++++ lib/l10n/app_localizations_id.dart | 20 +++++ lib/l10n/app_localizations_ja.dart | 20 +++++ lib/l10n/app_localizations_ko.dart | 20 +++++ lib/l10n/app_localizations_nl.dart | 20 +++++ lib/l10n/app_localizations_pt.dart | 20 +++++ lib/l10n/app_localizations_ru.dart | 20 +++++ lib/l10n/app_localizations_tr.dart | 20 +++++ lib/l10n/app_localizations_zh.dart | 20 +++++ lib/l10n/arb/app_en.arb | 14 +++- lib/models/settings.dart | 4 + lib/models/settings.g.dart | 3 + lib/providers/download_queue_provider.dart | 75 ++++++++++++++++++- lib/providers/settings_provider.dart | 7 +- lib/screens/queue_tab.dart | 60 ++++++++++++++- .../settings/download_settings_page.dart | 22 +++++- 22 files changed, 479 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b9e732..2fff3c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Same as 3.3.1 but fixes crash issues caused by FFmpeg. +### Added + +- **Export Failed Downloads**: Export failed downloads to TXT file for easy lookup on other platforms +- **Auto-Export Setting**: Option to automatically export failed downloads when queue finishes + ### Fixed - **FFmpeg Crash**: Fixed crash issues during M4A to MP3/Opus conversion diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 4e1f9cdd..a69ed000 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3652,6 +3652,42 @@ abstract class AppLocalizations { /// **'Are you sure you want to clear all downloads?'** String get queueClearAllMessage; + /// Button - export failed downloads to TXT + /// + /// In en, this message translates to: + /// **'Export'** + String get queueExportFailed; + + /// Success message after exporting failed downloads + /// + /// In en, this message translates to: + /// **'Failed downloads exported to TXT file'** + String get queueExportFailedSuccess; + + /// Action to clear failed downloads after export + /// + /// In en, this message translates to: + /// **'Clear Failed'** + String get queueExportFailedClear; + + /// Error message when export fails + /// + /// In en, this message translates to: + /// **'Failed to export downloads'** + String get queueExportFailedError; + + /// Setting toggle for auto-export + /// + /// In en, this message translates to: + /// **'Auto-export failed downloads'** + String get settingsAutoExportFailed; + + /// Subtitle for auto-export setting + /// + /// In en, this message translates to: + /// **'Save failed downloads to TXT file automatically'** + String get settingsAutoExportFailedSubtitle; + /// Empty queue state title /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 2caf7511..f8d52093 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2005,6 +2005,26 @@ class AppLocalizationsDe extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f8e8bab4..f6ad0c59 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1990,6 +1990,26 @@ class AppLocalizationsEn extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index d535699e..19c5cc9e 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1990,6 +1990,26 @@ class AppLocalizationsEs extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f8a9dc92..a3842abb 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1990,6 +1990,26 @@ class AppLocalizationsFr extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index d3c940a3..c35c0698 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1990,6 +1990,26 @@ class AppLocalizationsHi extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 88b145ba..33a2bb57 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2003,6 +2003,26 @@ class AppLocalizationsId extends AppLocalizations { String get queueClearAllMessage => 'Apakah Anda yakin ingin menghapus semua unduhan?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'Tidak ada unduhan dalam antrian'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index ee68b1a4..68ef482a 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1977,6 +1977,26 @@ class AppLocalizationsJa extends AppLocalizations { @override String get queueClearAllMessage => 'すべてのダウンロードを消去してもよろしいですか?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'キューにダウンロードがありません'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index ec860eae..5c2d7441 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1990,6 +1990,26 @@ class AppLocalizationsKo extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 70fe43bf..dd263eb2 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1990,6 +1990,26 @@ class AppLocalizationsNl extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 407cf3cf..a4a5a947 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1990,6 +1990,26 @@ class AppLocalizationsPt extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ea4d5536..fb5e8cab 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2029,6 +2029,26 @@ class AppLocalizationsRu extends AppLocalizations { String get queueClearAllMessage => 'Вы уверены, что хотите очистить все загрузки?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'Нет загрузок в очереди'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index d77dd0cc..17130ffd 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2005,6 +2005,26 @@ class AppLocalizationsTr extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6e721398..33367ed3 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1990,6 +1990,26 @@ class AppLocalizationsZh extends AppLocalizations { String get queueClearAllMessage => 'Are you sure you want to clear all downloads?'; + @override + String get queueExportFailed => 'Export'; + + @override + String get queueExportFailedSuccess => + 'Failed downloads exported to TXT file'; + + @override + String get queueExportFailedClear => 'Clear Failed'; + + @override + String get queueExportFailedError => 'Failed to export downloads'; + + @override + String get settingsAutoExportFailed => 'Auto-export failed downloads'; + + @override + String get settingsAutoExportFailedSubtitle => + 'Save failed downloads to TXT file automatically'; + @override String get queueEmpty => 'No downloads in queue'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6b798025..0972cd49 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1460,10 +1460,22 @@ "queueTitle": "Download Queue", "@queueTitle": {"description": "Queue screen title"}, - "queueClearAll": "Clear All", +"queueClearAll": "Clear All", "@queueClearAll": {"description": "Button - clear all queue items"}, "queueClearAllMessage": "Are you sure you want to clear all downloads?", "@queueClearAllMessage": {"description": "Clear queue confirmation"}, + "queueExportFailed": "Export", + "@queueExportFailed": {"description": "Button - export failed downloads to TXT"}, + "queueExportFailedSuccess": "Failed downloads exported to TXT file", + "@queueExportFailedSuccess": {"description": "Success message after exporting failed downloads"}, + "queueExportFailedClear": "Clear Failed", + "@queueExportFailedClear": {"description": "Action to clear failed downloads after export"}, + "queueExportFailedError": "Failed to export downloads", + "@queueExportFailedError": {"description": "Error message when export fails"}, + "settingsAutoExportFailed": "Auto-export failed downloads", + "@settingsAutoExportFailed": {"description": "Setting toggle for auto-export"}, + "settingsAutoExportFailedSubtitle": "Save failed downloads to TXT file automatically", + "@settingsAutoExportFailedSubtitle": {"description": "Subtitle for auto-export setting"}, "queueEmpty": "No downloads in queue", "@queueEmpty": {"description": "Empty queue state title"}, "queueEmptySubtitle": "Add tracks from the home screen", diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 9da9871c..426c1b57 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -34,6 +34,7 @@ class AppSettings { final String lyricsMode; final String tidalHighFormat; // Format for Tidal HIGH quality: 'mp3_320' or 'opus_128' final bool useAllFilesAccess; // Android 13+ only: enable MANAGE_EXTERNAL_STORAGE + final bool autoExportFailedDownloads; // Auto export failed downloads to TXT file const AppSettings({ this.defaultService = 'tidal', @@ -66,6 +67,7 @@ class AppSettings { this.lyricsMode = 'embed', this.tidalHighFormat = 'mp3_320', this.useAllFilesAccess = false, + this.autoExportFailedDownloads = false, }); AppSettings copyWith({ @@ -100,6 +102,7 @@ class AppSettings { String? lyricsMode, String? tidalHighFormat, bool? useAllFilesAccess, + bool? autoExportFailedDownloads, }) { return AppSettings( defaultService: defaultService ?? this.defaultService, @@ -132,6 +135,7 @@ class AppSettings { lyricsMode: lyricsMode ?? this.lyricsMode, tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat, useAllFilesAccess: useAllFilesAccess ?? this.useAllFilesAccess, + autoExportFailedDownloads: autoExportFailedDownloads ?? this.autoExportFailedDownloads, ); } diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index cefd0cae..69da84bb 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -39,6 +39,8 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( lyricsMode: json['lyricsMode'] as String? ?? 'embed', tidalHighFormat: json['tidalHighFormat'] as String? ?? 'mp3_320', useAllFilesAccess: json['useAllFilesAccess'] as bool? ?? false, + autoExportFailedDownloads: + json['autoExportFailedDownloads'] as bool? ?? false, ); Map _$AppSettingsToJson(AppSettings instance) => @@ -73,4 +75,5 @@ Map _$AppSettingsToJson(AppSettings instance) => 'lyricsMode': instance.lyricsMode, 'tidalHighFormat': instance.tidalHighFormat, 'useAllFilesAccess': instance.useAllFilesAccess, + 'autoExportFailedDownloads': instance.autoExportFailedDownloads, }; diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 950aebbe..caac069e 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -1012,12 +1012,74 @@ class DownloadQueueNotifier extends Notifier { } } - void removeItem(String id) { +void removeItem(String id) { final items = state.items.where((item) => item.id != id).toList(); state = state.copyWith(items: items); _saveQueueToStorage(); } + /// Export failed downloads to a TXT file + /// Returns the file path if successful, null otherwise + Future exportFailedDownloads() async { + final failedItems = state.items + .where((item) => item.status == DownloadStatus.failed) + .toList(); + + if (failedItems.isEmpty) { + _log.d('No failed downloads to export'); + return null; + } + + try { + final buffer = StringBuffer(); + buffer.writeln('# SpotiFLAC Failed Downloads'); + buffer.writeln('# Exported: ${DateTime.now().toIso8601String()}'); + buffer.writeln('# Total: ${failedItems.length} tracks'); + buffer.writeln('#'); + buffer.writeln('# Format: Track - Artist | Spotify URL | Error'); + buffer.writeln(''); + + for (final item in failedItems) { + final track = item.track; + final spotifyUrl = track.id.startsWith('deezer:') + ? 'https://www.deezer.com/track/${track.id.substring(7)}' + : 'https://open.spotify.com/track/${track.id}'; + final error = item.error ?? 'Unknown error'; + buffer.writeln('${track.name} - ${track.artistName} | $spotifyUrl | $error'); + } + + // Save to download directory + String exportDir = state.outputDir; + if (exportDir.isEmpty) { + final dir = await getApplicationDocumentsDirectory(); + exportDir = dir.path; + } + + final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-').split('.').first; + final fileName = 'failed_downloads_$timestamp.txt'; + final filePath = '$exportDir/$fileName'; + + final file = File(filePath); + await file.writeAsString(buffer.toString()); + + _log.i('Exported ${failedItems.length} failed downloads to: $filePath'); + return filePath; + } catch (e) { + _log.e('Failed to export failed downloads: $e'); + return null; + } + } + + /// Clear all failed downloads from queue + void clearFailedDownloads() { + final items = state.items + .where((item) => item.status != DownloadStatus.failed) + .toList(); + state = state.copyWith(items: items); + _saveQueueToStorage(); + _log.d('Cleared failed downloads from queue'); + } + Future _runPostProcessingHooks(String filePath, Track track) async { try { final settings = ref.read(settingsProvider); @@ -1626,7 +1688,7 @@ if (state.outputDir.isEmpty) { _downloadCount = 0; } - _log.i( +_log.i( 'Queue stats - completed: $_completedInSession, failed: $_failedInSession, totalAtStart: $_totalQueuedAtStart', ); if (_totalQueuedAtStart > 0) { @@ -1634,6 +1696,15 @@ if (state.outputDir.isEmpty) { completedCount: _completedInSession, failedCount: _failedInSession, ); + + // Auto-export failed downloads if enabled + final settings = ref.read(settingsProvider); + if (settings.autoExportFailedDownloads && _failedInSession > 0) { + final exportPath = await exportFailedDownloads(); + if (exportPath != null) { + _log.i('Auto-exported failed downloads to: $exportPath'); + } + } } _log.i('Queue processing finished'); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index cebf0dd2..7e2ec0d0 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -236,10 +236,15 @@ class SettingsNotifier extends Notifier { _saveSettings(); } - void setUseAllFilesAccess(bool enabled) { +void setUseAllFilesAccess(bool enabled) { state = state.copyWith(useAllFilesAccess: enabled); _saveSettings(); } + + void setAutoExportFailedDownloads(bool enabled) { + state = state.copyWith(autoExportFailedDownloads: enabled); + _saveSettings(); + } } final settingsProvider = NotifierProvider( diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 0e3dcf50..c1e41c8d 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -806,7 +806,9 @@ final queueItems = ref.watch(downloadQueueProvider.select((s) => s.items)); fontWeight: FontWeight.bold, ), ), - const Spacer(), +const Spacer(), + _buildExportFailedButton(context, ref, colorScheme), + const SizedBox(width: 4), _buildPauseResumeButton(context, ref, colorScheme), const SizedBox(width: 4), _buildClearAllButton(context, ref, colorScheme), @@ -1194,7 +1196,7 @@ if (queueItems.isEmpty && ); } - Widget _buildClearAllButton( +Widget _buildClearAllButton( BuildContext context, WidgetRef ref, ColorScheme colorScheme, @@ -1210,6 +1212,60 @@ if (queueItems.isEmpty && ); } + Widget _buildExportFailedButton( + BuildContext context, + WidgetRef ref, + ColorScheme colorScheme, + ) { + final queueState = ref.watch(downloadQueueProvider); + final failedCount = queueState.failedCount; + + if (failedCount == 0) { + return const SizedBox.shrink(); + } + + return TextButton.icon( + onPressed: () => _exportFailedDownloads(context, ref), + icon: const Icon(Icons.file_download, size: 18), + label: Text(context.l10n.queueExportFailed), + style: TextButton.styleFrom( + visualDensity: VisualDensity.compact, + foregroundColor: colorScheme.tertiary, + ), + ); + } + + Future _exportFailedDownloads( + BuildContext context, + WidgetRef ref, + ) async { + final filePath = await ref.read(downloadQueueProvider.notifier).exportFailedDownloads(); + + if (!context.mounted) return; + + if (filePath != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.queueExportFailedSuccess), + action: SnackBarAction( + label: context.l10n.queueExportFailedClear, + onPressed: () { + ref.read(downloadQueueProvider.notifier).clearFailedDownloads(); + }, + ), + duration: const Duration(seconds: 5), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.queueExportFailedError), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } + Future _showClearAllDialog( BuildContext context, WidgetRef ref, diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index eb562bba..0d8c0a44 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -385,7 +385,27 @@ class _DownloadSettingsPageState extends ConsumerState { ), ), ), - ], +], + + const SliverToBoxAdapter(child: SizedBox(height: 16)), + + // Auto Export Failed Downloads + SliverToBoxAdapter( + child: SettingsGroup( + children: [ + SettingsSwitchItem( + icon: Icons.file_download_outlined, + title: context.l10n.settingsAutoExportFailed, + subtitle: context.l10n.settingsAutoExportFailedSubtitle, + value: settings.autoExportFailedDownloads, + onChanged: (value) { + ref.read(settingsProvider.notifier).setAutoExportFailedDownloads(value); + }, + showDivider: false, + ), + ], + ), + ), const SliverToBoxAdapter(child: SizedBox(height: 32)), ],