mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
add upload mode to queue entries to prevent cross-submission
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, dynamic> 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<String, dynamic>
|
||||
? 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,
|
||||
|
||||
@@ -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<AppState>();
|
||||
@@ -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'
|
||||
|
||||
@@ -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();
|
||||
|
||||
83
test/models/pending_upload_test.dart
Normal file
83
test/models/pending_upload_test.dart
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user