fix(mobile): bring back map settings (#25448)

* fix(mobile): bring back map settings

* chore: styling

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong
2026-01-23 03:21:48 +05:30
committed by GitHub
parent bccad2940e
commit 9b2939d778
14 changed files with 343 additions and 80 deletions
@@ -30,3 +30,8 @@ class MultiSelectToggleEvent extends Event {
final bool isEnabled; final bool isEnabled;
const MultiSelectToggleEvent(this.isEnabled); const MultiSelectToggleEvent(this.isEnabled);
} }
// Map Events
class MapMarkerReloadEvent extends Event {
const MapMarkerReloadEvent();
}
+3 -1
View File
@@ -1,5 +1,6 @@
import 'package:immich_mobile/domain/models/map.model.dart'; import 'package:immich_mobile/domain/models/map.model.dart';
import 'package:immich_mobile/infrastructure/repositories/map.repository.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'; import 'package:maplibre_gl/maplibre_gl.dart';
typedef MapMarkerSource = Future<List<Marker>> Function(LatLngBounds? bounds); typedef MapMarkerSource = Future<List<Marker>> Function(LatLngBounds? bounds);
@@ -11,7 +12,8 @@ class MapFactory {
const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository; const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository;
MapService remote(String ownerId) => MapService(_mapRepository.remote(ownerId)); MapService remote(List<String> ownerIds, TimelineMapOptions options) =>
MapService(_mapRepository.remote(ownerIds, options));
} }
class MapService { class MapService {
@@ -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/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
typedef TimelineAssetSource = Future<List<BaseAsset>> Function(int index, int count); typedef TimelineAssetSource = Future<List<BaseAsset>> Function(int index, int count);
@@ -82,8 +81,8 @@ class TimelineFactory {
TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) => TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) =>
TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type)); TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type));
TimelineService map(String userId, LatLngBounds bounds) => TimelineService map(List<String> userIds, TimelineMapOptions options) =>
TimelineService(_timelineRepository.map(userId, bounds, groupBy)); TimelineService(_timelineRepository.map(userIds, options, groupBy));
} }
class TimelineService { class TimelineService {
@@ -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/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.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/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
class DriftMapRepository extends DriftDatabaseRepository { class DriftMapRepository extends DriftDatabaseRepository {
@@ -12,9 +13,27 @@ class DriftMapRepository extends DriftDatabaseRepository {
const DriftMapRepository(super._db) : _db = _db; const DriftMapRepository(super._db) : _db = _db;
MapQuery remote(String ownerId) => _mapQueryBuilder( MapQuery remote(List<String> ownerIds, TimelineMapOptions options) => _mapQueryBuilder(
assetFilter: (row) => assetFilter: (row) {
row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId), Expression<bool> 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<bool> Function($RemoteAssetEntityTable row)? assetFilter}) { MapQuery _mapQueryBuilder({Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter}) {
@@ -15,6 +15,22 @@ import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:stream_transform/stream_transform.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 { class DriftTimelineRepository extends DriftDatabaseRepository {
final Drift _db; final Drift _db;
@@ -467,15 +483,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
} }
TimelineQuery map(String userId, LatLngBounds bounds, GroupAssetsBy groupBy) => ( TimelineQuery map(List<String> userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => (
bucketSource: () => _watchMapBucket(userId, bounds, groupBy: groupBy), bucketSource: () => _watchMapBucket(userIds, options, groupBy: groupBy),
assetSource: (offset, count) => _getMapBucketAssets(userId, bounds, offset: offset, count: count), assetSource: (offset, count) => _getMapBucketAssets(userIds, options, offset: offset, count: count),
origin: TimelineOrigin.map, origin: TimelineOrigin.map,
); );
Stream<List<Bucket>> _watchMapBucket( Stream<List<Bucket>> _watchMapBucket(
String userId, List<String> userId,
LatLngBounds bounds, { TimelineMapOptions options, {
GroupAssetsBy groupBy = GroupAssetsBy.day, GroupAssetsBy groupBy = GroupAssetsBy.day,
}) { }) {
if (groupBy == GroupAssetsBy.none) { if (groupBy == GroupAssetsBy.none) {
@@ -496,14 +512,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
), ),
]) ])
..where( ..where(
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.isIn(userId) &
_db.remoteExifEntity.inBounds(bounds) & _db.remoteExifEntity.inBounds(options.bounds) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & _db.remoteAssetEntity.visibility.isIn([
AssetVisibility.timeline.index,
if (options.includeArchived) AssetVisibility.archive.index,
]) &
_db.remoteAssetEntity.deletedAt.isNull(), _db.remoteAssetEntity.deletedAt.isNull(),
) )
..groupBy([dateExp]) ..groupBy([dateExp])
..orderBy([OrderingTerm.desc(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) { return query.map((row) {
final timeline = row.read(dateExp)!.truncateDate(groupBy); final timeline = row.read(dateExp)!.truncateDate(groupBy);
final assetCount = row.read(assetCountExp)!; final assetCount = row.read(assetCountExp)!;
@@ -512,8 +540,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
} }
Future<List<BaseAsset>> _getMapBucketAssets( Future<List<BaseAsset>> _getMapBucketAssets(
String userId, List<String> userId,
LatLngBounds bounds, { TimelineMapOptions options, {
required int offset, required int offset,
required int count, required int count,
}) { }) {
@@ -526,13 +554,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
), ),
]) ])
..where( ..where(
_db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.ownerId.isIn(userId) &
_db.remoteExifEntity.inBounds(bounds) & _db.remoteExifEntity.inBounds(options.bounds) &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & _db.remoteAssetEntity.visibility.isIn([
AssetVisibility.timeline.index,
if (options.includeArchived) AssetVisibility.archive.index,
]) &
_db.remoteAssetEntity.deletedAt.isNull(), _db.remoteAssetEntity.deletedAt.isNull(),
) )
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset); ..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(); return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
} }
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
@RoutePage() @RoutePage()
@@ -10,6 +11,16 @@ class DriftMapPage extends StatelessWidget {
const DriftMapPage({super.key, this.initialLocation}); const DriftMapPage({super.key, this.initialLocation});
void onSettingsPressed(BuildContext context) {
showModalBottomSheet(
elevation: 0.0,
showDragHandle: true,
isScrollControlled: true,
context: context,
builder: (_) => const DriftMapSettingsSheet(),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -18,8 +29,8 @@ class DriftMapPage extends StatelessWidget {
children: [ children: [
DriftMap(initialLocation: initialLocation), DriftMap(initialLocation: initialLocation),
Positioned( Positioned(
left: 16, left: 20,
top: 60, top: 70,
child: IconButton.filled( child: IconButton.filled(
color: Colors.white, color: Colors.white,
onPressed: () => context.pop(), 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,
),
),
),
], ],
), ),
); );
@@ -14,7 +14,7 @@ class MapBottomSheet extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BaseBottomSheet( return BaseBottomSheet(
initialChildSize: 0.25, initialChildSize: 0.25,
maxChildSize: 0.9, maxChildSize: 0.75,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
resizeOnScroll: false, resizeOnScroll: false,
actions: [], actions: [],
@@ -38,8 +38,13 @@ class _ScopedMapTimeline extends StatelessWidget {
throw Exception('User must be logged in to access archive'); throw Exception('User must be logged in to access archive');
} }
final bounds = ref.watch(mapStateProvider).bounds; final users = ref.watch(mapStateProvider).withPartners
final timelineService = ref.watch(timelineFactoryProvider).map(user.id, bounds); ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id]
: [user.id];
final timelineService = ref
.watch(timelineFactoryProvider)
.map(users, ref.watch(mapStateProvider).toOptions());
ref.onDispose(timelineService.dispose); ref.onDispose(timelineService.dispose);
return timelineService; return timelineService;
}), }),
@@ -1,11 +1,30 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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'; import 'package:maplibre_gl/maplibre_gl.dart';
class MapState { class MapState {
final ThemeMode themeMode;
final LatLngBounds bounds; 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 @override
bool operator ==(covariant MapState other) { bool operator ==(covariant MapState other) {
@@ -15,9 +34,31 @@ class MapState {
@override @override
int get hashCode => bounds.hashCode; int get hashCode => bounds.hashCode;
MapState copyWith({LatLngBounds? bounds}) { MapState copyWith({
return MapState(bounds: bounds ?? this.bounds); 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<MapState> { class MapStateNotifier extends Notifier<MapState> {
@@ -31,11 +72,50 @@ class MapStateNotifier extends Notifier<MapState> {
return true; 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 @override
MapState build() => MapState( MapState build() {
// TODO: set default bounds final appSettingsService = ref.read(appSettingsServiceProvider);
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)), 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. // This provider watches the markers from the map service and serves the markers.
@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
@@ -51,11 +53,19 @@ class _DriftMapState extends ConsumerState<DriftMap> {
final _reloadMutex = AsyncMutex(); final _reloadMutex = AsyncMutex();
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2)); final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25); final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
StreamSubscription? _eventSubscription;
@override
void initState() {
super.initState();
_eventSubscription = EventStream.shared.listen<MapMarkerReloadEvent>(_onEvent);
}
@override @override
void dispose() { void dispose() {
_debouncer.dispose(); _debouncer.dispose();
bottomSheetOffset.dispose(); bottomSheetOffset.dispose();
_eventSubscription?.cancel();
super.dispose(); super.dispose();
} }
@@ -63,6 +73,8 @@ class _DriftMapState extends ConsumerState<DriftMap> {
mapController = controller; mapController = controller;
} }
void _onEvent(_) => _debouncer.run(() => setBounds(forceReload: true));
Future<void> onMapReady() async { Future<void> onMapReady() async {
final controller = mapController; final controller = mapController;
if (controller == null) { if (controller == null) {
@@ -98,7 +110,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
); );
} }
_debouncer.run(setBounds); _debouncer.run(() => setBounds(forceReload: true));
controller.addListener(onMapMoved); controller.addListener(onMapMoved);
} }
@@ -110,7 +122,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
_debouncer.run(setBounds); _debouncer.run(setBounds);
} }
Future<void> setBounds() async { Future<void> setBounds({bool forceReload = false}) async {
final controller = mapController; final controller = mapController;
if (controller == null || !mounted) { if (controller == null || !mounted) {
return; return;
@@ -127,7 +139,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
final bounds = await controller.getVisibleRegion(); final bounds = await controller.getVisibleRegion();
unawaited( unawaited(
_reloadMutex.run(() async { _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); final markers = await ref.read(mapMarkerProvider(bounds).future);
await reloadMarkers(markers); await reloadMarkers(markers);
} }
@@ -203,7 +215,7 @@ class _Map extends StatelessWidget {
onMapCreated: onMapCreated, onMapCreated: onMapCreated,
onStyleLoadedCallback: onMapReady, onStyleLoadedCallback: onMapReady,
attributionButtonPosition: AttributionButtonPosition.topRight, 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, valueListenable: bottomSheetOffset,
builder: (context, offset, child) { builder: (context, offset, child) {
return Positioned( return Positioned(
right: 16, right: 20,
bottom: context.height * (offset - 0.02) + context.padding.bottom, bottom: context.height * (offset - 0.02) + context.padding.bottom,
child: AnimatedOpacity( child: AnimatedOpacity(
opacity: offset < 0.8 ? 1 : 0, opacity: offset < 0.8 ? 1 : 0,
@@ -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),
],
),
),
),
);
}
}
@@ -1,7 +1,9 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/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'; import 'package:immich_mobile/providers/user.provider.dart';
final mapRepositoryProvider = Provider<DriftMapRepository>((ref) => DriftMapRepository(ref.watch(driftProvider))); final mapRepositoryProvider = Provider<DriftMapRepository>((ref) => DriftMapRepository(ref.watch(driftProvider)));
@@ -13,7 +15,11 @@ final mapServiceProvider = Provider<MapService>(
throw Exception('User must be logged in to access map'); 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; return mapService;
}, },
// Empty dependencies to inform the framework that this provider // Empty dependencies to inform the framework that this provider
@@ -1,4 +1,3 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -14,7 +13,7 @@ class MapSettingsListTile extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SwitchListTile.adaptive( return SwitchListTile.adaptive(
activeThumbColor: context.primaryColor, 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, value: selected,
onChanged: onChanged, onChanged: onChanged,
); );
@@ -1,5 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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 { class MapTimeDropDown extends StatelessWidget {
final int relativeTime; final int relativeTime;
@@ -11,41 +13,47 @@ class MapTimeDropDown extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final now = DateTime.now(); final now = DateTime.now();
return Column( return Padding(
crossAxisAlignment: CrossAxisAlignment.center, padding: const EdgeInsets.only(left: 16, right: 28.0),
children: [ child: Row(
Padding( crossAxisAlignment: CrossAxisAlignment.center,
padding: const EdgeInsets.only(bottom: 20), mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Text("date_range".tr(), style: const TextStyle(fontWeight: FontWeight.bold)), children: [
), Text(
LayoutBuilder( "date_range".t(context: context),
builder: (_, constraints) => DropdownMenu( style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5),
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"}),
),
],
), ),
), 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"}),
),
],
),
),
],
),
); );
} }
} }
@@ -1,6 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
@@ -18,9 +18,9 @@ class MapThemePicker extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.only(bottom: 20),
child: Center( child: Center(
child: Text( child: Text(
"map_settings_theme_settings", "map_settings_theme_settings".t(context: context),
style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5),
).tr(), ),
), ),
), ),
Row( Row(