diff --git a/assets/changelog.json b/assets/changelog.json
index c4d3718..ff204d3 100644
--- a/assets/changelog.json
+++ b/assets/changelog.json
@@ -1,4 +1,11 @@
{
+ "2.9.2": {
+ "content": [
+ "• Moved 'About OpenStreetMap' section from OSM Account page to Settings > About for better organization",
+ "• Added 'Clear Caches' option to tile provider menus - easily free up storage space by clearing cached tiles for specific providers",
+ "• Enhanced node deletion workflow - users can now provide an informative reason when deleting surveillance devices"
+ ]
+ },
"2.9.1": {
"content": [
"• When hitting node render limit, only render nodes closest to center of viewport.",
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
index 1dc6cf7..391a902 100644
--- a/ios/Flutter/AppFrameworkInfo.plist
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -20,7 +20,5 @@
????
CFBundleVersion
1.0
- MinimumOSVersion
- 13.0
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 6266644..c30b367 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -2,12 +2,15 @@ import Flutter
import UIKit
@main
-@objc class AppDelegate: FlutterAppDelegate {
+@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
- GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
+
+ func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
+ GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
+ }
}
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 84d4253..8f414e4 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -1,72 +1,91 @@
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ DeFlock
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ deflockapp
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ None
+ CFBundleURLSchemes
+
+ deflockapp
+
+
+
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSApplicationQueriesSchemes
+
+ https
+
+ LSRequiresIPhoneOS
+
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ This app optionally uses your location to center the map on your current position and provide proximity alerts for nearby surveillance devices. These features are entirely optional.
+ NSLocationWhenInUseUsageDescription
+ This app optionally uses your location to show nearby cameras by centering the map on your location.
+ UIApplicationSceneManifest
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- DeFlock
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- deflockapp
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- NSLocationWhenInUseUsageDescription
- This app optionally uses your location to show nearby cameras by centering the map on your location.
- NSLocationAlwaysAndWhenInUseUsageDescription
- This app optionally uses your location to center the map on your current position and provide proximity alerts for nearby surveillance devices. These features are entirely optional.
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- CADisableMinimumFrameDurationOnPhone
-
- UIApplicationSupportsIndirectInputEvents
-
-
- CFBundleURLTypes
-
-
- CFBundleTypeRole
- None
- CFBundleURLSchemes
-
- deflockapp
-
-
-
-
- LSApplicationQueriesSchemes
-
- https
-
- UIStatusBarHidden
+ UIApplicationSupportsMultipleScenes
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneClassName
+ UIWindowScene
+ UISceneConfigurationName
+ flutter
+ UISceneDelegateClassName
+ FlutterSceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIStatusBarHidden
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
diff --git a/lib/app_state.dart b/lib/app_state.dart
index a208d63..9991cb6 100644
--- a/lib/app_state.dart
+++ b/lib/app_state.dart
@@ -581,8 +581,8 @@ class AppState extends ChangeNotifier {
}
}
- void deleteNode(OsmNode node) {
- _uploadQueueState.addFromNodeDeletion(node, uploadMode: uploadMode);
+ void deleteNode(OsmNode node, {String? changesetComment}) {
+ _uploadQueueState.addFromNodeDeletion(node, uploadMode: uploadMode, changesetComment: changesetComment);
_startUploader();
}
@@ -717,6 +717,11 @@ class AppState extends ChangeNotifier {
await _settingsState.deleteTileProvider(providerId);
}
+ /// Clear all tile caches for a specific provider
+ Future clearTileProviderCaches(String providerId) async {
+ await _settingsState.clearTileProviderCaches(providerId);
+ }
+
/// Set follow-me mode
Future setFollowMeMode(FollowMeMode mode) async {
await _settingsState.setFollowMeMode(mode);
diff --git a/lib/dev_config.dart b/lib/dev_config.dart
index e10bfc9..d05f6c9 100644
--- a/lib/dev_config.dart
+++ b/lib/dev_config.dart
@@ -90,7 +90,7 @@ bool enableNavigationFeatures({required bool offlineMode}) {
// Marker/node interaction
const int kNodeMinZoomLevel = 10; // Minimum zoom to show nodes (Overpass)
const int kOsmApiMinZoomLevel = 13; // Minimum zoom for OSM API bbox queries (sandbox mode)
-const int kMinZoomForNodeEditingSheets = 15; // Minimum zoom to open add/edit node sheets
+const int kMinZoomForNodeEditingSheets = 16; // Minimum zoom to open add/edit node sheets
const int kMinZoomForOfflineDownload = 10; // Minimum zoom to download offline areas (prevents large area crashes)
const Duration kMarkerTapTimeout = Duration(milliseconds: 250);
const Duration kDebounceCameraRefresh = Duration(milliseconds: 500);
@@ -128,7 +128,7 @@ const int kProximityAlertMaxDistance = 1600; // meters
const Duration kProximityAlertCooldown = Duration(minutes: 10); // Cooldown between alerts for same node
// Node proximity warning configuration (for new/edited nodes that are too close to existing ones)
-const double kNodeProximityWarningDistance = 15.0; // meters - distance threshold to show warning
+const double kNodeProximityWarningDistance = 50.0; // meters - distance threshold to show warning
// Positioning tutorial configuration
const double kPositioningTutorialBlurSigma = 3.0; // Blur strength for sheet overlay
@@ -136,7 +136,7 @@ const double kPositioningTutorialMinMovementMeters = 1.0; // Minimum map movemen
// Navigation route planning configuration
const double kNavigationMinRouteDistance = 100.0; // meters - minimum distance between start and end points
-const double kNavigationDistanceWarningThreshold = 20000.0; // meters - distance threshold for timeout warning (30km)
+const double kNavigationDistanceWarningThreshold = 300000.0; // meters - distance threshold for timeout warning (30km)
// Node display configuration
const int kDefaultMaxNodes = 500; // Default maximum number of nodes to render on the map at once
diff --git a/lib/localizations/de.json b/lib/localizations/de.json
index cd9bdae..e31b349 100644
--- a/lib/localizations/de.json
+++ b/lib/localizations/de.json
@@ -95,7 +95,9 @@
"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."
+ "confirmDeleteMessage": "Sind Sie sicher, dass Sie Knoten #{} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
+ "deleteReasonLabel": "Grund für Löschung (Optional)",
+ "deleteReasonHint": "z.B. Gerät entfernt, falsche Position, Duplikat..."
},
"addNode": {
"profile": "Profil",
@@ -247,6 +249,9 @@
"addProvider": "Anbieter Hinzufügen",
"deleteProvider": "Anbieter Löschen",
"deleteProviderConfirm": "Sind Sie sicher, dass Sie \"{}\" löschen möchten?",
+ "clearCaches": "Caches Leeren",
+ "clearCachesConfirm": "Alle Kachel-Caches für \"{}\" löschen? Dies wird Speicherplatz freigeben, aber die Kacheln müssen beim Betrachten dieser Bereiche erneut heruntergeladen werden.",
+ "cachesCleared": "Caches für \"{}\" geleert",
"providerName": "Anbieter-Name",
"providerNameHint": "z.B. Benutzerdefinierte Karten GmbH",
"providerNameRequired": "Anbieter-Name ist erforderlich",
diff --git a/lib/localizations/en.json b/lib/localizations/en.json
index 7f7023b..3534972 100644
--- a/lib/localizations/en.json
+++ b/lib/localizations/en.json
@@ -132,7 +132,9 @@
"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."
+ "confirmDeleteMessage": "Are you sure you want to delete node #{}? This action cannot be undone.",
+ "deleteReasonLabel": "Reason for Deletion (Optional)",
+ "deleteReasonHint": "e.g., device removed, incorrect location, duplicate..."
},
"addNode": {
"profile": "Profile",
@@ -282,8 +284,11 @@
"needsApiKey": "Needs API key",
"editProvider": "Edit Provider",
"addProvider": "Add Provider",
- "deleteProvider": "Delete Provider",
+ "deleteProvider": "Delete Provider",
"deleteProviderConfirm": "Are you sure you want to delete \"{}\"?",
+ "clearCaches": "Clear Caches",
+ "clearCachesConfirm": "Clear all tile caches for \"{}\"? This will free up storage space but tiles will need to be downloaded again when viewing those areas.",
+ "cachesCleared": "Caches cleared for \"{}\"",
"providerName": "Provider Name",
"providerNameHint": "e.g., Custom Maps Inc.",
"providerNameRequired": "Provider name is required",
diff --git a/lib/localizations/es.json b/lib/localizations/es.json
index 423ca6d..ef83cc5 100644
--- a/lib/localizations/es.json
+++ b/lib/localizations/es.json
@@ -132,7 +132,9 @@
"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."
+ "confirmDeleteMessage": "¿Estás seguro de que quieres eliminar el nodo #{}? Esta acción no se puede deshacer.",
+ "deleteReasonLabel": "Razón para Eliminación (Opcional)",
+ "deleteReasonHint": "ej. dispositivo removido, ubicación incorrecta, duplicado..."
},
"addNode": {
"profile": "Perfil",
@@ -284,6 +286,9 @@
"addProvider": "Agregar Proveedor",
"deleteProvider": "Eliminar Proveedor",
"deleteProviderConfirm": "¿Está seguro de que desea eliminar \"{}\"?",
+ "clearCaches": "Limpiar Caché",
+ "clearCachesConfirm": "¿Limpiar todo el caché de mosaicos para \"{}\"? Esto liberará espacio de almacenamiento pero los mosaicos deberán descargarse nuevamente al ver esas áreas.",
+ "cachesCleared": "Caché limpiado para \"{}\"",
"providerName": "Nombre del Proveedor",
"providerNameHint": "ej., Mapas Personalizados Inc.",
"providerNameRequired": "El nombre del proveedor es requerido",
diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json
index b314a5b..fabfb7a 100644
--- a/lib/localizations/fr.json
+++ b/lib/localizations/fr.json
@@ -132,7 +132,9 @@
"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."
+ "confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer le nœud #{} ? Cette action ne peut pas être annulée.",
+ "deleteReasonLabel": "Raison de Suppression (Optionnel)",
+ "deleteReasonHint": "ex. appareil retiré, emplacement incorrect, doublon..."
},
"addNode": {
"profile": "Profil",
@@ -284,6 +286,9 @@
"addProvider": "Ajouter Fournisseur",
"deleteProvider": "Supprimer Fournisseur",
"deleteProviderConfirm": "Êtes-vous sûr de vouloir supprimer \"{}\"?",
+ "clearCaches": "Vider les Caches",
+ "clearCachesConfirm": "Vider tous les caches de tuiles pour \"{}\"? Cela libérera de l'espace de stockage mais les tuiles devront être téléchargées à nouveau lors de la consultation de ces zones.",
+ "cachesCleared": "Caches vidés pour \"{}\"",
"providerName": "Nom du Fournisseur",
"providerNameHint": "ex., Cartes Personnalisées Inc.",
"providerNameRequired": "Le nom du fournisseur est requis",
diff --git a/lib/localizations/it.json b/lib/localizations/it.json
index 6602d76..066f3ec 100644
--- a/lib/localizations/it.json
+++ b/lib/localizations/it.json
@@ -132,7 +132,9 @@
"editQueuedForUpload": "Modifica nodo in coda per il caricamento",
"deleteQueuedForUpload": "Eliminazione nodo in coda per il caricamento",
"confirmDeleteTitle": "Elimina Nodo",
- "confirmDeleteMessage": "Sei sicuro di voler eliminare il nodo #{}? Questa azione non può essere annullata."
+ "confirmDeleteMessage": "Sei sicuro di voler eliminare il nodo #{}? Questa azione non può essere annullata.",
+ "deleteReasonLabel": "Motivo per Eliminazione (Opzionale)",
+ "deleteReasonHint": "es. dispositivo rimosso, posizione errata, duplicato..."
},
"addNode": {
"profile": "Profilo",
@@ -284,6 +286,9 @@
"addProvider": "Aggiungi Fornitore",
"deleteProvider": "Elimina Fornitore",
"deleteProviderConfirm": "Sei sicuro di voler eliminare \"{}\"?",
+ "clearCaches": "Svuota Cache",
+ "clearCachesConfirm": "Svuotare tutte le cache delle tessere per \"{}\"? Questo libererà spazio di archiviazione ma le tessere dovranno essere scaricate nuovamente quando si visualizzano quelle aree.",
+ "cachesCleared": "Cache svuotate per \"{}\"",
"providerName": "Nome Fornitore",
"providerNameHint": "es., Mappe Personalizzate Inc.",
"providerNameRequired": "Il nome del fornitore è obbligatorio",
diff --git a/lib/localizations/nl.json b/lib/localizations/nl.json
index f9d0cd5..b619204 100644
--- a/lib/localizations/nl.json
+++ b/lib/localizations/nl.json
@@ -131,8 +131,10 @@
"queuedForUpload": "Node in wachtrij geplaatst voor upload",
"editQueuedForUpload": "Node bewerking in wachtrij geplaatst voor upload",
"deleteQueuedForUpload": "Node verwijdering in wachtrij geplaatst voor upload",
- "confirmDeleteTitle": "Verwijder Node",
- "confirmDeleteMessage": "Weet u zeker dat u node #{} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."
+ "confirmDeleteTitle": "Verwijder Node",
+ "confirmDeleteMessage": "Weet u zeker dat u node #{} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
+ "deleteReasonLabel": "Reden voor Verwijdering (Optioneel)",
+ "deleteReasonHint": "bijv. apparaat verwijderd, onjuiste locatie, duplicaat..."
},
"addNode": {
"profile": "Profiel",
@@ -284,6 +286,9 @@
"addProvider": "Voeg Provider Toe",
"deleteProvider": "Verwijder Provider",
"deleteProviderConfirm": "Weet u zeker dat u \"{}\" wilt verwijderen?",
+ "clearCaches": "Cache Wissen",
+ "clearCachesConfirm": "Alle tegelcaches wissen voor \"{}\"? Dit zal opslagruimte vrijmaken maar tegels moeten opnieuw worden gedownload bij het bekijken van die gebieden.",
+ "cachesCleared": "Caches gewist voor \"{}\"",
"providerName": "Provider Naam",
"providerNameHint": "bijv., Aangepaste Kaarten B.V.",
"providerNameRequired": "Provider naam is vereist",
diff --git a/lib/localizations/pl.json b/lib/localizations/pl.json
index 76fc22b..b97750c 100644
--- a/lib/localizations/pl.json
+++ b/lib/localizations/pl.json
@@ -132,7 +132,9 @@
"editQueuedForUpload": "Edycja węzła umieszczona w kolejce do przesłania",
"deleteQueuedForUpload": "Usuwanie węzła umieszczone w kolejce do przesłania",
"confirmDeleteTitle": "Usuń Węzeł",
- "confirmDeleteMessage": "Czy na pewno chcesz usunąć węzeł #{}? Tej akcji nie można cofnąć."
+ "confirmDeleteMessage": "Czy na pewno chcesz usunąć węzeł #{}? Tej akcji nie można cofnąć.",
+ "deleteReasonLabel": "Powód Usunięcia (Opcjonalny)",
+ "deleteReasonHint": "np. urządzenie usunięte, nieprawidłowa lokalizacja, duplikat..."
},
"addNode": {
"profile": "Profil",
@@ -284,6 +286,9 @@
"addProvider": "Dodaj Dostawcę",
"deleteProvider": "Usuń Dostawcę",
"deleteProviderConfirm": "Czy na pewno chcesz usunąć \"{}\"?",
+ "clearCaches": "Wyczyść Cache",
+ "clearCachesConfirm": "Wyczyścić wszystkie cache kafelków dla \"{}\"? To zwolni miejsce na dysku, ale kafelki będą musiały zostać ponownie pobrane podczas przeglądania tych obszarów.",
+ "cachesCleared": "Cache wyczyszczone dla \"{}\"",
"providerName": "Nazwa Dostawcy",
"providerNameHint": "np., Niestandardowe Mapy Sp. z o.o.",
"providerNameRequired": "Nazwa dostawcy jest wymagana",
diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json
index 8366a54..fe71712 100644
--- a/lib/localizations/pt.json
+++ b/lib/localizations/pt.json
@@ -132,7 +132,9 @@
"editQueuedForUpload": "Edição de nó na fila para envio",
"deleteQueuedForUpload": "Exclusão de nó na fila para envio",
"confirmDeleteTitle": "Excluir Nó",
- "confirmDeleteMessage": "Tem certeza de que deseja excluir o nó #{}? Esta ação não pode ser desfeita."
+ "confirmDeleteMessage": "Tem certeza de que deseja excluir o nó #{}? Esta ação não pode ser desfeita.",
+ "deleteReasonLabel": "Motivo para Exclusão (Opcional)",
+ "deleteReasonHint": "ex. dispositivo removido, localização incorreta, duplicado..."
},
"addNode": {
"profile": "Perfil",
@@ -284,6 +286,9 @@
"addProvider": "Adicionar Provedor",
"deleteProvider": "Excluir Provedor",
"deleteProviderConfirm": "Tem certeza de que deseja excluir \"{}\"?",
+ "clearCaches": "Limpar Caches",
+ "clearCachesConfirm": "Limpar todos os caches de mapas para \"{}\"? Isso liberará espaço de armazenamento, mas os mapas precisarão ser baixados novamente ao visualizar essas áreas.",
+ "cachesCleared": "Caches limpos para \"{}\"",
"providerName": "Nome do Provedor",
"providerNameHint": "ex., Mapas Personalizados Inc.",
"providerNameRequired": "Nome do provedor é obrigatório",
diff --git a/lib/localizations/tr.json b/lib/localizations/tr.json
index 06fc9ad..11ce32a 100644
--- a/lib/localizations/tr.json
+++ b/lib/localizations/tr.json
@@ -132,7 +132,9 @@
"editQueuedForUpload": "Düğüm düzenlemesi yükleme için sıraya alındı",
"deleteQueuedForUpload": "Düğüm silme işlemi yükleme için sıraya alındı",
"confirmDeleteTitle": "Düğümü Sil",
- "confirmDeleteMessage": "#{} düğümünü silmek istediğinizden emin misiniz? Bu işlem geri alınamaz."
+ "confirmDeleteMessage": "#{} düğümünü silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
+ "deleteReasonLabel": "Silme Nedeni (İsteğe Bağlı)",
+ "deleteReasonHint": "örn. cihaz kaldırıldı, yanlış konum, duplikat..."
},
"addNode": {
"profile": "Profil",
@@ -284,6 +286,9 @@
"addProvider": "Sağlayıcı Ekle",
"deleteProvider": "Sağlayıcıyı Sil",
"deleteProviderConfirm": "\"{}\" silmek istediğinizden emin misiniz?",
+ "clearCaches": "Önbellekleri Temizle",
+ "clearCachesConfirm": "\"{}\" için tüm döşeme önbelleklerini temizle? Bu depolama alanını boşaltacak ancak bu alanları görüntülerken döşemelerin tekrar indirilmesi gerekecek.",
+ "cachesCleared": "\"{}\" için önbellekler temizlendi",
"providerName": "Sağlayıcı Adı",
"providerNameHint": "örn., Özel Haritalar A.Ş.",
"providerNameRequired": "Sağlayıcı adı gerekli",
diff --git a/lib/localizations/uk.json b/lib/localizations/uk.json
index 499f1a2..322ee58 100644
--- a/lib/localizations/uk.json
+++ b/lib/localizations/uk.json
@@ -132,7 +132,9 @@
"editQueuedForUpload": "Редагування вузла поставлено в чергу для завантаження",
"deleteQueuedForUpload": "Видалення вузла поставлено в чергу для завантаження",
"confirmDeleteTitle": "Видалити Вузол",
- "confirmDeleteMessage": "Ви впевнені, що хочете видалити вузол #{}? Цю дію не можна скасувати."
+ "confirmDeleteMessage": "Ви впевнені, що хочете видалити вузол #{}? Цю дію не можна скасувати.",
+ "deleteReasonLabel": "Причина Видалення (Опціонально)",
+ "deleteReasonHint": "наприклад пристрій видалено, неправильне розташування, дублікат..."
},
"addNode": {
"profile": "Профіль",
@@ -284,6 +286,9 @@
"addProvider": "Додати Постачальника",
"deleteProvider": "Видалити Постачальника",
"deleteProviderConfirm": "Ви впевнені, що хочете видалити \"{}\"?",
+ "clearCaches": "Очистити Кеш",
+ "clearCachesConfirm": "Очистити всі кеші тайлів для \"{}\"? Це звільнить місце на диску, але тайли доведеться знову завантажити при перегляді цих областей.",
+ "cachesCleared": "Кеш очищено для \"{}\"",
"providerName": "Назва Постачальника",
"providerNameHint": "напр., Кастомні Карти ТОВ",
"providerNameRequired": "Назва постачальника обов'язкова",
diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json
index 0069558..add697b 100644
--- a/lib/localizations/zh.json
+++ b/lib/localizations/zh.json
@@ -132,7 +132,9 @@
"editQueuedForUpload": "节点编辑已排队上传",
"deleteQueuedForUpload": "节点删除已排队上传",
"confirmDeleteTitle": "删除节点",
- "confirmDeleteMessage": "您确定要删除节点 #{} 吗?此操作无法撤销。"
+ "confirmDeleteMessage": "您确定要删除节点 #{} 吗?此操作无法撤销。",
+ "deleteReasonLabel": "删除原因(可选)",
+ "deleteReasonHint": "例如:设备已移除、位置错误、重复..."
},
"addNode": {
"profile": "配置文件",
@@ -284,6 +286,9 @@
"addProvider": "添加提供商",
"deleteProvider": "删除提供商",
"deleteProviderConfirm": "您确定要删除 \"{}\" 吗?",
+ "clearCaches": "清除缓存",
+ "clearCachesConfirm": "清除 \"{}\" 的所有瓦片缓存?这将释放存储空间,但在查看这些区域时需要重新下载瓦片。",
+ "cachesCleared": "已清除 \"{}\" 的缓存",
"providerName": "提供商名称",
"providerNameHint": "例如,自定义地图公司",
"providerNameRequired": "提供商名称为必填项",
diff --git a/lib/screens/about_screen.dart b/lib/screens/about_screen.dart
index d7e7a0b..eae176c 100644
--- a/lib/screens/about_screen.dart
+++ b/lib/screens/about_screen.dart
@@ -76,6 +76,9 @@ class AboutScreen extends StatelessWidget {
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
+ // About OpenStreetMap section
+ _buildAboutOSMSection(context),
+ const SizedBox(height: 24),
// Information dialogs section
_buildDialogButtons(context),
const SizedBox(height: 24),
@@ -121,6 +124,39 @@ class AboutScreen extends StatelessWidget {
);
}
+ Widget _buildAboutOSMSection(BuildContext context) {
+ final locService = LocalizationService.instance;
+
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ locService.t('auth.aboutOSM'),
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ locService.t('auth.aboutOSMDescription'),
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
+ const SizedBox(height: 16),
+ SizedBox(
+ width: double.infinity,
+ child: OutlinedButton.icon(
+ onPressed: () => _launchUrl('https://openstreetmap.org', context),
+ icon: const Icon(Icons.open_in_new),
+ label: Text(locService.t('auth.visitOSM')),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
Widget _buildDialogButtons(BuildContext context) {
final locService = LocalizationService.instance;
diff --git a/lib/screens/osm_account_screen.dart b/lib/screens/osm_account_screen.dart
index 1e25488..8e95ddb 100644
--- a/lib/screens/osm_account_screen.dart
+++ b/lib/screens/osm_account_screen.dart
@@ -195,50 +195,9 @@ class _OSMAccountScreenState extends State {
child: UploadModeSection(),
),
),
- const SizedBox(height: 16),
+ const SizedBox(height: 16),
],
- // Information Section
- Card(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- locService.t('auth.aboutOSM'),
- style: Theme.of(context).textTheme.titleMedium,
- ),
- const SizedBox(height: 8),
- Text(
- locService.t('auth.aboutOSMDescription'),
- style: Theme.of(context).textTheme.bodyMedium,
- ),
- const SizedBox(height: 16),
- SizedBox(
- width: double.infinity,
- child: OutlinedButton.icon(
- onPressed: () async {
- final url = Uri.parse('https://openstreetmap.org');
- if (await canLaunchUrl(url)) {
- await launchUrl(url, mode: LaunchMode.externalApplication);
- } else {
- if (context.mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(locService.t('advancedEdit.couldNotOpenOSMWebsite'))),
- );
- }
- }
- },
- icon: const Icon(Icons.open_in_new),
- label: Text(locService.t('auth.visitOSM')),
- ),
- ),
- ],
- ),
- ),
- ),
-
// Account deletion section - only show when logged in and not in simulate mode
if (appState.isLoggedIn && appState.uploadMode != UploadMode.simulate) ...[
const SizedBox(height: 16),
diff --git a/lib/screens/settings/sections/tile_provider_section.dart b/lib/screens/settings/sections/tile_provider_section.dart
index 6ed55bd..1e8d158 100644
--- a/lib/screens/settings/sections/tile_provider_section.dart
+++ b/lib/screens/settings/sections/tile_provider_section.dart
@@ -104,6 +104,9 @@ class TileProviderSection extends StatelessWidget {
case 'edit':
_editProvider(context, provider);
break;
+ case 'clear_cache':
+ _clearProviderCaches(context, provider);
+ break;
case 'delete':
_deleteProvider(context, provider);
break;
@@ -120,6 +123,16 @@ class TileProviderSection extends StatelessWidget {
],
),
),
+ PopupMenuItem(
+ value: 'clear_cache',
+ child: Row(
+ children: [
+ const Icon(Icons.clear_all),
+ const SizedBox(width: 8),
+ Text(locService.t('tileProviders.clearCaches')),
+ ],
+ ),
+ ),
PopupMenuItem(
value: 'delete',
child: Row(
@@ -154,6 +167,37 @@ class TileProviderSection extends StatelessWidget {
);
}
+ void _clearProviderCaches(BuildContext context, TileProvider provider) {
+ final locService = LocalizationService.instance;
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(locService.t('tileProviders.clearCaches')),
+ content: Text(locService.t('tileProviders.clearCachesConfirm', params: [provider.name])),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text(locService.t('actions.cancel')),
+ ),
+ TextButton(
+ onPressed: () async {
+ Navigator.of(context).pop();
+ await context.read().clearTileProviderCaches(provider.id);
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(locService.t('tileProviders.cachesCleared', params: [provider.name])),
+ ),
+ );
+ }
+ },
+ child: Text(locService.t('tileProviders.clearCaches')),
+ ),
+ ],
+ ),
+ );
+ }
+
void _deleteProvider(BuildContext context, TileProvider provider) {
final locService = LocalizationService.instance;
showDialog(
diff --git a/lib/services/provider_tile_cache_manager.dart b/lib/services/provider_tile_cache_manager.dart
index cdbce71..d7309d6 100644
--- a/lib/services/provider_tile_cache_manager.dart
+++ b/lib/services/provider_tile_cache_manager.dart
@@ -61,10 +61,13 @@ class ProviderTileCacheManager {
/// Delete a specific provider's cache directory and remove the store.
static Future deleteCache(String providerId, String tileTypeId) async {
final key = '$providerId/$tileTypeId';
- final store = _stores.remove(key);
+ final store = _stores[key];
if (store != null) {
+ // Use the store's clear method to properly reset its internal state
await store.clear();
+ // Don't remove from registry - let it be reused with clean state
} else if (_baseCacheDir != null) {
+ // Fallback for stores not in registry
final cacheDir = Directory(p.join(_baseCacheDir!, providerId, tileTypeId));
if (await cacheDir.exists()) {
await cacheDir.delete(recursive: true);
diff --git a/lib/state/settings_state.dart b/lib/state/settings_state.dart
index 7fbc6d0..e884086 100644
--- a/lib/state/settings_state.dart
+++ b/lib/state/settings_state.dart
@@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:collection/collection.dart';
import '../models/tile_provider.dart';
+import '../services/provider_tile_cache_manager.dart';
import '../dev_config.dart';
import '../keys.dart';
@@ -332,6 +333,17 @@ class SettingsState extends ChangeNotifier {
notifyListeners();
}
+ /// Clear all tile caches for a specific provider
+ Future clearTileProviderCaches(String providerId) async {
+ final provider = _tileProviders.firstWhereOrNull((p) => p.id == providerId);
+ if (provider == null) return;
+
+ // Clear cache for each tile type in this provider
+ for (final tileType in provider.tileTypes) {
+ await ProviderTileCacheManager.deleteCache(providerId, tileType.id);
+ }
+ }
+
/// Set follow-me mode
Future setFollowMeMode(FollowMeMode mode) async {
if (_followMeMode != mode) {
diff --git a/lib/state/upload_queue_state.dart b/lib/state/upload_queue_state.dart
index cabb8d4..bd883a8 100644
--- a/lib/state/upload_queue_state.dart
+++ b/lib/state/upload_queue_state.dart
@@ -253,12 +253,12 @@ class UploadQueueState extends ChangeNotifier {
}
// Add a node deletion to the upload queue
- void addFromNodeDeletion(OsmNode node, {required UploadMode uploadMode}) {
+ void addFromNodeDeletion(OsmNode node, {required UploadMode uploadMode, String? changesetComment}) {
final upload = PendingUpload(
coord: node.coord,
direction: node.directionDeg.isNotEmpty ? node.directionDeg.first : 0, // Direction not used for deletions but required for API
profile: null, // No profile needed for deletions - just delete by node ID
- changesetComment: 'Delete a surveillance node', // Default comment for deletions
+ changesetComment: changesetComment ?? 'Delete a surveillance node', // Use provided comment or default
uploadMode: uploadMode,
operation: UploadOperation.delete,
originalNodeId: node.id,
diff --git a/lib/widgets/node_tag_sheet.dart b/lib/widgets/node_tag_sheet.dart
index 5c54b35..1af4814 100644
--- a/lib/widgets/node_tag_sheet.dart
+++ b/lib/widgets/node_tag_sheet.dart
@@ -65,30 +65,17 @@ class NodeTagSheet extends StatelessWidget {
}
void deleteNode() async {
- final shouldDelete = await showDialog(
+ final result = await showDialog<({bool confirmed, String comment})>(
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')),
- ),
- ],
- );
- },
+ builder: (BuildContext context) => _DeleteNodeDialog(
+ nodeId: node.id.toString(),
+ locService: locService,
+ ),
);
- if (shouldDelete == true && context.mounted) {
+ if ((result?.confirmed ?? false) && context.mounted) {
Navigator.pop(context); // Close this sheet first
- appState.deleteNode(node);
+ appState.deleteNode(node, changesetComment: result!.comment);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.deleteQueuedForUpload'))),
);
@@ -260,4 +247,80 @@ class NodeTagSheet extends StatelessWidget {
},
);
}
+}
+
+class _DeleteNodeDialog extends StatefulWidget {
+ final String nodeId;
+ final LocalizationService locService;
+
+ const _DeleteNodeDialog({
+ required this.nodeId,
+ required this.locService,
+ });
+
+ @override
+ State<_DeleteNodeDialog> createState() => _DeleteNodeDialogState();
+}
+
+class _DeleteNodeDialogState extends State<_DeleteNodeDialog> {
+ late final TextEditingController _commentController;
+
+ @override
+ void initState() {
+ super.initState();
+ _commentController = TextEditingController();
+ }
+
+ @override
+ void dispose() {
+ _commentController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: Text(widget.locService.t('node.confirmDeleteTitle')),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(widget.locService.t('node.confirmDeleteMessage', params: [widget.nodeId])),
+ const SizedBox(height: 16),
+ Text(
+ widget.locService.t('node.deleteReasonLabel'),
+ style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _commentController,
+ decoration: InputDecoration(
+ hintText: widget.locService.t('node.deleteReasonHint'),
+ border: const OutlineInputBorder(),
+ isDense: true,
+ ),
+ maxLines: 2,
+ textCapitalization: TextCapitalization.sentences,
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop((confirmed: false, comment: '')),
+ child: Text(widget.locService.cancel),
+ ),
+ TextButton(
+ onPressed: () {
+ final comment = _commentController.text.trim();
+ final finalComment = comment.isEmpty
+ ? 'Delete a surveillance node'
+ : 'Delete a surveillance node: $comment';
+ Navigator.of(context).pop((confirmed: true, comment: finalComment));
+ },
+ style: TextButton.styleFrom(foregroundColor: Colors.red),
+ child: Text(widget.locService.t('actions.delete')),
+ ),
+ ],
+ );
+ }
}
\ No newline at end of file
diff --git a/pubspec.lock b/pubspec.lock
index b61873a..b3ea42f 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -77,10 +77,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
- version: "1.4.0"
+ version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -564,18 +564,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
- version: "0.12.17"
+ version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
- version: "0.11.1"
+ version: "0.13.0"
meta:
dependency: transitive
description:
@@ -929,10 +929,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
+ sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
- version: "0.7.7"
+ version: "0.7.10"
timezone:
dependency: transitive
description: