mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-08-11 05:30:22 +02:00
notice about any in-flight submissions when enabling queue pause or offline mode
This commit is contained in:
@@ -188,6 +188,13 @@ class AppState extends ChangeNotifier {
|
||||
int get pendingCount => _uploadQueueState.pendingCount;
|
||||
List<PendingUpload> get pendingUploads => _uploadQueueState.pendingUploads;
|
||||
|
||||
/// True if any queue item currently has an open changeset that hasn't
|
||||
/// been closed yet (creating changeset, uploading node, or closing
|
||||
/// changeset). Used to gate enabling offline mode / pausing the upload
|
||||
/// queue so in-flight submissions can finish first.
|
||||
bool get hasInFlightUploads => pendingUploads.any((u) => u.isActivelyProcessing);
|
||||
|
||||
|
||||
// Suspected location state
|
||||
SuspectedLocation? get selectedSuspectedLocation => _suspectedLocationState.selectedLocation;
|
||||
bool get suspectedLocationsEnabled => _suspectedLocationState.isEnabled;
|
||||
|
||||
@@ -64,6 +64,13 @@ const Duration kChangesetCloseMaxRetryDelay = Duration(minutes: 5); // Cap at 5
|
||||
const Duration kChangesetAutoCloseTimeout = Duration(minutes: 59); // Give up and trust OSM auto-close
|
||||
const double kChangesetCloseBackoffMultiplier = 2.0;
|
||||
|
||||
// How long to keep the "Submission Complete" confirmation visible in the
|
||||
// in-flight upload wait dialog (shown when the user enables offline mode or
|
||||
// pauses the upload queue while a submission is actively in progress) before
|
||||
// automatically dismissing and applying the requested setting change.
|
||||
const Duration kInFlightUploadCompleteDisplayDuration = Duration(seconds: 2);
|
||||
|
||||
|
||||
// Overpass API configuration
|
||||
const Duration kOverpassQueryTimeout = Duration(seconds: 45); // Timeout for Overpass API queries (was 25s hardcoded)
|
||||
|
||||
|
||||
@@ -270,8 +270,13 @@
|
||||
"closingChangeset": " (Changeset schließen...)",
|
||||
"processingPaused": "Warteschlangenverarbeitung pausiert",
|
||||
"pausedDueToOffline": "Upload-Verarbeitung ist pausiert, da der Offline-Modus aktiviert ist.",
|
||||
"pausedByUser": "Upload-Verarbeitung ist manuell pausiert."
|
||||
"pausedByUser": "Upload-Verarbeitung ist manuell pausiert.",
|
||||
"inFlightWaitTitle": "Übermittlung wird abgeschlossen",
|
||||
"inFlightWaitMessage": "Eine Übermittlung läuft gerade und muss abgeschlossen werden, bevor diese Einstellung angewendet werden kann. Dies sollte nur einen Moment dauern.",
|
||||
"inFlightCompleteTitle": "Übermittlung Abgeschlossen",
|
||||
"inFlightCompleteMessage": "Ihre Übermittlung wurde erfolgreich abgeschlossen. Ihre Einstellung wird jetzt angewendet."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Kachel-Anbieter",
|
||||
"noProvidersConfigured": "Keine Kachel-Anbieter konfiguriert",
|
||||
|
||||
@@ -306,8 +306,13 @@
|
||||
"closingChangeset": " (Closing changeset...)",
|
||||
"processingPaused": "Queue Processing Paused",
|
||||
"pausedDueToOffline": "Upload processing is paused because offline mode is enabled.",
|
||||
"pausedByUser": "Upload processing is manually paused."
|
||||
"pausedByUser": "Upload processing is manually paused.",
|
||||
"inFlightWaitTitle": "Finishing Submission",
|
||||
"inFlightWaitMessage": "A submission is currently in progress and needs to finish before this setting can be applied. This should only take a moment.",
|
||||
"inFlightCompleteTitle": "Submission Complete",
|
||||
"inFlightCompleteMessage": "Your submission finished successfully. Applying your setting now."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Tile Providers",
|
||||
"noProvidersConfigured": "No tile providers configured",
|
||||
|
||||
@@ -307,8 +307,13 @@
|
||||
"closingChangeset": " (Cerrando changeset...)",
|
||||
"processingPaused": "Procesamiento de Cola Pausado",
|
||||
"pausedDueToOffline": "El procesamiento de subida está pausado porque el modo sin conexión está habilitado.",
|
||||
"pausedByUser": "El procesamiento de subida está pausado manualmente."
|
||||
"pausedByUser": "El procesamiento de subida está pausado manualmente.",
|
||||
"inFlightWaitTitle": "Finalizando Envío",
|
||||
"inFlightWaitMessage": "Actualmente hay un envío en curso que debe finalizar antes de que se pueda aplicar esta configuración. Esto solo debería tomar un momento.",
|
||||
"inFlightCompleteTitle": "Envío Completado",
|
||||
"inFlightCompleteMessage": "Su envío finalizó exitosamente. Aplicando su configuración ahora."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Proveedores de Tiles",
|
||||
"noProvidersConfigured": "No hay proveedores de tiles configurados",
|
||||
|
||||
@@ -307,8 +307,13 @@
|
||||
"closingChangeset": " (Fermeture du changeset...)",
|
||||
"processingPaused": "Traitement de la File d'Attente Interrompu",
|
||||
"pausedDueToOffline": "Le traitement des téléversements est interrompu car le mode hors ligne est activé.",
|
||||
"pausedByUser": "Le traitement des téléversements est interrompu manuellement."
|
||||
"pausedByUser": "Le traitement des téléversements est interrompu manuellement.",
|
||||
"inFlightWaitTitle": "Finalisation de la Soumission",
|
||||
"inFlightWaitMessage": "Une soumission est actuellement en cours et doit se terminer avant que ce paramètre puisse être appliqué. Cela ne devrait prendre qu'un instant.",
|
||||
"inFlightCompleteTitle": "Soumission Terminée",
|
||||
"inFlightCompleteMessage": "Votre soumission s'est terminée avec succès. Application de votre paramètre maintenant."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Fournisseurs de Tuiles",
|
||||
"noProvidersConfigured": "Aucun fournisseur de tuiles configuré",
|
||||
|
||||
@@ -307,8 +307,13 @@
|
||||
"closingChangeset": " (Chiusura changeset...)",
|
||||
"processingPaused": "Elaborazione Coda Sospesa",
|
||||
"pausedDueToOffline": "L'elaborazione dei caricamenti è sospesa perché la modalità offline è abilitata.",
|
||||
"pausedByUser": "L'elaborazione dei caricamenti è sospesa manualmente."
|
||||
"pausedByUser": "L'elaborazione dei caricamenti è sospesa manualmente.",
|
||||
"inFlightWaitTitle": "Completamento Invio",
|
||||
"inFlightWaitMessage": "Un invio è attualmente in corso e deve terminare prima che questa impostazione possa essere applicata. Questo dovrebbe richiedere solo un momento.",
|
||||
"inFlightCompleteTitle": "Invio Completato",
|
||||
"inFlightCompleteMessage": "Il tuo invio è terminato con successo. Applicazione della tua impostazione in corso."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Fornitori di Tile",
|
||||
"noProvidersConfigured": "Nessun fornitore di tile configurato",
|
||||
|
||||
@@ -307,8 +307,13 @@
|
||||
"closingChangeset": " (Changeset sluiten...)",
|
||||
"processingPaused": "Wachtrij Verwerking Gepauzeerd",
|
||||
"pausedDueToOffline": "Upload verwerking is gepauzeerd omdat offline modus is ingeschakeld.",
|
||||
"pausedByUser": "Upload verwerking is handmatig gepauzeerd."
|
||||
"pausedByUser": "Upload verwerking is handmatig gepauzeerd.",
|
||||
"inFlightWaitTitle": "Indiening Wordt Afgerond",
|
||||
"inFlightWaitMessage": "Er is momenteel een indiening bezig die moet worden afgerond voordat deze instelling kan worden toegepast. Dit zou maar een moment moeten duren.",
|
||||
"inFlightCompleteTitle": "Indiening Voltooid",
|
||||
"inFlightCompleteMessage": "Uw indiening is succesvol afgerond. Uw instelling wordt nu toegepast."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Tile Providers",
|
||||
"noProvidersConfigured": "Geen tile providers geconfigureerd",
|
||||
|
||||
@@ -307,8 +307,13 @@
|
||||
"closingChangeset": " (Zamykanie zestawu zmian...)",
|
||||
"processingPaused": "Przetwarzanie Kolejki Wstrzymane",
|
||||
"pausedDueToOffline": "Przetwarzanie przesyłania jest wstrzymane, ponieważ tryb offline jest włączony.",
|
||||
"pausedByUser": "Przetwarzanie przesyłania jest ręcznie wstrzymane."
|
||||
"pausedByUser": "Przetwarzanie przesyłania jest ręcznie wstrzymane.",
|
||||
"inFlightWaitTitle": "Kończenie Przesyłania",
|
||||
"inFlightWaitMessage": "Przesyłanie jest obecnie w toku i musi zostać zakończone, zanim to ustawienie będzie mogło zostać zastosowane. To powinno zająć tylko chwilę.",
|
||||
"inFlightCompleteTitle": "Przesyłanie Zakończone",
|
||||
"inFlightCompleteMessage": "Twoje przesyłanie zakończyło się pomyślnie. Stosowanie ustawienia teraz."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Dostawcy Kafelków",
|
||||
"noProvidersConfigured": "Brak skonfigurowanych dostawców kafelków",
|
||||
|
||||
@@ -307,8 +307,13 @@
|
||||
"closingChangeset": " (Fechando changeset...)",
|
||||
"processingPaused": "Processamento da Fila Pausado",
|
||||
"pausedDueToOffline": "O processamento de upload está pausado porque o modo offline está habilitado.",
|
||||
"pausedByUser": "O processamento de upload está pausado manualmente."
|
||||
"pausedByUser": "O processamento de upload está pausado manualmente.",
|
||||
"inFlightWaitTitle": "Finalizando Envio",
|
||||
"inFlightWaitMessage": "Atualmente há um envio em andamento que precisa terminar antes que esta configuração possa ser aplicada. Isso deve levar apenas um momento.",
|
||||
"inFlightCompleteTitle": "Envio Concluído",
|
||||
"inFlightCompleteMessage": "Seu envio foi concluído com sucesso. Aplicando sua configuração agora."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Provedores de Tiles",
|
||||
"noProvidersConfigured": "Nenhum provedor de tiles configurado",
|
||||
|
||||
@@ -307,8 +307,13 @@
|
||||
"closingChangeset": " (Değişiklik seti kapatılıyor...)",
|
||||
"processingPaused": "Kuyruk İşleme Duraklatıldı",
|
||||
"pausedDueToOffline": "Çevrimdışı mod etkin olduğu için yükleme işleme duraklatıldı.",
|
||||
"pausedByUser": "Yükleme işleme manuel olarak duraklatıldı."
|
||||
"pausedByUser": "Yükleme işleme manuel olarak duraklatıldı.",
|
||||
"inFlightWaitTitle": "Gönderim Tamamlanıyor",
|
||||
"inFlightWaitMessage": "Şu anda devam eden bir gönderim var ve bu ayar uygulanmadan önce tamamlanması gerekiyor. Bu sadece bir an sürmelidir.",
|
||||
"inFlightCompleteTitle": "Gönderim Tamamlandı",
|
||||
"inFlightCompleteMessage": "Gönderiminiz başarıyla tamamlandı. Ayarınız şimdi uygulanıyor."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Döşeme Sağlayıcıları",
|
||||
"noProvidersConfigured": "Döşeme sağlayıcısı yapılandırılmamış",
|
||||
|
||||
@@ -307,8 +307,13 @@
|
||||
"closingChangeset": " (Закриття набору змін...)",
|
||||
"processingPaused": "Обробка Черги Призупинена",
|
||||
"pausedDueToOffline": "Обробка завантаження призупинена, оскільки увімкнено офлайн режим.",
|
||||
"pausedByUser": "Обробка завантаження призупинена вручну."
|
||||
"pausedByUser": "Обробка завантаження призупинена вручну.",
|
||||
"inFlightWaitTitle": "Завершення Подання",
|
||||
"inFlightWaitMessage": "Наразі триває подання, яке потрібно завершити перед застосуванням цього налаштування. Це має зайняти лише мить.",
|
||||
"inFlightCompleteTitle": "Подання Завершено",
|
||||
"inFlightCompleteMessage": "Ваше подання успішно завершено. Застосування вашого налаштування зараз."
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "Постачальники Плиток",
|
||||
"noProvidersConfigured": "Постачальників плиток не налаштовано",
|
||||
|
||||
@@ -307,8 +307,13 @@
|
||||
"closingChangeset": " (关闭变更集...)",
|
||||
"processingPaused": "队列处理已暂停",
|
||||
"pausedDueToOffline": "因为离线模式已启用,上传处理已暂停。",
|
||||
"pausedByUser": "上传处理已手动暂停。"
|
||||
"pausedByUser": "上传处理已手动暂停。",
|
||||
"inFlightWaitTitle": "正在完成提交",
|
||||
"inFlightWaitMessage": "当前有一个提交正在进行中,需要先完成才能应用此设置。这应该只需要一点时间。",
|
||||
"inFlightCompleteTitle": "提交完成",
|
||||
"inFlightCompleteMessage": "您的提交已成功完成。正在应用您的设置。"
|
||||
},
|
||||
|
||||
"tileProviders": {
|
||||
"title": "瓦片提供商",
|
||||
"noProvidersConfigured": "未配置瓦片提供商",
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:provider/provider.dart';
|
||||
import '../../../app_state.dart';
|
||||
import '../../../services/offline_area_service.dart';
|
||||
import '../../../services/localization_service.dart';
|
||||
import '../../../widgets/in_flight_upload_wait_dialog.dart';
|
||||
|
||||
|
||||
class OfflineModeSection extends StatelessWidget {
|
||||
const OfflineModeSection({super.key});
|
||||
@@ -49,10 +51,17 @@ class OfflineModeSection extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed with the change
|
||||
await appState.setOfflineMode(value);
|
||||
// Proceed with the change, respecting any in-flight submission that
|
||||
// needs to finish before offline mode actually takes effect.
|
||||
if (!context.mounted) return;
|
||||
await applyQueueSettingChangeRespectingInFlightUploads(
|
||||
context: context,
|
||||
appState: appState,
|
||||
applyChange: () => appState.setOfflineMode(value),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:provider/provider.dart';
|
||||
import '../app_state.dart';
|
||||
import '../models/pending_upload.dart';
|
||||
import '../services/localization_service.dart';
|
||||
import '../widgets/in_flight_upload_wait_dialog.dart';
|
||||
|
||||
|
||||
class UploadQueueScreen extends StatelessWidget {
|
||||
const UploadQueueScreen({super.key});
|
||||
@@ -84,6 +86,22 @@ class UploadQueueScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePauseQueueChange(BuildContext context, AppState appState, bool value) async {
|
||||
// Only need to wait for in-flight uploads when turning pause ON.
|
||||
// Resuming (turning it off) can happen immediately.
|
||||
if (!value) {
|
||||
await appState.setPauseQueueProcessing(value);
|
||||
return;
|
||||
}
|
||||
|
||||
await applyQueueSettingChangeRespectingInFlightUploads(
|
||||
context: context,
|
||||
appState: appState,
|
||||
applyChange: () => appState.setPauseQueueProcessing(value),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
@@ -166,9 +184,10 @@ class UploadQueueScreen extends StatelessWidget {
|
||||
value: appState.pauseQueueProcessing,
|
||||
onChanged: appState.offlineMode
|
||||
? null // Disable when offline mode is on
|
||||
: (value) => appState.setPauseQueueProcessing(value),
|
||||
: (value) => _handlePauseQueueChange(context, appState, value),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app_state.dart';
|
||||
import '../dev_config.dart';
|
||||
import '../services/localization_service.dart';
|
||||
|
||||
/// Applies a queue-pausing settings change (enabling offline mode or pausing
|
||||
/// the upload queue), but if there's an in-flight submission (a queue item
|
||||
/// that has already opened a changeset and hasn't closed it yet), first
|
||||
/// shows a non-dismissible-by-tap-outside dialog explaining that the
|
||||
/// submission needs to finish. Once it completes, a brief confirmation is
|
||||
/// shown before the dialog auto-closes and the setting takes effect.
|
||||
///
|
||||
/// If nothing is in-flight, [applyChange] is invoked immediately with no
|
||||
/// dialog shown at all.
|
||||
///
|
||||
/// The dialog can also be dismissed early by the user tapping "View in
|
||||
/// Queue", in case something is stuck (e.g. a failed/erroring item) — this
|
||||
/// lets the user navigate to the queue screen to investigate/clear it out,
|
||||
/// rather than getting stuck waiting indefinitely.
|
||||
Future<void> applyQueueSettingChangeRespectingInFlightUploads({
|
||||
required BuildContext context,
|
||||
required AppState appState,
|
||||
required Future<void> Function() applyChange,
|
||||
}) async {
|
||||
if (!appState.hasInFlightUploads) {
|
||||
await applyChange();
|
||||
return;
|
||||
}
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => InFlightUploadWaitDialog(
|
||||
onAllComplete: () {
|
||||
// Fire and forget - the dialog doesn't need to await this.
|
||||
applyChange();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class InFlightUploadWaitDialog extends StatefulWidget {
|
||||
/// Called exactly once, as soon as no queue items are actively processing.
|
||||
final VoidCallback onAllComplete;
|
||||
|
||||
const InFlightUploadWaitDialog({super.key, required this.onAllComplete});
|
||||
|
||||
@override
|
||||
State<InFlightUploadWaitDialog> createState() => _InFlightUploadWaitDialogState();
|
||||
}
|
||||
|
||||
class _InFlightUploadWaitDialogState extends State<InFlightUploadWaitDialog> {
|
||||
bool _completed = false;
|
||||
bool _appliedChange = false;
|
||||
Timer? _autoCloseTimer;
|
||||
late final AppState _appState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_appState = AppState.instance;
|
||||
_appState.addListener(_onAppStateChanged);
|
||||
// Handle the (unlikely) race where the in-flight upload finishes between
|
||||
// the initial check and this dialog actually mounting.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onAppStateChanged());
|
||||
}
|
||||
|
||||
void _onAppStateChanged() {
|
||||
if (!mounted || _completed) return;
|
||||
if (_appState.hasInFlightUploads) return;
|
||||
|
||||
setState(() {
|
||||
_completed = true;
|
||||
});
|
||||
|
||||
if (!_appliedChange) {
|
||||
_appliedChange = true;
|
||||
widget.onAllComplete();
|
||||
}
|
||||
|
||||
_autoCloseTimer = Timer(kInFlightUploadCompleteDisplayDuration, () {
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_appState.removeListener(_onAppStateChanged);
|
||||
_autoCloseTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _viewInQueue() {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
Navigator.of(context).pushNamed('/settings/queue');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: LocalizationService.instance,
|
||||
builder: (context, child) {
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_completed ? Icons.check_circle : Icons.cloud_upload,
|
||||
color: _completed ? Colors.green : Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_completed
|
||||
? locService.t('queue.inFlightCompleteTitle')
|
||||
: locService.t('queue.inFlightWaitTitle'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_completed
|
||||
? locService.t('queue.inFlightCompleteMessage')
|
||||
: locService.t('queue.inFlightWaitMessage'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: _completed
|
||||
? const Icon(Icons.check_circle, size: 40, color: Colors.green)
|
||||
: const CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (!_completed)
|
||||
TextButton(
|
||||
onPressed: _viewInQueue,
|
||||
child: Text(locService.t('node.viewInQueue')),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user