diff --git a/apps/app_seeds/lib/ui/variety_detail_screen.dart b/apps/app_seeds/lib/ui/variety_detail_screen.dart index 8dd01cb..2442092 100644 --- a/apps/app_seeds/lib/ui/variety_detail_screen.dart +++ b/apps/app_seeds/lib/ui/variety_detail_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:typed_data'; import 'package:drift/drift.dart' show Value; @@ -619,8 +620,20 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { late int? _seedHarvestMonths = widget.detail.seedHarvestMonths; late bool _calendarOpen = widget.detail.hasCropCalendar; + // Typing stays responsive by not touching the DB on every keystroke: each + // field waits until the grower pauses before running its (heavy) lookup, and + // a per-field query counter drops any result that a later keystroke has + // already superseded, so stale suggestions never flicker back in. + static const _debounce = Duration(milliseconds: 250); + Timer? _speciesDebounce; + Timer? _nameDebounce; + int _speciesQueryId = 0; + int _nameQueryId = 0; + @override void dispose() { + _speciesDebounce?.cancel(); + _nameDebounce?.cancel(); _name.dispose(); _category.dispose(); _notes.dispose(); @@ -628,21 +641,38 @@ class _EditVarietySheetState extends State<_EditVarietySheet> { super.dispose(); } - Future _onSpeciesQuery(String query) async { + void _onSpeciesQuery(String query) { + _speciesDebounce?.cancel(); + _speciesDebounce = Timer(_debounce, () => _runSpeciesQuery(query)); + } + + Future _runSpeciesQuery(String query) async { + final queryId = ++_speciesQueryId; final lang = Localizations.localeOf(context).languageCode; final results = await widget.species.search(query, languageCode: lang); // A manual species search takes over from the name-based suggestion. - if (mounted) setState(() => _suggestions = results); + if (mounted && queryId == _speciesQueryId) { + setState(() => _suggestions = results); + } + } + + void _onNameChanged(String name) { + if (widget.detail.speciesId != null || _pickedSpeciesId != null) return; + _nameDebounce?.cancel(); + _nameDebounce = Timer(_debounce, () => _runNameClassify(name)); } /// As the name is typed, offer the species it names — but only when the /// variety has no species yet and the grower hasn't picked one, so an /// existing link is never second-guessed. - Future _onNameChanged(String name) async { + Future _runNameClassify(String name) async { if (widget.detail.speciesId != null || _pickedSpeciesId != null) return; + final queryId = ++_nameQueryId; final lang = Localizations.localeOf(context).languageCode; final match = await widget.species.classifyLabel(name, languageCode: lang); - if (mounted) setState(() => _suggested = match); + if (mounted && queryId == _nameQueryId) { + setState(() => _suggested = match); + } } void _pick(SpeciesMatch match) {