diff --git a/lib/app_state.dart b/lib/app_state.dart index c12c987..294c42c 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -43,6 +43,7 @@ class AppState extends ChangeNotifier { if (wasOffline && !enabled) { // Transitioning from offline to online: clear tile cache! TileProviderWithCache.clearCache(); + _startUploader(); // Resume upload queue processing as we leave offline mode } notifyListeners(); } @@ -387,17 +388,23 @@ class AppState extends ChangeNotifier { void _startUploader() { _uploadTimer?.cancel(); - // No uploads without auth or queue. - if (_queue.isEmpty) return; + // No uploads without auth or queue, or if offline mode is enabled. + if (_queue.isEmpty || _offlineMode) return; _uploadTimer = Timer.periodic(const Duration(seconds: 10), (t) async { - if (_queue.isEmpty) return; + if (_queue.isEmpty || _offlineMode) { + _uploadTimer?.cancel(); + return; + } + + // Find the first queue item that is NOT in error state and act on that + final item = _queue.where((pu) => !pu.error).cast().firstOrNull; + if (item == null) return; // Retrieve access after every tick (accounts for re-login) final access = await _auth.getAccessToken(); if (access == null) return; // not logged in - final item = _queue.first; bool ok; if (_uploadMode == UploadMode.simulate) { // Simulate successful upload without calling real API @@ -424,7 +431,10 @@ class AppState extends ChangeNotifier { if (!ok) { item.attempts++; if (item.attempts >= 3) { - // give up until next launch + // Mark as error and stop the uploader. User can manually retry. + item.error = true; + _saveQueue(); + notifyListeners(); _uploadTimer?.cancel(); } else { await Future.delayed(const Duration(seconds: 20)); @@ -451,4 +461,13 @@ class AppState extends ChangeNotifier { _saveQueue(); notifyListeners(); } + + // Retry a failed upload (clear error and attempts, then try uploading again) + void retryUpload(PendingUpload upload) { + upload.error = false; + upload.attempts = 0; + _saveQueue(); + notifyListeners(); + _startUploader(); // resume uploader if not busy + } } diff --git a/lib/models/pending_upload.dart b/lib/models/pending_upload.dart index 498a5ce..ae47afc 100644 --- a/lib/models/pending_upload.dart +++ b/lib/models/pending_upload.dart @@ -6,12 +6,14 @@ class PendingUpload { final double direction; final CameraProfile profile; int attempts; + bool error; PendingUpload({ required this.coord, required this.direction, required this.profile, this.attempts = 0, + this.error = false, }); Map toJson() => { @@ -20,6 +22,7 @@ class PendingUpload { 'dir': direction, 'profile': profile.toJson(), 'attempts': attempts, + 'error': error, }; factory PendingUpload.fromJson(Map j) => PendingUpload( @@ -27,8 +30,9 @@ class PendingUpload { direction: j['dir'], profile: j['profile'] is Map ? CameraProfile.fromJson(j['profile']) - : CameraProfile.alpr(), // fallback for legacy, more logic can be added + : CameraProfile.alpr(), attempts: j['attempts'] ?? 0, + error: j['error'] ?? false, ); } diff --git a/lib/screens/settings_screen_sections/queue_section.dart b/lib/screens/settings_screen_sections/queue_section.dart index 6d82405..393e405 100644 --- a/lib/screens/settings_screen_sections/queue_section.dart +++ b/lib/screens/settings_screen_sections/queue_section.dart @@ -71,22 +71,40 @@ class QueueSection extends StatelessWidget { itemBuilder: (context, index) { final upload = appState.pendingUploads[index]; return ListTile( - leading: const Icon(Icons.camera_alt), - title: Text('Camera ${index + 1}'), + leading: Icon( + upload.error ? Icons.error : Icons.camera_alt, + color: upload.error ? Colors.red : null, + ), + title: Text('Camera ${index + 1}${upload.error ? " (Error)" : ""}'), subtitle: Text( 'Lat: ${upload.coord.latitude.toStringAsFixed(6)}\n' 'Lon: ${upload.coord.longitude.toStringAsFixed(6)}\n' 'Direction: ${upload.direction.round()}°\n' - 'Attempts: ${upload.attempts}' + 'Attempts: ${upload.attempts}' + + (upload.error ? "\nUpload failed. Tap retry to try again." : "") ), - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - appState.removeFromQueue(upload); - if (appState.pendingCount == 0) { - Navigator.pop(context); - } - }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (upload.error) + IconButton( + icon: const Icon(Icons.refresh), + color: Colors.orange, + tooltip: 'Retry upload', + onPressed: () { + appState.retryUpload(upload); + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + appState.removeFromQueue(upload); + if (appState.pendingCount == 0) { + Navigator.pop(context); + } + }, + ), + ], ), ); },