log in button, submission guide cancel, filter nsi by popularity and entered text

This commit is contained in:
stopflock
2025-12-17 17:36:59 -06:00
parent aab4f6d445
commit c3752fd17e
15 changed files with 133 additions and 114 deletions
+6 -1
View File
@@ -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": {
+3
View File
@@ -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)
+1
View File
@@ -15,6 +15,7 @@
"ok": "OK",
"close": "Schließen",
"submit": "Senden",
"logIn": "Anmelden",
"saveEdit": "Bearbeitung Speichern",
"clear": "Löschen",
"viewOnOSM": "Auf OSM anzeigen",
+1
View File
@@ -52,6 +52,7 @@
"ok": "OK",
"close": "Close",
"submit": "Submit",
"logIn": "Log In",
"saveEdit": "Save Edit",
"clear": "Clear",
"viewOnOSM": "View on OSM",
+1
View File
@@ -52,6 +52,7 @@
"ok": "Aceptar",
"close": "Cerrar",
"submit": "Enviar",
"logIn": "Iniciar Sesión",
"saveEdit": "Guardar Edición",
"clear": "Limpiar",
"viewOnOSM": "Ver en OSM",
+1
View File
@@ -52,6 +52,7 @@
"ok": "OK",
"close": "Fermer",
"submit": "Soumettre",
"logIn": "Se Connecter",
"saveEdit": "Sauvegarder Modification",
"clear": "Effacer",
"viewOnOSM": "Voir sur OSM",
+1
View File
@@ -52,6 +52,7 @@
"ok": "OK",
"close": "Chiudi",
"submit": "Invia",
"logIn": "Accedi",
"saveEdit": "Salva Modifica",
"clear": "Pulisci",
"viewOnOSM": "Visualizza su OSM",
+1
View File
@@ -52,6 +52,7 @@
"ok": "OK",
"close": "Fechar",
"submit": "Enviar",
"logIn": "Entrar",
"saveEdit": "Salvar Edição",
"clear": "Limpar",
"viewOnOSM": "Ver no OSM",
+1
View File
@@ -52,6 +52,7 @@
"ok": "确定",
"close": "关闭",
"submit": "提交",
"logIn": "登录",
"saveEdit": "保存编辑",
"clear": "清空",
"viewOnOSM": "在OSM上查看",
+9 -2
View File
@@ -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<String, dynamic>;
final values = data['data'] as List<dynamic>? ?? [];
// Extract the most commonly used values
// Extract the most commonly used values that meet our minimum hit threshold
final suggestions = <String>[];
for (final item in values) {
if (item is Map<String, dynamic>) {
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());
}
}
+12 -3
View File
@@ -96,11 +96,16 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
if (!hasSeenGuide) {
// Show submission guide dialog first
await showDialog<void>(
final shouldProceed = await showDialog<bool>(
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<AddNodeSheet> {
session.profile != null &&
session.profile!.isSubmittable;
void _navigateToLogin() {
Navigator.pushNamed(context, '/settings/osm-account');
}
void _openRefineTags() async {
final result = await Navigator.push<RefineTagsResult?>(
context,
@@ -439,8 +448,8 @@ class _AddNodeSheetState extends State<AddNodeSheet> {
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')),
),
),
],
+12 -3
View File
@@ -81,11 +81,16 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
if (!hasSeenGuide) {
// Show submission guide dialog first
await showDialog<void>(
final shouldProceed = await showDialog<bool>(
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<EditNodeSheet> {
session.profile != null &&
session.profile!.isSubmittable;
void _navigateToLogin() {
Navigator.pushNamed(context, '/settings/osm-account');
}
void _openRefineTags() async {
final result = await Navigator.push<RefineTagsResult?>(
context,
@@ -495,8 +504,8 @@ class _EditNodeSheetState extends State<EditNodeSheet> {
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')),
),
),
],
+58 -12
View File
@@ -38,6 +38,7 @@ class _NSITagValueFieldState extends State<NSITagValueField> {
_loadSuggestions();
_focusNode.addListener(_onFocusChanged);
_controller.addListener(_onTextChanged);
}
@override
@@ -65,6 +66,26 @@ class _NSITagValueFieldState extends State<NSITagValueField> {
super.dispose();
}
/// Get filtered suggestions based on current text input (case-sensitive)
List<String> _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<NSITagValueField> {
}
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<NSITagValueField> {
}
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<String> 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<NSITagValueField> {
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<NSITagValueField> {
),
),
);
Overlay.of(context).insert(_overlayEntry);
setState(() {
_showingSuggestions = true;
});
}
void _hideSuggestions() {
@@ -150,6 +194,8 @@ class _NSITagValueFieldState extends State<NSITagValueField> {
@override
Widget build(BuildContext context) {
final filteredSuggestions = _getFilteredSuggestions();
return CompositedTransformTarget(
link: _layerLink,
child: TextField(
@@ -171,7 +217,7 @@ class _NSITagValueFieldState extends State<NSITagValueField> {
widget.onChanged(value);
},
onTap: () {
if (!widget.readOnly && _suggestions.isNotEmpty) {
if (!widget.readOnly && filteredSuggestions.isNotEmpty) {
_showSuggestions();
}
},
+18 -92
View File
@@ -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<RefineTagsSheet> {
OperatorProfile? _selectedOperatorProfile;
Map<String, String> _refinedTags = {};
Map<String, List<String>> _tagSuggestions = {};
Map<String, bool> _loadingSuggestions = {};
@override
void initState() {
super.initState();
_selectedOperatorProfile = widget.selectedOperatorProfile;
_refinedTags = Map<String, String>.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<RefineTagsSheet> {
];
}
/// 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<RefineTagsSheet> {
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<String>(
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<String>(
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<String>(
value: null,
child: Text(locService.t('refineTagsSheet.noValue'),
style: const TextStyle(color: Colors.grey)),
),
...suggestions.map((suggestion) => DropdownMenuItem<String>(
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();
}
});
},
),
],
),
);
+8 -1
View File
@@ -50,7 +50,7 @@ class _SubmissionGuideDialogState extends State<SubmissionGuideDialog> {
}
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<SubmissionGuideDialog> {
],
),
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')),