From e6c0f0e3aa7f0fa332306365a9e3438e16094e83 Mon Sep 17 00:00:00 2001
From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
Date: Mon, 15 Jan 2024 15:26:13 +0000
Subject: [PATCH] refactor(mobile): maplibre (#6087)

* chore: maplibre gl pubspec

* refactor(wip): maplibre for maps

* refactor(wip): dual pane + location button

* chore: remove flutter_map and deps

* refactor(wip): map zoom to location

* refactor: location picker

* open gallery_viewer on marker tap

* remove detectScaleGesture param

* test: debounce and throttle

* chore: rename get location method

* feat(mobile): Adds gps locator to map prompt for easy geolocation (#6282)

* Refactored get gps coords

* Use var for linter's sake, should handle errors better

* Cleanup

* Fix linter issues

* chore(dep): update maplibre to official lib

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Joshua Herrera <joshua.herrera227@gmail.com>
---
 mobile/assets/i18n/en-US.json                 |   2 +-
 mobile/assets/location-pin.png                | Bin 50753 -> 23260 bytes
 mobile/ios/Podfile.lock                       |  10 +
 .../lib/extensions/collection_extensions.dart |   6 +
 .../extensions/flutter_map_extensions.dart    |  67 --
 .../extensions/latlngbounds_extension.dart    |  20 +
 .../maplibrecontroller_extensions.dart        |  71 ++
 .../providers/current_asset.provider.g.dart   | Bin 913 -> 913 bytes
 .../asset_viewer/ui/exif_bottom_sheet.dart    |  21 +-
 .../home/ui/asset_grid/immich_asset_grid.dart |   8 +-
 .../ui/asset_grid/immich_asset_grid_view.dart |  12 +-
 .../modules/map/models/map_event.model.dart   |  13 +
 mobile/lib/modules/map/models/map_marker.dart |  39 +
 .../map/models/map_page_event.model.dart      |  40 -
 .../modules/map/models/map_state.model.dart   |  54 +-
 .../map/providers/map_marker.provider.dart    |  46 +-
 .../map/providers/map_marker.provider.g.dart  | Bin 0 -> 898 bytes
 .../map/providers/map_service.provider.dart   |   9 +
 .../map/providers/map_service.provider.g.dart | Bin 0 -> 877 bytes
 .../map/providers/map_state.provider.dart     | 229 +++--
 .../map/providers/map_state.provider.g.dart   | Bin 0 -> 933 bytes
 .../lib/modules/map/services/map.service.dart |  67 +-
 .../lib/modules/map/ui/location_dialog.dart   |  30 -
 .../modules/map/ui/map_location_picker.dart   | 114 ---
 .../lib/modules/map/ui/map_page_app_bar.dart  | 138 ---
 .../modules/map/ui/map_page_bottom_sheet.dart | 356 --------
 .../modules/map/ui/map_settings_dialog.dart   | 228 -----
 mobile/lib/modules/map/ui/map_thumbnail.dart  |  86 --
 .../map/utils/map_controller_hook.dart        |  32 -
 mobile/lib/modules/map/utils/map_utils.dart   | 138 +++
 .../map/views/map_location_picker_page.dart   | 185 ++++
 mobile/lib/modules/map/views/map_page.dart    | 792 ++++++++----------
 .../lib/modules/map/widgets/map_app_bar.dart  | 159 ++++
 .../modules/map/widgets/map_asset_grid.dart   | 273 ++++++
 .../modules/map/widgets/map_bottom_sheet.dart |  97 +++
 .../map_settings/map_settings_list_tile.dart  |  31 +
 .../map_settings_time_dropdown.dart           |  92 ++
 .../map_settings/map_theme_picker.dart        | 109 +++
 .../map/widgets/map_settings_sheet.dart       |  61 ++
 .../map/widgets/map_theme_override.dart       |  96 +++
 .../modules/map/widgets/map_thumbnail.dart    | 110 +++
 .../positioned_asset_marker_icon.dart}        |  58 +-
 .../search/services/person.service.g.dart     | Bin 907 -> 907 bytes
 .../modules/search/ui/curated_places_row.dart |   8 +-
 .../services/app_settings.service.dart        |   2 +-
 mobile/lib/routing/router.dart                |   4 +-
 mobile/lib/routing/router.gr.dart             |   6 +-
 mobile/lib/shared/models/store.dart           |  12 +-
 .../shared/providers/websocket.provider.dart  |   5 +-
 mobile/lib/shared/services/asset.service.dart |   2 +-
 mobile/lib/shared/ui/drag_sheet.dart          |   7 +-
 mobile/lib/shared/ui/location_picker.dart     | 385 ++++-----
 mobile/lib/utils/debounce.dart                |  53 +-
 .../utils/draggable_scroll_controller.dart    |  41 +
 mobile/lib/utils/selection_handlers.dart      |   2 +-
 mobile/lib/utils/throttle.dart                |  57 ++
 mobile/pubspec.lock                           | 172 +---
 mobile/pubspec.yaml                           |  19 +-
 .../album_sort_by_options_provider_test.dart  |   4 +-
 mobile/test/modules/map/map_mocks.dart        |  18 +
 .../modules/map/map_theme_override_test.dart  | 165 ++++
 .../test/modules/settings/settings_mocks.dart |   2 +-
 mobile/test/modules/utils/debouncer_test.dart |  41 +
 mobile/test/modules/utils/throttler_test.dart |  47 ++
 64 files changed, 2782 insertions(+), 2169 deletions(-)
 delete mode 100644 mobile/lib/extensions/flutter_map_extensions.dart
 create mode 100644 mobile/lib/extensions/latlngbounds_extension.dart
 create mode 100644 mobile/lib/extensions/maplibrecontroller_extensions.dart
 create mode 100644 mobile/lib/modules/map/models/map_event.model.dart
 create mode 100644 mobile/lib/modules/map/models/map_marker.dart
 delete mode 100644 mobile/lib/modules/map/models/map_page_event.model.dart
 create mode 100644 mobile/lib/modules/map/providers/map_marker.provider.g.dart
 create mode 100644 mobile/lib/modules/map/providers/map_service.provider.dart
 create mode 100644 mobile/lib/modules/map/providers/map_service.provider.g.dart
 create mode 100644 mobile/lib/modules/map/providers/map_state.provider.g.dart
 delete mode 100644 mobile/lib/modules/map/ui/location_dialog.dart
 delete mode 100644 mobile/lib/modules/map/ui/map_location_picker.dart
 delete mode 100644 mobile/lib/modules/map/ui/map_page_app_bar.dart
 delete mode 100644 mobile/lib/modules/map/ui/map_page_bottom_sheet.dart
 delete mode 100644 mobile/lib/modules/map/ui/map_settings_dialog.dart
 delete mode 100644 mobile/lib/modules/map/ui/map_thumbnail.dart
 delete mode 100644 mobile/lib/modules/map/utils/map_controller_hook.dart
 create mode 100644 mobile/lib/modules/map/utils/map_utils.dart
 create mode 100644 mobile/lib/modules/map/views/map_location_picker_page.dart
 create mode 100644 mobile/lib/modules/map/widgets/map_app_bar.dart
 create mode 100644 mobile/lib/modules/map/widgets/map_asset_grid.dart
 create mode 100644 mobile/lib/modules/map/widgets/map_bottom_sheet.dart
 create mode 100644 mobile/lib/modules/map/widgets/map_settings/map_settings_list_tile.dart
 create mode 100644 mobile/lib/modules/map/widgets/map_settings/map_settings_time_dropdown.dart
 create mode 100644 mobile/lib/modules/map/widgets/map_settings/map_theme_picker.dart
 create mode 100644 mobile/lib/modules/map/widgets/map_settings_sheet.dart
 create mode 100644 mobile/lib/modules/map/widgets/map_theme_override.dart
 create mode 100644 mobile/lib/modules/map/widgets/map_thumbnail.dart
 rename mobile/lib/modules/map/{ui/asset_marker_icon.dart => widgets/positioned_asset_marker_icon.dart} (72%)
 create mode 100644 mobile/lib/utils/draggable_scroll_controller.dart
 create mode 100644 mobile/lib/utils/throttle.dart
 create mode 100644 mobile/test/modules/map/map_mocks.dart
 create mode 100644 mobile/test/modules/map/map_theme_override_test.dart
 create mode 100644 mobile/test/modules/utils/debouncer_test.dart
 create mode 100644 mobile/test/modules/utils/throttler_test.dart

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 1c8ba878851f5c2506eba0f463fa5edb0e37fc98..9bfc53d05b7a0a858b70ae6cd60c13d170b01aac 100644
GIT binary patch
literal 23260
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
Mamf6E89DL)02~a<FaQ7m

literal 50753
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
a8ae&HKadlv3N9HV5K`iDV)-IEUjGMx6B*qA

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 53daa74a12667ae3f5248fc24392fe877ab37ed0..96628dab5897390c57fa0c92bdbea6039511ff0f 100644
GIT binary patch
delta 53
zcmbQpK9PNc8l#3$N@|*+L29C@rHP@bQL?3lNn)yzrMXFpnT2^`l9`36S&Dg5N{Yc|
IKSpOJ0E)j3cK`qY

delta 53
zcmbQpK9PNc8l#4Rp+$;inx(OsrKMrAiA9=$S+b#FQkr?Hxsjo<K~hqpk*S4&S*p=y
IKSpOJ0D5Q*+5i9m

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 0000000000000000000000000000000000000000..7df6adea9941a769cf0702c5baff327d80f32c70
GIT binary patch
literal 898
zcmb_a!H(K65WVLs=1?h6Z3)#jWnmXux(RHh1uEdOs;bC|C)BEAM`I^a|GqXXOQlpj
ztxq1mr#F6YCW>I0uG8&&pC)j>Nm6(R$p+S&J*+m#*ToZjd4h-eIthZBkp*2sSg|^*
z82v&T)TCR%-Gw_aybXdVg14`ByxLZ@NVVomR7lKfdgT_u&XQ0E$lf?xtD`IPz|7BH
zAEq;)a6HV1!+ccU#6|oee?KnckHv_`F~4T~CLYDbaLieLU7XBEutNkUjfU?Ry&r?%
z^S9@u7Mgk&q#u+*F$tHKGY_UPZ>&y)skOmHV;jOVcK0ZZy<46CJDUxJvPe#-7r<Xy
zSFTd5!in?I?GSpHW8NH=XzvM`#hq@bKyQEIFn<cIjD=ilIVee2scHpjRiU!I(9e9(
zR(=V1Q`oQ%pFlNI!laYtsa1`NqbdZN$&u(^nUs5C$_VQQonV*JcUxtD*T}Ji7t3br
zs{bV9;7&-lesjb@X~L}3l$AodWW4lLy8Ql$nkM&zOqZEKCXr`_WX4>9P%V>!XN@_6
a=lrt@s~o9=x59{Ap88re)@|G~HTVOz+8Nyd

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..7b4e68eaee3b9ba77ae4858113547a246095ed39
GIT binary patch
literal 877
zcmb_aL2lbH5WMRZdnh2gNaZ$3?Zm0mx{>1a5WBE_35r6X$fZkwB84R-AipnZ#c%-U
z7~Z%u+~v&9XatL5S*+*VB8R(GUce{FSFl`d;c=D!x_^S7Pw+5b=1I~p@?a&THET+T
z)G3ZxL$*@_DH8!rZ<1s*f{*WZVz;&GkQyr%Xpoq<^v)xajVEPxP<{xyHodF|=Kc+a
zFuMgx&*k`vf1WTt=He@#Tq3`!@S+muLSBE7m!cX^E~?2j_TopdK?J6qg+IrBf3xJr
z?-!@;Ee(#yFp<hI4adrX2Q!$r-sZ|R*5RT0?Q#D})p5Mdl<`R6^&o*)4bH;Nuo|b~
z^QUdo2n(#*-2(j`A@jJgEpd#RrvZzHTjM-b*6K`>tqapBvZhAk2Web%*41(IqUAmv
z!gnyO)-XM>upiZ+5oowVH{BCsm1(^tW}Nb2yE&dxh9OG+xj}&vddV?v>u`J1mH@nV
z<|bxD?TjTXrKM6T9qfqxW;&SmChFQM5=tvWxS2*#a?PAO17$j<l_*=c2M?x~HP#i<
T3EnBED&5cZ=zPf53pM!<j${(E

literal 0
HcmV?d00001

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 0000000000000000000000000000000000000000..ca75292e7892745a1c35f420ca38d7bc2fce6e39
GIT binary patch
literal 933
zcmb_aL2lbH5WMRZdnh2ga3v?L;mE1orjg?G5W9$d35r6X#Fa~cB84TTqQ5U`TRAAr
zExd7Oca}4pIEHn$$sTX^Sqit?G=mS2Zeg?C!^1ZHe)k05p5XpwlLkS<$bzmQtXY#A
z%Pd9>=}vH@h&wF24uUv__iuJQ+oR}^8qL?JkeJo<#w&uIC7}+GzjLZ~UercgQ3<4b
zW`2*xuv`I!AB)*EDQ5Fx{%JZbrpeXC9FvkECLD|EDoHNpmnENFo-BxAhX_m>4L=_S
ztiPh*>yNW*cbY~+d>m9lF$u%h^fRywz@7e)efjf{X99g?6$xdL9O=l7_SK#sQut@$
z>WbC4a7tg>8~P?Ewmq!T-V!>acDkh!y-f=!wK5h8tz}e^t~1pM(yB&fN1@L~)YU#L
zye(}wJ}LnC0;-krd|ABm1E;kODvqiYXcosIUu9D6i76wji%5zuTgEPr_HTn6D@Q&Q
z4$Ef~pUqoc`*ewe(u8@XDX)Zdu6UBAbT0lEHErP=a$V&HnM9tKk{L4rp*ki7&s%c@
a`(MawtP7+Ucqfb~<Y})(V_hoGt-(J)7bHjk

literal 0
HcmV?d00001

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 b80b439d1d3b9132c2c98f30fe2d4e0ef150377d..01a5ed8f309434afa387cbb946ebe1514667207f 100644
GIT binary patch
delta 53
zcmeBX?`Gd1&!}N)l4_QcW|Cx)nwn^CVv=T-l4@XJnPy>ZXqs%9mTG2XY>;MVZfUXE
IozaB}0FvJhZvX%Q

delta 53
zcmeBX?`Gd1&!~}{l4_7>nQUNWnPO;`l9G~Yl$es7Vrr3?YLRSVW}K8}k(_LpmYTBJ
IozaB}0Ir%40RR91

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);
+  });
+}