mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-04-21 11:16:23 +02:00
Deletions!
This commit is contained in:
@@ -216,6 +216,11 @@ class AppState extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void deleteNode(OsmCameraNode node) {
|
||||
_uploadQueueState.addFromNodeDeletion(node, uploadMode: uploadMode);
|
||||
_startUploader();
|
||||
}
|
||||
|
||||
// ---------- Settings Methods ----------
|
||||
Future<void> setOfflineMode(bool enabled) async {
|
||||
await _settingsState.setOfflineMode(enabled);
|
||||
|
||||
@@ -76,3 +76,4 @@ const Color kCameraRingColorMock = Color(0xC4FFFFFF); // Add node mock point - w
|
||||
const Color kCameraRingColorPending = Color(0xC49C27B0); // Submitted/pending nodes - purple
|
||||
const Color kCameraRingColorEditing = Color(0xC4FF9800); // Node being edited - orange
|
||||
const Color kCameraRingColorPendingEdit = Color(0xC4757575); // Original node with pending edit - grey
|
||||
const Color kCameraRingColorPendingDeletion = Color(0xA4F44336); // Node pending deletion - red, slightly transparent
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"download": "Herunterladen",
|
||||
"settings": "Einstellungen",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"cancel": "Abbrechen",
|
||||
"ok": "OK",
|
||||
"close": "Schließen",
|
||||
@@ -41,7 +42,10 @@
|
||||
"title": "Knoten #{}",
|
||||
"tagSheetTitle": "Gerät-Tags",
|
||||
"queuedForUpload": "Knoten zum Upload eingereiht",
|
||||
"editQueuedForUpload": "Knotenbearbeitung zum Upload eingereiht"
|
||||
"editQueuedForUpload": "Knotenbearbeitung zum Upload eingereiht",
|
||||
"deleteQueuedForUpload": "Knoten-Löschung zum Upload eingereiht",
|
||||
"confirmDeleteTitle": "Knoten löschen",
|
||||
"confirmDeleteMessage": "Sind Sie sicher, dass Sie Knoten #{} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"addNode": {
|
||||
"profile": "Profil",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"download": "Download",
|
||||
"settings": "Settings",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"ok": "OK",
|
||||
"close": "Close",
|
||||
@@ -41,7 +42,10 @@
|
||||
"title": "Node #{}",
|
||||
"tagSheetTitle": "Surveillance Device Tags",
|
||||
"queuedForUpload": "Node queued for upload",
|
||||
"editQueuedForUpload": "Node edit queued for upload"
|
||||
"editQueuedForUpload": "Node edit queued for upload",
|
||||
"deleteQueuedForUpload": "Node deletion queued for upload",
|
||||
"confirmDeleteTitle": "Delete Node",
|
||||
"confirmDeleteMessage": "Are you sure you want to delete node #{}? This action cannot be undone."
|
||||
},
|
||||
"addNode": {
|
||||
"profile": "Profile",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"download": "Descargar",
|
||||
"settings": "Configuración",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"cancel": "Cancelar",
|
||||
"ok": "Aceptar",
|
||||
"close": "Cerrar",
|
||||
@@ -41,7 +42,10 @@
|
||||
"title": "Nodo #{}",
|
||||
"tagSheetTitle": "Etiquetas del Dispositivo",
|
||||
"queuedForUpload": "Nodo en cola para subir",
|
||||
"editQueuedForUpload": "Edición de nodo en cola para subir"
|
||||
"editQueuedForUpload": "Edición de nodo en cola para subir",
|
||||
"deleteQueuedForUpload": "Eliminación de nodo en cola para subir",
|
||||
"confirmDeleteTitle": "Eliminar Nodo",
|
||||
"confirmDeleteMessage": "¿Estás seguro de que quieres eliminar el nodo #{}? Esta acción no se puede deshacer."
|
||||
},
|
||||
"addNode": {
|
||||
"profile": "Perfil",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"download": "Télécharger",
|
||||
"settings": "Paramètres",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"cancel": "Annuler",
|
||||
"ok": "OK",
|
||||
"close": "Fermer",
|
||||
@@ -41,7 +42,10 @@
|
||||
"title": "Nœud #{}",
|
||||
"tagSheetTitle": "Balises du Dispositif",
|
||||
"queuedForUpload": "Nœud mis en file pour envoi",
|
||||
"editQueuedForUpload": "Modification de nœud mise en file pour envoi"
|
||||
"editQueuedForUpload": "Modification de nœud mise en file pour envoi",
|
||||
"deleteQueuedForUpload": "Suppression de nœud mise en file pour envoi",
|
||||
"confirmDeleteTitle": "Supprimer le Nœud",
|
||||
"confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer le nœud #{} ? Cette action ne peut pas être annulée."
|
||||
},
|
||||
"addNode": {
|
||||
"profile": "Profil",
|
||||
|
||||
@@ -3,13 +3,16 @@ import 'node_profile.dart';
|
||||
import 'operator_profile.dart';
|
||||
import '../state/settings_state.dart';
|
||||
|
||||
enum UploadOperation { create, modify, delete }
|
||||
|
||||
class PendingUpload {
|
||||
final LatLng coord;
|
||||
final double direction;
|
||||
final NodeProfile profile;
|
||||
final OperatorProfile? operatorProfile;
|
||||
final UploadMode uploadMode; // Capture upload destination when queued
|
||||
final int? originalNodeId; // If this is an edit, the ID of the original OSM node
|
||||
final UploadOperation operation; // Type of operation: create, modify, or delete
|
||||
final int? originalNodeId; // If this is modify/delete, the ID of the original OSM node
|
||||
int? submittedNodeId; // The actual node ID returned by OSM after successful submission
|
||||
int attempts;
|
||||
bool error;
|
||||
@@ -21,15 +24,23 @@ class PendingUpload {
|
||||
required this.profile,
|
||||
this.operatorProfile,
|
||||
required this.uploadMode,
|
||||
required this.operation,
|
||||
this.originalNodeId,
|
||||
this.submittedNodeId,
|
||||
this.attempts = 0,
|
||||
this.error = false,
|
||||
this.completing = false,
|
||||
});
|
||||
}) : assert(
|
||||
(operation == UploadOperation.create && originalNodeId == null) ||
|
||||
(operation != UploadOperation.create && originalNodeId != null),
|
||||
'originalNodeId must be null for create operations and non-null for modify/delete operations'
|
||||
);
|
||||
|
||||
// True if this is an edit of an existing node, false if it's a new node
|
||||
bool get isEdit => originalNodeId != null;
|
||||
bool get isEdit => operation == UploadOperation.modify;
|
||||
|
||||
// True if this is a deletion of an existing node
|
||||
bool get isDeletion => operation == UploadOperation.delete;
|
||||
|
||||
// Get display name for the upload destination
|
||||
String get uploadModeDisplayName {
|
||||
@@ -67,6 +78,7 @@ class PendingUpload {
|
||||
'profile': profile.toJson(),
|
||||
'operatorProfile': operatorProfile?.toJson(),
|
||||
'uploadMode': uploadMode.index,
|
||||
'operation': operation.index,
|
||||
'originalNodeId': originalNodeId,
|
||||
'submittedNodeId': submittedNodeId,
|
||||
'attempts': attempts,
|
||||
@@ -86,6 +98,9 @@ class PendingUpload {
|
||||
uploadMode: j['uploadMode'] != null
|
||||
? UploadMode.values[j['uploadMode']]
|
||||
: UploadMode.production, // Default for legacy entries
|
||||
operation: j['operation'] != null
|
||||
? UploadOperation.values[j['operation']]
|
||||
: (j['originalNodeId'] != null ? UploadOperation.modify : UploadOperation.create), // Legacy compatibility
|
||||
originalNodeId: j['originalNodeId'],
|
||||
submittedNodeId: j['submittedNodeId'],
|
||||
attempts: j['attempts'] ?? 0,
|
||||
|
||||
@@ -60,6 +60,13 @@ class NodeCache {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a node by ID from the cache (used for successful deletions)
|
||||
void removeNodeById(int nodeId) {
|
||||
if (_nodes.remove(nodeId) != null) {
|
||||
print('[NodeCache] Removed node $nodeId from cache (successful deletion)');
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove temporary nodes (negative IDs) with _pending_upload marker at the given coordinate
|
||||
/// This is used when a real node ID is assigned to clean up temp placeholders
|
||||
|
||||
+104
-44
@@ -17,7 +17,18 @@ class Uploader {
|
||||
print('Uploader: Starting upload for node at ${p.coord.latitude}, ${p.coord.longitude}');
|
||||
|
||||
// 1. open changeset
|
||||
final action = p.isEdit ? 'Update' : 'Add';
|
||||
String action;
|
||||
switch (p.operation) {
|
||||
case UploadOperation.create:
|
||||
action = 'Add';
|
||||
break;
|
||||
case UploadOperation.modify:
|
||||
action = 'Update';
|
||||
break;
|
||||
case UploadOperation.delete:
|
||||
action = 'Delete';
|
||||
break;
|
||||
}
|
||||
final csXml = '''
|
||||
<osm>
|
||||
<changeset>
|
||||
@@ -35,63 +46,100 @@ class Uploader {
|
||||
final csId = csResp.body.trim();
|
||||
print('Uploader: Created changeset ID: $csId');
|
||||
|
||||
// 2. create or update node
|
||||
final mergedTags = p.getCombinedTags();
|
||||
final tagsXml = mergedTags.entries.map((e) =>
|
||||
'<tag k="${e.key}" v="${e.value}"/>').join('\n ');
|
||||
|
||||
// 2. create, update, or delete node
|
||||
final http.Response nodeResp;
|
||||
final String nodeId;
|
||||
|
||||
if (p.isEdit) {
|
||||
// First, fetch the current node to get its version
|
||||
print('Uploader: Fetching current node ${p.originalNodeId} to get version...');
|
||||
final currentNodeResp = await _get('/api/0.6/node/${p.originalNodeId}');
|
||||
print('Uploader: Current node response: ${currentNodeResp.statusCode}');
|
||||
if (currentNodeResp.statusCode != 200) {
|
||||
print('Uploader: Failed to fetch current node');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse version from the response XML
|
||||
final currentNodeXml = currentNodeResp.body;
|
||||
final versionMatch = RegExp(r'version="(\d+)"').firstMatch(currentNodeXml);
|
||||
if (versionMatch == null) {
|
||||
print('Uploader: Could not parse version from current node XML');
|
||||
return false;
|
||||
}
|
||||
final currentVersion = versionMatch.group(1)!;
|
||||
print('Uploader: Current node version: $currentVersion');
|
||||
|
||||
// Update existing node with version
|
||||
final nodeXml = '''
|
||||
<osm>
|
||||
<node changeset="$csId" id="${p.originalNodeId}" version="$currentVersion" lat="${p.coord.latitude}" lon="${p.coord.longitude}">
|
||||
$tagsXml
|
||||
</node>
|
||||
</osm>''';
|
||||
print('Uploader: Updating node ${p.originalNodeId}...');
|
||||
nodeResp = await _put('/api/0.6/node/${p.originalNodeId}', nodeXml);
|
||||
nodeId = p.originalNodeId.toString();
|
||||
} else {
|
||||
// Create new node
|
||||
final nodeXml = '''
|
||||
switch (p.operation) {
|
||||
case UploadOperation.create:
|
||||
// Create new node
|
||||
final mergedTags = p.getCombinedTags();
|
||||
final tagsXml = mergedTags.entries.map((e) =>
|
||||
'<tag k="${e.key}" v="${e.value}"/>').join('\n ');
|
||||
final nodeXml = '''
|
||||
<osm>
|
||||
<node changeset="$csId" lat="${p.coord.latitude}" lon="${p.coord.longitude}">
|
||||
$tagsXml
|
||||
</node>
|
||||
</osm>''';
|
||||
print('Uploader: Creating new node...');
|
||||
nodeResp = await _put('/api/0.6/node/create', nodeXml);
|
||||
nodeId = nodeResp.body.trim();
|
||||
print('Uploader: Creating new node...');
|
||||
nodeResp = await _put('/api/0.6/node/create', nodeXml);
|
||||
nodeId = nodeResp.body.trim();
|
||||
break;
|
||||
|
||||
case UploadOperation.modify:
|
||||
// First, fetch the current node to get its version
|
||||
print('Uploader: Fetching current node ${p.originalNodeId} to get version...');
|
||||
final currentNodeResp = await _get('/api/0.6/node/${p.originalNodeId}');
|
||||
print('Uploader: Current node response: ${currentNodeResp.statusCode}');
|
||||
if (currentNodeResp.statusCode != 200) {
|
||||
print('Uploader: Failed to fetch current node');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse version from the response XML
|
||||
final currentNodeXml = currentNodeResp.body;
|
||||
final versionMatch = RegExp(r'version="(\d+)"').firstMatch(currentNodeXml);
|
||||
if (versionMatch == null) {
|
||||
print('Uploader: Could not parse version from current node XML');
|
||||
return false;
|
||||
}
|
||||
final currentVersion = versionMatch.group(1)!;
|
||||
print('Uploader: Current node version: $currentVersion');
|
||||
|
||||
// Update existing node with version
|
||||
final mergedTags = p.getCombinedTags();
|
||||
final tagsXml = mergedTags.entries.map((e) =>
|
||||
'<tag k="${e.key}" v="${e.value}"/>').join('\n ');
|
||||
final nodeXml = '''
|
||||
<osm>
|
||||
<node changeset="$csId" id="${p.originalNodeId}" version="$currentVersion" lat="${p.coord.latitude}" lon="${p.coord.longitude}">
|
||||
$tagsXml
|
||||
</node>
|
||||
</osm>''';
|
||||
print('Uploader: Updating node ${p.originalNodeId}...');
|
||||
nodeResp = await _put('/api/0.6/node/${p.originalNodeId}', nodeXml);
|
||||
nodeId = p.originalNodeId.toString();
|
||||
break;
|
||||
|
||||
case UploadOperation.delete:
|
||||
// First, fetch the current node to get its version and coordinates
|
||||
print('Uploader: Fetching current node ${p.originalNodeId} for deletion...');
|
||||
final currentNodeResp = await _get('/api/0.6/node/${p.originalNodeId}');
|
||||
print('Uploader: Current node response: ${currentNodeResp.statusCode}');
|
||||
if (currentNodeResp.statusCode != 200) {
|
||||
print('Uploader: Failed to fetch current node');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse version and tags from the response XML
|
||||
final currentNodeXml = currentNodeResp.body;
|
||||
final versionMatch = RegExp(r'version="(\d+)"').firstMatch(currentNodeXml);
|
||||
if (versionMatch == null) {
|
||||
print('Uploader: Could not parse version from current node XML');
|
||||
return false;
|
||||
}
|
||||
final currentVersion = versionMatch.group(1)!;
|
||||
print('Uploader: Current node version: $currentVersion');
|
||||
|
||||
// Delete node - OSM requires current tags and coordinates
|
||||
final nodeXml = '''
|
||||
<osm>
|
||||
<node changeset="$csId" id="${p.originalNodeId}" version="$currentVersion" lat="${p.coord.latitude}" lon="${p.coord.longitude}">
|
||||
</node>
|
||||
</osm>''';
|
||||
print('Uploader: Deleting node ${p.originalNodeId}...');
|
||||
nodeResp = await _delete('/api/0.6/node/${p.originalNodeId}', nodeXml);
|
||||
nodeId = p.originalNodeId.toString();
|
||||
break;
|
||||
}
|
||||
|
||||
print('Uploader: Node response: ${nodeResp.statusCode} - ${nodeResp.body}');
|
||||
if (nodeResp.statusCode != 200) {
|
||||
print('Uploader: Failed to ${p.isEdit ? "update" : "create"} node');
|
||||
print('Uploader: Failed to ${p.operation.name} node');
|
||||
return false;
|
||||
}
|
||||
print('Uploader: ${p.isEdit ? "Updated" : "Created"} node ID: $nodeId');
|
||||
print('Uploader: ${p.operation.name.capitalize()} node ID: $nodeId');
|
||||
|
||||
// 3. close changeset
|
||||
print('Uploader: Closing changeset...');
|
||||
@@ -135,9 +183,21 @@ class Uploader {
|
||||
body: body,
|
||||
);
|
||||
|
||||
Future<http.Response> _delete(String path, String body) => http.delete(
|
||||
Uri.https(_host, path),
|
||||
headers: _headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
Map<String, String> get _headers => {
|
||||
'Authorization': 'Bearer $accessToken',
|
||||
'Content-Type': 'text/xml',
|
||||
};
|
||||
}
|
||||
|
||||
extension StringExtension on String {
|
||||
String capitalize() {
|
||||
return "${this[0].toUpperCase()}${substring(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/pending_upload.dart';
|
||||
import '../models/osm_camera_node.dart';
|
||||
import '../models/node_profile.dart';
|
||||
import '../services/node_cache.dart';
|
||||
import '../services/uploader.dart';
|
||||
import '../widgets/camera_provider_with_cache.dart';
|
||||
@@ -32,6 +33,7 @@ class UploadQueueState extends ChangeNotifier {
|
||||
profile: session.profile,
|
||||
operatorProfile: session.operatorProfile,
|
||||
uploadMode: uploadMode,
|
||||
operation: UploadOperation.create,
|
||||
);
|
||||
|
||||
_queue.add(upload);
|
||||
@@ -65,6 +67,7 @@ class UploadQueueState extends ChangeNotifier {
|
||||
profile: session.profile,
|
||||
operatorProfile: session.operatorProfile,
|
||||
uploadMode: uploadMode,
|
||||
operation: UploadOperation.modify,
|
||||
originalNodeId: session.originalNode.id, // Track which node we're editing
|
||||
);
|
||||
|
||||
@@ -102,6 +105,37 @@ class UploadQueueState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Add a node deletion to the upload queue
|
||||
void addFromNodeDeletion(OsmCameraNode node, {required UploadMode uploadMode}) {
|
||||
final upload = PendingUpload(
|
||||
coord: node.coord,
|
||||
direction: node.directionDeg ?? 0, // Use existing direction or default to 0
|
||||
profile: NodeProfile.genericAlpr(), // Dummy profile - not used for deletions
|
||||
uploadMode: uploadMode,
|
||||
operation: UploadOperation.delete,
|
||||
originalNodeId: node.id,
|
||||
);
|
||||
|
||||
_queue.add(upload);
|
||||
_saveQueue();
|
||||
|
||||
// Mark the original node as pending deletion in the cache
|
||||
final deletionTags = Map<String, String>.from(node.tags);
|
||||
deletionTags['_pending_deletion'] = 'true';
|
||||
|
||||
final nodeWithDeletionTag = OsmCameraNode(
|
||||
id: node.id,
|
||||
coord: node.coord,
|
||||
tags: deletionTags,
|
||||
);
|
||||
|
||||
NodeCache.instance.addOrUpdate([nodeWithDeletionTag]);
|
||||
// Notify node provider to update the map
|
||||
CameraProviderWithCache.instance.notifyListeners();
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearQueue() {
|
||||
_queue.clear();
|
||||
_saveQueue();
|
||||
@@ -189,14 +223,24 @@ class UploadQueueState extends ChangeNotifier {
|
||||
// Store the submitted node ID for cleanup purposes
|
||||
if (submittedNodeId != null) {
|
||||
item.submittedNodeId = submittedNodeId;
|
||||
debugPrint('[UploadQueue] Upload successful, OSM assigned node ID: $submittedNodeId');
|
||||
|
||||
// Update cache with real node ID instead of temp ID
|
||||
_updateCacheWithRealNodeId(item, submittedNodeId);
|
||||
if (item.isDeletion) {
|
||||
debugPrint('[UploadQueue] Deletion successful, removing node ID: $submittedNodeId from cache');
|
||||
_handleSuccessfulDeletion(item);
|
||||
} else {
|
||||
debugPrint('[UploadQueue] Upload successful, OSM assigned node ID: $submittedNodeId');
|
||||
// Update cache with real node ID instead of temp ID
|
||||
_updateCacheWithRealNodeId(item, submittedNodeId);
|
||||
}
|
||||
} else if (simulatedNodeId != null && item.uploadMode == UploadMode.simulate) {
|
||||
// For simulate mode, use a fake but positive ID
|
||||
item.submittedNodeId = simulatedNodeId;
|
||||
debugPrint('[UploadQueue] Simulated upload, fake node ID: $simulatedNodeId');
|
||||
if (item.isDeletion) {
|
||||
debugPrint('[UploadQueue] Simulated deletion, removing fake node ID: $simulatedNodeId from cache');
|
||||
_handleSuccessfulDeletion(item);
|
||||
} else {
|
||||
debugPrint('[UploadQueue] Simulated upload, fake node ID: $simulatedNodeId');
|
||||
}
|
||||
}
|
||||
|
||||
_saveQueue();
|
||||
@@ -238,6 +282,17 @@ class UploadQueueState extends ChangeNotifier {
|
||||
CameraProviderWithCache.instance.notifyListeners();
|
||||
}
|
||||
|
||||
// Handle successful deletion by removing the node from cache
|
||||
void _handleSuccessfulDeletion(PendingUpload item) {
|
||||
if (item.originalNodeId != null) {
|
||||
// Remove the node from cache entirely
|
||||
NodeCache.instance.removeNodeById(item.originalNodeId!);
|
||||
|
||||
// Notify node provider to update the map
|
||||
CameraProviderWithCache.instance.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Queue persistence ----------
|
||||
Future<void> _saveQueue() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
@@ -2,11 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import '../dev_config.dart';
|
||||
|
||||
enum CameraIconType {
|
||||
real, // Blue ring - real cameras from OSM
|
||||
mock, // White ring - add camera mock point
|
||||
pending, // Purple ring - submitted/pending cameras
|
||||
editing, // Orange ring - camera being edited
|
||||
pendingEdit, // Grey ring - original camera with pending edit
|
||||
real, // Blue ring - real cameras from OSM
|
||||
mock, // White ring - add camera mock point
|
||||
pending, // Purple ring - submitted/pending cameras
|
||||
editing, // Orange ring - camera being edited
|
||||
pendingEdit, // Grey ring - original camera with pending edit
|
||||
pendingDeletion, // Red ring - camera pending deletion
|
||||
}
|
||||
|
||||
/// Simple camera icon with grey dot and colored ring
|
||||
@@ -27,6 +28,8 @@ class CameraIcon extends StatelessWidget {
|
||||
return kCameraRingColorEditing;
|
||||
case CameraIconType.pendingEdit:
|
||||
return kCameraRingColorPendingEdit;
|
||||
case CameraIconType.pendingDeletion:
|
||||
return kCameraRingColorPendingDeletion;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,9 +51,13 @@ class _CameraMapMarkerState extends State<CameraMapMarker> {
|
||||
widget.node.tags['_pending_upload'] == 'true';
|
||||
final isPendingEdit = widget.node.tags.containsKey('_pending_edit') &&
|
||||
widget.node.tags['_pending_edit'] == 'true';
|
||||
final isPendingDeletion = widget.node.tags.containsKey('_pending_deletion') &&
|
||||
widget.node.tags['_pending_deletion'] == 'true';
|
||||
|
||||
CameraIconType iconType;
|
||||
if (isPendingUpload) {
|
||||
if (isPendingDeletion) {
|
||||
iconType = CameraIconType.pendingDeletion;
|
||||
} else if (isPendingUpload) {
|
||||
iconType = CameraIconType.pending;
|
||||
} else if (isPendingEdit) {
|
||||
iconType = CameraIconType.pendingEdit;
|
||||
|
||||
@@ -17,17 +17,50 @@ class NodeTagSheet extends StatelessWidget {
|
||||
final appState = context.watch<AppState>();
|
||||
final locService = LocalizationService.instance;
|
||||
|
||||
// Check if this device is editable (not a pending upload or pending edit)
|
||||
// Check if this device is editable (not a pending upload, pending edit, or pending deletion)
|
||||
final isEditable = (!node.tags.containsKey('_pending_upload') ||
|
||||
node.tags['_pending_upload'] != 'true') &&
|
||||
(!node.tags.containsKey('_pending_edit') ||
|
||||
node.tags['_pending_edit'] != 'true');
|
||||
node.tags['_pending_edit'] != 'true') &&
|
||||
(!node.tags.containsKey('_pending_deletion') ||
|
||||
node.tags['_pending_deletion'] != 'true');
|
||||
|
||||
void _openEditSheet() {
|
||||
Navigator.pop(context); // Close this sheet first
|
||||
appState.startEditSession(node); // HomeScreen will auto-show the edit sheet
|
||||
}
|
||||
|
||||
void _deleteNode() async {
|
||||
final shouldDelete = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(locService.t('node.confirmDeleteTitle')),
|
||||
content: Text(locService.t('node.confirmDeleteMessage', params: [node.id.toString()])),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(locService.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: Text(locService.t('actions.delete')),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (shouldDelete == true && context.mounted) {
|
||||
Navigator.pop(context); // Close this sheet first
|
||||
appState.deleteNode(node);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(locService.t('node.deleteQueuedForUpload'))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||
@@ -81,6 +114,16 @@ class NodeTagSheet extends StatelessWidget {
|
||||
minimumSize: const Size(0, 36),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _deleteNode,
|
||||
icon: const Icon(Icons.delete, size: 18),
|
||||
label: Text(locService.t('actions.delete')),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(0, 36),
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
TextButton(
|
||||
|
||||
Reference in New Issue
Block a user