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 applyQueueSettingChangeRespectingInFlightUploads({ required BuildContext context, required AppState appState, required Future Function() applyChange, }) async { if (!appState.hasInFlightUploads) { await applyChange(); return; } await showDialog( 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 createState() => _InFlightUploadWaitDialogState(); } class _InFlightUploadWaitDialogState extends State { 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')), ), ], ), ); }, ); } }