From 11fb4365a233fce16ab5e29243449f255bb58b25 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 26 Jul 2026 19:00:49 +0700 Subject: [PATCH] feat(queue): offer one-tap retry when connectivity returns The connectivity listener now stays alive while network-failed items remain and, on reconnect, shows a debounced snackbar counting them with a Retry action wired to retryAllFailed scoped to network failures. Also regenerates localizations for the recent feature strings. --- lib/l10n/app_localizations.dart | 72 ++++++++++++++++++++-- lib/l10n/app_localizations_de.dart | 43 ++++++++++++- lib/l10n/app_localizations_en.dart | 43 ++++++++++++- lib/l10n/app_localizations_es.dart | 46 ++++++++++++-- lib/l10n/app_localizations_fr.dart | 43 ++++++++++++- lib/l10n/app_localizations_id.dart | 43 ++++++++++++- lib/l10n/app_localizations_ja.dart | 43 ++++++++++++- lib/l10n/app_localizations_ko.dart | 43 ++++++++++++- lib/l10n/app_localizations_pt.dart | 46 ++++++++++++-- lib/l10n/app_localizations_ru.dart | 43 ++++++++++++- lib/l10n/app_localizations_tr.dart | 43 ++++++++++++- lib/l10n/app_localizations_uk.dart | 43 ++++++++++++- lib/l10n/arb/app_en.arb | 21 +++++++ lib/providers/download_queue_provider.dart | 62 +++++++++++++++++-- 14 files changed, 584 insertions(+), 50 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8d73eb5e..c1acf0f4 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1270,6 +1270,48 @@ abstract class AppLocalizations { /// **'No platform links found for this track.'** String get trackOpenOnNoLinks; + /// Library settings row opening the duplicate review sheet + /// + /// In en, this message translates to: + /// **'Review duplicates'** + String get libraryReviewDuplicates; + + /// Subtitle for the duplicate review settings row + /// + /// In en, this message translates to: + /// **'Find tracks stored more than once'** + String get libraryReviewDuplicatesSubtitle; + + /// Title of the duplicate review sheet + /// + /// In en, this message translates to: + /// **'Duplicates'** + String get duplicatesTitle; + + /// Shown when the duplicate review sheet finds nothing + /// + /// In en, this message translates to: + /// **'No duplicate tracks found.'** + String get duplicatesEmpty; + + /// Button that deletes all but the highest-quality copy in a duplicate group + /// + /// In en, this message translates to: + /// **'Keep best'** + String get duplicatesKeepBest; + + /// Confirmation message for the keep-best action + /// + /// In en, this message translates to: + /// **'Delete {count} lower-quality copies of \"{trackName}\"?'** + String duplicatesKeepBestMessage(int count, String trackName); + + /// Confirmation message for deleting one duplicate copy + /// + /// In en, this message translates to: + /// **'Delete this copy of \"{trackName}\"?'** + String duplicatesDeleteCopyMessage(String trackName); + /// Snackbar - track added to download queue /// /// In en, this message translates to: @@ -6252,6 +6294,30 @@ abstract class AppLocalizations { /// **'Download next'** String get queueDownloadNext; + /// Repeat toggle tooltip when repeat is disabled + /// + /// In en, this message translates to: + /// **'Repeat off'** + String get nowPlayingRepeatOff; + + /// Repeat toggle tooltip when the whole queue repeats + /// + /// In en, this message translates to: + /// **'Repeat all'** + String get nowPlayingRepeatAll; + + /// Repeat toggle tooltip when the current track repeats + /// + /// In en, this message translates to: + /// **'Repeat one'** + String get nowPlayingRepeatOne; + + /// Snackbar shown when connectivity returns and network-failed downloads can be retried + /// + /// In en, this message translates to: + /// **'{count} downloads failed while offline'** + String queueNetworkFailedOffline(int count); + /// Accessibility label when a downloaded file is missing from disk /// /// In en, this message translates to: @@ -6840,12 +6906,6 @@ abstract class AppLocalizations { /// **'Android background service for extension downloads'** String get downloadNativeWorkerSubtitle; - /// Badge label for beta features - /// - /// In en, this message translates to: - /// **'BETA'** - String get badgeBeta; - /// Extension detail section header for service status /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 39d52292..4c1de5c9 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -659,6 +659,32 @@ class AppLocalizationsDe extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return '\"$trackName\" hinzugefügt'; @@ -3808,6 +3834,20 @@ class AppLocalizationsDe extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Heruntergeladene Datei fehlt'; @@ -4220,9 +4260,6 @@ class AppLocalizationsDe extends AppLocalizations { String get downloadNativeWorkerSubtitle => 'Android-Hintergrunddienst für Downloads von Erweiterungen'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Dienststatus'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 802d40b5..2c1873e5 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -651,6 +651,32 @@ class AppLocalizationsEn extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return 'Added \"$trackName\" to queue'; @@ -3762,6 +3788,20 @@ class AppLocalizationsEn extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Downloaded file missing'; @@ -4175,9 +4215,6 @@ class AppLocalizationsEn extends AppLocalizations { String get downloadNativeWorkerSubtitle => 'Android background service for extension downloads'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Service Status'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 0f8c4959..06d4377a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -651,6 +651,32 @@ class AppLocalizationsEs extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return 'Added \"$trackName\" to queue'; @@ -3756,6 +3782,20 @@ class AppLocalizationsEs extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Downloaded file missing'; @@ -4169,9 +4209,6 @@ class AppLocalizationsEs extends AppLocalizations { String get downloadNativeWorkerSubtitle => 'Android background service for extension downloads'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Service Status'; @@ -8875,9 +8912,6 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { String get downloadNativeWorkerSubtitle => 'Servicio de Android en segundo plano para descargas de extensiones'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Estado del servicio'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f15258f4..32a71ca9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -667,6 +667,32 @@ class AppLocalizationsFr extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return 'Ajout de « $trackName » à la file d\'attente'; @@ -3863,6 +3889,20 @@ class AppLocalizationsFr extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Fichier téléchargé manquant'; @@ -4286,9 +4326,6 @@ class AppLocalizationsFr extends AppLocalizations { String get downloadNativeWorkerSubtitle => 'Service Android en arrière-plan pour les téléchargements d\'extensions'; - @override - String get badgeBeta => 'BÊTA'; - @override String get extensionServiceStatus => 'État du service'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 31fb2b03..1b64bbe2 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -653,6 +653,32 @@ class AppLocalizationsId extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return 'Menambahkan \"$trackName\" ke antrian'; @@ -3765,6 +3791,20 @@ class AppLocalizationsId extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Downloaded file missing'; @@ -4179,9 +4219,6 @@ class AppLocalizationsId extends AppLocalizations { String get downloadNativeWorkerSubtitle => 'Layanan latar belakang Android untuk unduhan ekstensi'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Service Status'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index eb11de7b..d5949119 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -648,6 +648,32 @@ class AppLocalizationsJa extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return '「$trackName」をキューに追加しました'; @@ -3751,6 +3777,20 @@ class AppLocalizationsJa extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Downloaded file missing'; @@ -4164,9 +4204,6 @@ class AppLocalizationsJa extends AppLocalizations { String get downloadNativeWorkerSubtitle => '拡張機能のダウンロード用 Android バックグラウンドサービス'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Service Status'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 9e4162a1..82967688 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -635,6 +635,32 @@ class AppLocalizationsKo extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return '\'\'$trackName\'\'가 다운로드 목록에 추가됨'; @@ -3652,6 +3678,20 @@ class AppLocalizationsKo extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => '다운로드된 파일이 없음'; @@ -4051,9 +4091,6 @@ class AppLocalizationsKo extends AppLocalizations { String get downloadNativeWorkerSubtitle => '확장 프로그램 다운로드를 위한 Android 백그라운드 서비스'; - @override - String get badgeBeta => '베타'; - @override String get extensionServiceStatus => '서비스 상태'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index bba88870..57ce9c02 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -651,6 +651,32 @@ class AppLocalizationsPt extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return 'Added \"$trackName\" to queue'; @@ -3756,6 +3782,20 @@ class AppLocalizationsPt extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Downloaded file missing'; @@ -4169,9 +4209,6 @@ class AppLocalizationsPt extends AppLocalizations { String get downloadNativeWorkerSubtitle => 'Android background service for extension downloads'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Service Status'; @@ -8840,9 +8877,6 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { String get downloadNativeWorkerSubtitle => 'Serviço Android em segundo plano para transferências de extensões'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Service Status'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 17517c38..987a0540 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -655,6 +655,32 @@ class AppLocalizationsRu extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return '\"$trackName\" добавлен в очередь'; @@ -3793,6 +3819,20 @@ class AppLocalizationsRu extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Downloaded file missing'; @@ -4206,9 +4246,6 @@ class AppLocalizationsRu extends AppLocalizations { String get downloadNativeWorkerSubtitle => 'Фоновая служба Android для загрузок через расширения'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Service Status'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 22bc8fe2..0cdf30c4 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -657,6 +657,32 @@ class AppLocalizationsTr extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return '\"$trackName\" kuyruğa eklendi'; @@ -3792,6 +3818,20 @@ class AppLocalizationsTr extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Downloaded file missing'; @@ -4205,9 +4245,6 @@ class AppLocalizationsTr extends AppLocalizations { String get downloadNativeWorkerSubtitle => 'Uzantı indirmeleri için Android arka plan hizmeti'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Service Status'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 749b8d5f..b156cf93 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -658,6 +658,32 @@ class AppLocalizationsUk extends AppLocalizations { @override String get trackOpenOnNoLinks => 'No platform links found for this track.'; + @override + String get libraryReviewDuplicates => 'Review duplicates'; + + @override + String get libraryReviewDuplicatesSubtitle => + 'Find tracks stored more than once'; + + @override + String get duplicatesTitle => 'Duplicates'; + + @override + String get duplicatesEmpty => 'No duplicate tracks found.'; + + @override + String get duplicatesKeepBest => 'Keep best'; + + @override + String duplicatesKeepBestMessage(int count, String trackName) { + return 'Delete $count lower-quality copies of \"$trackName\"?'; + } + + @override + String duplicatesDeleteCopyMessage(String trackName) { + return 'Delete this copy of \"$trackName\"?'; + } + @override String snackbarAddedToQueue(String trackName) { return 'Додано \"$trackName\" до черги'; @@ -3810,6 +3836,20 @@ class AppLocalizationsUk extends AppLocalizations { @override String get queueDownloadNext => 'Download next'; + @override + String get nowPlayingRepeatOff => 'Repeat off'; + + @override + String get nowPlayingRepeatAll => 'Repeat all'; + + @override + String get nowPlayingRepeatOne => 'Repeat one'; + + @override + String queueNetworkFailedOffline(int count) { + return '$count downloads failed while offline'; + } + @override String get queueDownloadedFileMissing => 'Downloaded file missing'; @@ -4223,9 +4263,6 @@ class AppLocalizationsUk extends AppLocalizations { String get downloadNativeWorkerSubtitle => 'Фонова служба Android для завантажень через розширення'; - @override - String get badgeBeta => 'BETA'; - @override String get extensionServiceStatus => 'Service Status'; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f35beea4..6aced480 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -4922,6 +4922,27 @@ "@queueDownloadNext": { "description": "Tooltip on a queued download row; moves the item to the front of the queue so the next free slot downloads it" }, + "nowPlayingRepeatOff": "Repeat off", + "@nowPlayingRepeatOff": { + "description": "Repeat toggle tooltip when repeat is disabled" + }, + "nowPlayingRepeatAll": "Repeat all", + "@nowPlayingRepeatAll": { + "description": "Repeat toggle tooltip when the whole queue repeats" + }, + "nowPlayingRepeatOne": "Repeat one", + "@nowPlayingRepeatOne": { + "description": "Repeat toggle tooltip when the current track repeats" + }, + "queueNetworkFailedOffline": "{count} downloads failed while offline", + "@queueNetworkFailedOffline": { + "description": "Snackbar shown when connectivity returns and network-failed downloads can be retried", + "placeholders": { + "count": { + "type": "int" + } + } + }, "queueDownloadedFileMissing": "Downloaded file missing", "@queueDownloadedFileMissing": { "description": "Accessibility label when a downloaded file is missing from disk" diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 1a555c3b..23f90157 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -2,14 +2,18 @@ import 'dart:async'; import 'dart:math'; import 'dart:convert'; import 'dart:io'; +import 'package:flutter/material.dart' + show ScaffoldMessenger, SnackBar, SnackBarAction, Text; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/models/download_item.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/models/track.dart'; +import 'package:spotiflac_android/services/app_navigation_service.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/download_verification_retry_guard.dart'; @@ -159,6 +163,9 @@ class DownloadQueueNotifier extends Notifier { bool _networkPausedByWifiOnly = false; List? _lastConnectivityResults; DateTime _lastConnectionCleanupAt = DateTime.fromMillisecondsSinceEpoch(0); + DateTime _lastReconnectRetryPromptAt = DateTime.fromMillisecondsSinceEpoch( + 0, + ); static const _connectionCleanupDebounce = Duration(seconds: 2); String? _lastServiceTrackName; String? _lastServiceArtistName; @@ -1581,12 +1588,14 @@ class DownloadQueueNotifier extends Notifier { } } - void retryAllFailed() { + void retryAllFailed({bool networkOnly = false}) { final failedIds = state.items .where( (item) => - item.status == DownloadStatus.failed || - item.status == DownloadStatus.skipped, + (item.status == DownloadStatus.failed || + item.status == DownloadStatus.skipped) && + (!networkOnly || + item.errorType == DownloadErrorType.network), ) .map((item) => item.id) .toSet(); @@ -1817,11 +1826,53 @@ class DownloadQueueNotifier extends Notifier { } void _stopConnectivityMonitoring({bool clearNetworkPause = true}) { - _connectivitySub?.cancel(); - _connectivitySub = null; if (clearNetworkPause) { _networkPausedByWifiOnly = false; } + // Keep listening while network-failed items remain so the reconnect + // retry prompt can still fire when the queue is otherwise idle. + if (_hasNetworkFailedItems) return; + _connectivitySub?.cancel(); + _connectivitySub = null; + } + + bool get _hasNetworkFailedItems => state.items.any( + (item) => + item.status == DownloadStatus.failed && + item.errorType == DownloadErrorType.network, + ); + + /// Offers a one-tap retry for network-failed items once connectivity + /// returns, debounced so network flapping doesn't spam snackbars. + void _maybeOfferRetryAfterReconnect(List results) { + if (results.every((result) => result == ConnectivityResult.none)) return; + final failedCount = state.items + .where( + (item) => + item.status == DownloadStatus.failed && + item.errorType == DownloadErrorType.network, + ) + .length; + if (failedCount == 0) return; + final now = DateTime.now(); + if (now.difference(_lastReconnectRetryPromptAt) < + const Duration(minutes: 1)) { + return; + } + _lastReconnectRetryPromptAt = now; + + final context = AppNavigationService.rootNavigatorKey.currentContext; + if (context == null || !context.mounted) return; + final l10n = context.l10n; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.queueNetworkFailedOffline(failedCount)), + action: SnackBarAction( + label: l10n.dialogRetry, + onPressed: () => retryAllFailed(networkOnly: true), + ), + ), + ); } void _handleDownloadNetworkModeChanged(String mode) { @@ -1883,6 +1934,7 @@ class DownloadQueueNotifier extends Notifier { void _handleConnectivityResults(List results) { _maybeCleanupOnNetworkChange(results); + _maybeOfferRetryAfterReconnect(results); final settings = ref.read(settingsProvider); if (settings.downloadNetworkMode != 'wifi_only') return;