mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
Wrap the quick date picker state into its own class, improve the interaction patterns
This commit is contained in:
parent
78467c078e
commit
0a32c50211
3 changed files with 206 additions and 75 deletions
|
|
@ -1175,6 +1175,7 @@
|
|||
"in_albums": "In {count, plural, one {# album} other {# albums}}",
|
||||
"in_archive": "In archive",
|
||||
"in_year": "In {year}",
|
||||
"in_year_selector": "In",
|
||||
"include_archived": "Include archived",
|
||||
"include_shared_albums": "Include shared albums",
|
||||
"include_shared_partner_assets": "Include shared partner assets",
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||
);
|
||||
|
||||
final previousFilter = useState<SearchFilter?>(null);
|
||||
final dateInputFilter = useState<DateFilterInputModel?>(null);
|
||||
|
||||
final peopleCurrentFilterWidget = useState<Widget?>(null);
|
||||
final dateRangeCurrentFilterWidget = useState<Widget?>(null);
|
||||
|
|
@ -243,8 +244,9 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
datePicked(DateTimeRange<DateTime>? date) {
|
||||
if (date == null) {
|
||||
datePicked(DateFilterInputModel? selectedDate) {
|
||||
dateInputFilter.value = selectedDate;
|
||||
if (selectedDate == null) {
|
||||
filter.value = filter.value.copyWith(date: SearchDateFilter());
|
||||
|
||||
dateRangeCurrentFilterWidget.value = null;
|
||||
|
|
@ -252,6 +254,8 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||
return;
|
||||
}
|
||||
|
||||
final date = selectedDate.asDateTimeRange();
|
||||
|
||||
filter.value = filter.value.copyWith(
|
||||
date: SearchDateFilter(
|
||||
takenAfter: date.start,
|
||||
|
|
@ -259,24 +263,10 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||
),
|
||||
);
|
||||
|
||||
// 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 {
|
||||
dateRangeCurrentFilterWidget.value = Text(
|
||||
'search_filter_date_interval'.t(
|
||||
context: context,
|
||||
args: {
|
||||
"start": DateFormat.yMMMd().format(date.start.toLocal()),
|
||||
"end": DateFormat.yMMMd().format(date.end.toLocal()),
|
||||
},
|
||||
),
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
}
|
||||
dateRangeCurrentFilterWidget.value = Text(
|
||||
selectedDate.asHumanReadable(context),
|
||||
style: context.textTheme.labelLarge,
|
||||
);
|
||||
|
||||
search();
|
||||
}
|
||||
|
|
@ -314,7 +304,11 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||
keyboardType: TextInputType.text,
|
||||
);
|
||||
|
||||
datePicked(date);
|
||||
if (date == null) {
|
||||
datePicked(null);
|
||||
} else {
|
||||
datePicked(CustomDateFilter.fromRange(date));
|
||||
}
|
||||
}
|
||||
|
||||
showQuickDatePicker() {
|
||||
|
|
@ -325,6 +319,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||
expanded: true,
|
||||
onClear: () => datePicked(null),
|
||||
child: QuickDatePicker(
|
||||
currentRange: dateInputFilter.value,
|
||||
onRequestPicker: () {
|
||||
context.pop();
|
||||
showDatePicker();
|
||||
|
|
|
|||
|
|
@ -3,68 +3,203 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
|
||||
class QuickDatePicker extends HookWidget {
|
||||
const QuickDatePicker({super.key, required this.onSelect, required this.onRequestPicker});
|
||||
sealed class DateFilterInputModel {
|
||||
DateTimeRange<DateTime> asDateTimeRange();
|
||||
|
||||
final Function() onRequestPicker;
|
||||
final Function(DateTimeRange<DateTime> range) onSelect;
|
||||
|
||||
void _selectRange(DateTimeRange range) {
|
||||
// Ensure we don't go beyond today, eg when picking "in $current_year"
|
||||
final now = DateTime.now();
|
||||
if (range.end.isAfter(now)) {
|
||||
range = DateTimeRange(start: range.start, end: now);
|
||||
String asHumanReadable(BuildContext context) {
|
||||
// Generail 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()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onSelect(range);
|
||||
}
|
||||
}
|
||||
|
||||
ListTile _monthListTile(BuildContext context, int monthsFromNow) {
|
||||
String label = 'last_months'.t(context: context, args: {"count": monthsFromNow.toString()});
|
||||
return ListTile(
|
||||
title: Text(label),
|
||||
onTap: () {
|
||||
final now = DateTime.now();
|
||||
// We use the first of the target month here to avoid issues with different month lengths
|
||||
// the negative overflow of months is handled by DateTime correctly
|
||||
final from = DateTime(now.year, now.month - monthsFromNow, 1);
|
||||
_selectRange(DateTimeRange(start: from, end: now));
|
||||
},
|
||||
);
|
||||
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.currentRange, required this.onSelect, required this.onRequestPicker})
|
||||
: _selection = _inputModelToType(currentRange),
|
||||
_initialYear = _initialYearFromModel(currentRange);
|
||||
|
||||
final Function() onRequestPicker;
|
||||
final Function(DateFilterInputModel range) onSelect;
|
||||
final DateFilterInputModel? currentRange;
|
||||
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? _inputModelToType(DateFilterInputModel? model) {
|
||||
if (model is RecentMonthRangeFilter) {
|
||||
switch (model.monthDelta) {
|
||||
case 1:
|
||||
return _QuickPickerType.last1Month;
|
||||
case 3:
|
||||
return _QuickPickerType.last3Months;
|
||||
case 6:
|
||||
return _QuickPickerType.last9Months;
|
||||
default:
|
||||
return _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 allow that by default so we wrap it in a GestureDetector
|
||||
Widget _exactPicker(BuildContext context) => InkWell(
|
||||
onTap: onRequestPicker,
|
||||
child: IgnorePointer(
|
||||
ignoring: true,
|
||||
child: RadioListTile(title: const Text('pick_exact_date').tr(), value: _QuickPickerType.custom, toggleable: true),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return ListTile(
|
||||
title: Text('pick_exact_date'.tr()),
|
||||
onTap: () {
|
||||
onRequestPicker();
|
||||
},
|
||||
);
|
||||
} else if (index == 1) {
|
||||
return _monthListTile(context, 1);
|
||||
} else if (index == 2) {
|
||||
return _monthListTile(context, 3);
|
||||
} else if (index == 3) {
|
||||
return _monthListTile(context, 9);
|
||||
} else {
|
||||
final now = DateTime.now();
|
||||
final years = index - 4;
|
||||
final year = now.year - years;
|
||||
return ListTile(
|
||||
title: Text("in_year".tr(namedArgs: {"year": year.toString()})),
|
||||
onTap: () {
|
||||
final from = DateTime(year, 1, 1);
|
||||
final to = DateTime(year, 12, 31, 23, 59, 59);
|
||||
_selectRange(DateTimeRange(start: from, end: to));
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
itemCount: 50,
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: SingleChildScrollView(
|
||||
clipBehavior: Clip.none,
|
||||
child: RadioGroup(
|
||||
onChanged: (value) {
|
||||
switch (value) {
|
||||
case _QuickPickerType.custom:
|
||||
onRequestPicker();
|
||||
break;
|
||||
case _QuickPickerType.last1Month:
|
||||
onSelect(RecentMonthRangeFilter(1));
|
||||
break;
|
||||
case _QuickPickerType.last3Months:
|
||||
onSelect(RecentMonthRangeFilter(3));
|
||||
break;
|
||||
case _QuickPickerType.last9Months:
|
||||
onSelect(RecentMonthRangeFilter(9));
|
||||
break;
|
||||
case _QuickPickerType.year:
|
||||
// The combobox triggers the onSelect event on its own so if this is ever selected it can only be on
|
||||
// the default value set by the constructor
|
||||
onSelect(YearFilter(_initialYear));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue