feat(mobile): Quick date picker in the search page (#22653)
* Quick date picker * Include current year in quick date picker * Quick date picker: localization, fix datetime overflows * Properly localized 'last_months' * Move quick_date_picker.dart to lib/presentation/widgets/search * Wrap the quick date picker state into its own class, improve the interaction patterns * Fix last9Months value * Improve method naming * Subtitle for "custom range" in quick date picker * Fix style warnings * Fix lint warning * fix: mobile unawaited_futures lint (#21661) * chore: add unawaited_futures lint as warning * remove unused dcm lints They will be added back later on a case by case basis * fix warning * auto gen file * review changes * conflict resolution --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> * Quick date picker * Wrap the quick date picker state into its own class, improve the interaction patterns * chore: delete file from rebase --------- Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: bwees <brandonwees@gmail.com>pull/23770/head
parent
787158247f
commit
7a2c8e0662
|
|
@ -1197,6 +1197,8 @@
|
||||||
"import_path": "Import path",
|
"import_path": "Import path",
|
||||||
"in_albums": "In {count, plural, one {# album} other {# albums}}",
|
"in_albums": "In {count, plural, one {# album} other {# albums}}",
|
||||||
"in_archive": "In archive",
|
"in_archive": "In archive",
|
||||||
|
"in_year": "In {year}",
|
||||||
|
"in_year_selector": "In",
|
||||||
"include_archived": "Include archived",
|
"include_archived": "Include archived",
|
||||||
"include_shared_albums": "Include shared albums",
|
"include_shared_albums": "Include shared albums",
|
||||||
"include_shared_partner_assets": "Include shared partner assets",
|
"include_shared_partner_assets": "Include shared partner assets",
|
||||||
|
|
@ -1233,6 +1235,7 @@
|
||||||
"language_setting_description": "Select your preferred language",
|
"language_setting_description": "Select your preferred language",
|
||||||
"large_files": "Large Files",
|
"large_files": "Large Files",
|
||||||
"last": "Last",
|
"last": "Last",
|
||||||
|
"last_months": "{count, plural, one {Last month} other {Last # months}}",
|
||||||
"last_seen": "Last seen",
|
"last_seen": "Last seen",
|
||||||
"latest_version": "Latest Version",
|
"latest_version": "Latest Version",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
|
|
@ -1554,6 +1557,8 @@
|
||||||
"photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}",
|
"photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}",
|
||||||
"photos_from_previous_years": "Photos from previous years",
|
"photos_from_previous_years": "Photos from previous years",
|
||||||
"pick_a_location": "Pick a location",
|
"pick_a_location": "Pick a location",
|
||||||
|
"pick_custom_range": "Custom range",
|
||||||
|
"pick_date_range": "Select a date range",
|
||||||
"pin_code_changed_successfully": "Successfully changed PIN code",
|
"pin_code_changed_successfully": "Successfully changed PIN code",
|
||||||
"pin_code_reset_successfully": "Successfully reset PIN code",
|
"pin_code_reset_successfully": "Successfully reset PIN code",
|
||||||
"pin_code_setup_successfully": "Successfully setup a PIN code",
|
"pin_code_setup_successfully": "Successfully setup a PIN code",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/search/quick_date_picker.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
|
|
@ -54,6 +55,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
final previousFilter = useState<SearchFilter?>(null);
|
final previousFilter = useState<SearchFilter?>(null);
|
||||||
|
final dateInputFilter = useState<DateFilterInputModel?>(null);
|
||||||
|
|
||||||
final peopleCurrentFilterWidget = useState<Widget?>(null);
|
final peopleCurrentFilterWidget = useState<Widget?>(null);
|
||||||
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
|
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
|
||||||
|
|
@ -245,19 +247,54 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
datePicked(DateFilterInputModel? selectedDate) {
|
||||||
|
dateInputFilter.value = selectedDate;
|
||||||
|
if (selectedDate == null) {
|
||||||
|
filter.value = filter.value.copyWith(date: SearchDateFilter());
|
||||||
|
|
||||||
|
dateRangeCurrentFilterWidget.value = null;
|
||||||
|
unawaited(search());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final date = selectedDate.asDateTimeRange();
|
||||||
|
|
||||||
|
filter.value = filter.value.copyWith(
|
||||||
|
date: SearchDateFilter(
|
||||||
|
takenAfter: date.start,
|
||||||
|
takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
dateRangeCurrentFilterWidget.value = Text(
|
||||||
|
selectedDate.asHumanReadable(context),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
|
||||||
|
unawaited(search());
|
||||||
|
}
|
||||||
|
|
||||||
showDatePicker() async {
|
showDatePicker() async {
|
||||||
final firstDate = DateTime(1900);
|
final firstDate = DateTime(1900);
|
||||||
final lastDate = DateTime.now();
|
final lastDate = DateTime.now();
|
||||||
|
|
||||||
|
var dateRange = DateTimeRange(
|
||||||
|
start: filter.value.date.takenAfter ?? lastDate,
|
||||||
|
end: filter.value.date.takenBefore ?? lastDate,
|
||||||
|
);
|
||||||
|
|
||||||
|
// datePicked() may increase the date, this will make the date picker fail an assertion
|
||||||
|
// Fixup the end date to be at most now.
|
||||||
|
if (dateRange.end.isAfter(lastDate)) {
|
||||||
|
dateRange = DateTimeRange(start: dateRange.start, end: lastDate);
|
||||||
|
}
|
||||||
|
|
||||||
final date = await showDateRangePicker(
|
final date = await showDateRangePicker(
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: firstDate,
|
firstDate: firstDate,
|
||||||
lastDate: lastDate,
|
lastDate: lastDate,
|
||||||
currentDate: DateTime.now(),
|
currentDate: DateTime.now(),
|
||||||
initialDateRange: DateTimeRange(
|
initialDateRange: dateRange,
|
||||||
start: filter.value.date.takenAfter ?? lastDate,
|
|
||||||
end: filter.value.date.takenBefore ?? lastDate,
|
|
||||||
),
|
|
||||||
helpText: 'search_filter_date_title'.t(context: context),
|
helpText: 'search_filter_date_title'.t(context: context),
|
||||||
cancelText: 'cancel'.t(context: context),
|
cancelText: 'cancel'.t(context: context),
|
||||||
confirmText: 'select'.t(context: context),
|
confirmText: 'select'.t(context: context),
|
||||||
|
|
@ -271,40 +308,32 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (date == null) {
|
if (date == null) {
|
||||||
filter.value = filter.value.copyWith(date: SearchDateFilter());
|
datePicked(null);
|
||||||
|
|
||||||
dateRangeCurrentFilterWidget.value = null;
|
|
||||||
unawaited(search());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
filter.value = filter.value.copyWith(
|
|
||||||
date: SearchDateFilter(
|
|
||||||
takenAfter: date.start,
|
|
||||||
takenBefore: date.end.add(const Duration(hours: 23, minutes: 59, seconds: 59)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If date range is less than 24 hours, set the end date to the end of the day
|
|
||||||
if (date.end.difference(date.start).inHours < 24) {
|
|
||||||
dateRangeCurrentFilterWidget.value = Text(
|
|
||||||
DateFormat.yMMMd().format(date.start.toLocal()),
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
dateRangeCurrentFilterWidget.value = Text(
|
datePicked(CustomDateFilter.fromRange(date));
|
||||||
'search_filter_date_interval'.t(
|
}
|
||||||
context: context,
|
}
|
||||||
args: {
|
|
||||||
"start": DateFormat.yMMMd().format(date.start.toLocal()),
|
showQuickDatePicker() {
|
||||||
"end": DateFormat.yMMMd().format(date.end.toLocal()),
|
showFilterBottomSheet(
|
||||||
|
context: context,
|
||||||
|
child: FilterBottomSheetScaffold(
|
||||||
|
title: "pick_date_range".tr(),
|
||||||
|
expanded: true,
|
||||||
|
onClear: () => datePicked(null),
|
||||||
|
child: QuickDatePicker(
|
||||||
|
currentInput: dateInputFilter.value,
|
||||||
|
onRequestPicker: () {
|
||||||
|
context.pop();
|
||||||
|
showDatePicker();
|
||||||
|
},
|
||||||
|
onSelect: (date) {
|
||||||
|
context.pop();
|
||||||
|
datePicked(date);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
style: context.textTheme.labelLarge,
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
unawaited(search());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MEDIA PICKER
|
// MEDIA PICKER
|
||||||
|
|
@ -589,7 +618,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
SearchFilterChip(
|
SearchFilterChip(
|
||||||
icon: Icons.date_range_outlined,
|
icon: Icons.date_range_outlined,
|
||||||
onTap: showDatePicker,
|
onTap: showQuickDatePicker,
|
||||||
label: 'search_filter_date'.t(context: context),
|
label: 'search_filter_date'.t(context: context),
|
||||||
currentFilter: dateRangeCurrentFilterWidget.value,
|
currentFilter: dateRangeCurrentFilterWidget.value,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
|
||||||
|
sealed class DateFilterInputModel {
|
||||||
|
DateTimeRange<DateTime> asDateTimeRange();
|
||||||
|
|
||||||
|
String asHumanReadable(BuildContext context) {
|
||||||
|
// General implementation for arbitrary date and time ranges
|
||||||
|
// If date range is less than 24 hours, set the end date to the end of the day
|
||||||
|
final date = asDateTimeRange();
|
||||||
|
if (date.end.difference(date.start).inHours < 24) {
|
||||||
|
return DateFormat.yMMMd().format(date.start.toLocal());
|
||||||
|
} else {
|
||||||
|
return 'search_filter_date_interval'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
"start": DateFormat.yMMMd().format(date.start.toLocal()),
|
||||||
|
"end": DateFormat.yMMMd().format(date.end.toLocal()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecentMonthRangeFilter extends DateFilterInputModel {
|
||||||
|
final int monthDelta;
|
||||||
|
RecentMonthRangeFilter(this.monthDelta);
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTimeRange<DateTime> asDateTimeRange() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
// Note that DateTime's constructor properly handles month overflow.
|
||||||
|
final from = DateTime(now.year, now.month - monthDelta, 1);
|
||||||
|
return DateTimeRange<DateTime>(start: from, end: now);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String asHumanReadable(BuildContext context) {
|
||||||
|
return 'last_months'.t(context: context, args: {"count": monthDelta.toString()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class YearFilter extends DateFilterInputModel {
|
||||||
|
final int year;
|
||||||
|
YearFilter(this.year);
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTimeRange<DateTime> asDateTimeRange() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final from = DateTime(year, 1, 1);
|
||||||
|
|
||||||
|
if (now.year == year) {
|
||||||
|
// To not go beyond today if the user picks the current year
|
||||||
|
return DateTimeRange<DateTime>(start: from, end: now);
|
||||||
|
}
|
||||||
|
|
||||||
|
final to = DateTime(year, 12, 31, 23, 59, 59);
|
||||||
|
return DateTimeRange<DateTime>(start: from, end: to);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String asHumanReadable(BuildContext context) {
|
||||||
|
return 'in_year'.tr(namedArgs: {"year": year.toString()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomDateFilter extends DateFilterInputModel {
|
||||||
|
final DateTime start;
|
||||||
|
final DateTime end;
|
||||||
|
|
||||||
|
CustomDateFilter(this.start, this.end);
|
||||||
|
|
||||||
|
factory CustomDateFilter.fromRange(DateTimeRange<DateTime> range) {
|
||||||
|
return CustomDateFilter(range.start, range.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTimeRange<DateTime> asDateTimeRange() {
|
||||||
|
return DateTimeRange<DateTime>(start: start, end: end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _QuickPickerType { last1Month, last3Months, last9Months, year, custom }
|
||||||
|
|
||||||
|
class QuickDatePicker extends HookWidget {
|
||||||
|
QuickDatePicker({super.key, required this.currentInput, required this.onSelect, required this.onRequestPicker})
|
||||||
|
: _selection = _selectionFromModel(currentInput),
|
||||||
|
_initialYear = _initialYearFromModel(currentInput);
|
||||||
|
|
||||||
|
final Function() onRequestPicker;
|
||||||
|
final Function(DateFilterInputModel range) onSelect;
|
||||||
|
|
||||||
|
final DateFilterInputModel? currentInput;
|
||||||
|
final _QuickPickerType? _selection;
|
||||||
|
final int _initialYear;
|
||||||
|
|
||||||
|
// Generate a list of recent years from 2000 to the current year (including the current one)
|
||||||
|
final List<int> _recentYears = List.generate(1 + DateTime.now().year - 2000, (index) {
|
||||||
|
return index + 2000;
|
||||||
|
});
|
||||||
|
|
||||||
|
static int _initialYearFromModel(DateFilterInputModel? model) {
|
||||||
|
return model?.asDateTimeRange().start.year ?? DateTime.now().year;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _QuickPickerType? _selectionFromModel(DateFilterInputModel? model) {
|
||||||
|
if (model is RecentMonthRangeFilter) {
|
||||||
|
return switch (model.monthDelta) {
|
||||||
|
1 => _QuickPickerType.last1Month,
|
||||||
|
3 => _QuickPickerType.last3Months,
|
||||||
|
9 => _QuickPickerType.last9Months,
|
||||||
|
_ => _QuickPickerType.custom,
|
||||||
|
};
|
||||||
|
} else if (model is YearFilter) {
|
||||||
|
return _QuickPickerType.year;
|
||||||
|
} else if (model is CustomDateFilter) {
|
||||||
|
return _QuickPickerType.custom;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text _monthLabel(BuildContext context, int monthsFromNow) =>
|
||||||
|
const Text('last_months').t(context: context, args: {"count": monthsFromNow.toString()});
|
||||||
|
|
||||||
|
Widget _yearPicker(BuildContext context) {
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const Text("in_year_selector").tr(),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
Expanded(
|
||||||
|
child: DropdownMenu(
|
||||||
|
initialSelection: _initialYear,
|
||||||
|
menuStyle: MenuStyle(maximumSize: WidgetStateProperty.all(Size(size.width, size.height * 0.5))),
|
||||||
|
dropdownMenuEntries: _recentYears.map((e) => DropdownMenuEntry(value: e, label: e.toString())).toList(),
|
||||||
|
onSelected: (year) {
|
||||||
|
if (year == null) return;
|
||||||
|
onSelect(YearFilter(year));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want the exact date picker to always be selectable.
|
||||||
|
// Even if it's already toggled it should always open the full date picker, RadioListTiles don't do that by default
|
||||||
|
// so we wrap it in a InkWell
|
||||||
|
Widget _exactPicker(BuildContext context) {
|
||||||
|
final hasPreviousInput = currentInput != null && currentInput is CustomDateFilter;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: onRequestPicker,
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: true,
|
||||||
|
child: RadioListTile(
|
||||||
|
title: const Text('pick_custom_range').tr(),
|
||||||
|
subtitle: hasPreviousInput ? Text(currentInput!.asHumanReadable(context)) : null,
|
||||||
|
secondary: hasPreviousInput ? const Icon(Icons.edit) : null,
|
||||||
|
value: _QuickPickerType.custom,
|
||||||
|
toggleable: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||||
|
child: Scrollbar(
|
||||||
|
// Depending on the screen size the last option might get cut off
|
||||||
|
// Add a clear visual cue that there are more options when scrolling
|
||||||
|
// When the screen size is large enough the scrollbar is hidden automatically
|
||||||
|
trackVisibility: true,
|
||||||
|
thumbVisibility: true,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: RadioGroup(
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
final _ = switch (value) {
|
||||||
|
_QuickPickerType.custom => onRequestPicker(),
|
||||||
|
_QuickPickerType.last1Month => onSelect(RecentMonthRangeFilter(1)),
|
||||||
|
_QuickPickerType.last3Months => onSelect(RecentMonthRangeFilter(3)),
|
||||||
|
_QuickPickerType.last9Months => onSelect(RecentMonthRangeFilter(9)),
|
||||||
|
// When a year is selected the combobox triggers onSelect() on its own.
|
||||||
|
// Here we handle the radio button being selected which can only ever be the initial year
|
||||||
|
_QuickPickerType.year => onSelect(YearFilter(_initialYear)),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
groupValue: _selection,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
RadioListTile(title: _monthLabel(context, 1), value: _QuickPickerType.last1Month, toggleable: true),
|
||||||
|
RadioListTile(title: _monthLabel(context, 3), value: _QuickPickerType.last3Months, toggleable: true),
|
||||||
|
RadioListTile(title: _monthLabel(context, 9), value: _QuickPickerType.last9Months, toggleable: true),
|
||||||
|
RadioListTile(title: _yearPicker(context), value: _QuickPickerType.year, toggleable: true),
|
||||||
|
_exactPicker(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ class FilterBottomSheetScaffold extends StatelessWidget {
|
||||||
const FilterBottomSheetScaffold({
|
const FilterBottomSheetScaffold({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.onSearch,
|
this.onSearch,
|
||||||
required this.onClear,
|
required this.onClear,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.expanded,
|
this.expanded,
|
||||||
|
|
@ -15,7 +15,7 @@ class FilterBottomSheetScaffold extends StatelessWidget {
|
||||||
final bool? expanded;
|
final bool? expanded;
|
||||||
final String title;
|
final String title;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Function() onSearch;
|
final Function()? onSearch;
|
||||||
final Function() onClear;
|
final Function() onClear;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -48,15 +48,16 @@ class FilterBottomSheetScaffold extends StatelessWidget {
|
||||||
},
|
},
|
||||||
child: const Text('clear').tr(),
|
child: const Text('clear').tr(),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
if (onSearch != null) const SizedBox(width: 8),
|
||||||
ElevatedButton(
|
if (onSearch != null)
|
||||||
key: const Key('search_filter_apply'),
|
ElevatedButton(
|
||||||
onPressed: () {
|
key: const Key('search_filter_apply'),
|
||||||
onSearch();
|
onPressed: () {
|
||||||
context.pop();
|
onSearch!();
|
||||||
},
|
context.pop();
|
||||||
child: const Text('search_filter_apply').tr(),
|
},
|
||||||
),
|
child: const Text('search_filter_apply').tr(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue