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.
This commit is contained in:
zarzet
2026-07-26 19:00:49 +07:00
parent 424ca5d77e
commit 11fb4365a2
14 changed files with 584 additions and 50 deletions
+66 -6
View File
@@ -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:
+40 -3
View File
@@ -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';
+40 -3
View File
@@ -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';
+40 -6
View File
@@ -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';
+40 -3
View File
@@ -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';
+40 -3
View File
@@ -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';
+40 -3
View File
@@ -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';
+40 -3
View File
@@ -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 => '서비스 상태';
+40 -6
View File
@@ -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';
+40 -3
View File
@@ -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';
+40 -3
View File
@@ -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';
+40 -3
View File
@@ -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';
+21
View File
@@ -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"
+57 -5
View File
@@ -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<DownloadQueueState> {
bool _networkPausedByWifiOnly = false;
List<ConnectivityResult>? _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<DownloadQueueState> {
}
}
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<DownloadQueueState> {
}
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<ConnectivityResult> 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<DownloadQueueState> {
void _handleConnectivityResults(List<ConnectivityResult> results) {
_maybeCleanupOnNetworkChange(results);
_maybeOfferRetryAfterReconnect(results);
final settings = ref.read(settingsProvider);
if (settings.downloadNetworkMode != 'wifi_only') return;