refactor: remove the unused submitUserInfo method and enhance the searchable dropdown functionality
- Removed `submitUserInfo` from auth services and integrations. - Refined dropdown with new multi-select and searchable options. - Added `PersianFormatter` for better input handling. - Updated `local.properties` to set flutter build mode to debug.
This commit is contained in:
@@ -1,70 +1,256 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:rasadyar_core/presentation/common/app_color.dart';
|
||||
import 'package:rasadyar_core/core.dart';
|
||||
|
||||
import 'multi_select_dropdown_logic.dart';
|
||||
|
||||
class MultiSelectDropdown<T> extends StatelessWidget {
|
||||
/// A customizable searchable dropdown widget that supports both single and multi-select modes.
|
||||
///
|
||||
/// The [SearchableDropdown] widget provides a text field with an overlay dropdown menu
|
||||
/// that can be filtered by search. It supports two modes:
|
||||
/// - **Single Select**: When [singleSelect] is `true`, only one item can be selected at a time.
|
||||
/// Requires [singleLabelBuilder] to be provided.
|
||||
/// - **Multi Select**: When [singleSelect] is `false`, multiple items can be selected.
|
||||
/// Requires [multiLabelBuilder] to be provided and displays selected items as chips.
|
||||
///
|
||||
/// The widget uses [SearchableDropdownLogic] for state management and overlay handling.
|
||||
///
|
||||
/// Example usage (Single Select):
|
||||
/// ```dart
|
||||
/// SearchableDropdown<String>(
|
||||
/// items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
/// singleSelect: true,
|
||||
/// singleLabelBuilder: (selected) => selected ?? 'Select an option',
|
||||
/// itemBuilder: (item) => Text(item),
|
||||
/// onChanged: (item) => print('Selected: $item'),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Example usage (Multi Select):
|
||||
/// ```dart
|
||||
/// SearchableDropdown<String>(
|
||||
/// items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
/// singleSelect: false,
|
||||
/// multiLabelBuilder: (selected) => Chip(label: Text(selected.toString())),
|
||||
/// itemBuilder: (item) => Text(item),
|
||||
/// onChanged: (item) => print('Selected: $item'),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Example with custom search:
|
||||
/// ```dart
|
||||
/// SearchableDropdown<User>(
|
||||
/// items: users,
|
||||
/// singleSelect: true,
|
||||
/// singleLabelBuilder: (user) => user?.name ?? 'Select user',
|
||||
/// itemBuilder: (user) => ListTile(title: Text(user.name)),
|
||||
/// onSearch: (query) async {
|
||||
/// return await searchUsers(query);
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
class SearchableDropdown<T> extends StatelessWidget {
|
||||
/// The list of items to display in the dropdown.
|
||||
final List<T> items;
|
||||
final T? selectedItem;
|
||||
final T? initialValue;
|
||||
final Widget Function(T item) itemBuilder;
|
||||
final Widget Function(T? selected) labelBuilder;
|
||||
final void Function(T selected)? onChanged;
|
||||
final EdgeInsets? contentPadding;
|
||||
final String Function(T item)? itemToString;
|
||||
|
||||
const MultiSelectDropdown({
|
||||
/// Pre-selected items. If provided, these items will be initially selected.
|
||||
final List<T>? selectedItem;
|
||||
|
||||
/// Initial value for single select mode. Ignored if [selectedItem] is provided.
|
||||
final T? initialValue;
|
||||
|
||||
/// Hint text to display in the text field when no item is selected.
|
||||
final String? hintText;
|
||||
|
||||
/// Text style for the hint text.
|
||||
final TextStyle? hintStyle;
|
||||
|
||||
/// Whether the dropdown is in single select mode.
|
||||
///
|
||||
/// - `true`: Only one item can be selected. Requires [singleLabelBuilder].
|
||||
/// - `false`: Multiple items can be selected. Requires [multiLabelBuilder].
|
||||
late final bool singleSelect;
|
||||
|
||||
/// Builder function to create the widget for each item in the dropdown list.
|
||||
///
|
||||
/// This is used to render items in the overlay dropdown menu.
|
||||
final Widget Function(T item) itemBuilder;
|
||||
|
||||
/// Builder function for single select mode to display the selected item as a string.
|
||||
///
|
||||
/// Required when [singleSelect] is `true`. This function receives the selected item
|
||||
/// and should return a string representation to display in the text field.
|
||||
final String Function(T? selected)? singleLabelBuilder;
|
||||
|
||||
/// Builder function for multi select mode to create widgets for selected items.
|
||||
///
|
||||
/// Required when [singleSelect] is `false`. This function receives a selected item
|
||||
/// and should return a widget (typically a Chip or similar) to display in the
|
||||
/// horizontal list below the text field.
|
||||
final Widget Function(T? selected)? multiLabelBuilder;
|
||||
|
||||
/// Callback function called when an item is selected.
|
||||
///
|
||||
/// Receives the selected item as a parameter.
|
||||
final void Function(T selected)? onChanged;
|
||||
|
||||
/// Padding for items in the dropdown list.
|
||||
final EdgeInsets? contentPadding;
|
||||
|
||||
/// Optional custom search function for filtering items.
|
||||
///
|
||||
/// If provided, the text field becomes editable and this function is called
|
||||
/// whenever the user types. The function receives the search query and should
|
||||
/// return a filtered list of items. If `null`, the text field is read-only and
|
||||
/// uses the default filtering behavior.
|
||||
final Future<List<T>?> Function(String query)? onSearch;
|
||||
|
||||
final InputBorder _inputBorder = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: AppColor.darkGreyLight, width: 1),
|
||||
);
|
||||
|
||||
/// Creates a [SearchableDropdown] widget.
|
||||
///
|
||||
/// The [items] and [itemBuilder] parameters are required.
|
||||
///
|
||||
/// When [singleSelect] is `true`, [singleLabelBuilder] must be provided and
|
||||
/// [multiLabelBuilder] must be `null`.
|
||||
///
|
||||
/// When [singleSelect] is `false`, [multiLabelBuilder] must be provided and
|
||||
/// [singleLabelBuilder] must be `null`.
|
||||
///
|
||||
/// Throws an [AssertionError] if the label builder requirements are not met.
|
||||
SearchableDropdown({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
required this.labelBuilder,
|
||||
this.singleLabelBuilder,
|
||||
this.multiLabelBuilder,
|
||||
this.initialValue,
|
||||
this.onChanged,
|
||||
this.selectedItem,
|
||||
this.contentPadding,
|
||||
this.itemToString,
|
||||
});
|
||||
this.hintText,
|
||||
this.hintStyle,
|
||||
this.onSearch,
|
||||
this.singleSelect = false,
|
||||
}) : assert(
|
||||
(singleSelect &&
|
||||
singleLabelBuilder != null &&
|
||||
multiLabelBuilder == null) ||
|
||||
(!singleSelect &&
|
||||
multiLabelBuilder != null &&
|
||||
singleLabelBuilder == null),
|
||||
'When singleSelect is true, only singleLabelBuilder should be provided. '
|
||||
'When singleSelect is false, only multiLabelBuilder should be provided.',
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MultiSelectDropdownLogic<T>>(
|
||||
init: MultiSelectDropdownLogic<T>(
|
||||
return GetBuilder<SearchableDropdownLogic<T>>(
|
||||
init: SearchableDropdownLogic<T>(
|
||||
items: items,
|
||||
selectedItem: selectedItem,
|
||||
initialValue: initialValue,
|
||||
itemToString: itemToString,
|
||||
onChanged: onChanged,
|
||||
contentPadding: contentPadding,
|
||||
itemBuilder: itemBuilder,
|
||||
labelBuilder: singleLabelBuilder,
|
||||
onSearch: onSearch,
|
||||
singleSelect: singleSelect,
|
||||
),
|
||||
builder: (controller) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.isOpen.value ? controller.removeOverlay() : controller.showOverlay(context);
|
||||
},
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: Get.width,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: items.isEmpty ? Colors.grey.shade200 : AppColor.bgLight,
|
||||
border: Border.all(color: AppColor.darkGreyLight),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
return Obx(() {
|
||||
iLog(controller.selectedItem);
|
||||
iLog(controller.selectedItem.length);
|
||||
if (controller.selectedItem.isNotEmpty && !singleSelect) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(child: labelBuilder(controller.selectedItem.value)),
|
||||
Icon(
|
||||
controller.isOpen.value ? CupertinoIcons.chevron_up : CupertinoIcons.chevron_down,
|
||||
size: 14,
|
||||
CompositedTransformTarget(
|
||||
link: controller.layerLink,
|
||||
child: TextField(
|
||||
controller: controller.searchController,
|
||||
maxLines: 1,
|
||||
minLines: 1,
|
||||
readOnly: onSearch == null ? true : false,
|
||||
onTapOutside: (_) => FocusScope.of(context).unfocus(),
|
||||
onSubmitted: (_) => FocusScope.of(context).unfocus(),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: AppColor.bgLight,
|
||||
border: _inputBorder,
|
||||
focusedBorder: _inputBorder,
|
||||
enabledBorder: _inputBorder,
|
||||
hintText: hintText,
|
||||
hintStyle: hintStyle,
|
||||
suffixIcon: Icon(
|
||||
controller.isOpen.value
|
||||
? CupertinoIcons.chevron_up
|
||||
: CupertinoIcons.chevron_down,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
onChanged: (query) => controller.performSearch(query),
|
||||
onTap: () {
|
||||
controller.isOpen.value
|
||||
? controller.removeOverlay()
|
||||
: controller.showOverlay(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
separatorBuilder: (context, index) => SizedBox(width: 6),
|
||||
itemBuilder: (context, index) => GestureDetector(
|
||||
onTap: () {
|
||||
controller.selectedItem.remove(
|
||||
controller.selectedItem[index],
|
||||
);
|
||||
},
|
||||
child: multiLabelBuilder!(controller.selectedItem[index]),
|
||||
),
|
||||
itemCount: controller.selectedItem.length,
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return CompositedTransformTarget(
|
||||
link: controller.layerLink,
|
||||
child: TextField(
|
||||
controller: controller.searchController,
|
||||
maxLines: 1,
|
||||
minLines: 1,
|
||||
readOnly: onSearch == null ? true : false,
|
||||
onTapOutside: (_) => FocusScope.of(context).unfocus(),
|
||||
onSubmitted: (_) => FocusScope.of(context).unfocus(),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: AppColor.bgLight,
|
||||
border: _inputBorder,
|
||||
focusedBorder: _inputBorder,
|
||||
enabledBorder: _inputBorder,
|
||||
hintText: hintText,
|
||||
hintStyle: hintStyle,
|
||||
suffixIcon: Icon(
|
||||
controller.isOpen.value
|
||||
? CupertinoIcons.chevron_up
|
||||
: CupertinoIcons.chevron_down,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
onChanged: (query) => controller.performSearch(query),
|
||||
onTap: () {
|
||||
controller.isOpen.value
|
||||
? controller.removeOverlay()
|
||||
: controller.showOverlay(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,28 +2,103 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:rasadyar_core/presentation/common/app_color.dart';
|
||||
|
||||
class MultiSelectDropdownLogic<T> extends GetxController {
|
||||
/// Controller class for managing the state and behavior of [SearchableDropdown].
|
||||
///
|
||||
/// This class extends [GetxController] and handles:
|
||||
/// - Managing selected items (single or multiple)
|
||||
/// - Filtering items based on search queries
|
||||
/// - Showing and hiding the overlay dropdown menu
|
||||
/// - Handling user interactions with the dropdown
|
||||
///
|
||||
/// The controller maintains:
|
||||
/// - [selectedItem]: Reactive list of currently selected items
|
||||
/// - [filteredItems]: Reactive list of items filtered by search query
|
||||
/// - [isOpen]: Reactive boolean indicating if the overlay is visible
|
||||
/// - [searchController]: Text editing controller for the search field
|
||||
/// - [layerLink]: Used for positioning the overlay relative to the text field
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final controller = SearchableDropdownLogic<String>(
|
||||
/// items: ['Item 1', 'Item 2', 'Item 3'],
|
||||
/// singleSelect: true,
|
||||
/// itemBuilder: (item) => Text(item),
|
||||
/// );
|
||||
/// ```
|
||||
class SearchableDropdownLogic<T> extends GetxController {
|
||||
/// The list of all available items.
|
||||
final List<T> items;
|
||||
|
||||
/// Initial value for single select mode.
|
||||
final T? initialValue;
|
||||
final String Function(T item)? itemToString;
|
||||
|
||||
/// Whether the dropdown is in single select mode.
|
||||
final bool singleSelect;
|
||||
|
||||
/// Hint text for the text field.
|
||||
final String? hintText;
|
||||
|
||||
/// Callback function called when an item is selected.
|
||||
final void Function(T selected)? onChanged;
|
||||
|
||||
/// Custom search function for filtering items.
|
||||
///
|
||||
/// If provided, this function is called when the user types in the search field.
|
||||
/// It should return a filtered list of items based on the query.
|
||||
final Future<List<T>?> Function(String query)? onSearch;
|
||||
|
||||
/// The overlay entry for the dropdown menu.
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
/// Reactive boolean indicating whether the overlay is currently visible.
|
||||
RxBool isOpen = false.obs;
|
||||
Rx<T?> selectedItem;
|
||||
|
||||
/// Reactive list of currently selected items.
|
||||
RxList<T> selectedItem = RxList<T>();
|
||||
|
||||
/// Text editing controller for the search field.
|
||||
late TextEditingController searchController;
|
||||
|
||||
/// Reactive list of items filtered by the current search query.
|
||||
late RxList<T> filteredItems;
|
||||
|
||||
/// Padding for items in the dropdown list.
|
||||
late EdgeInsets? contentPadding;
|
||||
|
||||
/// Builder function to create widgets for each item in the dropdown.
|
||||
late Widget Function(T item) itemBuilder;
|
||||
|
||||
MultiSelectDropdownLogic({
|
||||
/// Builder function for displaying the selected item label (single select mode).
|
||||
final String Function(T? selected)? labelBuilder;
|
||||
|
||||
/// Layer link used for positioning the overlay relative to the text field.
|
||||
final LayerLink layerLink = LayerLink();
|
||||
|
||||
/// Creates a [SearchableDropdownLogic] controller.
|
||||
///
|
||||
/// The [items] and [itemBuilder] parameters are required.
|
||||
///
|
||||
/// If [selectedItem] is provided, it will be used as the initial selection.
|
||||
/// Otherwise, if [initialValue] is provided (and [singleSelect] is `true`),
|
||||
/// it will be used as the initial selection.
|
||||
SearchableDropdownLogic({
|
||||
required this.items,
|
||||
this.singleSelect = false,
|
||||
this.initialValue,
|
||||
this.itemToString,
|
||||
this.onChanged,
|
||||
T? selectedItem,
|
||||
this.hintText,
|
||||
List<T>? selectedItem,
|
||||
this.contentPadding,
|
||||
required this.itemBuilder,
|
||||
}) : selectedItem = Rx(selectedItem ?? initialValue);
|
||||
this.labelBuilder,
|
||||
this.onSearch,
|
||||
}) {
|
||||
if (selectedItem != null) {
|
||||
this.selectedItem.value = selectedItem;
|
||||
} else {
|
||||
this.selectedItem.value = initialValue != null ? [?initialValue] : [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@@ -32,114 +107,127 @@ class MultiSelectDropdownLogic<T> extends GetxController {
|
||||
filteredItems = RxList<T>(items);
|
||||
}
|
||||
|
||||
/// Shows the overlay dropdown menu below the text field.
|
||||
///
|
||||
/// This method creates an [OverlayEntry] positioned relative to the text field
|
||||
/// using [layerLink]. The overlay displays the filtered list of items and
|
||||
/// handles item selection.
|
||||
///
|
||||
/// When called, it:
|
||||
/// - Clears the search controller
|
||||
/// - Resets the filtered items to all items
|
||||
/// - Creates and inserts the overlay entry
|
||||
/// - Sets [isOpen] to `true`
|
||||
void showOverlay(BuildContext context) {
|
||||
final RenderBox renderBox = (context.findRenderObject() as RenderBox);
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
final bool openUp = offset.dy + size.height + 300 > screenHeight;
|
||||
|
||||
searchController.clear();
|
||||
filteredItems.value = items;
|
||||
|
||||
OverlayEntry overlayEntry = OverlayEntry(
|
||||
builder: (_) => GestureDetector(
|
||||
onTap: () {
|
||||
removeOverlay();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: offset.dx,
|
||||
top: openUp ? offset.dy - 300 - 4 : offset.dy + size.height + 4,
|
||||
width: size.width,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Obx(
|
||||
() => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.bgLight,
|
||||
border: Border.all(color: AppColor.darkGreyLight),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: 300),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'جستجو...',
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (query) {
|
||||
filteredItems.value = items
|
||||
.where(
|
||||
(item) =>
|
||||
itemToString
|
||||
?.call(item)
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase()) ??
|
||||
false,
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: removeOverlay,
|
||||
child: Stack(
|
||||
children: [
|
||||
CompositedTransformFollower(
|
||||
link: layerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: Offset(0, size.height + 4),
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Obx(
|
||||
() => Container(
|
||||
width: size.width,
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.bgLight,
|
||||
border: Border.all(color: AppColor.darkGreyLight),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
if (filteredItems.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Text("نتیجهای یافت نشد."),
|
||||
),
|
||||
if (filteredItems.isNotEmpty)
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
itemCount: filteredItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
var item = filteredItems[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
onChanged?.call(item);
|
||||
selectedItem.value = item;
|
||||
removeOverlay();
|
||||
},
|
||||
child: Padding(
|
||||
padding:
|
||||
contentPadding ??
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: itemBuilder(item),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
child: (filteredItems.isEmpty)
|
||||
? const Center(child: Text("نتیجهای یافت نشد."))
|
||||
: ListView.builder(
|
||||
itemCount: filteredItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = filteredItems[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (!selectedItem.contains(item)) {
|
||||
selectedItem.add(item);
|
||||
}
|
||||
|
||||
onChanged?.call(item);
|
||||
removeOverlay();
|
||||
if (singleSelect) {
|
||||
searchController.text = labelBuilder!(
|
||||
item,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding:
|
||||
contentPadding ??
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: itemBuilder(item),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(overlayEntry);
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
isOpen.value = true;
|
||||
}
|
||||
|
||||
/// Removes the overlay dropdown menu.
|
||||
///
|
||||
/// This method removes the overlay entry from the overlay stack and sets
|
||||
/// [isOpen] to `false`.
|
||||
void removeOverlay() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
isOpen.value = false;
|
||||
}
|
||||
|
||||
/// Performs a search operation to filter items.
|
||||
///
|
||||
/// If [onSearch] is provided, it calls the custom search function with the
|
||||
/// given [query] and updates [filteredItems] with the result.
|
||||
///
|
||||
/// After updating the filtered items, it marks the overlay entry for rebuild
|
||||
/// to reflect the new filtered results.
|
||||
void performSearch(String query) async {
|
||||
if (onSearch != null) {
|
||||
final result = await onSearch!(query);
|
||||
filteredItems.value = result ?? [];
|
||||
}
|
||||
|
||||
if (_overlayEntry != null) {
|
||||
_overlayEntry!.markNeedsBuild();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
searchController.dispose();
|
||||
removeOverlay();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,9 @@ extension XString on String? {
|
||||
}
|
||||
|
||||
int get versionNumber => int.parse(this?.replaceAll(".", '') ?? '0');
|
||||
|
||||
bool get isDifferentDigits {
|
||||
final regex = RegExp(r'[۰-۹٠-٩]');
|
||||
return regex.hasMatch(this ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,3 +114,16 @@ Map<String, dynamic>? buildRawQueryParams({
|
||||
|
||||
return params.keys.isEmpty ? null : params;
|
||||
}
|
||||
|
||||
const Map<String, String> digitMap = {
|
||||
'۰': '0',
|
||||
'۱': '1',
|
||||
'۲': '2',
|
||||
'۳': '3',
|
||||
'۴': '4',
|
||||
'۵': '5',
|
||||
'۶': '6',
|
||||
'۷': '7',
|
||||
'۸': '8',
|
||||
'۹': '9',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../map_utils.dart';
|
||||
|
||||
class PersianFormatter extends TextInputFormatter {
|
||||
String _convert(String input) {
|
||||
final buffer = StringBuffer();
|
||||
for (var char in input.split('')) {
|
||||
buffer.write(digitMap[char] ?? char);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
final fixed = _convert(newValue.text);
|
||||
|
||||
return newValue.copyWith(
|
||||
text: fixed,
|
||||
selection: TextSelection.collapsed(offset: fixed.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,5 @@ export 'number_utils.dart';
|
||||
export 'parser.dart';
|
||||
export 'route_utils.dart';
|
||||
export 'separator_input_formatter.dart';
|
||||
export 'first_digit_decimal_formatter.dart';
|
||||
export 'text_input_formatter/first_digit_decimal_formatter.dart';
|
||||
export 'text_input_formatter/persian_formatter.dart';
|
||||
|
||||
Reference in New Issue
Block a user