From d4d0160642c7a6aae7f39f604489c6f4b0b84d73 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 24 Aug 2024 19:33:21 +0800 Subject: [PATCH] Allow customizing map browser default time range --- app/lib/controller/pref_controller.dart | 11 ++ app/lib/controller/pref_controller.g.dart | 9 + app/lib/controller/pref_controller/type.dart | 15 ++ app/lib/controller/pref_controller/util.dart | 6 + app/lib/entity/pref.dart | 3 + app/lib/l10n/app_en.arb | 1 + app/lib/l10n/untranslated-messages.txt | 42 +++-- app/lib/widget/map_browser.g.dart | 20 ++- app/lib/widget/map_browser/bloc.dart | 30 +++- app/lib/widget/map_browser/state_event.dart | 22 +++ app/lib/widget/map_browser/type.dart | 24 +++ app/lib/widget/map_browser/view.dart | 178 ++++++++++++------- 12 files changed, 281 insertions(+), 80 deletions(-) create mode 100644 app/lib/controller/pref_controller/type.dart diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index 62f2ae49..8de8a75d 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -19,6 +19,7 @@ import 'package:np_string/np_string.dart'; import 'package:rxdart/rxdart.dart'; part 'pref_controller.g.dart'; +part 'pref_controller/type.dart'; part 'pref_controller/util.dart'; @npSubjectAccessor @@ -196,6 +197,13 @@ class PrefController { value: value, ); + Future setMapDefaultRangeType(PrefMapDefaultRangeType value) => + _set( + controller: _mapDefaultRangeTypeController, + setter: (pref, value) => pref.setMapDefaultRangeType(value), + value: value, + ); + Future _set({ required BehaviorSubject controller, required Future Function(Pref pref, T value) setter, @@ -317,6 +325,9 @@ class PrefController { BehaviorSubject.seeded(pref.getLastVersion() ?? // v6 is the last version without saving the version number in pref (pref.getSetupProgress() == null ? k.version : 6)); + @npSubjectAccessor + late final _mapDefaultRangeTypeController = BehaviorSubject.seeded( + pref.getMapDefaultRangeType() ?? PrefMapDefaultRangeType.thisMonth); } extension PrefControllerExtension on PrefController { diff --git a/app/lib/controller/pref_controller.g.dart b/app/lib/controller/pref_controller.g.dart index 769cef33..dc0ebdfe 100644 --- a/app/lib/controller/pref_controller.g.dart +++ b/app/lib/controller/pref_controller.g.dart @@ -188,6 +188,15 @@ extension $PrefControllerNpSubjectAccessor on PrefController { Stream get lastVersionNew => lastVersion.skip(1); Stream get lastVersionChange => lastVersion.distinct().skip(1); int get lastVersionValue => _lastVersionController.value; +// _mapDefaultRangeTypeController + ValueStream get mapDefaultRangeType => + _mapDefaultRangeTypeController.stream; + Stream get mapDefaultRangeTypeNew => + mapDefaultRangeType.skip(1); + Stream get mapDefaultRangeTypeChange => + mapDefaultRangeType.distinct().skip(1); + PrefMapDefaultRangeType get mapDefaultRangeTypeValue => + _mapDefaultRangeTypeController.value; } extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController { diff --git a/app/lib/controller/pref_controller/type.dart b/app/lib/controller/pref_controller/type.dart new file mode 100644 index 00000000..d327a556 --- /dev/null +++ b/app/lib/controller/pref_controller/type.dart @@ -0,0 +1,15 @@ +part of '../pref_controller.dart'; + +enum PrefMapDefaultRangeType { + thisMonth(0), + prevMonth(1), + thisYear(2), + ; + + const PrefMapDefaultRangeType(this.value); + + static PrefMapDefaultRangeType fromValue(int value) => + PrefMapDefaultRangeType.values[value]; + + final int value; +} diff --git a/app/lib/controller/pref_controller/util.dart b/app/lib/controller/pref_controller/util.dart index 91889fde..64f0b395 100644 --- a/app/lib/controller/pref_controller/util.dart +++ b/app/lib/controller/pref_controller/util.dart @@ -119,6 +119,12 @@ extension on Pref { return provider.setInt(PrefKey.currentAccountIndex, value); } } + + PrefMapDefaultRangeType? getMapDefaultRangeType() => provider + .getInt(PrefKey.mapDefaultRangeType) + ?.let(PrefMapDefaultRangeType.fromValue); + Future setMapDefaultRangeType(PrefMapDefaultRangeType value) => + provider.setInt(PrefKey.mapDefaultRangeType, value.value); } MapCoord? _tryMapCoordFromJson(dynamic json) { diff --git a/app/lib/entity/pref.dart b/app/lib/entity/pref.dart index f7def3b1..668d2651 100644 --- a/app/lib/entity/pref.dart +++ b/app/lib/entity/pref.dart @@ -115,6 +115,7 @@ enum PrefKey implements PrefKeyInterface { dontShowVideoPreviewHint, mapBrowserPrevPosition, isNewHttpEngine, + mapDefaultRangeType, ; @override @@ -205,6 +206,8 @@ enum PrefKey implements PrefKeyInterface { return "mapBrowserPrevPosition"; case PrefKey.isNewHttpEngine: return "isNewHttpEngine"; + case PrefKey.mapDefaultRangeType: + return "mapDefaultRangeType"; } } } diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 7884d944..2112f61f 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1499,6 +1499,7 @@ "mapBrowserDateRangeThisYear": "This year", "mapBrowserDateRangeCustom": "Custom", "homeTabMapBrowser": "Map", + "mapBrowserSetDefaultDateRangeButton": "Set as default", "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index be55572c..c9216129 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -258,6 +258,7 @@ "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -302,7 +303,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "de": [ @@ -345,7 +347,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "el": [ @@ -491,7 +494,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "es": [ @@ -528,7 +532,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "fi": [ @@ -565,7 +570,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "fr": [ @@ -602,7 +608,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "it": [ @@ -644,7 +651,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "nl": [ @@ -1023,6 +1031,7 @@ "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -1071,7 +1080,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "pt": [ @@ -1128,7 +1138,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "ru": [ @@ -1165,7 +1176,12 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" + ], + + "tr": [ + "mapBrowserSetDefaultDateRangeButton" ], "zh": [ @@ -1233,7 +1249,8 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ], "zh_Hant": [ @@ -1395,6 +1412,7 @@ "mapBrowserDateRangePrevMonth", "mapBrowserDateRangeThisYear", "mapBrowserDateRangeCustom", - "homeTabMapBrowser" + "homeTabMapBrowser", + "mapBrowserSetDefaultDateRangeButton" ] } diff --git a/app/lib/widget/map_browser.g.dart b/app/lib/widget/map_browser.g.dart index 35fe8b8d..064aeb73 100644 --- a/app/lib/widget/map_browser.g.dart +++ b/app/lib/widget/map_browser.g.dart @@ -19,6 +19,7 @@ abstract class $_StateCopyWithWorker { bool? isShowDataRangeControlPanel, _DateRangeType? dateRangeType, DateRange? localDateRange, + _DateRangeType? prefDateRangeType, ExceptionEvent? error}); } @@ -32,6 +33,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { dynamic isShowDataRangeControlPanel, dynamic dateRangeType, dynamic localDateRange, + dynamic prefDateRangeType, dynamic error = copyWithNull}) { return _State( data: data as List<_DataPoint>? ?? that.data, @@ -42,6 +44,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { that.isShowDataRangeControlPanel, dateRangeType: dateRangeType as _DateRangeType? ?? that.dateRangeType, localDateRange: localDateRange as DateRange? ?? that.localDateRange, + prefDateRangeType: + prefDateRangeType as _DateRangeType? ?? that.prefDateRangeType, error: error == copyWithNull ? that.error : error as ExceptionEvent?); } @@ -85,7 +89,7 @@ extension _$_GoogleMarkerBitmapBuilderNpLog on _GoogleMarkerBitmapBuilder { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {data: [length: ${data.length}], initialPoint: $initialPoint, isShowDataRangeControlPanel: $isShowDataRangeControlPanel, dateRangeType: ${dateRangeType.name}, localDateRange: $localDateRange, error: $error}"; + return "_State {data: [length: ${data.length}], initialPoint: $initialPoint, isShowDataRangeControlPanel: $isShowDataRangeControlPanel, dateRangeType: ${dateRangeType.name}, localDateRange: $localDateRange, prefDateRangeType: ${prefDateRangeType.name}, error: $error}"; } } @@ -124,6 +128,20 @@ extension _$_SetLocalDateRangeToString on _SetLocalDateRange { } } +extension _$_SetPrefDateRangeTypeToString on _SetPrefDateRangeType { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetPrefDateRangeType {value: ${value.name}}"; + } +} + +extension _$_SetAsDefaultRangeToString on _SetAsDefaultRange { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetAsDefaultRange {}"; + } +} + extension _$_SetErrorToString on _SetError { String _$toString() { // ignore: unnecessary_string_interpolations diff --git a/app/lib/widget/map_browser/bloc.dart b/app/lib/widget/map_browser/bloc.dart index c1d55b42..e44e99e7 100644 --- a/app/lib/widget/map_browser/bloc.dart +++ b/app/lib/widget/map_browser/bloc.dart @@ -7,16 +7,14 @@ class _Bloc extends Bloc<_Event, _State> this._c, { required this.account, required this.prefController, - }) : super(_State.init( - dateRangeType: _DateRangeType.thisMonth, - localDateRange: - _calcDateRange(clock.now().toDate(), _DateRangeType.thisMonth), - )) { + }) : super(_Bloc._getInitialState(prefController)) { on<_LoadData>(_onLoadData); on<_OpenDataRangeControlPanel>(_onOpenDataRangeControlPanel); on<_CloseControlPanel>(_onCloseControlPanel); on<_SetDateRangeType>(_onSetDateRangeType); on<_SetLocalDateRange>(_onSetDateRange); + on<_SetPrefDateRangeType>(_onSetPrefDateRangeType); + on<_SetAsDefaultRange>(_onSetAsDefaultRange); on<_SetError>(_onSetError); _subscriptions.add(stream @@ -24,6 +22,9 @@ class _Bloc extends Bloc<_Event, _State> .listen((state) { add(const _LoadData()); })); + _subscriptions.add(prefController.mapDefaultRangeTypeChange.listen((state) { + add(_SetPrefDateRangeType(_DateRangeType.fromPref(state))); + })); } @override @@ -50,6 +51,15 @@ class _Bloc extends Bloc<_Event, _State> super.onError(error, stackTrace); } + static _State _getInitialState(PrefController prefController) { + final dateRangeType = + _DateRangeType.fromPref(prefController.mapDefaultRangeTypeValue); + return _State.init( + dateRangeType: dateRangeType, + localDateRange: _calcDateRange(clock.now().toDate(), dateRangeType), + ); + } + Future _onLoadData(_LoadData ev, Emitter<_State> emit) async { _log.info(ev); // convert local DateRange to TimeRange in UTC @@ -110,6 +120,16 @@ class _Bloc extends Bloc<_Event, _State> )); } + void _onSetPrefDateRangeType(_SetPrefDateRangeType ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(prefDateRangeType: ev.value)); + } + + void _onSetAsDefaultRange(_SetAsDefaultRange ev, Emitter<_State> emit) { + _log.info(ev); + prefController.setMapDefaultRangeType(state.dateRangeType.toPref()); + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); diff --git a/app/lib/widget/map_browser/state_event.dart b/app/lib/widget/map_browser/state_event.dart index f9912cb2..55e2df77 100644 --- a/app/lib/widget/map_browser/state_event.dart +++ b/app/lib/widget/map_browser/state_event.dart @@ -9,18 +9,21 @@ class _State { required this.isShowDataRangeControlPanel, required this.dateRangeType, required this.localDateRange, + required this.prefDateRangeType, this.error, }); factory _State.init({ required _DateRangeType dateRangeType, required DateRange localDateRange, + _DateRangeType? prefDateRangeType, }) { return _State( data: const [], isShowDataRangeControlPanel: false, dateRangeType: dateRangeType, localDateRange: localDateRange, + prefDateRangeType: prefDateRangeType ?? dateRangeType, ); } @@ -33,6 +36,7 @@ class _State { final bool isShowDataRangeControlPanel; final _DateRangeType dateRangeType; final DateRange localDateRange; + final _DateRangeType prefDateRangeType; final ExceptionEvent? error; } @@ -85,6 +89,24 @@ class _SetLocalDateRange implements _Event { final DateRange value; } +@toString +class _SetPrefDateRangeType implements _Event { + const _SetPrefDateRangeType(this.value); + + @override + String toString() => _$toString(); + + final _DateRangeType value; +} + +@toString +class _SetAsDefaultRange implements _Event { + const _SetAsDefaultRange(); + + @override + String toString() => _$toString(); +} + @toString class _SetError implements _Event { const _SetError(this.error, [this.stackTrace]); diff --git a/app/lib/widget/map_browser/type.dart b/app/lib/widget/map_browser/type.dart index 4a678505..c20284b2 100644 --- a/app/lib/widget/map_browser/type.dart +++ b/app/lib/widget/map_browser/type.dart @@ -251,6 +251,17 @@ enum _DateRangeType { custom, ; + static _DateRangeType fromPref(PrefMapDefaultRangeType value) { + switch (value) { + case PrefMapDefaultRangeType.thisMonth: + return thisMonth; + case PrefMapDefaultRangeType.prevMonth: + return prevMonth; + case PrefMapDefaultRangeType.thisYear: + return thisYear; + } + } + String toDisplayString() { switch (this) { case thisMonth: @@ -263,4 +274,17 @@ enum _DateRangeType { return L10n.global().mapBrowserDateRangeCustom; } } + + PrefMapDefaultRangeType toPref() { + switch (this) { + case thisMonth: + return PrefMapDefaultRangeType.thisMonth; + case prevMonth: + return PrefMapDefaultRangeType.prevMonth; + case thisYear: + return PrefMapDefaultRangeType.thisYear; + case custom: + throw ArgumentError("Value not supported"); + } + } } diff --git a/app/lib/widget/map_browser/view.dart b/app/lib/widget/map_browser/view.dart index 0abf6b9b..bb91fe36 100644 --- a/app/lib/widget/map_browser/view.dart +++ b/app/lib/widget/map_browser/view.dart @@ -246,74 +246,82 @@ class _DateRangeControlPanel extends StatelessWidget { ), ], ), - 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)); - } - }, + child: Material( + type: MaterialType.transparency, + 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, ), ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: _BlocSelector( - selector: (state) => state.localDateRange, - builder: (context, localDateRange) => _DateField( - localDateRange.from!, - onChanged: (value) { - context.addEvent(_SetLocalDateRange( - localDateRange.copyWith(from: value.toDate()))); - }, + 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)); + } + }, + ), ), ), - ), - const Text(" - "), - Expanded( - child: _BlocSelector( - selector: (state) => state.localDateRange, - builder: (context, localDateRange) => _DateField( - localDateRange.to!, - onChanged: (value) { - context.addEvent(_SetLocalDateRange( - localDateRange.copyWith(to: value.toDate()))); - }, + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: _BlocSelector( + selector: (state) => state.localDateRange, + builder: (context, localDateRange) => _DateField( + localDateRange.from!, + onChanged: (value) { + context.addEvent(_SetLocalDateRange( + localDateRange.copyWith(from: value.toDate()))); + }, + ), ), ), - ), - ], - ), - const SizedBox(height: 8), - ], + const Text(" - "), + Expanded( + child: _BlocSelector( + selector: (state) => state.localDateRange, + builder: (context, localDateRange) => _DateField( + localDateRange.to!, + onChanged: (value) { + context.addEvent(_SetLocalDateRange( + localDateRange.copyWith(to: value.toDate()))); + }, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + const Align( + alignment: Alignment.centerRight, + child: _SetAsDefaultSwitch(), + ), + const SizedBox(height: 8), + ], + ), ), ), ); @@ -384,3 +392,49 @@ class _DateFieldState extends State<_DateField> { late final _controller = TextEditingController(text: _stringify(widget.date)); } + +class _SetAsDefaultSwitch extends StatelessWidget { + const _SetAsDefaultSwitch(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => + previous.dateRangeType != current.dateRangeType || + previous.prefDateRangeType != current.prefDateRangeType, + builder: (context, state) { + final isChecked = state.dateRangeType == state.prefDateRangeType; + final isEnabled = state.dateRangeType != _DateRangeType.custom; + return InkWell( + customBorder: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(64)), + onTap: isEnabled && !isChecked + ? () { + if (!isChecked) { + context.addEvent(const _SetAsDefaultRange()); + } + } + : null, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 16), + Text( + L10n.global().mapBrowserSetDefaultDateRangeButton, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: isEnabled ? null : Theme.of(context).disabledColor, + ), + ), + IgnorePointer( + child: Checkbox( + value: isChecked, + onChanged: isEnabled ? (_) {} : null, + ), + ), + ], + ), + ); + }, + ); + } +}