diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json
index 4eb8693475..f205f22620 100644
--- a/mobile/assets/i18n/en-US.json
+++ b/mobile/assets/i18n/en-US.json
@@ -253,7 +253,7 @@
   "map_no_assets_in_bounds": "No photos in this area",
   "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
   "map_no_location_permission_title": "Location Permission denied",
-  "map_settings_dark_mode": "Dark mode",
+  "map_settings_theme_settings": "Map Theme",
   "map_settings_date_range_option_all": "All",
   "map_settings_date_range_option_day": "Past 24 hours",
   "map_settings_date_range_option_days": "Past {} days",
diff --git a/mobile/assets/location-pin.png b/mobile/assets/location-pin.png
index 1c8ba87885..9bfc53d05b 100644
Binary files a/mobile/assets/location-pin.png and b/mobile/assets/location-pin.png differ
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 75168ce1c9..24a209cec2 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -28,6 +28,10 @@ PODS:
     - Flutter
   - isar_flutter_libs (1.0.0):
     - Flutter
+  - MapLibre (5.14.0-pre3)
+  - maplibre_gl (0.0.1):
+    - Flutter
+    - MapLibre (= 5.14.0-pre3)
   - package_info_plus (0.4.5):
     - Flutter
   - path_provider_foundation (0.0.1):
@@ -71,6 +75,7 @@ DEPENDENCIES:
   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
   - integration_test (from `.symlinks/plugins/integration_test/ios`)
   - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
+  - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
@@ -86,6 +91,7 @@ DEPENDENCIES:
 SPEC REPOS:
   trunk:
     - FMDB
+    - MapLibre
     - ReachabilitySwift
     - SAMKeychain
     - Toast
@@ -115,6 +121,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/integration_test/ios"
   isar_flutter_libs:
     :path: ".symlinks/plugins/isar_flutter_libs/ios"
+  maplibre_gl:
+    :path: ".symlinks/plugins/maplibre_gl/ios"
   package_info_plus:
     :path: ".symlinks/plugins/package_info_plus/ios"
   path_provider_foundation:
@@ -152,6 +160,8 @@ SPEC CHECKSUMS:
   image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
   integration_test: 13825b8a9334a850581300559b8839134b124670
   isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
+  MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
+  maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
   package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
   path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
   path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart
index 9f08ba3efb..c03477cf43 100644
--- a/mobile/lib/extensions/collection_extensions.dart
+++ b/mobile/lib/extensions/collection_extensions.dart
@@ -96,3 +96,9 @@ extension AssetListExtension on Iterable<Asset> {
     return this;
   }
 }
+
+extension SortedByProperty<T> on Iterable<T> {
+  Iterable<T> sortedByField(Comparable Function(T e) key) {
+    return sorted((a, b) => key(a).compareTo(key(b)));
+  }
+}
diff --git a/mobile/lib/extensions/flutter_map_extensions.dart b/mobile/lib/extensions/flutter_map_extensions.dart
deleted file mode 100644
index 4fc812b4a7..0000000000
--- a/mobile/lib/extensions/flutter_map_extensions.dart
+++ /dev/null
@@ -1,67 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_map/flutter_map.dart';
-import 'package:latlong2/latlong.dart';
-import 'dart:math' as math;
-
-extension MoveByBounds on MapController {
-  // TODO: Remove this in favor of built-in method when upgrading flutter_map to 5.0.0
-  LatLng? centerBoundsWithPadding(
-    LatLng coordinates,
-    Offset offset, {
-    double? zoomLevel,
-  }) {
-    const crs = Epsg3857();
-    final oldCenterPt = crs.latLngToPoint(coordinates, zoomLevel ?? zoom);
-    final mapCenterPoint = _rotatePoint(
-      oldCenterPt,
-      oldCenterPt - CustomPoint(offset.dx, offset.dy),
-    );
-    return crs.pointToLatLng(mapCenterPoint, zoomLevel ?? zoom);
-  }
-
-  CustomPoint<double> _rotatePoint(
-    CustomPoint<double> mapCenter,
-    CustomPoint<double> point, {
-    bool counterRotation = true,
-  }) {
-    final counterRotationFactor = counterRotation ? -1 : 1;
-
-    final m = Matrix4.identity()
-      ..translate(mapCenter.x, mapCenter.y)
-      ..rotateZ(degToRadian(rotation) * counterRotationFactor)
-      ..translate(-mapCenter.x, -mapCenter.y);
-
-    final tp = MatrixUtils.transformPoint(m, Offset(point.x, point.y));
-
-    return CustomPoint(tp.dx, tp.dy);
-  }
-
-  double getTapThresholdForZoomLevel() {
-    const scale = [
-      25000000,
-      15000000,
-      8000000,
-      4000000,
-      2000000,
-      1000000,
-      500000,
-      250000,
-      100000,
-      50000,
-      25000,
-      15000,
-      8000,
-      4000,
-      2000,
-      1000,
-      500,
-      250,
-      100,
-      50,
-      25,
-      10,
-      5,
-    ];
-    return scale[math.max(0, math.min(20, zoom.round() + 2))].toDouble() / 6;
-  }
-}
diff --git a/mobile/lib/extensions/latlngbounds_extension.dart b/mobile/lib/extensions/latlngbounds_extension.dart
new file mode 100644
index 0000000000..a8948728bd
--- /dev/null
+++ b/mobile/lib/extensions/latlngbounds_extension.dart
@@ -0,0 +1,20 @@
+import 'package:maplibre_gl/maplibre_gl.dart';
+
+extension WithinBounds on LatLngBounds {
+  /// Checks whether [point] is inside bounds
+  bool contains(LatLng point) {
+    final sw = point;
+    final ne = point;
+    return containsBounds(LatLngBounds(southwest: sw, northeast: ne));
+  }
+
+  /// Checks whether [bounds] is contained inside bounds
+  bool containsBounds(LatLngBounds bounds) {
+    final sw = bounds.southwest;
+    final ne = bounds.northeast;
+    return (sw.latitude >= southwest.latitude) &&
+        (ne.latitude <= northeast.latitude) &&
+        (sw.longitude >= southwest.longitude) &&
+        (ne.longitude <= northeast.longitude);
+  }
+}
diff --git a/mobile/lib/extensions/maplibrecontroller_extensions.dart b/mobile/lib/extensions/maplibrecontroller_extensions.dart
new file mode 100644
index 0000000000..0c1e62e308
--- /dev/null
+++ b/mobile/lib/extensions/maplibrecontroller_extensions.dart
@@ -0,0 +1,71 @@
+import 'dart:math';
+
+import 'package:flutter/services.dart';
+import 'package:immich_mobile/modules/map/models/map_marker.dart';
+import 'package:immich_mobile/modules/map/utils/map_utils.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+
+extension MapMarkers on MaplibreMapController {
+  Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
+    return addSource(
+      MapUtils.defaultSourceId,
+      GeojsonSourceProperties(
+        data: MapUtils.generateGeoJsonForMarkers(markers.toList()),
+      ),
+    );
+  }
+
+  Future<void> reloadAllLayersForMarkers(List<MapMarker> markers) async {
+    // !! Make sure to remove layers before sources else the native
+    // maplibre library would crash when removing the source saying that
+    // the source is still in use
+    final existingLayers = await getLayerIds();
+    if (existingLayers.contains(MapUtils.defaultHeatMapLayerId)) {
+      await removeLayer(MapUtils.defaultHeatMapLayerId);
+    }
+
+    final existingSources = await getSourceIds();
+    if (existingSources.contains(MapUtils.defaultSourceId)) {
+      await removeSource(MapUtils.defaultSourceId);
+    }
+
+    await addGeoJSONSourceForMarkers(markers);
+
+    await addHeatmapLayer(
+      MapUtils.defaultSourceId,
+      MapUtils.defaultHeatMapLayerId,
+      MapUtils.defaultHeatMapLayerProperties,
+    );
+  }
+
+  Future<Symbol?> addMarkerAtLatLng(LatLng centre) async {
+    // no marker is displayed if asset-path is incorrect
+    try {
+      final ByteData bytes = await rootBundle.load("assets/location-pin.png");
+      await addImage("mapMarker", bytes.buffer.asUint8List());
+      return addSymbol(
+        SymbolOptions(
+          geometry: centre,
+          iconImage: "mapMarker",
+          iconSize: 0.15,
+          iconAnchor: "bottom",
+        ),
+      );
+    } finally {
+      // no-op
+    }
+  }
+
+  Future<LatLngBounds> getBoundsFromPoint(
+    Point<double> point,
+    double distance,
+  ) async {
+    final southWestPx = Point(point.x - distance, point.y + distance);
+    final northEastPx = Point(point.x + distance, point.y - distance);
+
+    final southWest = await toLatLng(southWestPx);
+    final northEast = await toLatLng(northEastPx);
+
+    return LatLngBounds(southwest: southWest, northeast: northEast);
+  }
+}
diff --git a/mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart b/mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart
index 53daa74a12..96628dab58 100644
Binary files a/mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart and b/mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart differ
diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
index e560bcb73b..f0665bbe81 100644
--- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
+++ b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
@@ -2,19 +2,18 @@ import 'dart:io';
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_map/flutter_map.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/asset_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/duration_extensions.dart';
 import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
-import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
+import 'package:immich_mobile/modules/map/widgets/map_thumbnail.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/shared/ui/drag_sheet.dart';
 import 'package:immich_mobile/utils/selection_handlers.dart';
-import 'package:latlong2/latlong.dart';
 import 'package:immich_mobile/utils/bytes_units.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
 import 'package:url_launcher/url_launcher.dart';
 
 class ExifBottomSheet extends HookConsumerWidget {
@@ -92,26 +91,14 @@ class ExifBottomSheet extends HookConsumerWidget {
         child: LayoutBuilder(
           builder: (context, constraints) {
             return MapThumbnail(
-              showAttribution: false,
-              coords: LatLng(
+              centre: LatLng(
                 exifInfo?.latitude ?? 0,
                 exifInfo?.longitude ?? 0,
               ),
               height: 150,
               width: constraints.maxWidth,
               zoom: 12.0,
-              markers: [
-                Marker(
-                  anchorPos: AnchorPos.align(AnchorAlign.top),
-                  point: LatLng(
-                    exifInfo?.latitude ?? 0,
-                    exifInfo?.longitude ?? 0,
-                  ),
-                  builder: (ctx) => const Image(
-                    image: AssetImage('assets/location-pin.png'),
-                  ),
-                ),
-              ],
+              assetMarkerRemoteId: asset.remoteId,
               onTap: (tapPosition, latLong) async {
                 Uri? uri = await createCoordinatesUri();
 
diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
index 8695a39f88..687e7aaac0 100644
--- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
@@ -27,7 +27,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
   final bool canDeselect;
   final bool? dynamicLayout;
   final bool showMultiSelectIndicator;
-  final void Function(ItemPosition start, ItemPosition end)?
+  final void Function(Iterable<ItemPosition> itemPositions)?
       visibleItemsListener;
   final Widget? topWidget;
   final bool shrinkWrap;
@@ -89,8 +89,10 @@ class ImmichAssetGrid extends HookConsumerWidget {
             };
 
             scale.onUpdate = (details) {
-              scaleFactor.value =
-                  max(min(5.0, baseScaleFactor.value * details.scale), 1.0);
+              scaleFactor.value = max(
+                min(5.0, baseScaleFactor.value * details.scale),
+                1.0,
+              );
               if (7 - scaleFactor.value.toInt() != perRow.value) {
                 perRow.value = 7 - scaleFactor.value.toInt();
               }
diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
index 6b302375a6..a7587893d7 100644
--- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
@@ -32,7 +32,7 @@ class ImmichAssetGridView extends StatefulWidget {
   final bool canDeselect;
   final bool dynamicLayout;
   final bool showMultiSelectIndicator;
-  final void Function(ItemPosition start, ItemPosition end)?
+  final void Function(Iterable<ItemPosition> itemPositions)?
       visibleItemsListener;
   final Widget? topWidget;
   final int heroOffset;
@@ -421,15 +421,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
 
   void _positionListener() {
     final values = _itemPositionsListener.itemPositions.value;
-    final start = values.firstOrNull;
-    final end = values.lastOrNull;
-    if (start != null && end != null) {
-      if (start.index <= end.index) {
-        widget.visibleItemsListener?.call(start, end);
-      } else {
-        widget.visibleItemsListener?.call(end, start);
-      }
-    }
+    widget.visibleItemsListener?.call(values);
   }
 
   void _scrollToTop() {
diff --git a/mobile/lib/modules/map/models/map_event.model.dart b/mobile/lib/modules/map/models/map_event.model.dart
new file mode 100644
index 0000000000..0baeefeceb
--- /dev/null
+++ b/mobile/lib/modules/map/models/map_event.model.dart
@@ -0,0 +1,13 @@
+// ignore_for_file: add-copy-with
+
+sealed class MapEvent {
+  const MapEvent();
+}
+
+class MapAssetsInBoundsUpdated extends MapEvent {
+  final List<String> assetRemoteIds;
+
+  const MapAssetsInBoundsUpdated(this.assetRemoteIds);
+}
+
+class MapCloseBottomSheet extends MapEvent {}
diff --git a/mobile/lib/modules/map/models/map_marker.dart b/mobile/lib/modules/map/models/map_marker.dart
new file mode 100644
index 0000000000..c9253a37cc
--- /dev/null
+++ b/mobile/lib/modules/map/models/map_marker.dart
@@ -0,0 +1,39 @@
+import 'package:maplibre_gl/maplibre_gl.dart';
+import 'package:openapi/api.dart';
+
+class MapMarker {
+  final LatLng latLng;
+  final String assetRemoteId;
+  MapMarker({
+    required this.latLng,
+    required this.assetRemoteId,
+  });
+
+  MapMarker copyWith({
+    LatLng? latLng,
+    String? assetRemoteId,
+  }) {
+    return MapMarker(
+      latLng: latLng ?? this.latLng,
+      assetRemoteId: assetRemoteId ?? this.assetRemoteId,
+    );
+  }
+
+  MapMarker.fromDto(MapMarkerResponseDto dto)
+      : latLng = LatLng(dto.lat, dto.lon),
+        assetRemoteId = dto.id;
+
+  @override
+  String toString() =>
+      'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)';
+
+  @override
+  bool operator ==(covariant MapMarker other) {
+    if (identical(this, other)) return true;
+
+    return other.latLng == latLng && other.assetRemoteId == assetRemoteId;
+  }
+
+  @override
+  int get hashCode => latLng.hashCode ^ assetRemoteId.hashCode;
+}
diff --git a/mobile/lib/modules/map/models/map_page_event.model.dart b/mobile/lib/modules/map/models/map_page_event.model.dart
deleted file mode 100644
index 63665173d9..0000000000
--- a/mobile/lib/modules/map/models/map_page_event.model.dart
+++ /dev/null
@@ -1,40 +0,0 @@
-import 'package:immich_mobile/shared/models/asset.dart';
-
-enum MapPageEventType {
-  mapTap,
-  bottomSheetScrolled,
-  assetsInBoundUpdated,
-  zoomToAsset,
-  zoomToCurrentLocation,
-}
-
-class MapPageEventBase {
-  final MapPageEventType type;
-
-  const MapPageEventBase(this.type);
-}
-
-class MapPageOnTapEvent extends MapPageEventBase {
-  const MapPageOnTapEvent() : super(MapPageEventType.mapTap);
-}
-
-class MapPageAssetsInBoundUpdated extends MapPageEventBase {
-  List<Asset> assets;
-  MapPageAssetsInBoundUpdated(this.assets)
-      : super(MapPageEventType.assetsInBoundUpdated);
-}
-
-class MapPageBottomSheetScrolled extends MapPageEventBase {
-  Asset? asset;
-  MapPageBottomSheetScrolled(this.asset)
-      : super(MapPageEventType.bottomSheetScrolled);
-}
-
-class MapPageZoomToAsset extends MapPageEventBase {
-  Asset? asset;
-  MapPageZoomToAsset(this.asset) : super(MapPageEventType.zoomToAsset);
-}
-
-class MapPageZoomToLocation extends MapPageEventBase {
-  const MapPageZoomToLocation() : super(MapPageEventType.zoomToCurrentLocation);
-}
diff --git a/mobile/lib/modules/map/models/map_state.model.dart b/mobile/lib/modules/map/models/map_state.model.dart
index d606f1005a..85a3e3f37f 100644
--- a/mobile/lib/modules/map/models/map_state.model.dart
+++ b/mobile/lib/modules/map/models/map_state.model.dart
@@ -1,65 +1,71 @@
-import 'package:vector_map_tiles/vector_map_tiles.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 
 class MapState {
-  final bool isDarkTheme;
+  final ThemeMode themeMode;
   final bool showFavoriteOnly;
   final bool includeArchived;
   final int relativeTime;
-  final Style? mapStyle;
-  final bool isLoading;
+  final bool shouldRefetchMarkers;
+  final AsyncValue<String> lightStyleFetched;
+  final AsyncValue<String> darkStyleFetched;
 
   MapState({
-    this.isDarkTheme = false,
+    this.themeMode = ThemeMode.system,
     this.showFavoriteOnly = false,
     this.includeArchived = false,
     this.relativeTime = 0,
-    this.mapStyle,
-    this.isLoading = false,
+    this.shouldRefetchMarkers = false,
+    this.lightStyleFetched = const AsyncLoading(),
+    this.darkStyleFetched = const AsyncLoading(),
   });
 
   MapState copyWith({
-    bool? isDarkTheme,
+    ThemeMode? themeMode,
     bool? showFavoriteOnly,
     bool? includeArchived,
     int? relativeTime,
-    Style? mapStyle,
-    bool? isLoading,
+    bool? shouldRefetchMarkers,
+    AsyncValue<String>? lightStyleFetched,
+    AsyncValue<String>? darkStyleFetched,
   }) {
     return MapState(
-      isDarkTheme: isDarkTheme ?? this.isDarkTheme,
+      themeMode: themeMode ?? this.themeMode,
       showFavoriteOnly: showFavoriteOnly ?? this.showFavoriteOnly,
       includeArchived: includeArchived ?? this.includeArchived,
       relativeTime: relativeTime ?? this.relativeTime,
-      mapStyle: mapStyle ?? this.mapStyle,
-      isLoading: isLoading ?? this.isLoading,
+      shouldRefetchMarkers: shouldRefetchMarkers ?? this.shouldRefetchMarkers,
+      lightStyleFetched: lightStyleFetched ?? this.lightStyleFetched,
+      darkStyleFetched: darkStyleFetched ?? this.darkStyleFetched,
     );
   }
 
   @override
   String toString() {
-    return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime, includeArchived: $includeArchived, mapStyle: $mapStyle, isLoading: $isLoading)';
+    return 'MapState(themeMode: $themeMode, showFavoriteOnly: $showFavoriteOnly, includeArchived: $includeArchived, relativeTime: $relativeTime, shouldRefetchMarkers: $shouldRefetchMarkers, lightStyleFetched: $lightStyleFetched, darkStyleFetched: $darkStyleFetched)';
   }
 
   @override
-  bool operator ==(Object other) {
+  bool operator ==(covariant MapState other) {
     if (identical(this, other)) return true;
 
-    return other is MapState &&
-        other.isDarkTheme == isDarkTheme &&
+    return other.themeMode == themeMode &&
         other.showFavoriteOnly == showFavoriteOnly &&
-        other.relativeTime == relativeTime &&
         other.includeArchived == includeArchived &&
-        other.mapStyle == mapStyle &&
-        other.isLoading == isLoading;
+        other.relativeTime == relativeTime &&
+        other.shouldRefetchMarkers == shouldRefetchMarkers &&
+        other.lightStyleFetched == lightStyleFetched &&
+        other.darkStyleFetched == darkStyleFetched;
   }
 
   @override
   int get hashCode {
-    return isDarkTheme.hashCode ^
+    return themeMode.hashCode ^
         showFavoriteOnly.hashCode ^
-        relativeTime.hashCode ^
         includeArchived.hashCode ^
-        mapStyle.hashCode ^
-        isLoading.hashCode;
+        relativeTime.hashCode ^
+        shouldRefetchMarkers.hashCode ^
+        lightStyleFetched.hashCode ^
+        darkStyleFetched.hashCode;
   }
 }
diff --git a/mobile/lib/modules/map/providers/map_marker.provider.dart b/mobile/lib/modules/map/providers/map_marker.provider.dart
index d9541c72cc..fec7708b38 100644
--- a/mobile/lib/modules/map/providers/map_marker.provider.dart
+++ b/mobile/lib/modules/map/providers/map_marker.provider.dart
@@ -1,13 +1,14 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/map/models/map_marker.dart';
+import 'package:immich_mobile/modules/map/providers/map_service.provider.dart';
 import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
-import 'package:immich_mobile/modules/map/services/map.service.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:latlong2/latlong.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
 
-final mapMarkersProvider =
-    FutureProvider.autoDispose<Set<AssetMarkerData>>((ref) async {
+part 'map_marker.provider.g.dart';
+
+@riverpod
+Future<List<MapMarker>> mapMarkers(MapMarkersRef ref) async {
   final service = ref.read(mapServiceProvider);
-  final mapState = ref.read(mapStateNotifier);
+  final mapState = ref.read(mapStateNotifierProvider);
   DateTime? fileCreatedAfter;
   bool? isFavorite;
   bool? isIncludeArchived;
@@ -31,34 +32,5 @@ final mapMarkersProvider =
     fileCreatedAfter: fileCreatedAfter,
   );
 
-  final assetMarkerData = await Future.wait(
-    markers.map((e) async {
-      final asset = await service.getAssetForMarkerId(e.id);
-      bool hasInvalidCoords = e.lat < -90 || e.lat > 90;
-      hasInvalidCoords = hasInvalidCoords || (e.lon < -180 || e.lon > 180);
-      if (asset == null || hasInvalidCoords) return null;
-      return AssetMarkerData(asset, LatLng(e.lat, e.lon));
-    }),
-  );
-
-  return assetMarkerData.nonNulls.toSet();
-});
-
-class AssetMarkerData {
-  final LatLng point;
-  final Asset asset;
-
-  const AssetMarkerData(this.asset, this.point);
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-
-    return other is AssetMarkerData && other.asset.remoteId == asset.remoteId;
-  }
-
-  @override
-  int get hashCode {
-    return asset.remoteId.hashCode;
-  }
+  return markers.toList();
 }
diff --git a/mobile/lib/modules/map/providers/map_marker.provider.g.dart b/mobile/lib/modules/map/providers/map_marker.provider.g.dart
new file mode 100644
index 0000000000..7df6adea99
Binary files /dev/null and b/mobile/lib/modules/map/providers/map_marker.provider.g.dart differ
diff --git a/mobile/lib/modules/map/providers/map_service.provider.dart b/mobile/lib/modules/map/providers/map_service.provider.dart
new file mode 100644
index 0000000000..666ca7acda
--- /dev/null
+++ b/mobile/lib/modules/map/providers/map_service.provider.dart
@@ -0,0 +1,9 @@
+import 'package:immich_mobile/modules/map/services/map.service.dart';
+import 'package:immich_mobile/shared/providers/api.provider.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'map_service.provider.g.dart';
+
+@riverpod
+MapSerivce mapService(MapServiceRef ref) =>
+    MapSerivce(ref.watch(apiServiceProvider));
diff --git a/mobile/lib/modules/map/providers/map_service.provider.g.dart b/mobile/lib/modules/map/providers/map_service.provider.g.dart
new file mode 100644
index 0000000000..7b4e68eaee
Binary files /dev/null and b/mobile/lib/modules/map/providers/map_service.provider.g.dart differ
diff --git a/mobile/lib/modules/map/providers/map_state.provider.dart b/mobile/lib/modules/map/providers/map_state.provider.dart
index fccde751be..de6265c233 100644
--- a/mobile/lib/modules/map/providers/map_state.provider.dart
+++ b/mobile/lib/modules/map/providers/map_state.provider.dart
@@ -1,159 +1,138 @@
-import 'dart:convert';
 import 'dart:io';
 
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_map/flutter_map.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/map/models/map_state.model.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/providers/api.provider.dart';
-import 'package:immich_mobile/shared/services/api.service.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
-import 'package:immich_mobile/utils/color_filter_generator.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
-import 'package:vector_map_tiles/vector_map_tiles.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
 
-class MapStateNotifier extends StateNotifier<MapState> {
-  MapStateNotifier(this._appSettingsProvider, this._apiService)
-      : super(
-          MapState(
-            isDarkTheme: _appSettingsProvider
-                .getSetting<bool>(AppSettingsEnum.mapThemeMode),
-            showFavoriteOnly: _appSettingsProvider
-                .getSetting<bool>(AppSettingsEnum.mapShowFavoriteOnly),
-            includeArchived: _appSettingsProvider
-                .getSetting<bool>(AppSettingsEnum.mapIncludeArchived),
-            relativeTime: _appSettingsProvider
-                .getSetting<int>(AppSettingsEnum.mapRelativeDate),
-            isLoading: true,
-          ),
-        ) {
-    _fetchStyleFromServer(
-      _appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapThemeMode),
+part 'map_state.provider.g.dart';
+
+@Riverpod(keepAlive: true)
+class MapStateNotifier extends _$MapStateNotifier {
+  final _log = Logger("MapStateNotifier");
+
+  @override
+  MapState build() {
+    final appSettingsProvider = ref.read(appSettingsServiceProvider);
+
+    // Fetch and save the Style JSONs
+    loadStyles();
+    return MapState(
+      themeMode: ThemeMode.values[
+          appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
+      showFavoriteOnly: appSettingsProvider
+          .getSetting<bool>(AppSettingsEnum.mapShowFavoriteOnly),
+      includeArchived: appSettingsProvider
+          .getSetting<bool>(AppSettingsEnum.mapIncludeArchived),
+      relativeTime:
+          appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
     );
   }
 
-  final AppSettingsService _appSettingsProvider;
-  final ApiService _apiService;
-  final Logger _log = Logger("MapStateNotifier");
+  void loadStyles() async {
+    final documents = (await getApplicationDocumentsDirectory()).path;
 
-  bool get isRaster =>
-      state.mapStyle != null && state.mapStyle!.rasterTileProvider != null;
+    // Set to loading
+    state = state.copyWith(lightStyleFetched: const AsyncLoading());
 
-  double get maxZoom =>
-      (isRaster ? state.mapStyle!.rasterTileProvider!.maximumZoom : 18)
-          .toDouble();
+    // Fetch and save light theme
+    final lightResponse = await ref
+        .read(apiServiceProvider)
+        .systemConfigApi
+        .getMapStyleWithHttpInfo(MapTheme.light);
 
-  void switchTheme(bool isDarkTheme) {
-    _updateThemeMode(isDarkTheme);
-    _fetchStyleFromServer(isDarkTheme);
-  }
-
-  void _updateThemeMode(bool isDarkTheme) {
-    _appSettingsProvider.setSetting(
-      AppSettingsEnum.mapThemeMode,
-      isDarkTheme,
-    );
-    state = state.copyWith(isDarkTheme: isDarkTheme, isLoading: true);
-  }
-
-  void _fetchStyleFromServer(bool isDarkTheme) async {
-    final styleResponse = await _apiService.systemConfigApi
-        .getMapStyleWithHttpInfo(isDarkTheme ? MapTheme.dark : MapTheme.light);
-    if (styleResponse.statusCode >= HttpStatus.badRequest) {
-      throw ApiException(styleResponse.statusCode, styleResponse.body);
-    }
-    final styleJsonString = styleResponse.body.isNotEmpty &&
-            styleResponse.statusCode != HttpStatus.noContent
-        ? styleResponse.body
-        : null;
-
-    if (styleJsonString == null) {
-      _log.severe('Style JSON from server is empty');
+    if (lightResponse.statusCode >= HttpStatus.badRequest) {
+      state = state.copyWith(
+        lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current),
+      );
+      _log.severe(
+        "Cannot fetch map light style with status - ${lightResponse.statusCode} and response - ${lightResponse.body}",
+      );
       return;
     }
-    final styleJson = await compute(jsonDecode, styleJsonString);
-    if (styleJson is! Map<String, dynamic>) {
-      _log.severe('Style JSON from server is invalid');
+
+    final lightJSON = lightResponse.body;
+    final lightFile = await File("$documents/map-style-light.json")
+        .writeAsString(lightJSON, flush: true);
+
+    // Update state with path
+    state =
+        state.copyWith(lightStyleFetched: AsyncData(lightFile.absolute.path));
+
+    // Set to loading
+    state = state.copyWith(darkStyleFetched: const AsyncLoading());
+
+    // Fetch and save dark theme
+    final darkResponse = await ref
+        .read(apiServiceProvider)
+        .systemConfigApi
+        .getMapStyleWithHttpInfo(MapTheme.dark);
+
+    if (darkResponse.statusCode >= HttpStatus.badRequest) {
+      state = state.copyWith(
+        darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current),
+      );
+      _log.severe(
+        "Cannot fetch map dark style with status - ${darkResponse.statusCode} and response - ${darkResponse.body}",
+      );
       return;
     }
-    final styleReader = StyleReader(uri: '');
-    Style? style;
-    try {
-      style = await styleReader.readFromMap(styleJson);
-    } finally {
-      // Consume all error
-    }
-    state = state.copyWith(
-      mapStyle: style,
-      isLoading: false,
-    );
+
+    final darkJSON = darkResponse.body;
+    final darkFile = await File("$documents/map-style-dark.json")
+        .writeAsString(darkJSON, flush: true);
+
+    // Update state with path
+    state = state.copyWith(darkStyleFetched: AsyncData(darkFile.absolute.path));
+  }
+
+  void switchTheme(ThemeMode mode) {
+    ref.read(appSettingsServiceProvider).setSetting(
+          AppSettingsEnum.mapThemeMode,
+          mode.index,
+        );
+    state = state.copyWith(themeMode: mode);
   }
 
   void switchFavoriteOnly(bool isFavoriteOnly) {
-    _appSettingsProvider.setSetting(
-      AppSettingsEnum.mapShowFavoriteOnly,
-      isFavoriteOnly,
+    ref.read(appSettingsServiceProvider).setSetting(
+          AppSettingsEnum.mapShowFavoriteOnly,
+          isFavoriteOnly,
+        );
+    state = state.copyWith(
+      showFavoriteOnly: isFavoriteOnly,
+      shouldRefetchMarkers: true,
     );
-    state = state.copyWith(showFavoriteOnly: isFavoriteOnly);
+  }
+
+  void setRefetchMarkers(bool shouldRefetch) {
+    state = state.copyWith(shouldRefetchMarkers: shouldRefetch);
   }
 
   void switchIncludeArchived(bool isIncludeArchived) {
-    _appSettingsProvider.setSetting(
-      AppSettingsEnum.mapIncludeArchived,
-      isIncludeArchived,
+    ref.read(appSettingsServiceProvider).setSetting(
+          AppSettingsEnum.mapIncludeArchived,
+          isIncludeArchived,
+        );
+    state = state.copyWith(
+      includeArchived: isIncludeArchived,
+      shouldRefetchMarkers: true,
     );
-    state = state.copyWith(includeArchived: isIncludeArchived);
   }
 
   void setRelativeTime(int relativeTime) {
-    _appSettingsProvider.setSetting(
-      AppSettingsEnum.mapRelativeDate,
-      relativeTime,
+    ref.read(appSettingsServiceProvider).setSetting(
+          AppSettingsEnum.mapRelativeDate,
+          relativeTime,
+        );
+    state = state.copyWith(
+      relativeTime: relativeTime,
+      shouldRefetchMarkers: true,
     );
-    state = state.copyWith(relativeTime: relativeTime);
-  }
-
-  Widget getTileLayer([bool forceDark = false]) {
-    if (isRaster) {
-      final rasterProvider = state.mapStyle!.rasterTileProvider;
-      final rasterLayer = TileLayer(
-        urlTemplate: rasterProvider!.url,
-        maxNativeZoom: rasterProvider.maximumZoom,
-        maxZoom: rasterProvider.maximumZoom.toDouble(),
-      );
-      return state.isDarkTheme || forceDark
-          ? InvertionFilter(
-              child: SaturationFilter(
-                saturation: -1,
-                child: BrightnessFilter(
-                  brightness: -1,
-                  child: rasterLayer,
-                ),
-              ),
-            )
-          : rasterLayer;
-    }
-    if (state.mapStyle != null && !isRaster) {
-      return VectorTileLayer(
-        // Tiles and themes will be set for vector providers
-        tileProviders: state.mapStyle!.providers!,
-        theme: state.mapStyle!.theme!,
-        sprites: state.mapStyle!.sprites,
-        concurrency: 6,
-      );
-    }
-    return const Center(child: ImmichLoadingIndicator());
   }
 }
-
-final mapStateNotifier =
-    StateNotifierProvider<MapStateNotifier, MapState>((ref) {
-  return MapStateNotifier(
-    ref.watch(appSettingsServiceProvider),
-    ref.watch(apiServiceProvider),
-  );
-});
diff --git a/mobile/lib/modules/map/providers/map_state.provider.g.dart b/mobile/lib/modules/map/providers/map_state.provider.g.dart
new file mode 100644
index 0000000000..ca75292e78
Binary files /dev/null and b/mobile/lib/modules/map/providers/map_state.provider.g.dart differ
diff --git a/mobile/lib/modules/map/services/map.service.dart b/mobile/lib/modules/map/services/map.service.dart
index b5ee010014..b3a904cbf1 100644
--- a/mobile/lib/modules/map/services/map.service.dart
+++ b/mobile/lib/modules/map/services/map.service.dart
@@ -1,62 +1,33 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/providers/api.provider.dart';
-import 'package:immich_mobile/shared/providers/db.provider.dart';
+import 'package:immich_mobile/mixins/error_logger.mixin.dart';
+import 'package:immich_mobile/modules/map/models/map_marker.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
-import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
-import 'package:openapi/api.dart';
 
-final mapServiceProvider = Provider(
-  (ref) => MapSerivce(
-    ref.read(apiServiceProvider),
-    ref.read(dbProvider),
-  ),
-);
-
-class MapSerivce {
+class MapSerivce with ErrorLoggerMixin {
   final ApiService _apiService;
-  final Isar _db;
-  final _log = Logger("MapService");
+  @override
+  final logger = Logger("MapService");
 
-  MapSerivce(this._apiService, this._db);
+  MapSerivce(this._apiService);
 
-  Future<List<MapMarkerResponseDto>> getMapMarkers({
+  Future<Iterable<MapMarker>> getMapMarkers({
     bool? isFavorite,
     bool? withArchived,
     DateTime? fileCreatedAfter,
     DateTime? fileCreatedBefore,
   }) async {
-    try {
-      final markers = await _apiService.assetApi.getMapMarkers(
-        isFavorite: isFavorite,
-        isArchived: withArchived,
-        fileCreatedAfter: fileCreatedAfter,
-        fileCreatedBefore: fileCreatedBefore,
-      );
+    return logError(
+      () async {
+        final markers = await _apiService.assetApi.getMapMarkers(
+          isFavorite: isFavorite,
+          isArchived: withArchived,
+          fileCreatedAfter: fileCreatedAfter,
+          fileCreatedBefore: fileCreatedBefore,
+        );
 
-      return markers ?? [];
-    } catch (error, stack) {
-      _log.severe("Cannot get map markers ${error.toString()}", error, stack);
-      return [];
-    }
-  }
-
-  Future<Asset?> getAssetForMarkerId(String remoteId) async {
-    try {
-      final assets = await _db.assets.getAllByRemoteId([remoteId]);
-      if (assets.isNotEmpty) return assets[0];
-
-      final dto = await _apiService.assetApi.getAssetById(remoteId);
-      if (dto == null) return null;
-      return _db.assets.getByRemoteId(dto.id);
-    } catch (error, stack) {
-      _log.severe(
-        "Cannot get asset for marker ${error.toString()}",
-        error,
-        stack,
-      );
-      return null;
-    }
+        return markers?.map(MapMarker.fromDto) ?? [];
+      },
+      defaultValue: [],
+    );
   }
 }
diff --git a/mobile/lib/modules/map/ui/location_dialog.dart b/mobile/lib/modules/map/ui/location_dialog.dart
deleted file mode 100644
index a55202e145..0000000000
--- a/mobile/lib/modules/map/ui/location_dialog.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:geolocator/geolocator.dart';
-import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
-
-class LocationServiceDisabledDialog extends ConfirmDialog {
-  LocationServiceDisabledDialog({Key? key})
-      : super(
-          key: key,
-          title: 'map_location_service_disabled_title'.tr(),
-          content: 'map_location_service_disabled_content'.tr(),
-          cancel: 'map_location_dialog_cancel'.tr(),
-          ok: 'map_location_dialog_yes'.tr(),
-          onOk: () async {
-            await Geolocator.openLocationSettings();
-          },
-        );
-}
-
-class LocationPermissionDisabledDialog extends ConfirmDialog {
-  LocationPermissionDisabledDialog({Key? key})
-      : super(
-          key: key,
-          title: 'map_no_location_permission_title'.tr(),
-          content: 'map_no_location_permission_content'.tr(),
-          cancel: 'map_location_dialog_cancel'.tr(),
-          ok: 'map_location_dialog_yes'.tr(),
-          onOk: () {},
-        );
-}
diff --git a/mobile/lib/modules/map/ui/map_location_picker.dart b/mobile/lib/modules/map/ui/map_location_picker.dart
deleted file mode 100644
index 24873c6372..0000000000
--- a/mobile/lib/modules/map/ui/map_location_picker.dart
+++ /dev/null
@@ -1,114 +0,0 @@
-import 'package:auto_route/auto_route.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:flutter_map/flutter_map.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
-import 'package:immich_mobile/utils/immich_app_theme.dart';
-import 'package:latlong2/latlong.dart';
-
-class MapLocationPickerPage extends HookConsumerWidget {
-  final LatLng? initialLatLng;
-
-  const MapLocationPickerPage({super.key, this.initialLatLng});
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final selectedLatLng = useState<LatLng>(initialLatLng ?? LatLng(0, 0));
-    final isDarkTheme =
-        ref.watch(mapStateNotifier.select((state) => state.isDarkTheme));
-    final isLoading =
-        ref.watch(mapStateNotifier.select((state) => state.isLoading));
-    final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom;
-
-    return Theme(
-      // Override app theme based on map theme
-      data: isDarkTheme ? immichDarkTheme : immichLightTheme,
-      child: Scaffold(
-        extendBodyBehindAppBar: true,
-        body: Stack(
-          children: [
-            if (!isLoading)
-              FlutterMap(
-                options: MapOptions(
-                  maxBounds:
-                      LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)),
-                  interactiveFlags: InteractiveFlag.doubleTapZoom |
-                      InteractiveFlag.drag |
-                      InteractiveFlag.flingAnimation |
-                      InteractiveFlag.pinchMove |
-                      InteractiveFlag.pinchZoom,
-                  center: LatLng(20, 20),
-                  zoom: 2,
-                  minZoom: 1,
-                  maxZoom: maxZoom,
-                  onTap: (tapPosition, point) => selectedLatLng.value = point,
-                ),
-                children: [
-                  ref.read(mapStateNotifier.notifier).getTileLayer(),
-                  MarkerLayer(
-                    markers: [
-                      Marker(
-                        anchorPos: AnchorPos.align(AnchorAlign.top),
-                        point: selectedLatLng.value,
-                        builder: (ctx) => const Image(
-                          image: AssetImage('assets/location-pin.png'),
-                        ),
-                        height: 40,
-                        width: 40,
-                      ),
-                    ],
-                  ),
-                ],
-              ),
-            if (isLoading)
-              Positioned(
-                top: context.height * 0.35,
-                left: context.width * 0.425,
-                child: const ImmichLoadingIndicator(),
-              ),
-          ],
-        ),
-        bottomSheet: BottomSheet(
-          onClosing: () {},
-          builder: (context) => SizedBox(
-            height: 150,
-            child: Column(
-              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
-              crossAxisAlignment: CrossAxisAlignment.center,
-              children: [
-                Text(
-                  "${selectedLatLng.value.latitude.toStringAsFixed(4)}, ${selectedLatLng.value.longitude.toStringAsFixed(4)}",
-                  style: context.textTheme.bodyLarge?.copyWith(
-                    color: context.primaryColor,
-                    fontWeight: FontWeight.w600,
-                  ),
-                ),
-                Row(
-                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
-                  children: [
-                    ElevatedButton(
-                      onPressed: () => context.popRoute(selectedLatLng.value),
-                      child: const Text("map_location_picker_page_use_location")
-                          .tr(),
-                    ),
-                    ElevatedButton(
-                      onPressed: () => context.popRoute(),
-                      style: ElevatedButton.styleFrom(
-                        backgroundColor: context.colorScheme.error,
-                      ),
-                      child: const Text("action_common_cancel").tr(),
-                    ),
-                  ],
-                ),
-              ],
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/modules/map/ui/map_page_app_bar.dart b/mobile/lib/modules/map/ui/map_page_app_bar.dart
deleted file mode 100644
index bfb29ba3d0..0000000000
--- a/mobile/lib/modules/map/ui/map_page_app_bar.dart
+++ /dev/null
@@ -1,138 +0,0 @@
-import 'dart:io';
-
-import 'package:auto_route/auto_route.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:immich_mobile/modules/home/ui/asset_grid/disable_multi_select_button.dart';
-import 'package:immich_mobile/modules/map/ui/map_settings_dialog.dart';
-
-class MapAppBar extends HookWidget implements PreferredSizeWidget {
-  final ValueNotifier<bool> selectionEnabled;
-  final int selectedAssetsLength;
-  final bool isDarkTheme;
-
-  final void Function() onShare;
-  final void Function() onFavorite;
-  final void Function() onArchive;
-
-  const MapAppBar({
-    super.key,
-    required this.selectionEnabled,
-    required this.selectedAssetsLength,
-    required this.onShare,
-    required this.onArchive,
-    required this.onFavorite,
-    this.isDarkTheme = false,
-  });
-
-  List<Widget> buildNonSelectionWidgets(BuildContext context) {
-    return [
-      Padding(
-        padding: const EdgeInsets.only(left: 15, top: 15),
-        child: ElevatedButton(
-          onPressed: () => context.popRoute(),
-          style: ElevatedButton.styleFrom(
-            shape: const CircleBorder(),
-            padding: const EdgeInsets.all(12),
-          ),
-          child: const Icon(Icons.arrow_back_ios_new_rounded, size: 22),
-        ),
-      ),
-      Padding(
-        padding: const EdgeInsets.only(right: 15, top: 15),
-        child: ElevatedButton(
-          onPressed: () => showDialog(
-            context: context,
-            builder: (BuildContext _) {
-              return const MapSettingsDialog();
-            },
-          ),
-          style: ElevatedButton.styleFrom(
-            shape: const CircleBorder(),
-            padding: const EdgeInsets.all(12),
-          ),
-          child: const Icon(Icons.more_vert_rounded, size: 22),
-        ),
-      ),
-    ];
-  }
-
-  List<Widget> buildSelectionWidgets() {
-    return [
-      DisableMultiSelectButton(
-        onPressed: () {
-          selectionEnabled.value = false;
-        },
-        selectedItemCount: selectedAssetsLength,
-      ),
-      Row(
-        children: [
-          // Share button
-          Padding(
-            padding: const EdgeInsets.only(top: 15),
-            child: ElevatedButton(
-              onPressed: onShare,
-              style: ElevatedButton.styleFrom(
-                shape: const CircleBorder(),
-                padding: const EdgeInsets.all(12),
-              ),
-              child: Icon(
-                Platform.isAndroid
-                    ? Icons.share_rounded
-                    : Icons.ios_share_rounded,
-                size: 22,
-              ),
-            ),
-          ),
-          // Favorite button
-          Padding(
-            padding: const EdgeInsets.only(top: 15),
-            child: ElevatedButton(
-              onPressed: onFavorite,
-              style: ElevatedButton.styleFrom(
-                shape: const CircleBorder(),
-                padding: const EdgeInsets.all(12),
-              ),
-              child: const Icon(
-                Icons.favorite,
-                size: 22,
-              ),
-            ),
-          ),
-          // Archive Button
-          Padding(
-            padding: const EdgeInsets.only(right: 10, top: 15),
-            child: ElevatedButton(
-              onPressed: onArchive,
-              style: ElevatedButton.styleFrom(
-                shape: const CircleBorder(),
-                padding: const EdgeInsets.all(12),
-              ),
-              child: const Icon(
-                Icons.archive,
-                size: 22,
-              ),
-            ),
-          ),
-        ],
-      ),
-    ];
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Padding(
-      padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top + 15),
-      child: Row(
-        mainAxisAlignment: MainAxisAlignment.spaceBetween,
-        children: [
-          if (!selectionEnabled.value) ...buildNonSelectionWidgets(context),
-          if (selectionEnabled.value) ...buildSelectionWidgets(),
-        ],
-      ),
-    );
-  }
-
-  @override
-  Size get preferredSize => const Size.fromHeight(100);
-}
diff --git a/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart b/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart
deleted file mode 100644
index 21902de4e3..0000000000
--- a/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart
+++ /dev/null
@@ -1,356 +0,0 @@
-import 'dart:async';
-import 'dart:io';
-
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
-import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
-import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
-import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.dart';
-import 'package:immich_mobile/modules/map/models/map_page_event.model.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/ui/drag_sheet.dart';
-import 'package:immich_mobile/utils/color_filter_generator.dart';
-import 'package:immich_mobile/utils/debounce.dart';
-import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
-
-class MapPageBottomSheet extends StatefulHookConsumerWidget {
-  final Stream mapPageEventStream;
-  final StreamController bottomSheetEventSC;
-  final bool selectionEnabled;
-  final ImmichAssetGridSelectionListener selectionlistener;
-  final bool isDarkTheme;
-
-  const MapPageBottomSheet({
-    super.key,
-    required this.mapPageEventStream,
-    required this.bottomSheetEventSC,
-    required this.selectionEnabled,
-    required this.selectionlistener,
-    this.isDarkTheme = false,
-  });
-
-  @override
-  AssetsInBoundBottomSheetState createState() =>
-      AssetsInBoundBottomSheetState();
-}
-
-class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
-  // Non-State variables
-  bool userTappedOnMap = false;
-  RenderList? _cachedRenderList;
-  int assetOffsetInSheet = -1;
-  late final DraggableScrollableController bottomSheetController;
-  late final Debounce debounce;
-
-  @override
-  void initState() {
-    super.initState();
-    bottomSheetController = DraggableScrollableController();
-    debounce = Debounce(
-      const Duration(milliseconds: 100),
-    );
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final isDarkTheme = context.isDarkTheme;
-    final bottomPadding =
-        Platform.isAndroid ? MediaQuery.of(context).padding.bottom - 10 : 0.0;
-    final maxHeight = context.height - bottomPadding;
-    final isSheetScrolled = useState(false);
-    final isSheetExpanded = useState(false);
-    final assetsInBound = useState(<Asset>[]);
-    final currentExtend = useState(0.1);
-
-    void handleMapPageEvents(dynamic event) {
-      if (event is MapPageAssetsInBoundUpdated) {
-        assetsInBound.value = event.assets;
-      } else if (event is MapPageOnTapEvent) {
-        userTappedOnMap = true;
-        assetOffsetInSheet = -1;
-        bottomSheetController.animateTo(
-          0.1,
-          duration: const Duration(milliseconds: 200),
-          curve: Curves.linearToEaseOut,
-        );
-        isSheetScrolled.value = false;
-      }
-    }
-
-    useEffect(
-      () {
-        final mapPageEventSubscription =
-            widget.mapPageEventStream.listen(handleMapPageEvents);
-        return mapPageEventSubscription.cancel;
-      },
-      [widget.mapPageEventStream],
-    );
-
-    void handleVisibleItems(ItemPosition start, ItemPosition end) {
-      final renderElement = _cachedRenderList?.elements[start.index];
-      if (renderElement == null) {
-        return;
-      }
-      final rowOffset = renderElement.offset;
-      if ((-start.itemLeadingEdge) != 0) {
-        var columnOffset = -start.itemLeadingEdge ~/ 0.05;
-        columnOffset = columnOffset < renderElement.totalCount
-            ? columnOffset
-            : renderElement.totalCount - 1;
-        assetOffsetInSheet = rowOffset + columnOffset;
-        final asset = _cachedRenderList?.allAssets?[assetOffsetInSheet];
-        userTappedOnMap = false;
-        if (!userTappedOnMap && isSheetExpanded.value) {
-          widget.bottomSheetEventSC.add(
-            MapPageBottomSheetScrolled(asset),
-          );
-        }
-        if (isSheetExpanded.value) {
-          isSheetScrolled.value = true;
-        }
-      }
-    }
-
-    void visibleItemsListener(ItemPosition start, ItemPosition end) {
-      if (_cachedRenderList == null) {
-        debounce.dispose();
-        return;
-      }
-      debounce.call(() => handleVisibleItems(start, end));
-    }
-
-    Widget buildNoPhotosWidget() {
-      const image = Image(
-        image: AssetImage('assets/lighthouse.png'),
-      );
-
-      return isSheetExpanded.value
-          ? Column(
-              children: [
-                const SizedBox(
-                  height: 80,
-                ),
-                SizedBox(
-                  height: 150,
-                  width: 150,
-                  child: isDarkTheme
-                      ? const InvertionFilter(
-                          child: SaturationFilter(
-                            saturation: -1,
-                            child: BrightnessFilter(
-                              brightness: -5,
-                              child: image,
-                            ),
-                          ),
-                        )
-                      : image,
-                ),
-                const SizedBox(
-                  height: 20,
-                ),
-                Text(
-                  "map_zoom_to_see_photos".tr(),
-                  style: TextStyle(
-                    fontSize: 20,
-                    color: context.textTheme.displayLarge?.color,
-                  ),
-                ),
-              ],
-            )
-          : const SizedBox.shrink();
-    }
-
-    void onTapMapButton() {
-      if (assetOffsetInSheet != -1) {
-        widget.bottomSheetEventSC.add(
-          MapPageZoomToAsset(
-            _cachedRenderList?.allAssets?[assetOffsetInSheet],
-          ),
-        );
-      }
-    }
-
-    Widget buildDragHandle(ScrollController scrollController) {
-      final textToDisplay = assetsInBound.value.isNotEmpty
-          ? "map_assets_in_bounds"
-              .tr(args: [assetsInBound.value.length.toString()])
-          : "map_no_assets_in_bounds".tr();
-      final dragHandle = Container(
-        height: 70,
-        width: double.infinity,
-        decoration: BoxDecoration(
-          color: isDarkTheme ? Colors.grey[900] : Colors.grey[100],
-        ),
-        child: Stack(
-          children: [
-            Column(
-              crossAxisAlignment: CrossAxisAlignment.center,
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: [
-                const SizedBox(height: 5),
-                const CustomDraggingHandle(),
-                const SizedBox(height: 15),
-                Text(
-                  textToDisplay,
-                  style: context.textTheme.bodyLarge,
-                ),
-                Divider(
-                  height: 10,
-                  color:
-                      context.textTheme.displayLarge?.color?.withOpacity(0.5),
-                ),
-              ],
-            ),
-            if (isSheetExpanded.value && isSheetScrolled.value)
-              Positioned(
-                top: 5,
-                right: 10,
-                child: IconButton(
-                  icon: Icon(
-                    Icons.map_outlined,
-                    color: context.textTheme.displayLarge?.color,
-                  ),
-                  iconSize: 20,
-                  tooltip: 'Zoom to bounds',
-                  onPressed: onTapMapButton,
-                ),
-              ),
-          ],
-        ),
-      );
-      return SingleChildScrollView(
-        controller: scrollController,
-        physics: const ClampingScrollPhysics(),
-        child: dragHandle,
-      );
-    }
-
-    return NotificationListener<DraggableScrollableNotification>(
-      onNotification: (DraggableScrollableNotification notification) {
-        final sheetExtended = notification.extent > 0.2;
-        isSheetExpanded.value = sheetExtended;
-        currentExtend.value = notification.extent;
-        if (!sheetExtended) {
-          // reset state
-          userTappedOnMap = false;
-          assetOffsetInSheet = -1;
-          isSheetScrolled.value = false;
-        }
-
-        return true;
-      },
-      child: Padding(
-        padding: EdgeInsets.only(
-          bottom: bottomPadding,
-        ),
-        child: Stack(
-          children: [
-            DraggableScrollableSheet(
-              controller: bottomSheetController,
-              initialChildSize: 0.1,
-              minChildSize: 0.1,
-              maxChildSize: 0.55,
-              snap: true,
-              builder: (
-                BuildContext context,
-                ScrollController scrollController,
-              ) {
-                return Card(
-                  color: isDarkTheme ? Colors.grey[900] : Colors.grey[100],
-                  surfaceTintColor: Colors.transparent,
-                  elevation: 18.0,
-                  margin: const EdgeInsets.all(0),
-                  child: Column(
-                    children: [
-                      buildDragHandle(scrollController),
-                      if (isSheetExpanded.value &&
-                          assetsInBound.value.isNotEmpty)
-                        ref
-                            .watch(
-                              renderListProvider(
-                                assetsInBound.value,
-                              ),
-                            )
-                            .when(
-                              data: (renderList) {
-                                _cachedRenderList = renderList;
-                                final assetGrid = ImmichAssetGrid(
-                                  shrinkWrap: true,
-                                  renderList: renderList,
-                                  showDragScroll: false,
-                                  selectionActive: widget.selectionEnabled,
-                                  showMultiSelectIndicator: false,
-                                  listener: widget.selectionlistener,
-                                  visibleItemsListener: visibleItemsListener,
-                                );
-
-                                return Expanded(child: assetGrid);
-                              },
-                              error: (error, stackTrace) {
-                                log.warning(
-                                  "Cannot get assets in the current map bounds ${error.toString()}",
-                                  error,
-                                  stackTrace,
-                                );
-                                return const SizedBox.shrink();
-                              },
-                              loading: () => const SizedBox.shrink(),
-                            ),
-                      if (isSheetExpanded.value && assetsInBound.value.isEmpty)
-                        Expanded(
-                          child: SingleChildScrollView(
-                            child: buildNoPhotosWidget(),
-                          ),
-                        ),
-                    ],
-                  ),
-                );
-              },
-            ),
-            Positioned(
-              bottom: maxHeight * currentExtend.value,
-              left: 0,
-              child: ColoredBox(
-                color:
-                    (widget.isDarkTheme ? Colors.grey[900] : Colors.grey[100])!,
-                child: Padding(
-                  padding: const EdgeInsets.all(3),
-                  child: Text(
-                    'OpenStreetMap contributors',
-                    style: TextStyle(
-                      fontSize: 6,
-                      color: !widget.isDarkTheme
-                          ? Colors.grey[900]
-                          : Colors.grey[100],
-                    ),
-                  ),
-                ),
-              ),
-            ),
-            Positioned(
-              bottom: maxHeight * (0.14 + (currentExtend.value - 0.1)),
-              right: 15,
-              child: ElevatedButton(
-                onPressed: () => widget.bottomSheetEventSC
-                    .add(const MapPageZoomToLocation()),
-                style: ElevatedButton.styleFrom(
-                  shape: const CircleBorder(),
-                  padding: const EdgeInsets.all(12),
-                ),
-                child: const Icon(
-                  Icons.my_location,
-                  size: 22,
-                  fill: 1,
-                ),
-              ),
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/modules/map/ui/map_settings_dialog.dart b/mobile/lib/modules/map/ui/map_settings_dialog.dart
deleted file mode 100644
index 7f88f74d42..0000000000
--- a/mobile/lib/modules/map/ui/map_settings_dialog.dart
+++ /dev/null
@@ -1,228 +0,0 @@
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
-
-class MapSettingsDialog extends HookConsumerWidget {
-  const MapSettingsDialog({super.key});
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final mapSettingsNotifier = ref.read(mapStateNotifier.notifier);
-    final mapSettings = ref.read(mapStateNotifier);
-    final isDarkMode = useState(mapSettings.isDarkTheme);
-    final showFavoriteOnly = useState(mapSettings.showFavoriteOnly);
-    final showIncludeArchived = useState(mapSettings.includeArchived);
-    final showRelativeDate = useState(mapSettings.relativeTime);
-    final ThemeData theme = context.themeData;
-
-    Widget buildMapThemeSetting() {
-      return SwitchListTile.adaptive(
-        value: isDarkMode.value,
-        onChanged: (value) {
-          isDarkMode.value = value;
-        },
-        activeColor: theme.primaryColor,
-        dense: true,
-        title: Text(
-          "map_settings_dark_mode".tr(),
-          style:
-              theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
-        ),
-      );
-    }
-
-    Widget buildFavoriteOnlySetting() {
-      return SwitchListTile.adaptive(
-        value: showFavoriteOnly.value,
-        onChanged: (value) {
-          showFavoriteOnly.value = value;
-        },
-        activeColor: theme.primaryColor,
-        dense: true,
-        title: Text(
-          "map_settings_only_show_favorites".tr(),
-          style:
-              theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
-        ),
-      );
-    }
-
-    Widget buildIncludeArchivedSetting() {
-      return SwitchListTile.adaptive(
-        value: showIncludeArchived.value,
-        onChanged: (value) {
-          showIncludeArchived.value = value;
-        },
-        activeColor: theme.primaryColor,
-        dense: true,
-        title: Text(
-          "map_settings_include_show_archived".tr(),
-          style:
-              theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
-        ),
-      );
-    }
-
-    Widget buildDateRangeSetting() {
-      final now = DateTime.now();
-      return DropdownMenu(
-        enableSearch: false,
-        enableFilter: false,
-        initialSelection: showRelativeDate.value,
-        onSelected: (value) {
-          showRelativeDate.value = value!;
-        },
-        dropdownMenuEntries: [
-          DropdownMenuEntry(
-            value: 0,
-            label: "map_settings_date_range_option_all".tr(),
-          ),
-          DropdownMenuEntry(
-            value: 1,
-            label: "map_settings_date_range_option_day".tr(),
-          ),
-          DropdownMenuEntry(
-            value: 7,
-            label: "map_settings_date_range_option_days".tr(
-              args: ["7"],
-            ),
-          ),
-          DropdownMenuEntry(
-            value: 30,
-            label: "map_settings_date_range_option_days".tr(
-              args: ["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(args: ["3"]),
-          ),
-        ],
-      );
-    }
-
-    List<Widget> getDialogActions() {
-      return <Widget>[
-        TextButton(
-          onPressed: () => context.pop(),
-          style: TextButton.styleFrom(
-            backgroundColor:
-                mapSettings.isDarkTheme ? Colors.grey[100] : Colors.grey[700],
-          ),
-          child: Padding(
-            padding: const EdgeInsets.symmetric(horizontal: 16.0),
-            child: Text(
-              "map_settings_dialog_cancel".tr(),
-              style: theme.textTheme.labelLarge?.copyWith(
-                fontWeight: FontWeight.w500,
-                color: mapSettings.isDarkTheme
-                    ? Colors.grey[900]
-                    : Colors.grey[100],
-              ),
-            ),
-          ),
-        ),
-        TextButton(
-          onPressed: () {
-            mapSettingsNotifier.switchTheme(isDarkMode.value);
-            mapSettingsNotifier.switchFavoriteOnly(showFavoriteOnly.value);
-            mapSettingsNotifier.setRelativeTime(showRelativeDate.value);
-            mapSettingsNotifier
-                .switchIncludeArchived(showIncludeArchived.value);
-            context.pop();
-          },
-          style: TextButton.styleFrom(
-            backgroundColor: theme.primaryColor,
-          ),
-          child: Padding(
-            padding: const EdgeInsets.symmetric(horizontal: 16.0),
-            child: Text(
-              "map_settings_dialog_save".tr(),
-              style: theme.textTheme.labelLarge?.copyWith(
-                fontWeight: FontWeight.w500,
-                color: theme.primaryTextTheme.labelLarge?.color,
-              ),
-            ),
-          ),
-        ),
-      ];
-    }
-
-    return AlertDialog(
-      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
-      title: Center(
-        child: Text(
-          "map_settings_dialog_title".tr(),
-          style: TextStyle(
-            color: theme.primaryColor,
-            fontWeight: FontWeight.bold,
-            fontSize: 18,
-          ),
-        ),
-      ),
-      content: SizedBox(
-        width: double.maxFinite,
-        child: ConstrainedBox(
-          constraints: BoxConstraints(
-            maxHeight: context.height * 0.6,
-          ),
-          child: ListView(
-            shrinkWrap: true,
-            children: [
-              buildMapThemeSetting(),
-              buildFavoriteOnlySetting(),
-              buildIncludeArchivedSetting(),
-              const SizedBox(
-                height: 10,
-              ),
-              Padding(
-                padding: const EdgeInsets.only(left: 20),
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    Text(
-                      "map_settings_only_relative_range".tr(),
-                      style: const TextStyle(fontWeight: FontWeight.bold),
-                    ),
-                    buildDateRangeSetting(),
-                  ],
-                ),
-              ),
-            ].toList(),
-          ),
-        ),
-      ),
-      actions: getDialogActions(),
-      actionsAlignment: MainAxisAlignment.spaceEvenly,
-    );
-  }
-}
diff --git a/mobile/lib/modules/map/ui/map_thumbnail.dart b/mobile/lib/modules/map/ui/map_thumbnail.dart
deleted file mode 100644
index e385eb9705..0000000000
--- a/mobile/lib/modules/map/ui/map_thumbnail.dart
+++ /dev/null
@@ -1,86 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:flutter_map/plugin_api.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
-import 'package:immich_mobile/modules/map/utils/map_controller_hook.dart';
-import 'package:latlong2/latlong.dart';
-import 'package:url_launcher/url_launcher.dart';
-
-// A non-interactive thumbnail of a map in the given coordinates with optional markers
-class MapThumbnail extends HookConsumerWidget {
-  final Function(TapPosition, LatLng)? onTap;
-  final LatLng coords;
-  final double zoom;
-  final List<Marker> markers;
-  final double height;
-  final double width;
-  final bool showAttribution;
-  final bool isDarkTheme;
-
-  const MapThumbnail({
-    super.key,
-    required this.coords,
-    this.height = 100,
-    this.width = 100,
-    this.onTap,
-    this.zoom = 1,
-    this.showAttribution = true,
-    this.isDarkTheme = false,
-    this.markers = const [],
-  });
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final mapController = useMapController();
-    final isMapReady = useRef(false);
-    ref.watch(mapStateNotifier.select((s) => s.mapStyle));
-
-    useEffect(
-      () {
-        if (isMapReady.value && mapController.center != coords) {
-          mapController.move(coords, zoom);
-        }
-        return null;
-      },
-      [coords],
-    );
-
-    return SizedBox(
-      height: height,
-      width: width,
-      child: ClipRRect(
-        borderRadius: const BorderRadius.all(Radius.circular(15)),
-        child: FlutterMap(
-          mapController: mapController,
-          options: MapOptions(
-            interactiveFlags: InteractiveFlag.none,
-            center: coords,
-            zoom: zoom,
-            onTap: onTap,
-            onMapReady: () => isMapReady.value = true,
-          ),
-          nonRotatedChildren: [
-            if (showAttribution)
-              RichAttributionWidget(
-                animationConfig: const ScaleRAWA(),
-                attributions: [
-                  TextSourceAttribution(
-                    'OpenStreetMap contributors',
-                    onTap: () => launchUrl(
-                      Uri.parse('https://openstreetmap.org/copyright'),
-                      mode: LaunchMode.externalApplication,
-                    ),
-                  ),
-                ],
-              ),
-          ],
-          children: [
-            ref.read(mapStateNotifier.notifier).getTileLayer(isDarkTheme),
-            if (markers.isNotEmpty) MarkerLayer(markers: markers),
-          ],
-        ),
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/modules/map/utils/map_controller_hook.dart b/mobile/lib/modules/map/utils/map_controller_hook.dart
deleted file mode 100644
index e5812c938b..0000000000
--- a/mobile/lib/modules/map/utils/map_controller_hook.dart
+++ /dev/null
@@ -1,32 +0,0 @@
-import 'package:flutter/widgets.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:flutter_map/flutter_map.dart';
-
-MapController useMapController({
-  String? debugLabel,
-  List<Object?>? keys,
-}) {
-  return use(_MapControllerHook(keys: keys));
-}
-
-class _MapControllerHook extends Hook<MapController> {
-  const _MapControllerHook({List<Object?>? keys}) : super(keys: keys);
-
-  @override
-  HookState<MapController, Hook<MapController>> createState() =>
-      _MapControllerHookState();
-}
-
-class _MapControllerHookState
-    extends HookState<MapController, _MapControllerHook> {
-  late final controller = MapController();
-
-  @override
-  MapController build(BuildContext context) => controller;
-
-  @override
-  void dispose() => controller.dispose();
-
-  @override
-  String get debugLabel => 'useMapController';
-}
diff --git a/mobile/lib/modules/map/utils/map_utils.dart b/mobile/lib/modules/map/utils/map_utils.dart
new file mode 100644
index 0000000000..5fec97ea03
--- /dev/null
+++ b/mobile/lib/modules/map/utils/map_utils.dart
@@ -0,0 +1,138 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/modules/map/models/map_marker.dart';
+import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
+import 'package:geolocator/geolocator.dart';
+import 'package:logging/logging.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+
+class MapUtils {
+  MapUtils._();
+
+  static final Logger _log = Logger("MapUtils");
+  static const defaultSourceId = 'asset-map-markers';
+  static const defaultHeatMapLayerId = 'asset-heatmap-layer';
+
+  static const defaultHeatMapLayerProperties = HeatmapLayerProperties(
+    heatmapColor: [
+      Expressions.interpolate,
+      ["linear"],
+      ["heatmap-density"],
+      0.0,
+      "rgba(246,239,247,0.0)",
+      0.2,
+      "rgb(208,209,230)",
+      0.4,
+      "rgb(166,189,219)",
+      0.6,
+      "rgb(103,169,207)",
+      0.8,
+      "rgb(28,144,153)",
+      1.0,
+      "rgb(1,108,89)",
+    ],
+    heatmapIntensity: [
+      Expressions.interpolate, ["linear"], //
+      [Expressions.zoom],
+      0, 0.5,
+      9, 2,
+    ],
+    heatmapRadius: [
+      Expressions.interpolate, ["linear"], //
+      [Expressions.zoom],
+      0, 4,
+      4, 8,
+      9, 16,
+    ],
+  );
+
+  static Map<String, dynamic> _addFeature(MapMarker marker) => {
+        'type': 'Feature',
+        'id': marker.assetRemoteId,
+        'geometry': {
+          'type': 'Point',
+          'coordinates': [marker.latLng.longitude, marker.latLng.latitude],
+        },
+      };
+
+  static Map<String, dynamic> generateGeoJsonForMarkers(
+    List<MapMarker> markers,
+  ) =>
+      {
+        'type': 'FeatureCollection',
+        'features': markers.map(_addFeature).toList(),
+      };
+
+  static Future<(Position?, LocationPermission?)> checkPermAndGetLocation(
+    BuildContext context,
+  ) async {
+    try {
+      bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
+      if (!serviceEnabled) {
+        showDialog(
+          context: context,
+          builder: (context) => _LocationServiceDisabledDialog(),
+        );
+        return (null, LocationPermission.deniedForever);
+      }
+
+      LocationPermission permission = await Geolocator.checkPermission();
+      bool shouldRequestPermission = false;
+
+      if (permission == LocationPermission.denied) {
+        shouldRequestPermission = await showDialog(
+          context: context,
+          builder: (context) => _LocationPermissionDisabledDialog(),
+        );
+        if (shouldRequestPermission) {
+          permission = await Geolocator.requestPermission();
+        }
+      }
+
+      if (permission == LocationPermission.denied ||
+          permission == LocationPermission.deniedForever) {
+        // Open app settings only if you did not request for permission before
+        if (permission == LocationPermission.deniedForever &&
+            !shouldRequestPermission) {
+          await Geolocator.openAppSettings();
+        }
+        return (null, LocationPermission.deniedForever);
+      }
+
+      Position currentUserLocation = await Geolocator.getCurrentPosition(
+        desiredAccuracy: LocationAccuracy.medium,
+        timeLimit: const Duration(seconds: 5),
+      );
+      return (currentUserLocation, null);
+    } catch (error) {
+      _log.severe(
+        "Cannot get user's current location due to ${error.toString()}",
+      );
+      return (null, LocationPermission.unableToDetermine);
+    }
+  }
+}
+
+class _LocationServiceDisabledDialog extends ConfirmDialog {
+  _LocationServiceDisabledDialog()
+      : super(
+          title: 'map_location_service_disabled_title'.tr(),
+          content: 'map_location_service_disabled_content'.tr(),
+          cancel: 'map_location_dialog_cancel'.tr(),
+          ok: 'map_location_dialog_yes'.tr(),
+          onOk: () async {
+            await Geolocator.openLocationSettings();
+          },
+        );
+}
+
+class _LocationPermissionDisabledDialog extends ConfirmDialog {
+  _LocationPermissionDisabledDialog()
+      : super(
+          title: 'map_no_location_permission_title'.tr(),
+          content: 'map_no_location_permission_content'.tr(),
+          cancel: 'map_location_dialog_cancel'.tr(),
+          ok: 'map_location_dialog_yes'.tr(),
+          onOk: () {},
+        );
+}
diff --git a/mobile/lib/modules/map/views/map_location_picker_page.dart b/mobile/lib/modules/map/views/map_location_picker_page.dart
new file mode 100644
index 0000000000..34634106df
--- /dev/null
+++ b/mobile/lib/modules/map/views/map_location_picker_page.dart
@@ -0,0 +1,185 @@
+import 'dart:math';
+
+import 'package:auto_route/auto_route.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
+import 'package:immich_mobile/modules/map/widgets/map_theme_override.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import 'package:immich_mobile/modules/map/utils/map_utils.dart';
+import 'package:geolocator/geolocator.dart';
+
+class MapLocationPickerPage extends HookConsumerWidget {
+  final LatLng initialLatLng;
+
+  const MapLocationPickerPage({
+    super.key,
+    this.initialLatLng = const LatLng(0, 0),
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final selectedLatLng = useValueNotifier<LatLng>(initialLatLng);
+    final controller = useRef<MaplibreMapController?>(null);
+    final marker = useRef<Symbol?>(null);
+
+    Future<void> onStyleLoaded() async {
+      marker.value = await controller.value?.addMarkerAtLatLng(initialLatLng);
+    }
+
+    Future<void> onMapClick(Point<num> point, LatLng centre) async {
+      selectedLatLng.value = centre;
+      controller.value?.animateCamera(CameraUpdate.newLatLng(centre));
+      if (marker.value != null) {
+        await controller.value
+            ?.updateSymbol(marker.value!, SymbolOptions(geometry: centre));
+      }
+    }
+
+    void onClose([LatLng? selected]) {
+      context.popRoute(selected);
+    }
+
+    Future<void> getCurrentLocation() async {
+      var (currentLocation, locationPermission)  = await MapUtils.checkPermAndGetLocation(context);
+      if (locationPermission == LocationPermission.denied ||
+          locationPermission == LocationPermission.deniedForever) {
+        return;
+      }
+      if (currentLocation == null) {
+        return;
+      }
+      var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude);
+      selectedLatLng.value = currentLatLng;
+      controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng));
+    }
+
+    return MapThemeOveride(
+      mapBuilder: (style) => Builder(
+        builder: (ctx) => Scaffold(
+          backgroundColor: ctx.themeData.cardColor,
+          appBar: _AppBar(onClose: onClose),
+          extendBodyBehindAppBar: true,
+          body: Column(
+            children: [
+              style.widgetWhen(
+                onData: (style) => Expanded(
+                  child: Container(
+                    clipBehavior: Clip.antiAliasWithSaveLayer,
+                    decoration: const BoxDecoration(
+                      borderRadius: BorderRadius.only(
+                        bottomLeft: Radius.circular(40),
+                        bottomRight: Radius.circular(40),
+                      ),
+                    ),
+                    child: MaplibreMap(
+                      initialCameraPosition:
+                          CameraPosition(target: initialLatLng, zoom: 12),
+                      styleString: style,
+                      onMapCreated: (mapController) =>
+                          controller.value = mapController,
+                      onStyleLoadedCallback: onStyleLoaded,
+                      onMapClick: onMapClick,
+                      dragEnabled: false,
+                      tiltGesturesEnabled: false,
+                      myLocationEnabled: false,
+                      attributionButtonMargins: const Point(20, 15),
+                    ),
+                  ),
+                ),
+              ),
+              _BottomBar(
+                selectedLatLng: selectedLatLng,
+                onUseLocation: () => onClose(selectedLatLng.value),
+                onGetCurrentLocation: getCurrentLocation,
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class _AppBar extends StatelessWidget implements PreferredSizeWidget {
+  final Function() onClose;
+
+  const _AppBar({required this.onClose});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top + 25),
+      child: Expanded(
+        child: Align(
+          alignment: Alignment.centerLeft,
+          child: ElevatedButton(
+            onPressed: onClose,
+            style: ElevatedButton.styleFrom(
+              shape: const CircleBorder(),
+            ),
+            child: const Icon(Icons.arrow_back_ios_new_rounded),
+          ),
+        ),
+      ),
+    );
+  }
+
+  @override
+  Size get preferredSize => const Size.fromHeight(100);
+}
+
+class _BottomBar extends StatelessWidget {
+  final ValueNotifier<LatLng> selectedLatLng;
+  final Function() onUseLocation;
+  final Function() onGetCurrentLocation;
+
+  const _BottomBar({
+    required this.selectedLatLng,
+    required this.onUseLocation,
+    required this.onGetCurrentLocation,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: 150,
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              const Icon(Icons.public, size: 18),
+              const SizedBox(width: 15),
+              ValueListenableBuilder(
+                valueListenable: selectedLatLng,
+                builder: (_, value, __) => Text(
+                  "${value.latitude.toStringAsFixed(4)}, ${value.longitude.toStringAsFixed(4)}",
+                ),
+              ),
+            ],
+          ),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+            children: [
+              ElevatedButton(
+                onPressed: onUseLocation,
+                child: const Text("map_location_picker_page_use_location").tr(),
+              ),
+              ElevatedButton(
+                onPressed: onGetCurrentLocation,
+                child: const Icon(Icons.my_location),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/views/map_page.dart b/mobile/lib/modules/map/views/map_page.dart
index e61bb236e0..b01e29898b 100644
--- a/mobile/lib/modules/map/views/map_page.dart
+++ b/mobile/lib/modules/map/views/map_page.dart
@@ -1,250 +1,225 @@
-import 'dart:async';
-import 'dart:math' as math;
-
+import 'dart:math';
 import 'package:auto_route/auto_route.dart';
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:flutter_map/plugin_api.dart';
-import 'package:flutter_map_heatmap/flutter_map_heatmap.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:geolocator/geolocator.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/modules/map/models/map_page_event.model.dart';
+import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
+import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
+import 'package:immich_mobile/modules/map/models/map_event.model.dart';
+import 'package:immich_mobile/modules/map/models/map_marker.dart';
 import 'package:immich_mobile/modules/map/providers/map_marker.provider.dart';
 import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
-import 'package:immich_mobile/modules/map/ui/asset_marker_icon.dart';
-import 'package:immich_mobile/modules/map/ui/location_dialog.dart';
-import 'package:immich_mobile/modules/map/ui/map_page_bottom_sheet.dart';
-import 'package:immich_mobile/modules/map/ui/map_page_app_bar.dart';
+import 'package:immich_mobile/modules/map/utils/map_utils.dart';
+import 'package:immich_mobile/modules/map/widgets/map_app_bar.dart';
+import 'package:immich_mobile/modules/map/widgets/map_asset_grid.dart';
+import 'package:immich_mobile/modules/map/widgets/map_bottom_sheet.dart';
+import 'package:immich_mobile/modules/map/widgets/map_theme_override.dart';
+import 'package:immich_mobile/modules/map/widgets/positioned_asset_marker_icon.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
+import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 import 'package:immich_mobile/utils/debounce.dart';
-import 'package:immich_mobile/extensions/flutter_map_extensions.dart';
-import 'package:immich_mobile/utils/immich_app_theme.dart';
-import 'package:immich_mobile/utils/selection_handlers.dart';
-import 'package:latlong2/latlong.dart';
-import 'package:logging/logging.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
 
-class MapPage extends StatefulHookConsumerWidget {
+class MapPage extends HookConsumerWidget {
   const MapPage({super.key});
 
   @override
-  MapPageState createState() => MapPageState();
-}
+  Widget build(BuildContext context, WidgetRef ref) {
+    final mapController = useRef<MaplibreMapController?>(null);
+    final markers = useRef<List<MapMarker>>([]);
+    final markersInBounds = useRef<List<MapMarker>>([]);
+    final bottomSheetStreamController = useStreamController<MapEvent>();
+    final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null);
+    final assetsDebouncer = useDebouncer();
+    final isLoading = useProcessingOverlay();
+    final scrollController = useScrollController();
+    final markerDebouncer =
+        useDebouncer(interval: const Duration(milliseconds: 800));
+    final selectedAssets = useValueNotifier<Set<Asset>>({});
+    const mapZoomToAssetLevel = 12.0;
 
-class MapPageState extends ConsumerState<MapPage> {
-  // Non-State variables
-  late final MapController mapController;
-  // Streams are used instead of callbacks to prevent unnecessary rebuilds on events
-  final StreamController mapPageEventSC =
-      StreamController<MapPageEventBase>.broadcast();
-  final StreamController bottomSheetEventSC =
-      StreamController<MapPageEventBase>.broadcast();
-  late final Stream bottomSheetEventStream;
-  // Making assets in bounds as a state variable will result in un-necessary rebuilds of the bottom sheet
-  // resulting in it getting reloaded each time a map move occurs
-  Set<AssetMarkerData> assetsInBounds = {};
-  // TODO: Migrate the handling to MapEventMove#id when flutter_map is upgraded
-  // https://github.com/fleaflet/flutter_map/issues/1542
-  // The below is used instead of MapEventMove#id to handle event from controller
-  // in onMapEvent() since MapEventMove#id is not populated properly in the
-  // current version of flutter_map(4.0.0) used
-  bool forceAssetUpdate = false;
-  bool isMapReady = false;
-  late final Debounce debounce;
-
-  @override
-  void initState() {
-    super.initState();
-    mapController = MapController();
-    bottomSheetEventStream = bottomSheetEventSC.stream;
-    // Map zoom events and move events are triggered often. Throttle the call to limit rebuilds
-    debounce = Debounce(
-      const Duration(milliseconds: 300),
-    );
-  }
-
-  @override
-  void dispose() {
-    debounce.dispose();
-    super.dispose();
-  }
-
-  void reloadAssetsInBound(
-    Set<AssetMarkerData>? assetMarkers, {
-    bool forceReload = false,
-  }) {
-    try {
-      final bounds = isMapReady ? mapController.bounds : null;
-      if (bounds != null) {
-        final oldAssetsInBounds = assetsInBounds.toSet();
-        assetsInBounds =
-            assetMarkers?.where((e) => bounds.contains(e.point)).toSet() ?? {};
-        final shouldReload = forceReload ||
-            assetsInBounds.difference(oldAssetsInBounds).isNotEmpty ||
-            assetsInBounds.length != oldAssetsInBounds.length;
-        if (shouldReload) {
-          mapPageEventSC.add(
-            MapPageAssetsInBoundUpdated(
-              assetsInBounds.map((e) => e.asset).toList(),
-            ),
-          );
-        }
+    // updates the markersInBounds value with the map markers that are visible in the current
+    // map camera bounds
+    Future<void> updateAssetsInBounds() async {
+      // Guard map not created
+      if (mapController.value == null) {
+        return;
       }
-    } finally {
-      // Consume all error
-    }
-  }
 
-  void openAssetInViewer(Asset asset) {
-    context.pushRoute(
-      GalleryViewerRoute(
-        initialIndex: 0,
-        loadAsset: (index) => asset,
-        totalAssets: 1,
-        heroOffset: 0,
-      ),
+      final bounds = await mapController.value!.getVisibleRegion();
+      final inBounds = markers.value
+          .where(
+            (m) =>
+                bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude)),
+          )
+          .toList();
+      // Notify bottom sheet to update asset grid only when there are new assets
+      if (markersInBounds.value.length != inBounds.length) {
+        bottomSheetStreamController.add(
+          MapAssetsInBoundsUpdated(
+            inBounds.map((e) => e.assetRemoteId).toList(),
+          ),
+        );
+      }
+      markersInBounds.value = inBounds;
+    }
+
+    // removes all sources and layers and re-adds them with the updated markers
+    Future<void> reloadLayers() async {
+      if (mapController.value != null) {
+        mapController.value!.reloadAllLayersForMarkers(markers.value);
+      }
+    }
+
+    Future<void> loadMarkers() async {
+      try {
+        isLoading.value = true;
+        markers.value = await ref.read(mapMarkersProvider.future);
+        assetsDebouncer.run(updateAssetsInBounds);
+        reloadLayers();
+      } finally {
+        isLoading.value = false;
+      }
+    }
+
+    useEffect(
+      () {
+        loadMarkers();
+        return null;
+      },
+      [],
     );
-  }
 
-  @override
-  Widget build(BuildContext context) {
-    final log = Logger("MapService");
-    final isDarkTheme =
-        ref.watch(mapStateNotifier.select((state) => state.isDarkTheme));
-    final ValueNotifier<Set<AssetMarkerData>> mapMarkerData =
-        useState(<AssetMarkerData>{});
-    final ValueNotifier<AssetMarkerData?> closestAssetMarker = useState(null);
-    final selectionEnabledHook = useState(false);
-    final selectedAssets = useState(<Asset>{});
-    final showLoadingIndicator = useState(false);
-    final refetchMarkers = useState(true);
-    final isLoading =
-        ref.watch(mapStateNotifier.select((state) => state.isLoading));
-    final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom;
-    final zoomLevel = math.min(maxZoom, 14.0);
-
-    if (refetchMarkers.value) {
-      mapMarkerData.value = ref.watch(mapMarkersProvider).when(
-            skipLoadingOnRefresh: false,
-            error: (error, stackTrace) {
-              log.warning(
-                "Cannot get map markers ${error.toString()}",
-                error,
-                stackTrace,
-              );
-              showLoadingIndicator.value = false;
-              return {};
-            },
-            loading: () {
-              showLoadingIndicator.value = true;
-              return {};
-            },
-            data: (data) {
-              showLoadingIndicator.value = false;
-              refetchMarkers.value = false;
-              closestAssetMarker.value = null;
-              debounce(
-                () => reloadAssetsInBound(
-                  mapMarkerData.value,
-                  forceReload: true,
-                ),
-              );
-              return data;
-            },
-          );
-    }
-
-    ref.listen(mapStateNotifier, (previous, next) {
-      bool shouldRefetch =
-          previous?.showFavoriteOnly != next.showFavoriteOnly ||
-              previous?.relativeTime != next.relativeTime ||
-              previous?.includeArchived != next.includeArchived;
-      if (shouldRefetch) {
-        refetchMarkers.value = shouldRefetch;
-        ref.invalidate(mapMarkersProvider);
+    // Refetch markers when map state is changed
+    ref.listen(mapStateNotifierProvider, (_, current) {
+      if (current.shouldRefetchMarkers) {
+        markerDebouncer.run(() {
+          ref.invalidate(mapMarkersProvider);
+          // Reset marker
+          selectedMarker.value = null;
+          loadMarkers();
+          ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false);
+        });
       }
     });
 
-    void onZoomToAssetEvent(Asset? assetInBottomSheet) {
-      if (assetInBottomSheet != null) {
-        final mapMarker = mapMarkerData.value
-            .firstWhereOrNull((e) => e.asset.id == assetInBottomSheet.id);
-        if (mapMarker != null) {
-          LatLng? newCenter = mapController.centerBoundsWithPadding(
-            mapMarker.point,
-            const Offset(0, -120),
-            zoomLevel: zoomLevel,
-          );
-          if (newCenter != null) {
-            forceAssetUpdate = true;
-            mapController.move(newCenter, zoomLevel);
-          }
+    // updates the selected markers position based on the current map camera
+    Future<void> updateAssetMarkerPosition(
+      MapMarker marker, {
+      bool shouldAnimate = true,
+    }) async {
+      final assetPoint =
+          await mapController.value!.toScreenLocation(marker.latLng);
+      selectedMarker.value = _AssetMarkerMeta(
+        point: assetPoint,
+        marker: marker,
+        shouldAnimate: shouldAnimate,
+      );
+      (assetPoint, marker, shouldAnimate);
+    }
+
+    // finds the nearest asset marker from the tap point and store it as the selectedMarker
+    Future<void> onMarkerClicked(Point<double> point, LatLng coords) async {
+      // Guard map not created
+      if (mapController.value == null) {
+        return;
+      }
+      final latlngBound =
+          await mapController.value!.getBoundsFromPoint(point, 50);
+      final marker = markersInBounds.value.firstWhereOrNull(
+        (m) =>
+            latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)),
+      );
+
+      if (marker != null) {
+        updateAssetMarkerPosition(marker);
+      } else {
+        // If no asset was previously selected and no new asset is available, close the bottom sheet
+        if (selectedMarker.value == null) {
+          bottomSheetStreamController.add(MapCloseBottomSheet());
         }
+        selectedMarker.value = null;
+      }
+    }
+
+    void onMapCreated(MaplibreMapController controller) async {
+      mapController.value = controller;
+      controller.addListener(() {
+        if (controller.isCameraMoving && selectedMarker.value != null) {
+          updateAssetMarkerPosition(
+            selectedMarker.value!.marker,
+            shouldAnimate: false,
+          );
+        }
+      });
+    }
+
+    Future<void> onMarkerTapped() async {
+      final assetId = selectedMarker.value?.marker.assetRemoteId;
+      if (assetId == null) {
+        return;
+      }
+
+      final asset = await ref.read(dbProvider).assets.getByRemoteId(assetId);
+      if (asset == null) {
+        return;
+      }
+
+      context.pushRoute(
+        GalleryViewerRoute(
+          initialIndex: 0,
+          loadAsset: (index) => asset,
+          totalAssets: 1,
+          heroOffset: 0,
+        ),
+      );
+    }
+
+    /// BOTTOM SHEET CALLBACKS
+
+    Future<void> onMapMoved() async {
+      assetsDebouncer.run(updateAssetsInBounds);
+    }
+
+    void onBottomSheetScrolled(String assetRemoteId) {
+      final assetMarker = markersInBounds.value
+          .firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId);
+      if (assetMarker != null) {
+        updateAssetMarkerPosition(assetMarker);
+      }
+    }
+
+    void onZoomToAsset(String assetRemoteId) {
+      final assetMarker = markersInBounds.value
+          .firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId);
+      if (mapController.value != null && assetMarker != null) {
+        // Offset the latitude a little to show the marker just above the viewports center
+        final offset = context.isMobile ? 0.02 : 0;
+        final latlng = LatLng(
+          assetMarker.latLng.latitude - offset,
+          assetMarker.latLng.longitude,
+        );
+        mapController.value!.animateCamera(
+          CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel),
+          duration: const Duration(milliseconds: 800),
+        );
       }
     }
 
     void onZoomToLocation() async {
-      try {
-        bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
-        if (!serviceEnabled) {
-          showDialog(
-            context: context,
-            builder: (context) => Theme(
-              data: isDarkTheme ? immichDarkTheme : immichLightTheme,
-              child: LocationServiceDisabledDialog(),
-            ),
-          );
-          return;
-        }
-
-        LocationPermission permission = await Geolocator.checkPermission();
-        bool shouldRequestPermission = false;
-
-        if (permission == LocationPermission.denied) {
-          shouldRequestPermission = await showDialog(
-            context: context,
-            builder: (context) => Theme(
-              data: isDarkTheme ? immichDarkTheme : immichLightTheme,
-              child: LocationPermissionDisabledDialog(),
-            ),
-          );
-          if (shouldRequestPermission) {
-            permission = await Geolocator.requestPermission();
-          }
-        }
-
-        if (permission == LocationPermission.denied ||
-            permission == LocationPermission.deniedForever) {
-          // Open app settings only if you did not request for permission before
-          if (permission == LocationPermission.deniedForever &&
-              !shouldRequestPermission) {
-            await Geolocator.openAppSettings();
-          }
-          return;
-        }
-
-        Position currentUserLocation = await Geolocator.getCurrentPosition(
-          desiredAccuracy: LocationAccuracy.medium,
-          timeLimit: const Duration(seconds: 5),
-        );
-
-        forceAssetUpdate = true;
-        mapController.move(
-          LatLng(currentUserLocation.latitude, currentUserLocation.longitude),
-          zoomLevel,
-        );
-      } catch (error) {
-        log.severe(
-          "Cannot get user's current location due to ${error.toString()}",
-        );
-        if (context.mounted) {
+      final location = await MapUtils.checkPermAndGetLocation(context);
+      if (location.$2 != null) {
+        if (location.$2 == LocationPermission.unableToDetermine &&
+            context.mounted) {
           ImmichToast.show(
             context: context,
             gravity: ToastGravity.BOTTOM,
@@ -252,253 +227,180 @@ class MapPageState extends ConsumerState<MapPage> {
             msg: "map_cannot_get_user_location".tr(),
           );
         }
+        return;
       }
-    }
 
-    void handleBottomSheetEvents(dynamic event) {
-      if (event is MapPageBottomSheetScrolled) {
-        final assetInBottomSheet = event.asset;
-        if (assetInBottomSheet != null) {
-          final mapMarker = mapMarkerData.value
-              .firstWhereOrNull((e) => e.asset.id == assetInBottomSheet.id);
-          closestAssetMarker.value = mapMarker;
-          if (mapMarker != null && mapController.zoom >= 5) {
-            LatLng? newCenter = mapController.centerBoundsWithPadding(
-              mapMarker.point,
-              const Offset(0, -120),
-            );
-            if (newCenter != null) {
-              mapController.move(
-                newCenter,
-                mapController.zoom,
-              );
-            }
-          }
-        }
-      } else if (event is MapPageZoomToAsset) {
-        onZoomToAssetEvent(event.asset);
-      } else if (event is MapPageZoomToLocation) {
-        onZoomToLocation();
-      }
-    }
-
-    useEffect(
-      () {
-        final bottomSheetEventSubscription =
-            bottomSheetEventStream.listen(handleBottomSheetEvents);
-        return bottomSheetEventSubscription.cancel;
-      },
-      [bottomSheetEventStream],
-    );
-
-    void handleMapTapEvent(LatLng tapPosition) {
-      const d = Distance();
-      final assetsInBoundsList = assetsInBounds.toList();
-      assetsInBoundsList.sort(
-        (a, b) => d
-            .distance(a.point, tapPosition)
-            .compareTo(d.distance(b.point, tapPosition)),
-      );
-      // First asset less than the threshold from the tap point
-      final nearestAsset = assetsInBoundsList.firstWhereOrNull(
-        (element) =>
-            d.distance(element.point, tapPosition) <
-            mapController.getTapThresholdForZoomLevel(),
-      );
-      // Reset marker if no assets are near the tap point
-      if (nearestAsset == null && closestAssetMarker.value != null) {
-        selectionEnabledHook.value = false;
-        mapPageEventSC.add(
-          const MapPageOnTapEvent(),
+      if (mapController.value != null && location.$1 != null) {
+        mapController.value!.animateCamera(
+          CameraUpdate.newLatLngZoom(
+            LatLng(location.$1!.latitude, location.$1!.longitude),
+            mapZoomToAssetLevel,
+          ),
+          duration: const Duration(milliseconds: 800),
         );
       }
-      closestAssetMarker.value = nearestAsset;
     }
 
-    void onMapEvent(MapEvent mapEvent) {
-      if (mapEvent is MapEventMove || mapEvent is MapEventDoubleTapZoom) {
-        if (forceAssetUpdate ||
-            mapEvent.source != MapEventSource.mapController) {
-          debounce(() {
-            if (selectionEnabledHook.value) {
-              selectionEnabledHook.value = false;
-            }
-            reloadAssetsInBound(
-              mapMarkerData.value,
-              forceReload: forceAssetUpdate,
-            );
-            forceAssetUpdate = false;
-          });
-        }
-      } else if (mapEvent is MapEventTap) {
-        handleMapTapEvent(mapEvent.tapPosition);
-      }
+    void onAssetsSelected(bool selected, Set<Asset> selection) {
+      selectedAssets.value = selected ? selection : {};
     }
 
-    void onShareAsset() {
-      handleShareAssets(ref, context, selectedAssets.value.toList());
-      selectionEnabledHook.value = false;
-    }
+    return MapThemeOveride(
+      mapBuilder: (style) => context.isMobile
+          // Single-column
+          ? Scaffold(
+              extendBodyBehindAppBar: true,
+              appBar: MapAppBar(selectedAssets: selectedAssets),
+              body: Stack(
+                children: [
+                  _MapWithMarker(
+                    style: style,
+                    selectedMarker: selectedMarker,
+                    onMapCreated: onMapCreated,
+                    onMapMoved: onMapMoved,
+                    onMapClicked: onMarkerClicked,
+                    onStyleLoaded: reloadLayers,
+                    onMarkerTapped: onMarkerTapped,
+                  ),
+                  // Should be a part of the body and not scaffold::bottomsheet for the
+                  // location button to be hit testable
+                  MapBottomSheet(
+                    mapEventStream: bottomSheetStreamController.stream,
+                    onGridAssetChanged: onBottomSheetScrolled,
+                    onZoomToAsset: onZoomToAsset,
+                    onAssetsSelected: onAssetsSelected,
+                    onZoomToLocation: onZoomToLocation,
+                    selectedAssets: selectedAssets,
+                  ),
+                ],
+              ),
+            )
+          // Two-pane
+          : Row(
+              children: [
+                Expanded(
+                  child: Scaffold(
+                    extendBodyBehindAppBar: true,
+                    appBar: MapAppBar(selectedAssets: selectedAssets),
+                    body: Stack(
+                      children: [
+                        _MapWithMarker(
+                          style: style,
+                          selectedMarker: selectedMarker,
+                          onMapCreated: onMapCreated,
+                          onMapMoved: onMapMoved,
+                          onMapClicked: onMarkerClicked,
+                          onStyleLoaded: reloadLayers,
+                          onMarkerTapped: onMarkerTapped,
+                        ),
+                        Positioned(
+                          right: 0,
+                          bottom: 30,
+                          child: ElevatedButton(
+                            onPressed: onZoomToLocation,
+                            style: ElevatedButton.styleFrom(
+                              shape: const CircleBorder(),
+                            ),
+                            child: const Icon(Icons.my_location),
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                ),
+                Expanded(
+                  child: LayoutBuilder(
+                    builder: (ctx, constraints) => MapAssetGrid(
+                      controller: scrollController,
+                      mapEventStream: bottomSheetStreamController.stream,
+                      onGridAssetChanged: onBottomSheetScrolled,
+                      onZoomToAsset: onZoomToAsset,
+                      onAssetsSelected: onAssetsSelected,
+                      selectedAssets: selectedAssets,
+                    ),
+                  ),
+                ),
+              ],
+            ),
+    );
+  }
+}
 
-    void onFavoriteAsset() async {
-      showLoadingIndicator.value = true;
-      try {
-        await handleFavoriteAssets(ref, context, selectedAssets.value.toList());
-      } finally {
-        showLoadingIndicator.value = false;
-        selectionEnabledHook.value = false;
-        refetchMarkers.value = true;
-      }
-    }
+class _AssetMarkerMeta {
+  final Point<num> point;
+  final MapMarker marker;
+  final bool shouldAnimate;
 
-    void onArchiveAsset() async {
-      showLoadingIndicator.value = true;
-      try {
-        await handleArchiveAssets(ref, context, selectedAssets.value.toList());
-      } finally {
-        showLoadingIndicator.value = false;
-        selectionEnabledHook.value = false;
-        refetchMarkers.value = true;
-      }
-    }
+  const _AssetMarkerMeta({
+    required this.point,
+    required this.marker,
+    required this.shouldAnimate,
+  });
 
-    void selectionListener(bool isMultiSelect, Set<Asset> selection) {
-      selectionEnabledHook.value = isMultiSelect;
-      selectedAssets.value = selection;
-    }
+  @override
+  String toString() =>
+      '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)';
+}
 
-    final markerLayer = MarkerLayer(
-      markers: [
-        if (closestAssetMarker.value != null)
-          AssetMarker(
-            remoteId: closestAssetMarker.value!.asset.remoteId!,
-            anchorPos: AnchorPos.align(AnchorAlign.top),
-            point: closestAssetMarker.value!.point,
-            width: 100,
-            height: 100,
-            builder: (ctx) => GestureDetector(
-              onTap: () => openAssetInViewer(closestAssetMarker.value!.asset),
-              child: AssetMarkerIcon(
-                key: Key(closestAssetMarker.value!.asset.remoteId!),
-                isDarkTheme: isDarkTheme,
-                id: closestAssetMarker.value!.asset.remoteId!,
+class _MapWithMarker extends StatelessWidget {
+  final AsyncValue<String> style;
+  final MapCreatedCallback onMapCreated;
+  final OnCameraIdleCallback onMapMoved;
+  final OnMapClickCallback onMapClicked;
+  final OnStyleLoadedCallback onStyleLoaded;
+  final Function()? onMarkerTapped;
+  final ValueNotifier<_AssetMarkerMeta?> selectedMarker;
+
+  const _MapWithMarker({
+    required this.style,
+    required this.onMapCreated,
+    required this.onMapMoved,
+    required this.onMapClicked,
+    required this.onStyleLoaded,
+    required this.selectedMarker,
+    this.onMarkerTapped,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (ctx, constraints) => SizedBox(
+        height: constraints.maxHeight,
+        width: constraints.maxWidth,
+        child: Stack(
+          children: [
+            style.widgetWhen(
+              onData: (style) => MaplibreMap(
+                initialCameraPosition:
+                    const CameraPosition(target: LatLng(0, 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]
+                trackCameraPosition: true,
+                onMapCreated: onMapCreated,
+                onCameraIdle: onMapMoved,
+                onMapClick: onMapClicked,
+                onStyleLoadedCallback: onStyleLoaded,
+                tiltGesturesEnabled: false,
+                dragEnabled: false,
+                myLocationEnabled: false,
+                attributionButtonPosition: AttributionButtonPosition.TopRight,
               ),
             ),
-          ),
-      ],
-    );
-
-    final heatMapLayer = mapMarkerData.value.isNotEmpty
-        ? HeatMapLayer(
-            heatMapDataSource: InMemoryHeatMapDataSource(
-              data: mapMarkerData.value
-                  .map(
-                    (e) => WeightedLatLng(
-                      LatLng(e.point.latitude, e.point.longitude),
-                      1,
-                    ),
-                  )
-                  .toList(),
+            ValueListenableBuilder(
+              valueListenable: selectedMarker,
+              builder: (ctx, value, _) => value != null
+                  ? PositionedAssetMarkerIcon(
+                      point: value.point,
+                      assetRemoteId: value.marker.assetRemoteId,
+                      durationInMilliseconds: value.shouldAnimate ? 100 : 0,
+                      onTap: onMarkerTapped,
+                    )
+                  : const SizedBox.shrink(),
             ),
-            heatMapOptions: HeatMapOptions(
-              radius: 60,
-              layerOpacity: 0.5,
-              gradient: {
-                0.20: Colors.deepPurple,
-                0.40: Colors.blue,
-                0.60: Colors.green,
-                0.95: Colors.yellow,
-                1.0: Colors.deepOrange,
-              },
-            ),
-          )
-        : const SizedBox.shrink();
-
-    return AnnotatedRegion<SystemUiOverlayStyle>(
-      value: SystemUiOverlayStyle(
-        statusBarColor:
-            (isDarkTheme ? Colors.black : Colors.white).withOpacity(0.5),
-        statusBarIconBrightness:
-            isDarkTheme ? Brightness.light : Brightness.dark,
-        systemNavigationBarColor:
-            isDarkTheme ? Colors.grey[900] : Colors.grey[100],
-        systemNavigationBarIconBrightness:
-            isDarkTheme ? Brightness.light : Brightness.dark,
-        systemNavigationBarDividerColor: Colors.transparent,
-      ),
-      child: Theme(
-        // Override app theme based on map theme
-        data: isDarkTheme ? immichDarkTheme : immichLightTheme,
-        child: Scaffold(
-          appBar: MapAppBar(
-            isDarkTheme: isDarkTheme,
-            selectionEnabled: selectionEnabledHook,
-            selectedAssetsLength: selectedAssets.value.length,
-            onShare: onShareAsset,
-            onArchive: onArchiveAsset,
-            onFavorite: onFavoriteAsset,
-          ),
-          extendBodyBehindAppBar: true,
-          body: Stack(
-            children: [
-              if (!isLoading)
-                FlutterMap(
-                  mapController: mapController,
-                  options: MapOptions(
-                    maxBounds:
-                        LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)),
-                    interactiveFlags: InteractiveFlag.doubleTapZoom |
-                        InteractiveFlag.drag |
-                        InteractiveFlag.flingAnimation |
-                        InteractiveFlag.pinchMove |
-                        InteractiveFlag.pinchZoom,
-                    center: LatLng(20, 20),
-                    zoom: 2,
-                    minZoom: 1,
-                    maxZoom: maxZoom,
-                    onMapReady: () {
-                      isMapReady = true;
-                      mapController.mapEventStream.listen(onMapEvent);
-                    },
-                  ),
-                  children: [
-                    ref.read(mapStateNotifier.notifier).getTileLayer(),
-                    heatMapLayer,
-                    markerLayer,
-                  ],
-                ),
-              if (!isLoading)
-                MapPageBottomSheet(
-                  mapPageEventStream: mapPageEventSC.stream,
-                  bottomSheetEventSC: bottomSheetEventSC,
-                  selectionEnabled: selectionEnabledHook.value,
-                  selectionlistener: selectionListener,
-                  isDarkTheme: isDarkTheme,
-                ),
-              if (showLoadingIndicator.value || isLoading)
-                Positioned(
-                  top: context.height * 0.35,
-                  left: context.width * 0.425,
-                  child: const ImmichLoadingIndicator(),
-                ),
-            ],
-          ),
+          ],
         ),
       ),
     );
   }
 }
-
-class AssetMarker extends Marker {
-  String remoteId;
-
-  AssetMarker({
-    super.key,
-    required this.remoteId,
-    super.anchorPos,
-    required super.point,
-    super.width = 100.0,
-    super.height = 100.0,
-    required super.builder,
-  });
-}
diff --git a/mobile/lib/modules/map/widgets/map_app_bar.dart b/mobile/lib/modules/map/widgets/map_app_bar.dart
new file mode 100644
index 0000000000..ea73319c4b
--- /dev/null
+++ b/mobile/lib/modules/map/widgets/map_app_bar.dart
@@ -0,0 +1,159 @@
+import 'dart:async';
+
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
+import 'package:immich_mobile/modules/map/widgets/map_settings_sheet.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
+import 'package:immich_mobile/utils/selection_handlers.dart';
+
+class MapAppBar extends HookWidget implements PreferredSizeWidget {
+  final ValueNotifier<Set<Asset>> selectedAssets;
+
+  const MapAppBar({super.key, required this.selectedAssets});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top + 25),
+      child: ValueListenableBuilder(
+        valueListenable: selectedAssets,
+        builder: (ctx, value, child) => value.isNotEmpty
+            ? _SelectionRow(selectedAssets: selectedAssets)
+            : _NonSelectionRow(),
+      ),
+    );
+  }
+
+  @override
+  Size get preferredSize => const Size.fromHeight(100);
+}
+
+class _NonSelectionRow extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    void onSettingsPressed() {
+      showModalBottomSheet(
+        elevation: 0.0,
+        showDragHandle: true,
+        isScrollControlled: true,
+        context: context,
+        builder: (_) => const MapSettingsSheet(),
+      );
+    }
+
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      children: [
+        ElevatedButton(
+          onPressed: () => context.popRoute(),
+          style: ElevatedButton.styleFrom(
+            shape: const CircleBorder(),
+          ),
+          child: const Icon(Icons.arrow_back_ios_new_rounded),
+        ),
+        ElevatedButton(
+          onPressed: onSettingsPressed,
+          style: ElevatedButton.styleFrom(
+            shape: const CircleBorder(),
+          ),
+          child: const Icon(Icons.more_vert_rounded),
+        ),
+      ],
+    );
+  }
+}
+
+class _SelectionRow extends HookConsumerWidget {
+  final ValueNotifier<Set<Asset>> selectedAssets;
+
+  const _SelectionRow({required this.selectedAssets});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final isProcessing = useProcessingOverlay();
+
+    Future<void> handleProcessing(
+      FutureOr<void> Function() action, [
+      bool reloadMarkers = false,
+    ]) async {
+      isProcessing.value = true;
+      await action();
+      // Reset state
+      selectedAssets.value = {};
+      isProcessing.value = false;
+      if (reloadMarkers) {
+        ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(true);
+      }
+    }
+
+    return Row(
+      children: [
+        Padding(
+          padding: const EdgeInsets.only(left: 20),
+          child: ElevatedButton.icon(
+            onPressed: () => selectedAssets.value = {},
+            icon: const Icon(Icons.close_rounded),
+            label: Text(
+              '${selectedAssets.value.length}',
+              style: context.textTheme.titleMedium?.copyWith(
+                color: context.colorScheme.onPrimary,
+              ),
+            ),
+          ),
+        ),
+        Expanded(
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.end,
+            children: [
+              ElevatedButton(
+                onPressed: () => handleProcessing(
+                  () => handleShareAssets(
+                    ref,
+                    context,
+                    selectedAssets.value.toList(),
+                  ),
+                ),
+                style: ElevatedButton.styleFrom(
+                  shape: const CircleBorder(),
+                ),
+                child: const Icon(Icons.ios_share_rounded),
+              ),
+              ElevatedButton(
+                onPressed: () => handleProcessing(
+                  () => handleFavoriteAssets(
+                    ref,
+                    context,
+                    selectedAssets.value.toList(),
+                  ),
+                ),
+                style: ElevatedButton.styleFrom(
+                  shape: const CircleBorder(),
+                ),
+                child: const Icon(Icons.favorite),
+              ),
+              ElevatedButton(
+                onPressed: () => handleProcessing(
+                  () => handleArchiveAssets(
+                    ref,
+                    context,
+                    selectedAssets.value.toList(),
+                  ),
+                  true,
+                ),
+                style: ElevatedButton.styleFrom(
+                  shape: const CircleBorder(),
+                ),
+                child: const Icon(Icons.archive),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/widgets/map_asset_grid.dart b/mobile/lib/modules/map/widgets/map_asset_grid.dart
new file mode 100644
index 0000000000..411039f981
--- /dev/null
+++ b/mobile/lib/modules/map/widgets/map_asset_grid.dart
@@ -0,0 +1,273 @@
+import 'dart:math' as math;
+import 'package:collection/collection.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/extensions/collection_extensions.dart';
+import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
+import 'package:immich_mobile/modules/map/models/map_event.model.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/shared/providers/db.provider.dart';
+import 'package:immich_mobile/shared/ui/drag_sheet.dart';
+import 'package:immich_mobile/utils/color_filter_generator.dart';
+import 'package:immich_mobile/utils/throttle.dart';
+import 'package:logging/logging.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
+
+class MapAssetGrid extends HookConsumerWidget {
+  final Stream<MapEvent> mapEventStream;
+  final Function(String)? onGridAssetChanged;
+  final Function(String)? onZoomToAsset;
+  final Function(bool, Set<Asset>)? onAssetsSelected;
+  final ValueNotifier<Set<Asset>> selectedAssets;
+  final ScrollController controller;
+
+  const MapAssetGrid({
+    required this.mapEventStream,
+    this.onGridAssetChanged,
+    this.onZoomToAsset,
+    this.onAssetsSelected,
+    required this.selectedAssets,
+    required this.controller,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final log = Logger("MapAssetGrid");
+    final assetsInBounds = useState<List<Asset>>([]);
+    final cachedRenderList = useRef<RenderList?>(null);
+    final lastRenderElementIndex = useRef<int?>(null);
+    final assetInSheet = useValueNotifier<String?>(null);
+    final gridScrollThrottler =
+        useThrottler(interval: const Duration(milliseconds: 300));
+
+    void handleMapEvents(MapEvent event) async {
+      if (event is MapAssetsInBoundsUpdated) {
+        assetsInBounds.value = await ref
+            .read(dbProvider)
+            .assets
+            .getAllByRemoteId(event.assetRemoteIds);
+        return;
+      }
+    }
+
+    useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
+
+    // Hard-restrict to 4 assets / row in portrait mode
+    const assetsPerRow = 4;
+
+    void handleVisibleItems(Iterable<ItemPosition> positions) {
+      final orderedPos = positions.sortedByField((p) => p.index);
+      // Index of row where the items are mostly visible
+      const partialOffset = 0.20;
+      final item = orderedPos
+          .firstWhereOrNull((p) => p.itemTrailingEdge > partialOffset);
+
+      // Guard no elements, reset state
+      // Also fail fast when the sheet is just opened and the user is yet to scroll (i.e leading = 0)
+      if (item == null || item.itemLeadingEdge == 0) {
+        lastRenderElementIndex.value = null;
+        return;
+      }
+
+      final renderElement =
+          cachedRenderList.value?.elements.elementAtOrNull(item.index);
+      // Guard no render list or render element
+      if (renderElement == null) {
+        return;
+      }
+      // Reset index
+      lastRenderElementIndex.value == item.index;
+
+      //  <RenderElement:offset:0>
+      //  | 1 | 2 | 3 | 4 | 5 | 6 |
+      //  <RenderElement:offset:6>
+      //  | 7 | 8 | 9 |
+      //  <RenderElement:offset:9>
+      //  | 10 |
+
+      // Skip through the assets from the previous row
+      final rowOffset = renderElement.offset;
+      // Column offset = (total trailingEdge - trailingEdge crossed) / offset for each asset
+      final totalOffset = item.itemTrailingEdge - item.itemLeadingEdge;
+      final edgeOffset = (totalOffset - partialOffset) /
+          // Round the total count to the next multiple of [assetsPerRow]
+          ((renderElement.totalCount / assetsPerRow) * assetsPerRow).floor();
+
+      // trailing should never be above the totalOffset
+      final columnOffset =
+          (totalOffset - math.min(item.itemTrailingEdge, totalOffset)) ~/
+              edgeOffset;
+      final assetOffset = rowOffset + columnOffset;
+      final selectedAsset = cachedRenderList.value?.allAssets
+          ?.elementAtOrNull(assetOffset)
+          ?.remoteId;
+
+      if (selectedAsset != null) {
+        onGridAssetChanged?.call(selectedAsset);
+        assetInSheet.value = selectedAsset;
+      }
+    }
+
+    return Card(
+      margin: EdgeInsets.zero,
+      child: Stack(
+        children: [
+          /// The Align and FractionallySizedBox are to prevent the Asset Grid from going behind the
+          /// _MapSheetDragRegion and thereby displaying content behind the top right and top left curves
+          Align(
+            alignment: Alignment.bottomCenter,
+            child: FractionallySizedBox(
+              // Place it just below the drag handle
+              heightFactor: 0.80,
+              child: assetsInBounds.value.isNotEmpty
+                  ? ref.watch(renderListProvider(assetsInBounds.value)).when(
+                        data: (renderList) {
+                          // Cache render list here to use it back during visibleItemsListener
+                          cachedRenderList.value = renderList;
+                          return ValueListenableBuilder(
+                            valueListenable: selectedAssets,
+                            builder: (_, value, __) => ImmichAssetGrid(
+                              shrinkWrap: true,
+                              renderList: renderList,
+                              showDragScroll: false,
+                              assetsPerRow: assetsPerRow,
+                              showMultiSelectIndicator: false,
+                              selectionActive: value.isNotEmpty,
+                              listener: onAssetsSelected,
+                              visibleItemsListener: (pos) => gridScrollThrottler
+                                  .run(() => handleVisibleItems(pos)),
+                            ),
+                          );
+                        },
+                        error: (error, stackTrace) {
+                          log.warning(
+                            "Cannot get assets in the current map bounds $error",
+                            error,
+                            stackTrace,
+                          );
+                          return const SizedBox.shrink();
+                        },
+                        loading: () => const SizedBox.shrink(),
+                      )
+                  : _MapNoAssetsInSheet(),
+            ),
+          ),
+          _MapSheetDragRegion(
+            controller: controller,
+            assetsInBoundCount: assetsInBounds.value.length,
+            assetInSheet: assetInSheet,
+            onZoomToAsset: onZoomToAsset,
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _MapNoAssetsInSheet extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    const image = Image(
+      height: 150,
+      width: 150,
+      image: AssetImage('assets/lighthouse.png'),
+    );
+
+    return Center(
+      child: ListView(
+        shrinkWrap: true,
+        children: [
+          context.isDarkTheme
+              ? const InvertionFilter(
+                  child: SaturationFilter(
+                    saturation: -1,
+                    child: BrightnessFilter(
+                      brightness: -5,
+                      child: image,
+                    ),
+                  ),
+                )
+              : image,
+          const SizedBox(height: 20),
+          Center(
+            child: Text(
+              "map_zoom_to_see_photos".tr(),
+              style: context.textTheme.displayLarge?.copyWith(fontSize: 18),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _MapSheetDragRegion extends StatelessWidget {
+  final ScrollController controller;
+  final int assetsInBoundCount;
+  final ValueNotifier<String?> assetInSheet;
+  final Function(String)? onZoomToAsset;
+
+  const _MapSheetDragRegion({
+    required this.controller,
+    required this.assetsInBoundCount,
+    required this.assetInSheet,
+    this.onZoomToAsset,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final assetsInBoundsText = assetsInBoundCount > 0
+        ? "map_assets_in_bounds".tr(args: [assetsInBoundCount.toString()])
+        : "map_no_assets_in_bounds".tr();
+
+    return SingleChildScrollView(
+      controller: controller,
+      physics: const ClampingScrollPhysics(),
+      child: Card(
+        margin: EdgeInsets.zero,
+        shape: context.isMobile ? null : const BeveledRectangleBorder(),
+        elevation: 0.0,
+        child: Stack(
+          children: [
+            Column(
+              crossAxisAlignment: CrossAxisAlignment.center,
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                const SizedBox(height: 15),
+                const CustomDraggingHandle(),
+                const SizedBox(height: 15),
+                Text(assetsInBoundsText, style: context.textTheme.bodyLarge),
+                const Divider(height: 35),
+              ],
+            ),
+            ValueListenableBuilder(
+              valueListenable: assetInSheet,
+              builder: (_, value, __) => Visibility(
+                visible: value != null,
+                child: Positioned(
+                  right: 15,
+                  top: 15,
+                  child: IconButton(
+                    icon: Icon(
+                      Icons.map_outlined,
+                      color: context.textTheme.displayLarge?.color,
+                    ),
+                    iconSize: 20,
+                    tooltip: 'Zoom to bounds',
+                    onPressed: () => onZoomToAsset?.call(value!),
+                  ),
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/widgets/map_bottom_sheet.dart b/mobile/lib/modules/map/widgets/map_bottom_sheet.dart
new file mode 100644
index 0000000000..7bef846c96
--- /dev/null
+++ b/mobile/lib/modules/map/widgets/map_bottom_sheet.dart
@@ -0,0 +1,97 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/modules/map/models/map_event.model.dart';
+import 'package:immich_mobile/modules/map/widgets/map_asset_grid.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
+
+class MapBottomSheet extends HookConsumerWidget {
+  final Stream<MapEvent> mapEventStream;
+  final Function(String)? onGridAssetChanged;
+  final Function(String)? onZoomToAsset;
+  final Function()? onZoomToLocation;
+  final Function(bool, Set<Asset>)? onAssetsSelected;
+  final ValueNotifier<Set<Asset>> selectedAssets;
+
+  const MapBottomSheet({
+    required this.mapEventStream,
+    this.onGridAssetChanged,
+    this.onZoomToAsset,
+    this.onAssetsSelected,
+    this.onZoomToLocation,
+    required this.selectedAssets,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    const sheetMinExtent = 0.1;
+    final sheetController = useDraggableScrollController();
+    final bottomSheetOffset = useValueNotifier(sheetMinExtent);
+    final isBottomSheetOpened = useRef(false);
+
+    void handleMapEvents(MapEvent event) async {
+      if (event is MapCloseBottomSheet) {
+        sheetController.animateTo(
+          0.1,
+          duration: const Duration(milliseconds: 200),
+          curve: Curves.linearToEaseOut,
+        );
+      }
+    }
+
+    useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
+
+    bool onScrollNotification(DraggableScrollableNotification notification) {
+      isBottomSheetOpened.value =
+          notification.extent > (notification.maxExtent * 0.9);
+      bottomSheetOffset.value = notification.extent;
+      // do not bubble
+      return true;
+    }
+
+    return Stack(
+      children: [
+        NotificationListener<DraggableScrollableNotification>(
+          onNotification: onScrollNotification,
+          child: DraggableScrollableSheet(
+            controller: sheetController,
+            minChildSize: sheetMinExtent,
+            maxChildSize: 0.5,
+            initialChildSize: sheetMinExtent,
+            snap: true,
+            shouldCloseOnMinExtent: false,
+            builder: (ctx, scrollController) => MapAssetGrid(
+              controller: scrollController,
+              mapEventStream: mapEventStream,
+              selectedAssets: selectedAssets,
+              onAssetsSelected: onAssetsSelected,
+              // Do not bother with the event if the bottom sheet is not user scrolled
+              onGridAssetChanged: (assetId) => isBottomSheetOpened.value
+                  ? onGridAssetChanged?.call(assetId)
+                  : null,
+              onZoomToAsset: onZoomToAsset,
+            ),
+          ),
+        ),
+        ValueListenableBuilder(
+          valueListenable: bottomSheetOffset,
+          builder: (ctx, value, child) => Positioned(
+            right: 0,
+            bottom: context.height * (value + 0.02),
+            child: child!,
+          ),
+          child: ElevatedButton(
+            onPressed: onZoomToLocation,
+            style: ElevatedButton.styleFrom(
+              shape: const CircleBorder(),
+            ),
+            child: const Icon(Icons.my_location),
+          ),
+        ),
+      ],
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/widgets/map_settings/map_settings_list_tile.dart b/mobile/lib/modules/map/widgets/map_settings/map_settings_list_tile.dart
new file mode 100644
index 0000000000..1abe64ce31
--- /dev/null
+++ b/mobile/lib/modules/map/widgets/map_settings/map_settings_list_tile.dart
@@ -0,0 +1,31 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+
+class MapSettingsListTile extends StatelessWidget {
+  final String title;
+  final bool selected;
+  final Function(bool) onChanged;
+
+  const MapSettingsListTile({
+    super.key,
+    required this.title,
+    required this.selected,
+    required this.onChanged,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return SwitchListTile.adaptive(
+      activeColor: context.primaryColor,
+      title: Text(
+        title,
+        style:
+            context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold),
+      ).tr(),
+      value: selected,
+      onChanged: onChanged,
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/widgets/map_settings/map_settings_time_dropdown.dart b/mobile/lib/modules/map/widgets/map_settings/map_settings_time_dropdown.dart
new file mode 100644
index 0000000000..bf391428d9
--- /dev/null
+++ b/mobile/lib/modules/map/widgets/map_settings/map_settings_time_dropdown.dart
@@ -0,0 +1,92 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+
+class MapTimeDropDown extends StatelessWidget {
+  final int relativeTime;
+  final Function(int) onTimeChange;
+
+  const MapTimeDropDown({
+    super.key,
+    required this.relativeTime,
+    required this.onTimeChange,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final now = DateTime.now();
+
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: [
+        Padding(
+          padding: const EdgeInsets.only(bottom: 20),
+          child: Text(
+            "map_settings_only_relative_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: "map_settings_date_range_option_all".tr(),
+              ),
+              DropdownMenuEntry(
+                value: 1,
+                label: "map_settings_date_range_option_day".tr(),
+              ),
+              DropdownMenuEntry(
+                value: 7,
+                label: "map_settings_date_range_option_days".tr(
+                  args: ["7"],
+                ),
+              ),
+              DropdownMenuEntry(
+                value: 30,
+                label: "map_settings_date_range_option_days".tr(
+                  args: ["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(args: ["3"]),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/widgets/map_settings/map_theme_picker.dart b/mobile/lib/modules/map/widgets/map_settings/map_theme_picker.dart
new file mode 100644
index 0000000000..fed119c97e
--- /dev/null
+++ b/mobile/lib/modules/map/widgets/map_settings/map_theme_picker.dart
@@ -0,0 +1,109 @@
+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/modules/map/widgets/map_thumbnail.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+
+class MapThemePicker extends StatelessWidget {
+  final ThemeMode themeMode;
+  final Function(ThemeMode) onThemeChange;
+
+  const MapThemePicker({
+    super.key,
+    required this.themeMode,
+    required this.onThemeChange,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        Padding(
+          padding: const EdgeInsets.only(bottom: 20),
+          child: Center(
+            child: Text(
+              "map_settings_theme_settings",
+              style: context.textTheme.bodyMedium
+                  ?.copyWith(fontWeight: FontWeight.bold),
+            ).tr(),
+          ),
+        ),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            _BorderedMapThumbnail(
+              name: "Light",
+              mode: ThemeMode.light,
+              shouldHighlight: themeMode == ThemeMode.light,
+              onThemeChange: onThemeChange,
+            ),
+            _BorderedMapThumbnail(
+              name: "Dark",
+              mode: ThemeMode.dark,
+              shouldHighlight: themeMode == ThemeMode.dark,
+              onThemeChange: onThemeChange,
+            ),
+            _BorderedMapThumbnail(
+              name: "System",
+              mode: ThemeMode.system,
+              shouldHighlight: themeMode == ThemeMode.system,
+              onThemeChange: onThemeChange,
+            ),
+          ],
+        ),
+      ],
+    );
+  }
+}
+
+class _BorderedMapThumbnail extends StatelessWidget {
+  final ThemeMode mode;
+  final String name;
+  final bool shouldHighlight;
+  final Function(ThemeMode) onThemeChange;
+
+  const _BorderedMapThumbnail({
+    required this.mode,
+    required this.name,
+    required this.shouldHighlight,
+    required this.onThemeChange,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        Container(
+          decoration: BoxDecoration(
+            border: Border.fromBorderSide(
+              BorderSide(
+                width: 4,
+                color: shouldHighlight
+                    ? context.colorScheme.onSurface
+                    : Colors.transparent,
+              ),
+            ),
+            borderRadius: const BorderRadius.all(Radius.circular(20)),
+          ),
+          child: MapThumbnail(
+            zoom: 2,
+            centre: const LatLng(47, 5),
+            onTap: (_, __) => onThemeChange(mode),
+            themeMode: mode,
+            showAttribution: false,
+          ),
+        ),
+        Padding(
+          padding: const EdgeInsets.only(top: 10),
+          child: Text(
+            name,
+            style: context.textTheme.bodyMedium?.copyWith(
+              fontWeight: shouldHighlight ? FontWeight.bold : null,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/widgets/map_settings_sheet.dart b/mobile/lib/modules/map/widgets/map_settings_sheet.dart
new file mode 100644
index 0000000000..4fe53fd0e4
--- /dev/null
+++ b/mobile/lib/modules/map/widgets/map_settings_sheet.dart
@@ -0,0 +1,61 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
+import 'package:immich_mobile/modules/map/widgets/map_settings/map_settings_list_tile.dart';
+import 'package:immich_mobile/modules/map/widgets/map_settings/map_settings_time_dropdown.dart';
+import 'package:immich_mobile/modules/map/widgets/map_settings/map_theme_picker.dart';
+
+class MapSettingsSheet extends HookConsumerWidget {
+  const MapSettingsSheet({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final mapState = ref.watch(mapStateNotifierProvider);
+
+    return DraggableScrollableSheet(
+      expand: false,
+      initialChildSize: 0.6,
+      builder: (ctx, scrollController) => SingleChildScrollView(
+        controller: scrollController,
+        child: Card(
+          elevation: 0.0,
+          shadowColor: Colors.transparent,
+          margin: EdgeInsets.zero,
+          child: Column(
+            mainAxisSize: MainAxisSize.max,
+            children: [
+              MapThemePicker(
+                themeMode: mapState.themeMode,
+                onThemeChange: (mode) => ref
+                    .read(mapStateNotifierProvider.notifier)
+                    .switchTheme(mode),
+              ),
+              const Divider(height: 30, thickness: 2),
+              MapSettingsListTile(
+                title: "map_settings_only_show_favorites",
+                selected: mapState.showFavoriteOnly,
+                onChanged: (favoriteOnly) => ref
+                    .read(mapStateNotifierProvider.notifier)
+                    .switchFavoriteOnly(favoriteOnly),
+              ),
+              MapSettingsListTile(
+                title: "map_settings_include_show_archived",
+                selected: mapState.includeArchived,
+                onChanged: (includeArchive) => ref
+                    .read(mapStateNotifierProvider.notifier)
+                    .switchIncludeArchived(includeArchive),
+              ),
+              MapTimeDropDown(
+                relativeTime: mapState.relativeTime,
+                onTimeChange: (time) => ref
+                    .read(mapStateNotifierProvider.notifier)
+                    .setRelativeTime(time),
+              ),
+              const SizedBox(height: 20),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/widgets/map_theme_override.dart b/mobile/lib/modules/map/widgets/map_theme_override.dart
new file mode 100644
index 0000000000..bd6429a5a2
--- /dev/null
+++ b/mobile/lib/modules/map/widgets/map_theme_override.dart
@@ -0,0 +1,96 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
+import 'package:immich_mobile/utils/immich_app_theme.dart';
+
+/// Overrides the theme below the widget tree to use the theme data based on the
+/// map settings instead of the one from the app settings
+class MapThemeOveride extends StatefulHookConsumerWidget {
+  final ThemeMode? themeMode;
+  final Widget Function(AsyncValue<String> style) mapBuilder;
+
+  const MapThemeOveride({required this.mapBuilder, this.themeMode, super.key});
+
+  @override
+  ConsumerState createState() => _MapThemeOverideState();
+}
+
+class _MapThemeOverideState extends ConsumerState<MapThemeOveride>
+    with WidgetsBindingObserver {
+  late ThemeMode _theme;
+  bool _isDarkTheme = false;
+
+  bool get _isSystemDark =>
+      WidgetsBinding.instance.platformDispatcher.platformBrightness ==
+      Brightness.dark;
+
+  bool checkDarkTheme() {
+    return _theme == ThemeMode.dark ||
+        _theme == ThemeMode.system && _isSystemDark;
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _theme = widget.themeMode ??
+        ref.read(mapStateNotifierProvider.select((v) => v.themeMode));
+    setState(() {
+      _isDarkTheme = checkDarkTheme();
+    });
+    if (_theme == ThemeMode.system) {
+      WidgetsBinding.instance.addObserver(this);
+    }
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    if (_theme != ThemeMode.system) {
+      WidgetsBinding.instance.removeObserver(this);
+    }
+  }
+
+  @override
+  void dispose() {
+    WidgetsBinding.instance.removeObserver(this);
+    super.dispose();
+  }
+
+  @override
+  void didChangePlatformBrightness() {
+    super.didChangePlatformBrightness();
+
+    if (_theme == ThemeMode.system) {
+      setState(() => _isDarkTheme = _isSystemDark);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    _theme = widget.themeMode ??
+        ref.watch(mapStateNotifierProvider.select((v) => v.themeMode));
+
+    useValueChanged<ThemeMode, void>(_theme, (_, __) {
+      if (_theme == ThemeMode.system) {
+        WidgetsBinding.instance.addObserver(this);
+      } else {
+        WidgetsBinding.instance.removeObserver(this);
+      }
+      setState(() {
+        _isDarkTheme = checkDarkTheme();
+      });
+    });
+
+    return Theme(
+      data: _isDarkTheme ? immichDarkTheme : immichLightTheme,
+      child: widget.mapBuilder.call(
+        ref.watch(
+          mapStateNotifierProvider.select(
+            (v) => _isDarkTheme ? v.darkStyleFetched : v.lightStyleFetched,
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/widgets/map_thumbnail.dart b/mobile/lib/modules/map/widgets/map_thumbnail.dart
new file mode 100644
index 0000000000..b162d2896c
--- /dev/null
+++ b/mobile/lib/modules/map/widgets/map_thumbnail.dart
@@ -0,0 +1,110 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
+import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
+import 'package:immich_mobile/modules/map/widgets/map_theme_override.dart';
+import 'package:immich_mobile/modules/map/widgets/positioned_asset_marker_icon.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+
+/// A non-interactive thumbnail of a map in the given coordinates with optional markers
+///
+/// User can provide either a [assetMarkerRemoteId] to display the asset's thumbnail or set
+/// [showMarkerPin] to true which would display a marker pin instead. If both are provided,
+/// [assetMarkerRemoteId] will take precedence
+class MapThumbnail extends HookConsumerWidget {
+  final Function(Point<double>, LatLng)? onTap;
+  final LatLng centre;
+  final String? assetMarkerRemoteId;
+  final bool showMarkerPin;
+  final double zoom;
+  final double height;
+  final double width;
+  final ThemeMode? themeMode;
+  final bool showAttribution;
+
+  const MapThumbnail({
+    super.key,
+    required this.centre,
+    this.height = 100,
+    this.width = 100,
+    this.onTap,
+    this.zoom = 8,
+    this.assetMarkerRemoteId,
+    this.showMarkerPin = false,
+    this.themeMode,
+    this.showAttribution = true,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final offsettedCentre = LatLng(centre.latitude + 0.002, centre.longitude);
+    final controller = useRef<MaplibreMapController?>(null);
+    final position = useValueNotifier<Point<num>?>(null);
+
+    Future<void> onMapCreated(MaplibreMapController mapController) async {
+      controller.value = mapController;
+      if (assetMarkerRemoteId != null) {
+        // The iOS impl returns wrong toScreenLocation without the delay
+        Future.delayed(
+          const Duration(milliseconds: 100),
+          () async =>
+              position.value = await mapController.toScreenLocation(centre),
+        );
+      }
+    }
+
+    Future<void> onStyleLoaded() async {
+      if (showMarkerPin && controller.value != null) {
+        await controller.value?.addMarkerAtLatLng(centre);
+      }
+    }
+
+    return MapThemeOveride(
+      themeMode: themeMode,
+      mapBuilder: (style) => SizedBox(
+        height: height,
+        width: width,
+        child: ClipRRect(
+          borderRadius: const BorderRadius.all(Radius.circular(15)),
+          child: Stack(
+            alignment: Alignment.center,
+            children: [
+              style.widgetWhen(
+                onData: (style) => MaplibreMap(
+                  initialCameraPosition:
+                      CameraPosition(target: offsettedCentre, zoom: zoom),
+                  styleString: style,
+                  onMapCreated: onMapCreated,
+                  onStyleLoadedCallback: onStyleLoaded,
+                  onMapClick: onTap,
+                  doubleClickZoomEnabled: false,
+                  dragEnabled: false,
+                  zoomGesturesEnabled: false,
+                  tiltGesturesEnabled: false,
+                  scrollGesturesEnabled: false,
+                  rotateGesturesEnabled: false,
+                  myLocationEnabled: false,
+                  attributionButtonMargins:
+                      showAttribution == false ? const Point(-100, 0) : null,
+                ),
+              ),
+              ValueListenableBuilder(
+                valueListenable: position,
+                builder: (_, value, __) => value != null
+                    ? PositionedAssetMarkerIcon(
+                        size: height / 2,
+                        point: value,
+                        assetRemoteId: assetMarkerRemoteId!,
+                      )
+                    : const SizedBox.shrink(),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/modules/map/ui/asset_marker_icon.dart b/mobile/lib/modules/map/widgets/positioned_asset_marker_icon.dart
similarity index 72%
rename from mobile/lib/modules/map/ui/asset_marker_icon.dart
rename to mobile/lib/modules/map/widgets/positioned_asset_marker_icon.dart
index 969c78e70f..e880bcd44d 100644
--- a/mobile/lib/modules/map/ui/asset_marker_icon.dart
+++ b/mobile/lib/modules/map/widgets/positioned_asset_marker_icon.dart
@@ -1,17 +1,57 @@
+import 'dart:io';
+import 'dart:math';
+
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
 
-class AssetMarkerIcon extends StatelessWidget {
-  const AssetMarkerIcon({
+class PositionedAssetMarkerIcon extends StatelessWidget {
+  final Point<num> point;
+  final String assetRemoteId;
+  final double size;
+  final int durationInMilliseconds;
+
+  final Function()? onTap;
+
+  const PositionedAssetMarkerIcon({
+    required this.point,
+    required this.assetRemoteId,
+    this.size = 100,
+    this.durationInMilliseconds = 100,
+    this.onTap,
     super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final ratio = Platform.isIOS ? 1.0 : MediaQuery.devicePixelRatioOf(context);
+    return AnimatedPositioned(
+      left: point.x / ratio - size / 2,
+      top: point.y / ratio - size,
+      duration: Duration(milliseconds: durationInMilliseconds),
+      child: GestureDetector(
+        onTap: () => onTap?.call(),
+        child: SizedBox.square(
+          dimension: size,
+          child: _AssetMarkerIcon(
+            id: assetRemoteId,
+            key: Key(assetRemoteId),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class _AssetMarkerIcon extends StatelessWidget {
+  const _AssetMarkerIcon({
     required this.id,
-    this.isDarkTheme = false,
+    super.key,
   });
 
   final String id;
-  final bool isDarkTheme;
 
   @override
   Widget build(BuildContext context) {
@@ -26,8 +66,8 @@ class AssetMarkerIcon extends StatelessWidget {
               left: constraints.maxWidth * 0.5,
               child: CustomPaint(
                 painter: _PinPainter(
-                  primaryColor: isDarkTheme ? Colors.white : Colors.black,
-                  secondaryColor: isDarkTheme ? Colors.black : Colors.white,
+                  primaryColor: context.colorScheme.onSurface,
+                  secondaryColor: context.colorScheme.surface,
                   primaryRadius: constraints.maxHeight * 0.06,
                   secondaryRadius: constraints.maxHeight * 0.038,
                 ),
@@ -42,7 +82,7 @@ class AssetMarkerIcon extends StatelessWidget {
               left: constraints.maxWidth * 0.17,
               child: CircleAvatar(
                 radius: constraints.maxHeight * 0.40,
-                backgroundColor: isDarkTheme ? Colors.white : Colors.black,
+                backgroundColor: context.colorScheme.onSurface,
                 child: CircleAvatar(
                   radius: constraints.maxHeight * 0.37,
                   backgroundImage: CachedNetworkImageProvider(
@@ -72,8 +112,8 @@ class _PinPainter extends CustomPainter {
   final double secondaryRadius;
 
   _PinPainter({
-    this.primaryColor = Colors.black,
-    this.secondaryColor = Colors.white,
+    required this.primaryColor,
+    required this.secondaryColor,
     required this.primaryRadius,
     required this.secondaryRadius,
   });
diff --git a/mobile/lib/modules/search/services/person.service.g.dart b/mobile/lib/modules/search/services/person.service.g.dart
index b80b439d1d..01a5ed8f30 100644
Binary files a/mobile/lib/modules/search/services/person.service.g.dart and b/mobile/lib/modules/search/services/person.service.g.dart differ
diff --git a/mobile/lib/modules/search/ui/curated_places_row.dart b/mobile/lib/modules/search/ui/curated_places_row.dart
index 5840819f95..9078e4192a 100644
--- a/mobile/lib/modules/search/ui/curated_places_row.dart
+++ b/mobile/lib/modules/search/ui/curated_places_row.dart
@@ -1,13 +1,12 @@
 import 'package:auto_route/auto_route.dart';
 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/modules/map/ui/map_thumbnail.dart';
+import 'package:immich_mobile/modules/map/widgets/map_thumbnail.dart';
 import 'package:immich_mobile/modules/search/ui/curated_row.dart';
 import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/store.dart';
-import 'package:latlong2/latlong.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
 
 class CuratedPlacesRow extends CuratedRow {
   final bool isMapEnabled;
@@ -38,14 +37,13 @@ class CuratedPlacesRow extends CuratedRow {
                 padding: const EdgeInsets.only(right: 10.0),
                 child: MapThumbnail(
                   zoom: 2,
-                  coords: LatLng(
+                  centre: const LatLng(
                     47,
                     5,
                   ),
                   height: imageSize,
                   width: imageSize,
                   showAttribution: false,
-                  isDarkTheme: context.isDarkTheme,
                 ),
               ),
               Padding(
diff --git a/mobile/lib/modules/settings/services/app_settings.service.dart b/mobile/lib/modules/settings/services/app_settings.service.dart
index 7e43b2103d..5432215cc6 100644
--- a/mobile/lib/modules/settings/services/app_settings.service.dart
+++ b/mobile/lib/modules/settings/services/app_settings.service.dart
@@ -46,7 +46,7 @@ enum AppSettingsEnum<T> {
   advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
   logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
   preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
-  mapThemeMode<bool>(StoreKey.mapThemeMode, null, false),
+  mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
   mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
   mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
   mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart
index 4ac13ce94d..038525e213 100644
--- a/mobile/lib/routing/router.dart
+++ b/mobile/lib/routing/router.dart
@@ -9,7 +9,7 @@ import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
 import 'package:immich_mobile/modules/album/views/create_album_page.dart';
 import 'package:immich_mobile/modules/album/views/library_page.dart';
 import 'package:immich_mobile/modules/backup/views/backup_options_page.dart';
-import 'package:immich_mobile/modules/map/ui/map_location_picker.dart';
+import 'package:immich_mobile/modules/map/views/map_location_picker_page.dart';
 import 'package:immich_mobile/modules/map/views/map_page.dart';
 import 'package:immich_mobile/modules/memories/models/memory.dart';
 import 'package:immich_mobile/modules/memories/views/memory_page.dart';
@@ -59,8 +59,8 @@ import 'package:immich_mobile/shared/views/app_log_page.dart';
 import 'package:immich_mobile/shared/views/splash_screen.dart';
 import 'package:immich_mobile/shared/views/tab_controller_page.dart';
 import 'package:isar/isar.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
 import 'package:photo_manager/photo_manager.dart' hide LatLng;
-import 'package:latlong2/latlong.dart';
 
 part 'router.gr.dart';
 
diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart
index 3fa3f18a26..8e30770bb1 100644
--- a/mobile/lib/routing/router.gr.dart
+++ b/mobile/lib/routing/router.gr.dart
@@ -1593,7 +1593,7 @@ class ActivitiesRoute extends PageRouteInfo<void> {
 class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
   MapLocationPickerRoute({
     Key? key,
-    LatLng? initialLatLng,
+    LatLng initialLatLng = const LatLng(0, 0),
   }) : super(
           MapLocationPickerRoute.name,
           path: '/map-location-picker-page',
@@ -1609,12 +1609,12 @@ class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
 class MapLocationPickerRouteArgs {
   const MapLocationPickerRouteArgs({
     this.key,
-    this.initialLatLng,
+    this.initialLatLng = const LatLng(0, 0),
   });
 
   final Key? key;
 
-  final LatLng? initialLatLng;
+  final LatLng initialLatLng;
 
   @override
   String toString() {
diff --git a/mobile/lib/shared/models/store.dart b/mobile/lib/shared/models/store.dart
index b8b3ba8a5c..2faeeed123 100644
--- a/mobile/lib/shared/models/store.dart
+++ b/mobile/lib/shared/models/store.dart
@@ -1,6 +1,7 @@
 import 'package:collection/collection.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:isar/isar.dart';
+import 'package:logging/logging.dart';
 
 part 'store.g.dart';
 
@@ -8,6 +9,7 @@ part 'store.g.dart';
 /// Supports String, int and JSON-serializable Objects
 /// Can be used concurrently from multiple isolates
 class Store {
+  static final Logger _log = Logger("Store");
   static late final Isar _db;
   static final List<dynamic> _cache =
       List.filled(StoreKey.values.map((e) => e.id).max + 1, null);
@@ -72,8 +74,12 @@ class Store {
   static void _onChangeListener(List<StoreValue>? data) {
     if (data != null) {
       for (StoreValue value in data) {
-        _cache[value.id] =
-            value._extract(StoreKey.values.firstWhere((e) => e.id == value.id));
+        final key = StoreKey.values.firstWhereOrNull((e) => e.id == value.id);
+        if (key != null) {
+          _cache[value.id] = value._extract(key);
+        } else {
+          _log.warning("No key available for value id - ${value.id}");
+        }
       }
     }
   }
@@ -177,13 +183,13 @@ enum StoreKey<T> {
   logLevel<int>(115, type: int),
   preferRemoteImage<bool>(116, type: bool),
   // map related settings
-  mapThemeMode<bool>(117, type: bool),
   mapShowFavoriteOnly<bool>(118, type: bool),
   mapRelativeDate<int>(119, type: int),
   selfSignedCert<bool>(120, type: bool),
   mapIncludeArchived<bool>(121, type: bool),
   ignoreIcloudAssets<bool>(122, type: bool),
   selectedAlbumSortReverse<bool>(123, type: bool),
+  mapThemeMode<int>(124, type: int),
   ;
 
   const StoreKey(
diff --git a/mobile/lib/shared/providers/websocket.provider.dart b/mobile/lib/shared/providers/websocket.provider.dart
index ebe69b8144..c78777da5a 100644
--- a/mobile/lib/shared/providers/websocket.provider.dart
+++ b/mobile/lib/shared/providers/websocket.provider.dart
@@ -94,7 +94,8 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
 
   final _log = Logger('WebsocketNotifier');
   final Ref _ref;
-  final Debounce _debounce = Debounce(const Duration(milliseconds: 500));
+  final Debouncer _debounce =
+      Debouncer(interval: const Duration(milliseconds: 500));
 
   /// Connects websocket to server unless already connected
   void connect() {
@@ -194,7 +195,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
         PendingChange(now.millisecondsSinceEpoch.toString(), action, value),
       ],
     );
-    _debounce(handlePendingChanges);
+    _debounce.run(handlePendingChanges);
   }
 
   Future<void> _handlePendingDeletes() async {
diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart
index a7bb4f019c..2ffeb53faa 100644
--- a/mobile/lib/shared/services/asset.service.dart
+++ b/mobile/lib/shared/services/asset.service.dart
@@ -11,8 +11,8 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
 import 'package:immich_mobile/shared/services/sync.service.dart';
 import 'package:isar/isar.dart';
-import 'package:latlong2/latlong.dart';
 import 'package:logging/logging.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
 import 'package:openapi/api.dart';
 
 final assetServiceProvider = Provider(
diff --git a/mobile/lib/shared/ui/drag_sheet.dart b/mobile/lib/shared/ui/drag_sheet.dart
index 31ed8f482a..b9da9ce735 100644
--- a/mobile/lib/shared/ui/drag_sheet.dart
+++ b/mobile/lib/shared/ui/drag_sheet.dart
@@ -1,4 +1,5 @@
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 
 class CustomDraggingHandle extends StatelessWidget {
   const CustomDraggingHandle({super.key});
@@ -6,11 +7,11 @@ class CustomDraggingHandle extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return Container(
-      height: 5,
+      height: 4,
       width: 30,
       decoration: BoxDecoration(
-        color: Colors.grey[500],
-        borderRadius: BorderRadius.circular(16),
+        color: context.themeData.dividerColor,
+        borderRadius: const BorderRadius.all(Radius.circular(20)),
       ),
     );
   }
diff --git a/mobile/lib/shared/ui/location_picker.dart b/mobile/lib/shared/ui/location_picker.dart
index 9ce5d96a38..ed68c05b24 100644
--- a/mobile/lib/shared/ui/location_picker.dart
+++ b/mobile/lib/shared/ui/location_picker.dart
@@ -3,12 +3,11 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:flutter_map/plugin_api.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/string_extensions.dart';
-import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
+import 'package:immich_mobile/modules/map/widgets/map_thumbnail.dart';
 import 'package:immich_mobile/routing/router.dart';
-import 'package:latlong2/latlong.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
 
 Future<LatLng?> showLocationPicker({
   required BuildContext context,
@@ -25,16 +24,6 @@ Future<LatLng?> showLocationPicker({
 
 enum _LocationPickerMode { map, manual }
 
-bool _validateLat(String value) {
-  final l = double.tryParse(value);
-  return l != null && l > -90 && l < 90;
-}
-
-bool _validateLong(String value) {
-  final l = double.tryParse(value);
-  return l != null && l > -180 && l < 180;
-}
-
 class _LocationPicker extends HookWidget {
   final LatLng? initialLatLng;
 
@@ -48,187 +37,35 @@ class _LocationPicker extends HookWidget {
     final longitude = useState(initialLatLng?.longitude ?? 0.0);
     final latlng = LatLng(latitude.value, longitude.value);
     final pickerMode = useState(_LocationPickerMode.map);
-    final latitudeController = useTextEditingController();
-    final isValidLatitude = useState(true);
-    final latitiudeFocusNode = useFocusNode();
-    final longitudeController = useTextEditingController();
-    final longitudeFocusNode = useFocusNode();
-    final isValidLongitude = useState(true);
 
-    void validateInputs() {
-      isValidLatitude.value = _validateLat(latitudeController.text);
-      if (isValidLatitude.value) {
-        latitude.value = latitudeController.text.toDouble();
+    Future<void> onMapTap() async {
+      final newLatLng = await context.pushRoute<LatLng?>(
+        MapLocationPickerRoute(initialLatLng: latlng),
+      );
+      if (newLatLng != null) {
+        latitude.value = newLatLng.latitude;
+        longitude.value = newLatLng.longitude;
       }
-      isValidLongitude.value = _validateLong(longitudeController.text);
-      if (isValidLongitude.value) {
-        longitude.value = longitudeController.text.toDouble();
-      }
-    }
-
-    void validateAndPop() {
-      if (pickerMode.value == _LocationPickerMode.manual) {
-        validateInputs();
-      }
-      if (isValidLatitude.value && isValidLongitude.value) {
-        return context.pop(latlng);
-      }
-    }
-
-    List<Widget> buildMapPickerMode() {
-      return [
-        TextButton.icon(
-          icon: Text(
-            "${latitude.value.toStringAsFixed(4)}, ${longitude.value.toStringAsFixed(4)}",
-          ),
-          label: const Icon(Icons.edit_outlined, size: 16),
-          onPressed: () {
-            latitudeController.text = latitude.value.toStringAsFixed(4);
-            longitudeController.text = longitude.value.toStringAsFixed(4);
-            pickerMode.value = _LocationPickerMode.manual;
-          },
-        ),
-        const SizedBox(
-          height: 12,
-        ),
-        MapThumbnail(
-          coords: latlng,
-          height: 200,
-          width: 200,
-          zoom: 6,
-          showAttribution: false,
-          onTap: (p0, p1) async {
-            final newLatLng = await context.pushRoute<LatLng?>(
-              MapLocationPickerRoute(initialLatLng: latlng),
-            );
-            if (newLatLng != null) {
-              latitude.value = newLatLng.latitude;
-              longitude.value = newLatLng.longitude;
-            }
-          },
-          markers: [
-            Marker(
-              anchorPos: AnchorPos.align(AnchorAlign.top),
-              point: LatLng(
-                latitude.value,
-                longitude.value,
-              ),
-              builder: (ctx) => const Image(
-                image: AssetImage('assets/location-pin.png'),
-              ),
-            ),
-          ],
-        ),
-      ];
-    }
-
-    List<Widget> buildManualPickerMode() {
-      return [
-        TextButton.icon(
-          icon: const Text("location_picker_choose_on_map").tr(),
-          label: const Icon(Icons.map_outlined, size: 16),
-          onPressed: () {
-            validateInputs();
-            if (isValidLatitude.value && isValidLongitude.value) {
-              pickerMode.value = _LocationPickerMode.map;
-            }
-          },
-        ),
-        const SizedBox(
-          height: 12,
-        ),
-        TextField(
-          controller: latitudeController,
-          focusNode: latitiudeFocusNode,
-          textInputAction: TextInputAction.done,
-          autofocus: false,
-          decoration: InputDecoration(
-            labelText: 'location_picker_latitude'.tr(),
-            labelStyle: TextStyle(
-              fontWeight: FontWeight.bold,
-              color: context.primaryColor,
-            ),
-            floatingLabelBehavior: FloatingLabelBehavior.auto,
-            border: const OutlineInputBorder(),
-            hintText: 'location_picker_latitude_hint'.tr(),
-            hintStyle: const TextStyle(
-              fontWeight: FontWeight.normal,
-              fontSize: 14,
-            ),
-            errorText: isValidLatitude.value
-                ? null
-                : "location_picker_latitude_error".tr(),
-          ),
-          onEditingComplete: () {
-            isValidLatitude.value = _validateLat(latitudeController.text);
-            if (isValidLatitude.value) {
-              latitude.value = latitudeController.text.toDouble();
-              longitudeFocusNode.requestFocus();
-            }
-          },
-          keyboardType: const TextInputType.numberWithOptions(decimal: true),
-          inputFormatters: [LengthLimitingTextInputFormatter(8)],
-          onTapOutside: (_) => latitiudeFocusNode.unfocus(),
-        ),
-        const SizedBox(
-          height: 24,
-        ),
-        TextField(
-          controller: longitudeController,
-          focusNode: longitudeFocusNode,
-          textInputAction: TextInputAction.done,
-          autofocus: false,
-          decoration: InputDecoration(
-            labelText: 'location_picker_longitude'.tr(),
-            labelStyle: TextStyle(
-              fontWeight: FontWeight.bold,
-              color: context.primaryColor,
-            ),
-            floatingLabelBehavior: FloatingLabelBehavior.auto,
-            border: const OutlineInputBorder(),
-            hintText: 'location_picker_longitude_hint'.tr(),
-            hintStyle: const TextStyle(
-              fontWeight: FontWeight.normal,
-              fontSize: 14,
-            ),
-            errorText: isValidLongitude.value
-                ? null
-                : "location_picker_longitude_error".tr(),
-          ),
-          onEditingComplete: () {
-            isValidLongitude.value = _validateLong(longitudeController.text);
-            if (isValidLongitude.value) {
-              longitude.value = longitudeController.text.toDouble();
-              longitudeFocusNode.unfocus();
-            }
-          },
-          keyboardType: const TextInputType.numberWithOptions(decimal: true),
-          inputFormatters: [LengthLimitingTextInputFormatter(8)],
-          onTapOutside: (_) => longitudeFocusNode.unfocus(),
-        ),
-      ];
     }
 
     return AlertDialog(
       contentPadding: const EdgeInsets.all(30),
       alignment: Alignment.center,
       content: SingleChildScrollView(
-        child: Column(
-          mainAxisSize: MainAxisSize.min,
-          children: [
-            const Text(
-              "edit_location_dialog_title",
-              textAlign: TextAlign.center,
-            ).tr(),
-            const SizedBox(
-              height: 12,
-            ),
-            if (pickerMode.value == _LocationPickerMode.manual)
-              ...buildManualPickerMode(),
-            if (pickerMode.value == _LocationPickerMode.map)
-              ...buildMapPickerMode(),
-          ],
-        ),
+        child: pickerMode.value == _LocationPickerMode.map
+            ? _MapPicker(
+                key: ValueKey(latlng),
+                latlng: latlng,
+                onModeSwitch: () =>
+                    pickerMode.value = _LocationPickerMode.manual,
+                onMapTap: onMapTap,
+              )
+            : _ManualPicker(
+                latlng: latlng,
+                onModeSwitch: () => pickerMode.value = _LocationPickerMode.map,
+                onLatUpdated: (value) => latitude.value = value,
+                onLonUpdated: (value) => longitude.value = value,
+              ),
       ),
       actions: [
         TextButton(
@@ -242,7 +79,7 @@ class _LocationPicker extends HookWidget {
           ).tr(),
         ),
         TextButton(
-          onPressed: validateAndPop,
+          onPressed: () => context.popRoute(latlng),
           child: Text(
             "action_common_update",
             style: context.textTheme.bodyMedium?.copyWith(
@@ -255,3 +92,177 @@ class _LocationPicker extends HookWidget {
     );
   }
 }
+
+class _ManualPickerInput extends HookWidget {
+  final String initialValue;
+  final String decorationText;
+  final String hintText;
+  final String errorText;
+  final FocusNode focusNode;
+  final bool Function(String value) validator;
+  final Function(double value) onUpdated;
+
+  const _ManualPickerInput({
+    required this.initialValue,
+    required this.decorationText,
+    required this.hintText,
+    required this.errorText,
+    required this.focusNode,
+    required this.validator,
+    required this.onUpdated,
+  });
+  @override
+  Widget build(BuildContext context) {
+    final isValid = useState(true);
+    final controller = useTextEditingController(text: initialValue);
+
+    void onEditingComplete() {
+      isValid.value = validator(controller.text);
+      if (isValid.value) {
+        onUpdated(controller.text.toDouble());
+      }
+    }
+
+    return TextField(
+      controller: controller,
+      focusNode: focusNode,
+      textInputAction: TextInputAction.done,
+      autofocus: false,
+      decoration: InputDecoration(
+        labelText: decorationText.tr(),
+        labelStyle: TextStyle(
+          fontWeight: FontWeight.bold,
+          color: context.primaryColor,
+        ),
+        floatingLabelBehavior: FloatingLabelBehavior.auto,
+        border: const OutlineInputBorder(),
+        hintText: hintText.tr(),
+        hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
+        errorText: isValid.value ? null : errorText.tr(),
+      ),
+      onEditingComplete: onEditingComplete,
+      keyboardType: const TextInputType.numberWithOptions(decimal: true),
+      inputFormatters: [LengthLimitingTextInputFormatter(8)],
+      onTapOutside: (_) => focusNode.unfocus(),
+    );
+  }
+}
+
+class _ManualPicker extends HookWidget {
+  final LatLng latlng;
+  final Function() onModeSwitch;
+  final Function(double) onLatUpdated;
+  final Function(double) onLonUpdated;
+
+  const _ManualPicker({
+    required this.latlng,
+    required this.onModeSwitch,
+    required this.onLatUpdated,
+    required this.onLonUpdated,
+  });
+
+  bool _validateLat(String value) {
+    final l = double.tryParse(value);
+    return l != null && l > -90 && l < 90;
+  }
+
+  bool _validateLong(String value) {
+    final l = double.tryParse(value);
+    return l != null && l > -180 && l < 180;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final latitiudeFocusNode = useFocusNode();
+    final longitudeFocusNode = useFocusNode();
+
+    void onLatitudeUpdated(double value) {
+      onLatUpdated(value);
+      longitudeFocusNode.requestFocus();
+    }
+
+    void onLongitudeEditingCompleted(double value) {
+      onLonUpdated(value);
+      longitudeFocusNode.unfocus();
+    }
+
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        const Text(
+          "edit_location_dialog_title",
+          textAlign: TextAlign.center,
+        ).tr(),
+        const SizedBox(height: 12),
+        TextButton.icon(
+          icon: const Text("location_picker_choose_on_map").tr(),
+          label: const Icon(Icons.map_outlined, size: 16),
+          onPressed: onModeSwitch,
+        ),
+        const SizedBox(height: 12),
+        _ManualPickerInput(
+          initialValue: latlng.latitude.toStringAsFixed(4),
+          decorationText: "location_picker_latitude",
+          hintText: "location_picker_latitude_hint",
+          errorText: "location_picker_latitude_error",
+          focusNode: latitiudeFocusNode,
+          validator: _validateLat,
+          onUpdated: onLatitudeUpdated,
+        ),
+        const SizedBox(height: 24),
+        _ManualPickerInput(
+          initialValue: latlng.longitude.toStringAsFixed(4),
+          decorationText: "location_picker_longitude",
+          hintText: "location_picker_longitude_hint",
+          errorText: "location_picker_longitude_error",
+          focusNode: latitiudeFocusNode,
+          validator: _validateLong,
+          onUpdated: onLongitudeEditingCompleted,
+        ),
+      ],
+    );
+  }
+}
+
+class _MapPicker extends StatelessWidget {
+  final LatLng latlng;
+  final Function() onModeSwitch;
+  final Function() onMapTap;
+
+  const _MapPicker({
+    required this.latlng,
+    required this.onModeSwitch,
+    required this.onMapTap,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        const Text(
+          "edit_location_dialog_title",
+          textAlign: TextAlign.center,
+        ).tr(),
+        const SizedBox(height: 12),
+        TextButton.icon(
+          icon: Text(
+            "${latlng.latitude.toStringAsFixed(4)}, ${latlng.longitude.toStringAsFixed(4)}",
+          ),
+          label: const Icon(Icons.edit_outlined, size: 16),
+          onPressed: onModeSwitch,
+        ),
+        const SizedBox(height: 12),
+        MapThumbnail(
+          centre: latlng,
+          height: 200,
+          width: 200,
+          zoom: 8,
+          showMarkerPin: true,
+          onTap: (_, __) => onMapTap(),
+        ),
+      ],
+    );
+  }
+}
diff --git a/mobile/lib/utils/debounce.dart b/mobile/lib/utils/debounce.dart
index 273ee8ba95..3432417665 100644
--- a/mobile/lib/utils/debounce.dart
+++ b/mobile/lib/utils/debounce.dart
@@ -1,26 +1,61 @@
 import 'dart:async';
 
-import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
 
-class Debounce {
-  Debounce(Duration interval) : _interval = interval.inMilliseconds;
-  final int _interval;
+/// Used to debounce function calls with the [interval] provided.
+class Debouncer {
+  Debouncer({required this.interval});
+  final Duration interval;
   Timer? _timer;
-  VoidCallback? action;
+  FutureOr<void> Function()? _lastAction;
 
-  void call(VoidCallback? action) {
-    this.action = action;
+  void run(FutureOr<void> Function() action) {
+    _lastAction = action;
     _timer?.cancel();
-    _timer = Timer(Duration(milliseconds: _interval), _callAndRest);
+    _timer = Timer(interval, _callAndRest);
   }
 
   void _callAndRest() {
-    action?.call();
+    _lastAction?.call();
     _timer = null;
   }
 
   void dispose() {
     _timer?.cancel();
     _timer = null;
+    _lastAction = null;
   }
 }
+
+/// Creates a [Debouncer] that will be disposed automatically. If no [interval] is provided, a
+/// default interval of 300ms is used to debounce the function calls
+Debouncer useDebouncer({
+  Duration interval = const Duration(milliseconds: 300),
+  List<Object?>? keys,
+}) =>
+    use(_DebouncerHook(interval: interval, keys: keys));
+
+class _DebouncerHook extends Hook<Debouncer> {
+  const _DebouncerHook({
+    required this.interval,
+    List<Object?>? keys,
+  }) : super(keys: keys);
+
+  final Duration interval;
+
+  @override
+  HookState<Debouncer, Hook<Debouncer>> createState() => _DebouncerHookState();
+}
+
+class _DebouncerHookState extends HookState<Debouncer, _DebouncerHook> {
+  late final debouncer = Debouncer(interval: hook.interval);
+
+  @override
+  Debouncer build(_) => debouncer;
+
+  @override
+  void dispose() => debouncer.dispose();
+
+  @override
+  String get debugLabel => 'useDebouncer';
+}
diff --git a/mobile/lib/utils/draggable_scroll_controller.dart b/mobile/lib/utils/draggable_scroll_controller.dart
new file mode 100644
index 0000000000..6e320ad3c9
--- /dev/null
+++ b/mobile/lib/utils/draggable_scroll_controller.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+/// Creates a [DraggableScrollableController] that will be disposed automatically.
+///
+/// See also:
+/// - [DraggableScrollableController]
+DraggableScrollableController useDraggableScrollController({
+  List<Object?>? keys,
+}) {
+  return use(
+    _DraggableScrollControllerHook(
+      keys: keys,
+    ),
+  );
+}
+
+class _DraggableScrollControllerHook
+    extends Hook<DraggableScrollableController> {
+  const _DraggableScrollControllerHook({
+    List<Object?>? keys,
+  }) : super(keys: keys);
+
+  @override
+  HookState<DraggableScrollableController, Hook<DraggableScrollableController>>
+      createState() => _DraggableScrollControllerHookState();
+}
+
+class _DraggableScrollControllerHookState extends HookState<
+    DraggableScrollableController, _DraggableScrollControllerHook> {
+  late final controller = DraggableScrollableController();
+
+  @override
+  DraggableScrollableController build(BuildContext context) => controller;
+
+  @override
+  void dispose() => controller.dispose();
+
+  @override
+  String get debugLabel => 'useDraggableScrollController';
+}
diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart
index 0be6e77d11..9ad6773870 100644
--- a/mobile/lib/utils/selection_handlers.dart
+++ b/mobile/lib/utils/selection_handlers.dart
@@ -12,7 +12,7 @@ import 'package:immich_mobile/shared/ui/date_time_picker.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
 import 'package:immich_mobile/shared/ui/location_picker.dart';
 import 'package:immich_mobile/shared/ui/share_dialog.dart';
-import 'package:latlong2/latlong.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
 
 void handleShareAssets(
   WidgetRef ref,
diff --git a/mobile/lib/utils/throttle.dart b/mobile/lib/utils/throttle.dart
new file mode 100644
index 0000000000..34619e1dc0
--- /dev/null
+++ b/mobile/lib/utils/throttle.dart
@@ -0,0 +1,57 @@
+import 'dart:async';
+
+import 'package:flutter_hooks/flutter_hooks.dart';
+
+/// Throttles function calls with the [interval] provided.
+/// Also make sures to call the last Action after the elapsed interval
+class Throttler {
+  final Duration interval;
+  DateTime? _lastActionTime;
+
+  Throttler({required this.interval});
+
+  void run(FutureOr<void> Function() action) {
+    if (_lastActionTime == null ||
+        (DateTime.now().difference(_lastActionTime!) > interval)) {
+      action();
+      _lastActionTime = DateTime.now();
+    }
+  }
+
+  void dispose() {
+    _lastActionTime = null;
+  }
+}
+
+/// Creates a [Throttler] that will be disposed automatically. If no [interval] is provided, a
+/// default interval of 300ms is used to throttle the function calls
+Throttler useThrottler({
+  Duration interval = const Duration(milliseconds: 300),
+  List<Object?>? keys,
+}) =>
+    use(_ThrottleHook(interval: interval, keys: keys));
+
+class _ThrottleHook extends Hook<Throttler> {
+  const _ThrottleHook({
+    required this.interval,
+    List<Object?>? keys,
+  }) : super(keys: keys);
+
+  final Duration interval;
+
+  @override
+  HookState<Throttler, Hook<Throttler>> createState() => _ThrottlerHookState();
+}
+
+class _ThrottlerHookState extends HookState<Throttler, _ThrottleHook> {
+  late final throttler = Throttler(interval: hook.interval);
+
+  @override
+  Throttler build(_) => throttler;
+
+  @override
+  void dispose() => throttler.dispose();
+
+  @override
+  String get debugLabel => 'useThrottler';
+}
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index d31d64c3a9..8598a76dac 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -25,14 +25,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.11.2"
+  ansicolor:
+    dependency: transitive
+    description:
+      name: ansicolor
+      sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.2"
   archive:
     dependency: transitive
     description:
       name: archive
-      sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
+      sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
       url: "https://pub.dev"
     source: hosted
-    version: "3.3.7"
+    version: "3.4.9"
   args:
     dependency: transitive
     description:
@@ -385,14 +393,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.0.2"
-  executor_lib:
-    dependency: transitive
-    description:
-      name: executor_lib
-      sha256: "544889daa5726462657dab6410b75f2f8e3a77479d85b307a25c346e243bc38e"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.1.1"
   fake_async:
     dependency: transitive
     description:
@@ -503,10 +503,10 @@ packages:
     dependency: "direct dev"
     description:
       name: flutter_launcher_icons
-      sha256: "559c600f056e7c704bd843723c21e01b5fba47e8824bd02422165bcc02a5de1d"
+      sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
       url: "https://pub.dev"
     source: hosted
-    version: "0.9.3"
+    version: "0.13.1"
   flutter_lints:
     dependency: "direct dev"
     description:
@@ -544,30 +544,14 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
-  flutter_map:
-    dependency: "direct main"
-    description:
-      name: flutter_map
-      sha256: "52c65a977daae42f9aae6748418dd1535eaf27186e9bac9bf431843082bc75a3"
-      url: "https://pub.dev"
-    source: hosted
-    version: "4.0.0"
-  flutter_map_heatmap:
-    dependency: "direct main"
-    description:
-      name: flutter_map_heatmap
-      sha256: "2d16cf5bf41f40a79ae79bcdf2afc92ec45fea0cc311b3a51e3eae661392df88"
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.0.4+2"
   flutter_native_splash:
     dependency: "direct dev"
     description:
       name: flutter_native_splash
-      sha256: "6777a3abb974021a39b5fdd2d46a03ca390e03903b6351f21d10e7ecc969f12d"
+      sha256: "17d9671396fb8ec45ad10f4a975eb8a0f70bedf0fdaf0720b31ea9de6da8c4da"
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.16"
+    version: "2.3.7"
   flutter_plugin_android_lifecycle:
     dependency: transitive
     description:
@@ -755,10 +739,10 @@ packages:
     dependency: transitive
     description:
       name: image
-      sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6"
+      sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271"
       url: "https://pub.dev"
     source: hosted
-    version: "3.3.0"
+    version: "4.1.3"
   image_picker:
     dependency: "direct main"
     description:
@@ -884,14 +868,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.8.1"
-  latlong2:
-    dependency: "direct main"
-    description:
-      name: latlong2
-      sha256: "08ef7282ba9f76e8495e49e2dc4d653015ac929dce5f92b375a415d30b407ea0"
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.8.2"
   lints:
     dependency: transitive
     description:
@@ -900,14 +876,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
-  lists:
-    dependency: transitive
-    description:
-      name: lists
-      sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.1"
   logging:
     dependency: "direct main"
     description:
@@ -916,6 +884,33 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.2.0"
+  maplibre_gl:
+    dependency: "direct main"
+    description:
+      path: "."
+      ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
+      resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
+      url: "https://github.com/maplibre/flutter-maplibre-gl.git"
+    source: git
+    version: "0.18.0"
+  maplibre_gl_platform_interface:
+    dependency: transitive
+    description:
+      path: maplibre_gl_platform_interface
+      ref: main
+      resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
+      url: "https://github.com/maplibre/flutter-maplibre-gl.git"
+    source: git
+    version: "0.18.0"
+  maplibre_gl_web:
+    dependency: transitive
+    description:
+      path: maplibre_gl_web
+      ref: main
+      resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
+      url: "https://github.com/maplibre/flutter-maplibre-gl.git"
+    source: git
+    version: "0.18.0"
   matcher:
     dependency: transitive
     description:
@@ -940,14 +935,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.9.1"
-  mgrs_dart:
-    dependency: transitive
-    description:
-      name: mgrs_dart
-      sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.0.0"
   mime:
     dependency: transitive
     description:
@@ -1163,14 +1150,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.7.3"
-  polylabel:
-    dependency: transitive
-    description:
-      name: polylabel
-      sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.1"
   pool:
     dependency: transitive
     description:
@@ -1187,22 +1166,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.2.4"
-  proj4dart:
-    dependency: transitive
-    description:
-      name: proj4dart
-      sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.1.0"
-  protobuf:
-    dependency: transitive
-    description:
-      name: protobuf
-      sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.1.0"
   provider:
     dependency: transitive
     description:
@@ -1520,14 +1483,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.1"
-  tuple:
-    dependency: transitive
-    description:
-      name: tuple
-      sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.0.2"
   typed_data:
     dependency: transitive
     description:
@@ -1536,14 +1491,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.2"
-  unicode:
-    dependency: transitive
-    description:
-      name: unicode
-      sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.3.1"
   universal_io:
     dependency: transitive
     description:
@@ -1624,15 +1571,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.0.7"
-  vector_map_tiles:
-    dependency: "direct main"
-    description:
-      path: "."
-      ref: immich_above_4
-      resolved-ref: dc685bdbcca2ff2b49b4d0fb77b7bc17fad48608
-      url: "https://github.com/shenlong-tanwen/flutter-vector-map-tiles.git"
-    source: git
-    version: "4.0.0"
   vector_math:
     dependency: transitive
     description:
@@ -1641,22 +1579,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.4"
-  vector_tile:
-    dependency: transitive
-    description:
-      name: vector_tile
-      sha256: "2ac77f6bbd7ddd97efe059207d67bb7eaf807ab98ad58d99fe41a22c230f44e1"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.0"
-  vector_tile_renderer:
-    dependency: transitive
-    description:
-      name: vector_tile_renderer
-      sha256: de212da0f5e48107d3b763a940a428eb1f49d8a4664d41ac0b654f77209a2d0b
-      url: "https://pub.dev"
-    source: hosted
-    version: "4.0.0"
   video_player:
     dependency: "direct main"
     description:
@@ -1761,14 +1683,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.1.4"
-  wkt_parser:
-    dependency: transitive
-    description:
-      name: wkt_parser
-      sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.0.0"
   xdg_directories:
     dependency: transitive
     description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 52e499565a..3759e31852 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -25,13 +25,12 @@ dependencies:
   video_player: ^2.2.18
   chewie: ^1.4.0
   socket_io_client: ^2.0.0-beta.4-nullsafety.0
-  flutter_map: ^4.0.0
-  flutter_map_heatmap: ^0.0.4
+  # Update it to tag once next stable release
+  maplibre_gl:
+      git:
+        url: https://github.com/maplibre/flutter-maplibre-gl.git
+        ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
   geolocator: ^10.0.0 # used to move to current location in map view
-  vector_map_tiles:
-    git:
-      url: https://github.com/shenlong-tanwen/flutter-vector-map-tiles.git
-      ref: immich_above_4
   flutter_udid: ^2.0.0
   package_info_plus: ^4.1.0
   url_launcher: ^6.1.3
@@ -40,10 +39,9 @@ dependencies:
   easy_localization: ^3.0.1
   share_plus: ^7.1.0
   flutter_displaymode: ^0.4.0
-  scrollable_positioned_list: ^0.3.4
+  scrollable_positioned_list: ^0.3.8
   path: ^1.8.1
   path_provider: ^2.0.11
-  latlong2: ^0.8.1
   collection: ^1.16.0
   http_parser: ^4.0.1
   flutter_web_auth: ^0.5.0
@@ -79,7 +77,7 @@ dev_dependencies:
   flutter_lints: ^2.0.1
   build_runner: ^2.2.1
   auto_route_generator: ^5.0.2
-  flutter_launcher_icons: "^0.9.2"
+  flutter_launcher_icons: ^0.13.1
   flutter_native_splash: ^2.2.16
   isar_generator: *isar_version
   integration_test:
@@ -117,11 +115,12 @@ flutter:
       fonts:
         - asset: fonts/overpass/OverpassMono.ttf
 
-flutter_icons:
+flutter_launcher_icons:
   image_path_android: "assets/immich-logo-no-outline.png"
   image_path_ios: "assets/immich-logo-no-outline.png"
   android: true # can specify file name here e.g. "ic_launcher"
   ios: true # can specify file name here e.g. "My-Launcher-Icon
+  remove_alpha_ios: true
 
 analyzer:
   exclude:
diff --git a/mobile/test/modules/album/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart
index b39c495ae5..e42dccaa47 100644
--- a/mobile/test/modules/album/album_sort_by_options_provider_test.dart
+++ b/mobile/test/modules/album/album_sort_by_options_provider_test.dart
@@ -203,7 +203,7 @@ void main() {
     late ProviderContainer container;
 
     setUp(() async {
-      settingsMock = AppSettingsServiceMock();
+      settingsMock = MockAppSettingsService();
       container = TestUtils.createContainer(
         overrides: [
           appSettingsServiceProvider.overrideWith((ref) => settingsMock),
@@ -283,7 +283,7 @@ void main() {
     late ProviderContainer container;
 
     setUp(() async {
-      settingsMock = AppSettingsServiceMock();
+      settingsMock = MockAppSettingsService();
       container = TestUtils.createContainer(
         overrides: [
           appSettingsServiceProvider.overrideWith((ref) => settingsMock),
diff --git a/mobile/test/modules/map/map_mocks.dart b/mobile/test/modules/map/map_mocks.dart
new file mode 100644
index 0000000000..e5000a8382
--- /dev/null
+++ b/mobile/test/modules/map/map_mocks.dart
@@ -0,0 +1,18 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/map/models/map_state.model.dart';
+import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
+import 'package:mocktail/mocktail.dart';
+
+class MockMapStateNotifier extends Notifier<MapState>
+    with Mock
+    implements MapStateNotifier {
+  final MapState initState;
+
+  MockMapStateNotifier(this.initState);
+
+  @override
+  MapState build() => initState;
+
+  @override
+  set state(MapState mapState) => super.state = mapState;
+}
diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart
new file mode 100644
index 0000000000..94c5087cdd
--- /dev/null
+++ b/mobile/test/modules/map/map_theme_override_test.dart
@@ -0,0 +1,165 @@
+@Tags(['widget'])
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/map/models/map_state.model.dart';
+import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
+import 'package:immich_mobile/modules/map/widgets/map_theme_override.dart';
+
+import '../../test_utils.dart';
+import '../../widget_tester_extensions.dart';
+import 'map_mocks.dart';
+
+void main() {
+  late MockMapStateNotifier mapStateNotifier;
+  late List<Override> overrides;
+  late MapState mapState;
+
+  setUpAll(() async {
+    TestUtils.init();
+  });
+
+  setUp(() {
+    mapState = MapState(themeMode: ThemeMode.dark);
+    mapStateNotifier = MockMapStateNotifier(mapState);
+    overrides = [mapStateNotifierProvider.overrideWith(() => mapStateNotifier)];
+  });
+
+  testWidgets("Return dark theme style when theme mode is dark",
+      (tester) async {
+    AsyncValue<String>? mapStyle;
+    await tester.pumpConsumerWidget(
+      MapThemeOveride(
+        mapBuilder: (AsyncValue<String> style) {
+          mapStyle = style;
+          return const Text("Mock");
+        },
+      ),
+      overrides: overrides,
+    );
+
+    mapStateNotifier.state =
+        mapState.copyWith(darkStyleFetched: const AsyncData("dark"));
+    await tester.pumpAndSettle();
+    expect(mapStyle?.valueOrNull, "dark");
+  });
+
+  testWidgets("Return error when style is not fetched", (tester) async {
+    AsyncValue<String>? mapStyle;
+    await tester.pumpConsumerWidget(
+      MapThemeOveride(
+        mapBuilder: (AsyncValue<String> style) {
+          mapStyle = style;
+          return const Text("Mock");
+        },
+      ),
+      overrides: overrides,
+    );
+
+    mapStateNotifier.state = mapState.copyWith(
+      darkStyleFetched: const AsyncError("Error", StackTrace.empty),
+    );
+    await tester.pumpAndSettle();
+    expect(mapStyle?.hasError, isTrue);
+  });
+
+  testWidgets("Return light theme style when theme mode is light",
+      (tester) async {
+    AsyncValue<String>? mapStyle;
+    await tester.pumpConsumerWidget(
+      MapThemeOveride(
+        mapBuilder: (AsyncValue<String> style) {
+          mapStyle = style;
+          return const Text("Mock");
+        },
+      ),
+      overrides: overrides,
+    );
+
+    mapStateNotifier.state = mapState.copyWith(
+      themeMode: ThemeMode.light,
+      lightStyleFetched: const AsyncData("light"),
+    );
+    await tester.pumpAndSettle();
+    expect(mapStyle?.valueOrNull, "light");
+  });
+
+  group("System mode", () {
+    testWidgets("Return dark theme style when system is dark", (tester) async {
+      AsyncValue<String>? mapStyle;
+      await tester.pumpConsumerWidget(
+        MapThemeOveride(
+          mapBuilder: (AsyncValue<String> style) {
+            mapStyle = style;
+            return const Text("Mock");
+          },
+        ),
+        overrides: overrides,
+      );
+
+      tester.binding.platformDispatcher.platformBrightnessTestValue =
+          Brightness.dark;
+      mapStateNotifier.state = mapState.copyWith(
+        themeMode: ThemeMode.system,
+        darkStyleFetched: const AsyncData("dark"),
+      );
+      await tester.pumpAndSettle();
+
+      expect(mapStyle?.valueOrNull, "dark");
+    });
+
+    testWidgets("Return light theme style when system is light",
+        (tester) async {
+      AsyncValue<String>? mapStyle;
+      await tester.pumpConsumerWidget(
+        MapThemeOveride(
+          mapBuilder: (AsyncValue<String> style) {
+            mapStyle = style;
+            return const Text("Mock");
+          },
+        ),
+        overrides: overrides,
+      );
+
+      tester.binding.platformDispatcher.platformBrightnessTestValue =
+          Brightness.light;
+      mapStateNotifier.state = mapState.copyWith(
+        themeMode: ThemeMode.system,
+        lightStyleFetched: const AsyncData("light"),
+      );
+      await tester.pumpAndSettle();
+
+      expect(mapStyle?.valueOrNull, "light");
+    });
+
+    testWidgets("Switches style when system brightness changes",
+        (tester) async {
+      AsyncValue<String>? mapStyle;
+      await tester.pumpConsumerWidget(
+        MapThemeOveride(
+          mapBuilder: (AsyncValue<String> style) {
+            mapStyle = style;
+            return const Text("Mock");
+          },
+        ),
+        overrides: overrides,
+      );
+
+      tester.binding.platformDispatcher.platformBrightnessTestValue =
+          Brightness.light;
+      mapStateNotifier.state = mapState.copyWith(
+        themeMode: ThemeMode.system,
+        lightStyleFetched: const AsyncData("light"),
+        darkStyleFetched: const AsyncData("dark"),
+      );
+      await tester.pumpAndSettle();
+      expect(mapStyle?.valueOrNull, "light");
+
+      tester.binding.platformDispatcher.platformBrightnessTestValue =
+          Brightness.dark;
+      await tester.pumpAndSettle();
+      expect(mapStyle?.valueOrNull, "dark");
+    });
+  });
+}
diff --git a/mobile/test/modules/settings/settings_mocks.dart b/mobile/test/modules/settings/settings_mocks.dart
index 0fd6948702..469fe7728b 100644
--- a/mobile/test/modules/settings/settings_mocks.dart
+++ b/mobile/test/modules/settings/settings_mocks.dart
@@ -1,4 +1,4 @@
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:mocktail/mocktail.dart';
 
-class AppSettingsServiceMock extends Mock implements AppSettingsService {}
+class MockAppSettingsService extends Mock implements AppSettingsService {}
diff --git a/mobile/test/modules/utils/debouncer_test.dart b/mobile/test/modules/utils/debouncer_test.dart
new file mode 100644
index 0000000000..7aa13842d6
--- /dev/null
+++ b/mobile/test/modules/utils/debouncer_test.dart
@@ -0,0 +1,41 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:immich_mobile/utils/debounce.dart';
+
+class _Counter {
+  int _count = 0;
+  _Counter();
+
+  int get count => _count;
+  void increment() => _count = _count + 1;
+}
+
+void main() {
+  test('Executes the method after the interval', () async {
+    var counter = _Counter();
+    final debouncer = Debouncer(interval: const Duration(milliseconds: 300));
+    debouncer.run(() => counter.increment());
+    expect(counter.count, 0);
+    await Future.delayed(const Duration(milliseconds: 300));
+    expect(counter.count, 1);
+  });
+
+  test('Executes the method immediately if zero interval', () async {
+    var counter = _Counter();
+    final debouncer = Debouncer(interval: const Duration(milliseconds: 0));
+    debouncer.run(() => counter.increment());
+    // Even though it is supposed to be executed immediately, it is added to the async queue and so
+    // we need this delay to make sure the actual debounced method is called
+    await Future.delayed(const Duration(milliseconds: 0));
+    expect(counter.count, 1);
+  });
+
+  test('Delayes method execution after all the calls are completed', () async {
+    var counter = _Counter();
+    final debouncer = Debouncer(interval: const Duration(milliseconds: 100));
+    debouncer.run(() => counter.increment());
+    debouncer.run(() => counter.increment());
+    debouncer.run(() => counter.increment());
+    await Future.delayed(const Duration(milliseconds: 300));
+    expect(counter.count, 1);
+  });
+}
diff --git a/mobile/test/modules/utils/throttler_test.dart b/mobile/test/modules/utils/throttler_test.dart
new file mode 100644
index 0000000000..76d8bd2ad7
--- /dev/null
+++ b/mobile/test/modules/utils/throttler_test.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:immich_mobile/utils/throttle.dart';
+
+class _Counter {
+  int _count = 0;
+  _Counter();
+
+  int get count => _count;
+  void increment() {
+    debugPrint("Counter inside increment: $count");
+    _count = _count + 1;
+  }
+}
+
+void main() {
+  test('Executes the method immediately if no calls received previously',
+      () async {
+    var counter = _Counter();
+    final throttler = Throttler(interval: const Duration(milliseconds: 300));
+    throttler.run(() => counter.increment());
+    expect(counter.count, 1);
+  });
+
+  test('Does not execute calls before throttle interval', () async {
+    var counter = _Counter();
+    final throttler = Throttler(interval: const Duration(milliseconds: 100));
+    throttler.run(() => counter.increment());
+    throttler.run(() => counter.increment());
+    throttler.run(() => counter.increment());
+    throttler.run(() => counter.increment());
+    throttler.run(() => counter.increment());
+    await Future.delayed(const Duration(seconds: 1));
+    expect(counter.count, 1);
+  });
+
+  test('Executes the method if received in intervals', () async {
+    var counter = _Counter();
+    final throttler = Throttler(interval: const Duration(milliseconds: 100));
+    for (final _ in Iterable<int>.generate(10)) {
+      throttler.run(() => counter.increment());
+      await Future.delayed(const Duration(milliseconds: 50));
+    }
+    await Future.delayed(const Duration(seconds: 1));
+    expect(counter.count, 5);
+  });
+}