import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class FullscreenLabelForm extends StatefulWidget { final IdQueryParameter? initialValue; final Map options; final Future Function(String? initialName)? onCreateNewLabel; final bool showNotAssignedOption; final bool showAnyAssignedOption; final void Function({IdQueryParameter returnValue}) onSubmit; final Widget leadingIcon; final String? addNewLabelText; FullscreenLabelForm({ super.key, this.initialValue, required this.options, required this.onCreateNewLabel, this.showNotAssignedOption = true, this.showAnyAssignedOption = true, required this.onSubmit, required this.leadingIcon, this.addNewLabelText, }) : assert( !(initialValue?.onlyAssigned ?? false) || showAnyAssignedOption, ), assert( !(initialValue?.onlyNotAssigned ?? false) || showNotAssignedOption, ), assert((addNewLabelText != null) == (onCreateNewLabel != null)); @override State createState() => _FullscreenLabelFormState(); } class _FullscreenLabelFormState extends State> { bool _showClearIcon = false; final _textEditingController = TextEditingController(); final _focusNode = FocusNode(); @override void initState() { super.initState(); _textEditingController.addListener(() => setState(() { _showClearIcon = _textEditingController.text.isNotEmpty; })); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { //Delay keyboard popup to ensure open animation is finished before. Future.delayed( const Duration(milliseconds: 200), () => _focusNode.requestFocus(), ); }); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final options = _filterOptionsByQuery(_textEditingController.text); return Scaffold( appBar: AppBar( backgroundColor: theme.colorScheme.surface, toolbarHeight: 72, leading: BackButton( color: theme.colorScheme.onSurface, ), title: TextFormField( focusNode: _focusNode, controller: _textEditingController, onFieldSubmitted: (value) { FocusScope.of(context).unfocus(); final index = AutocompleteHighlightedOption.of(context); final value = index.isNegative ? null : options.elementAt(index); widget.onSubmit(returnValue: IdQueryParameter.fromId(value?.id)); }, autofocus: true, style: theme.textTheme.bodyLarge?.apply( color: theme.colorScheme.onSurface, ), decoration: InputDecoration( contentPadding: EdgeInsets.zero, hintStyle: theme.textTheme.bodyLarge?.apply( color: theme.colorScheme.onSurfaceVariant, ), icon: widget.leadingIcon, hintText: _buildHintText(), border: InputBorder.none, ), textInputAction: TextInputAction.done, ), actions: [ if (_showClearIcon) IconButton( icon: const Icon(Icons.clear), onPressed: () { _textEditingController.clear(); }, ), ], bottom: PreferredSize( preferredSize: const Size.fromHeight(1), child: Divider( color: theme.colorScheme.outline, ), ), ), body: Builder( builder: (context) { return Column( children: [ Expanded( child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, itemCount: options.length, itemBuilder: (BuildContext context, int index) { final option = options.elementAt(index); final highlight = AutocompleteHighlightedOption.of(context) == index; if (highlight) { SchedulerBinding.instance .addPostFrameCallback((Duration timeStamp) { Scrollable.ensureVisible( context, alignment: 0, ); }); } return _buildOptionWidget(option, highlight); }, ), ), ], ); }, ), ); } void _onCreateNewLabel() async { final label = await widget.onCreateNewLabel!(_textEditingController.text); if (label?.id != null) { widget.onSubmit( returnValue: IdQueryParameter.fromId(label!.id!), ); } } /// /// Filters the options passed to this widget by the current [query] and /// adds not-/any assigned options /// Iterable _filterOptionsByQuery(String query) sync* { final normalizedQuery = query.trim().toLowerCase(); if (normalizedQuery.isEmpty) { if (widget.initialValue == null) { // If nothing is selected yet, show all options first. for (final option in widget.options.values) { yield IdQueryParameter.fromId(option.id); } if (widget.showNotAssignedOption) { yield const IdQueryParameter.notAssigned(); } if (widget.showAnyAssignedOption) { yield const IdQueryParameter.anyAssigned(); } } else { // If an initial value is given, show not assigned first, which will be selected by default when pressing "done" on keyboard. if (widget.showNotAssignedOption) { yield const IdQueryParameter.notAssigned(); } if (widget.showAnyAssignedOption) { yield const IdQueryParameter.anyAssigned(); } for (final option in widget.options.values) { // Don't include the initial value in the selection if (option.id == widget.initialValue?.id) { continue; } yield IdQueryParameter.fromId(option.id); } } } else { // Show filtered options, if no matching option is found, always show not assigned and any assigned (if enabled) and proceed. final matches = widget.options.values .where((e) => e.name.trim().toLowerCase().contains(normalizedQuery)); if (matches.isNotEmpty) { for (final match in matches) { yield IdQueryParameter.fromId(match.id); } if (widget.showNotAssignedOption) { yield const IdQueryParameter.notAssigned(); } if (widget.showAnyAssignedOption) { yield const IdQueryParameter.anyAssigned(); } } else { if (widget.showNotAssignedOption) { yield const IdQueryParameter.notAssigned(); } if (widget.showAnyAssignedOption) { yield const IdQueryParameter.anyAssigned(); } if (!(widget.showAnyAssignedOption || widget.showNotAssignedOption)) { yield const IdQueryParameter.unset(); } } } } String? _buildHintText() { if (widget.initialValue?.isSet ?? false) { return widget.options[widget.initialValue!.id]?.name ?? 'undefined'; } if (widget.initialValue?.onlyNotAssigned ?? false) { return S.of(context)!.notAssigned; } if (widget.initialValue?.onlyAssigned ?? false) { return S.of(context)!.anyAssigned; } return S.of(context)!.startTyping; } Widget _buildOptionWidget(IdQueryParameter option, bool highlight) { void onTap() => widget.onSubmit(returnValue: option); late final String title; if (option.isSet) { title = widget.options[option.id]!.name; } if (option.onlyNotAssigned) { title = S.of(context)!.notAssigned; } if (option.onlyAssigned) { title = S.of(context)!.anyAssigned; } if (option.isUnset) { return Center( child: Column( children: [ Text(S.of(context)!.noItemsFound).padded(), if (widget.onCreateNewLabel != null) TextButton( child: Text(widget.addNewLabelText!), onPressed: _onCreateNewLabel, ), ], ), ); } return ListTile( selected: highlight, selectedTileColor: Theme.of(context).focusColor, title: Text(title), onTap: onTap, ); } }