mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Filter photos by date in map browser
This commit is contained in:
parent
877bed1640
commit
17e7aa55f3
10 changed files with 637 additions and 30 deletions
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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 ? "[" : "("}"
|
||||
|
|
|
@ -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 ? "[" : "("}"
|
||||
|
|
Loading…
Reference in a new issue