Filter photos by date in map browser

This commit is contained in:
Ming Ming 2024-07-15 01:42:39 +08:00
parent 877bed1640
commit 17e7aa55f3
10 changed files with 637 additions and 30 deletions

View file

@ -1487,6 +1487,14 @@
"trustedCertManagerFailedToRemoveCertError": "Failed to remove certificate",
"missingVideoThumbnailHelpDialogTitle": "Having trouble with video thumbnails?",
"dontShowAgain": "Don't show again",
"mapBrowserDateRangeLabel": "Date range",
"@mapBrowserDateRangeLabel": {
"description": "Filter photos by date range"
},
"mapBrowserDateRangeThisMonth": "This month",
"mapBrowserDateRangePrevMonth": "Previous month",
"mapBrowserDateRangeThisYear": "This year",
"mapBrowserDateRangeCustom": "Custom",
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": {

View file

@ -249,6 +249,11 @@
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom",
"errorUnauthenticated",
"errorDisconnected",
"errorLocked",
@ -284,7 +289,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"de": [
@ -318,7 +328,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"el": [
@ -455,7 +470,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"es": [
@ -483,7 +503,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"fi": [
@ -511,7 +536,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"fr": [
@ -539,7 +569,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"it": [
@ -572,7 +607,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"nl": [
@ -942,6 +982,11 @@
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom",
"errorUnauthenticated",
"errorDisconnected",
"errorLocked",
@ -981,7 +1026,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"pt": [
@ -1029,7 +1079,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"ru": [
@ -1057,7 +1112,20 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"tr": [
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"zh": [
@ -1116,7 +1184,12 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
],
"zh_Hant": [
@ -1269,6 +1342,11 @@
"trustedCertManagerNoHttpsServerError",
"trustedCertManagerFailedToRemoveCertError",
"missingVideoThumbnailHelpDialogTitle",
"dontShowAgain"
"dontShowAgain",
"mapBrowserDateRangeLabel",
"mapBrowserDateRangeThisMonth",
"mapBrowserDateRangePrevMonth",
"mapBrowserDateRangeThisYear",
"mapBrowserDateRangeCustom"
]
}

View file

@ -1,16 +1,18 @@
import 'dart:async';
import 'dart:ui';
import 'package:clock/clock.dart';
import 'package:copy_with/copy_with.dart';
import 'package:flex_seed_scheme/flex_seed_scheme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intl/intl.dart' as intl;
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/di_container.dart';
@ -18,9 +20,14 @@ import 'package:nc_photos/entity/collection.dart';
import 'package:nc_photos/entity/collection/content_provider/ad_hoc.dart';
import 'package:nc_photos/entity/image_location/repo.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/stream_extension.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/widget/collection_browser.dart';
import 'package:nc_photos/widget/measure.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_datetime/np_datetime.dart';
import 'package:to_string/to_string.dart';
part 'map_browser.g.dart';
@ -44,7 +51,7 @@ class MapBrowser extends StatelessWidget {
create: (_) => _Bloc(
KiwiContainer().resolve(),
account: context.read<AccountController>().account,
)..add(const _Init()),
)..add(const _LoadData()),
child: const _WrappedMapBrowser(),
);
}
@ -69,7 +76,42 @@ class _WrappedMapBrowser extends StatelessWidget {
},
),
],
child: const _MapView(),
child: Stack(
children: [
const _MapView(),
Positioned.directional(
textDirection: Directionality.of(context),
top: MediaQuery.of(context).padding.top + 8,
end: 8,
child: const _DateRangeToggle(),
),
_BlocSelector<bool>(
selector: (state) => state.isShowDataRangeControlPanel,
builder: (context, isShowAnyPanel) => Positioned.fill(
child: isShowAnyPanel
? GestureDetector(
onTap: () {
context.addEvent(const _CloseControlPanel());
},
)
: const SizedBox.shrink(),
),
),
Positioned(
left: 8,
right: 8,
top: MediaQuery.of(context).padding.top + 8,
child: _BlocSelector<bool>(
selector: (state) => state.isShowDataRangeControlPanel,
builder: (context, isShowDataRangeControlPanel) =>
_PanelContainer(
isShow: isShowDataRangeControlPanel,
child: const _DateRangeControlPanel(),
),
),
),
],
),
),
),
);
@ -79,7 +121,7 @@ class _WrappedMapBrowser extends StatelessWidget {
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
// typedef _BlocListener = BlocListener<_Bloc, _State>;
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
// typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
extension on BuildContext {
_Bloc get bloc => read<_Bloc>();

View file

@ -17,6 +17,9 @@ abstract class $_StateCopyWithWorker {
{List<_DataPoint>? data,
LatLng? initialPoint,
Set<Marker>? markers,
bool? isShowDataRangeControlPanel,
_DateRangeType? dateRangeType,
DateRange? localDateRange,
ExceptionEvent? error});
}
@ -28,6 +31,9 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
{dynamic data,
dynamic initialPoint = copyWithNull,
dynamic markers,
dynamic isShowDataRangeControlPanel,
dynamic dateRangeType,
dynamic localDateRange,
dynamic error = copyWithNull}) {
return _State(
data: data as List<_DataPoint>? ?? that.data,
@ -35,6 +41,10 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
? that.initialPoint
: initialPoint as LatLng?,
markers: markers as Set<Marker>? ?? that.markers,
isShowDataRangeControlPanel: isShowDataRangeControlPanel as bool? ??
that.isShowDataRangeControlPanel,
dateRangeType: dateRangeType as _DateRangeType? ?? that.dateRangeType,
localDateRange: localDateRange as DateRange? ?? that.localDateRange,
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
}
@ -64,14 +74,14 @@ extension _$_BlocNpLog on _Bloc {
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {data: [length: ${data.length}], initialPoint: $initialPoint, markers: {length: ${markers.length}}, error: $error}";
return "_State {data: [length: ${data.length}], initialPoint: $initialPoint, markers: {length: ${markers.length}}, isShowDataRangeControlPanel: $isShowDataRangeControlPanel, dateRangeType: ${dateRangeType.name}, localDateRange: $localDateRange, error: $error}";
}
}
extension _$_InitToString on _Init {
extension _$_LoadDataToString on _LoadData {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Init {}";
return "_LoadData {}";
}
}
@ -82,6 +92,34 @@ extension _$_SetMarkersToString on _SetMarkers {
}
}
extension _$_OpenDataRangeControlPanelToString on _OpenDataRangeControlPanel {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_OpenDataRangeControlPanel {}";
}
}
extension _$_CloseControlPanelToString on _CloseControlPanel {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_CloseControlPanel {}";
}
}
extension _$_SetDateRangeTypeToString on _SetDateRangeType {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetDateRangeType {value: ${value.name}}";
}
}
extension _$_SetLocalDateRangeToString on _SetLocalDateRange {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetLocalDateRange {value: $value}";
}
}
extension _$_SetErrorToString on _SetError {
String _$toString() {
// ignore: unnecessary_string_interpolations

View file

@ -6,10 +6,31 @@ class _Bloc extends Bloc<_Event, _State>
_Bloc(
this._c, {
required this.account,
}) : super(_State.init()) {
on<_Init>(_onInit);
}) : super(_State.init(
dateRangeType: _DateRangeType.thisMonth,
localDateRange:
_calcDateRange(clock.now().toDate(), _DateRangeType.thisMonth),
)) {
on<_LoadData>(_onLoadData);
on<_SetMarkers>(_onSetMarkers);
on<_OpenDataRangeControlPanel>(_onOpenDataRangeControlPanel);
on<_CloseControlPanel>(_onCloseControlPanel);
on<_SetDateRangeType>(_onSetDateRangeType);
on<_SetLocalDateRange>(_onSetDateRange);
on<_SetError>(_onSetError);
_subscriptions
.add(stream.distinctBy((state) => state.localDateRange).listen((state) {
add(const _LoadData());
}));
}
@override
Future<void> close() {
for (final s in _subscriptions) {
s.cancel();
}
return super.close();
}
@override
@ -28,10 +49,16 @@ class _Bloc extends Bloc<_Event, _State>
super.onError(error, stackTrace);
}
Future<void> _onInit(_Init ev, Emitter<_State> emit) async {
Future<void> _onLoadData(_LoadData ev, Emitter<_State> emit) async {
_log.info(ev);
final raw = await _c.imageLocationRepo.getLocations(account);
_log.info("[_onInit] Loaded ${raw.length} markers");
// convert local DateRange to TimeRange in UTC
final localTimeRange = state.localDateRange.toLocalTimeRange();
final utcTimeRange = localTimeRange.copyWith(
from: localTimeRange.from?.toUtc(),
to: localTimeRange.to?.toUtc(),
);
final raw = await _c.imageLocationRepo.getLocations(account, utcTimeRange);
_log.info("[_onLoadData] Loaded ${raw.length} markers");
emit(state.copyWith(
data: raw.map(_DataPoint.fromImageLatLng).toList(),
initialPoint: state.initialPoint ??
@ -46,13 +73,86 @@ class _Bloc extends Bloc<_Event, _State>
emit(state.copyWith(markers: ev.markers));
}
void _onOpenDataRangeControlPanel(
_OpenDataRangeControlPanel ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(
isShowDataRangeControlPanel: true,
));
}
void _onCloseControlPanel(_CloseControlPanel ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(
isShowDataRangeControlPanel: false,
));
}
void _onSetDateRangeType(_SetDateRangeType ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(
dateRangeType: ev.value,
localDateRange: ev.value == _DateRangeType.custom
? null
: _calcDateRange(clock.now().toDate(), ev.value),
));
}
void _onSetDateRange(_SetLocalDateRange ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(
dateRangeType: _DateRangeType.custom,
localDateRange: ev.value,
));
}
void _onSetError(_SetError ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace)));
}
static DateRange _calcDateRange(Date today, _DateRangeType type) {
assert(type != _DateRangeType.custom);
switch (type) {
case _DateRangeType.thisMonth:
return DateRange(
from: today.copyWith(day: 1),
to: today,
toBound: TimeRangeBound.inclusive,
);
case _DateRangeType.prevMonth:
if (today.month == 1) {
return DateRange(
from: Date(today.year - 1, 12, 1),
to: Date(today.year - 1, 12, 31),
toBound: TimeRangeBound.inclusive,
);
} else {
return DateRange(
from: Date(today.year, today.month - 1, 1),
to: Date(today.year, today.month, 1).add(day: -1),
toBound: TimeRangeBound.inclusive,
);
}
case _DateRangeType.thisYear:
return DateRange(
from: today.copyWith(month: 1, day: 1),
to: today,
toBound: TimeRangeBound.inclusive,
);
case _DateRangeType.custom:
return DateRange(
from: today,
to: today,
toBound: TimeRangeBound.inclusive,
);
}
}
final DiContainer _c;
final Account account;
final _subscriptions = <StreamSubscription>[];
var _isHandlingError = false;
}

View file

@ -7,13 +7,22 @@ class _State {
required this.data,
this.initialPoint,
required this.markers,
required this.isShowDataRangeControlPanel,
required this.dateRangeType,
required this.localDateRange,
this.error,
});
factory _State.init() {
return const _State(
data: [],
markers: {},
factory _State.init({
required _DateRangeType dateRangeType,
required DateRange localDateRange,
}) {
return _State(
data: const [],
markers: const {},
isShowDataRangeControlPanel: false,
dateRangeType: dateRangeType,
localDateRange: localDateRange,
);
}
@ -24,6 +33,10 @@ class _State {
final LatLng? initialPoint;
final Set<Marker> markers;
final bool isShowDataRangeControlPanel;
final _DateRangeType dateRangeType;
final DateRange localDateRange;
final ExceptionEvent? error;
}
@ -32,8 +45,8 @@ abstract class _Event {
}
@toString
class _Init implements _Event {
const _Init();
class _LoadData implements _Event {
const _LoadData();
@override
String toString() => _$toString();
@ -49,6 +62,42 @@ class _SetMarkers implements _Event {
final Set<Marker> markers;
}
@toString
class _OpenDataRangeControlPanel implements _Event {
const _OpenDataRangeControlPanel();
@override
String toString() => _$toString();
}
@toString
class _CloseControlPanel implements _Event {
const _CloseControlPanel();
@override
String toString() => _$toString();
}
@toString
class _SetDateRangeType implements _Event {
const _SetDateRangeType(this.value);
@override
String toString() => _$toString();
final _DateRangeType value;
}
@toString
class _SetLocalDateRange implements _Event {
const _SetLocalDateRange(this.value);
@override
String toString() => _$toString();
final DateRange value;
}
@toString
class _SetError implements _Event {
const _SetError(this.error, [this.stackTrace]);

View file

@ -19,3 +19,24 @@ class _DataPoint implements ClusterItem {
final LatLng location;
final int fileId;
}
enum _DateRangeType {
thisMonth,
prevMonth,
thisYear,
custom,
;
String toDisplayString() {
switch (this) {
case thisMonth:
return L10n.global().mapBrowserDateRangeThisMonth;
case prevMonth:
return L10n.global().mapBrowserDateRangePrevMonth;
case thisYear:
return L10n.global().mapBrowserDateRangeThisYear;
case custom:
return L10n.global().mapBrowserDateRangeCustom;
}
}
}

View file

@ -200,3 +200,242 @@ class _MapViewState extends State<_MapView> {
late final _colorHsl =
HSLColor.fromColor(Theme.of(context).colorScheme.primaryContainer);
}
class _PanelContainer extends StatefulWidget {
const _PanelContainer({
required this.isShow,
required this.child,
});
@override
State<StatefulWidget> createState() => _PanelContainerState();
final bool isShow;
final Widget child;
}
class _PanelContainerState extends State<_PanelContainer>
with TickerProviderStateMixin {
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: k.animationDurationNormal,
vsync: this,
value: 0,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant _PanelContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.isShow != widget.isShow) {
if (widget.isShow) {
_animationController.animateTo(1);
} else {
_animationController.animateBack(0);
}
}
}
@override
Widget build(BuildContext context) {
return MatrixTransition(
animation: _animation,
onTransform: (animationValue) => Matrix4.identity()
..translate(0.0, -(_size.height / 2) * (1 - animationValue), 0.0)
..scale(1.0, animationValue, 1.0),
child: MeasureSize(
onChange: (size) => setState(() {
_size = size;
}),
child: widget.child,
),
);
}
late AnimationController _animationController;
late Animation<double> _animation;
var _size = Size.zero;
}
class _DateRangeToggle extends StatelessWidget {
const _DateRangeToggle();
@override
Widget build(BuildContext context) {
return FloatingActionButton.small(
onPressed: () {
context.addEvent(const _OpenDataRangeControlPanel());
},
child: const Icon(Icons.date_range_outlined),
);
}
}
class _DateRangeControlPanel extends StatelessWidget {
const _DateRangeControlPanel();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.elevate(theme.colorScheme.surface, 2),
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
blurRadius: 4,
offset: Offset(0, 2),
color: Colors.black26,
),
],
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Text(
L10n.global().mapBrowserDateRangeLabel,
style: Theme.of(context).listTileTheme.titleTextStyle,
),
),
Expanded(
child: _BlocSelector<_DateRangeType>(
selector: (state) => state.dateRangeType,
builder: (context, dateRangeType) =>
DropdownButtonFormField<_DateRangeType>(
items: _DateRangeType.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.toDisplayString()),
))
.toList(),
value: dateRangeType,
onChanged: (value) {
if (value != null) {
context.addEvent(_SetDateRangeType(value));
}
},
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: _BlocSelector<DateRange>(
selector: (state) => state.localDateRange,
builder: (context, localDateRange) => _DateField(
localDateRange.from!,
onChanged: (value) {
context.addEvent(_SetLocalDateRange(
localDateRange.copyWith(from: value.toDate())));
},
),
),
),
const Text(" - "),
Expanded(
child: _BlocSelector<DateRange>(
selector: (state) => state.localDateRange,
builder: (context, localDateRange) => _DateField(
localDateRange.to!,
onChanged: (value) {
context.addEvent(_SetLocalDateRange(
localDateRange.copyWith(to: value.toDate())));
},
),
),
),
],
),
const SizedBox(height: 8),
],
),
),
);
}
}
class _DateField extends StatefulWidget {
const _DateField(
this.date, {
this.onChanged,
});
@override
State<StatefulWidget> createState() => _DateFieldState();
final Date date;
final ValueChanged<DateTime>? onChanged;
}
class _DateFieldState extends State<_DateField> {
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
void didUpdateWidget(covariant _DateField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.date != oldWidget.date) {
_controller.text = _stringify(widget.date);
}
}
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
final result = await showDatePicker(
context: context,
firstDate: DateTime(1970),
lastDate: clock.now(),
currentDate: widget.date.toLocalDateTime(),
);
if (result == null) {
return;
}
widget.onChanged?.call(result);
},
child: IgnorePointer(
child: ExcludeFocus(
child: TextFormField(
controller: _controller,
),
),
),
),
);
}
String _stringify(Date date) {
return intl.DateFormat(intl.DateFormat.YEAR_ABBR_MONTH_DAY,
Localizations.localeOf(context).languageCode)
.format(date.toLocalDateTime());
}
late final _controller = TextEditingController(text: _stringify(widget.date));
}

View file

@ -9,6 +9,22 @@ class DateRange {
this.toBound = TimeRangeBound.exclusive,
});
/// Return a copy of the current instance with some changed fields. Setting
/// null is not supported
DateRange copyWith({
Date? from,
TimeRangeBound? fromBound,
Date? to,
TimeRangeBound? toBound,
}) {
return DateRange(
from: from ?? this.from,
fromBound: fromBound ?? this.fromBound,
to: to ?? this.to,
toBound: toBound ?? this.toBound,
);
}
@override
String toString() {
return "${fromBound == TimeRangeBound.inclusive ? "[" : "("}"

View file

@ -11,6 +11,22 @@ class TimeRange {
this.toBound = TimeRangeBound.exclusive,
});
/// Return a copy of the current instance with some changed fields. Setting
/// null is not supported
TimeRange copyWith({
DateTime? from,
TimeRangeBound? fromBound,
DateTime? to,
TimeRangeBound? toBound,
}) {
return TimeRange(
from: from ?? this.from,
fromBound: fromBound ?? this.fromBound,
to: to ?? this.to,
toBound: toBound ?? this.toBound,
);
}
@override
String toString() {
return "${fromBound == TimeRangeBound.inclusive ? "[" : "("}"