diff --git a/mobile/lib/domain/models/events.model.dart b/mobile/lib/domain/models/events.model.dart index b3ab756414..fc9cebc80f 100644 --- a/mobile/lib/domain/models/events.model.dart +++ b/mobile/lib/domain/models/events.model.dart @@ -30,3 +30,8 @@ class MultiSelectToggleEvent extends Event { final bool isEnabled; const MultiSelectToggleEvent(this.isEnabled); } + +// Map Events +class MapMarkerReloadEvent extends Event { + const MapMarkerReloadEvent(); +} diff --git a/mobile/lib/domain/services/map.service.dart b/mobile/lib/domain/services/map.service.dart index 8c50a5aaeb..6c64e2817e 100644 --- a/mobile/lib/domain/services/map.service.dart +++ b/mobile/lib/domain/services/map.service.dart @@ -1,5 +1,6 @@ import 'package:immich_mobile/domain/models/map.model.dart'; import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; typedef MapMarkerSource = Future> Function(LatLngBounds? bounds); @@ -11,7 +12,8 @@ class MapFactory { const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository; - MapService remote(String ownerId) => MapService(_mapRepository.remote(ownerId)); + MapService remote(List ownerIds, TimelineMapOptions options) => + MapService(_mapRepository.remote(ownerIds, options)); } class MapService { diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index e866a965c4..61e114762c 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -11,7 +11,6 @@ import 'package:immich_mobile/domain/services/setting.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; typedef TimelineAssetSource = Future> Function(int index, int count); @@ -82,8 +81,8 @@ class TimelineFactory { TimelineService fromAssetsWithBuckets(List assets, TimelineOrigin type) => TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type)); - TimelineService map(String userId, LatLngBounds bounds) => - TimelineService(_timelineRepository.map(userId, bounds, groupBy)); + TimelineService map(List userIds, TimelineMapOptions options) => + TimelineService(_timelineRepository.map(userIds, options, groupBy)); } class TimelineService { diff --git a/mobile/lib/infrastructure/repositories/map.repository.dart b/mobile/lib/infrastructure/repositories/map.repository.dart index 9b8cdcc19d..95e42337fc 100644 --- a/mobile/lib/infrastructure/repositories/map.repository.dart +++ b/mobile/lib/infrastructure/repositories/map.repository.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/services/map.service.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.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/timeline.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; class DriftMapRepository extends DriftDatabaseRepository { @@ -12,9 +13,27 @@ class DriftMapRepository extends DriftDatabaseRepository { const DriftMapRepository(super._db) : _db = _db; - MapQuery remote(String ownerId) => _mapQueryBuilder( - assetFilter: (row) => - row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId), + MapQuery remote(List ownerIds, TimelineMapOptions options) => _mapQueryBuilder( + assetFilter: (row) { + Expression condition = + row.deletedAt.isNull() & + row.ownerId.isIn(ownerIds) & + _db.remoteAssetEntity.visibility.isIn([ + AssetVisibility.timeline.index, + if (options.includeArchived) AssetVisibility.archive.index, + ]); + + if (options.onlyFavorites) { + 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); + } + + return condition; + }, ); MapQuery _mapQueryBuilder({Expression Function($RemoteAssetEntityTable row)? assetFilter}) { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index f57ef04b07..b0548bdd28 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -15,6 +15,22 @@ import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:stream_transform/stream_transform.dart'; +class TimelineMapOptions { + final LatLngBounds bounds; + final bool onlyFavorites; + final bool includeArchived; + final bool withPartners; + final int relativeDays; + + const TimelineMapOptions({ + required this.bounds, + this.onlyFavorites = false, + this.includeArchived = false, + this.withPartners = false, + this.relativeDays = 0, + }); +} + class DriftTimelineRepository extends DriftDatabaseRepository { final Drift _db; @@ -467,15 +483,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository { return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); } - TimelineQuery map(String userId, LatLngBounds bounds, GroupAssetsBy groupBy) => ( - bucketSource: () => _watchMapBucket(userId, bounds, groupBy: groupBy), - assetSource: (offset, count) => _getMapBucketAssets(userId, bounds, offset: offset, count: count), + TimelineQuery map(List userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchMapBucket(userIds, options, groupBy: groupBy), + assetSource: (offset, count) => _getMapBucketAssets(userIds, options, offset: offset, count: count), origin: TimelineOrigin.map, ); Stream> _watchMapBucket( - String userId, - LatLngBounds bounds, { + List userId, + TimelineMapOptions options, { GroupAssetsBy groupBy = GroupAssetsBy.day, }) { if (groupBy == GroupAssetsBy.none) { @@ -496,14 +512,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ), ]) ..where( - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteExifEntity.inBounds(bounds) & - _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & + _db.remoteAssetEntity.ownerId.isIn(userId) & + _db.remoteExifEntity.inBounds(options.bounds) & + _db.remoteAssetEntity.visibility.isIn([ + AssetVisibility.timeline.index, + if (options.includeArchived) AssetVisibility.archive.index, + ]) & _db.remoteAssetEntity.deletedAt.isNull(), ) ..groupBy([dateExp]) ..orderBy([OrderingTerm.desc(dateExp)]); + if (options.onlyFavorites) { + query.where(_db.remoteAssetEntity.isFavorite.equals(true)); + } + + if (options.relativeDays != 0) { + final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate)); + } + return query.map((row) { final timeline = row.read(dateExp)!.truncateDate(groupBy); final assetCount = row.read(assetCountExp)!; @@ -512,8 +540,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } Future> _getMapBucketAssets( - String userId, - LatLngBounds bounds, { + List userId, + TimelineMapOptions options, { required int offset, required int count, }) { @@ -526,13 +554,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ), ]) ..where( - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteExifEntity.inBounds(bounds) & - _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & + _db.remoteAssetEntity.ownerId.isIn(userId) & + _db.remoteExifEntity.inBounds(options.bounds) & + _db.remoteAssetEntity.visibility.isIn([ + AssetVisibility.timeline.index, + if (options.includeArchived) AssetVisibility.archive.index, + ]) & _db.remoteAssetEntity.deletedAt.isNull(), ) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..limit(count, offset: offset); + + if (options.onlyFavorites) { + query.where(_db.remoteAssetEntity.isFavorite.equals(true)); + } + + if (options.relativeDays != 0) { + final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate)); + } + return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); } diff --git a/mobile/lib/presentation/pages/drift_map.page.dart b/mobile/lib/presentation/pages/drift_map.page.dart index de8dde7714..96384c97e5 100644 --- a/mobile/lib/presentation/pages/drift_map.page.dart +++ b/mobile/lib/presentation/pages/drift_map.page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/map/map.widget.dart'; +import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() @@ -10,6 +11,16 @@ class DriftMapPage extends StatelessWidget { const DriftMapPage({super.key, this.initialLocation}); + void onSettingsPressed(BuildContext context) { + showModalBottomSheet( + elevation: 0.0, + showDragHandle: true, + isScrollControlled: true, + context: context, + builder: (_) => const DriftMapSettingsSheet(), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -18,8 +29,8 @@ class DriftMapPage extends StatelessWidget { children: [ DriftMap(initialLocation: initialLocation), Positioned( - left: 16, - top: 60, + left: 20, + top: 70, child: IconButton.filled( color: Colors.white, onPressed: () => context.pop(), @@ -32,6 +43,21 @@ class DriftMapPage extends StatelessWidget { ), ), ), + Positioned( + right: 20, + top: 70, + child: IconButton.filled( + color: Colors.white, + onPressed: () => onSettingsPressed(context), + icon: const Icon(Icons.more_vert_rounded), + style: IconButton.styleFrom( + padding: const EdgeInsets.all(8), + backgroundColor: Colors.indigo, + shadowColor: Colors.black26, + elevation: 4, + ), + ), + ), ], ), ); diff --git a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart index ac3772a02b..d7ef604718 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart @@ -14,7 +14,7 @@ class MapBottomSheet extends StatelessWidget { Widget build(BuildContext context) { return BaseBottomSheet( initialChildSize: 0.25, - maxChildSize: 0.9, + maxChildSize: 0.75, shouldCloseOnMinExtent: false, resizeOnScroll: false, actions: [], @@ -38,8 +38,13 @@ class _ScopedMapTimeline extends StatelessWidget { throw Exception('User must be logged in to access archive'); } - final bounds = ref.watch(mapStateProvider).bounds; - final timelineService = ref.watch(timelineFactoryProvider).map(user.id, bounds); + final users = ref.watch(mapStateProvider).withPartners + ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id] + : [user.id]; + + final timelineService = ref + .watch(timelineFactoryProvider) + .map(users, ref.watch(mapStateProvider).toOptions()); ref.onDispose(timelineService.dispose); return timelineService; }), diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index b849f954ae..bfd3011050 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -1,11 +1,30 @@ +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/utils/event_stream.dart'; +import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; +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:maplibre_gl/maplibre_gl.dart'; class MapState { + final ThemeMode themeMode; final LatLngBounds bounds; + final bool onlyFavorites; + final bool includeArchived; + final bool withPartners; + final int relativeDays; - const MapState({required this.bounds}); + const MapState({ + this.themeMode = ThemeMode.system, + required this.bounds, + this.onlyFavorites = false, + this.includeArchived = false, + this.withPartners = false, + this.relativeDays = 0, + }); @override bool operator ==(covariant MapState other) { @@ -15,9 +34,31 @@ class MapState { @override int get hashCode => bounds.hashCode; - MapState copyWith({LatLngBounds? bounds}) { - return MapState(bounds: bounds ?? this.bounds); + MapState copyWith({ + LatLngBounds? bounds, + ThemeMode? themeMode, + bool? onlyFavorites, + bool? includeArchived, + bool? withPartners, + int? relativeDays, + }) { + return MapState( + bounds: bounds ?? this.bounds, + themeMode: themeMode ?? this.themeMode, + onlyFavorites: onlyFavorites ?? this.onlyFavorites, + includeArchived: includeArchived ?? this.includeArchived, + withPartners: withPartners ?? this.withPartners, + relativeDays: relativeDays ?? this.relativeDays, + ); } + + TimelineMapOptions toOptions() => TimelineMapOptions( + bounds: bounds, + onlyFavorites: onlyFavorites, + includeArchived: includeArchived, + withPartners: withPartners, + relativeDays: relativeDays, + ); } class MapStateNotifier extends Notifier { @@ -31,11 +72,50 @@ class MapStateNotifier extends Notifier { return true; } + void switchTheme(ThemeMode mode) { + // TODO: Remove this line when map theme provider is removed + // Until then, keep both in sync as MapThemeOverride uses map state provider + // ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapThemeMode, mode.index); + ref.read(mapStateNotifierProvider.notifier).switchTheme(mode); + state = state.copyWith(themeMode: mode); + } + + void switchFavoriteOnly(bool isFavoriteOnly) { + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapShowFavoriteOnly, isFavoriteOnly); + state = state.copyWith(onlyFavorites: isFavoriteOnly); + EventStream.shared.emit(const MapMarkerReloadEvent()); + } + + void switchIncludeArchived(bool isIncludeArchived) { + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapIncludeArchived, isIncludeArchived); + state = state.copyWith(includeArchived: isIncludeArchived); + EventStream.shared.emit(const MapMarkerReloadEvent()); + } + + void switchWithPartners(bool isWithPartners) { + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapwithPartners, isWithPartners); + state = state.copyWith(withPartners: isWithPartners); + 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()); + } + @override - MapState build() => MapState( - // TODO: set default bounds - bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)), - ); + MapState build() { + final appSettingsService = ref.read(appSettingsServiceProvider); + return MapState( + themeMode: ThemeMode.values[appSettingsService.getSetting(AppSettingsEnum.mapThemeMode)], + 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)), + ); + } } // This provider watches the markers from the map service and serves the markers. diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 17dcffdade..72f4e8bda6 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -6,6 +6,8 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:geolocator/geolocator.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/events.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; @@ -51,11 +53,19 @@ class _DriftMapState extends ConsumerState { final _reloadMutex = AsyncMutex(); final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2)); final ValueNotifier bottomSheetOffset = ValueNotifier(0.25); + StreamSubscription? _eventSubscription; + + @override + void initState() { + super.initState(); + _eventSubscription = EventStream.shared.listen(_onEvent); + } @override void dispose() { _debouncer.dispose(); bottomSheetOffset.dispose(); + _eventSubscription?.cancel(); super.dispose(); } @@ -63,6 +73,8 @@ class _DriftMapState extends ConsumerState { mapController = controller; } + void _onEvent(_) => _debouncer.run(() => setBounds(forceReload: true)); + Future onMapReady() async { final controller = mapController; if (controller == null) { @@ -98,7 +110,7 @@ class _DriftMapState extends ConsumerState { ); } - _debouncer.run(setBounds); + _debouncer.run(() => setBounds(forceReload: true)); controller.addListener(onMapMoved); } @@ -110,7 +122,7 @@ class _DriftMapState extends ConsumerState { _debouncer.run(setBounds); } - Future setBounds() async { + Future setBounds({bool forceReload = false}) async { final controller = mapController; if (controller == null || !mounted) { return; @@ -127,7 +139,7 @@ class _DriftMapState extends ConsumerState { final bounds = await controller.getVisibleRegion(); unawaited( _reloadMutex.run(() async { - if (mounted && ref.read(mapStateProvider.notifier).setBounds(bounds)) { + if (mounted && (ref.read(mapStateProvider.notifier).setBounds(bounds) || forceReload)) { final markers = await ref.read(mapMarkerProvider(bounds).future); await reloadMarkers(markers); } @@ -203,7 +215,7 @@ class _Map extends StatelessWidget { onMapCreated: onMapCreated, onStyleLoadedCallback: onMapReady, attributionButtonPosition: AttributionButtonPosition.topRight, - attributionButtonMargins: Platform.isIOS ? const Point(40, 12) : const Point(40, 72), + attributionButtonMargins: const Point(8, kToolbarHeight), ), ), ); @@ -244,7 +256,7 @@ class _DynamicMyLocationButton extends StatelessWidget { valueListenable: bottomSheetOffset, builder: (context, offset, child) { return Positioned( - right: 16, + right: 20, bottom: context.height * (offset - 0.02) + context.padding.bottom, child: AnimatedOpacity( opacity: offset < 0.8 ? 1 : 0, diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart new file mode 100644 index 0000000000..c581dd6292 --- /dev/null +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -0,0 +1,61 @@ +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_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 { + const DriftMapSettingsSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mapState = ref.watch(mapStateProvider); + + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.6, + builder: (ctx, scrollController) => SingleChildScrollView( + controller: scrollController, + child: Card( + elevation: 0.0, + shadowColor: Colors.transparent, + color: Colors.transparent, + margin: EdgeInsets.zero, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + MapThemePicker( + themeMode: mapState.themeMode, + onThemeChange: (mode) => ref.read(mapStateProvider.notifier).switchTheme(mode), + ), + const Divider(height: 30, thickness: 1), + MapSettingsListTile( + title: "map_settings_only_show_favorites".t(context: context), + selected: mapState.onlyFavorites, + onChanged: (favoriteOnly) => ref.read(mapStateProvider.notifier).switchFavoriteOnly(favoriteOnly), + ), + MapSettingsListTile( + title: "map_settings_include_show_archived".t(context: context), + selected: mapState.includeArchived, + onChanged: (includeArchive) => + ref.read(mapStateProvider.notifier).switchIncludeArchived(includeArchive), + ), + MapSettingsListTile( + title: "map_settings_include_show_partners".t(context: context), + selected: mapState.withPartners, + onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners), + ), + MapTimeDropDown( + relativeTime: mapState.relativeDays, + onTimeChange: (time) => ref.read(mapStateProvider.notifier).setRelativeTime(time), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/providers/infrastructure/map.provider.dart b/mobile/lib/providers/infrastructure/map.provider.dart index e774cec756..d9d261521e 100644 --- a/mobile/lib/providers/infrastructure/map.provider.dart +++ b/mobile/lib/providers/infrastructure/map.provider.dart @@ -1,7 +1,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/domain/services/map.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; +import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; final mapRepositoryProvider = Provider((ref) => DriftMapRepository(ref.watch(driftProvider))); @@ -13,7 +15,11 @@ final mapServiceProvider = Provider( throw Exception('User must be logged in to access map'); } - final mapService = ref.watch(mapFactoryProvider).remote(user.id); + final users = ref.watch(mapStateProvider).withPartners + ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id] + : [user.id]; + + final mapService = ref.watch(mapFactoryProvider).remote(users, ref.watch(mapStateProvider).toOptions()); return mapService; }, // Empty dependencies to inform the framework that this provider diff --git a/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart b/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart index e97875fd90..762c402def 100644 --- a/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart +++ b/mobile/lib/widgets/map/map_settings/map_settings_list_tile.dart @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -14,7 +13,7 @@ class MapSettingsListTile extends StatelessWidget { Widget build(BuildContext context) { return SwitchListTile.adaptive( activeThumbColor: context.primaryColor, - title: Text(title, style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold)).tr(), + title: Text(title, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5)), value: selected, onChanged: onChanged, ); diff --git a/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart b/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart index b601887e1e..2a4dacaff7 100644 --- a/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart +++ b/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart @@ -1,5 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; class MapTimeDropDown extends StatelessWidget { final int relativeTime; @@ -11,41 +13,47 @@ class MapTimeDropDown extends StatelessWidget { Widget build(BuildContext context) { final now = DateTime.now(); - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text("date_range".tr(), style: const TextStyle(fontWeight: FontWeight.bold)), - ), - LayoutBuilder( - builder: (_, constraints) => DropdownMenu( - width: constraints.maxWidth * 0.9, - enableSearch: false, - enableFilter: false, - initialSelection: relativeTime, - onSelected: (value) => onTimeChange(value!), - dropdownMenuEntries: [ - DropdownMenuEntry(value: 0, label: "all".tr()), - DropdownMenuEntry(value: 1, label: "map_settings_date_range_option_day".tr()), - DropdownMenuEntry(value: 7, label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "7"})), - DropdownMenuEntry(value: 30, label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "30"})), - DropdownMenuEntry( - value: now - .difference(DateTime(now.year - 1, now.month, now.day, now.hour, now.minute, now.second)) - .inDays, - label: "map_settings_date_range_option_year".tr(), - ), - DropdownMenuEntry( - value: now - .difference(DateTime(now.year - 3, now.month, now.day, now.hour, now.minute, now.second)) - .inDays, - label: "map_settings_date_range_option_years".tr(namedArgs: {'years': "3"}), - ), - ], + return Padding( + padding: const EdgeInsets.only(left: 16, right: 28.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "date_range".t(context: context), + style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), - ), - ], + Flexible( + child: DropdownMenu( + enableSearch: false, + enableFilter: false, + initialSelection: relativeTime, + onSelected: (value) => onTimeChange(value!), + dropdownMenuEntries: [ + DropdownMenuEntry(value: 0, label: "all".t(context: context)), + DropdownMenuEntry(value: 1, label: "map_settings_date_range_option_day".t(context: context)), + DropdownMenuEntry(value: 7, label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "7"})), + DropdownMenuEntry( + value: 30, + label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "30"}), + ), + DropdownMenuEntry( + value: now + .difference(DateTime(now.year - 1, now.month, now.day, now.hour, now.minute, now.second)) + .inDays, + label: "map_settings_date_range_option_year".t(context: context), + ), + DropdownMenuEntry( + value: now + .difference(DateTime(now.year - 3, now.month, now.day, now.hour, now.minute, now.second)) + .inDays, + label: "map_settings_date_range_option_years".t(args: {'years': "3"}), + ), + ], + ), + ), + ], + ), ); } } diff --git a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart index 63f35ebe4c..7866c0ecdc 100644 --- a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart +++ b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -18,9 +18,9 @@ class MapThemePicker extends StatelessWidget { padding: const EdgeInsets.only(bottom: 20), child: Center( child: Text( - "map_settings_theme_settings", - style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), - ).tr(), + "map_settings_theme_settings".t(context: context), + style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), + ), ), ), Row(