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: