From 096fb5868b63382f93a58f2dff89be20d45c6dd8 Mon Sep 17 00:00:00 2001 From: stopflock Date: Sat, 25 Jul 2026 17:43:41 -0500 Subject: [PATCH] require delete reason, give options --- lib/localizations/de.json | 11 +++- lib/localizations/en.json | 11 +++- lib/localizations/es.json | 11 +++- lib/localizations/fr.json | 11 +++- lib/localizations/it.json | 11 +++- lib/localizations/nl.json | 11 +++- lib/localizations/pl.json | 11 +++- lib/localizations/pt.json | 11 +++- lib/localizations/tr.json | 11 +++- lib/localizations/uk.json | 11 +++- lib/localizations/zh.json | 11 +++- lib/widgets/node_tag_sheet.dart | 109 +++++++++++++++++++++++++++----- 12 files changed, 193 insertions(+), 37 deletions(-) diff --git a/lib/localizations/de.json b/lib/localizations/de.json index 885fea3..a0190e1 100644 --- a/lib/localizations/de.json +++ b/lib/localizations/de.json @@ -101,10 +101,17 @@ "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.", - "deleteReasonLabel": "Grund für Löschung (Optional)", - "deleteReasonHint": "z.B. Gerät entfernt, falsche Position, Duplikat...", + "deleteReasonLabel": "Grund für Löschung", + "deleteReasonSelectHint": "Auswählen...", + "deleteReasonDeviceRemoved": "Gerät entfernt", + "deleteReasonDuplicateNode": "Doppelter Knoten", + "deleteReasonMisidentifiedInfra": "Falsch identifizierte Infrastruktur", + "deleteReasonSpamVandalism": "Spam/Vandalismus", + "deleteReasonCustom": "Benutzerdefiniert..", + "deleteReasonHint": "Bitte Grund beschreiben...", "mustBeLoggedInToDelete": "Sie müssen mit einem OpenStreetMap-Konto angemeldet sein, um Kartendaten beizutragen." }, + "addNode": { "profile": "Profil", "selectProfile": "Profil auswählen...", diff --git a/lib/localizations/en.json b/lib/localizations/en.json index 3ca642f..e2db888 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -138,9 +138,16 @@ "deleteQueuedForUpload": "Node deletion queued for upload", "confirmDeleteTitle": "Delete Node", "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...", + "deleteReasonLabel": "Reason for Deletion", + "deleteReasonSelectHint": "Select...", + "deleteReasonDeviceRemoved": "Device removed", + "deleteReasonDuplicateNode": "Duplicate node", + "deleteReasonMisidentifiedInfra": "Misidentified infra", + "deleteReasonSpamVandalism": "Spam/vandalism", + "deleteReasonCustom": "Custom..", + "deleteReasonHint": "Please describe the reason...", "mustBeLoggedInToDelete": "You must be logged in with an OpenStreetMap account to contribute map data." + }, "addNode": { "profile": "Profile", diff --git a/lib/localizations/es.json b/lib/localizations/es.json index d9bbada..a45249f 100644 --- a/lib/localizations/es.json +++ b/lib/localizations/es.json @@ -138,8 +138,15 @@ "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.", - "deleteReasonLabel": "Razón para Eliminación (Opcional)", - "deleteReasonHint": "ej. dispositivo removido, ubicación incorrecta, duplicado...", + "deleteReasonLabel": "Razón para Eliminación", + "deleteReasonSelectHint": "Seleccionar...", + "deleteReasonDeviceRemoved": "Dispositivo removido", + "deleteReasonDuplicateNode": "Nodo duplicado", + "deleteReasonMisidentifiedInfra": "Infraestructura mal identificada", + "deleteReasonSpamVandalism": "Spam/vandalismo", + "deleteReasonCustom": "Personalizado..", + "deleteReasonHint": "Por favor describa la razón...", + "mustBeLoggedInToDelete": "Debes iniciar sesión con una cuenta de OpenStreetMap para contribuir con datos del mapa." }, "addNode": { diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json index 336d5c6..e5f7bb2 100644 --- a/lib/localizations/fr.json +++ b/lib/localizations/fr.json @@ -138,8 +138,15 @@ "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.", - "deleteReasonLabel": "Raison de Suppression (Optionnel)", - "deleteReasonHint": "ex. appareil retiré, emplacement incorrect, doublon...", + "deleteReasonLabel": "Raison de Suppression", + "deleteReasonSelectHint": "Sélectionner...", + "deleteReasonDeviceRemoved": "Appareil retiré", + "deleteReasonDuplicateNode": "Nœud dupliqué", + "deleteReasonMisidentifiedInfra": "Infrastructure mal identifiée", + "deleteReasonSpamVandalism": "Spam/vandalisme", + "deleteReasonCustom": "Personnalisé..", + "deleteReasonHint": "Veuillez décrire la raison...", + "mustBeLoggedInToDelete": "Vous devez être connecté avec un compte OpenStreetMap pour contribuer aux données cartographiques." }, "addNode": { diff --git a/lib/localizations/it.json b/lib/localizations/it.json index 87f92e0..cff10a6 100644 --- a/lib/localizations/it.json +++ b/lib/localizations/it.json @@ -138,8 +138,15 @@ "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.", - "deleteReasonLabel": "Motivo per Eliminazione (Opzionale)", - "deleteReasonHint": "es. dispositivo rimosso, posizione errata, duplicato...", + "deleteReasonLabel": "Motivo per Eliminazione", + "deleteReasonSelectHint": "Seleziona...", + "deleteReasonDeviceRemoved": "Dispositivo rimosso", + "deleteReasonDuplicateNode": "Nodo duplicato", + "deleteReasonMisidentifiedInfra": "Infrastruttura identificata erroneamente", + "deleteReasonSpamVandalism": "Spam/vandalismo", + "deleteReasonCustom": "Personalizzato..", + "deleteReasonHint": "Descrivi il motivo...", + "mustBeLoggedInToDelete": "Devi aver effettuato l'accesso con un account OpenStreetMap per contribuire ai dati della mappa." }, "addNode": { diff --git a/lib/localizations/nl.json b/lib/localizations/nl.json index 5012e77..677c1c3 100644 --- a/lib/localizations/nl.json +++ b/lib/localizations/nl.json @@ -138,8 +138,15 @@ "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.", - "deleteReasonLabel": "Reden voor Verwijdering (Optioneel)", - "deleteReasonHint": "bijv. apparaat verwijderd, onjuiste locatie, duplicaat...", + "deleteReasonLabel": "Reden voor Verwijdering", + "deleteReasonSelectHint": "Selecteer...", + "deleteReasonDeviceRemoved": "Apparaat verwijderd", + "deleteReasonDuplicateNode": "Dubbele node", + "deleteReasonMisidentifiedInfra": "Verkeerd geïdentificeerde infrastructuur", + "deleteReasonSpamVandalism": "Spam/vandalisme", + "deleteReasonCustom": "Aangepast..", + "deleteReasonHint": "Beschrijf de reden...", + "mustBeLoggedInToDelete": "U moet ingelogd zijn met een OpenStreetMap-account om kaartgegevens bij te dragen." }, "addNode": { diff --git a/lib/localizations/pl.json b/lib/localizations/pl.json index 3f4692a..7a6247c 100644 --- a/lib/localizations/pl.json +++ b/lib/localizations/pl.json @@ -138,8 +138,15 @@ "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ąć.", - "deleteReasonLabel": "Powód Usunięcia (Opcjonalny)", - "deleteReasonHint": "np. urządzenie usunięte, nieprawidłowa lokalizacja, duplikat...", + "deleteReasonLabel": "Powód Usunięcia", + "deleteReasonSelectHint": "Wybierz...", + "deleteReasonDeviceRemoved": "Urządzenie usunięte", + "deleteReasonDuplicateNode": "Zduplikowany węzeł", + "deleteReasonMisidentifiedInfra": "Błędnie zidentyfikowana infrastruktura", + "deleteReasonSpamVandalism": "Spam/wandalizm", + "deleteReasonCustom": "Niestandardowy..", + "deleteReasonHint": "Opisz powód...", + "mustBeLoggedInToDelete": "Musisz być zalogowany na konto OpenStreetMap, aby dodawać dane do mapy." }, "addNode": { diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json index 2f09eba..ce7ae71 100644 --- a/lib/localizations/pt.json +++ b/lib/localizations/pt.json @@ -138,8 +138,15 @@ "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.", - "deleteReasonLabel": "Motivo para Exclusão (Opcional)", - "deleteReasonHint": "ex. dispositivo removido, localização incorreta, duplicado...", + "deleteReasonLabel": "Motivo para Exclusão", + "deleteReasonSelectHint": "Selecionar...", + "deleteReasonDeviceRemoved": "Dispositivo removido", + "deleteReasonDuplicateNode": "Nó duplicado", + "deleteReasonMisidentifiedInfra": "Infraestrutura mal identificada", + "deleteReasonSpamVandalism": "Spam/vandalismo", + "deleteReasonCustom": "Personalizado..", + "deleteReasonHint": "Descreva o motivo...", + "mustBeLoggedInToDelete": "Você precisa estar conectado com uma conta do OpenStreetMap para contribuir com dados do mapa." }, "addNode": { diff --git a/lib/localizations/tr.json b/lib/localizations/tr.json index 73ef038..afdf7d9 100644 --- a/lib/localizations/tr.json +++ b/lib/localizations/tr.json @@ -138,8 +138,15 @@ "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.", - "deleteReasonLabel": "Silme Nedeni (İsteğe Bağlı)", - "deleteReasonHint": "örn. cihaz kaldırıldı, yanlış konum, duplikat...", + "deleteReasonLabel": "Silme Nedeni", + "deleteReasonSelectHint": "Seçin...", + "deleteReasonDeviceRemoved": "Cihaz kaldırıldı", + "deleteReasonDuplicateNode": "Yinelenen düğüm", + "deleteReasonMisidentifiedInfra": "Yanlış tanımlanmış altyapı", + "deleteReasonSpamVandalism": "Spam/vandalizm", + "deleteReasonCustom": "Özel..", + "deleteReasonHint": "Lütfen nedeni açıklayın...", + "mustBeLoggedInToDelete": "Harita verilerine katkıda bulunmak için bir OpenStreetMap hesabıyla oturum açmış olmanız gerekir." }, "addNode": { diff --git a/lib/localizations/uk.json b/lib/localizations/uk.json index 257945b..a848891 100644 --- a/lib/localizations/uk.json +++ b/lib/localizations/uk.json @@ -138,8 +138,15 @@ "deleteQueuedForUpload": "Видалення вузла поставлено в чергу для завантаження", "confirmDeleteTitle": "Видалити Вузол", "confirmDeleteMessage": "Ви впевнені, що хочете видалити вузол #{}? Цю дію не можна скасувати.", - "deleteReasonLabel": "Причина Видалення (Опціонально)", - "deleteReasonHint": "наприклад пристрій видалено, неправильне розташування, дублікат...", + "deleteReasonLabel": "Причина Видалення", + "deleteReasonSelectHint": "Вибрати...", + "deleteReasonDeviceRemoved": "Пристрій видалено", + "deleteReasonDuplicateNode": "Дубльований вузол", + "deleteReasonMisidentifiedInfra": "Неправильно ідентифікована інфраструктура", + "deleteReasonSpamVandalism": "Спам/вандалізм", + "deleteReasonCustom": "Власний..", + "deleteReasonHint": "Опишіть причину...", + "mustBeLoggedInToDelete": "Ви повинні увійти в обліковий запис OpenStreetMap, щоб надавати дані карти." }, "addNode": { diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json index 9266fc7..6d474cf 100644 --- a/lib/localizations/zh.json +++ b/lib/localizations/zh.json @@ -138,8 +138,15 @@ "deleteQueuedForUpload": "节点删除已排队上传", "confirmDeleteTitle": "删除节点", "confirmDeleteMessage": "您确定要删除节点 #{} 吗?此操作无法撤销。", - "deleteReasonLabel": "删除原因(可选)", - "deleteReasonHint": "例如:设备已移除、位置错误、重复...", + "deleteReasonLabel": "删除原因", + "deleteReasonSelectHint": "请选择...", + "deleteReasonDeviceRemoved": "设备已移除", + "deleteReasonDuplicateNode": "重复节点", + "deleteReasonMisidentifiedInfra": "识别错误的基础设施", + "deleteReasonSpamVandalism": "垃圾信息/破坏", + "deleteReasonCustom": "自定义..", + "deleteReasonHint": "请描述原因...", + "mustBeLoggedInToDelete": "您必须使用 OpenStreetMap 账户登录才能贡献地图数据。" }, "addNode": { diff --git a/lib/widgets/node_tag_sheet.dart b/lib/widgets/node_tag_sheet.dart index a16a536..99d8af7 100644 --- a/lib/widgets/node_tag_sheet.dart +++ b/lib/widgets/node_tag_sheet.dart @@ -274,6 +274,48 @@ class NodeTagSheet extends StatelessWidget { } } +enum _DeleteReason { + deviceRemoved, + duplicateNode, + misidentifiedInfra, + spamVandalism, + custom, +} + +extension _DeleteReasonLabel on _DeleteReason { + String label(LocalizationService locService) { + switch (this) { + case _DeleteReason.deviceRemoved: + return locService.t('node.deleteReasonDeviceRemoved'); + case _DeleteReason.duplicateNode: + return locService.t('node.deleteReasonDuplicateNode'); + case _DeleteReason.misidentifiedInfra: + return locService.t('node.deleteReasonMisidentifiedInfra'); + case _DeleteReason.spamVandalism: + return locService.t('node.deleteReasonSpamVandalism'); + case _DeleteReason.custom: + return locService.t('node.deleteReasonCustom'); + } + } + + /// The text used to build the changeset comment (kept in English/consistent + /// regardless of UI language, matching prior app behavior for uploads). + String get commentText { + switch (this) { + case _DeleteReason.deviceRemoved: + return 'device removed'; + case _DeleteReason.duplicateNode: + return 'duplicate node'; + case _DeleteReason.misidentifiedInfra: + return 'misidentified infra'; + case _DeleteReason.spamVandalism: + return 'spam/vandalism'; + case _DeleteReason.custom: + return ''; + } + } +} + class _DeleteNodeDialog extends StatefulWidget { final String nodeId; final LocalizationService locService; @@ -289,6 +331,7 @@ class _DeleteNodeDialog extends StatefulWidget { class _DeleteNodeDialogState extends State<_DeleteNodeDialog> { late final TextEditingController _commentController; + _DeleteReason? _selectedReason; @override void initState() { @@ -302,6 +345,14 @@ class _DeleteNodeDialogState extends State<_DeleteNodeDialog> { super.dispose(); } + bool get _canConfirm { + if (_selectedReason == null) return false; + if (_selectedReason == _DeleteReason.custom) { + return _commentController.text.trim().isNotEmpty; + } + return true; + } + @override Widget build(BuildContext context) { return AlertDialog( @@ -317,16 +368,39 @@ class _DeleteNodeDialogState extends State<_DeleteNodeDialog> { 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(), + DropdownButtonFormField<_DeleteReason>( + initialValue: _selectedReason, + hint: Text(widget.locService.t('node.deleteReasonSelectHint')), + decoration: const InputDecoration( + border: OutlineInputBorder(), isDense: true, ), - maxLines: 2, - textCapitalization: TextCapitalization.sentences, + items: _DeleteReason.values + .map((reason) => DropdownMenuItem( + value: reason, + child: Text(reason.label(widget.locService)), + )) + .toList(), + onChanged: (reason) { + setState(() { + _selectedReason = reason; + }); + }, ), + if (_selectedReason == _DeleteReason.custom) ...[ + 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, + onChanged: (_) => setState(() {}), + ), + ], ], ), actions: [ @@ -335,17 +409,22 @@ class _DeleteNodeDialogState extends State<_DeleteNodeDialog> { 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)); - }, + onPressed: _canConfirm + ? () { + final reasonText = _selectedReason == _DeleteReason.custom + ? _commentController.text.trim() + : _selectedReason!.commentText; + final finalComment = reasonText.isEmpty + ? 'Delete a surveillance node' + : 'Delete a surveillance node: $reasonText'; + Navigator.of(context).pop((confirmed: true, comment: finalComment)); + } + : null, style: TextButton.styleFrom(foregroundColor: Colors.red), child: Text(widget.locService.t('actions.delete')), ), ], ); } -} \ No newline at end of file +} +