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

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')),
),
),
],

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')),
),
),
],

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();
}
},

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();
}
});
},
),
],
),
);

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')),