diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 1852fb7877..c08a1c715d 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; @@ -12,6 +13,7 @@ import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/utils/map_utils.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/user_avatar.dart'; @@ -297,32 +299,34 @@ class LocalAlbumsCollectionCard extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( + SizedBox( height: size, width: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: LinearGradient( - colors: [ - context.colorScheme.primary.withAlpha(30), - context.colorScheme.primary.withAlpha(25), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withAlpha(30), + context.colorScheme.primary.withAlpha(25), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + physics: const NeverScrollableScrollPhysics(), + children: albums.take(4).map((album) { + return AlbumThumbnailCard( + album: album, + showTitle: false, + ); + }).toList(), ), - ), - child: GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: albums.take(4).map((album) { - return AlbumThumbnailCard( - album: album, - showTitle: false, - ); - }).toList(), ), ), Padding( @@ -353,43 +357,66 @@ class PlacesCollectionCard extends StatelessWidget { final widthFactor = isTablet ? 0.25 : 0.5; final size = context.width * widthFactor - 20.0; - return GestureDetector( - onTap: () => context.pushRoute(const PlacesCollectionRoute()), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: context.colorScheme.secondaryContainer.withAlpha(100), - ), - child: IgnorePointer( - child: MapThumbnail( - zoom: 8, - centre: const LatLng( - 21.44950, - -157.91959, - ), - showAttribution: false, - themeMode: - context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'places'.tr(), - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], + return FutureBuilder<(Position?, LocationPermission?)>( + future: MapUtils.checkPermAndGetLocation( + context: context, + silent: true, ), + builder: (context, snapshot) { + var position = snapshot.data?.$1; + return GestureDetector( + onTap: () => context.pushRoute( + PlacesCollectionRoute( + currentLocation: position != null + ? LatLng(position.latitude, position.longitude) + : null, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: size, + width: size, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(20)), + color: context.colorScheme.secondaryContainer + .withAlpha(100), + ), + child: IgnorePointer( + child: snapshot.connectionState == + ConnectionState.waiting + ? const Center(child: CircularProgressIndicator()) + : MapThumbnail( + zoom: 8, + centre: LatLng( + position?.latitude ?? 21.44950, + position?.longitude ?? -157.91959, + ), + showAttribution: false, + themeMode: context.isDarkTheme + ? ThemeMode.dark + : ThemeMode.light, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'places'.tr(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + }, ); }, ); diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart index f9a2d4292c..5f2dea0dec 100644 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ b/mobile/lib/pages/library/places/places_collection.page.dart @@ -19,7 +19,8 @@ import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() class PlacesCollectionPage extends HookConsumerWidget { - const PlacesCollectionPage({super.key}); + const PlacesCollectionPage({super.key, this.currentLocation}); + final LatLng? currentLocation; @override Widget build(BuildContext context, WidgetRef ref) { final places = ref.watch(getAllPlacesProvider); @@ -58,12 +59,14 @@ class PlacesCollectionPage extends HookConsumerWidget { height: 200, width: context.width, child: MapThumbnail( - onTap: (_, __) => context.pushRoute(const MapRoute()), + onTap: (_, __) => context + .pushRoute(MapRoute(initialLocation: currentLocation)), zoom: 8, - centre: const LatLng( - 21.44950, - -157.91959, - ), + centre: currentLocation ?? + const LatLng( + 21.44950, + -157.91959, + ), showAttribution: false, themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index 0e64759241..b80b96f94f 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -34,7 +34,8 @@ import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() class MapPage extends HookConsumerWidget { - const MapPage({super.key}); + const MapPage({super.key, this.initialLocation}); + final LatLng? initialLocation; @override Widget build(BuildContext context, WidgetRef ref) { @@ -235,7 +236,8 @@ class MapPage extends HookConsumerWidget { } void onZoomToLocation() async { - final (location, error) = await MapUtils.checkPermAndGetLocation(context); + final (location, error) = + await MapUtils.checkPermAndGetLocation(context: context); if (error != null) { if (error == LocationPermission.unableToDetermine && context.mounted) { ImmichToast.show( @@ -272,6 +274,7 @@ class MapPage extends HookConsumerWidget { body: Stack( children: [ _MapWithMarker( + initialLocation: initialLocation, style: style, selectedMarker: selectedMarker, onMapCreated: onMapCreated, @@ -303,6 +306,7 @@ class MapPage extends HookConsumerWidget { body: Stack( children: [ _MapWithMarker( + initialLocation: initialLocation, style: style, selectedMarker: selectedMarker, onMapCreated: onMapCreated, @@ -368,6 +372,7 @@ class _MapWithMarker extends StatelessWidget { final OnStyleLoadedCallback onStyleLoaded; final Function()? onMarkerTapped; final ValueNotifier<_AssetMarkerMeta?> selectedMarker; + final LatLng? initialLocation; const _MapWithMarker({ required this.style, @@ -377,6 +382,7 @@ class _MapWithMarker extends StatelessWidget { required this.onStyleLoaded, required this.selectedMarker, this.onMarkerTapped, + this.initialLocation, }); @override @@ -389,8 +395,10 @@ class _MapWithMarker extends StatelessWidget { children: [ style.widgetWhen( onData: (style) => MapLibreMap( - initialCameraPosition: - const CameraPosition(target: LatLng(0, 0)), + initialCameraPosition: CameraPosition( + target: initialLocation ?? const LatLng(0, 0), + zoom: initialLocation != null ? 12 : 0, + ), styleString: style, // This is needed to update the selectedMarker's position on map camera updates // The changes are notified through the mapController ValueListener which is added in [onMapCreated] diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index 9d526d8080..f27deae052 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -46,7 +46,7 @@ class MapLocationPickerPage extends HookConsumerWidget { Future getCurrentLocation() async { var (currentLocation, _) = - await MapUtils.checkPermAndGetLocation(context); + await MapUtils.checkPermAndGetLocation(context: context); if (currentLocation == null) { return; diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index a78371e05e..89e83e8159 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1024,10 +1024,17 @@ class MapLocationPickerRouteArgs { /// generated route for /// [MapPage] -class MapRoute extends PageRouteInfo { - const MapRoute({List? children}) - : super( +class MapRoute extends PageRouteInfo { + MapRoute({ + Key? key, + LatLng? initialLocation, + List? children, + }) : super( MapRoute.name, + args: MapRouteArgs( + key: key, + initialLocation: initialLocation, + ), initialChildren: children, ); @@ -1036,11 +1043,32 @@ class MapRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - return const MapPage(); + final args = + data.argsAs(orElse: () => const MapRouteArgs()); + return MapPage( + key: args.key, + initialLocation: args.initialLocation, + ); }, ); } +class MapRouteArgs { + const MapRouteArgs({ + this.key, + this.initialLocation, + }); + + final Key? key; + + final LatLng? initialLocation; + + @override + String toString() { + return 'MapRouteArgs{key: $key, initialLocation: $initialLocation}'; + } +} + /// generated route for /// [MemoryPage] class MemoryRoute extends PageRouteInfo { @@ -1333,10 +1361,17 @@ class PhotosRoute extends PageRouteInfo { /// generated route for /// [PlacesCollectionPage] -class PlacesCollectionRoute extends PageRouteInfo { - const PlacesCollectionRoute({List? children}) - : super( +class PlacesCollectionRoute extends PageRouteInfo { + PlacesCollectionRoute({ + Key? key, + LatLng? currentLocation, + List? children, + }) : super( PlacesCollectionRoute.name, + args: PlacesCollectionRouteArgs( + key: key, + currentLocation: currentLocation, + ), initialChildren: children, ); @@ -1345,11 +1380,32 @@ class PlacesCollectionRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - return const PlacesCollectionPage(); + final args = data.argsAs( + orElse: () => const PlacesCollectionRouteArgs()); + return PlacesCollectionPage( + key: args.key, + currentLocation: args.currentLocation, + ); }, ); } +class PlacesCollectionRouteArgs { + const PlacesCollectionRouteArgs({ + this.key, + this.currentLocation, + }); + + final Key? key; + + final LatLng? currentLocation; + + @override + String toString() { + return 'PlacesCollectionRouteArgs{key: $key, currentLocation: $currentLocation}'; + } +} + /// generated route for /// [RecentlyAddedPage] class RecentlyAddedRoute extends PageRouteInfo { diff --git a/mobile/lib/utils/map_utils.dart b/mobile/lib/utils/map_utils.dart index 44f7ebf271..df1ff28d8f 100644 --- a/mobile/lib/utils/map_utils.dart +++ b/mobile/lib/utils/map_utils.dart @@ -64,12 +64,13 @@ class MapUtils { 'features': markers.map(_addFeature).toList(), }; - static Future<(Position?, LocationPermission?)> checkPermAndGetLocation( - BuildContext context, - ) async { + static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({ + required BuildContext context, + bool silent = false, + }) async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); - if (!serviceEnabled) { + if (!serviceEnabled && !silent) { showDialog( context: context, builder: (context) => _LocationServiceDisabledDialog(), @@ -80,7 +81,7 @@ class MapUtils { LocationPermission permission = await Geolocator.checkPermission(); bool shouldRequestPermission = false; - if (permission == LocationPermission.denied) { + if (permission == LocationPermission.denied && !silent) { shouldRequestPermission = await showDialog( context: context, builder: (context) => _LocationPermissionDisabledDialog(), @@ -94,15 +95,19 @@ class MapUtils { permission == LocationPermission.deniedForever) { // Open app settings only if you did not request for permission before if (permission == LocationPermission.deniedForever && - !shouldRequestPermission) { + !shouldRequestPermission && + !silent) { await Geolocator.openAppSettings(); } return (null, LocationPermission.deniedForever); } Position currentUserLocation = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.medium, - timeLimit: const Duration(seconds: 5), + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 0, + timeLimit: Duration(seconds: 5), + ), ); return (currentUserLocation, null); } catch (error, stack) { diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart index 18003cf293..a9ddc86df9 100644 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ b/mobile/lib/widgets/map/map_asset_grid.dart @@ -46,12 +46,39 @@ class MapAssetGrid extends HookConsumerWidget { final gridScrollThrottler = useThrottler(interval: const Duration(milliseconds: 300)); + // Add a cache for assets we've already loaded + final assetCache = useRef>({}); + void handleMapEvents(MapEvent event) async { if (event is MapAssetsInBoundsUpdated) { - assetsInBounds.value = await ref - .read(dbProvider) - .assets - .getAllByRemoteId(event.assetRemoteIds); + final assetIds = event.assetRemoteIds; + final missingIds = []; + final currentAssets = []; + + for (final id in assetIds) { + final asset = assetCache.value[id]; + if (asset != null) { + currentAssets.add(asset); + } else { + missingIds.add(id); + } + } + + // Only fetch missing assets + if (missingIds.isNotEmpty) { + final newAssets = + await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); + + // Add new assets to cache and current list + for (final asset in newAssets) { + if (asset.remoteId != null) { + assetCache.value[asset.remoteId!] = asset; + currentAssets.add(asset); + } + } + } + + assetsInBounds.value = currentAssets; return; } } @@ -124,7 +151,7 @@ class MapAssetGrid extends HookConsumerWidget { alignment: Alignment.bottomCenter, child: FractionallySizedBox( // Place it just below the drag handle - heightFactor: 0.80, + heightFactor: 0.87, child: assetsInBounds.value.isNotEmpty ? ref .watch(assetsTimelineProvider(assetsInBounds.value)) @@ -251,8 +278,18 @@ class _MapSheetDragRegion extends StatelessWidget { const SizedBox(height: 15), const CustomDraggingHandle(), const SizedBox(height: 15), - Text(assetsInBoundsText, style: context.textTheme.bodyLarge), - const Divider(height: 35), + Center( + child: Text( + assetsInBoundsText, + style: TextStyle( + fontSize: 20, + color: context.textTheme.displayLarge?.color + ?.withValues(alpha: 0.75), + fontWeight: FontWeight.w500, + ), + ), + ), + const SizedBox(height: 8), ], ), ValueListenableBuilder( @@ -260,14 +297,14 @@ class _MapSheetDragRegion extends StatelessWidget { builder: (_, value, __) => Visibility( visible: value != null, child: Positioned( - right: 15, - top: 15, + right: 18, + top: 24, child: IconButton( icon: Icon( Icons.map_outlined, color: context.textTheme.displayLarge?.color, ), - iconSize: 20, + iconSize: 24, tooltip: 'Zoom to bounds', onPressed: () => onZoomToAsset?.call(value!), ), diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart index b4a12ab826..78af8f936b 100644 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ b/mobile/lib/widgets/search/search_map_thumbnail.dart @@ -20,7 +20,7 @@ class SearchMapThumbnail extends StatelessWidget { return ThumbnailWithInfoContainer( label: 'search_page_your_map'.tr(), onTap: () { - context.pushRoute(const MapRoute()); + context.pushRoute(MapRoute()); }, child: IgnorePointer( child: MapThumbnail( diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 235b3f71c3..9e8aced11c 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -696,18 +696,18 @@ packages: dependency: "direct main" description: name: geolocator - sha256: "6cb9fb6e5928b58b9a84bdf85012d757fd07aab8215c5205337021c4999bad27" + sha256: e7ebfa04ce451daf39b5499108c973189a71a919aa53c1204effda1c5b93b822 url: "https://pub.dev" source: hosted - version: "11.1.0" + version: "14.0.0" geolocator_android: dependency: transitive description: name: geolocator_android - sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" + sha256: "114072db5d1dce0ec0b36af2697f55c133bc89a2c8dd513e137c0afe59696ed4" url: "https://pub.dev" source: hosted - version: "4.6.1" + version: "5.0.1+1" geolocator_apple: dependency: transitive description: @@ -728,10 +728,10 @@ packages: dependency: transitive description: name: geolocator_web - sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed" + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.1.3" geolocator_windows: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index fdd91e1f87..44d2e7e5d1 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: flutter_udid: ^3.0.0 flutter_web_auth_2: ^5.0.0-alpha.0 fluttertoast: ^8.2.12 - geolocator: ^11.0.0 + geolocator: ^14.0.0 hooks_riverpod: ^2.6.1 http: ^1.3.0 image_picker: ^1.1.2