From c3752fd17e1b97e44a7aa69aa2d09609232c389d Mon Sep 17 00:00:00 2001 From: stopflock Date: Wed, 17 Dec 2025 17:36:59 -0600 Subject: [PATCH] log in button, submission guide cancel, filter nsi by popularity and entered text --- assets/changelog.json | 7 +- lib/dev_config.dart | 3 + lib/localizations/de.json | 1 + lib/localizations/en.json | 1 + lib/localizations/es.json | 1 + lib/localizations/fr.json | 1 + lib/localizations/it.json | 1 + lib/localizations/pt.json | 1 + lib/localizations/zh.json | 1 + lib/services/nsi_service.dart | 11 ++- lib/widgets/add_node_sheet.dart | 15 +++- lib/widgets/edit_node_sheet.dart | 15 +++- lib/widgets/nsi_tag_value_field.dart | 70 ++++++++++++--- lib/widgets/refine_tags_sheet.dart | 110 ++++------------------- lib/widgets/submission_guide_dialog.dart | 9 +- 15 files changed, 133 insertions(+), 114 deletions(-) diff --git a/assets/changelog.json b/assets/changelog.json index 413578c..a83281a 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,7 +1,12 @@ { "2.2.0": { "content": [ - "• Fixed follow-me sync issues where tracking would sometimes stop working after mode changes" + "• Fixed follow-me sync issues where tracking would sometimes stop working after mode changes", + "• Added cancel button to submission guide dialog - users can now go back and revise their submissions", + "• When not logged in, submit/edit buttons now say 'Log In' and navigate to account settings instead of being disabled", + "• Improved NSI tag suggestions: now only shows values with sufficient usage (100+ occurrences) to avoid rare/unhelpful suggestions like for 'image=' tags", + "• Enhanced tag refinement: refine tags sheet now allows arbitrary text entry like the profile editor, not just dropdown selection", + "• New tags are now added to the top of the profile tag list for immediate visibility instead of being hidden at the bottom" ] }, "2.1.3": { diff --git a/lib/dev_config.dart b/lib/dev_config.dart index dfdddcb..3f4eb85 100644 --- a/lib/dev_config.dart +++ b/lib/dev_config.dart @@ -137,6 +137,9 @@ const double kNavigationDistanceWarningThreshold = 20000.0; // meters - distance // Node display configuration const int kDefaultMaxNodes = 500; // Default maximum number of nodes to render on the map at once +// NSI (Name Suggestion Index) configuration +const int kNSIMinimumHitCount = 500; // Minimum hit count for NSI suggestions to be considered useful + // Map interaction configuration const double kNodeDoubleTapZoomDelta = 1.0; // How much to zoom in when double-tapping nodes (was 1.0) const double kScrollWheelVelocity = 0.01; // Mouse scroll wheel zoom speed (default 0.005) diff --git a/lib/localizations/de.json b/lib/localizations/de.json index bce6105..5c57eca 100644 --- a/lib/localizations/de.json +++ b/lib/localizations/de.json @@ -15,6 +15,7 @@ "ok": "OK", "close": "Schließen", "submit": "Senden", + "logIn": "Anmelden", "saveEdit": "Bearbeitung Speichern", "clear": "Löschen", "viewOnOSM": "Auf OSM anzeigen", diff --git a/lib/localizations/en.json b/lib/localizations/en.json index e19c068..6ed604e 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -52,6 +52,7 @@ "ok": "OK", "close": "Close", "submit": "Submit", + "logIn": "Log In", "saveEdit": "Save Edit", "clear": "Clear", "viewOnOSM": "View on OSM", diff --git a/lib/localizations/es.json b/lib/localizations/es.json index 483cd4e..4d8a07c 100644 --- a/lib/localizations/es.json +++ b/lib/localizations/es.json @@ -52,6 +52,7 @@ "ok": "Aceptar", "close": "Cerrar", "submit": "Enviar", + "logIn": "Iniciar Sesión", "saveEdit": "Guardar Edición", "clear": "Limpiar", "viewOnOSM": "Ver en OSM", diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json index 18f7799..1018c92 100644 --- a/lib/localizations/fr.json +++ b/lib/localizations/fr.json @@ -52,6 +52,7 @@ "ok": "OK", "close": "Fermer", "submit": "Soumettre", + "logIn": "Se Connecter", "saveEdit": "Sauvegarder Modification", "clear": "Effacer", "viewOnOSM": "Voir sur OSM", diff --git a/lib/localizations/it.json b/lib/localizations/it.json index 5059d5c..b281942 100644 --- a/lib/localizations/it.json +++ b/lib/localizations/it.json @@ -52,6 +52,7 @@ "ok": "OK", "close": "Chiudi", "submit": "Invia", + "logIn": "Accedi", "saveEdit": "Salva Modifica", "clear": "Pulisci", "viewOnOSM": "Visualizza su OSM", diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json index 3fd4b6a..ae2d36a 100644 --- a/lib/localizations/pt.json +++ b/lib/localizations/pt.json @@ -52,6 +52,7 @@ "ok": "OK", "close": "Fechar", "submit": "Enviar", + "logIn": "Entrar", "saveEdit": "Salvar Edição", "clear": "Limpar", "viewOnOSM": "Ver no OSM", diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json index 69945ab..7daf0fe 100644 --- a/lib/localizations/zh.json +++ b/lib/localizations/zh.json @@ -52,6 +52,7 @@ "ok": "确定", "close": "关闭", "submit": "提交", + "logIn": "登录", "saveEdit": "保存编辑", "clear": "清空", "viewOnOSM": "在OSM上查看", diff --git a/lib/services/nsi_service.dart b/lib/services/nsi_service.dart index 0368415..f799d26 100644 --- a/lib/services/nsi_service.dart +++ b/lib/services/nsi_service.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../app_state.dart'; +import '../dev_config.dart'; /// Service for fetching tag value suggestions from OpenStreetMap Name Suggestion Index class NSIService { @@ -66,13 +67,19 @@ class NSIService { final data = jsonDecode(response.body) as Map; final values = data['data'] as List? ?? []; - // Extract the most commonly used values + // Extract the most commonly used values that meet our minimum hit threshold final suggestions = []; for (final item in values) { if (item is Map) { final value = item['value'] as String?; - if (value != null && value.trim().isNotEmpty && _isValidSuggestion(value)) { + final count = item['count'] as int? ?? 0; + + // Only include suggestions that meet our minimum hit count threshold + if (value != null && + value.trim().isNotEmpty && + count >= kNSIMinimumHitCount && + _isValidSuggestion(value)) { suggestions.add(value.trim()); } } diff --git a/lib/widgets/add_node_sheet.dart b/lib/widgets/add_node_sheet.dart index 6b7b1d2..634e954 100644 --- a/lib/widgets/add_node_sheet.dart +++ b/lib/widgets/add_node_sheet.dart @@ -96,11 +96,16 @@ class _AddNodeSheetState extends State { if (!hasSeenGuide) { // Show submission guide dialog first - await showDialog( + final shouldProceed = await showDialog( context: context, barrierDismissible: false, builder: (context) => const SubmissionGuideDialog(), ); + + // If user canceled the submission guide, don't proceed with submission + if (shouldProceed != true) { + return; + } } // Now proceed with proximity check @@ -296,6 +301,10 @@ class _AddNodeSheetState extends State { session.profile != null && session.profile!.isSubmittable; + void _navigateToLogin() { + Navigator.pushNamed(context, '/settings/osm-account'); + } + void _openRefineTags() async { final result = await Navigator.push( context, @@ -439,8 +448,8 @@ class _AddNodeSheetState extends State { const SizedBox(width: 12), Expanded( child: ElevatedButton( - onPressed: allowSubmit ? _commit : null, - child: Text(locService.t('actions.submit')), + onPressed: !appState.isLoggedIn ? _navigateToLogin : (allowSubmit ? _commit : null), + child: Text(!appState.isLoggedIn ? locService.t('actions.logIn') : locService.t('actions.submit')), ), ), ], diff --git a/lib/widgets/edit_node_sheet.dart b/lib/widgets/edit_node_sheet.dart index 87a4a8e..76a8291 100644 --- a/lib/widgets/edit_node_sheet.dart +++ b/lib/widgets/edit_node_sheet.dart @@ -81,11 +81,16 @@ class _EditNodeSheetState extends State { if (!hasSeenGuide) { // Show submission guide dialog first - await showDialog( + final shouldProceed = await showDialog( context: context, barrierDismissible: false, builder: (context) => const SubmissionGuideDialog(), ); + + // If user canceled the submission guide, don't proceed with submission + if (shouldProceed != true) { + return; + } } // Now proceed with proximity check @@ -278,6 +283,10 @@ class _EditNodeSheetState extends State { session.profile != null && session.profile!.isSubmittable; + void _navigateToLogin() { + Navigator.pushNamed(context, '/settings/osm-account'); + } + void _openRefineTags() async { final result = await Navigator.push( context, @@ -495,8 +504,8 @@ class _EditNodeSheetState extends State { const SizedBox(width: 12), Expanded( child: ElevatedButton( - onPressed: allowSubmit ? _commit : null, - child: Text(locService.t('actions.saveEdit')), + onPressed: !appState.isLoggedIn ? _navigateToLogin : (allowSubmit ? _commit : null), + child: Text(!appState.isLoggedIn ? locService.t('actions.logIn') : locService.t('actions.saveEdit')), ), ), ], diff --git a/lib/widgets/nsi_tag_value_field.dart b/lib/widgets/nsi_tag_value_field.dart index 86d9528..1f33c71 100644 --- a/lib/widgets/nsi_tag_value_field.dart +++ b/lib/widgets/nsi_tag_value_field.dart @@ -38,6 +38,7 @@ class _NSITagValueFieldState extends State { _loadSuggestions(); _focusNode.addListener(_onFocusChanged); + _controller.addListener(_onTextChanged); } @override @@ -65,6 +66,26 @@ class _NSITagValueFieldState extends State { super.dispose(); } + /// Get filtered suggestions based on current text input (case-sensitive) + List _getFilteredSuggestions() { + final currentText = _controller.text; + if (currentText.isEmpty) { + return _suggestions; + } + + return _suggestions + .where((suggestion) => suggestion.contains(currentText)) + .toList(); + } + + /// Handle text changes to update suggestion filtering + void _onTextChanged() { + if (_showingSuggestions) { + // Update the overlay with filtered suggestions + _updateSuggestionsOverlay(); + } + } + void _loadSuggestions() async { if (widget.tagKey.trim().isEmpty) return; @@ -86,7 +107,8 @@ class _NSITagValueFieldState extends State { } void _onFocusChanged() { - if (_focusNode.hasFocus && _suggestions.isNotEmpty && !widget.readOnly) { + final filteredSuggestions = _getFilteredSuggestions(); + if (_focusNode.hasFocus && filteredSuggestions.isNotEmpty && !widget.readOnly) { _showSuggestions(); } else { _hideSuggestions(); @@ -94,11 +116,38 @@ class _NSITagValueFieldState extends State { } void _showSuggestions() { - if (_showingSuggestions || _suggestions.isEmpty) return; + final filteredSuggestions = _getFilteredSuggestions(); + if (_showingSuggestions || filteredSuggestions.isEmpty) return; - _overlayEntry = OverlayEntry( + _overlayEntry = _buildSuggestionsOverlay(filteredSuggestions); + Overlay.of(context).insert(_overlayEntry); + setState(() { + _showingSuggestions = true; + }); + } + + /// Update the suggestions overlay with current filtered suggestions + void _updateSuggestionsOverlay() { + final filteredSuggestions = _getFilteredSuggestions(); + + if (filteredSuggestions.isEmpty) { + _hideSuggestions(); + return; + } + + if (_showingSuggestions) { + // Remove current overlay and create new one with filtered suggestions + _overlayEntry.remove(); + _overlayEntry = _buildSuggestionsOverlay(filteredSuggestions); + Overlay.of(context).insert(_overlayEntry); + } + } + + /// Build the suggestions overlay with the given suggestions list + OverlayEntry _buildSuggestionsOverlay(List suggestions) { + return OverlayEntry( builder: (context) => Positioned( - width: 200, // Fixed width for suggestions + width: 250, // Slightly wider to fit more content in refine tags child: CompositedTransformFollower( link: _layerLink, showWhenUnlinked: false, @@ -111,9 +160,9 @@ class _NSITagValueFieldState extends State { child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, - itemCount: _suggestions.length, + itemCount: suggestions.length, itemBuilder: (context, index) { - final suggestion = _suggestions[index]; + final suggestion = suggestions[index]; return ListTile( dense: true, title: Text(suggestion, style: const TextStyle(fontSize: 14)), @@ -126,11 +175,6 @@ class _NSITagValueFieldState extends State { ), ), ); - - Overlay.of(context).insert(_overlayEntry); - setState(() { - _showingSuggestions = true; - }); } void _hideSuggestions() { @@ -150,6 +194,8 @@ class _NSITagValueFieldState extends State { @override Widget build(BuildContext context) { + final filteredSuggestions = _getFilteredSuggestions(); + return CompositedTransformTarget( link: _layerLink, child: TextField( @@ -171,7 +217,7 @@ class _NSITagValueFieldState extends State { widget.onChanged(value); }, onTap: () { - if (!widget.readOnly && _suggestions.isNotEmpty) { + if (!widget.readOnly && filteredSuggestions.isNotEmpty) { _showSuggestions(); } }, diff --git a/lib/widgets/refine_tags_sheet.dart b/lib/widgets/refine_tags_sheet.dart index b9a0a76..85d82e6 100644 --- a/lib/widgets/refine_tags_sheet.dart +++ b/lib/widgets/refine_tags_sheet.dart @@ -5,7 +5,7 @@ import '../app_state.dart'; import '../models/operator_profile.dart'; import '../models/node_profile.dart'; import '../services/localization_service.dart'; -import '../services/nsi_service.dart'; +import 'nsi_tag_value_field.dart'; /// Result returned from RefineTagsSheet class RefineTagsResult { @@ -37,47 +37,12 @@ class RefineTagsSheet extends StatefulWidget { class _RefineTagsSheetState extends State { OperatorProfile? _selectedOperatorProfile; Map _refinedTags = {}; - Map> _tagSuggestions = {}; - Map _loadingSuggestions = {}; @override void initState() { super.initState(); _selectedOperatorProfile = widget.selectedOperatorProfile; _refinedTags = Map.from(widget.currentRefinedTags ?? {}); - _loadTagSuggestions(); - } - - /// Load suggestions for all empty-value tags in the selected profile - void _loadTagSuggestions() async { - if (widget.selectedProfile == null) return; - - final refinableTags = _getRefinableTags(); - - for (final tagKey in refinableTags) { - if (_tagSuggestions.containsKey(tagKey)) continue; - - setState(() { - _loadingSuggestions[tagKey] = true; - }); - - try { - final suggestions = await NSIService().getAllSuggestions(tagKey); - if (mounted) { - setState(() { - _tagSuggestions[tagKey] = suggestions; - _loadingSuggestions[tagKey] = false; - }); - } - } catch (e) { - if (mounted) { - setState(() { - _tagSuggestions[tagKey] = []; - _loadingSuggestions[tagKey] = false; - }); - } - } - } } /// Get list of tag keys that have empty values and can be refined @@ -262,11 +227,9 @@ class _RefineTagsSheetState extends State { ]; } - /// Build a dropdown for a single refineable tag + /// Build a text field for a single refineable tag (similar to profile editor) Widget _buildTagDropdown(String tagKey, LocalizationService locService) { - final suggestions = _tagSuggestions[tagKey] ?? []; - final isLoading = _loadingSuggestions[tagKey] ?? false; - final currentValue = _refinedTags[tagKey]; + final currentValue = _refinedTags[tagKey] ?? ''; return Padding( padding: const EdgeInsets.only(bottom: 16.0), @@ -278,58 +241,21 @@ class _RefineTagsSheetState extends State { style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), const SizedBox(height: 4), - if (isLoading) - const Row( - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - SizedBox(width: 8), - Text('Loading suggestions...', style: TextStyle(color: Colors.grey)), - ], - ) - else if (suggestions.isEmpty) - DropdownButtonFormField( - value: currentValue?.isNotEmpty == true ? currentValue : null, - decoration: InputDecoration( - hintText: locService.t('refineTagsSheet.noSuggestions'), - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), - items: const [], - onChanged: null, // Disabled when no suggestions - ) - else - DropdownButtonFormField( - value: currentValue?.isNotEmpty == true ? currentValue : null, - decoration: InputDecoration( - hintText: locService.t('refineTagsSheet.selectValue'), - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), - items: [ - DropdownMenuItem( - value: null, - child: Text(locService.t('refineTagsSheet.noValue'), - style: const TextStyle(color: Colors.grey)), - ), - ...suggestions.map((suggestion) => DropdownMenuItem( - value: suggestion, - child: Text(suggestion), - )), - ], - onChanged: (value) { - setState(() { - if (value == null) { - _refinedTags.remove(tagKey); - } else { - _refinedTags[tagKey] = value; - } - }); - }, - ), + NSITagValueField( + key: ValueKey('${tagKey}_refine'), + tagKey: tagKey, + initialValue: currentValue, + hintText: locService.t('refineTagsSheet.selectValue'), + onChanged: (value) { + setState(() { + if (value.trim().isEmpty) { + _refinedTags.remove(tagKey); + } else { + _refinedTags[tagKey] = value.trim(); + } + }); + }, + ), ], ), ); diff --git a/lib/widgets/submission_guide_dialog.dart b/lib/widgets/submission_guide_dialog.dart index 9d1bd9e..37262ba 100644 --- a/lib/widgets/submission_guide_dialog.dart +++ b/lib/widgets/submission_guide_dialog.dart @@ -50,7 +50,7 @@ class _SubmissionGuideDialogState extends State { } if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context).pop(true); // Return true to indicate "proceed with submission" } } @@ -148,6 +148,13 @@ class _SubmissionGuideDialogState extends State { ], ), actions: [ + TextButton( + onPressed: () { + // Cancel - just close dialog without marking as seen, return false to cancel submission + Navigator.of(context).pop(false); + }, + child: Text(locService.cancel), + ), TextButton( onPressed: _onClose, child: Text(locService.t('submissionGuide.gotIt')),