diff --git a/lib/app_state.dart b/lib/app_state.dart index f76a8eb..4a5c4fd 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -180,7 +180,7 @@ class AppState extends ChangeNotifier { void commitSession() { final session = _sessionState.commitSession(); if (session != null) { - _uploadQueueState.addFromSession(session); + _uploadQueueState.addFromSession(session, uploadMode: uploadMode); _startUploader(); } } @@ -188,7 +188,7 @@ class AppState extends ChangeNotifier { void commitEditSession() { final session = _sessionState.commitEditSession(); if (session != null) { - _uploadQueueState.addFromEditSession(session); + _uploadQueueState.addFromEditSession(session, uploadMode: uploadMode); _startUploader(); } } diff --git a/lib/models/pending_upload.dart b/lib/models/pending_upload.dart index 185d397..0adf94d 100644 --- a/lib/models/pending_upload.dart +++ b/lib/models/pending_upload.dart @@ -1,10 +1,12 @@ import 'package:latlong2/latlong.dart'; import 'camera_profile.dart'; +import '../state/settings_state.dart'; class PendingUpload { final LatLng coord; final double direction; final CameraProfile profile; + final UploadMode uploadMode; // Capture upload destination when queued final int? originalNodeId; // If this is an edit, the ID of the original OSM node int attempts; bool error; @@ -13,6 +15,7 @@ class PendingUpload { required this.coord, required this.direction, required this.profile, + required this.uploadMode, this.originalNodeId, this.attempts = 0, this.error = false, @@ -21,11 +24,24 @@ class PendingUpload { // True if this is an edit of an existing camera, false if it's a new camera bool get isEdit => originalNodeId != null; + // Get display name for the upload destination + String get uploadModeDisplayName { + switch (uploadMode) { + case UploadMode.production: + return 'Production'; + case UploadMode.sandbox: + return 'Sandbox'; + case UploadMode.simulate: + return 'Simulate'; + } + } + Map toJson() => { 'lat': coord.latitude, 'lon': coord.longitude, 'dir': direction, 'profile': profile.toJson(), + 'uploadMode': uploadMode.index, 'originalNodeId': originalNodeId, 'attempts': attempts, 'error': error, @@ -37,6 +53,9 @@ class PendingUpload { profile: j['profile'] is Map ? CameraProfile.fromJson(j['profile']) : CameraProfile.genericAlpr(), + uploadMode: j['uploadMode'] != null + ? UploadMode.values[j['uploadMode']] + : UploadMode.production, // Default for legacy entries originalNodeId: j['originalNodeId'], 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 393e405..3781c86 100644 --- a/lib/screens/settings_screen_sections/queue_section.dart +++ b/lib/screens/settings_screen_sections/queue_section.dart @@ -1,10 +1,33 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../app_state.dart'; +import '../../state/settings_state.dart'; class QueueSection extends StatelessWidget { const QueueSection({super.key}); + String _getUploadModeDisplayName(UploadMode mode) { + switch (mode) { + case UploadMode.production: + return 'Production'; + case UploadMode.sandbox: + return 'Sandbox'; + case UploadMode.simulate: + return 'Simulate'; + } + } + + Color _getUploadModeColor(UploadMode mode) { + switch (mode) { + case UploadMode.production: + return Colors.green; // Green for production (real) + case UploadMode.sandbox: + return Colors.orange; // Orange for sandbox (testing) + case UploadMode.simulate: + return Colors.grey; // Grey for simulate (fake) + } + } + @override Widget build(BuildContext context) { final appState = context.watch(); @@ -73,10 +96,13 @@ class QueueSection extends StatelessWidget { return ListTile( leading: Icon( upload.error ? Icons.error : Icons.camera_alt, - color: upload.error ? Colors.red : null, + color: upload.error + ? Colors.red + : _getUploadModeColor(upload.uploadMode), ), title: Text('Camera ${index + 1}${upload.error ? " (Error)" : ""}'), subtitle: Text( + 'Dest: ${_getUploadModeDisplayName(upload.uploadMode)}\n' 'Lat: ${upload.coord.latitude.toStringAsFixed(6)}\n' 'Lon: ${upload.coord.longitude.toStringAsFixed(6)}\n' 'Direction: ${upload.direction.round()}°\n' diff --git a/lib/state/upload_queue_state.dart b/lib/state/upload_queue_state.dart index b1cd21f..2c3961c 100644 --- a/lib/state/upload_queue_state.dart +++ b/lib/state/upload_queue_state.dart @@ -25,11 +25,12 @@ class UploadQueueState extends ChangeNotifier { } // Add a completed session to the upload queue - void addFromSession(AddCameraSession session) { + void addFromSession(AddCameraSession session, {required UploadMode uploadMode}) { final upload = PendingUpload( coord: session.target!, direction: session.directionDegrees, profile: session.profile, + uploadMode: uploadMode, ); _queue.add(upload); @@ -57,11 +58,12 @@ class UploadQueueState extends ChangeNotifier { } // Add a completed edit session to the upload queue - void addFromEditSession(EditCameraSession session) { + void addFromEditSession(EditCameraSession session, {required UploadMode uploadMode}) { final upload = PendingUpload( coord: session.target, direction: session.directionDegrees, profile: session.profile, + uploadMode: uploadMode, originalNodeId: session.originalNode.id, // Track which node we're editing ); @@ -145,21 +147,24 @@ class UploadQueueState extends ChangeNotifier { if (access == null) return; // not logged in bool ok; - if (uploadMode == UploadMode.simulate) { + debugPrint('[UploadQueue] Processing item with uploadMode: ${item.uploadMode}'); + if (item.uploadMode == UploadMode.simulate) { // Simulate successful upload without calling real API + debugPrint('[UploadQueue] Simulating upload (no real API call)'); await Future.delayed(const Duration(seconds: 1)); // Simulate network delay ok = true; } else { - // Real upload -- pass uploadMode so uploader can switch between prod and sandbox + // Real upload -- use the upload mode that was saved when this item was queued + debugPrint('[UploadQueue] Real upload to: ${item.uploadMode}'); final up = Uploader(access, () { _queue.remove(item); _saveQueue(); notifyListeners(); - }, uploadMode: uploadMode); + }, uploadMode: item.uploadMode); ok = await up.upload(item); } - if (ok && uploadMode == UploadMode.simulate) { + if (ok && item.uploadMode == UploadMode.simulate) { // Remove manually for simulate mode _queue.remove(item); _saveQueue(); diff --git a/test/models/pending_upload_test.dart b/test/models/pending_upload_test.dart new file mode 100644 index 0000000..25cd4e5 --- /dev/null +++ b/test/models/pending_upload_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:flock_map_app/models/pending_upload.dart'; +import 'package:flock_map_app/models/camera_profile.dart'; +import 'package:flock_map_app/state/settings_state.dart'; + +void main() { + group('PendingUpload', () { + test('should serialize and deserialize upload mode correctly', () { + // Test each upload mode + final testModes = [ + UploadMode.production, + UploadMode.sandbox, + UploadMode.simulate, + ]; + + for (final mode in testModes) { + final original = PendingUpload( + coord: LatLng(37.7749, -122.4194), + direction: 90.0, + profile: CameraProfile.flock(), + uploadMode: mode, + ); + + // Serialize to JSON + final json = original.toJson(); + + // Deserialize from JSON + final restored = PendingUpload.fromJson(json); + + // Verify upload mode is preserved + expect(restored.uploadMode, equals(mode)); + expect(restored.uploadModeDisplayName, equals(original.uploadModeDisplayName)); + + // Verify other fields too + expect(restored.coord.latitude, equals(original.coord.latitude)); + expect(restored.coord.longitude, equals(original.coord.longitude)); + expect(restored.direction, equals(original.direction)); + expect(restored.profile.id, equals(original.profile.id)); + } + }); + + test('should handle legacy JSON without uploadMode', () { + // Simulate old JSON format without uploadMode field + final legacyJson = { + 'lat': 37.7749, + 'lon': -122.4194, + 'dir': 90.0, + 'profile': CameraProfile.flock().toJson(), + 'originalNodeId': null, + 'attempts': 0, + 'error': false, + // Note: no 'uploadMode' field + }; + + final upload = PendingUpload.fromJson(legacyJson); + + // Should default to production mode for legacy entries + expect(upload.uploadMode, equals(UploadMode.production)); + expect(upload.uploadModeDisplayName, equals('Production')); + }); + + test('should correctly identify edits vs new cameras', () { + final newCamera = PendingUpload( + coord: LatLng(37.7749, -122.4194), + direction: 90.0, + profile: CameraProfile.flock(), + uploadMode: UploadMode.production, + ); + + final editCamera = PendingUpload( + coord: LatLng(37.7749, -122.4194), + direction: 90.0, + profile: CameraProfile.flock(), + uploadMode: UploadMode.production, + originalNodeId: 12345, + ); + + expect(newCamera.isEdit, isFalse); + expect(editCamera.isEdit, isTrue); + }); + }); +} \ No newline at end of file