From 39d2e14d3aa46345135b311b244509b1655f6443 Mon Sep 17 00:00:00 2001 From: Yaros Date: Sat, 14 Feb 2026 09:56:09 +0100 Subject: [PATCH 01/14] feat(mobile): custom date range for map --- i18n/en.json | 1 + mobile/lib/domain/models/store.model.dart | 4 ++ .../repositories/map.repository.dart | 13 +++- .../repositories/timeline.repository.dart | 25 +++++++- .../presentation/widgets/map/map.state.dart | 38 +++++++++++ .../widgets/map/map_settings_sheet.dart | 60 +++++++++++++++-- mobile/lib/services/app_settings.service.dart | 2 + .../map_settings/map_custom_time_range.dart | 64 +++++++++++++++++++ 8 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 mobile/lib/widgets/map/map_settings/map_custom_time_range.dart diff --git a/i18n/en.json b/i18n/en.json index 6e35085be8..93a4fc57c6 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1619,6 +1619,7 @@ "not_available": "N/A", "not_in_any_album": "Not in any album", "not_selected": "Not selected", + "not_set": "Not set", "notes": "Notes", "nothing_here_yet": "Nothing here yet", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index f6bed7cf61..a657fe333f 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -73,6 +73,10 @@ enum StoreKey { autoPlayVideo._(139), albumGridView._(140), + // Map custom time range settings + mapCustomFrom._(141), + mapCustomTo._(142), + // Experimental stuff photoManagerCustomFilter._(1000), betaPromptShown._(1001), diff --git a/mobile/lib/infrastructure/repositories/map.repository.dart b/mobile/lib/infrastructure/repositories/map.repository.dart index 95e42337fc..fbac8de02c 100644 --- a/mobile/lib/infrastructure/repositories/map.repository.dart +++ b/mobile/lib/infrastructure/repositories/map.repository.dart @@ -27,9 +27,16 @@ class DriftMapRepository extends DriftDatabaseRepository { condition = condition & _db.remoteAssetEntity.isFavorite.equals(true); } - if (options.relativeDays != 0) { - final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); - condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate); + if (options.customTimeRange.isValid) { + if (options.customTimeRange.from != null) { + condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(options.customTimeRange.from!); + } + if (options.customTimeRange.to != null) { + condition = condition & _db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(options.customTimeRange.to!); + } + } else if (options.relativeDays > 0) { + final fromDate = DateTime.now().subtract(Duration(days: options.relativeDays)); + condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(fromDate); } return condition; diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 7544b4b2ac..b0fcc503c3 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; +import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -21,6 +22,7 @@ class TimelineMapOptions { final bool includeArchived; final bool withPartners; final int relativeDays; + final CustomTimeRange customTimeRange; const TimelineMapOptions({ required this.bounds, @@ -28,6 +30,7 @@ class TimelineMapOptions { this.includeArchived = false, this.withPartners = false, this.relativeDays = 0, + this.customTimeRange = const CustomTimeRange(), }); } @@ -528,7 +531,16 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.where(_db.remoteAssetEntity.isFavorite.equals(true)); } - if (options.relativeDays != 0) { + if (options.customTimeRange.isValid) { + // Use custom from/to filters + if (options.customTimeRange.from != null) { + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(options.customTimeRange.from!)); + } + if (options.customTimeRange.to != null) { + query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(options.customTimeRange.to!)); + } + } else if (options.relativeDays > 0) { + // Use relative days final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate)); } @@ -570,7 +582,16 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.where(_db.remoteAssetEntity.isFavorite.equals(true)); } - if (options.relativeDays != 0) { + if (options.customTimeRange.isValid) { + // Use custom from/to filters + if (options.customTimeRange.from != null) { + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(options.customTimeRange.from!)); + } + if (options.customTimeRange.to != null) { + query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(options.customTimeRange.to!)); + } + } else if (options.relativeDays > 0) { + // Use relative days final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate)); } diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index bfd3011050..3b5103fcb3 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -9,6 +9,22 @@ import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; +class CustomTimeRange { + final DateTime? from; + final DateTime? to; + + const CustomTimeRange({this.from, this.to}); + + bool get isValid => from != null || to != null; + + CustomTimeRange copyWith({DateTime? from, DateTime? to}) { + return CustomTimeRange(from: from ?? this.from, to: to ?? this.to); + } + + CustomTimeRange clearFrom() => CustomTimeRange(to: to); + CustomTimeRange clearTo() => CustomTimeRange(from: from); +} + class MapState { final ThemeMode themeMode; final LatLngBounds bounds; @@ -16,6 +32,7 @@ class MapState { final bool includeArchived; final bool withPartners; final int relativeDays; + final CustomTimeRange customTimeRange; const MapState({ this.themeMode = ThemeMode.system, @@ -24,6 +41,7 @@ class MapState { this.includeArchived = false, this.withPartners = false, this.relativeDays = 0, + this.customTimeRange = const CustomTimeRange(), }); @override @@ -41,6 +59,7 @@ class MapState { bool? includeArchived, bool? withPartners, int? relativeDays, + CustomTimeRange? customTimeRange, }) { return MapState( bounds: bounds ?? this.bounds, @@ -49,6 +68,7 @@ class MapState { includeArchived: includeArchived ?? this.includeArchived, withPartners: withPartners ?? this.withPartners, relativeDays: relativeDays ?? this.relativeDays, + customTimeRange: customTimeRange ?? this.customTimeRange, ); } @@ -58,6 +78,7 @@ class MapState { includeArchived: includeArchived, withPartners: withPartners, relativeDays: relativeDays, + customTimeRange: customTimeRange, ); } @@ -104,9 +125,22 @@ class MapStateNotifier extends Notifier { EventStream.shared.emit(const MapMarkerReloadEvent()); } + void setCustomTimeRange(CustomTimeRange range) { + ref + .read(appSettingsServiceProvider) + .setSetting(AppSettingsEnum.mapCustomFrom, range.from == null ? "" : range.from!.toIso8601String()); + ref + .read(appSettingsServiceProvider) + .setSetting(AppSettingsEnum.mapCustomTo, range.to == null ? "" : range.to!.toIso8601String()); + state = state.copyWith(customTimeRange: range); + EventStream.shared.emit(const MapMarkerReloadEvent()); + } + @override MapState build() { final appSettingsService = ref.read(appSettingsServiceProvider); + final customFrom = appSettingsService.getSetting(AppSettingsEnum.mapCustomFrom); + final customTo = appSettingsService.getSetting(AppSettingsEnum.mapCustomTo); return MapState( themeMode: ThemeMode.values[appSettingsService.getSetting(AppSettingsEnum.mapThemeMode)], onlyFavorites: appSettingsService.getSetting(AppSettingsEnum.mapShowFavoriteOnly), @@ -114,6 +148,10 @@ class MapStateNotifier extends Notifier { withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners), relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate), bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)), + customTimeRange: CustomTimeRange( + from: customFrom.isNotEmpty ? DateTime.parse(customFrom) : null, + to: customTo.isNotEmpty ? DateTime.parse(customTo) : null, + ), ); } } diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart index c581dd6292..e14349993f 100644 --- a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -2,20 +2,35 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; +import 'package:immich_mobile/widgets/map/map_settings/map_custom_time_range.dart'; import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart'; import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart'; import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart'; -class DriftMapSettingsSheet extends HookConsumerWidget { +class DriftMapSettingsSheet extends ConsumerStatefulWidget { const DriftMapSettingsSheet({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _DriftMapSettingsSheetState(); +} + +class _DriftMapSettingsSheetState extends ConsumerState { + late bool useCustomRange; + + @override + void initState() { + super.initState(); + final mapState = ref.read(mapStateProvider); + useCustomRange = mapState.customTimeRange.isValid; + } + + @override + Widget build(BuildContext context) { final mapState = ref.watch(mapStateProvider); return DraggableScrollableSheet( expand: false, - initialChildSize: 0.6, + initialChildSize: useCustomRange ? 0.7 : 0.6, builder: (ctx, scrollController) => SingleChildScrollView( controller: scrollController, child: Card( @@ -47,10 +62,41 @@ class DriftMapSettingsSheet extends HookConsumerWidget { selected: mapState.withPartners, onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners), ), - MapTimeDropDown( - relativeTime: mapState.relativeDays, - onTimeChange: (time) => ref.read(mapStateProvider.notifier).setRelativeTime(time), - ), + if (useCustomRange) ...[ + MapCustomTimeRange( + customTimeRange: mapState.customTimeRange, + onChanged: (range) { + ref.read(mapStateProvider.notifier).setCustomTimeRange(range); + }, + ), + Align( + alignment: Alignment.centerLeft, + child: TextButton( + onPressed: () => setState(() { + useCustomRange = false; + ref.read(mapStateProvider.notifier).setRelativeTime(0); + ref.read(mapStateProvider.notifier).setCustomTimeRange(const CustomTimeRange()); + }), + child: Text("remove_custom_date_range".t(context: context)), + ), + ), + ] else ...[ + MapTimeDropDown( + relativeTime: mapState.relativeDays, + onTimeChange: (time) => ref.read(mapStateProvider.notifier).setRelativeTime(time), + ), + Align( + alignment: Alignment.centerLeft, + child: TextButton( + onPressed: () => setState(() { + useCustomRange = true; + ref.read(mapStateProvider.notifier).setRelativeTime(0); + ref.read(mapStateProvider.notifier).setCustomTimeRange(const CustomTimeRange()); + }), + child: Text("use_custom_date_range".t(context: context)), + ), + ), + ], const SizedBox(height: 20), ], ), diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 4e740ebfe5..bdd897b2d9 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -40,6 +40,8 @@ enum AppSettingsEnum { mapIncludeArchived(StoreKey.mapIncludeArchived, null, false), mapwithPartners(StoreKey.mapwithPartners, null, false), mapRelativeDate(StoreKey.mapRelativeDate, null, 0), + mapCustomFrom(StoreKey.mapCustomFrom, null, ""), + mapCustomTo(StoreKey.mapCustomTo, null, ""), allowSelfSignedSSLCert(StoreKey.selfSignedCert, null, false), ignoreIcloudAssets(StoreKey.ignoreIcloudAssets, null, false), selectedAlbumSortReverse(StoreKey.selectedAlbumSortReverse, null, true), diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart new file mode 100644 index 0000000000..1f41e13ce8 --- /dev/null +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; +import 'package:intl/intl.dart'; + +class MapCustomTimeRange extends StatelessWidget { + const MapCustomTimeRange({super.key, required this.customTimeRange, required this.onChanged}); + + final CustomTimeRange customTimeRange; + final Function(CustomTimeRange) onChanged; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("date_after".t(context: context)), + subtitle: Text( + customTimeRange.from != null + ? DateFormat.yMMMd().add_jm().format(customTimeRange.from!) + : "not_set".t(context: context), + ), + trailing: customTimeRange.from != null + ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(customTimeRange.clearFrom())) + : null, + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: customTimeRange.from ?? DateTime.now(), + firstDate: DateTime(1970), + lastDate: DateTime.now(), + ); + if (picked != null) { + onChanged(customTimeRange.copyWith(from: picked)); + } + }, + ), + ListTile( + title: Text("date_before".t(context: context)), + subtitle: Text( + customTimeRange.to != null + ? DateFormat.yMMMd().add_jm().format(customTimeRange.to!) + : "not_set".t(context: context), + ), + trailing: customTimeRange.to != null + ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(customTimeRange.clearTo())) + : null, + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: customTimeRange.to ?? DateTime.now(), + firstDate: DateTime(1970), + lastDate: DateTime.now(), + ); + if (picked != null) { + onChanged(customTimeRange.copyWith(to: picked)); + } + }, + ), + ], + ); + } +} From b0f6d5cf388be80b8ffb41c1b7a3a060f79e4279 Mon Sep 17 00:00:00 2001 From: Yaros Date: Thu, 19 Feb 2026 13:23:40 +0100 Subject: [PATCH 02/14] refactor: rename timerange & remove isvalid --- .../repositories/map.repository.dart | 13 ++++---- .../repositories/timeline.repository.dart | 30 +++++++++++-------- .../presentation/widgets/map/map.state.dart | 24 +++++++-------- .../widgets/map/map_settings_sheet.dart | 7 +++-- .../map_settings/map_custom_time_range.dart | 4 +-- 5 files changed, 43 insertions(+), 35 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/map.repository.dart b/mobile/lib/infrastructure/repositories/map.repository.dart index fbac8de02c..43a8e3d1bc 100644 --- a/mobile/lib/infrastructure/repositories/map.repository.dart +++ b/mobile/lib/infrastructure/repositories/map.repository.dart @@ -27,12 +27,15 @@ class DriftMapRepository extends DriftDatabaseRepository { condition = condition & _db.remoteAssetEntity.isFavorite.equals(true); } - if (options.customTimeRange.isValid) { - if (options.customTimeRange.from != null) { - condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(options.customTimeRange.from!); + final from = options.customTimeRange.from; + final to = options.customTimeRange.to; + + if (from != null || to != null) { + if (from != null) { + condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from); } - if (options.customTimeRange.to != null) { - condition = condition & _db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(options.customTimeRange.to!); + if (to != null) { + condition = condition & _db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to); } } else if (options.relativeDays > 0) { final fromDate = DateTime.now().subtract(Duration(days: options.relativeDays)); diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index b0fcc503c3..f620413bd2 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -22,7 +22,7 @@ class TimelineMapOptions { final bool includeArchived; final bool withPartners; final int relativeDays; - final CustomTimeRange customTimeRange; + final TimeRange customTimeRange; const TimelineMapOptions({ required this.bounds, @@ -30,7 +30,7 @@ class TimelineMapOptions { this.includeArchived = false, this.withPartners = false, this.relativeDays = 0, - this.customTimeRange = const CustomTimeRange(), + this.customTimeRange = const TimeRange(), }); } @@ -531,13 +531,16 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.where(_db.remoteAssetEntity.isFavorite.equals(true)); } - if (options.customTimeRange.isValid) { + final from = options.customTimeRange.from; + final to = options.customTimeRange.to; + + if (from != null || to != null) { // Use custom from/to filters - if (options.customTimeRange.from != null) { - query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(options.customTimeRange.from!)); + if (from != null) { + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from)); } - if (options.customTimeRange.to != null) { - query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(options.customTimeRange.to!)); + if (to != null) { + query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to)); } } else if (options.relativeDays > 0) { // Use relative days @@ -582,13 +585,16 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.where(_db.remoteAssetEntity.isFavorite.equals(true)); } - if (options.customTimeRange.isValid) { + final from = options.customTimeRange.from; + final to = options.customTimeRange.to; + + if (from != null || to != null) { // Use custom from/to filters - if (options.customTimeRange.from != null) { - query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(options.customTimeRange.from!)); + if (from != null) { + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from)); } - if (options.customTimeRange.to != null) { - query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(options.customTimeRange.to!)); + if (to != null) { + query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to)); } } else if (options.relativeDays > 0) { // Use relative days diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index 3b5103fcb3..565658a3ad 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -9,20 +9,18 @@ import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -class CustomTimeRange { +class TimeRange { final DateTime? from; final DateTime? to; - const CustomTimeRange({this.from, this.to}); + const TimeRange({this.from, this.to}); - bool get isValid => from != null || to != null; - - CustomTimeRange copyWith({DateTime? from, DateTime? to}) { - return CustomTimeRange(from: from ?? this.from, to: to ?? this.to); + TimeRange copyWith({DateTime? from, DateTime? to}) { + return TimeRange(from: from ?? this.from, to: to ?? this.to); } - CustomTimeRange clearFrom() => CustomTimeRange(to: to); - CustomTimeRange clearTo() => CustomTimeRange(from: from); + TimeRange clearFrom() => TimeRange(to: to); + TimeRange clearTo() => TimeRange(from: from); } class MapState { @@ -32,7 +30,7 @@ class MapState { final bool includeArchived; final bool withPartners; final int relativeDays; - final CustomTimeRange customTimeRange; + final TimeRange customTimeRange; const MapState({ this.themeMode = ThemeMode.system, @@ -41,7 +39,7 @@ class MapState { this.includeArchived = false, this.withPartners = false, this.relativeDays = 0, - this.customTimeRange = const CustomTimeRange(), + this.customTimeRange = const TimeRange(), }); @override @@ -59,7 +57,7 @@ class MapState { bool? includeArchived, bool? withPartners, int? relativeDays, - CustomTimeRange? customTimeRange, + TimeRange? customTimeRange, }) { return MapState( bounds: bounds ?? this.bounds, @@ -125,7 +123,7 @@ class MapStateNotifier extends Notifier { EventStream.shared.emit(const MapMarkerReloadEvent()); } - void setCustomTimeRange(CustomTimeRange range) { + void setCustomTimeRange(TimeRange range) { ref .read(appSettingsServiceProvider) .setSetting(AppSettingsEnum.mapCustomFrom, range.from == null ? "" : range.from!.toIso8601String()); @@ -148,7 +146,7 @@ class MapStateNotifier extends Notifier { withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners), relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate), bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)), - customTimeRange: CustomTimeRange( + customTimeRange: TimeRange( from: customFrom.isNotEmpty ? DateTime.parse(customFrom) : null, to: customTo.isNotEmpty ? DateTime.parse(customTo) : null, ), diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart index e14349993f..d467730a8a 100644 --- a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -21,7 +21,8 @@ class _DriftMapSettingsSheetState extends ConsumerState { void initState() { super.initState(); final mapState = ref.read(mapStateProvider); - useCustomRange = mapState.customTimeRange.isValid; + final timeRange = mapState.customTimeRange; + useCustomRange = timeRange.from != null || timeRange.to != null; } @override @@ -75,7 +76,7 @@ class _DriftMapSettingsSheetState extends ConsumerState { onPressed: () => setState(() { useCustomRange = false; ref.read(mapStateProvider.notifier).setRelativeTime(0); - ref.read(mapStateProvider.notifier).setCustomTimeRange(const CustomTimeRange()); + ref.read(mapStateProvider.notifier).setCustomTimeRange(const TimeRange()); }), child: Text("remove_custom_date_range".t(context: context)), ), @@ -91,7 +92,7 @@ class _DriftMapSettingsSheetState extends ConsumerState { onPressed: () => setState(() { useCustomRange = true; ref.read(mapStateProvider.notifier).setRelativeTime(0); - ref.read(mapStateProvider.notifier).setCustomTimeRange(const CustomTimeRange()); + ref.read(mapStateProvider.notifier).setCustomTimeRange(const TimeRange()); }), child: Text("use_custom_date_range".t(context: context)), ), diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart index 1f41e13ce8..00a952a63b 100644 --- a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -6,8 +6,8 @@ import 'package:intl/intl.dart'; class MapCustomTimeRange extends StatelessWidget { const MapCustomTimeRange({super.key, required this.customTimeRange, required this.onChanged}); - final CustomTimeRange customTimeRange; - final Function(CustomTimeRange) onChanged; + final TimeRange customTimeRange; + final Function(TimeRange) onChanged; @override Widget build(BuildContext context) { From 733100f6ec6939eff982a5aab228e6e02e96d5af Mon Sep 17 00:00:00 2001 From: Yaros Date: Thu, 19 Feb 2026 14:08:50 +0100 Subject: [PATCH 03/14] refactor: rename customtimerange variables --- .../repositories/map.repository.dart | 4 +-- .../repositories/timeline.repository.dart | 12 ++++---- .../presentation/widgets/map/map.state.dart | 24 +++++---------- .../widgets/map/map_settings_sheet.dart | 10 +++---- .../map_settings/map_custom_time_range.dart | 30 +++++++++---------- 5 files changed, 35 insertions(+), 45 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/map.repository.dart b/mobile/lib/infrastructure/repositories/map.repository.dart index 43a8e3d1bc..a7652d0678 100644 --- a/mobile/lib/infrastructure/repositories/map.repository.dart +++ b/mobile/lib/infrastructure/repositories/map.repository.dart @@ -27,8 +27,8 @@ class DriftMapRepository extends DriftDatabaseRepository { condition = condition & _db.remoteAssetEntity.isFavorite.equals(true); } - final from = options.customTimeRange.from; - final to = options.customTimeRange.to; + final from = options.timeRange.from; + final to = options.timeRange.to; if (from != null || to != null) { if (from != null) { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index f620413bd2..70b4701eb0 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -22,7 +22,7 @@ class TimelineMapOptions { final bool includeArchived; final bool withPartners; final int relativeDays; - final TimeRange customTimeRange; + final TimeRange timeRange; const TimelineMapOptions({ required this.bounds, @@ -30,7 +30,7 @@ class TimelineMapOptions { this.includeArchived = false, this.withPartners = false, this.relativeDays = 0, - this.customTimeRange = const TimeRange(), + this.timeRange = const TimeRange(), }); } @@ -531,8 +531,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.where(_db.remoteAssetEntity.isFavorite.equals(true)); } - final from = options.customTimeRange.from; - final to = options.customTimeRange.to; + final from = options.timeRange.from; + final to = options.timeRange.to; if (from != null || to != null) { // Use custom from/to filters @@ -585,8 +585,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.where(_db.remoteAssetEntity.isFavorite.equals(true)); } - final from = options.customTimeRange.from; - final to = options.customTimeRange.to; + final from = options.timeRange.from; + final to = options.timeRange.to; if (from != null || to != null) { // Use custom from/to filters diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index 565658a3ad..e198e2f2c5 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -30,7 +30,7 @@ class MapState { final bool includeArchived; final bool withPartners; final int relativeDays; - final TimeRange customTimeRange; + final TimeRange timeRange; const MapState({ this.themeMode = ThemeMode.system, @@ -39,7 +39,7 @@ class MapState { this.includeArchived = false, this.withPartners = false, this.relativeDays = 0, - this.customTimeRange = const TimeRange(), + this.timeRange = const TimeRange(), }); @override @@ -57,7 +57,7 @@ class MapState { bool? includeArchived, bool? withPartners, int? relativeDays, - TimeRange? customTimeRange, + TimeRange? timeRange, }) { return MapState( bounds: bounds ?? this.bounds, @@ -66,7 +66,7 @@ class MapState { includeArchived: includeArchived ?? this.includeArchived, withPartners: withPartners ?? this.withPartners, relativeDays: relativeDays ?? this.relativeDays, - customTimeRange: customTimeRange ?? this.customTimeRange, + timeRange: timeRange ?? this.timeRange, ); } @@ -75,8 +75,7 @@ class MapState { onlyFavorites: onlyFavorites, includeArchived: includeArchived, withPartners: withPartners, - relativeDays: relativeDays, - customTimeRange: customTimeRange, + timeRange: timeRange, ); } @@ -117,20 +116,14 @@ class MapStateNotifier extends Notifier { EventStream.shared.emit(const MapMarkerReloadEvent()); } - void setRelativeTime(int relativeDays) { - ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapRelativeDate, relativeDays); - state = state.copyWith(relativeDays: relativeDays); - EventStream.shared.emit(const MapMarkerReloadEvent()); - } - - void setCustomTimeRange(TimeRange range) { + void setTimeRange(TimeRange range) { ref .read(appSettingsServiceProvider) .setSetting(AppSettingsEnum.mapCustomFrom, range.from == null ? "" : range.from!.toIso8601String()); ref .read(appSettingsServiceProvider) .setSetting(AppSettingsEnum.mapCustomTo, range.to == null ? "" : range.to!.toIso8601String()); - state = state.copyWith(customTimeRange: range); + state = state.copyWith(timeRange: range); EventStream.shared.emit(const MapMarkerReloadEvent()); } @@ -144,9 +137,8 @@ class MapStateNotifier extends Notifier { onlyFavorites: appSettingsService.getSetting(AppSettingsEnum.mapShowFavoriteOnly), includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived), withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners), - relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate), bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)), - customTimeRange: TimeRange( + timeRange: TimeRange( from: customFrom.isNotEmpty ? DateTime.parse(customFrom) : null, to: customTo.isNotEmpty ? DateTime.parse(customTo) : null, ), diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart index d467730a8a..0d0ff8f39d 100644 --- a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -21,7 +21,7 @@ class _DriftMapSettingsSheetState extends ConsumerState { void initState() { super.initState(); final mapState = ref.read(mapStateProvider); - final timeRange = mapState.customTimeRange; + final timeRange = mapState.timeRange; useCustomRange = timeRange.from != null || timeRange.to != null; } @@ -64,10 +64,10 @@ class _DriftMapSettingsSheetState extends ConsumerState { onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners), ), if (useCustomRange) ...[ - MapCustomTimeRange( - customTimeRange: mapState.customTimeRange, + MapTimeRange( + timeRange: mapState.timeRange, onChanged: (range) { - ref.read(mapStateProvider.notifier).setCustomTimeRange(range); + ref.read(mapStateProvider.notifier).setTimeRange(range); }, ), Align( @@ -76,7 +76,7 @@ class _DriftMapSettingsSheetState extends ConsumerState { onPressed: () => setState(() { useCustomRange = false; ref.read(mapStateProvider.notifier).setRelativeTime(0); - ref.read(mapStateProvider.notifier).setCustomTimeRange(const TimeRange()); + ref.read(mapStateProvider.notifier).setTimeRange(const TimeRange()); }), child: Text("remove_custom_date_range".t(context: context)), ), diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart index 00a952a63b..60e7f57951 100644 --- a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -3,10 +3,10 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:intl/intl.dart'; -class MapCustomTimeRange extends StatelessWidget { - const MapCustomTimeRange({super.key, required this.customTimeRange, required this.onChanged}); +class MapTimeRange extends StatelessWidget { + const MapTimeRange({super.key, required this.timeRange, required this.onChanged}); - final TimeRange customTimeRange; + final TimeRange timeRange; final Function(TimeRange) onChanged; @override @@ -17,44 +17,42 @@ class MapCustomTimeRange extends StatelessWidget { ListTile( title: Text("date_after".t(context: context)), subtitle: Text( - customTimeRange.from != null - ? DateFormat.yMMMd().add_jm().format(customTimeRange.from!) + timeRange.from != null + ? DateFormat.yMMMd().add_jm().format(timeRange.from!) : "not_set".t(context: context), ), - trailing: customTimeRange.from != null - ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(customTimeRange.clearFrom())) + trailing: timeRange.from != null + ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(timeRange.clearFrom())) : null, onTap: () async { final picked = await showDatePicker( context: context, - initialDate: customTimeRange.from ?? DateTime.now(), + initialDate: timeRange.from ?? DateTime.now(), firstDate: DateTime(1970), lastDate: DateTime.now(), ); if (picked != null) { - onChanged(customTimeRange.copyWith(from: picked)); + onChanged(timeRange.copyWith(from: picked)); } }, ), ListTile( title: Text("date_before".t(context: context)), subtitle: Text( - customTimeRange.to != null - ? DateFormat.yMMMd().add_jm().format(customTimeRange.to!) - : "not_set".t(context: context), + timeRange.to != null ? DateFormat.yMMMd().add_jm().format(timeRange.to!) : "not_set".t(context: context), ), - trailing: customTimeRange.to != null - ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(customTimeRange.clearTo())) + trailing: timeRange.to != null + ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(timeRange.clearTo())) : null, onTap: () async { final picked = await showDatePicker( context: context, - initialDate: customTimeRange.to ?? DateTime.now(), + initialDate: timeRange.to ?? DateTime.now(), firstDate: DateTime(1970), lastDate: DateTime.now(), ); if (picked != null) { - onChanged(customTimeRange.copyWith(to: picked)); + onChanged(timeRange.copyWith(to: picked)); } }, ), From 2424952b9a2b68ce634a2ff73802175de839de0e Mon Sep 17 00:00:00 2001 From: Yaros Date: Thu, 19 Feb 2026 14:11:41 +0100 Subject: [PATCH 04/14] refactor: add back setRelativeTime --- mobile/lib/presentation/widgets/map/map.state.dart | 6 ++++++ mobile/lib/presentation/widgets/map/map_settings_sheet.dart | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index e198e2f2c5..a872757961 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -116,6 +116,12 @@ class MapStateNotifier extends Notifier { EventStream.shared.emit(const MapMarkerReloadEvent()); } + void setRelativeTime(int relativeDays) { + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapRelativeDate, relativeDays); + state = state.copyWith(relativeDays: relativeDays); + EventStream.shared.emit(const MapMarkerReloadEvent()); + } + void setTimeRange(TimeRange range) { ref .read(appSettingsServiceProvider) diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart index 0d0ff8f39d..00e5341f08 100644 --- a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -92,7 +92,7 @@ class _DriftMapSettingsSheetState extends ConsumerState { onPressed: () => setState(() { useCustomRange = true; ref.read(mapStateProvider.notifier).setRelativeTime(0); - ref.read(mapStateProvider.notifier).setCustomTimeRange(const TimeRange()); + ref.read(mapStateProvider.notifier).setTimeRange(const TimeRange()); }), child: Text("use_custom_date_range".t(context: context)), ), From 12c4ee83d61f6c1080d6db7401bfb56b2cc85cc2 Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 8 May 2026 15:59:41 +0200 Subject: [PATCH 05/14] refactor: implement suggestions --- .../repositories/map.repository.dart | 19 ++++---- .../repositories/timeline.repository.dart | 38 ++++++++-------- .../presentation/widgets/map/map.state.dart | 43 ++++++++++++------- .../widgets/map/map_settings_sheet.dart | 7 +-- mobile/lib/utils/option.dart | 15 +++++++ .../map_settings/map_custom_time_range.dart | 33 ++++++++------ 6 files changed, 96 insertions(+), 59 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/map.repository.dart b/mobile/lib/infrastructure/repositories/map.repository.dart index a7652d0678..9de0ca722d 100644 --- a/mobile/lib/infrastructure/repositories/map.repository.dart +++ b/mobile/lib/infrastructure/repositories/map.repository.dart @@ -27,19 +27,20 @@ class DriftMapRepository extends DriftDatabaseRepository { condition = condition & _db.remoteAssetEntity.isFavorite.equals(true); } - final from = options.timeRange.from; - final to = options.timeRange.to; + final timeRange = options.timeRange; + final hasCustomRange = timeRange.from.isSome || timeRange.to.isSome; - if (from != null || to != null) { - if (from != null) { + if (hasCustomRange) { + timeRange.from.ifSome((from) { condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from); - } - if (to != null) { + }); + + timeRange.to.ifSome((to) { condition = condition & _db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to); - } + }); } else if (options.relativeDays > 0) { - final fromDate = DateTime.now().subtract(Duration(days: options.relativeDays)); - condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(fromDate); + final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); + condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate); } return condition; diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 291f111913..ac6ccf37ef 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -552,20 +552,21 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.where(_db.remoteAssetEntity.isFavorite.equals(true)); } - final from = options.timeRange.from; - final to = options.timeRange.to; + final timeRange = options.timeRange; - if (from != null || to != null) { - // Use custom from/to filters - if (from != null) { + final hasCustomRange = timeRange.from.isSome || timeRange.to.isSome; + + if (hasCustomRange) { + timeRange.from.ifSome((from) { query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from)); - } - if (to != null) { + }); + + timeRange.to.ifSome((to) { query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to)); - } + }); } else if (options.relativeDays > 0) { - // Use relative days final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate)); } @@ -606,20 +607,21 @@ class DriftTimelineRepository extends DriftDatabaseRepository { query.where(_db.remoteAssetEntity.isFavorite.equals(true)); } - final from = options.timeRange.from; - final to = options.timeRange.to; + final timeRange = options.timeRange; - if (from != null || to != null) { - // Use custom from/to filters - if (from != null) { + final hasCustomRange = timeRange.from.isSome || timeRange.to.isSome; + + if (hasCustomRange) { + timeRange.from.ifSome((from) { query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from)); - } - if (to != null) { + }); + + timeRange.to.ifSome((to) { query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to)); - } + }); } else if (options.relativeDays > 0) { - // Use relative days final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate)); } diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index a872757961..a7b4ce19e9 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -7,15 +7,16 @@ import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/map.provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/option.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; class TimeRange { - final DateTime? from; - final DateTime? to; + final Option from; + final Option to; - const TimeRange({this.from, this.to}); + const TimeRange({this.from = const None(), this.to = const None()}); - TimeRange copyWith({DateTime? from, DateTime? to}) { + TimeRange copyWith({Option? from, Option? to}) { return TimeRange(from: from ?? this.from, to: to ?? this.to); } @@ -75,6 +76,7 @@ class MapState { onlyFavorites: onlyFavorites, includeArchived: includeArchived, withPartners: withPartners, + relativeDays: relativeDays, timeRange: timeRange, ); } @@ -123,31 +125,40 @@ class MapStateNotifier extends Notifier { } void setTimeRange(TimeRange range) { - ref - .read(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.mapCustomFrom, range.from == null ? "" : range.from!.toIso8601String()); - ref - .read(appSettingsServiceProvider) - .setSetting(AppSettingsEnum.mapCustomTo, range.to == null ? "" : range.to!.toIso8601String()); + final from = range.from.unwrapOrNull; + final to = range.to.unwrapOrNull; + + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapCustomFrom, from?.toIso8601String() ?? ""); + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapCustomTo, to?.toIso8601String() ?? ""); + state = state.copyWith(timeRange: range); EventStream.shared.emit(const MapMarkerReloadEvent()); } + Option parseDateOption(String s) { + try { + if (s.trim().isEmpty) return const Option.none(); + return Option.some(DateTime.parse(s)); + } catch (_) { + return const Option.none(); + } + } + @override MapState build() { final appSettingsService = ref.read(appSettingsServiceProvider); - final customFrom = appSettingsService.getSetting(AppSettingsEnum.mapCustomFrom); - final customTo = appSettingsService.getSetting(AppSettingsEnum.mapCustomTo); + + final customFrom = appSettingsService.getSetting(AppSettingsEnum.mapCustomFrom).toOption().flatMap(parseDateOption); + final customTo = appSettingsService.getSetting(AppSettingsEnum.mapCustomTo).toOption().flatMap(parseDateOption); + return MapState( themeMode: ThemeMode.values[appSettingsService.getSetting(AppSettingsEnum.mapThemeMode)], onlyFavorites: appSettingsService.getSetting(AppSettingsEnum.mapShowFavoriteOnly), includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived), withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners), bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)), - timeRange: TimeRange( - from: customFrom.isNotEmpty ? DateTime.parse(customFrom) : null, - to: customTo.isNotEmpty ? DateTime.parse(customTo) : null, - ), + relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate), + timeRange: TimeRange(from: customFrom, to: customTo), ); } } diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart index 00e5341f08..9a88c6c55c 100644 --- a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/widgets/map/map_settings/map_custom_time_range.dart'; import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart'; @@ -22,7 +23,7 @@ class _DriftMapSettingsSheetState extends ConsumerState { super.initState(); final mapState = ref.read(mapStateProvider); final timeRange = mapState.timeRange; - useCustomRange = timeRange.from != null || timeRange.to != null; + useCustomRange = timeRange.from.isSome || timeRange.to.isSome; } @override @@ -78,7 +79,7 @@ class _DriftMapSettingsSheetState extends ConsumerState { ref.read(mapStateProvider.notifier).setRelativeTime(0); ref.read(mapStateProvider.notifier).setTimeRange(const TimeRange()); }), - child: Text("remove_custom_date_range".t(context: context)), + child: Text(context.t.remove_custom_date_range), ), ), ] else ...[ @@ -94,7 +95,7 @@ class _DriftMapSettingsSheetState extends ConsumerState { ref.read(mapStateProvider.notifier).setRelativeTime(0); ref.read(mapStateProvider.notifier).setTimeRange(const TimeRange()); }), - child: Text("use_custom_date_range".t(context: context)), + child: Text(context.t.use_custom_date_range), ), ), ], diff --git a/mobile/lib/utils/option.dart b/mobile/lib/utils/option.dart index 3470e8489e..326b60cb39 100644 --- a/mobile/lib/utils/option.dart +++ b/mobile/lib/utils/option.dart @@ -24,6 +24,21 @@ sealed class Option { None() => onNone(), }; + Option flatMap(Option Function(T value) f) => switch (this) { + Some(:final value) => f(value), + None() => const Option.none(), + }; + + void ifSome(void Function(T value) action) { + switch (this) { + case Some(:final value): + action(value); + break; + case None(): + break; + } + } + @override String toString() => switch (this) { Some(:final value) => 'Some($value)', diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart index 60e7f57951..2dc0cdaea4 100644 --- a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -1,6 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; +import 'package:immich_mobile/utils/option.dart'; import 'package:intl/intl.dart'; class MapTimeRange extends StatelessWidget { @@ -15,44 +17,49 @@ class MapTimeRange extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ListTile( - title: Text("date_after".t(context: context)), + title: Text(context.t.date_after), subtitle: Text( - timeRange.from != null - ? DateFormat.yMMMd().add_jm().format(timeRange.from!) - : "not_set".t(context: context), + timeRange.from.fold((from) => DateFormat.yMMMd().add_jm().format(from), () => context.t.not_set), ), - trailing: timeRange.from != null + trailing: timeRange.from.isSome ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(timeRange.clearFrom())) : null, onTap: () async { + final initial = timeRange.from.unwrapOrNull ?? DateTime.now(); + final picked = await showDatePicker( context: context, - initialDate: timeRange.from ?? DateTime.now(), + initialDate: initial, firstDate: DateTime(1970), lastDate: DateTime.now(), ); + if (picked != null) { - onChanged(timeRange.copyWith(from: picked)); + onChanged(timeRange.copyWith(from: Option.some(picked))); } }, ), + ListTile( - title: Text("date_before".t(context: context)), + title: Text(context.t.date_before), subtitle: Text( - timeRange.to != null ? DateFormat.yMMMd().add_jm().format(timeRange.to!) : "not_set".t(context: context), + timeRange.to.fold((to) => DateFormat.yMMMd().add_jm().format(to), () => context.t.not_set), ), - trailing: timeRange.to != null + trailing: timeRange.to.isSome ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(timeRange.clearTo())) : null, onTap: () async { + final initial = timeRange.to.unwrapOrNull ?? DateTime.now(); + final picked = await showDatePicker( context: context, - initialDate: timeRange.to ?? DateTime.now(), + initialDate: initial, firstDate: DateTime(1970), lastDate: DateTime.now(), ); + if (picked != null) { - onChanged(timeRange.copyWith(to: picked)); + onChanged(timeRange.copyWith(to: Option.some(picked))); } }, ), From 21506090a587ddc98ef78a6d671c6622448ef2db Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 13 May 2026 18:10:59 +0200 Subject: [PATCH 06/14] refactor: suggestions --- mobile/lib/utils/option.dart | 10 +++------- .../map/map_settings/map_custom_time_range.dart | 10 ++++++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/mobile/lib/utils/option.dart b/mobile/lib/utils/option.dart index 326b60cb39..4824d9c507 100644 --- a/mobile/lib/utils/option.dart +++ b/mobile/lib/utils/option.dart @@ -29,13 +29,9 @@ sealed class Option { None() => const Option.none(), }; - void ifSome(void Function(T value) action) { - switch (this) { - case Some(:final value): - action(value); - break; - case None(): - break; + void ifPresent(void Function(T value) f) { + if (this case Some(:final value)) { + f(value); } } diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart index 2dc0cdaea4..35d218b0ca 100644 --- a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -19,7 +19,10 @@ class MapTimeRange extends StatelessWidget { ListTile( title: Text(context.t.date_after), subtitle: Text( - timeRange.from.fold((from) => DateFormat.yMMMd().add_jm().format(from), () => context.t.not_set), + timeRange.from.fold( + (from) => DateFormat.yMMMd(context.locale).add_jm().format(from), + () => context.t.not_set, + ), ), trailing: timeRange.from.isSome ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(timeRange.clearFrom())) @@ -43,7 +46,10 @@ class MapTimeRange extends StatelessWidget { ListTile( title: Text(context.t.date_before), subtitle: Text( - timeRange.to.fold((to) => DateFormat.yMMMd().add_jm().format(to), () => context.t.not_set), + timeRange.to.fold( + (to) => DateFormat.yMMMd(context.locale).add_jm().format(to), + () => context.t.not_set, + ), ), trailing: timeRange.to.isSome ? IconButton(icon: const Icon(Icons.close), onPressed: () => onChanged(timeRange.clearTo())) From 179e72da7a42c50d94f82006670f3a23ed5ab189 Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 13 May 2026 18:22:14 +0200 Subject: [PATCH 07/14] fix: ifPresent --- .../lib/infrastructure/repositories/map.repository.dart | 4 ++-- .../infrastructure/repositories/timeline.repository.dart | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/map.repository.dart b/mobile/lib/infrastructure/repositories/map.repository.dart index 9de0ca722d..0c6864b718 100644 --- a/mobile/lib/infrastructure/repositories/map.repository.dart +++ b/mobile/lib/infrastructure/repositories/map.repository.dart @@ -31,11 +31,11 @@ class DriftMapRepository extends DriftDatabaseRepository { final hasCustomRange = timeRange.from.isSome || timeRange.to.isSome; if (hasCustomRange) { - timeRange.from.ifSome((from) { + timeRange.from.ifPresent((from) { condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from); }); - timeRange.to.ifSome((to) { + timeRange.to.ifPresent((to) { condition = condition & _db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to); }); } else if (options.relativeDays > 0) { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index ac6ccf37ef..13a1c9ca24 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -557,11 +557,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository { final hasCustomRange = timeRange.from.isSome || timeRange.to.isSome; if (hasCustomRange) { - timeRange.from.ifSome((from) { + timeRange.from.ifPresent((from) { query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from)); }); - timeRange.to.ifSome((to) { + timeRange.to.ifPresent((to) { query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to)); }); } else if (options.relativeDays > 0) { @@ -612,11 +612,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository { final hasCustomRange = timeRange.from.isSome || timeRange.to.isSome; if (hasCustomRange) { - timeRange.from.ifSome((from) { + timeRange.from.ifPresent((from) { query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(from)); }); - timeRange.to.ifSome((to) { + timeRange.to.ifPresent((to) { query.where(_db.remoteAssetEntity.createdAt.isSmallerOrEqualValue(to)); }); } else if (options.relativeDays > 0) { From 6bd001d9ff988a165ca79765e9b60a5ca2ad43a4 Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 13 May 2026 18:36:17 +0200 Subject: [PATCH 08/14] fix: context.locale --- .../lib/widgets/map/map_settings/map_custom_time_range.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart index 35d218b0ca..3e5d96e530 100644 --- a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/utils/option.dart'; -import 'package:intl/intl.dart'; class MapTimeRange extends StatelessWidget { const MapTimeRange({super.key, required this.timeRange, required this.onChanged}); @@ -20,7 +19,7 @@ class MapTimeRange extends StatelessWidget { title: Text(context.t.date_after), subtitle: Text( timeRange.from.fold( - (from) => DateFormat.yMMMd(context.locale).add_jm().format(from), + (from) => DateFormat.yMMMd(context.locale.toString()).add_jm().format(from), () => context.t.not_set, ), ), @@ -47,7 +46,7 @@ class MapTimeRange extends StatelessWidget { title: Text(context.t.date_before), subtitle: Text( timeRange.to.fold( - (to) => DateFormat.yMMMd(context.locale).add_jm().format(to), + (to) => DateFormat.yMMMd(context.locale.toString()).add_jm().format(to), () => context.t.not_set, ), ), From 86ff3737527d267480e2b6b03656be98a157fe3c Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 13 May 2026 18:36:46 +0200 Subject: [PATCH 09/14] chore: restrict selection --- .../map/map_settings/map_custom_time_range.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart index 3e5d96e530..a0507e87db 100644 --- a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -28,12 +28,13 @@ class MapTimeRange extends StatelessWidget { : null, onTap: () async { final initial = timeRange.from.unwrapOrNull ?? DateTime.now(); + final currentTo = timeRange.to.unwrapOrNull; final picked = await showDatePicker( context: context, - initialDate: initial, + initialDate: currentTo != null && initial.isAfter(currentTo) ? currentTo : initial, firstDate: DateTime(1970), - lastDate: DateTime.now(), + lastDate: currentTo ?? DateTime.now(), ); if (picked != null) { @@ -41,7 +42,6 @@ class MapTimeRange extends StatelessWidget { } }, ), - ListTile( title: Text(context.t.date_before), subtitle: Text( @@ -55,11 +55,12 @@ class MapTimeRange extends StatelessWidget { : null, onTap: () async { final initial = timeRange.to.unwrapOrNull ?? DateTime.now(); + final currentFrom = timeRange.from.unwrapOrNull; final picked = await showDatePicker( context: context, - initialDate: initial, - firstDate: DateTime(1970), + initialDate: currentFrom != null && initial.isBefore(currentFrom) ? currentFrom : initial, + firstDate: currentFrom ?? DateTime(1970), lastDate: DateTime.now(), ); From 2382427488580af9fb1020cb2565205487290f53 Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 13 May 2026 18:48:50 +0200 Subject: [PATCH 10/14] refactor: move options to mapconfig --- .../lib/domain/models/config/map_config.dart | 18 +++++++++++++++--- mobile/lib/domain/models/metadata_key.dart | 2 ++ mobile/lib/domain/models/store.model.dart | 4 ---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/mobile/lib/domain/models/config/map_config.dart b/mobile/lib/domain/models/config/map_config.dart index e37ab0f431..20d3466b1d 100644 --- a/mobile/lib/domain/models/config/map_config.dart +++ b/mobile/lib/domain/models/config/map_config.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/utils/option.dart'; class MapConfig { final int relativeDays; @@ -6,6 +7,8 @@ class MapConfig { final bool includeArchived; final ThemeMode themeMode; final bool withPartners; + final Option customFrom; + final Option customTo; const MapConfig({ this.relativeDays = 0, @@ -13,6 +16,8 @@ class MapConfig { this.includeArchived = false, this.themeMode = ThemeMode.system, this.withPartners = false, + this.customFrom = const Option.none(), + this.customTo = const Option.none(), }); MapConfig copyWith({ @@ -21,12 +26,16 @@ class MapConfig { bool? includeArchived, ThemeMode? themeMode, bool? withPartners, + Option? customFrom, + Option? customTo, }) => MapConfig( relativeDays: relativeDays ?? this.relativeDays, favoritesOnly: favoritesOnly ?? this.favoritesOnly, includeArchived: includeArchived ?? this.includeArchived, themeMode: themeMode ?? this.themeMode, withPartners: withPartners ?? this.withPartners, + customFrom: customFrom ?? this.customFrom, + customTo: customTo ?? this.customTo, ); @override @@ -37,12 +46,15 @@ class MapConfig { other.favoritesOnly == favoritesOnly && other.includeArchived == includeArchived && other.themeMode == themeMode && - other.withPartners == withPartners); + other.withPartners == withPartners && + other.customFrom == customFrom && + other.customTo == customTo); @override - int get hashCode => Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners); + int get hashCode => + Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners, customFrom, customTo); @override String toString() => - 'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners)'; + 'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners, customFrom: $customFrom, customTo: $customTo)'; } diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index 61a3cebc8a..29df5e7838 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -50,6 +50,8 @@ enum MetadataKey { // Map mapShowFavoriteOnly(.appConfig, 'map.showFavoriteOnly', false), mapRelativeDate(.appConfig, 'map.relativeDate', 0), + mapCustomFrom(.appConfig, 'map.customFrom', ''), + mapCustomTo(.appConfig, 'map.customTo', ''), mapIncludeArchived(.appConfig, 'map.includeArchived', false), mapThemeMode(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)), mapWithPartners(.appConfig, 'map.withPartners', false), diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index cae99834bf..e52e8a0a92 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -40,10 +40,6 @@ enum StoreKey { albumGridView._(140), loadOriginal._(101), - // Map custom time range settings - mapCustomFrom._(142), - mapCustomTo._(143), - // Experimental stuff enableBackup._(1003), useWifiForUploadVideos._(1004), From c64767034deed6a6ad91de44e3f8159aab201114 Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 13 May 2026 18:53:46 +0200 Subject: [PATCH 11/14] fix: parse dateOption --- .../repositories/metadata.repository.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index d8c8f55898..3566c55da2 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -4,6 +4,7 @@ import 'package:immich_mobile/domain/models/config/system_config.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/utils/option.dart'; class MetadataRepository extends DriftDatabaseRepository { final Drift _db; @@ -97,6 +98,17 @@ class MetadataRepository extends DriftDatabaseRepository { } } +Option _parseDateOption(String s) { + if (s.trim().isEmpty) { + return const Option.none(); + } + try { + return Option.some(DateTime.parse(s)); + } catch (_) { + return const Option.none(); + } +} + extension on MetadataDomain { T config(MetadataRepository repo) => switch (this) { .appConfig => repo._appConfig as T, @@ -126,6 +138,8 @@ extension on MetadataDomain { includeArchived: repo._read(.mapIncludeArchived), themeMode: repo._read(.mapThemeMode), withPartners: repo._read(.mapWithPartners), + customFrom: _parseDateOption(repo._read(.mapCustomFrom)), + customTo: _parseDateOption(repo._read(.mapCustomTo)), ), timeline: .new( tilesPerRow: repo._read(.timelineTilesPerRow), From 955f491a66df3cb78ca66d809476b62ba1fbd9d6 Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 13 May 2026 18:54:50 +0200 Subject: [PATCH 12/14] refactor: move model to domain --- mobile/lib/domain/models/time_range.model.dart | 15 +++++++++++++++ .../repositories/timeline.repository.dart | 2 +- .../lib/presentation/widgets/map/map.state.dart | 15 +-------------- .../widgets/map/map_settings_sheet.dart | 1 + .../map/map_settings/map_custom_time_range.dart | 2 +- 5 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 mobile/lib/domain/models/time_range.model.dart diff --git a/mobile/lib/domain/models/time_range.model.dart b/mobile/lib/domain/models/time_range.model.dart new file mode 100644 index 0000000000..bb46c01c75 --- /dev/null +++ b/mobile/lib/domain/models/time_range.model.dart @@ -0,0 +1,15 @@ +import 'package:immich_mobile/utils/option.dart'; + +class TimeRange { + final Option from; + final Option to; + + const TimeRange({this.from = const None(), this.to = const None()}); + + TimeRange copyWith({Option? from, Option? to}) { + return TimeRange(from: from ?? this.from, to: to ?? this.to); + } + + TimeRange clearFrom() => TimeRange(to: to); + TimeRange clearTo() => TimeRange(from: from); +} diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 610fcc9c39..0278367246 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/time_range.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; @@ -12,7 +13,6 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; -import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:stream_transform/stream_transform.dart'; diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index d3ae23200d..fef25411f3 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/time_range.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/providers/infrastructure/map.provider.dart'; @@ -10,20 +11,6 @@ import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/utils/option.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -class TimeRange { - final Option from; - final Option to; - - const TimeRange({this.from = const None(), this.to = const None()}); - - TimeRange copyWith({Option? from, Option? to}) { - return TimeRange(from: from ?? this.from, to: to ?? this.to); - } - - TimeRange clearFrom() => TimeRange(to: to); - TimeRange clearTo() => TimeRange(from: from); -} - class MapState { final ThemeMode themeMode; final LatLngBounds bounds; diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart index 9a88c6c55c..51c0d31363 100644 --- a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/time_range.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart index a0507e87db..c2dc5879a6 100644 --- a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/time_range.model.dart'; import 'package:immich_mobile/generated/translations.g.dart'; -import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/utils/option.dart'; class MapTimeRange extends StatelessWidget { From 9e76f09c91601553288e68d8b2fbdf27c668839b Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 13 May 2026 20:47:28 +0200 Subject: [PATCH 13/14] chore: locale toLanguageTag --- .../lib/widgets/map/map_settings/map_custom_time_range.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart index c2dc5879a6..abdf10519b 100644 --- a/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart +++ b/mobile/lib/widgets/map/map_settings/map_custom_time_range.dart @@ -19,7 +19,7 @@ class MapTimeRange extends StatelessWidget { title: Text(context.t.date_after), subtitle: Text( timeRange.from.fold( - (from) => DateFormat.yMMMd(context.locale.toString()).add_jm().format(from), + (from) => DateFormat.yMMMd(context.locale.toLanguageTag()).add_jm().format(from), () => context.t.not_set, ), ), @@ -46,7 +46,7 @@ class MapTimeRange extends StatelessWidget { title: Text(context.t.date_before), subtitle: Text( timeRange.to.fold( - (to) => DateFormat.yMMMd(context.locale.toString()).add_jm().format(to), + (to) => DateFormat.yMMMd(context.locale.toLanguageTag()).add_jm().format(to), () => context.t.not_set, ), ), From 156277c629eaf587f0039378126f31360d23b9a2 Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 13 May 2026 22:25:42 +0200 Subject: [PATCH 14/14] chore: add datestringcodec --- mobile/lib/domain/models/metadata_key.dart | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index 29df5e7838..7b573c76de 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -50,8 +50,8 @@ enum MetadataKey { // Map mapShowFavoriteOnly(.appConfig, 'map.showFavoriteOnly', false), mapRelativeDate(.appConfig, 'map.relativeDate', 0), - mapCustomFrom(.appConfig, 'map.customFrom', ''), - mapCustomTo(.appConfig, 'map.customTo', ''), + mapCustomFrom(.appConfig, 'map.customFrom', '', _DateStringCodec()), + mapCustomTo(.appConfig, 'map.customTo', '', _DateStringCodec()), mapIncludeArchived(.appConfig, 'map.includeArchived', false), mapThemeMode(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)), mapWithPartners(.appConfig, 'map.withPartners', false), @@ -166,6 +166,21 @@ final class _ListCodec extends _MetadataCodec> { } } +final class _DateStringCodec extends _MetadataCodec { + const _DateStringCodec(); + + @override + String encode(String value) => value; + + @override + String? decode(String raw) { + if (raw.isEmpty) { + return raw; + } + return DateTime.tryParse(raw) != null ? raw : null; + } +} + final class _PrimitiveCodec extends _MetadataCodec { final T? Function(String) _parse;