diff --git a/assets/images/ic_attachment_sb.svg b/assets/images/ic_attachment_sb.svg
new file mode 100644
index 000000000..587f09577
--- /dev/null
+++ b/assets/images/ic_attachment_sb.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/ic_calendar_sb.svg b/assets/images/ic_calendar_sb.svg
new file mode 100644
index 000000000..547bca3ce
--- /dev/null
+++ b/assets/images/ic_calendar_sb.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/ic_clock_sb.svg b/assets/images/ic_clock_sb.svg
new file mode 100644
index 000000000..2df7eed1c
--- /dev/null
+++ b/assets/images/ic_clock_sb.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/ic_selected_sb.svg b/assets/images/ic_selected_sb.svg
new file mode 100644
index 000000000..d30ae3226
--- /dev/null
+++ b/assets/images/ic_selected_sb.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/ic_user_sb.svg b/assets/images/ic_user_sb.svg
new file mode 100644
index 000000000..031b55554
--- /dev/null
+++ b/assets/images/ic_user_sb.svg
@@ -0,0 +1,4 @@
+
diff --git a/core/lib/core.dart b/core/lib/core.dart
index 59a353a0c..97a865cbd 100644
--- a/core/lib/core.dart
+++ b/core/lib/core.dart
@@ -62,6 +62,7 @@ export 'presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart';
export 'presentation/views/search/search_bar_view.dart';
export 'presentation/views/popup_menu/popup_menu_item_widget.dart';
export 'presentation/views/tab_bar/custom_tab_indicator.dart';
+export 'presentation/views/typeahead/typeahead_for_quick_search.dart';
// Resources
export 'presentation/resources/assets_paths.dart';
diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart
index 4866722ce..e3fba3905 100644
--- a/core/lib/presentation/resources/image_paths.dart
+++ b/core/lib/presentation/resources/image_paths.dart
@@ -112,6 +112,11 @@ class ImagePaths {
String get icTeam => _getImagePath('ic_team.svg');
String get loginGraphic => _getImagePath('login_graphic.png');
String get icPowerByLinagora => _getImagePath('power_by_linagora.svg');
+ String get icAttachmentSB => _getImagePath('ic_attachment_sb.svg');
+ String get icCalendarSB => _getImagePath('ic_calendar_sb.svg');
+ String get icClockSB => _getImagePath('ic_clock_sb.svg');
+ String get icSelectedSB => _getImagePath('ic_selected_sb.svg');
+ String get icUserSB => _getImagePath('ic_user_sb.svg');
String _getImagePath(String imageName) {
return AssetsPaths.images + imageName;
diff --git a/core/lib/presentation/views/typeahead/typeahead_for_quick_search.dart b/core/lib/presentation/views/typeahead/typeahead_for_quick_search.dart
new file mode 100644
index 000000000..9f712c50e
--- /dev/null
+++ b/core/lib/presentation/views/typeahead/typeahead_for_quick_search.dart
@@ -0,0 +1,1847 @@
+/// # Flutter TypeAhead
+/// A TypeAhead widget for Flutter, where you can show suggestions to
+/// users as they type
+import 'dart:async';
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
+import 'package:pointer_interceptor/pointer_interceptor.dart';
+
+import 'typedef.dart';
+import 'utils.dart';
+
+/// A [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html)
+/// implementation of [TypeAheadFieldQuickSearch], that allows the value to be saved,
+/// validated, etc.
+///
+/// See also:
+///
+/// * [TypeAheadFieldQuickSearch], A [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
+/// that displays a list of suggestions as the user types
+class TypeAheadFormFieldQuickSearch extends FormField {
+ /// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
+ /// that the TypeAhead widget displays
+ final QuickSearchTextFieldConfiguration textFieldConfiguration;
+
+ /// Creates a [TypeAheadFormFieldQuickSearch]
+ TypeAheadFormFieldQuickSearch(
+ {Key? key,
+ String? initialValue,
+ bool getImmediateSuggestions: false,
+ @Deprecated('Use autovalidateMode parameter which provides more specific '
+ 'behavior related to auto validation. '
+ 'This feature was deprecated after Flutter v1.19.0.')
+ bool autovalidate: false,
+ bool enabled: true,
+ AutovalidateMode autovalidateMode: AutovalidateMode.disabled,
+ FormFieldSetter? onSaved,
+ FormFieldValidator? validator,
+ ErrorBuilder? errorBuilder,
+ WidgetBuilder? noItemsFoundBuilder,
+ WidgetBuilder? loadingBuilder,
+ Duration debounceDuration: const Duration(milliseconds: 300),
+ QuickSearchSuggestionsBoxDecoration suggestionsBoxDecoration:
+ const QuickSearchSuggestionsBoxDecoration(),
+ QuickSearchSuggestionsBoxController? suggestionsBoxController,
+ required SuggestionSelectionCallback onSuggestionSelected,
+ required ItemBuilder itemBuilder,
+ required SuggestionsCallback suggestionsCallback,
+ double suggestionsBoxVerticalOffset: 5.0,
+ this.textFieldConfiguration: const QuickSearchTextFieldConfiguration(),
+ AnimationTransitionBuilder? transitionBuilder,
+ Duration animationDuration: const Duration(milliseconds: 500),
+ double animationStart: 0.25,
+ AxisDirection direction: AxisDirection.down,
+ bool hideOnLoading: false,
+ bool hideOnEmpty: false,
+ bool hideOnError: false,
+ bool hideSuggestionsOnKeyboardHide: true,
+ bool keepSuggestionsOnLoading: true,
+ bool keepSuggestionsOnSuggestionSelected: false,
+ bool autoFlipDirection: false,
+ bool hideKeyboard: false,
+ int minCharsForSuggestions: 0,
+ List? listActionButton,
+ ButtonActionBuilder? actionButtonBuilder,
+ ButtonActionCallback? buttonActionCallback,
+ ButtonActionBuilder? buttonShowAllResult,
+ Widget? titleHeaderRecent,
+ ItemBuilder? itemRecentBuilder,
+ FetchRecentActionCallback? fetchRecentActionCallback,
+ RecentSelectionCallback? onRecentSelected,
+ EdgeInsets? listActionPadding,
+ bool hideSuggestionsBox: false,
+ }) : assert(
+ initialValue == null || textFieldConfiguration.controller == null),
+ assert(minCharsForSuggestions >= 0),
+ super(
+ key: key,
+ onSaved: onSaved,
+ validator: validator,
+ initialValue: textFieldConfiguration.controller != null
+ ? textFieldConfiguration.controller!.text
+ : (initialValue ?? ''),
+ enabled: enabled,
+ autovalidateMode: autovalidateMode,
+ builder: (FormFieldState field) {
+ final _TypeAheadFormFieldState state =
+ field as _TypeAheadFormFieldState;
+
+ return TypeAheadFieldQuickSearch(
+ getImmediateSuggestions: getImmediateSuggestions,
+ transitionBuilder: transitionBuilder,
+ errorBuilder: errorBuilder,
+ noItemsFoundBuilder: noItemsFoundBuilder,
+ loadingBuilder: loadingBuilder,
+ debounceDuration: debounceDuration,
+ suggestionsBoxDecoration: suggestionsBoxDecoration,
+ suggestionsBoxController: suggestionsBoxController,
+ textFieldConfiguration: textFieldConfiguration.copyWith(
+ decoration: textFieldConfiguration.decoration
+ .copyWith(errorText: state.errorText),
+ onChanged: (text) {
+ state.didChange(text);
+ textFieldConfiguration.onChanged?.call(text);
+ },
+ controller: state._effectiveController,
+ ),
+ suggestionsBoxVerticalOffset: suggestionsBoxVerticalOffset,
+ onSuggestionSelected: onSuggestionSelected,
+ itemBuilder: itemBuilder,
+ suggestionsCallback: suggestionsCallback,
+ animationStart: animationStart,
+ animationDuration: animationDuration,
+ direction: direction,
+ hideOnLoading: hideOnLoading,
+ hideOnEmpty: hideOnEmpty,
+ hideOnError: hideOnError,
+ hideSuggestionsOnKeyboardHide: hideSuggestionsOnKeyboardHide,
+ keepSuggestionsOnLoading: keepSuggestionsOnLoading,
+ keepSuggestionsOnSuggestionSelected:
+ keepSuggestionsOnSuggestionSelected,
+ autoFlipDirection: autoFlipDirection,
+ hideKeyboard: hideKeyboard,
+ minCharsForSuggestions: minCharsForSuggestions,
+ listActionButton: listActionButton,
+ actionButtonBuilder: actionButtonBuilder,
+ buttonActionCallback: buttonActionCallback,
+ buttonShowAllResult: buttonShowAllResult,
+ titleHeaderRecent: titleHeaderRecent,
+ itemRecentBuilder: itemRecentBuilder,
+ fetchRecentActionCallback: fetchRecentActionCallback,
+ onRecentSelected: onRecentSelected,
+ listActionPadding: listActionPadding,
+ hideSuggestionsBox: hideSuggestionsBox,
+ );
+ });
+
+ @override
+ _TypeAheadFormFieldState createState() => _TypeAheadFormFieldState();
+}
+
+class _TypeAheadFormFieldState extends FormFieldState {
+ TextEditingController? _controller;
+
+ TextEditingController? get _effectiveController =>
+ widget.textFieldConfiguration.controller ?? _controller;
+
+ @override
+ TypeAheadFormFieldQuickSearch get widget => super.widget as TypeAheadFormFieldQuickSearch;
+
+ @override
+ void initState() {
+ super.initState();
+ if (widget.textFieldConfiguration.controller == null) {
+ _controller = TextEditingController(text: widget.initialValue);
+ } else {
+ widget.textFieldConfiguration.controller!
+ .addListener(_handleControllerChanged);
+ }
+ }
+
+ @override
+ void didUpdateWidget(TypeAheadFormFieldQuickSearch oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.textFieldConfiguration.controller !=
+ oldWidget.textFieldConfiguration.controller) {
+ oldWidget.textFieldConfiguration.controller
+ ?.removeListener(_handleControllerChanged);
+ widget.textFieldConfiguration.controller
+ ?.addListener(_handleControllerChanged);
+
+ if (oldWidget.textFieldConfiguration.controller != null &&
+ widget.textFieldConfiguration.controller == null)
+ _controller = TextEditingController.fromValue(
+ oldWidget.textFieldConfiguration.controller!.value);
+ if (widget.textFieldConfiguration.controller != null) {
+ setValue(widget.textFieldConfiguration.controller!.text);
+ if (oldWidget.textFieldConfiguration.controller == null)
+ _controller = null;
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ widget.textFieldConfiguration.controller
+ ?.removeListener(_handleControllerChanged);
+ super.dispose();
+ }
+
+ @override
+ void reset() {
+ super.reset();
+ setState(() {
+ _effectiveController!.text = widget.initialValue!;
+ });
+ }
+
+ void _handleControllerChanged() {
+ // Suppress changes that originated from within this class.
+ //
+ // In the case where a controller has been passed in to this widget, we
+ // register this change listener. In these cases, we'll also receive change
+ // notifications for changes originating from within this class -- for
+ // example, the reset() method. In such cases, the FormField value will
+ // already have been set.
+ if (_effectiveController!.text != value)
+ didChange(_effectiveController!.text);
+ }
+}
+
+/// A [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
+/// that displays a list of suggestions as the user types
+///
+/// See also:
+///
+/// * [TypeAheadFormFieldQuickSearch], a [FormField](https://docs.flutter.io/flutter/widgets/FormField-class.html)
+/// implementation of [TypeAheadFieldQuickSearch] that allows the value to be saved,
+/// validated, etc.
+class TypeAheadFieldQuickSearch extends StatefulWidget {
+ /// Called with the search pattern to get the search suggestions.
+ ///
+ /// This callback must not be null. It is be called by the TypeAhead widget
+ /// and provided with the search pattern. It should return a [List](https://api.dartlang.org/stable/2.0.0/dart-core/List-class.html)
+ /// of suggestions either synchronously, or asynchronously (as the result of a
+ /// [Future](https://api.dartlang.org/stable/dart-async/Future-class.html)).
+ /// Typically, the list of suggestions should not contain more than 4 or 5
+ /// entries. These entries will then be provided to [itemBuilder] to display
+ /// the suggestions.
+ ///
+ /// Example:
+ /// ```dart
+ /// suggestionsCallback: (pattern) async {
+ /// return await _getSuggestions(pattern);
+ /// }
+ /// ```
+ final SuggestionsCallback suggestionsCallback;
+
+ /// Called when a suggestion is tapped.
+ ///
+ /// This callback must not be null. It is called by the TypeAhead widget and
+ /// provided with the value of the tapped suggestion.
+ ///
+ /// For example, you might want to navigate to a specific view when the user
+ /// tabs a suggestion:
+ /// ```dart
+ /// onSuggestionSelected: (suggestion) {
+ /// Navigator.of(context).push(MaterialPageRoute(
+ /// builder: (context) => SearchResult(
+ /// searchItem: suggestion
+ /// )
+ /// ));
+ /// }
+ /// ```
+ ///
+ /// Or to set the value of the text field:
+ /// ```dart
+ /// onSuggestionSelected: (suggestion) {
+ /// _controller.text = suggestion['name'];
+ /// }
+ /// ```
+ final SuggestionSelectionCallback onSuggestionSelected;
+
+ /// Called for each suggestion returned by [suggestionsCallback] to build the
+ /// corresponding widget.
+ ///
+ /// This callback must not be null. It is called by the TypeAhead widget for
+ /// each suggestion, and expected to build a widget to display this
+ /// suggestion's info. For example:
+ ///
+ /// ```dart
+ /// itemBuilder: (context, suggestion) {
+ /// return ListTile(
+ /// title: Text(suggestion['name']),
+ /// subtitle: Text('USD' + suggestion['price'].toString())
+ /// );
+ /// }
+ /// ```
+ final ItemBuilder itemBuilder;
+
+ /// used to control the scroll behavior of item-builder list
+ final ScrollController? scrollController;
+
+ /// The decoration of the material sheet that contains the suggestions.
+ ///
+ /// If null, default decoration with an elevation of 4.0 is used
+ ///
+
+ final QuickSearchSuggestionsBoxDecoration suggestionsBoxDecoration;
+
+ /// Used to control the `_SuggestionsBox`. Allows manual control to
+ /// open, close, toggle, or resize the `_SuggestionsBox`.
+ final QuickSearchSuggestionsBoxController? suggestionsBoxController;
+
+ /// The duration to wait after the user stops typing before calling
+ /// [suggestionsCallback]
+ ///
+ /// This is useful, because, if not set, a request for suggestions will be
+ /// sent for every character that the user types.
+ ///
+ /// This duration is set by default to 300 milliseconds
+ final Duration debounceDuration;
+
+ /// Called when waiting for [suggestionsCallback] to return.
+ ///
+ /// It is expected to return a widget to display while waiting.
+ /// For example:
+ /// ```dart
+ /// (BuildContext context) {
+ /// return Text('Loading...');
+ /// }
+ /// ```
+ ///
+ /// If not specified, a [CircularProgressIndicator](https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html) is shown
+ final WidgetBuilder? loadingBuilder;
+
+ /// Called when [suggestionsCallback] returns an empty array.
+ ///
+ /// It is expected to return a widget to display when no suggestions are
+ /// avaiable.
+ /// For example:
+ /// ```dart
+ /// (BuildContext context) {
+ /// return Text('No Items Found!');
+ /// }
+ /// ```
+ ///
+ /// If not specified, a simple text is shown
+ final WidgetBuilder? noItemsFoundBuilder;
+
+ /// Called when [suggestionsCallback] throws an exception.
+ ///
+ /// It is called with the error object, and expected to return a widget to
+ /// display when an exception is thrown
+ /// For example:
+ /// ```dart
+ /// (BuildContext context, error) {
+ /// return Text('$error');
+ /// }
+ /// ```
+ ///
+ /// If not specified, the error is shown in [ThemeData.errorColor](https://docs.flutter.io/flutter/material/ThemeData/errorColor.html)
+ final ErrorBuilder? errorBuilder;
+
+ /// Called to display animations when [suggestionsCallback] returns suggestions
+ ///
+ /// It is provided with the suggestions box instance and the animation
+ /// controller, and expected to return some animation that uses the controller
+ /// to display the suggestion box.
+ ///
+ /// For example:
+ /// ```dart
+ /// transitionBuilder: (context, suggestionsBox, animationController) {
+ /// return FadeTransition(
+ /// child: suggestionsBox,
+ /// opacity: CurvedAnimation(
+ /// parent: animationController,
+ /// curve: Curves.fastOutSlowIn
+ /// ),
+ /// );
+ /// }
+ /// ```
+ /// This argument is best used with [animationDuration] and [animationStart]
+ /// to fully control the animation.
+ ///
+ /// To fully remove the animation, just return `suggestionsBox`
+ ///
+ /// If not specified, a [SizeTransition](https://docs.flutter.io/flutter/widgets/SizeTransition-class.html) is shown.
+ final AnimationTransitionBuilder? transitionBuilder;
+
+ /// The duration that [transitionBuilder] animation takes.
+ ///
+ /// This argument is best used with [transitionBuilder] and [animationStart]
+ /// to fully control the animation.
+ ///
+ /// Defaults to 500 milliseconds.
+ final Duration animationDuration;
+
+ /// Determine the [SuggestionBox]'s direction.
+ ///
+ /// If [AxisDirection.down], the [SuggestionBox] will be below the [TextField]
+ /// and the [_SuggestionsList] will grow **down**.
+ ///
+ /// If [AxisDirection.up], the [SuggestionBox] will be above the [TextField]
+ /// and the [_SuggestionsList] will grow **up**.
+ ///
+ /// [AxisDirection.left] and [AxisDirection.right] are not allowed.
+ final AxisDirection direction;
+
+ /// The value at which the [transitionBuilder] animation starts.
+ ///
+ /// This argument is best used with [transitionBuilder] and [animationDuration]
+ /// to fully control the animation.
+ ///
+ /// Defaults to 0.25.
+ final double animationStart;
+
+ /// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
+ /// that the TypeAhead widget displays
+ final QuickSearchTextFieldConfiguration textFieldConfiguration;
+
+ /// How far below the text field should the suggestions box be
+ ///
+ /// Defaults to 5.0
+ final double suggestionsBoxVerticalOffset;
+
+ /// If set to true, suggestions will be fetched immediately when the field is
+ /// added to the view.
+ ///
+ /// But the suggestions box will only be shown when the field receives focus.
+ /// To make the field receive focus immediately, you can set the `autofocus`
+ /// property in the [textFieldConfiguration] to true
+ ///
+ /// Defaults to false
+ final bool getImmediateSuggestions;
+
+ /// If set to true, no loading box will be shown while suggestions are
+ /// being fetched. [loadingBuilder] will also be ignored.
+ ///
+ /// Defaults to false.
+ final bool hideOnLoading;
+
+ /// If set to true, nothing will be shown if there are no results.
+ /// [noItemsFoundBuilder] will also be ignored.
+ ///
+ /// Defaults to false.
+ final bool hideOnEmpty;
+
+ /// If set to true, nothing will be shown if there is an error.
+ /// [errorBuilder] will also be ignored.
+ ///
+ /// Defaults to false.
+ final bool hideOnError;
+
+ /// If set to false, the suggestions box will stay opened after
+ /// the keyboard is closed.
+ ///
+ /// Defaults to true.
+ final bool hideSuggestionsOnKeyboardHide;
+
+ /// If set to false, the suggestions box will show a circular
+ /// progress indicator when retrieving suggestions.
+ ///
+ /// Defaults to true.
+ final bool keepSuggestionsOnLoading;
+
+ /// If set to true, the suggestions box will remain opened even after
+ /// selecting a suggestion.
+ ///
+ /// Note that if this is enabled, the only way
+ /// to close the suggestions box is either manually via the
+ /// `QuickSearchSuggestionsBoxController` or when the user closes the software
+ /// keyboard if `hideSuggestionsOnKeyboardHide` is set to true. Users
+ /// with a physical keyboard will be unable to close the
+ /// box without a manual way via `QuickSearchSuggestionsBoxController`.
+ ///
+ /// Defaults to false.
+ final bool keepSuggestionsOnSuggestionSelected;
+
+ /// If set to true, in the case where the suggestions box has less than
+ /// _SuggestionsBoxController.minOverlaySpace to grow in the desired [direction], the direction axis
+ /// will be temporarily flipped if there's more room available in the opposite
+ /// direction.
+ ///
+ /// Defaults to false
+ final bool autoFlipDirection;
+ final bool hideKeyboard;
+
+ /// The minimum number of characters which must be entered before
+ /// [suggestionsCallback] is triggered.
+ ///
+ /// Defaults to 0.
+ final int minCharsForSuggestions;
+
+ /// List button action for suggestion box
+ final List? listActionButton;
+ final ButtonActionBuilder? actionButtonBuilder;
+ final ButtonActionCallback? buttonActionCallback;
+
+ /// Button show all result
+ final ButtonActionBuilder? buttonShowAllResult;
+
+ /// Title header recent suggestion box
+ final Widget? titleHeaderRecent;
+ /// Widget item recent
+ final ItemBuilder? itemRecentBuilder;
+ /// Get all recent callback
+ final FetchRecentActionCallback? fetchRecentActionCallback;
+ /// On listen select recent
+ final RecentSelectionCallback? onRecentSelected;
+ /// Padding button action
+ final EdgeInsets? listActionPadding;
+ final bool hideSuggestionsBox;
+
+ /// Creates a [TypeAheadFieldQuickSearch]
+ TypeAheadFieldQuickSearch(
+ {Key? key,
+ required this.suggestionsCallback,
+ required this.itemBuilder,
+ required this.onSuggestionSelected,
+ this.textFieldConfiguration: const QuickSearchTextFieldConfiguration(),
+ this.suggestionsBoxDecoration: const QuickSearchSuggestionsBoxDecoration(),
+ this.debounceDuration: const Duration(milliseconds: 300),
+ this.suggestionsBoxController,
+ this.scrollController,
+ this.loadingBuilder,
+ this.noItemsFoundBuilder,
+ this.errorBuilder,
+ this.transitionBuilder,
+ this.animationStart: 0.25,
+ this.animationDuration: const Duration(milliseconds: 500),
+ this.getImmediateSuggestions: false,
+ this.suggestionsBoxVerticalOffset: 5.0,
+ this.direction: AxisDirection.down,
+ this.hideOnLoading: false,
+ this.hideOnEmpty: false,
+ this.hideOnError: false,
+ this.hideSuggestionsOnKeyboardHide: true,
+ this.keepSuggestionsOnLoading: true,
+ this.keepSuggestionsOnSuggestionSelected: false,
+ this.autoFlipDirection: false,
+ this.hideKeyboard: false,
+ this.minCharsForSuggestions: 0,
+ this.listActionButton,
+ this.actionButtonBuilder,
+ this.buttonActionCallback,
+ this.buttonShowAllResult,
+ this.titleHeaderRecent,
+ this.itemRecentBuilder,
+ this.fetchRecentActionCallback,
+ this.onRecentSelected,
+ this.listActionPadding,
+ this.hideSuggestionsBox = false,
+ }) : assert(animationStart >= 0.0 && animationStart <= 1.0),
+ assert(
+ direction == AxisDirection.down || direction == AxisDirection.up),
+ assert(minCharsForSuggestions >= 0),
+ super(key: key);
+
+ @override
+ _TypeAheadFieldQuickSearchState createState() => _TypeAheadFieldQuickSearchState();
+}
+
+class _TypeAheadFieldQuickSearchState extends State>
+ with WidgetsBindingObserver {
+ FocusNode? _focusNode;
+ TextEditingController? _textEditingController;
+ _SuggestionsBox? _suggestionsBox;
+
+ TextEditingController? get _effectiveController =>
+ widget.textFieldConfiguration.controller ?? _textEditingController;
+ FocusNode? get _effectiveFocusNode =>
+ widget.textFieldConfiguration.focusNode ?? _focusNode;
+ late VoidCallback _focusNodeListener;
+
+ final LayerLink _layerLink = LayerLink();
+
+ // Timer that resizes the suggestion box on each tick. Only active when the user is scrolling.
+ Timer? _resizeOnScrollTimer;
+ // The rate at which the suggestion box will resize when the user is scrolling
+ final Duration _resizeOnScrollRefreshRate = const Duration(milliseconds: 500);
+ // Will have a value if the typeahead is inside a scrollable widget
+ ScrollPosition? _scrollPosition;
+
+ // Keyboard detection
+ final Stream? _keyboardVisibility =
+ (supportedPlatform) ? KeyboardVisibilityController().onChange : null;
+ late StreamSubscription? _keyboardVisibilitySubscription;
+
+ @override
+ void didChangeMetrics() {
+ // Catch keyboard event and orientation change; resize suggestions list
+ this._suggestionsBox!.onChangeMetrics();
+ }
+
+ @override
+ void dispose() {
+ this._suggestionsBox!.close();
+ this._suggestionsBox!.widgetMounted = false;
+ WidgetsBinding.instance?.removeObserver(this);
+ _keyboardVisibilitySubscription?.cancel();
+ _effectiveFocusNode!.removeListener(_focusNodeListener);
+ _focusNode?.dispose();
+ _resizeOnScrollTimer?.cancel();
+ _scrollPosition?.removeListener(_scrollResizeListener);
+ _textEditingController?.dispose();
+ super.dispose();
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance?.addObserver(this);
+
+ if (widget.textFieldConfiguration.controller == null) {
+ this._textEditingController = TextEditingController();
+ }
+
+ if (widget.textFieldConfiguration.focusNode == null) {
+ this._focusNode = FocusNode();
+ }
+
+ this._suggestionsBox = _SuggestionsBox(
+ context,
+ widget.direction,
+ widget.autoFlipDirection,
+ widget.hideSuggestionsBox);
+ widget.suggestionsBoxController?._suggestionsBox = this._suggestionsBox;
+ widget.suggestionsBoxController?._effectiveFocusNode =
+ this._effectiveFocusNode;
+
+ this._focusNodeListener = () {
+ if (_effectiveFocusNode!.hasFocus) {
+ this._suggestionsBox!.open();
+ } else {
+ if (widget.hideSuggestionsOnKeyboardHide){
+ this._suggestionsBox!.close();
+ }
+ }
+ };
+
+ this._effectiveFocusNode!.addListener(_focusNodeListener);
+
+ // hide suggestions box on keyboard closed
+ this._keyboardVisibilitySubscription =
+ _keyboardVisibility?.listen((bool isVisible) {
+ if (widget.hideSuggestionsOnKeyboardHide && !isVisible) {
+ _effectiveFocusNode!.unfocus();
+ }
+ });
+
+ WidgetsBinding.instance?.addPostFrameCallback((duration) {
+ if (mounted) {
+ this._initOverlayEntry();
+ // calculate initial suggestions list size
+ this._suggestionsBox!.resize();
+
+ // in case we already missed the focus event
+ if (this._effectiveFocusNode!.hasFocus) {
+ this._suggestionsBox!.open();
+ }
+ }
+ });
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ ScrollableState? scrollableState = Scrollable.of(context);
+ if (scrollableState != null) {
+ // The TypeAheadFieldQuickSearch is inside a scrollable widget
+ _scrollPosition = scrollableState.position;
+
+ _scrollPosition!.removeListener(_scrollResizeListener);
+ _scrollPosition!.isScrollingNotifier.addListener(_scrollResizeListener);
+ }
+ }
+
+ void _scrollResizeListener() {
+ bool isScrolling = _scrollPosition!.isScrollingNotifier.value;
+ _resizeOnScrollTimer?.cancel();
+ if (isScrolling) {
+ // Scroll started
+ _resizeOnScrollTimer =
+ Timer.periodic(_resizeOnScrollRefreshRate, (timer) {
+ _suggestionsBox!.resize();
+ });
+ } else {
+ // Scroll finished
+ _suggestionsBox!.resize();
+ }
+ }
+
+ void _initOverlayEntry() {
+ this._suggestionsBox!._overlayEntry = OverlayEntry(builder: (context) {
+ final suggestionsList = _SuggestionsList(
+ suggestionsBox: _suggestionsBox,
+ decoration: widget.suggestionsBoxDecoration,
+ debounceDuration: widget.debounceDuration,
+ controller: this._effectiveController,
+ loadingBuilder: widget.loadingBuilder,
+ scrollController: widget.scrollController,
+ noItemsFoundBuilder: widget.noItemsFoundBuilder,
+ errorBuilder: widget.errorBuilder,
+ transitionBuilder: widget.transitionBuilder,
+ suggestionsCallback: widget.suggestionsCallback,
+ animationDuration: widget.animationDuration,
+ animationStart: widget.animationStart,
+ getImmediateSuggestions: widget.getImmediateSuggestions,
+ onSuggestionSelected: (T selection) {
+ if (!widget.keepSuggestionsOnSuggestionSelected) {
+ this._effectiveFocusNode!.unfocus();
+ this._suggestionsBox!.close();
+ }
+ widget.onSuggestionSelected(selection);
+ },
+ itemBuilder: widget.itemBuilder,
+ direction: _suggestionsBox!.direction,
+ hideOnLoading: widget.hideOnLoading,
+ hideOnEmpty: widget.hideOnEmpty,
+ hideOnError: widget.hideOnError,
+ keepSuggestionsOnLoading: widget.keepSuggestionsOnLoading,
+ minCharsForSuggestions: widget.minCharsForSuggestions,
+ listActionButton: widget.listActionButton,
+ actionButtonBuilder: widget.actionButtonBuilder,
+ buttonActionCallback: widget.buttonActionCallback,
+ buttonShowAllResult: widget.buttonShowAllResult,
+ titleHeaderRecent: widget.titleHeaderRecent,
+ itemRecentBuilder: widget.itemRecentBuilder,
+ fetchRecentActionCallback: widget.fetchRecentActionCallback,
+ onRecentSelected: (R selection) {
+ if (!widget.keepSuggestionsOnSuggestionSelected) {
+ this._effectiveFocusNode!.unfocus();
+ this._suggestionsBox!.close();
+ }
+ if (widget.onRecentSelected != null) {
+ widget.onRecentSelected!(selection);
+ }
+ },
+ listActionPadding: widget.listActionPadding,
+ hideSuggestionsBox: widget.hideSuggestionsBox,
+ );
+
+ double w = _suggestionsBox!.textBoxWidth;
+ if (widget.suggestionsBoxDecoration.constraints != null) {
+ if (widget.suggestionsBoxDecoration.constraints!.minWidth != 0.0 &&
+ widget.suggestionsBoxDecoration.constraints!.maxWidth !=
+ double.infinity) {
+ w = (widget.suggestionsBoxDecoration.constraints!.minWidth +
+ widget.suggestionsBoxDecoration.constraints!.maxWidth) /
+ 2;
+ } else if (widget.suggestionsBoxDecoration.constraints!.minWidth !=
+ 0.0 &&
+ widget.suggestionsBoxDecoration.constraints!.minWidth > w) {
+ w = widget.suggestionsBoxDecoration.constraints!.minWidth;
+ } else if (widget.suggestionsBoxDecoration.constraints!.maxWidth !=
+ double.infinity &&
+ widget.suggestionsBoxDecoration.constraints!.maxWidth < w) {
+ w = widget.suggestionsBoxDecoration.constraints!.maxWidth;
+ }
+ }
+
+ return Positioned(
+ width: w,
+ child: CompositedTransformFollower(
+ link: this._layerLink,
+ showWhenUnlinked: false,
+ offset: Offset(
+ widget.suggestionsBoxDecoration.offsetX,
+ _suggestionsBox!.direction == AxisDirection.down
+ ? _suggestionsBox!.textBoxHeight +
+ widget.suggestionsBoxVerticalOffset
+ : _suggestionsBox!.directionUpOffset),
+ child: _suggestionsBox!.direction == AxisDirection.down
+ ? suggestionsList
+ : FractionalTranslation(
+ translation:
+ Offset(0.0, -1.0), // visually flips list to go up
+ child: suggestionsList,
+ ),
+ ),
+ );
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return CompositedTransformTarget(
+ link: this._layerLink,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (widget.textFieldConfiguration.leftButton != null)
+ widget.textFieldConfiguration.leftButton!,
+ Expanded(
+ child: TextField(
+ focusNode: this._effectiveFocusNode,
+ controller: this._effectiveController,
+ decoration: widget.textFieldConfiguration.decoration,
+ style: widget.textFieldConfiguration.style,
+ textAlign: widget.textFieldConfiguration.textAlign,
+ enabled: widget.textFieldConfiguration.enabled,
+ keyboardType: widget.textFieldConfiguration.keyboardType,
+ autofocus: widget.textFieldConfiguration.autofocus,
+ inputFormatters: widget.textFieldConfiguration.inputFormatters,
+ autocorrect: widget.textFieldConfiguration.autocorrect,
+ maxLines: widget.textFieldConfiguration.maxLines,
+ textAlignVertical: widget.textFieldConfiguration.textAlignVertical,
+ minLines: widget.textFieldConfiguration.minLines,
+ maxLength: widget.textFieldConfiguration.maxLength,
+ maxLengthEnforcement: widget.textFieldConfiguration.maxLengthEnforcement,
+ obscureText: widget.textFieldConfiguration.obscureText,
+ onChanged: widget.textFieldConfiguration.onChanged,
+ onSubmitted: widget.textFieldConfiguration.onSubmitted,
+ onEditingComplete: widget.textFieldConfiguration.onEditingComplete,
+ onTap: widget.textFieldConfiguration.onTap,
+ scrollPadding: widget.textFieldConfiguration.scrollPadding,
+ textInputAction: widget.textFieldConfiguration.textInputAction,
+ textCapitalization: widget.textFieldConfiguration.textCapitalization,
+ keyboardAppearance: widget.textFieldConfiguration.keyboardAppearance,
+ cursorWidth: widget.textFieldConfiguration.cursorWidth,
+ cursorRadius: widget.textFieldConfiguration.cursorRadius,
+ cursorColor: widget.textFieldConfiguration.cursorColor,
+ textDirection: widget.textFieldConfiguration.textDirection,
+ enableInteractiveSelection:
+ widget.textFieldConfiguration.enableInteractiveSelection,
+ readOnly: widget.hideKeyboard,
+ ),
+ ),
+ if (widget.textFieldConfiguration.clearTextButton != null
+ && this._effectiveController?.text.isNotEmpty == true)
+ widget.textFieldConfiguration.clearTextButton!,
+ if (widget.textFieldConfiguration.rightButton != null)
+ widget.textFieldConfiguration.rightButton!,
+ ],
+ ),
+ );
+ }
+}
+
+class _SuggestionsList extends StatefulWidget {
+ final _SuggestionsBox? suggestionsBox;
+ final TextEditingController? controller;
+ final bool getImmediateSuggestions;
+ final SuggestionSelectionCallback? onSuggestionSelected;
+ final SuggestionsCallback? suggestionsCallback;
+ final ItemBuilder? itemBuilder;
+ final ScrollController? scrollController;
+ final QuickSearchSuggestionsBoxDecoration? decoration;
+ final Duration? debounceDuration;
+ final WidgetBuilder? loadingBuilder;
+ final WidgetBuilder? noItemsFoundBuilder;
+ final ErrorBuilder? errorBuilder;
+ final AnimationTransitionBuilder? transitionBuilder;
+ final Duration? animationDuration;
+ final double? animationStart;
+ final AxisDirection? direction;
+ final bool? hideOnLoading;
+ final bool? hideOnEmpty;
+ final bool? hideOnError;
+ final bool? keepSuggestionsOnLoading;
+ final int? minCharsForSuggestions;
+ final List? listActionButton;
+ final ButtonActionBuilder? actionButtonBuilder;
+ final ButtonActionCallback? buttonActionCallback;
+ final ButtonActionBuilder? buttonShowAllResult;
+ final Widget? titleHeaderRecent;
+ final ItemBuilder? itemRecentBuilder;
+ final FetchRecentActionCallback? fetchRecentActionCallback;
+ final RecentSelectionCallback? onRecentSelected;
+ final EdgeInsets? listActionPadding;
+ final bool hideSuggestionsBox;
+
+ _SuggestionsList({
+ required this.suggestionsBox,
+ this.controller,
+ this.getImmediateSuggestions: false,
+ this.onSuggestionSelected,
+ this.suggestionsCallback,
+ this.itemBuilder,
+ this.scrollController,
+ this.decoration,
+ this.debounceDuration,
+ this.loadingBuilder,
+ this.noItemsFoundBuilder,
+ this.errorBuilder,
+ this.transitionBuilder,
+ this.animationDuration,
+ this.animationStart,
+ this.direction,
+ this.hideOnLoading,
+ this.hideOnEmpty,
+ this.hideOnError,
+ this.keepSuggestionsOnLoading,
+ this.minCharsForSuggestions,
+ this.listActionButton,
+ this.actionButtonBuilder,
+ this.buttonActionCallback,
+ this.buttonShowAllResult,
+ this.titleHeaderRecent,
+ this.itemRecentBuilder,
+ this.fetchRecentActionCallback,
+ this.onRecentSelected,
+ this.listActionPadding,
+ this.hideSuggestionsBox: false,
+ });
+
+ @override
+ _SuggestionsListState createState() => _SuggestionsListState();
+}
+
+class _SuggestionsListState extends State<_SuggestionsList>
+ with SingleTickerProviderStateMixin {
+ Iterable? _suggestions;
+ Iterable? _recentItems;
+ late bool _suggestionsValid;
+ late VoidCallback _controllerListener;
+ Timer? _debounceTimer;
+ bool? _isLoading, _isQueued;
+ AnimationController? _animationController;
+ String? _lastTextValue;
+ late final ScrollController _scrollController =
+ widget.scrollController ?? ScrollController();
+
+ _SuggestionsListState() {
+ this._controllerListener = () async {
+ // If we came here because of a change in selected text, not because of
+ // actual change in text
+ if (widget.controller!.text == this._lastTextValue) return;
+
+ this._lastTextValue = widget.controller!.text;
+
+ this._debounceTimer?.cancel();
+ if (widget.controller!.text.length <= widget.minCharsForSuggestions!) {
+ if (mounted) {
+ Iterable? recentItems;
+ try {
+ if (widget.fetchRecentActionCallback != null) {
+ recentItems = await widget.fetchRecentActionCallback!(widget.controller!.text);
+ }
+ } catch (e) {}
+
+ setState(() {
+ _isLoading = false;
+ _suggestions = null;
+ _recentItems = recentItems;
+ _suggestionsValid = true;
+ });
+ }
+ return;
+ } else {
+ this._debounceTimer = Timer(widget.debounceDuration!, () async {
+ if (this._debounceTimer!.isActive) return;
+ if (_isLoading!) {
+ _isQueued = true;
+ return;
+ }
+
+ await this.invalidateSuggestions();
+ while (_isQueued!) {
+ _isQueued = false;
+ await this.invalidateSuggestions();
+ }
+ });
+ }
+ };
+ }
+
+ @override
+ void didUpdateWidget(_SuggestionsList oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ widget.controller!.addListener(this._controllerListener);
+ _getSuggestions();
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ _getSuggestions();
+ }
+
+ @override
+ void initState() {
+ super.initState();
+
+ this._animationController = AnimationController(
+ vsync: this,
+ duration: widget.animationDuration,
+ );
+
+ this._suggestionsValid = widget.minCharsForSuggestions! > 0 ? true : false;
+ this._isLoading = false;
+ this._isQueued = false;
+ this._lastTextValue = widget.controller!.text;
+
+ if (widget.getImmediateSuggestions) {
+ this._getSuggestions();
+ }
+
+ widget.controller!.addListener(this._controllerListener);
+ }
+
+ Future invalidateSuggestions() async {
+ if (widget.hideSuggestionsBox) return;
+ _suggestionsValid = false;
+ await _getSuggestions();
+ }
+
+ Future _getSuggestions() async {
+ if (_suggestionsValid || widget.hideSuggestionsBox) return;
+ _suggestionsValid = true;
+
+ if (mounted) {
+ setState(() {
+ this._animationController!.forward(from: 1.0);
+
+ this._isLoading = true;
+ });
+
+ Iterable? suggestions;
+ Iterable? recentItems;
+ Object? error;
+
+ try {
+ suggestions = await widget.suggestionsCallback!(widget.controller!.text);
+ if (widget.fetchRecentActionCallback != null) {
+ recentItems = await widget.fetchRecentActionCallback!(widget.controller!.text);
+ }
+ } catch (e) {
+ error = e;
+ }
+
+ if (this.mounted) {
+ // if it wasn't removed in the meantime
+ setState(() {
+ double? animationStart = widget.animationStart;
+ // allow suggestionsCallback to return null and not throw error here
+ if (error != null || suggestions?.isEmpty == true) {
+ animationStart = 1.0;
+ }
+ this._animationController!.forward(from: animationStart);
+
+ this._isLoading = false;
+ this._suggestions = suggestions;
+ this._recentItems = recentItems;
+ });
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ _animationController!.dispose();
+ _debounceTimer?.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (widget.hideSuggestionsBox) {
+ return SizedBox.shrink();
+ }
+
+ Widget child;
+
+ if (this._suggestions?.isNotEmpty == true && widget.controller?.text.isNotEmpty == true) {
+ child = createSuggestionsWidget();
+ } else {
+ child = createRecentWidget();
+ }
+
+ final animationChild = widget.transitionBuilder != null
+ ? widget.transitionBuilder!(context, child, this._animationController)
+ : SizeTransition(
+ axisAlignment: -1.0,
+ sizeFactor: CurvedAnimation(
+ parent: this._animationController!,
+ curve: Curves.fastOutSlowIn),
+ child: child,
+ );
+
+ BoxConstraints constraints;
+ if (widget.decoration!.constraints == null) {
+ constraints = BoxConstraints(
+ maxHeight: widget.suggestionsBox!.maxHeight,
+ );
+ } else {
+ double maxHeight = min(widget.decoration!.constraints!.maxHeight,
+ widget.suggestionsBox!.maxHeight);
+ constraints = widget.decoration!.constraints!.copyWith(
+ minHeight: min(widget.decoration!.constraints!.minHeight, maxHeight),
+ maxHeight: maxHeight,
+ );
+ }
+
+ var container = Material(
+ elevation: widget.decoration!.elevation,
+ color: widget.decoration!.color,
+ shape: widget.decoration!.shape,
+ borderRadius: widget.decoration!.borderRadius,
+ shadowColor: widget.decoration!.shadowColor,
+ clipBehavior: widget.decoration!.clipBehavior,
+ child: ConstrainedBox(
+ constraints: constraints,
+ child: animationChild,
+ ),
+ );
+
+ return PointerInterceptor(child: container);
+ }
+
+ Widget createSuggestionsWidget() {
+ final listItemSuggestionWidget = this._suggestions?.map((T suggestion) {
+ if ( widget.itemBuilder != null) {
+ return InkWell(
+ child: widget.itemBuilder!(context, suggestion),
+ onTap: () {
+ widget.onSuggestionSelected!(suggestion);
+ },
+ );
+ } else {
+ return SizedBox.shrink();
+ }
+ }).toList() ?? [];
+
+ final loadingWidget = widget.loadingBuilder != null
+ ? widget.loadingBuilder!(context)
+ : Align(
+ alignment: Alignment.center,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: CircularProgressIndicator(),
+ ),
+ );
+
+ final listAction = Row(
+ mainAxisSize: MainAxisSize.min,
+ children: widget.listActionButton!.map((dynamic action) {
+ if (widget.actionButtonBuilder != null) {
+ return Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: InkWell(
+ child: widget.actionButtonBuilder!(context, action),
+ borderRadius: BorderRadius.all(Radius.circular(10)),
+ onTap: () {
+ if (widget.buttonActionCallback != null) {
+ widget.buttonActionCallback!(action);
+ this.invalidateSuggestions();
+ }
+ },
+ ),
+ );
+ } else {
+ return SizedBox.shrink();
+ }
+ }).toList());
+
+ Widget child = ListView(
+ padding: EdgeInsets.zero,
+ primary: false,
+ shrinkWrap: true,
+ controller: _scrollController,
+ reverse: widget.suggestionsBox!.direction == AxisDirection.down ? false : true, // reverses the list to start at the bottom
+ children: [
+ if (widget.listActionButton != null && widget.listActionButton?.isNotEmpty == true)
+ Padding(
+ padding: widget.listActionPadding ?? EdgeInsets.zero,
+ child: listAction,
+ ),
+ if (this._isLoading == true && widget.hideOnLoading == false && widget.keepSuggestionsOnLoading == false)
+ loadingWidget,
+ if (widget.buttonShowAllResult != null && widget.controller?.text.isNotEmpty == true)
+ widget.buttonShowAllResult!(context, widget.controller?.text),
+ if (listItemSuggestionWidget.isNotEmpty)
+ ... [
+ ... listItemSuggestionWidget,
+ SizedBox(height: 16)
+ ],
+ ],
+ );
+
+ if (widget.decoration!.hasScrollbar) {
+ child = Scrollbar(
+ controller: _scrollController,
+ child: child,
+ );
+ }
+
+ return child;
+ }
+
+ Widget createRecentWidget() {
+ final listItemRecent = this._recentItems?.map((R recent) {
+ if (widget.itemRecentBuilder != null) {
+ return InkWell(
+ child: widget.itemRecentBuilder!(context, recent),
+ onTap: () {
+ if (widget.onRecentSelected != null) {
+ widget.onRecentSelected!(recent);
+ }
+ },
+ );
+ } else {
+ return SizedBox.shrink();
+ }
+ }).toList() ?? [];
+
+ final loadingWidget = widget.loadingBuilder != null
+ ? widget.loadingBuilder!(context)
+ : Align(
+ alignment: Alignment.center,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: CircularProgressIndicator(),
+ ),
+ );
+
+ final listAction = Row(
+ mainAxisSize: MainAxisSize.min,
+ children: widget.listActionButton!.map((dynamic action) {
+ if (widget.actionButtonBuilder != null) {
+ return Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: InkWell(
+ child: widget.actionButtonBuilder!(context, action),
+ borderRadius: BorderRadius.all(Radius.circular(10)),
+ onTap: () {
+ if (widget.buttonActionCallback != null) {
+ widget.buttonActionCallback!(action);
+ this.invalidateSuggestions();
+ }
+ },
+ ),
+ );
+ } else {
+ return SizedBox.shrink();
+ }
+ }).toList());
+
+ Widget child = ListView(
+ padding: EdgeInsets.zero,
+ primary: false,
+ shrinkWrap: true,
+ controller: _scrollController,
+ reverse: widget.suggestionsBox!.direction == AxisDirection.down ? false : true, // reverses the list to start at the bottom
+ children: [
+ if (widget.listActionButton != null && widget.listActionButton?.isNotEmpty == true)
+ Padding(
+ padding: widget.listActionPadding ?? EdgeInsets.zero,
+ child: listAction,
+ ),
+ if (this._isLoading == true && widget.hideOnLoading == false && widget.keepSuggestionsOnLoading == false)
+ loadingWidget,
+ if (widget.buttonShowAllResult != null && widget.controller?.text.isNotEmpty == true)
+ widget.buttonShowAllResult!(context, widget.controller?.text),
+ if (_recentItems?.isNotEmpty == true && widget.itemRecentBuilder != null && widget.titleHeaderRecent != null)
+ widget.titleHeaderRecent!,
+ if (listItemRecent.isNotEmpty)
+ ... [
+ ... listItemRecent,
+ SizedBox(height: 16)
+ ],
+ ],
+ );
+
+ if (widget.decoration!.hasScrollbar) {
+ child = Scrollbar(
+ controller: _scrollController,
+ child: child,
+ );
+ }
+
+ return child;
+ }
+}
+
+/// Supply an instance of this class to the [TypeAhead.suggestionsBoxDecoration]
+/// property to configure the suggestions box decoration
+class QuickSearchSuggestionsBoxDecoration {
+ /// The z-coordinate at which to place the suggestions box. This controls the size
+ /// of the shadow below the box.
+ ///
+ /// Same as [Material.elevation](https://docs.flutter.io/flutter/material/Material/elevation.html)
+ final double elevation;
+
+ /// The color to paint the suggestions box.
+ ///
+ /// Same as [Material.color](https://docs.flutter.io/flutter/material/Material/color.html)
+ final Color? color;
+
+ /// Defines the material's shape as well its shadow.
+ ///
+ /// Same as [Material.shape](https://docs.flutter.io/flutter/material/Material/shape.html)
+ final ShapeBorder? shape;
+
+ /// Defines if a scrollbar will be displayed or not.
+ final bool hasScrollbar;
+
+ /// If non-null, the corners of this box are rounded by this [BorderRadius](https://docs.flutter.io/flutter/painting/BorderRadius-class.html).
+ ///
+ /// Same as [Material.borderRadius](https://docs.flutter.io/flutter/material/Material/borderRadius.html)
+ final BorderRadius? borderRadius;
+
+ /// The color to paint the shadow below the material.
+ ///
+ /// Same as [Material.shadowColor](https://docs.flutter.io/flutter/material/Material/shadowColor.html)
+ final Color shadowColor;
+
+ /// The constraints to be applied to the suggestions box
+ final BoxConstraints? constraints;
+
+ /// Adds an offset to the suggestions box
+ final double offsetX;
+
+ /// The content will be clipped (or not) according to this option.
+ ///
+ /// Same as [Material.clipBehavior](https://api.flutter.dev/flutter/material/Material/clipBehavior.html)
+ final Clip clipBehavior;
+
+ /// Creates a QuickSearchSuggestionsBoxDecoration
+ const QuickSearchSuggestionsBoxDecoration(
+ {this.elevation: 4.0,
+ this.color,
+ this.shape,
+ this.hasScrollbar: true,
+ this.borderRadius,
+ this.shadowColor: const Color(0xFF000000),
+ this.constraints,
+ this.clipBehavior: Clip.none,
+ this.offsetX: 0.0});
+}
+
+/// Supply an instance of this class to the [TypeAhead.textFieldConfiguration]
+/// property to configure the displayed text field
+class QuickSearchTextFieldConfiguration {
+ /// The decoration to show around the text field.
+ ///
+ /// Same as [TextField.decoration](https://docs.flutter.io/flutter/material/TextField/decoration.html)
+ final InputDecoration decoration;
+
+ /// Controls the text being edited.
+ ///
+ /// If null, this widget will create its own [TextEditingController](https://docs.flutter.io/flutter/widgets/TextEditingController-class.html).
+ /// A typical use case for this field in the TypeAhead widget is to set the
+ /// text of the widget when a suggestion is selected. For example:
+ ///
+ /// ```dart
+ /// final _controller = TextEditingController();
+ /// ...
+ /// ...
+ /// TypeAheadFieldQuickSearch(
+ /// controller: _controller,
+ /// ...
+ /// ...
+ /// onSuggestionSelected: (suggestion) {
+ /// _controller.text = suggestion['city_name'];
+ /// }
+ /// )
+ /// ```
+ final TextEditingController? controller;
+
+ /// Controls whether this widget has keyboard focus.
+ ///
+ /// Same as [TextField.focusNode](https://docs.flutter.io/flutter/material/TextField/focusNode.html)
+ final FocusNode? focusNode;
+
+ /// The style to use for the text being edited.
+ ///
+ /// Same as [TextField.style](https://docs.flutter.io/flutter/material/TextField/style.html)
+ final TextStyle? style;
+
+ /// How the text being edited should be aligned horizontally.
+ ///
+ /// Same as [TextField.textAlign](https://docs.flutter.io/flutter/material/TextField/textAlign.html)
+ final TextAlign textAlign;
+
+ /// Same as [TextField.textDirection](https://docs.flutter.io/flutter/material/TextField/textDirection.html)
+ ///
+ /// Defaults to null
+ final TextDirection? textDirection;
+
+ /// Same as [TextField.textAlignVertical](https://api.flutter.dev/flutter/material/TextField/textAlignVertical.html)
+ final TextAlignVertical? textAlignVertical;
+
+ /// If false the textfield is "disabled": it ignores taps and its
+ /// [decoration] is rendered in grey.
+ ///
+ /// Same as [TextField.enabled](https://docs.flutter.io/flutter/material/TextField/enabled.html)
+ final bool enabled;
+
+ /// Whether to show input suggestions as the user types.
+ ///
+ /// Same as [TextField.enableSuggestions](https://api.flutter.dev/flutter/material/TextField/enableSuggestions.html)
+ final bool enableSuggestions;
+
+ /// The type of keyboard to use for editing the text.
+ ///
+ /// Same as [TextField.keyboardType](https://docs.flutter.io/flutter/material/TextField/keyboardType.html)
+ final TextInputType keyboardType;
+
+ /// Whether this text field should focus itself if nothing else is already
+ /// focused.
+ ///
+ /// Same as [TextField.autofocus](https://docs.flutter.io/flutter/material/TextField/autofocus.html)
+ final bool autofocus;
+
+ /// Optional input validation and formatting overrides.
+ ///
+ /// Same as [TextField.inputFormatters](https://docs.flutter.io/flutter/material/TextField/inputFormatters.html)
+ final List? inputFormatters;
+
+ /// Whether to enable autocorrection.
+ ///
+ /// Same as [TextField.autocorrect](https://docs.flutter.io/flutter/material/TextField/autocorrect.html)
+ final bool autocorrect;
+
+ /// The maximum number of lines for the text to span, wrapping if necessary.
+ ///
+ /// Same as [TextField.maxLines](https://docs.flutter.io/flutter/material/TextField/maxLines.html)
+ final int? maxLines;
+
+ /// The minimum number of lines to occupy when the content spans fewer lines.
+ ///
+ /// Same as [TextField.minLines](https://docs.flutter.io/flutter/material/TextField/minLines.html)
+ final int? minLines;
+
+ /// The maximum number of characters (Unicode scalar values) to allow in the
+ /// text field.
+ ///
+ /// Same as [TextField.maxLength](https://docs.flutter.io/flutter/material/TextField/maxLength.html)
+ final int? maxLength;
+
+ /// If true, prevents the field from allowing more than [maxLength]
+ /// characters.
+ ///
+ /// Same as [TextField.maxLengthEnforcement](https://api.flutter.dev/flutter/material/TextField/maxLengthEnforcement.html)
+ final MaxLengthEnforcement? maxLengthEnforcement;
+
+ /// Whether to hide the text being edited (e.g., for passwords).
+ ///
+ /// Same as [TextField.obscureText](https://docs.flutter.io/flutter/material/TextField/obscureText.html)
+ final bool obscureText;
+
+ /// Called when the text being edited changes.
+ ///
+ /// Same as [TextField.onChanged](https://docs.flutter.io/flutter/material/TextField/onChanged.html)
+ final ValueChanged? onChanged;
+
+ /// Called when the user indicates that they are done editing the text in the
+ /// field.
+ ///
+ /// Same as [TextField.onSubmitted](https://docs.flutter.io/flutter/material/TextField/onSubmitted.html)
+ final ValueChanged? onSubmitted;
+
+ /// The color to use when painting the cursor.
+ ///
+ /// Same as [TextField.cursorColor](https://docs.flutter.io/flutter/material/TextField/cursorColor.html)
+ final Color? cursorColor;
+
+ /// How rounded the corners of the cursor should be. By default, the cursor has a null Radius
+ ///
+ /// Same as [TextField.cursorRadius](https://docs.flutter.io/flutter/material/TextField/cursorRadius.html)
+ final Radius? cursorRadius;
+
+ /// How thick the cursor will be.
+ ///
+ /// Same as [TextField.cursorWidth](https://docs.flutter.io/flutter/material/TextField/cursorWidth.html)
+ final double cursorWidth;
+
+ /// The appearance of the keyboard.
+ ///
+ /// Same as [TextField.keyboardAppearance](https://docs.flutter.io/flutter/material/TextField/keyboardAppearance.html)
+ final Brightness? keyboardAppearance;
+
+ /// Called when the user submits editable content (e.g., user presses the "done" button on the keyboard).
+ ///
+ /// Same as [TextField.onEditingComplete](https://docs.flutter.io/flutter/material/TextField/onEditingComplete.html)
+ final VoidCallback? onEditingComplete;
+
+ /// Called for each distinct tap except for every second tap of a double tap.
+ ///
+ /// Same as [TextField.onTap](https://docs.flutter.io/flutter/material/TextField/onTap.html)
+ final GestureTapCallback? onTap;
+
+ /// Configures padding to edges surrounding a Scrollable when the Textfield scrolls into view.
+ ///
+ /// Same as [TextField.scrollPadding](https://docs.flutter.io/flutter/material/TextField/scrollPadding.html)
+ final EdgeInsets scrollPadding;
+
+ /// Configures how the platform keyboard will select an uppercase or lowercase keyboard.
+ ///
+ /// Same as [TextField.TextCapitalization](https://docs.flutter.io/flutter/material/TextField/textCapitalization.html)
+ final TextCapitalization textCapitalization;
+
+ /// The type of action button to use for the keyboard.
+ ///
+ /// Same as [TextField.textInputAction](https://docs.flutter.io/flutter/material/TextField/textInputAction.html)
+ final TextInputAction? textInputAction;
+
+ final bool enableInteractiveSelection;
+
+ final Widget? leftButton, rightButton;
+ final Widget? clearTextButton;
+
+ /// Creates a QuickSearchTextFieldConfiguration
+ const QuickSearchTextFieldConfiguration({
+ this.decoration: const InputDecoration(),
+ this.style,
+ this.controller,
+ this.onChanged,
+ this.onSubmitted,
+ this.obscureText: false,
+ this.maxLengthEnforcement,
+ this.maxLength,
+ this.maxLines: 1,
+ this.minLines,
+ this.textAlignVertical,
+ this.autocorrect: true,
+ this.inputFormatters,
+ this.autofocus: false,
+ this.keyboardType: TextInputType.text,
+ this.enabled: true,
+ this.enableSuggestions: true,
+ this.textAlign: TextAlign.start,
+ this.focusNode,
+ this.cursorColor,
+ this.cursorRadius,
+ this.textInputAction,
+ this.textCapitalization: TextCapitalization.none,
+ this.cursorWidth: 2.0,
+ this.keyboardAppearance,
+ this.onEditingComplete,
+ this.onTap,
+ this.textDirection,
+ this.scrollPadding: const EdgeInsets.all(20.0),
+ this.enableInteractiveSelection: true,
+ this.leftButton,
+ this.rightButton,
+ this.clearTextButton,
+ });
+
+ /// Copies the [QuickSearchTextFieldConfiguration] and only changes the specified
+ /// properties
+ QuickSearchTextFieldConfiguration copyWith(
+ {InputDecoration? decoration,
+ TextStyle? style,
+ TextEditingController? controller,
+ ValueChanged? onChanged,
+ ValueChanged? onSubmitted,
+ bool? obscureText,
+ MaxLengthEnforcement? maxLengthEnforcement,
+ int? maxLength,
+ int? maxLines,
+ int? minLines,
+ bool? autocorrect,
+ List? inputFormatters,
+ bool? autofocus,
+ TextInputType? keyboardType,
+ bool? enabled,
+ bool? enableSuggestions,
+ TextAlign? textAlign,
+ FocusNode? focusNode,
+ Color? cursorColor,
+ TextAlignVertical? textAlignVertical,
+ Radius? cursorRadius,
+ double? cursorWidth,
+ Brightness? keyboardAppearance,
+ VoidCallback? onEditingComplete,
+ GestureTapCallback? onTap,
+ EdgeInsets? scrollPadding,
+ TextCapitalization? textCapitalization,
+ TextDirection? textDirection,
+ TextInputAction? textInputAction,
+ bool? enableInteractiveSelection,
+ Widget? leftButton,
+ Widget? rightButton,
+ Widget? clearTextButton}) {
+ return QuickSearchTextFieldConfiguration(
+ decoration: decoration ?? this.decoration,
+ style: style ?? this.style,
+ controller: controller ?? this.controller,
+ onChanged: onChanged ?? this.onChanged,
+ onSubmitted: onSubmitted ?? this.onSubmitted,
+ obscureText: obscureText ?? this.obscureText,
+ maxLengthEnforcement: maxLengthEnforcement ?? this.maxLengthEnforcement,
+ maxLength: maxLength ?? this.maxLength,
+ maxLines: maxLines ?? this.maxLines,
+ minLines: minLines ?? this.minLines,
+ autocorrect: autocorrect ?? this.autocorrect,
+ inputFormatters: inputFormatters ?? this.inputFormatters,
+ autofocus: autofocus ?? this.autofocus,
+ keyboardType: keyboardType ?? this.keyboardType,
+ enabled: enabled ?? this.enabled,
+ enableSuggestions: enableSuggestions ?? this.enableSuggestions,
+ textAlign: textAlign ?? this.textAlign,
+ textAlignVertical: textAlignVertical ?? this.textAlignVertical,
+ focusNode: focusNode ?? this.focusNode,
+ cursorColor: cursorColor ?? this.cursorColor,
+ cursorRadius: cursorRadius ?? this.cursorRadius,
+ cursorWidth: cursorWidth ?? this.cursorWidth,
+ keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
+ onEditingComplete: onEditingComplete ?? this.onEditingComplete,
+ onTap: onTap ?? this.onTap,
+ scrollPadding: scrollPadding ?? this.scrollPadding,
+ textCapitalization: textCapitalization ?? this.textCapitalization,
+ textInputAction: textInputAction ?? this.textInputAction,
+ textDirection: textDirection ?? this.textDirection,
+ enableInteractiveSelection:
+ enableInteractiveSelection ?? this.enableInteractiveSelection,
+ leftButton: leftButton ?? this.leftButton,
+ rightButton: rightButton ?? this.rightButton,
+ clearTextButton: clearTextButton ?? this.clearTextButton,
+ );
+ }
+}
+
+class _SuggestionsBox {
+ static const int waitMetricsTimeoutMillis = 1000;
+ static const double minOverlaySpace = 64.0;
+
+ final BuildContext context;
+ final AxisDirection desiredDirection;
+ final bool autoFlipDirection;
+ final bool hideSuggestionBox;
+
+ OverlayEntry? _overlayEntry;
+ AxisDirection direction;
+
+ bool isOpened = false;
+ bool widgetMounted = true;
+ double maxHeight = 300.0;
+ double textBoxWidth = 100.0;
+ double textBoxHeight = 100.0;
+ late double directionUpOffset;
+
+ _SuggestionsBox(
+ this.context,
+ this.direction,
+ this.autoFlipDirection,
+ this.hideSuggestionBox
+ ) : desiredDirection = direction;
+
+ void open() {
+ if (this.hideSuggestionBox) return;
+ if (this.isOpened) return;
+ assert(this._overlayEntry != null);
+ resize();
+ Overlay.of(context)!.insert(this._overlayEntry!);
+ this.isOpened = true;
+ }
+
+ void close() {
+ if (!this.isOpened) return;
+ assert(this._overlayEntry != null);
+ this._overlayEntry!.remove();
+ this.isOpened = false;
+ }
+
+ void toggle() {
+ if (this.isOpened) {
+ this.close();
+ } else {
+ this.open();
+ }
+ }
+
+ MediaQuery? _findRootMediaQuery() {
+ MediaQuery? rootMediaQuery;
+ context.visitAncestorElements((element) {
+ if (element.widget is MediaQuery) {
+ rootMediaQuery = element.widget as MediaQuery;
+ }
+ return true;
+ });
+
+ return rootMediaQuery;
+ }
+
+ /// Delays until the keyboard has toggled or the orientation has fully changed
+ Future _waitChangeMetrics() async {
+ if (widgetMounted) {
+ // initial viewInsets which are before the keyboard is toggled
+ EdgeInsets initial = MediaQuery.of(context).viewInsets;
+ // initial MediaQuery for orientation change
+ MediaQuery? initialRootMediaQuery = _findRootMediaQuery();
+
+ int timer = 0;
+ // viewInsets or MediaQuery have changed once keyboard has toggled or orientation has changed
+ while (widgetMounted && timer < waitMetricsTimeoutMillis) {
+ // reduce delay if showDialog ever exposes detection of animation end
+ await Future.delayed(const Duration(milliseconds: 170));
+ timer += 170;
+
+ if (widgetMounted &&
+ (MediaQuery.of(context).viewInsets != initial ||
+ _findRootMediaQuery() != initialRootMediaQuery)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ void resize() {
+ // check to see if widget is still mounted
+ // user may have closed the widget with the keyboard still open
+ if (widgetMounted) {
+ _adjustMaxHeightAndOrientation();
+ _overlayEntry!.markNeedsBuild();
+ }
+ }
+
+ // See if there's enough room in the desired direction for the overlay to display
+ // correctly. If not, try the opposite direction if things look more roomy there
+ void _adjustMaxHeightAndOrientation() {
+ TypeAheadFieldQuickSearch widget = context.widget as TypeAheadFieldQuickSearch;
+
+ RenderBox? box = context.findRenderObject() as RenderBox?;
+ if (box == null || box.hasSize == false) {
+ return;
+ }
+
+ textBoxWidth = box.size.width;
+ textBoxHeight = box.size.height;
+
+ // top of text box
+ double textBoxAbsY = box.localToGlobal(Offset.zero).dy;
+
+ // height of window
+ double windowHeight = MediaQuery.of(context).size.height;
+
+ // we need to find the root MediaQuery for the unsafe area height
+ // we cannot use BuildContext.ancestorWidgetOfExactType because
+ // widgets like SafeArea creates a new MediaQuery with the padding removed
+ MediaQuery rootMediaQuery = _findRootMediaQuery()!;
+
+ // height of keyboard
+ double keyboardHeight = rootMediaQuery.data.viewInsets.bottom;
+
+ double maxHDesired = _calculateMaxHeight(desiredDirection, box, widget,
+ windowHeight, rootMediaQuery, keyboardHeight, textBoxAbsY);
+
+ // if there's enough room in the desired direction, update the direction and the max height
+ if (maxHDesired >= minOverlaySpace || !autoFlipDirection) {
+ direction = desiredDirection;
+ maxHeight = maxHDesired;
+ } else {
+ // There's not enough room in the desired direction so see how much room is in the opposite direction
+ AxisDirection flipped = flipAxisDirection(desiredDirection);
+ double maxHFlipped = _calculateMaxHeight(flipped, box, widget,
+ windowHeight, rootMediaQuery, keyboardHeight, textBoxAbsY);
+
+ // if there's more room in this opposite direction, update the direction and maxHeight
+ if (maxHFlipped > maxHDesired) {
+ direction = flipped;
+ maxHeight = maxHFlipped;
+ }
+ }
+
+ if (maxHeight < 0) maxHeight = 0;
+ }
+
+ double _calculateMaxHeight(
+ AxisDirection direction,
+ RenderBox box,
+ TypeAheadFieldQuickSearch widget,
+ double windowHeight,
+ MediaQuery rootMediaQuery,
+ double keyboardHeight,
+ double textBoxAbsY) {
+ return direction == AxisDirection.down
+ ? _calculateMaxHeightDown(box, widget, windowHeight, rootMediaQuery,
+ keyboardHeight, textBoxAbsY)
+ : _calculateMaxHeightUp(box, widget, windowHeight, rootMediaQuery,
+ keyboardHeight, textBoxAbsY);
+ }
+
+ double _calculateMaxHeightDown(
+ RenderBox box,
+ TypeAheadFieldQuickSearch widget,
+ double windowHeight,
+ MediaQuery rootMediaQuery,
+ double keyboardHeight,
+ double textBoxAbsY) {
+ // unsafe area, ie: iPhone X 'home button'
+ // keyboardHeight includes unsafeAreaHeight, if keyboard is showing, set to 0
+ double unsafeAreaHeight =
+ keyboardHeight == 0 ? rootMediaQuery.data.padding.bottom : 0;
+
+ return windowHeight -
+ keyboardHeight -
+ unsafeAreaHeight -
+ textBoxHeight -
+ textBoxAbsY -
+ 2 * widget.suggestionsBoxVerticalOffset;
+ }
+
+ double _calculateMaxHeightUp(
+ RenderBox box,
+ TypeAheadFieldQuickSearch widget,
+ double windowHeight,
+ MediaQuery rootMediaQuery,
+ double keyboardHeight,
+ double textBoxAbsY) {
+ // recalculate keyboard absolute y value
+ double keyboardAbsY = windowHeight - keyboardHeight;
+
+ directionUpOffset = textBoxAbsY > keyboardAbsY
+ ? keyboardAbsY - textBoxAbsY - widget.suggestionsBoxVerticalOffset
+ : -widget.suggestionsBoxVerticalOffset;
+
+ // unsafe area, ie: iPhone X notch
+ double unsafeAreaHeight = rootMediaQuery.data.padding.top;
+
+ return textBoxAbsY > keyboardAbsY
+ ? keyboardAbsY -
+ unsafeAreaHeight -
+ 2 * widget.suggestionsBoxVerticalOffset
+ : textBoxAbsY -
+ unsafeAreaHeight -
+ 2 * widget.suggestionsBoxVerticalOffset;
+ }
+
+ Future onChangeMetrics() async {
+ if (await _waitChangeMetrics()) {
+ resize();
+ }
+ }
+}
+
+/// Supply an instance of this class to the [TypeAhead.suggestionsBoxController]
+/// property to manually control the suggestions box
+class QuickSearchSuggestionsBoxController {
+ _SuggestionsBox? _suggestionsBox;
+ FocusNode? _effectiveFocusNode;
+
+ /// Opens the suggestions box
+ void open() {
+ _effectiveFocusNode!.requestFocus();
+ }
+
+ bool isOpened() {
+ return _suggestionsBox!.isOpened;
+ }
+
+ /// Closes the suggestions box
+ void close() {
+ _effectiveFocusNode!.unfocus();
+ }
+
+ /// Opens the suggestions box if closed and vice-versa
+ void toggle() {
+ if (_suggestionsBox!.isOpened) {
+ close();
+ } else {
+ open();
+ }
+ }
+
+ /// Recalculates the height of the suggestions box
+ void resize() {
+ _suggestionsBox!.resize();
+ }
+}
diff --git a/core/lib/presentation/views/typeahead/typedef.dart b/core/lib/presentation/views/typeahead/typedef.dart
new file mode 100644
index 000000000..5fc9dd714
--- /dev/null
+++ b/core/lib/presentation/views/typeahead/typedef.dart
@@ -0,0 +1,15 @@
+import 'dart:async';
+
+import 'package:flutter/widgets.dart';
+
+typedef FutureOr> SuggestionsCallback(String pattern);
+typedef FutureOr> FetchRecentActionCallback(String pattern);
+typedef Widget ItemBuilder(BuildContext context, T itemData);
+typedef void SuggestionSelectionCallback(T suggestion);
+typedef void RecentSelectionCallback(R recent);
+typedef Widget ErrorBuilder(BuildContext context, Object? error);
+typedef Widget ButtonActionBuilder(BuildContext context, dynamic action);
+typedef void ButtonActionCallback(dynamic action);
+
+typedef Widget AnimationTransitionBuilder(
+ BuildContext context, Widget child, AnimationController? controller);
diff --git a/core/lib/presentation/views/typeahead/utils.dart b/core/lib/presentation/views/typeahead/utils.dart
new file mode 100644
index 000000000..f8a3062c3
--- /dev/null
+++ b/core/lib/presentation/views/typeahead/utils.dart
@@ -0,0 +1,5 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+
+final supportedPlatform = (kIsWeb || Platform.isAndroid || Platform.isIOS);
diff --git a/lib/features/caching/recent_search_cache_client.dart b/lib/features/caching/recent_search_cache_client.dart
new file mode 100644
index 000000000..63b6bb213
--- /dev/null
+++ b/lib/features/caching/recent_search_cache_client.dart
@@ -0,0 +1,111 @@
+
+import 'package:hive/hive.dart';
+import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart';
+import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/recent_search_cache.dart';
+
+class RecentSearchCacheClient extends HiveCacheClient {
+
+ @override
+ String get tableName => 'RecentSearchCache';
+
+ @override
+ Future clearAllData() {
+ return Future.sync(() async {
+ final boxState = await openBox();
+ boxState.clear();
+ }).catchError((error) {
+ throw error;
+ });
+ }
+
+ @override
+ Future deleteItem(String key) {
+ return Future.sync(() async {
+ final boxState = await openBox();
+ return boxState.delete(key);
+ }).catchError((error) {
+ throw error;
+ });
+ }
+
+ @override
+ Future deleteMultipleItem(List listKey) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future> getAll() {
+ return Future.sync(() async {
+ final boxState = await openBox();
+ return boxState.values.toList();
+ }).catchError((error) {
+ throw error;
+ });
+ }
+
+ @override
+ Future getItem(String key) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future insertItem(String key, RecentSearchCache newObject) {
+ return Future.sync(() async {
+ final boxState = await openBox();
+ boxState.put(key, newObject);
+ }).catchError((error) {
+ throw error;
+ });
+ }
+
+ @override
+ Future insertMultipleItem(Map mapObject) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future isExistItem(String key) {
+ return Future.sync(() async {
+ final boxState = await openBox();
+ return boxState.containsKey(key);
+ }).catchError((error) {
+ throw error;
+ });
+ }
+
+ @override
+ Future isExistTable() {
+ return Future.sync(() async {
+ return await Hive.boxExists(tableName);
+ }).catchError((error) {
+ throw error;
+ });
+ }
+
+ @override
+ Future> openBox() {
+ return Future.sync(() async {
+ if (Hive.isBoxOpen(tableName)) {
+ return Hive.box(tableName);
+ }
+ return await Hive.openBox(tableName);
+ }).catchError((error) {
+ throw error;
+ });
+ }
+
+ @override
+ Future updateItem(String key, RecentSearchCache newObject) {
+ return Future.sync(() async {
+ final boxState = await openBox();
+ boxState.put(key, newObject);
+ }).catchError((error) {
+ throw error;
+ });
+ }
+
+ @override
+ Future updateMultipleItem(Map mapObject) {
+ throw UnimplementedError();
+ }
+}
\ No newline at end of file
diff --git a/lib/features/caching/utils/caching_constants.dart b/lib/features/caching/utils/caching_constants.dart
index 15f29baa9..636083f62 100644
--- a/lib/features/caching/utils/caching_constants.dart
+++ b/lib/features/caching/utils/caching_constants.dart
@@ -6,4 +6,5 @@ class CachingConstants {
static const int STATE_TYPE_IDENTIFY = 4;
static const int EMAIL_CACHE_IDENTIFY = 5;
static const int EMAIL_ADDRESS_HIVE_CACHE_IDENTIFY = 6;
+ static const int RECENT_SEARCH_HIVE_CACHE_IDENTIFY = 7;
}
\ No newline at end of file
diff --git a/lib/features/mailbox/presentation/widgets/search_form_widget_builder.dart b/lib/features/mailbox/presentation/widgets/search_form_widget_builder.dart
deleted file mode 100644
index 3e44f6a60..000000000
--- a/lib/features/mailbox/presentation/widgets/search_form_widget_builder.dart
+++ /dev/null
@@ -1,64 +0,0 @@
-
-import 'package:core/core.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:flutter_typeahead/flutter_typeahead.dart';
-import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
-
-typedef OnNewSearchQuery = Function(String);
-
-class SearchFormWidgetBuilder {
-
- final _typeAheadController = TextEditingController();
- final BuildContext _context;
- final ImagePaths _imagePaths;
-
- OnNewSearchQuery? _onNewSearchQuery;
-
- SearchFormWidgetBuilder(this._context, this._imagePaths);
-
- SearchFormWidgetBuilder onNewSearchQuery(OnNewSearchQuery onNewSearchQuery) {
- _onNewSearchQuery = onNewSearchQuery;
- return this;
- }
-
- Widget build() {
- return Container(
- key: const Key('search_folder_form'),
- alignment: Alignment.center,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(16),
- border: Border.all(color: AppColor.searchBorderColor),
- color: AppColor.primaryLightColor),
- child: TypeAheadFormField(
- textFieldConfiguration: TextFieldConfiguration(
- controller: _typeAheadController,
- textInputAction: TextInputAction.done,
- decoration: InputDecoration(
- border: InputBorder.none,
- focusedBorder: InputBorder.none,
- enabledBorder: InputBorder.none,
- contentPadding: const EdgeInsets.only(left: 0, top: 15, bottom: 15, right: 15),
- hintText: AppLocalizations.of(_context).search_folder,
- hintStyle: const TextStyle(color: AppColor.searchHintTextColor, fontSize: 15.0, fontWeight: FontWeight.w500),
- icon: Padding(
- padding: const EdgeInsets.only(left: 20),
- child: SvgPicture.asset(_imagePaths.icSearch, width: 24, height: 24, fit: BoxFit.fill),
- ))),
- debounceDuration: const Duration(milliseconds: 300),
- suggestionsCallback: (pattern) async {
- if (_onNewSearchQuery != null) {
- _onNewSearchQuery!(pattern);
- }
- return [];
- },
- itemBuilder: (BuildContext context, itemData) => const SizedBox.shrink(),
- onSuggestionSelected: (suggestion) {},
- noItemsFoundBuilder: (context) => const SizedBox(),
- hideOnEmpty: true,
- hideOnError: true,
- hideOnLoading: true,
- hideSuggestionsOnKeyboardHide: true,
- ));
- }
-}
\ No newline at end of file
diff --git a/lib/features/mailbox_dashboard/data/model/recent_search_cache.dart b/lib/features/mailbox_dashboard/data/model/recent_search_cache.dart
new file mode 100644
index 000000000..dba8c07a7
--- /dev/null
+++ b/lib/features/mailbox_dashboard/data/model/recent_search_cache.dart
@@ -0,0 +1,21 @@
+
+import 'package:equatable/equatable.dart';
+import 'package:hive/hive.dart';
+import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart';
+
+part 'recent_search_cache.g.dart';
+
+@HiveType(typeId: CachingConstants.RECENT_SEARCH_HIVE_CACHE_IDENTIFY)
+class RecentSearchCache extends HiveObject with EquatableMixin {
+
+ @HiveField(0)
+ final String value;
+
+ @HiveField(1)
+ final DateTime creationTime;
+
+ RecentSearchCache(this.value, this.creationTime);
+
+ @override
+ List