@@ -8,6 +8,7 @@ import 'package:kdbx/kdbx.dart' hide FieldType;
88import 'package:keevault/cubit/entry_cubit.dart' ;
99import 'package:keevault/logging/logger.dart' ;
1010import 'package:keevault/model/entry.dart' ;
11+ import 'package:keevault/cubit/autocomplete_cubit.dart' ;
1112
1213import 'package:clock/clock.dart' ;
1314import 'package:keevault/model/field.dart' ;
@@ -171,6 +172,17 @@ class _EntryTextFieldState extends _EntryFieldState implements FieldDelegate {
171172 void initState () {
172173 super .initState ();
173174 _focusNode.addListener (_focusNodeChanged);
175+
176+ final isUsernameField = widget.field.key? .key == KdbxKeyCommon .KEY_USER_NAME ;
177+ if (isUsernameField) {
178+ _focusNode.addListener (() {
179+ if (_focusNode.hasFocus) {
180+ _updateAutocompleteSuggestions (context);
181+ } else {
182+ _removeAutocompleteOverlay ();
183+ }
184+ });
185+ }
174186 _initController ();
175187 }
176188
@@ -184,10 +196,137 @@ class _EntryTextFieldState extends _EntryFieldState implements FieldDelegate {
184196 _controller = TextEditingController (text: widget.field.textValue);
185197 }
186198
199+ OverlayEntry ? _autocompleteOverlay;
200+ List <String > _filteredUsernames = [];
201+ bool _showAutocomplete = false ;
202+
203+ void _showAutocompleteOverlay (BuildContext context) {
204+ if (_autocompleteOverlay != null ) {
205+ _removeAutocompleteOverlay ();
206+ }
207+ final overlay = Overlay .of (context);
208+ // Find the RenderBox of the TextFormField
209+ final box = _formFieldKey.currentContext? .findRenderObject () as RenderBox ? ;
210+ final offset = box? .localToGlobal (Offset .zero) ?? Offset .zero;
211+ final width = box? .size.width ?? 300 ;
212+ _autocompleteOverlay = OverlayEntry (
213+ builder: (context) => Positioned (
214+ left: offset.dx,
215+ top: offset.dy + (box? .size.height ?? 56 ),
216+ width: width,
217+ child: Material (
218+ elevation: 4.0 ,
219+ color: Theme .of (context).colorScheme.surfaceContainer,
220+ child: ConstrainedBox (
221+ constraints: BoxConstraints (maxHeight: MediaQuery .of (context).size.height * 0.4 ),
222+ child: ListView .builder (
223+ shrinkWrap: true ,
224+ padding: EdgeInsets .zero,
225+ itemCount: _filteredUsernames.length,
226+ itemBuilder: (context, index) {
227+ final option = _filteredUsernames[index];
228+ return ListTile (
229+ title: Text (option),
230+ onTap: () {
231+ setState (() {
232+ _controller.text = option;
233+ _controller.selection = TextSelection .fromPosition (TextPosition (offset: option.length));
234+ _showAutocomplete = false ;
235+ _filteredUsernames = [];
236+ _removeAutocompleteOverlay ();
237+ _focusNode.unfocus ();
238+ _onUsernameChanged (option);
239+ });
240+ },
241+ );
242+ },
243+ ),
244+ ),
245+ ),
246+ ),
247+ );
248+ overlay.insert (_autocompleteOverlay! );
249+ }
250+
251+ void _removeAutocompleteOverlay () {
252+ _autocompleteOverlay? .remove ();
253+ _autocompleteOverlay = null ;
254+ }
255+
256+ void _updateAutocompleteSuggestions (BuildContext context) {
257+ final cubit = BlocProvider .of <AutocompleteCubit >(context, listen: false );
258+ final usernamesState = cubit.state;
259+ List <String > usernames = [];
260+ if (usernamesState is AutocompleteUsernamesLoaded ) {
261+ usernames = usernamesState.usernames;
262+ }
263+ final input = _controller.text.trim ();
264+ if (_focusNode.hasFocus) {
265+ if (input.isEmpty) {
266+ _filteredUsernames = usernames;
267+ } else {
268+ final lcInput = input.toLowerCase ();
269+ _filteredUsernames = usernames.where ((u) => u != lcInput && u.toLowerCase ().contains (lcInput)).toList ();
270+ }
271+ _showAutocomplete = _filteredUsernames.isNotEmpty;
272+ } else {
273+ _showAutocomplete = false ;
274+ _filteredUsernames = [];
275+ }
276+ if (_showAutocomplete) {
277+ WidgetsBinding .instance.addPostFrameCallback ((_) => _showAutocompleteOverlay (context));
278+ } else {
279+ _removeAutocompleteOverlay ();
280+ }
281+ }
282+
283+ _onUsernameChanged (value) {
284+ _updateAutocompleteSuggestions (context);
285+ final StringValue ? newValue = value == null
286+ ? null
287+ : _isProtected
288+ ? ProtectedValue .fromString (value)
289+ : PlainValue (value);
290+ final cubit = BlocProvider .of <EntryCubit >(context);
291+ if (widget.field.fieldStorage == FieldStorage .JSON ) {
292+ if (widget.field.browserModel! .value == value) {
293+ // Flutter can call onChange when no changes have occurred!
294+ return ;
295+ }
296+ cubit.updateField (
297+ null ,
298+ widget.field.browserModel! .name,
299+ value: newValue,
300+ browserModel: widget.field.browserModel! .copyWith (value: value),
301+ );
302+ } else {
303+ if (widget.field.value.getText () == value) {
304+ // Flutter can call onChange when no changes have occurred!
305+ return ;
306+ }
307+ cubit.updateField (widget.field.key, null , value: newValue);
308+ }
309+ }
310+
187311 @override
188312 Widget build (BuildContext context) {
189313 l.d ('building ${widget .key } ($_isValueObscured )' );
190314 final str = S .of (context);
315+ final isUsernameField = widget.field.key? .key == KdbxKeyCommon .KEY_USER_NAME ;
316+ Widget fieldEditor;
317+ if (isUsernameField) {
318+ fieldEditor = StringEntryFieldEditor (
319+ onChange: _onUsernameChanged,
320+ fieldKey: widget.field.key,
321+ controller: _controller,
322+ formFieldKey: _formFieldKey,
323+ focusNode: _focusNode,
324+ delegate: this ,
325+ field: widget.field,
326+ );
327+ } else {
328+ fieldEditor = _buildEntryFieldEditor ();
329+ }
191330 return Dismissible (
192331 key: ValueKey (widget.field.key),
193332 background: Container (
@@ -215,7 +354,7 @@ class _EntryTextFieldState extends _EntryFieldState implements FieldDelegate {
215354 padding: const EdgeInsets .symmetric (horizontal: 16 ),
216355 child: Row (
217356 children: < Widget > [
218- Expanded (child: _buildEntryFieldEditor () ),
357+ Expanded (child: fieldEditor ),
219358 Container (
220359 width: 48 ,
221360 height: 48 ,
@@ -234,6 +373,12 @@ class _EntryTextFieldState extends _EntryFieldState implements FieldDelegate {
234373 }
235374
236375 void _focusNodeChanged () {
376+ // // For username field, trigger loading suggestions when focused
377+ // final isUsernameField = widget.field.key?.key == KdbxKeyCommon.KEY_USER_NAME;
378+ // if (isUsernameField && _focusNode.hasFocus) {
379+ // final autocompleteCubit = BlocProvider.of<AutocompleteCubit>(context, listen: false);
380+ // autocompleteCubit.loadUsernames();
381+ // }
237382 if (! _isProtected) {
238383 return ;
239384 }
@@ -464,6 +609,7 @@ class _EntryTextFieldState extends _EntryFieldState implements FieldDelegate {
464609 @override
465610 void dispose () {
466611 l.d ('EntryFieldState.dispose() - ${widget .key } (${widget .field .key })' );
612+ _removeAutocompleteOverlay ();
467613 _controller.dispose ();
468614 _focusNode.dispose ();
469615 super .dispose ();
0 commit comments