From c087b7c06333a31e3da53e800d4652e473fbe6ca Mon Sep 17 00:00:00 2001 From: Thomas Way Date: Sat, 21 Feb 2026 01:17:06 +0000 Subject: [PATCH] chore(mobile): replace maplibre_gl with maplibre maplibre is a ground-up rewrite of maplibre_gl with a more modern and ergonomic API. It should fix a few bugs we've seen with maps, and perform better. --- mise.toml | 2 +- mobile/ios/Podfile.lock | 38 +-- mobile/ios/Runner.xcodeproj/project.pbxproj | 9 + .../xcshareddata/swiftpm/Package.resolved | 9 + mobile/lib/domain/models/map.model.dart | 4 +- mobile/lib/domain/services/map.service.dart | 6 +- .../extensions/latlngbounds_extension.dart | 29 ++- .../maplibrecontroller_extensions.dart | 81 ++---- .../repositories/db.repository.dart | 1 + .../repositories/map.repository.dart | 23 +- .../repositories/remote_asset.repository.dart | 6 +- .../repositories/timeline.repository.dart | 4 +- mobile/lib/models/map/map_marker.model.dart | 8 +- mobile/lib/pages/library/library.page.dart | 4 +- .../places/places_collection.page.dart | 6 +- mobile/lib/pages/search/map/map.page.dart | 243 +++++++++--------- .../search/map/map_location_picker.page.dart | 67 ++--- .../pages/drift_library.page.dart | 4 +- .../presentation/pages/drift_map.page.dart | 4 +- .../presentation/pages/drift_place.page.dart | 8 +- .../location_details.widget.dart | 10 +- .../presentation/widgets/map/map.state.dart | 14 +- .../presentation/widgets/map/map.widget.dart | 111 +++----- .../presentation/widgets/map/map_utils.dart | 55 ++-- .../repositories/asset_api.repository.dart | 6 +- mobile/lib/routing/router.dart | 2 +- mobile/lib/routing/router.gr.dart | 35 +-- mobile/lib/services/action.service.dart | 6 +- mobile/lib/services/asset.service.dart | 8 +- mobile/lib/services/map.service.dart | 11 +- mobile/lib/utils/map_utils.dart | 58 +++-- mobile/lib/utils/selection_handlers.dart | 6 +- .../asset_viewer/detail_panel/exif_map.dart | 6 +- .../lib/widgets/common/location_picker.dart | 20 +- mobile/lib/widgets/map/map_asset_grid.dart | 53 ++-- mobile/lib/widgets/map/map_bottom_sheet.dart | 10 +- .../map/map_settings/map_theme_picker.dart | 4 +- .../lib/widgets/map/map_theme_override.dart | 9 +- mobile/lib/widgets/map/map_thumbnail.dart | 98 ++++--- .../map/positioned_asset_marker_icon.dart | 43 ---- .../widgets/search/search_map_thumbnail.dart | 10 +- mobile/mise.toml | 2 +- mobile/pubspec.lock | 168 ++++++++++-- mobile/pubspec.yaml | 6 +- mobile/test/services/asset.service_test.dart | 8 +- server/Dockerfile.dev | 2 +- 46 files changed, 666 insertions(+), 651 deletions(-) delete mode 100644 mobile/lib/widgets/map/positioned_asset_marker_icon.dart diff --git a/mise.toml b/mise.toml index 5e3088974c..66483145b1 100644 --- a/mise.toml +++ b/mise.toml @@ -15,7 +15,7 @@ config_roots = [ [tools] node = "24.13.1" -flutter = "3.35.7" +flutter = "3.41.2" pnpm = "10.29.3" terragrunt = "0.98.0" opentofu = "1.11.4" diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index e1ec4aff07..bf2a398457 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -38,10 +38,10 @@ PODS: - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS - - MapLibre (6.14.0) - - maplibre_gl (0.0.1): + - MapLibre (6.23.0) + - maplibre_ios (0.0.1): - Flutter - - MapLibre (= 6.14.0) + - MapLibre (~> 6.21) - native_video_player (1.0.0): - Flutter - network_info_plus (0.0.1): @@ -58,6 +58,8 @@ PODS: - photo_manager (3.7.1): - Flutter - FlutterMacOS + - pointer_interceptor_ios (0.0.1): + - Flutter - SAMKeychain (1.5.3) - share_handler_ios (0.0.14): - Flutter @@ -75,16 +77,16 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - sqlite3 (3.49.1): - - sqlite3/common (= 3.49.1) - - sqlite3/common (3.49.1) - - sqlite3/dbstatvtab (3.49.1): + - sqlite3 (3.49.2): + - sqlite3/common (= 3.49.2) + - sqlite3/common (3.49.2) + - sqlite3/dbstatvtab (3.49.2): - sqlite3/common - - sqlite3/fts5 (3.49.1): + - sqlite3/fts5 (3.49.2): - sqlite3/common - - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/perf-threadsafe (3.49.2): - sqlite3/common - - sqlite3/rtree (3.49.1): + - sqlite3/rtree (3.49.2): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter @@ -118,7 +120,7 @@ DEPENDENCIES: - integration_test (from `.symlinks/plugins/integration_test/ios`) - isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) + - maplibre_ios (from `.symlinks/plugins/maplibre_ios/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - objective_c (from `.symlinks/plugins/objective_c/ios`) @@ -126,6 +128,7 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) + - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) - share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`) - share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`) - share_plus (from `.symlinks/plugins/share_plus/ios`) @@ -178,8 +181,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/isar_community_flutter_libs/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" - maplibre_gl: - :path: ".symlinks/plugins/maplibre_gl/ios" + maplibre_ios: + :path: ".symlinks/plugins/maplibre_ios/ios" native_video_player: :path: ".symlinks/plugins/native_video_player/ios" network_info_plus: @@ -194,6 +197,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" photo_manager: :path: ".symlinks/plugins/photo_manager/ios" + pointer_interceptor_ios: + :path: ".symlinks/plugins/pointer_interceptor_ios/ios" share_handler_ios: :path: ".symlinks/plugins/share_handler_ios/ios" share_handler_ios_models: @@ -230,8 +235,8 @@ SPEC CHECKSUMS: integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 - MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd - maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f + MapLibre: c0fcafabb341f230657d959970c6eb47fb55750e + maplibre_ios: 05031d5f79702672d2c01cc77b6ba3187d4bf896 native_video_player: b65c58951ede2f93d103a25366bdebca95081265 network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 @@ -239,13 +244,14 @@ SPEC CHECKSUMS: path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62 + pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 + sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1 sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 22a7abcbac..570b3b2a63 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -446,6 +446,7 @@ packageReferences = ( FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */, FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */, + A1B2C3D4E5F6A7B8C9D0E1F2 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */, ); preferredProjectObjectVersion = 77; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; @@ -1250,6 +1251,14 @@ minimumVersion = 1.5.0; }; }; + A1B2C3D4E5F6A7B8C9D0E1F2 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/maplibre/maplibre-gl-native-distribution"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 6.21.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4962230c22..77b9fae7fc 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,6 +10,15 @@ "version" : "1.0.3" } }, + { + "identity" : "maplibre-gl-native-distribution", + "kind" : "remoteSourceControl", + "location" : "https://github.com/maplibre/maplibre-gl-native-distribution", + "state" : { + "revision" : "2aefb4dd47ca6e897c93086f348a457839aac2fe", + "version" : "6.23.0" + } + }, { "identity" : "grdb.swift", "kind" : "remoteSourceControl", diff --git a/mobile/lib/domain/models/map.model.dart b/mobile/lib/domain/models/map.model.dart index ce0834f0cb..b3a0a3fb2e 100644 --- a/mobile/lib/domain/models/map.model.dart +++ b/mobile/lib/domain/models/map.model.dart @@ -1,7 +1,7 @@ -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; class Marker { - final LatLng location; + final Geographic location; final String assetId; const Marker({required this.location, required this.assetId}); diff --git a/mobile/lib/domain/services/map.service.dart b/mobile/lib/domain/services/map.service.dart index 6c64e2817e..55e9b06e99 100644 --- a/mobile/lib/domain/services/map.service.dart +++ b/mobile/lib/domain/services/map.service.dart @@ -1,9 +1,9 @@ import 'package:immich_mobile/domain/models/map.model.dart'; import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart' hide Marker; -typedef MapMarkerSource = Future> Function(LatLngBounds? bounds); +typedef MapMarkerSource = Future> Function(LngLatBounds? bounds); typedef MapQuery = ({MapMarkerSource markerSource}); @@ -21,5 +21,5 @@ class MapService { MapService(MapQuery query) : _markerSource = query.markerSource; - Future> Function(LatLngBounds? bounds) get getMarkers => _markerSource; + Future> Function(LngLatBounds? bounds) get getMarkers => _markerSource; } diff --git a/mobile/lib/extensions/latlngbounds_extension.dart b/mobile/lib/extensions/latlngbounds_extension.dart index a8948728bd..a56d7ceb81 100644 --- a/mobile/lib/extensions/latlngbounds_extension.dart +++ b/mobile/lib/extensions/latlngbounds_extension.dart @@ -1,20 +1,23 @@ -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; -extension WithinBounds on LatLngBounds { +extension WithinBounds on LngLatBounds { /// Checks whether [point] is inside bounds - bool contains(LatLng point) { - final sw = point; - final ne = point; - return containsBounds(LatLngBounds(southwest: sw, northeast: ne)); + bool contains(Geographic point) { + return containsBounds( + LngLatBounds( + longitudeWest: point.lon, + longitudeEast: point.lon, + latitudeSouth: point.lat, + latitudeNorth: point.lat, + ), + ); } /// 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); + bool containsBounds(LngLatBounds bounds) { + return (bounds.latitudeSouth >= latitudeSouth) && + (bounds.latitudeNorth <= latitudeNorth) && + (bounds.longitudeWest >= longitudeWest) && + (bounds.longitudeEast <= longitudeEast); } } diff --git a/mobile/lib/extensions/maplibrecontroller_extensions.dart b/mobile/lib/extensions/maplibrecontroller_extensions.dart index e1f32a4d8c..c4e2531a56 100644 --- a/mobile/lib/extensions/maplibrecontroller_extensions.dart +++ b/mobile/lib/extensions/maplibrecontroller_extensions.dart @@ -1,19 +1,19 @@ import 'dart:async'; -import 'dart:io'; -import 'dart:math'; +import 'dart:convert'; -import 'package:flutter/services.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/utils/map_utils.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; -extension MapMarkers on MapLibreMapController { +extension MapMarkers on MapController { static var _completer = Completer()..complete(); Future addGeoJSONSourceForMarkers(List markers) async { - return addSource( - MapUtils.defaultSourceId, - GeojsonSourceProperties(data: MapUtils.generateGeoJsonForMarkers(markers.toList())), + return style!.addSource( + GeoJsonSource( + id: MapUtils.defaultSourceId, + data: jsonEncode(MapUtils.generateGeoJsonForMarkers(markers.toList())), + ), ); } @@ -27,63 +27,28 @@ extension MapMarkers on MapLibreMapController { // !! 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); + try { + await style!.removeLayer(MapUtils.defaultHeatMapLayerId); + } catch (_) { + // Layer may not exist } - final existingSources = await getSourceIds(); - if (existingSources.contains(MapUtils.defaultSourceId)) { - await removeSource(MapUtils.defaultSourceId); + try { + await style!.removeSource(MapUtils.defaultSourceId); + } catch (_) { + // Source may not exist } await addGeoJSONSourceForMarkers(markers); - if (Platform.isAndroid) { - await addCircleLayer( - MapUtils.defaultSourceId, - MapUtils.defaultHeatMapLayerId, - const CircleLayerProperties( - circleRadius: 10, - circleColor: "rgba(150,86,34,0.7)", - circleBlur: 1.0, - circleOpacity: 0.7, - circleStrokeWidth: 0.1, - circleStrokeColor: "rgba(203,46,19,0.5)", - circleStrokeOpacity: 0.7, - ), - ); - } - - if (Platform.isIOS) { - await addHeatmapLayer( - MapUtils.defaultSourceId, - MapUtils.defaultHeatMapLayerId, - MapUtils.defaultHeatMapLayerProperties, - ); - } + await style!.addLayer( + const HeatmapStyleLayer( + id: MapUtils.defaultHeatMapLayerId, + sourceId: MapUtils.defaultSourceId, + paint: MapUtils.defaultHeatMapLayerPaint, + ), + ); _completer.complete(); } - - Future 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 getBoundsFromPoint(Point 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/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 5495d21bd3..503f00cd78 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -1,3 +1,4 @@ +// ignore_for_file: experimental_member_use import 'dart:async'; import 'package:drift/drift.dart'; diff --git a/mobile/lib/infrastructure/repositories/map.repository.dart b/mobile/lib/infrastructure/repositories/map.repository.dart index 95e42337fc..03f42a024d 100644 --- a/mobile/lib/infrastructure/repositories/map.repository.dart +++ b/mobile/lib/infrastructure/repositories/map.repository.dart @@ -6,7 +6,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart' hide Marker; class DriftMapRepository extends DriftDatabaseRepository { final Drift _db; @@ -42,7 +42,7 @@ class DriftMapRepository extends DriftDatabaseRepository { Future> _watchMapMarker({ Expression Function($RemoteAssetEntityTable row)? assetFilter, - LatLngBounds? bounds, + LngLatBounds? bounds, }) async { final assetId = _db.remoteExifEntity.assetId; final latitude = _db.remoteExifEntity.latitude; @@ -66,20 +66,21 @@ class DriftMapRepository extends DriftDatabaseRepository { final rows = await query.get(); return List.generate(rows.length, (i) { final row = rows[i]; - return Marker(assetId: row.read(assetId)!, location: LatLng(row.read(latitude)!, row.read(longitude)!)); + return Marker( + assetId: row.read(assetId)!, + location: Geographic(lat: row.read(latitude)!, lon: row.read(longitude)!), + ); }, growable: false); } } extension MapBounds on $RemoteExifEntityTable { - Expression inBounds(LatLngBounds bounds) { - final southwest = bounds.southwest; - final northeast = bounds.northeast; - - final latInBounds = latitude.isBetweenValues(southwest.latitude, northeast.latitude); - final longInBounds = southwest.longitude <= northeast.longitude - ? longitude.isBetweenValues(southwest.longitude, northeast.longitude) - : (longitude.isBiggerOrEqualValue(southwest.longitude) | longitude.isSmallerOrEqualValue(northeast.longitude)); + Expression inBounds(LngLatBounds bounds) { + final latInBounds = latitude.isBetweenValues(bounds.latitudeSouth, bounds.latitudeNorth); + final longInBounds = bounds.longitudeWest <= bounds.longitudeEast + ? longitude.isBetweenValues(bounds.longitudeWest, bounds.longitudeEast) + : (longitude.isBiggerOrEqualValue(bounds.longitudeWest) | + longitude.isSmallerOrEqualValue(bounds.longitudeEast)); return latInBounds & longInBounds; } } diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index df4172df99..6c2b0e6177 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; class RemoteAssetRepository extends DriftDatabaseRepository { final Drift _db; @@ -170,12 +170,12 @@ class RemoteAssetRepository extends DriftDatabaseRepository { }); } - Future updateLocation(List ids, LatLng location) { + Future updateLocation(List ids, Geographic location) { return _db.batch((batch) async { for (final id in ids) { batch.update( _db.remoteExifEntity, - RemoteExifEntityCompanion(latitude: Value(location.latitude), longitude: Value(location.longitude)), + RemoteExifEntityCompanion(latitude: Value(location.lat), longitude: Value(location.lon)), where: (e) => e.assetId.equals(id), ); } diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 7544b4b2ac..d2393294e7 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -12,11 +12,11 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; import 'package:stream_transform/stream_transform.dart'; class TimelineMapOptions { - final LatLngBounds bounds; + final LngLatBounds bounds; final bool onlyFavorites; final bool includeArchived; final bool withPartners; diff --git a/mobile/lib/models/map/map_marker.model.dart b/mobile/lib/models/map/map_marker.model.dart index 0f425306ff..f1088dd93b 100644 --- a/mobile/lib/models/map/map_marker.model.dart +++ b/mobile/lib/models/map/map_marker.model.dart @@ -1,16 +1,16 @@ -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; import 'package:openapi/api.dart'; class MapMarker { - final LatLng latLng; + final Geographic latLng; final String assetRemoteId; const MapMarker({required this.latLng, required this.assetRemoteId}); - MapMarker copyWith({LatLng? latLng, String? assetRemoteId}) { + MapMarker copyWith({Geographic? 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; + MapMarker.fromDto(MapMarkerResponseDto dto) : latLng = Geographic(lat: dto.lat, lon: dto.lon), assetRemoteId = dto.id; @override String toString() => 'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)'; diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 99a534e9cf..3e2eb8bd78 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -17,7 +17,7 @@ import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/user_avatar.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; @RoutePage() class LibraryPage extends ConsumerWidget { @@ -325,7 +325,7 @@ class PlacesCollectionCard extends StatelessWidget { child: IgnorePointer( child: MapThumbnail( zoom: 8, - centre: const LatLng(21.44950, -157.91959), + centre: const Geographic(lat: 21.44950, lon: -157.91959), showAttribution: false, themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart index a4a6f66915..5057a2a78e 100644 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ b/mobile/lib/pages/library/places/places_collection.page.dart @@ -15,12 +15,12 @@ import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; @RoutePage() class PlacesCollectionPage extends HookConsumerWidget { const PlacesCollectionPage({super.key, this.currentLocation}); - final LatLng? currentLocation; + final Geographic? currentLocation; @override Widget build(BuildContext context, WidgetRef ref) { final places = ref.watch(getAllPlacesProvider); @@ -61,7 +61,7 @@ class PlacesCollectionPage extends HookConsumerWidget { child: MapThumbnail( onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)), zoom: 8, - centre: currentLocation ?? const LatLng(21.44950, -157.91959), + centre: currentLocation ?? const Geographic(lat: 21.44950, lon: -157.91959), showAttribution: false, themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index 993b91d8f7..8ab738b3e6 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; @@ -12,8 +11,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/latlngbounds_extension.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; -import 'package:immich_mobile/models/map/map_event.model.dart'; +import 'package:immich_mobile/models/map/map_event.model.dart' as app; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; @@ -26,25 +26,25 @@ import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/map_utils.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; import 'package:immich_mobile/widgets/map/map_app_bar.dart'; import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; @RoutePage() class MapPage extends HookConsumerWidget { const MapPage({super.key, this.initialLocation}); - final LatLng? initialLocation; + final Geographic? initialLocation; @override Widget build(BuildContext context, WidgetRef ref) { - final mapController = useRef(null); + final mapController = useRef(null); final markers = useRef>([]); final markersInBounds = useRef>([]); - final bottomSheetStreamController = useStreamController(); - final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null); + final bottomSheetStreamController = useStreamController(); + final selectedMarker = useValueNotifier(null); final assetsDebouncer = useDebouncer(); final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1)); final isLoading = useProcessingOverlay(); @@ -55,19 +55,17 @@ class MapPage extends HookConsumerWidget { // updates the markersInBounds value with the map markers that are visible in the current // map camera bounds - Future updateAssetsInBounds() async { - // Guard map not created - if (mapController.value == null) { - return; - } + void updateAssetsInBounds() { + if (mapController.value == null) return; - final bounds = await mapController.value!.getVisibleRegion(); + final bounds = mapController.value!.getVisibleRegion(); final inBounds = markers.value - .where((m) => bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude))) + .where((m) => bounds.contains(Geographic(lat: m.latLng.lat, lon: m.latLng.lon))) .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())); + bottomSheetStreamController.add(app.MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList())); } markersInBounds.value = inBounds; } @@ -99,57 +97,67 @@ class MapPage extends HookConsumerWidget { // 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); - }); - } + if (!current.shouldRefetchMarkers) return; + + markerDebouncer.run(() { + ref.invalidate(mapMarkersProvider); + // Reset marker + selectedMarker.value = null; + loadMarkers(); + ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false); + }); }); - // updates the selected markers position based on the current map camera - Future 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); + void selectMarker(MapMarker marker) { + selectedMarker.value = marker; } // finds the nearest asset marker from the tap point and store it as the selectedMarker - Future onMarkerClicked(Point point, LatLng _) 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)), + void onMarkerClicked(Offset point) { + if (mapController.value == null) return; + + final features = mapController.value!.featuresInRect( + Rect.fromCircle(center: point, radius: 50), + layerIds: [MapUtils.defaultHeatMapLayerId], ); + final featureId = features.firstOrNull?.id?.toString(); + + final marker = featureId != null + ? markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == featureId) + : null; + if (marker != null) { - await 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(const MapCloseBottomSheet()); - } - selectedMarker.value = null; + selectMarker(marker); + return; } + + if (selectedMarker.value == null) { + // If no asset was previously selected and no new asset is available, + // close the bottom sheet. + bottomSheetStreamController.add(const app.MapCloseBottomSheet()); + return; + } + + selectedMarker.value = null; } - void onMapCreated(MapLibreMapController controller) async { + void onMapCreated(MapController controller) { mapController.value = controller; - controller.addListener(() { - if (controller.isCameraMoving && selectedMarker.value != null) { - updateAssetMarkerPosition(selectedMarker.value!.marker, shouldAnimate: false); - } - }); + } + + void onMapEvent(MapEvent event) { + switch (event) { + case MapEventClick(): + onMarkerClicked(event.screenPoint); + case MapEventCameraIdle(): + assetsDebouncer.run(updateAssetsInBounds); + default: + } } Future onMarkerTapped() async { - final assetId = selectedMarker.value?.marker.assetRemoteId; + final assetId = selectedMarker.value?.assetRemoteId; if (assetId == null) { return; } @@ -171,14 +179,10 @@ class MapPage extends HookConsumerWidget { /// BOTTOM SHEET CALLBACKS - Future onMapMoved() async { - assetsDebouncer.run(updateAssetsInBounds); - } - void onBottomSheetScrolled(String assetRemoteId) { final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId); if (assetMarker != null) { - updateAssetMarkerPosition(assetMarker); + selectMarker(assetMarker); } } @@ -187,10 +191,11 @@ class MapPage extends HookConsumerWidget { 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); + final latlng = Geographic(lat: assetMarker.latLng.lat - offset, lon: assetMarker.latLng.lon); mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), + center: latlng, + zoom: mapZoomToAssetLevel, + nativeDuration: Durations.extralong2, ); } } @@ -211,8 +216,9 @@ class MapPage extends HookConsumerWidget { if (mapController.value != null && location != null) { await mapController.value!.animateCamera( - CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), + center: Geographic(lat: location.latitude, lon: location.longitude), + zoom: mapZoomToAssetLevel, + nativeDuration: Durations.extralong2, ); } } @@ -234,9 +240,8 @@ class MapPage extends HookConsumerWidget { style: style, selectedMarker: selectedMarker, onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, + onMapEvent: onMapEvent, + onStyleLoaded: (_) => reloadLayers(), onMarkerTapped: onMarkerTapped, ), // Should be a part of the body and not scaffold::bottomsheet for the @@ -266,9 +271,8 @@ class MapPage extends HookConsumerWidget { style: style, selectedMarker: selectedMarker, onMapCreated: onMapCreated, - onMapMoved: onMapMoved, - onMapClicked: onMarkerClicked, - onStyleLoaded: reloadLayers, + onMapEvent: onMapEvent, + onStyleLoaded: (_) => reloadLayers(), onMarkerTapped: onMarkerTapped, ), Positioned( @@ -302,32 +306,19 @@ class MapPage extends HookConsumerWidget { } } -class _AssetMarkerMeta { - final Point point; - final MapMarker marker; - final bool shouldAnimate; - - const _AssetMarkerMeta({required this.point, required this.marker, required this.shouldAnimate}); - - @override - String toString() => '_AssetMarkerMeta(point: $point, marker: $marker, shouldAnimate: $shouldAnimate)'; -} - class _MapWithMarker extends StatelessWidget { final AsyncValue style; - final MapCreatedCallback onMapCreated; - final OnCameraIdleCallback onMapMoved; - final OnMapClickCallback onMapClicked; - final OnStyleLoadedCallback onStyleLoaded; + final void Function(MapController) onMapCreated; + final void Function(MapEvent) onMapEvent; + final void Function(StyleController) onStyleLoaded; final Function()? onMarkerTapped; - final ValueNotifier<_AssetMarkerMeta?> selectedMarker; - final LatLng? initialLocation; + final ValueNotifier selectedMarker; + final Geographic? initialLocation; const _MapWithMarker({ required this.style, required this.onMapCreated, - required this.onMapMoved, - required this.onMapClicked, + required this.onMapEvent, required this.onStyleLoaded, required this.selectedMarker, this.onMarkerTapped, @@ -336,48 +327,44 @@ class _MapWithMarker extends StatelessWidget { @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( - attributionButtonMargins: const Point(8, kToolbarHeight), - initialCameraPosition: CameraPosition( - target: initialLocation ?? const LatLng(0, 0), - zoom: initialLocation != null ? 12 : 0, - ), - styleString: style, - // This is needed to update the selectedMarker's position on map camera updates - // The changes are notified through the mapController ValueListener which is added in [onMapCreated] - trackCameraPosition: true, - onMapCreated: onMapCreated, - onCameraIdle: onMapMoved, - onMapClick: onMapClicked, - onStyleLoadedCallback: onStyleLoaded, - tiltGesturesEnabled: false, - dragEnabled: false, - myLocationEnabled: false, - attributionButtonPosition: AttributionButtonPosition.topRight, - rotateGesturesEnabled: false, - ), - ), - ValueListenableBuilder( - valueListenable: selectedMarker, - builder: (ctx, value, _) => value != null - ? PositionedAssetMarkerIcon( - point: value.point, - assetRemoteId: value.marker.assetRemoteId, - assetThumbhash: '', - durationInMilliseconds: value.shouldAnimate ? 100 : 0, - onTap: onMarkerTapped, - ) - : const SizedBox.shrink(), - ), - ], + return style.widgetWhen( + onData: (style) => MapLibreMap( + options: MapOptions( + initCenter: initialLocation ?? const Geographic(lat: 0, lon: 0), + initZoom: initialLocation != null ? 12 : 0, + initStyle: style, + gestures: const MapGestures.all(pitch: false, rotate: false), ), + onMapCreated: onMapCreated, + onStyleLoaded: onStyleLoaded, + onEvent: onMapEvent, + children: [ + ValueListenableBuilder( + valueListenable: selectedMarker, + builder: (ctx, marker, _) => marker != null + ? WidgetLayer( + markers: [ + Marker( + point: marker.latLng, + size: const Size(100, 100), + alignment: Alignment.bottomCenter, + child: GestureDetector( + onTap: () => onMarkerTapped?.call(), + child: SizedBox.square( + dimension: 100, + child: AssetMarkerIcon( + id: marker.assetRemoteId, + thumbhash: '', + key: Key(marker.assetRemoteId), + ), + ), + ), + ), + ], + ) + : const SizedBox.shrink(), + ), + ], ), ); } diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index 3dace15ced..93feec8679 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -7,36 +5,34 @@ 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/utils/map_utils.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; @RoutePage() class MapLocationPickerPage extends HookConsumerWidget { - final LatLng initialLatLng; + final Geographic initialLatLng; - const MapLocationPickerPage({super.key, this.initialLatLng = const LatLng(0, 0)}); + const MapLocationPickerPage({super.key, this.initialLatLng = const Geographic(lat: 0, lon: 0)}); @override Widget build(BuildContext context, WidgetRef ref) { - final selectedLatLng = useValueNotifier(initialLatLng); - final controller = useRef(null); - final marker = useRef(null); + final selectedLatLng = useValueNotifier(initialLatLng); + final currentLatLng = useValueListenable(selectedLatLng); + final controller = useRef(null); - Future onStyleLoaded() async { - marker.value = await controller.value?.addMarkerAtLatLng(initialLatLng); + Future onStyleLoaded(StyleController style) async { + await style.addImageFromAssets(id: 'mapMarker', asset: 'assets/location-pin.png'); } - Future onMapClick(Point _, LatLng centre) async { - selectedLatLng.value = centre; - await controller.value?.animateCamera(CameraUpdate.newLatLng(centre)); - if (marker.value != null) { - await controller.value?.updateSymbol(marker.value!, SymbolOptions(geometry: centre)); - } + void onEvent(MapEvent event) { + if (event is! MapEventClick) return; + + selectedLatLng.value = event.point; + controller.value?.animateCamera(center: event.point); } - void onClose([LatLng? selected]) { + void onClose([Geographic? selected]) { context.maybePop(selected); } @@ -47,9 +43,9 @@ class MapLocationPickerPage extends HookConsumerWidget { return; } - var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude); + var currentLatLng = Geographic(lat: currentLocation.latitude, lon: currentLocation.longitude); selectedLatLng.value = currentLatLng; - await controller.value?.animateCamera(CameraUpdate.newLatLngZoom(currentLatLng, 12)); + await controller.value?.animateCamera(center: currentLatLng, zoom: 12); } return MapThemeOverride( @@ -66,18 +62,24 @@ class MapLocationPickerPage extends HookConsumerWidget { borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)), ), child: MapLibreMap( - initialCameraPosition: CameraPosition( - target: initialLatLng, - zoom: (initialLatLng.latitude == 0 && initialLatLng.longitude == 0) ? 1 : 12, + options: MapOptions( + initCenter: initialLatLng, + initZoom: (initialLatLng.lat == 0 && initialLatLng.lon == 0) ? 1 : 12, + initStyle: style, + gestures: const MapGestures.all(pitch: false), ), - styleString: style, onMapCreated: (mapController) => controller.value = mapController, - onStyleLoadedCallback: onStyleLoaded, - onMapClick: onMapClick, - dragEnabled: false, - tiltGesturesEnabled: false, - myLocationEnabled: false, - attributionButtonMargins: const Point(20, 15), + onStyleLoaded: onStyleLoaded, + onEvent: onEvent, + layers: [ + MarkerLayer( + points: [Feature(geometry: Point(currentLatLng))], + iconImage: 'mapMarker', + iconSize: 0.15, + iconAnchor: IconAnchor.bottom, + iconAllowOverlap: true, + ), + ], ), ), ), @@ -117,7 +119,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { } class _BottomBar extends StatelessWidget { - final ValueNotifier selectedLatLng; + final ValueNotifier selectedLatLng; final Function() onUseLocation; final Function() onGetCurrentLocation; @@ -140,8 +142,7 @@ class _BottomBar extends StatelessWidget { const SizedBox(width: 15), ValueListenableBuilder( valueListenable: selectedLatLng, - builder: (_, value, __) => - Text("${value.latitude.toStringAsFixed(4)}, ${value.longitude.toStringAsFixed(4)}"), + builder: (_, value, __) => Text("${value.lat.toStringAsFixed(4)}, ${value.lon.toStringAsFixed(4)}"), ), ], ), diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index 4708b5e615..fb981a6dc0 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -17,7 +17,7 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; @RoutePage() class DriftLibraryPage extends ConsumerWidget { @@ -230,7 +230,7 @@ class _PlacesCollectionCard extends StatelessWidget { child: IgnorePointer( child: MapThumbnail( zoom: 8, - centre: const LatLng(21.44950, -157.91959), + centre: const Geographic(lat: 21.44950, lon: -157.91959), showAttribution: false, themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), diff --git a/mobile/lib/presentation/pages/drift_map.page.dart b/mobile/lib/presentation/pages/drift_map.page.dart index 96384c97e5..843a756ca3 100644 --- a/mobile/lib/presentation/pages/drift_map.page.dart +++ b/mobile/lib/presentation/pages/drift_map.page.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/map/map.widget.dart'; import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; @RoutePage() class DriftMapPage extends StatelessWidget { - final LatLng? initialLocation; + final Geographic? initialLocation; const DriftMapPage({super.key, this.initialLocation}); diff --git a/mobile/lib/presentation/pages/drift_place.page.dart b/mobile/lib/presentation/pages/drift_place.page.dart index 10b9ca7ae4..61ce78434b 100644 --- a/mobile/lib/presentation/pages/drift_place.page.dart +++ b/mobile/lib/presentation/pages/drift_place.page.dart @@ -10,13 +10,13 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; @RoutePage() class DriftPlacePage extends StatelessWidget { const DriftPlacePage({super.key, this.currentLocation}); - final LatLng? currentLocation; + final Geographic? currentLocation; @override Widget build(BuildContext context) { @@ -82,7 +82,7 @@ class _Map extends StatelessWidget { const _Map({required this.search, this.currentLocation}); final ValueNotifier search; - final LatLng? currentLocation; + final Geographic? currentLocation; @override Widget build(BuildContext context) { @@ -96,7 +96,7 @@ class _Map extends StatelessWidget { child: MapThumbnail( onTap: (_, __) => context.pushRoute(DriftMapRoute(initialLocation: currentLocation)), zoom: 8, - centre: currentLocation ?? const LatLng(21.44950, -157.91959), + centre: currentLocation ?? const Geographic(lat: 21.44950, lon: -157.91959), showAttribution: false, themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart index 0665f4d46c..c0a1891025 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widge import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; class LocationDetails extends ConsumerStatefulWidget { const LocationDetails({super.key}); @@ -20,7 +20,7 @@ class LocationDetails extends ConsumerStatefulWidget { } class _LocationDetailsState extends ConsumerState { - MapLibreMapController? _mapController; + MapController? _mapController; String? _getLocationName(ExifInfo? exifInfo) { if (exifInfo == null) { @@ -36,14 +36,16 @@ class _LocationDetailsState extends ConsumerState { return null; } - void _onMapCreated(MapLibreMapController controller) { + void _onMapCreated(MapController controller) { _mapController = controller; } void _onExifChanged(AsyncValue? previous, AsyncValue current) { final currentExif = current.valueOrNull; if (currentExif != null && currentExif.hasCoordinates) { - _mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentExif.latitude!, currentExif.longitude!))); + _mapController?.moveCamera( + center: Geographic(lat: currentExif.latitude!, lon: currentExif.longitude!), + ); } } diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index bfd3011050..a6d25ea37e 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -7,11 +7,11 @@ import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/map.provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; class MapState { final ThemeMode themeMode; - final LatLngBounds bounds; + final LngLatBounds bounds; final bool onlyFavorites; final bool includeArchived; final bool withPartners; @@ -35,7 +35,7 @@ class MapState { int get hashCode => bounds.hashCode; MapState copyWith({ - LatLngBounds? bounds, + LngLatBounds? bounds, ThemeMode? themeMode, bool? onlyFavorites, bool? includeArchived, @@ -64,7 +64,7 @@ class MapState { class MapStateNotifier extends Notifier { MapStateNotifier(); - bool setBounds(LatLngBounds bounds) { + bool setBounds(LngLatBounds bounds) { if (state.bounds == bounds) { return false; } @@ -113,14 +113,14 @@ class MapStateNotifier extends Notifier { includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived), withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners), relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate), - bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)), + bounds: const LngLatBounds(longitudeWest: 0, longitudeEast: 0, latitudeSouth: 0, latitudeNorth: 0), ); } } // This provider watches the markers from the map service and serves the markers. // It should be used only after the map service provider is overridden -final mapMarkerProvider = FutureProvider.family, LatLngBounds?>((ref, bounds) async { +final mapMarkerProvider = FutureProvider.family, LngLatBounds?>((ref, bounds) async { final mapService = ref.watch(mapServiceProvider); final markers = await mapService.getMarkers(bounds); final features = List.filled(markers.length, const {}); @@ -131,7 +131,7 @@ final mapMarkerProvider = FutureProvider.family, LatLngBoun 'id': marker.assetId, 'geometry': { 'type': 'Point', - 'coordinates': [marker.location.longitude, marker.location.latitude], + 'coordinates': [marker.location.lon, marker.location.lat], }, }; } diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 72f4e8bda6..41d78fb7ef 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'dart:io'; -import 'dart:math'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -20,27 +19,10 @@ import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; - -class CustomSourceProperties implements SourceProperties { - final Map data; - const CustomSourceProperties({required this.data}); - - @override - Map toJson() { - return { - "type": "geojson", - "data": data, - // "cluster": true, - // "clusterRadius": 1, - // "clusterMinPoints": 5, - // "tolerance": 0.1, - }; - } -} +import 'package:maplibre/maplibre.dart'; class DriftMap extends ConsumerStatefulWidget { - final LatLng? initialLocation; + final Geographic? initialLocation; const DriftMap({super.key, this.initialLocation}); @@ -49,7 +31,7 @@ class DriftMap extends ConsumerStatefulWidget { } class _DriftMapState extends ConsumerState { - MapLibreMapController? mapController; + MapController? mapController; final _reloadMutex = AsyncMutex(); final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2)); final ValueNotifier bottomSheetOffset = ValueNotifier(0.25); @@ -69,7 +51,7 @@ class _DriftMapState extends ConsumerState { super.dispose(); } - void onMapCreated(MapLibreMapController controller) { + void onMapCreated(MapController controller) { mapController = controller; } @@ -81,43 +63,23 @@ class _DriftMapState extends ConsumerState { return; } - await controller.addSource( - MapUtils.defaultSourceId, - const CustomSourceProperties(data: {'type': 'FeatureCollection', 'features': []}), + await controller.style!.addSource( + GeoJsonSource(id: MapUtils.defaultSourceId, data: jsonEncode({'type': 'FeatureCollection', 'features': []})), ); - if (Platform.isAndroid) { - await controller.addCircleLayer( - MapUtils.defaultSourceId, - MapUtils.defaultHeatMapLayerId, - const CircleLayerProperties( - circleRadius: 10, - circleColor: "rgba(150,86,34,0.7)", - circleBlur: 1.0, - circleOpacity: 0.7, - circleStrokeWidth: 0.1, - circleStrokeColor: "rgba(203,46,19,0.5)", - circleStrokeOpacity: 0.7, - ), - ); - } - - if (Platform.isIOS) { - await controller.addHeatmapLayer( - MapUtils.defaultSourceId, - MapUtils.defaultHeatMapLayerId, - MapUtils.defaultHeatmapLayerProperties, - ); - } + await controller.style!.addLayer( + const HeatmapStyleLayer( + id: MapUtils.defaultHeatMapLayerId, + sourceId: MapUtils.defaultSourceId, + paint: MapUtils.defaultHeatmapLayerPaint, + ), + ); _debouncer.run(() => setBounds(forceReload: true)); - controller.addListener(onMapMoved); } - void onMapMoved() { - if (mapController!.isCameraMoving || !mounted) { - return; - } + void onMapEvent(MapEvent event) { + if (event is! MapEventCameraIdle || !mounted) return; _debouncer.run(setBounds); } @@ -136,7 +98,7 @@ class _DriftMapState extends ConsumerState { return; } - final bounds = await controller.getVisibleRegion(); + final bounds = controller.getVisibleRegion(); unawaited( _reloadMutex.run(() async { if (mounted && (ref.read(mapStateProvider.notifier).setBounds(bounds) || forceReload)) { @@ -153,7 +115,7 @@ class _DriftMapState extends ConsumerState { return; } - await controller.setGeoJsonSource(MapUtils.defaultSourceId, markers); + await controller.style!.updateGeoJsonSource(id: MapUtils.defaultSourceId, data: jsonEncode(markers)); } Future onZoomToLocation() async { @@ -173,8 +135,9 @@ class _DriftMapState extends ConsumerState { final controller = mapController; if (controller != null && location != null) { await controller.animateCamera( - CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), MapUtils.mapZoomToAssetLevel), - duration: const Duration(milliseconds: 800), + center: Geographic(lat: location.latitude, lon: location.longitude), + zoom: MapUtils.mapZoomToAssetLevel, + nativeDuration: Durations.extralong2, ); } } @@ -183,7 +146,12 @@ class _DriftMapState extends ConsumerState { Widget build(BuildContext context) { return Stack( children: [ - _Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady), + _Map( + initialLocation: widget.initialLocation, + onMapCreated: onMapCreated, + onMapReady: onMapReady, + onMapEvent: onMapEvent, + ), _DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset), _DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset), ], @@ -192,13 +160,13 @@ class _DriftMapState extends ConsumerState { } class _Map extends StatelessWidget { - final LatLng? initialLocation; + final Geographic? initialLocation; - const _Map({this.initialLocation, required this.onMapCreated, required this.onMapReady}); - - final MapCreatedCallback onMapCreated; + const _Map({this.initialLocation, required this.onMapCreated, required this.onMapReady, required this.onMapEvent}); + final void Function(MapController) onMapCreated; final VoidCallback onMapReady; + final void Function(MapEvent) onMapEvent; @override Widget build(BuildContext context) { @@ -206,16 +174,15 @@ class _Map extends StatelessWidget { return MapThemeOverride( mapBuilder: (style) => style.widgetWhen( onData: (style) => MapLibreMap( - initialCameraPosition: initialLocation == null - ? const CameraPosition(target: LatLng(0, 0), zoom: 0) - : CameraPosition(target: initialLocation, zoom: MapUtils.mapZoomToAssetLevel), - compassEnabled: false, - rotateGesturesEnabled: false, - styleString: style, + options: MapOptions( + initCenter: initialLocation ?? const Geographic(lat: 0, lon: 0), + initZoom: initialLocation == null ? 0 : MapUtils.mapZoomToAssetLevel, + initStyle: style, + gestures: const MapGestures.all(rotate: false), + ), onMapCreated: onMapCreated, - onStyleLoadedCallback: onMapReady, - attributionButtonPosition: AttributionButtonPosition.topRight, - attributionButtonMargins: const Point(8, kToolbarHeight), + onStyleLoaded: (_) => onMapReady(), + onEvent: onMapEvent, ), ), ); diff --git a/mobile/lib/presentation/widgets/map/map_utils.dart b/mobile/lib/presentation/widgets/map/map_utils.dart index 80df5995b6..b5716111e7 100644 --- a/mobile/lib/presentation/widgets/map/map_utils.dart +++ b/mobile/lib/presentation/widgets/map/map_utils.dart @@ -5,7 +5,6 @@ import 'package:geolocator/geolocator.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:logging/logging.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; class MapUtils { static final Logger _logger = Logger("MapUtils"); @@ -13,49 +12,37 @@ class MapUtils { static const mapZoomToAssetLevel = 12.0; static const defaultSourceId = 'asset-map-markers'; static const defaultHeatMapLayerId = 'asset-heatmap-layer'; - static var markerCompleter = Completer()..complete(); - - static const defaultCircleLayerLayerProperties = CircleLayerProperties( - circleRadius: 10, - circleColor: "rgba(150,86,34,0.7)", - circleBlur: 1.0, - circleOpacity: 0.7, - circleStrokeWidth: 0.1, - circleStrokeColor: "rgba(203,46,19,0.5)", - circleStrokeOpacity: 0.7, - ); - - static const defaultHeatmapLayerProperties = HeatmapLayerProperties( - heatmapColor: [ - Expressions.interpolate, - ["linear"], - ["heatmap-density"], + static const defaultHeatmapLayerPaint = { + 'heatmap-color': [ + 'interpolate', + ['linear'], + ['heatmap-density'], 0.0, - "rgba(103,58,183,0.0)", + 'rgba(103,58,183,0.0)', 0.3, - "rgb(103,58,183)", + 'rgb(103,58,183)', 0.5, - "rgb(33,149,243)", + 'rgb(33,149,243)', 0.7, - "rgb(76,175,79)", + 'rgb(76,175,79)', 0.95, - "rgb(255,235,59)", + 'rgb(255,235,59)', 1.0, - "rgb(255,86,34)", + 'rgb(255,86,34)', ], - heatmapIntensity: [ - Expressions.interpolate, - ["linear"], - [Expressions.zoom], + 'heatmap-intensity': [ + 'interpolate', + ['linear'], + ['zoom'], 0, 0.5, 9, 2, ], - heatmapRadius: [ - Expressions.interpolate, - ["linear"], - [Expressions.zoom], + 'heatmap-radius': [ + 'interpolate', + ['linear'], + ['zoom'], 0, 4, 4, @@ -63,8 +50,8 @@ class MapUtils { 9, 16, ], - heatmapOpacity: 0.7, - ); + 'heatmap-opacity': 0.7, + }; static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({ required BuildContext context, diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 011b1edc94..e8eff7d129 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/stack.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; import 'package:openapi/api.dart'; final assetApiRepositoryProvider = Provider( @@ -62,8 +62,8 @@ class AssetApiRepository extends ApiRepository { return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite)); } - Future updateLocation(List ids, LatLng location) async { - return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude)); + Future updateLocation(List ids, Geographic location) async { + return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.lat, longitude: location.lon)); } Future updateDateTime(List ids, DateTime dateTime) async { diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 81616f8880..a301d03254 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -123,7 +123,7 @@ import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; part 'router.gr.dart'; diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 86c52d90dc..b001bb4e14 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1226,7 +1226,7 @@ class DriftLockedFolderRoute extends PageRouteInfo { class DriftMapRoute extends PageRouteInfo { DriftMapRoute({ Key? key, - LatLng? initialLocation, + Geographic? initialLocation, List? children, }) : super( DriftMapRoute.name, @@ -1252,7 +1252,7 @@ class DriftMapRouteArgs { final Key? key; - final LatLng? initialLocation; + final Geographic? initialLocation; @override String toString() { @@ -1461,7 +1461,7 @@ class DriftPlaceDetailRouteArgs { class DriftPlaceRoute extends PageRouteInfo { DriftPlaceRoute({ Key? key, - LatLng? currentLocation, + Geographic? currentLocation, List? children, }) : super( DriftPlaceRoute.name, @@ -1490,7 +1490,7 @@ class DriftPlaceRouteArgs { final Key? key; - final LatLng? currentLocation; + final Geographic? currentLocation; @override String toString() { @@ -2011,7 +2011,7 @@ class MainTimelineRoute extends PageRouteInfo { class MapLocationPickerRoute extends PageRouteInfo { MapLocationPickerRoute({ Key? key, - LatLng initialLatLng = const LatLng(0, 0), + Geographic initialLatLng = const Geographic(lat: 0, lon: 0), List? children, }) : super( MapLocationPickerRoute.name, @@ -2041,12 +2041,12 @@ class MapLocationPickerRoute extends PageRouteInfo { class MapLocationPickerRouteArgs { const MapLocationPickerRouteArgs({ this.key, - this.initialLatLng = const LatLng(0, 0), + this.initialLatLng = const Geographic(lat: 0, lon: 0), }); final Key? key; - final LatLng initialLatLng; + final Geographic initialLatLng; @override String toString() { @@ -2057,12 +2057,15 @@ class MapLocationPickerRouteArgs { /// generated route for /// [MapPage] class MapRoute extends PageRouteInfo { - MapRoute({Key? key, LatLng? initialLocation, List? children}) - : super( - MapRoute.name, - args: MapRouteArgs(key: key, initialLocation: initialLocation), - initialChildren: children, - ); + MapRoute({ + Key? key, + Geographic? initialLocation, + List? children, + }) : super( + MapRoute.name, + args: MapRouteArgs(key: key, initialLocation: initialLocation), + initialChildren: children, + ); static const String name = 'MapRoute'; @@ -2082,7 +2085,7 @@ class MapRouteArgs { final Key? key; - final LatLng? initialLocation; + final Geographic? initialLocation; @override String toString() { @@ -2403,7 +2406,7 @@ class PinAuthRouteArgs { class PlacesCollectionRoute extends PageRouteInfo { PlacesCollectionRoute({ Key? key, - LatLng? currentLocation, + Geographic? currentLocation, List? children, }) : super( PlacesCollectionRoute.name, @@ -2435,7 +2438,7 @@ class PlacesCollectionRouteArgs { final Key? key; - final LatLng? currentLocation; + final Geographic? currentLocation; @override String toString() { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 3d3ef1494c..93707f6f56 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -22,7 +22,7 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/timezone.dart'; import 'package:immich_mobile/widgets/common/date_time_picker.dart'; import 'package:immich_mobile/widgets/common/location_picker.dart'; -import 'package:maplibre_gl/maplibre_gl.dart' as maplibre; +import 'package:maplibre/maplibre.dart' as maplibre; import 'package:riverpod_annotation/riverpod_annotation.dart'; final actionServiceProvider = Provider( @@ -131,12 +131,12 @@ class ActionService { } Future editLocation(List remoteIds, BuildContext context) async { - maplibre.LatLng? initialLatLng; + maplibre.Geographic? initialLatLng; if (remoteIds.length == 1) { final exif = await _remoteAssetRepository.getExif(remoteIds[0]); if (exif?.latitude != null && exif?.longitude != null) { - initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!); + initialLatLng = maplibre.Geographic(lat: exif!.latitude!, lon: exif.longitude!); } } diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index b9fab35442..6f52c55754 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -23,7 +23,7 @@ import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:logging/logging.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; import 'package:openapi/api.dart'; import 'package:immich_mobile/utils/debug_print.dart'; @@ -236,12 +236,12 @@ class AssetService { } } - Future?> changeLocation(List assets, LatLng location) async { + Future?> changeLocation(List assets, Geographic location) async { try { - await updateAssets(assets, UpdateAssetDto(latitude: location.latitude, longitude: location.longitude)); + await updateAssets(assets, UpdateAssetDto(latitude: location.lat, longitude: location.lon)); for (var element in assets) { - element.exifInfo = element.exifInfo?.copyWith(latitude: location.latitude, longitude: location.longitude); + element.exifInfo = element.exifInfo?.copyWith(latitude: location.lat, longitude: location.lon); } await _syncService.upsertAssetsWithExif(assets); diff --git a/mobile/lib/services/map.service.dart b/mobile/lib/services/map.service.dart index 5b50e8a890..51af90e884 100644 --- a/mobile/lib/services/map.service.dart +++ b/mobile/lib/services/map.service.dart @@ -1,23 +1,14 @@ import 'package:immich_mobile/mixins/error_logger.mixin.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/user_agent.dart'; import 'package:logging/logging.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; class MapService with ErrorLoggerMixin { final ApiService _apiService; @override final logger = Logger("MapService"); - MapService(this._apiService) { - _setMapUserAgentHeader(); - } - - Future _setMapUserAgentHeader() async { - final userAgent = await getUserAgentString(); - await setHttpHeaders({'User-Agent': userAgent}); - } + MapService(this._apiService); Future> getMapMarkers({ bool? isFavorite, diff --git a/mobile/lib/utils/map_utils.dart b/mobile/lib/utils/map_utils.dart index 6213b214a9..bc1edd5e92 100644 --- a/mobile/lib/utils/map_utils.dart +++ b/mobile/lib/utils/map_utils.dart @@ -6,7 +6,6 @@ import 'package:geolocator/geolocator.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:logging/logging.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; class MapUtils { const MapUtils._(); @@ -15,46 +14,53 @@ class MapUtils { static const defaultSourceId = 'asset-map-markers'; static const defaultHeatMapLayerId = 'asset-heatmap-layer'; - static const defaultHeatMapLayerProperties = HeatmapLayerProperties( - heatmapColor: [ - Expressions.interpolate, - ["linear"], - ["heatmap-density"], + static const defaultHeatMapLayerPaint = { + 'heatmap-color': [ + 'interpolate', + ['linear'], + ['heatmap-density'], 0.0, - "rgba(103,58,183,0.0)", + 'rgba(103,58,183,0.0)', 0.3, - "rgb(103,58,183)", + 'rgb(103,58,183)', 0.5, - "rgb(33,149,243)", + 'rgb(33,149,243)', 0.7, - "rgb(76,175,79)", + 'rgb(76,175,79)', 0.95, - "rgb(255,235,59)", + 'rgb(255,235,59)', 1.0, - "rgb(255,86,34)", + 'rgb(255,86,34)', ], - heatmapIntensity: [ - Expressions.interpolate, ["linear"], // - [Expressions.zoom], - 0, 0.5, - 9, 2, + 'heatmap-intensity': [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + 0.5, + 9, + 2, ], - heatmapRadius: [ - Expressions.interpolate, ["linear"], // - [Expressions.zoom], - 0, 4, - 4, 8, - 9, 16, + 'heatmap-radius': [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + 4, + 4, + 8, + 9, + 16, ], - heatmapOpacity: 0.7, - ); + 'heatmap-opacity': 0.7, + }; static Map _addFeature(MapMarker marker) => { 'type': 'Feature', 'id': marker.assetRemoteId, 'geometry': { 'type': 'Point', - 'coordinates': [marker.latLng.longitude, marker.latLng.latitude], + 'coordinates': [marker.latLng.lon, marker.latLng.lat], }, }; diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart index f0d333e262..f697f2bf41 100644 --- a/mobile/lib/utils/selection_handlers.dart +++ b/mobile/lib/utils/selection_handlers.dart @@ -14,7 +14,7 @@ import 'package:immich_mobile/widgets/common/date_time_picker.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/location_picker.dart'; import 'package:immich_mobile/widgets/common/share_dialog.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; void handleShareAssets(WidgetRef ref, BuildContext context, Iterable selection) { showDialog( @@ -105,12 +105,12 @@ Future handleEditDateTime(WidgetRef ref, BuildContext context, List } Future handleEditLocation(WidgetRef ref, BuildContext context, List selection) async { - LatLng? initialLatLng; + Geographic? initialLatLng; if (selection.length == 1) { final asset = selection.first; final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset); if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) { - initialLatLng = LatLng(assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!); + initialLatLng = Geographic(lat: assetWithExif.exifInfo!.latitude!, lon: assetWithExif.exifInfo!.longitude!); } } diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart index f48ee06fdd..bf42f30cc5 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; import 'package:url_launcher/url_launcher.dart'; class ExifMap extends StatelessWidget { @@ -15,7 +15,7 @@ class ExifMap extends StatelessWidget { // reusing this component final String? markerId; final String? markerAssetThumbhash; - final MapCreatedCallback? onMapCreated; + final void Function(MapController)? onMapCreated; const ExifMap({ super.key, @@ -66,7 +66,7 @@ class ExifMap extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { return MapThumbnail( - centre: LatLng(exifInfo.latitude ?? 0, exifInfo.longitude ?? 0), + centre: Geographic(lat: exifInfo.latitude ?? 0, lon: exifInfo.longitude ?? 0), height: 150, width: constraints.maxWidth, zoom: 12.0, diff --git a/mobile/lib/widgets/common/location_picker.dart b/mobile/lib/widgets/common/location_picker.dart index 4736b182ed..978bd408e9 100644 --- a/mobile/lib/widgets/common/location_picker.dart +++ b/mobile/lib/widgets/common/location_picker.dart @@ -6,10 +6,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; -Future showLocationPicker({required BuildContext context, LatLng? initialLatLng}) { - return showDialog( +Future showLocationPicker({required BuildContext context, Geographic? initialLatLng}) { + return showDialog( context: context, useRootNavigator: false, builder: (ctx) => _LocationPicker(initialLatLng: initialLatLng), @@ -17,7 +17,7 @@ Future showLocationPicker({required BuildContext context, LatLng? initi } class _LocationPicker extends HookWidget { - final LatLng? initialLatLng; + final Geographic? initialLatLng; const _LocationPicker({this.initialLatLng}); @@ -33,9 +33,9 @@ class _LocationPicker extends HookWidget { @override Widget build(BuildContext context) { - final latitude = useState(initialLatLng?.latitude ?? 0.0); - final longitude = useState(initialLatLng?.longitude ?? 0.0); - final latlng = LatLng(latitude.value, longitude.value); + final latitude = useState(initialLatLng?.lat ?? 0.0); + final longitude = useState(initialLatLng?.lon ?? 0.0); + final latlng = Geographic(lat: latitude.value, lon: longitude.value); final latitiudeFocusNode = useFocusNode(); final longitudeFocusNode = useFocusNode(); final latitudeController = useTextEditingController(text: latitude.value.toStringAsFixed(4)); @@ -48,10 +48,10 @@ class _LocationPicker extends HookWidget { }, [latitude.value, longitude.value]); Future onMapTap() async { - final newLatLng = await context.pushRoute(MapLocationPickerRoute(initialLatLng: latlng)); + final newLatLng = await context.pushRoute(MapLocationPickerRoute(initialLatLng: latlng)); if (newLatLng != null) { - latitude.value = newLatLng.latitude; - longitude.value = newLatLng.longitude; + latitude.value = newLatLng.lat; + longitude.value = newLatLng.lon; } } diff --git a/mobile/lib/widgets/map/map_asset_grid.dart b/mobile/lib/widgets/map/map_asset_grid.dart index b6c1e708a7..afb6f9c691 100644 --- a/mobile/lib/widgets/map/map_asset_grid.dart +++ b/mobile/lib/widgets/map/map_asset_grid.dart @@ -51,36 +51,35 @@ class MapAssetGrid extends HookConsumerWidget { final assetCache = useRef>({}); void handleMapEvents(MapEvent event) async { - if (event is MapAssetsInBoundsUpdated) { - final assetIds = event.assetRemoteIds; - final missingIds = []; - final currentAssets = []; + if (event is! MapAssetsInBoundsUpdated) return; - for (final id in assetIds) { - final asset = assetCache.value[id]; - if (asset != null) { - currentAssets.add(asset); - } else { - missingIds.add(id); - } + final assetIds = event.assetRemoteIds; + final missingIds = []; + final currentAssets = []; + + for (final id in assetIds) { + final asset = assetCache.value[id]; + if (asset != null) { + currentAssets.add(asset); + } else { + missingIds.add(id); } - - // Only fetch missing assets - if (missingIds.isNotEmpty) { - final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); - - // Add new assets to cache and current list - for (final asset in newAssets) { - if (asset.remoteId != null) { - assetCache.value[asset.remoteId!] = asset; - currentAssets.add(asset); - } - } - } - - assetsInBounds.value = currentAssets; - return; } + + // Only fetch missing assets + if (missingIds.isNotEmpty) { + final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds); + + // Add new assets to cache and current list + for (final asset in newAssets) { + if (asset.remoteId != null) { + assetCache.value[asset.remoteId!] = asset; + currentAssets.add(asset); + } + } + } + + assetsInBounds.value = currentAssets; } useOnStreamChange(mapEventStream, onData: handleMapEvents); diff --git a/mobile/lib/widgets/map/map_bottom_sheet.dart b/mobile/lib/widgets/map/map_bottom_sheet.dart index fba9e9a041..16e641e8db 100644 --- a/mobile/lib/widgets/map/map_bottom_sheet.dart +++ b/mobile/lib/widgets/map/map_bottom_sheet.dart @@ -33,13 +33,9 @@ class MapBottomSheet extends HookConsumerWidget { final isBottomSheetOpened = useRef(false); void handleMapEvents(MapEvent event) async { - if (event is MapCloseBottomSheet) { - await sheetController.animateTo( - 0.1, - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - ); - } + if (event is! MapCloseBottomSheet) return; + + await sheetController.animateTo(0.1, duration: const Duration(milliseconds: 200), curve: Curves.linearToEaseOut); } useOnStreamChange(mapEventStream, onData: handleMapEvents); diff --git a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart index 7866c0ecdc..fd5c79af1e 100644 --- a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart +++ b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; class MapThemePicker extends StatelessWidget { final ThemeMode themeMode; @@ -78,7 +78,7 @@ class _BorderedMapThumbnail extends StatelessWidget { ), child: MapThumbnail( zoom: 2, - centre: const LatLng(47, 5), + centre: const Geographic(lat: 47, lon: 5), onTap: (_, __) => onThemeChange(mode), themeMode: mode, showAttribution: false, diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart index 57f970b0d1..9e9c65059a 100644 --- a/mobile/lib/widgets/map/map_theme_override.dart +++ b/mobile/lib/widgets/map/map_theme_override.dart @@ -84,8 +84,13 @@ class _MapThemeOverrideState extends ConsumerState with Widget data: _isDarkTheme ? getThemeData(colorScheme: appTheme.dark, locale: locale) : getThemeData(colorScheme: appTheme.light, locale: locale), - child: widget.mapBuilder.call( - ref.watch(mapStateNotifierProvider.select((v) => _isDarkTheme ? v.darkStyleFetched : v.lightStyleFetched)), + // Key on _isDarkTheme to force MapLibreMap recreation on theme change, + // since initStyle is only applied on creation. + child: KeyedSubtree( + key: ValueKey(_isDarkTheme), + child: widget.mapBuilder.call( + ref.watch(mapStateNotifierProvider.select((v) => _isDarkTheme ? v.darkStyleFetched : v.lightStyleFetched)), + ), ), ); } diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart index 7defb52264..7dc281cc0c 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -1,14 +1,11 @@ -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/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; /// A non-interactive thumbnail of a map in the given coordinates with optional markers /// @@ -16,8 +13,8 @@ import 'package:maplibre_gl/maplibre_gl.dart'; /// [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, LatLng)? onTap; - final LatLng centre; + final Function(Offset, Geographic)? onTap; + final Geographic centre; final String? assetMarkerRemoteId; final String? assetThumbhash; final bool showMarkerPin; @@ -26,7 +23,7 @@ class MapThumbnail extends HookConsumerWidget { final double width; final ThemeMode? themeMode; final bool showAttribution; - final MapCreatedCallback? onCreated; + final void Function(MapController)? onCreated; const MapThumbnail({ super.key, @@ -45,28 +42,21 @@ class MapThumbnail extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final controller = useRef(null); final styleLoaded = useState(false); - Future onMapCreated(MapLibreMapController mapController) async { - controller.value = mapController; - styleLoaded.value = false; - onCreated?.call(mapController); - } - - Future onStyleLoaded() async { - try { - if (showMarkerPin && controller.value != null) { - await controller.value?.addMarkerAtLatLng(centre); - } - } finally { - // Calling methods on the controller after it is disposed will throw an error - // We do not have a way to check if the controller is disposed for now - // https://github.com/maplibre/flutter-maplibre-gl/issues/192 + Future onStyleLoaded(StyleController style) async { + if (showMarkerPin) { + await style.addImageFromAssets(id: 'mapMarker', asset: 'assets/location-pin.png'); } styleLoaded.value = true; } + void onEvent(MapEvent event) { + if (event is MapEventClick && onTap != null) { + onTap!(event.screenPoint, event.point); + } + } + return MapThemeOverride( themeMode: themeMode, mapBuilder: (style) => AnimatedContainer( @@ -80,37 +70,41 @@ class MapThumbnail extends HookConsumerWidget { width: width, child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(15)), - child: Stack( - alignment: AlignmentGeometry.topCenter, - children: [ - style.widgetWhen( - onData: (style) => MapLibreMap( - initialCameraPosition: CameraPosition(target: centre, 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, - ), + child: style.widgetWhen( + onData: (style) => MapLibreMap( + options: MapOptions( + initCenter: Geographic(lat: centre.lat + 0.002, lon: centre.lon), + initZoom: zoom, + initStyle: style, + gestures: const MapGestures.none(), ), - if (assetMarkerRemoteId != null && assetThumbhash != null) - Container( - width: width, - height: height / 2, - alignment: Alignment.bottomCenter, - child: SizedBox.square( - dimension: height / 2.5, - child: AssetMarkerIcon(id: assetMarkerRemoteId!, thumbhash: assetThumbhash!), + onMapCreated: onCreated, + onStyleLoaded: onStyleLoaded, + onEvent: onEvent, + layers: [ + if (showMarkerPin) + MarkerLayer( + points: [Feature(geometry: Point(centre))], + iconImage: 'mapMarker', + iconSize: 0.15, + iconAnchor: IconAnchor.bottom, + iconAllowOverlap: true, ), - ), - ], + ], + children: [ + if (assetMarkerRemoteId != null && assetThumbhash != null) + WidgetLayer( + markers: [ + Marker( + point: centre, + size: Size.square(height / 2), + alignment: Alignment.bottomCenter, + child: AssetMarkerIcon(id: assetMarkerRemoteId!, thumbhash: assetThumbhash!), + ), + ], + ), + ], + ), ), ), ), diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart deleted file mode 100644 index b6d7241cf4..0000000000 --- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; - -class PositionedAssetMarkerIcon extends StatelessWidget { - final Point point; - final String assetRemoteId; - final String assetThumbhash; - final double size; - final int durationInMilliseconds; - - final Function()? onTap; - - const PositionedAssetMarkerIcon({ - required this.point, - required this.assetRemoteId, - required this.assetThumbhash, - this.size = 100, - this.durationInMilliseconds = 100, - this.onTap, - super.key, - }); - - @override - Widget build(BuildContext context) { - final ratio = Platform.isIOS ? 1.0 : context.devicePixelRatio; - 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, thumbhash: assetThumbhash, key: Key(assetRemoteId)), - ), - ), - ); - } -} diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart index 7533e46f1a..cf284c7ce1 100644 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ b/mobile/lib/widgets/search/search_map_thumbnail.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; class SearchMapThumbnail extends StatelessWidget { const SearchMapThumbnail({super.key, this.size = 60.0}); @@ -20,7 +20,13 @@ class SearchMapThumbnail extends StatelessWidget { context.pushRoute(MapRoute()); }, child: IgnorePointer( - child: MapThumbnail(zoom: 2, centre: const LatLng(47, 5), height: size, width: size, showAttribution: false), + child: MapThumbnail( + zoom: 2, + centre: const Geographic(lat: 47, lon: 5), + height: size, + width: size, + showAttribution: false, + ), ), ); } diff --git a/mobile/mise.toml b/mobile/mise.toml index 88b8902053..5041171940 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -1,5 +1,5 @@ [tools] -flutter = "3.35.7" +flutter = "3.41.2" [tools."github:CQLabs/homebrew-dcm"] version = "1.30.0" diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 28adfc2ab7..25880441fe 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -273,6 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: @@ -776,6 +784,14 @@ packages: description: flutter source: sdk version: "0.0.0" + geobase: + dependency: transitive + description: + name: geobase + sha256: "3a4e2eb17a7ab452dda78fb45ee498a3ab02469ded749a5f4a9abea21fc95919" + url: "https://pub.dev" + source: hosted + version: "1.5.0" geoclue: dependency: transitive description: @@ -872,6 +888,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.1" + hooks: + dependency: transitive + description: + name: hooks + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" hooks_riverpod: dependency: "direct main" description: @@ -1125,6 +1149,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" local_auth: dependency: "direct main" description: @@ -1173,54 +1205,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - maplibre_gl: + maplibre: dependency: "direct main" description: - name: maplibre_gl - sha256: "5c7b1008396b2a321bada7d986ed60f9423406fbc7bd16f7ce91b385dfa054cd" + name: maplibre + sha256: "03aad98086ef8e24caf9abcbbacf43f7ceb6267a6b914d907f57fb05ccb65e09" url: "https://pub.dev" source: hosted - version: "0.22.0" - maplibre_gl_platform_interface: + version: "0.3.4" + maplibre_android: dependency: transitive description: - name: maplibre_gl_platform_interface - sha256: "08ee0a2d0853ea945a0ab619d52c0c714f43144145cd67478fc6880b52f37509" + name: maplibre_android + sha256: be8a9c29b20c10f4b2207790e8ab35489955a29cb69df10e38bdf993b44f1547 url: "https://pub.dev" source: hosted - version: "0.22.0" - maplibre_gl_web: + version: "0.3.4" + maplibre_ios: dependency: transitive description: - name: maplibre_gl_web - sha256: "2b13d4b1955a9a54e38a718f2324e56e4983c080fc6de316f6f4b5458baacb58" + name: maplibre_ios + sha256: "3e261d99697cc191e64ceb256acec5d96662d429059057bb6c1740dd11eaa7c3" url: "https://pub.dev" source: hosted - version: "0.22.0" + version: "0.3.4" + maplibre_platform_interface: + dependency: transitive + description: + name: maplibre_platform_interface + sha256: "7d912d82d41a31daed4e91a243a1324f483f7ffbf3671c229b98328beab854b4" + url: "https://pub.dev" + source: hosted + version: "0.3.4" + maplibre_web: + dependency: transitive + description: + name: maplibre_web + sha256: "7e79427cdb0098c1e054fb90f5f35f4538c69e8d170c0e92f6abe7c61e106461" + url: "https://pub.dev" + source: hosted + version: "0.3.4" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" mime: dependency: transitive description: @@ -1237,6 +1293,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" native_video_player: dependency: "direct main" description: @@ -1477,6 +1541,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" + url: "https://pub.dev" + source: hosted + version: "0.10.1+2" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: "03c5fa5896080963ab4917eeffda8d28c90f22863a496fb5ba13bc10943e40e4" + url: "https://pub.dev" + source: hosted + version: "0.10.1+1" + pointer_interceptor_platform_interface: + dependency: transitive + description: + name: pointer_interceptor_platform_interface + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" + url: "https://pub.dev" + source: hosted + version: "0.10.0+1" + pointer_interceptor_web: + dependency: transitive + description: + name: pointer_interceptor_web + sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a" + url: "https://pub.dev" + source: hosted + version: "0.10.3" pool: dependency: transitive description: @@ -1501,6 +1597,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" protobuf: dependency: transitive description: @@ -1910,10 +2014,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.9" thumbhash: dependency: "direct main" description: @@ -1954,6 +2058,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" universal_io: dependency: transitive description: @@ -2162,6 +2274,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" worker_manager: dependency: "direct main" description: @@ -2203,5 +2323,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.7" + dart: ">=3.11.0 <4.0.0" + flutter: "3.41.2" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 0b54dfc53e..7db8362917 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -5,8 +5,8 @@ publish_to: 'none' version: 2.5.6+3037 environment: - sdk: '>=3.8.0 <4.0.0' - flutter: 3.35.7 + sdk: '>=3.11.0 <4.0.0' + flutter: 3.41.2 dependencies: async: ^2.13.0 @@ -52,7 +52,7 @@ dependencies: isar_community_flutter_libs: 3.3.0-dev.3 local_auth: ^2.3.0 logging: ^1.3.0 - maplibre_gl: ^0.22.0 + maplibre: ^0.3.4 native_video_player: git: diff --git a/mobile/test/services/asset.service_test.dart b/mobile/test/services/asset.service_test.dart index b741150165..80f55ea993 100644 --- a/mobile/test/services/asset.service_test.dart +++ b/mobile/test/services/asset.service_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/services/asset.service.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:maplibre/maplibre.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; @@ -85,9 +85,9 @@ void main() { expect(receivedDatetime.every((d) => d == dateTime), isTrue); }); - test("asset is updated with LatLng", () async { + test("asset is updated with Geographic", () async { final assets = [AssetStub.image1, AssetStub.image2]; - final latLng = const LatLng(37.7749, -122.4194); + final latLng = const Geographic(lat: 37.7749, lon: -122.4194); await sut.changeLocation(assets, latLng); verify(() => assetsApi.updateAssets(any())).called(1); @@ -95,7 +95,7 @@ void main() { upsertExifCallback.called(1); final receivedAssets = upsertExifCallback.captured.firstOrNull as List? ?? []; final receivedCoords = receivedAssets.cast().map( - (a) => LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0), + (a) => Geographic(lat: a.exifInfo?.latitude ?? 0, lon: a.exifInfo?.longitude ?? 0), ); expect(receivedCoords.every((l) => l == latLng), isTrue); }); diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index f778c20afb..a29c41e8da 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -59,7 +59,7 @@ RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \ # Flutter SDK # https://flutter.dev/docs/development/tools/sdk/releases?tab=linux ENV FLUTTER_CHANNEL="stable" -ENV FLUTTER_VERSION="3.35.7" +ENV FLUTTER_VERSION="3.41.2" ENV FLUTTER_HOME=/flutter ENV PATH=${PATH}:${FLUTTER_HOME}/bin