Repopulate node cache from pending

This commit is contained in:
stopflock
2025-12-02 19:16:33 -06:00
parent d902495312
commit 5043ef3e34
17 changed files with 335 additions and 54 deletions

View File

@@ -227,6 +227,12 @@ The previous implementation conflated changeset creation + node operation as one
**Why immediate visual feedback:**
Users expect instant response to their actions. By immediately updating the cache with temporary markers (e.g., `_pending_deletion`), the UI stays responsive while the actual API calls happen in background.
**Queue persistence & cache synchronization (v1.5.4+):**
- **Startup repopulation**: Queue initialization now repopulates cache with pending nodes, ensuring visual continuity after app restarts
- **Specific node cleanup**: Each upload stores a `tempNodeId` for precise removal, preventing accidental cleanup of other pending nodes at the same location
- **Proximity awareness**: Proximity warnings now consider pending nodes to prevent duplicate submissions at the same location
- **Processing status UI**: Upload queue screen shows clear indicators when processing is paused due to offline mode or user settings
### 4. Cache & Visual States
**Node visual states:**

View File

@@ -107,7 +107,6 @@ cp lib/keys.dart.example lib/keys.dart
- Manual cleanup (cognitive load for users)
- Delete the old one (also wrong answer unless user chooses intentionally)
- Give multiple of these options??
- Persistent cache for MY submissions: assume submissions worked, cache,clean up when we see that node appear in overpass/OSM results or when older than 24h
- Dropdown on "refine tags" page to select acceptable options for camera:mount= (is this a boolean property of a profile?)
- Option to pull in profiles from NSI (man_made=surveillance only?)

View File

@@ -2,9 +2,13 @@
"1.5.4": {
"content": [
"• OSM message notifications - dot appears on Settings button and OSM Account section when you have unread messages on OpenStreetMap",
"• Download area max zoom level is now limited to the currently selected tile provider's maximum zoom level",
"• Download area max zoom level is now limited to the currently selected tile provider's maximum zoom level",
"• Navigation route planning now prevents selecting start and end locations that are too close together",
"• Cleaned up internal 'maxCameras' references to use 'maxNodes' terminology consistently"
"• Cleaned up internal 'maxCameras' references to use 'maxNodes' terminology consistently",
"• FIXED: Proximity warnings now consider pending nodes - prevents submitting multiple nodes at the same location without warning",
"• FIXED: Deleting queue items now only removes that specific item, not all pending nodes at the same location",
"• FIXED: Pending nodes now reappear on the map after app restart - queue items repopulate the visual cache on startup",
"• NEW: Upload queue screen shows when processing is paused (offline mode or manually paused)"
]
},
"1.5.3": {

View File

@@ -18,6 +18,7 @@ import 'services/node_cache.dart';
import 'services/tile_preview_service.dart';
import 'services/changelog_service.dart';
import 'services/operator_profile_service.dart';
import 'widgets/camera_provider_with_cache.dart';
import 'services/profile_service.dart';
import 'widgets/proximity_warning_dialog.dart';
import 'widgets/reauth_messages_dialog.dart';
@@ -212,6 +213,11 @@ class AppState extends ChangeNotifier {
await _uploadQueueState.init();
await _authState.init(_settingsState.uploadMode);
// Set up callback to repopulate pending nodes after cache clears
CameraProviderWithCache.instance.setOnCacheClearedCallback(() {
_uploadQueueState.repopulateCacheFromQueue();
});
// Check for messages on app launch if user is already logged in
if (isLoggedIn) {
checkMessages();

View File

@@ -152,7 +152,8 @@
"simulate": "Simulieren",
"productionDescription": "Hochladen in die Live-OSM-Datenbank (für alle Benutzer sichtbar)",
"sandboxDescription": "Uploads gehen an die OSM Sandbox (sicher zum Testen, wird regelmäßig zurückgesetzt).",
"simulateDescription": "Uploads simulieren (kontaktiert OSM-Server nicht)"
"simulateDescription": "Uploads simulieren (kontaktiert OSM-Server nicht)",
"cannotChangeWithQueue": "Upload-Ziel kann nicht geändert werden, während {} Elemente in der Warteschlange sind. Warteschlange zuerst leeren."
},
"auth": {
"osmAccountTitle": "OpenStreetMap-Konto",
@@ -220,7 +221,10 @@
"errorDetails": "Fehlerdetails",
"creatingChangeset": " (Changeset erstellen...)",
"uploading": " (Uploading...)",
"closingChangeset": " (Changeset schließen...)"
"closingChangeset": " (Changeset schließen...)",
"processingPaused": "Warteschlangenverarbeitung pausiert",
"pausedDueToOffline": "Upload-Verarbeitung ist pausiert, da der Offline-Modus aktiviert ist.",
"pausedByUser": "Upload-Verarbeitung ist manuell pausiert."
},
"tileProviders": {
"title": "Kachel-Anbieter",

View File

@@ -184,7 +184,8 @@
"simulate": "Simulate",
"productionDescription": "Upload to the live OSM database (visible to all users)",
"sandboxDescription": "Uploads go to the OSM Sandbox (safe for testing, resets regularly).",
"simulateDescription": "Simulate uploads (does not contact OSM servers)"
"simulateDescription": "Simulate uploads (does not contact OSM servers)",
"cannotChangeWithQueue": "Cannot change upload destination while {} items are in queue. Clear queue first."
},
"auth": {
"osmAccountTitle": "OpenStreetMap Account",
@@ -252,7 +253,10 @@
"errorDetails": "Error Details",
"creatingChangeset": " (Creating changeset...)",
"uploading": " (Uploading...)",
"closingChangeset": " (Closing changeset...)"
"closingChangeset": " (Closing changeset...)",
"processingPaused": "Queue Processing Paused",
"pausedDueToOffline": "Upload processing is paused because offline mode is enabled.",
"pausedByUser": "Upload processing is manually paused."
},
"tileProviders": {
"title": "Tile Providers",

View File

@@ -184,7 +184,8 @@
"simulate": "Simular",
"productionDescription": "Subir a la base de datos OSM en vivo (visible para todos los usuarios)",
"sandboxDescription": "Las subidas van al Sandbox de OSM (seguro para pruebas, se reinicia regularmente).",
"simulateDescription": "Simular subidas (no contacta servidores OSM)"
"simulateDescription": "Simular subidas (no contacta servidores OSM)",
"cannotChangeWithQueue": "No se puede cambiar el destino de subida mientras hay {} elementos en cola. Limpie la cola primero."
},
"auth": {
"osmAccountTitle": "Cuenta de OpenStreetMap",
@@ -252,7 +253,10 @@
"errorDetails": "Detalles del Error",
"creatingChangeset": " (Creando changeset...)",
"uploading": " (Subiendo...)",
"closingChangeset": " (Cerrando changeset...)"
"closingChangeset": " (Cerrando changeset...)",
"processingPaused": "Procesamiento de Cola Pausado",
"pausedDueToOffline": "El procesamiento de subida está pausado porque el modo sin conexión está habilitado.",
"pausedByUser": "El procesamiento de subida está pausado manualmente."
},
"tileProviders": {
"title": "Proveedores de Tiles",

View File

@@ -184,7 +184,8 @@
"simulate": "Simuler",
"productionDescription": "Télécharger vers la base de données OSM en direct (visible pour tous les utilisateurs)",
"sandboxDescription": "Les téléchargements vont vers le Sandbox OSM (sûr pour les tests, réinitialisé régulièrement).",
"simulateDescription": "Simuler les téléchargements (ne contacte pas les serveurs OSM)"
"simulateDescription": "Simuler les téléchargements (ne contacte pas les serveurs OSM)",
"cannotChangeWithQueue": "Impossible de changer la destination de téléversement tant que {} éléments sont en file d'attente. Videz d'abord la file d'attente."
},
"auth": {
"osmAccountTitle": "Compte OpenStreetMap",
@@ -252,7 +253,10 @@
"errorDetails": "Détails de l'Erreur",
"creatingChangeset": " (Création du changeset...)",
"uploading": " (Téléchargement...)",
"closingChangeset": " (Fermeture du changeset...)"
"closingChangeset": " (Fermeture du changeset...)",
"processingPaused": "Traitement de la File d'Attente Interrompu",
"pausedDueToOffline": "Le traitement des téléversements est interrompu car le mode hors ligne est activé.",
"pausedByUser": "Le traitement des téléversements est interrompu manuellement."
},
"tileProviders": {
"title": "Fournisseurs de Tuiles",

View File

@@ -184,7 +184,8 @@
"simulate": "Simula",
"productionDescription": "Carica nel database OSM dal vivo (visibile a tutti gli utenti)",
"sandboxDescription": "Gli upload vanno alla Sandbox OSM (sicuro per i test, si resetta regolarmente).",
"simulateDescription": "Simula upload (non contatta i server OSM)"
"simulateDescription": "Simula upload (non contatta i server OSM)",
"cannotChangeWithQueue": "Impossibile cambiare la destinazione di upload mentre ci sono {} elementi in coda. Svuota prima la coda."
},
"auth": {
"osmAccountTitle": "Account OpenStreetMap",
@@ -252,7 +253,10 @@
"errorDetails": "Dettagli dell'Errore",
"creatingChangeset": " (Creazione changeset...)",
"uploading": " (Caricamento...)",
"closingChangeset": " (Chiusura changeset...)"
"closingChangeset": " (Chiusura changeset...)",
"processingPaused": "Elaborazione Coda Sospesa",
"pausedDueToOffline": "L'elaborazione dei caricamenti è sospesa perché la modalità offline è abilitata.",
"pausedByUser": "L'elaborazione dei caricamenti è sospesa manualmente."
},
"tileProviders": {
"title": "Fornitori di Tile",

View File

@@ -184,7 +184,8 @@
"simulate": "Simular",
"productionDescription": "Enviar para o banco de dados OSM ao vivo (visível para todos os usuários)",
"sandboxDescription": "Uploads vão para o Sandbox OSM (seguro para testes, redefine regularmente).",
"simulateDescription": "Simular uploads (não contacta servidores OSM)"
"simulateDescription": "Simular uploads (não contacta servidores OSM)",
"cannotChangeWithQueue": "Não é possível alterar o destino de upload enquanto {} itens estão na fila. Limpe a fila primeiro."
},
"auth": {
"osmAccountTitle": "Conta OpenStreetMap",
@@ -252,7 +253,10 @@
"errorDetails": "Detalhes do Erro",
"creatingChangeset": " (Criando changeset...)",
"uploading": " (Enviando...)",
"closingChangeset": " (Fechando changeset...)"
"closingChangeset": " (Fechando changeset...)",
"processingPaused": "Processamento da Fila Pausado",
"pausedDueToOffline": "O processamento de upload está pausado porque o modo offline está habilitado.",
"pausedByUser": "O processamento de upload está pausado manualmente."
},
"tileProviders": {
"title": "Provedores de Tiles",

View File

@@ -184,7 +184,8 @@
"simulate": "模拟",
"productionDescription": "上传到实时 OSM 数据库(对所有用户可见)",
"sandboxDescription": "上传到 OSM 沙盒(测试安全,定期重置)。",
"simulateDescription": "模拟上传(不联系 OSM 服务器)"
"simulateDescription": "模拟上传(不联系 OSM 服务器)",
"cannotChangeWithQueue": "队列中有 {} 个项目时无法更改上传目标。请先清空队列。"
},
"auth": {
"osmAccountTitle": "OpenStreetMap 账户",
@@ -252,7 +253,10 @@
"errorDetails": "错误详情",
"creatingChangeset": " (创建变更集...)",
"uploading": " (上传中...)",
"closingChangeset": " (关闭变更集...)"
"closingChangeset": " (关闭变更集...)",
"processingPaused": "队列处理已暂停",
"pausedDueToOffline": "因为离线模式已启用,上传处理已暂停。",
"pausedByUser": "上传处理已手动暂停。"
},
"tileProviders": {
"title": "瓦片提供商",

View File

@@ -25,6 +25,7 @@ class PendingUpload {
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? tempNodeId; // ID of temporary node created in cache (for specific cleanup)
int attempts;
bool error; // DEPRECATED: Use uploadState instead
String? errorMessage; // Detailed error message for debugging
@@ -46,6 +47,7 @@ class PendingUpload {
required this.operation,
this.originalNodeId,
this.submittedNodeId,
this.tempNodeId,
this.attempts = 0,
this.error = false,
this.errorMessage,
@@ -255,6 +257,7 @@ class PendingUpload {
'operation': operation.index,
'originalNodeId': originalNodeId,
'submittedNodeId': submittedNodeId,
'tempNodeId': tempNodeId,
'attempts': attempts,
'error': error,
'errorMessage': errorMessage,
@@ -285,6 +288,7 @@ class PendingUpload {
: (j['originalNodeId'] != null ? UploadOperation.modify : UploadOperation.create), // Legacy compatibility
originalNodeId: j['originalNodeId'],
submittedNodeId: j['submittedNodeId'],
tempNodeId: j['tempNodeId'],
attempts: j['attempts'] ?? 0,
error: j['error'] ?? false,
errorMessage: j['errorMessage'], // Can be null for legacy entries

View File

@@ -37,7 +37,7 @@ class UploadModeSection extends StatelessWidget {
child: Text(locService.t('uploadMode.simulate')),
),
],
onChanged: (mode) {
onChanged: appState.pendingCount > 0 ? null : (mode) {
if (mode != null) {
appState.setUploadMode(mode);
// Check if re-authentication is needed after mode change
@@ -50,27 +50,65 @@ class UploadModeSection extends StatelessWidget {
),
Padding(
padding: const EdgeInsets.only(left: 56, top: 2, right: 16, bottom: 12),
child: Builder(
builder: (context) {
switch (appState.uploadMode) {
case UploadMode.production:
return Text(
locService.t('uploadMode.productionDescription'),
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7))
);
case UploadMode.sandbox:
return Text(
locService.t('uploadMode.sandboxDescription'),
style: const TextStyle(fontSize: 12, color: Colors.orange),
);
case UploadMode.simulate:
default:
return Text(
locService.t('uploadMode.simulateDescription'),
style: const TextStyle(fontSize: 12, color: Colors.deepPurple)
);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Upload mode restriction message when queue has items
if (appState.pendingCount > 0) ...[
Row(
children: [
const Icon(Icons.info_outline, color: Colors.orange, size: 16),
const SizedBox(width: 6),
Expanded(
child: Text(
locService.t('uploadMode.cannotChangeWithQueue', params: [appState.pendingCount.toString()]),
style: const TextStyle(fontSize: 12, color: Colors.orange),
),
),
],
),
const SizedBox(height: 6),
],
// Normal upload mode description
Builder(
builder: (context) {
switch (appState.uploadMode) {
case UploadMode.production:
return Text(
locService.t('uploadMode.productionDescription'),
style: TextStyle(
fontSize: 12,
color: appState.pendingCount > 0
? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.onSurface.withOpacity(0.7)
)
);
case UploadMode.sandbox:
return Text(
locService.t('uploadMode.sandboxDescription'),
style: TextStyle(
fontSize: 12,
color: appState.pendingCount > 0
? Theme.of(context).disabledColor
: Colors.orange
),
);
case UploadMode.simulate:
default:
return Text(
locService.t('uploadMode.simulateDescription'),
style: TextStyle(
fontSize: 12,
color: appState.pendingCount > 0
? Theme.of(context).disabledColor
: Colors.deepPurple
)
);
}
},
),
],
),
),
],

View File

@@ -93,6 +93,9 @@ class UploadQueueScreen extends StatelessWidget {
final locService = LocalizationService.instance;
final appState = context.watch<AppState>();
// Check if queue processing is paused
final isQueuePaused = appState.offlineMode || appState.pauseQueueProcessing;
return Scaffold(
appBar: AppBar(
title: Text(locService.t('queue.title')),
@@ -105,6 +108,46 @@ class UploadQueueScreen extends StatelessWidget {
16 + MediaQuery.of(context).padding.bottom,
),
children: [
// Queue processing status indicator
if (isQueuePaused && appState.pendingCount > 0)
Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
border: Border.all(color: Colors.orange.withOpacity(0.3)),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.pause_circle_outline, color: Colors.orange),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
locService.t('queue.processingPaused'),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
Text(
appState.offlineMode
? locService.t('queue.pausedDueToOffline')
: locService.t('queue.pausedByUser'),
style: TextStyle(
fontSize: 12,
color: Colors.orange.shade700,
),
),
],
),
),
],
),
),
// Clear Upload Queue button - always visible
SizedBox(
width: double.infinity,

View File

@@ -114,6 +114,23 @@ class NodeCache {
print('[NodeCache] Removed ${nodesToRemove.length} temp nodes at coordinate ${coord.latitude}, ${coord.longitude}');
}
}
/// Remove a specific temporary node by its ID (for queue item-specific cleanup)
void removeTempNodeById(int tempNodeId) {
if (tempNodeId >= 0) {
print('[NodeCache] Warning: Attempted to remove non-temp node ID $tempNodeId');
return;
}
if (_nodes.remove(tempNodeId) != null) {
print('[NodeCache] Removed specific temp node $tempNodeId from cache');
}
}
/// Get a specific node by ID (returns null if not found)
OsmNode? getNodeById(int nodeId) {
return _nodes[nodeId];
}
/// Check if two coordinates match within tolerance
bool _coordsMatch(LatLng coord1, LatLng coord2, double tolerance) {
@@ -123,6 +140,7 @@ class NodeCache {
/// Find nodes within the specified distance (in meters) of the given coordinate
/// Excludes nodes with the excludeNodeId (useful when checking proximity for edited nodes)
/// Includes pending nodes to warn about potential duplicates
List<OsmNode> findNodesWithinDistance(LatLng coord, double distanceMeters, {int? excludeNodeId}) {
final nearbyNodes = <OsmNode>[];
@@ -132,11 +150,9 @@ class NodeCache {
continue;
}
// Skip temporary nodes (negative IDs) with pending upload/edit/deletion markers
if (node.id < 0 && (
node.tags.containsKey('_pending_upload') ||
node.tags.containsKey('_pending_edit') ||
node.tags.containsKey('_pending_deletion'))) {
// Include all nodes (real and pending) to catch potential duplicates
// Only skip nodes marked for deletion since they won't actually exist after processing
if (node.tags.containsKey('_pending_deletion')) {
continue;
}

View File

@@ -21,9 +21,100 @@ class UploadQueueState extends ChangeNotifier {
int get pendingCount => _queue.length;
List<PendingUpload> get pendingUploads => List.unmodifiable(_queue);
// Initialize by loading queue from storage
// Initialize by loading queue from storage and repopulate cache with pending nodes
Future<void> init() async {
await _loadQueue();
print('[UploadQueue] Loaded ${_queue.length} items from storage');
_repopulateCacheFromQueue();
}
// Repopulate the cache with pending nodes from the queue on startup
void _repopulateCacheFromQueue() {
print('[UploadQueue] Repopulating cache from ${_queue.length} queue items');
final nodesToAdd = <OsmNode>[];
for (final upload in _queue) {
// Skip completed uploads - they should already be in OSM and will be fetched normally
if (upload.isComplete) {
print('[UploadQueue] Skipping completed upload at ${upload.coord}');
continue;
}
print('[UploadQueue] Processing ${upload.operation} upload at ${upload.coord}');
if (upload.isDeletion) {
// For deletions: mark the original node as pending deletion if it exists in cache
if (upload.originalNodeId != null) {
final existingNode = NodeCache.instance.getNodeById(upload.originalNodeId!);
if (existingNode != null) {
final deletionTags = Map<String, String>.from(existingNode.tags);
deletionTags['_pending_deletion'] = 'true';
final nodeWithDeletionTag = OsmNode(
id: upload.originalNodeId!,
coord: existingNode.coord,
tags: deletionTags,
);
nodesToAdd.add(nodeWithDeletionTag);
}
}
} else {
// For creates, edits, and extracts: recreate temp node if needed
// Generate new temp ID if not already stored (for backward compatibility)
final tempId = upload.tempNodeId ?? -DateTime.now().millisecondsSinceEpoch - _queue.indexOf(upload);
final tags = upload.getCombinedTags();
tags['_pending_upload'] = 'true';
tags['_temp_id'] = tempId.toString();
// Store temp ID for future cleanup if not already set
if (upload.tempNodeId == null) {
upload.tempNodeId = tempId;
}
if (upload.isEdit) {
// For edits: also mark original with _pending_edit if it exists
if (upload.originalNodeId != null) {
final existingOriginal = NodeCache.instance.getNodeById(upload.originalNodeId!);
if (existingOriginal != null) {
final originalTags = Map<String, String>.from(existingOriginal.tags);
originalTags['_pending_edit'] = 'true';
final originalWithEdit = OsmNode(
id: upload.originalNodeId!,
coord: existingOriginal.coord,
tags: originalTags,
);
nodesToAdd.add(originalWithEdit);
}
}
// Add connection line marker
tags['_original_node_id'] = upload.originalNodeId.toString();
} else if (upload.operation == UploadOperation.extract) {
// For extracts: add connection line marker
tags['_original_node_id'] = upload.originalNodeId.toString();
}
final tempNode = OsmNode(
id: tempId,
coord: upload.coord,
tags: tags,
);
nodesToAdd.add(tempNode);
}
}
if (nodesToAdd.isNotEmpty) {
NodeCache.instance.addOrUpdate(nodesToAdd);
print('[UploadQueue] Repopulated cache with ${nodesToAdd.length} pending nodes from queue');
// Save queue if we updated any temp IDs for backward compatibility
_saveQueue();
// Notify node provider to update the map
CameraProviderWithCache.instance.notifyListeners();
}
}
// Add a completed session to the upload queue
@@ -46,6 +137,10 @@ class UploadQueueState extends ChangeNotifier {
final tempId = -DateTime.now().millisecondsSinceEpoch;
final tags = upload.getCombinedTags();
tags['_pending_upload'] = 'true'; // Mark as pending for potential UI distinction
tags['_temp_id'] = tempId.toString(); // Store temp ID for specific removal
// Store the temp ID in the upload for cleanup purposes
upload.tempNodeId = tempId;
final tempNode = OsmNode(
id: tempId,
@@ -100,6 +195,10 @@ class UploadQueueState extends ChangeNotifier {
final extractedTags = upload.getCombinedTags();
extractedTags['_pending_upload'] = 'true'; // Mark as pending upload
extractedTags['_original_node_id'] = session.originalNode.id.toString(); // Track original for line drawing
extractedTags['_temp_id'] = tempId.toString(); // Store temp ID for specific removal
// Store the temp ID in the upload for cleanup purposes
upload.tempNodeId = tempId;
final extractedNode = OsmNode(
id: tempId,
@@ -125,6 +224,10 @@ class UploadQueueState extends ChangeNotifier {
final editedTags = upload.getCombinedTags();
editedTags['_pending_upload'] = 'true'; // Mark as pending upload
editedTags['_original_node_id'] = session.originalNode.id.toString(); // Track original for line drawing
editedTags['_temp_id'] = tempId.toString(); // Store temp ID for specific removal
// Store the temp ID in the upload for cleanup purposes
upload.tempNodeId = tempId;
final editedNode = OsmNode(
id: tempId,
@@ -548,8 +651,10 @@ class UploadQueueState extends ChangeNotifier {
// Add/update the cache with the real node
NodeCache.instance.addOrUpdate([realNode]);
// Clean up any temp nodes at the same coordinate
NodeCache.instance.removeTempNodesByCoordinate(item.coord);
// Clean up the specific temp node for this upload
if (item.tempNodeId != null) {
NodeCache.instance.removeTempNodeById(item.tempNodeId!);
}
// For modify operations, clean up the original node's _pending_edit marker
// For extract operations, we don't modify the original node so leave it unchanged
@@ -609,17 +714,23 @@ class UploadQueueState extends ChangeNotifier {
NodeCache.instance.removePendingDeletionMarker(upload.originalNodeId!);
}
} else if (upload.isEdit) {
// For edits: remove both the temp node and the _pending_edit marker from original
NodeCache.instance.removeTempNodesByCoordinate(upload.coord);
// For edits: remove the specific temp node and the _pending_edit marker from original
if (upload.tempNodeId != null) {
NodeCache.instance.removeTempNodeById(upload.tempNodeId!);
}
if (upload.originalNodeId != null) {
NodeCache.instance.removePendingEditMarker(upload.originalNodeId!);
}
} else if (upload.operation == UploadOperation.extract) {
// For extracts: remove the temp node (leave original unchanged)
NodeCache.instance.removeTempNodesByCoordinate(upload.coord);
// For extracts: remove the specific temp node (leave original unchanged)
if (upload.tempNodeId != null) {
NodeCache.instance.removeTempNodeById(upload.tempNodeId!);
}
} else {
// For creates: remove the temp node
NodeCache.instance.removeTempNodesByCoordinate(upload.coord);
// For creates: remove the specific temp node
if (upload.tempNodeId != null) {
NodeCache.instance.removeTempNodeById(upload.tempNodeId!);
}
}
}
@@ -646,6 +757,11 @@ class UploadQueueState extends ChangeNotifier {
notifyListeners();
}
// Public method to manually trigger cache repopulation (useful for debugging or after cache clears)
void repopulateCacheFromQueue() {
_repopulateCacheFromQueue();
}
@override
void dispose() {
_uploadTimer?.cancel();

View File

@@ -67,12 +67,33 @@ class CameraProviderWithCache extends ChangeNotifier {
});
}
/// Optionally: clear the cache (could be used for testing/dev)
/// Clear the cache and repopulate with pending nodes from upload queue
void clearCache() {
NodeCache.instance.clear();
// Repopulate with pending nodes from upload queue if available
_repopulatePendingNodesAfterClear();
notifyListeners();
}
/// Repopulate pending nodes after cache clear
void _repopulatePendingNodesAfterClear() {
// We need access to the upload queue state, but we don't have direct access here
// Instead, we'll trigger a callback that the app state can handle
// For now, let's use a more direct approach through a global service access
// This could be refactored to use proper dependency injection later
Future.microtask(() {
// This will be called from app state when cache clears happen
_onCacheCleared?.call();
});
}
VoidCallback? _onCacheCleared;
/// Set callback for when cache is cleared (used by app state to repopulate pending nodes)
void setOnCacheClearedCallback(VoidCallback? callback) {
_onCacheCleared = callback;
}
/// Force refresh the display (useful when filters change but cache doesn't)
void refreshDisplay() {
notifyListeners();