require delete reason, give options

This commit is contained in:
stopflock
2026-07-25 17:43:41 -05:00
parent 974204bd91
commit 096fb5868b
12 changed files with 193 additions and 37 deletions
+9 -2
View File
@@ -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...",
+9 -2
View File
@@ -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",
+9 -2
View File
@@ -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": {
+9 -2
View File
@@ -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": {
+9 -2
View File
@@ -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": {
+9 -2
View File
@@ -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": {
+9 -2
View File
@@ -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": {
+9 -2
View File
@@ -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": {
+9 -2
View File
@@ -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": {
+9 -2
View File
@@ -138,8 +138,15 @@
"deleteQueuedForUpload": "Видалення вузла поставлено в чергу для завантаження",
"confirmDeleteTitle": "Видалити Вузол",
"confirmDeleteMessage": "Ви впевнені, що хочете видалити вузол #{}? Цю дію не можна скасувати.",
"deleteReasonLabel": "Причина Видалення (Опціонально)",
"deleteReasonHint": "наприклад пристрій видалено, неправильне розташування, дублікат...",
"deleteReasonLabel": "Причина Видалення",
"deleteReasonSelectHint": "Вибрати...",
"deleteReasonDeviceRemoved": "Пристрій видалено",
"deleteReasonDuplicateNode": "Дубльований вузол",
"deleteReasonMisidentifiedInfra": "Неправильно ідентифікована інфраструктура",
"deleteReasonSpamVandalism": "Спам/вандалізм",
"deleteReasonCustom": "Власний..",
"deleteReasonHint": "Опишіть причину...",
"mustBeLoggedInToDelete": "Ви повинні увійти в обліковий запис OpenStreetMap, щоб надавати дані карти."
},
"addNode": {
+9 -2
View File
@@ -138,8 +138,15 @@
"deleteQueuedForUpload": "节点删除已排队上传",
"confirmDeleteTitle": "删除节点",
"confirmDeleteMessage": "您确定要删除节点 #{} 吗?此操作无法撤销。",
"deleteReasonLabel": "删除原因(可选)",
"deleteReasonHint": "例如:设备已移除、位置错误、重复...",
"deleteReasonLabel": "删除原因",
"deleteReasonSelectHint": "请选择...",
"deleteReasonDeviceRemoved": "设备已移除",
"deleteReasonDuplicateNode": "重复节点",
"deleteReasonMisidentifiedInfra": "识别错误的基础设施",
"deleteReasonSpamVandalism": "垃圾信息/破坏",
"deleteReasonCustom": "自定义..",
"deleteReasonHint": "请描述原因...",
"mustBeLoggedInToDelete": "您必须使用 OpenStreetMap 账户登录才能贡献地图数据。"
},
"addNode": {
+94 -15
View File
@@ -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')),
),
],
);
}
}
}