offline mode fix for camera upload queue

This commit is contained in:
stopflock
2025-08-12 19:49:03 -05:00
parent fa3b8b3456
commit d0f92f6daf
3 changed files with 58 additions and 17 deletions

View File

@@ -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<PendingUpload?>().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
}
}

View File

@@ -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<String, dynamic> toJson() => {
@@ -20,6 +22,7 @@ class PendingUpload {
'dir': direction,
'profile': profile.toJson(),
'attempts': attempts,
'error': error,
};
factory PendingUpload.fromJson(Map<String, dynamic> j) => PendingUpload(
@@ -27,8 +30,9 @@ class PendingUpload {
direction: j['dir'],
profile: j['profile'] is Map<String, dynamic>
? CameraProfile.fromJson(j['profile'])
: CameraProfile.alpr(), // fallback for legacy, more logic can be added
: CameraProfile.alpr(),
attempts: j['attempts'] ?? 0,
error: j['error'] ?? false,
);
}

View File

@@ -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);
}
},
),
],
),
);
},