Move "about osm" to about page, add option to clear tile caches, option to add reason when deleting, min zoom 16 for edits/submissions, 300km nav distance warning

This commit is contained in:
stopflock
2026-03-12 18:59:48 -05:00
parent 256dd1a43c
commit 08b395214b
25 changed files with 364 additions and 160 deletions

View File

@@ -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.",

View File

@@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -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)
}
}

View File

@@ -1,72 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>DeFlock</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>deflockapp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>None</string>
<key>CFBundleURLSchemes</key>
<array>
<string>deflockapp</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>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.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app optionally uses your location to show nearby cameras by centering the map on your location.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>DeFlock</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>deflockapp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app optionally uses your location to show nearby cameras by centering the map on your location.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>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.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- OAuth2 redirect handler -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>None</string>
<key>CFBundleURLSchemes</key>
<array>
<string>deflockapp</string>
</array>
</dict>
</array>
<!-- (Optional) allow opening the system browser and returning -->
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>UIStatusBarHidden</key>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -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<void> clearTileProviderCaches(String providerId) async {
await _settingsState.clearTileProviderCaches(providerId);
}
/// Set follow-me mode
Future<void> setFollowMeMode(FollowMeMode mode) async {
await _settingsState.setFollowMeMode(mode);

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "Назва постачальника обов'язкова",

View File

@@ -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": "提供商名称为必填项",

View File

@@ -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;

View File

@@ -195,50 +195,9 @@ class _OSMAccountScreenState extends State<OSMAccountScreen> {
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),

View File

@@ -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<AppState>().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(

View File

@@ -61,10 +61,13 @@ class ProviderTileCacheManager {
/// Delete a specific provider's cache directory and remove the store.
static Future<void> 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);

View File

@@ -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<void> 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<void> setFollowMeMode(FollowMeMode mode) async {
if (_followMeMode != mode) {

View File

@@ -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,

View File

@@ -65,30 +65,17 @@ class NodeTagSheet extends StatelessWidget {
}
void deleteNode() async {
final shouldDelete = await showDialog<bool>(
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')),
),
],
);
}
}

View File

@@ -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: