mirror of
https://github.com/immich-app/immich.git
synced 2026-03-22 16:19:24 +03:00
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.
This commit is contained in:
@@ -15,7 +15,7 @@ config_roots = [
|
|||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.13.1"
|
node = "24.13.1"
|
||||||
flutter = "3.35.7"
|
flutter = "3.41.2"
|
||||||
pnpm = "10.29.3"
|
pnpm = "10.29.3"
|
||||||
terragrunt = "0.98.0"
|
terragrunt = "0.98.0"
|
||||||
opentofu = "1.11.4"
|
opentofu = "1.11.4"
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ PODS:
|
|||||||
- local_auth_darwin (0.0.1):
|
- local_auth_darwin (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- MapLibre (6.14.0)
|
- MapLibre (6.23.0)
|
||||||
- maplibre_gl (0.0.1):
|
- maplibre_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- MapLibre (= 6.14.0)
|
- MapLibre (~> 6.21)
|
||||||
- native_video_player (1.0.0):
|
- native_video_player (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- network_info_plus (0.0.1):
|
- network_info_plus (0.0.1):
|
||||||
@@ -58,6 +58,8 @@ PODS:
|
|||||||
- photo_manager (3.7.1):
|
- photo_manager (3.7.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- pointer_interceptor_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
- share_handler_ios (0.0.14):
|
- share_handler_ios (0.0.14):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -75,16 +77,16 @@ PODS:
|
|||||||
- sqflite_darwin (0.0.4):
|
- sqflite_darwin (0.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqlite3 (3.49.1):
|
- sqlite3 (3.49.2):
|
||||||
- sqlite3/common (= 3.49.1)
|
- sqlite3/common (= 3.49.2)
|
||||||
- sqlite3/common (3.49.1)
|
- sqlite3/common (3.49.2)
|
||||||
- sqlite3/dbstatvtab (3.49.1):
|
- sqlite3/dbstatvtab (3.49.2):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/fts5 (3.49.1):
|
- sqlite3/fts5 (3.49.2):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/perf-threadsafe (3.49.1):
|
- sqlite3/perf-threadsafe (3.49.2):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/rtree (3.49.1):
|
- sqlite3/rtree (3.49.2):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3_flutter_libs (0.0.1):
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -118,7 +120,7 @@ DEPENDENCIES:
|
|||||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
- isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`)
|
- isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`)
|
||||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
- 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`)
|
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
||||||
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
||||||
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||||
@@ -126,6 +128,7 @@ DEPENDENCIES:
|
|||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/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 (from `.symlinks/plugins/share_handler_ios/ios`)
|
||||||
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
@@ -178,8 +181,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/isar_community_flutter_libs/ios"
|
:path: ".symlinks/plugins/isar_community_flutter_libs/ios"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||||
maplibre_gl:
|
maplibre_ios:
|
||||||
:path: ".symlinks/plugins/maplibre_gl/ios"
|
:path: ".symlinks/plugins/maplibre_ios/ios"
|
||||||
native_video_player:
|
native_video_player:
|
||||||
:path: ".symlinks/plugins/native_video_player/ios"
|
:path: ".symlinks/plugins/native_video_player/ios"
|
||||||
network_info_plus:
|
network_info_plus:
|
||||||
@@ -194,6 +197,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
|
pointer_interceptor_ios:
|
||||||
|
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||||
share_handler_ios:
|
share_handler_ios:
|
||||||
:path: ".symlinks/plugins/share_handler_ios/ios"
|
:path: ".symlinks/plugins/share_handler_ios/ios"
|
||||||
share_handler_ios_models:
|
share_handler_ios_models:
|
||||||
@@ -230,8 +235,8 @@ SPEC CHECKSUMS:
|
|||||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||||
isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d
|
isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d
|
||||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||||
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
|
MapLibre: c0fcafabb341f230657d959970c6eb47fb55750e
|
||||||
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
|
maplibre_ios: 05031d5f79702672d2c01cc77b6ba3187d4bf896
|
||||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
||||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||||
@@ -239,13 +244,14 @@ SPEC CHECKSUMS:
|
|||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
||||||
|
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
|
||||||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
||||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
|
|||||||
@@ -446,6 +446,7 @@
|
|||||||
packageReferences = (
|
packageReferences = (
|
||||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
||||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
||||||
|
A1B2C3D4E5F6A7B8C9D0E1F2 /* XCRemoteSwiftPackageReference "maplibre-gl-native-distribution" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
@@ -1250,6 +1251,14 @@
|
|||||||
minimumVersion = 1.5.0;
|
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 */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
|||||||
@@ -10,6 +10,15 @@
|
|||||||
"version" : "1.0.3"
|
"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",
|
"identity" : "grdb.swift",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
class Marker {
|
class Marker {
|
||||||
final LatLng location;
|
final Geographic location;
|
||||||
final String assetId;
|
final String assetId;
|
||||||
|
|
||||||
const Marker({required this.location, required this.assetId});
|
const Marker({required this.location, required this.assetId});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:immich_mobile/domain/models/map.model.dart';
|
import 'package:immich_mobile/domain/models/map.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart' hide Marker;
|
||||||
|
|
||||||
typedef MapMarkerSource = Future<List<Marker>> Function(LatLngBounds? bounds);
|
typedef MapMarkerSource = Future<List<Marker>> Function(LngLatBounds? bounds);
|
||||||
|
|
||||||
typedef MapQuery = ({MapMarkerSource markerSource});
|
typedef MapQuery = ({MapMarkerSource markerSource});
|
||||||
|
|
||||||
@@ -21,5 +21,5 @@ class MapService {
|
|||||||
|
|
||||||
MapService(MapQuery query) : _markerSource = query.markerSource;
|
MapService(MapQuery query) : _markerSource = query.markerSource;
|
||||||
|
|
||||||
Future<List<Marker>> Function(LatLngBounds? bounds) get getMarkers => _markerSource;
|
Future<List<Marker>> Function(LngLatBounds? bounds) get getMarkers => _markerSource;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
/// Checks whether [point] is inside bounds
|
||||||
bool contains(LatLng point) {
|
bool contains(Geographic point) {
|
||||||
final sw = point;
|
return containsBounds(
|
||||||
final ne = point;
|
LngLatBounds(
|
||||||
return containsBounds(LatLngBounds(southwest: sw, northeast: ne));
|
longitudeWest: point.lon,
|
||||||
|
longitudeEast: point.lon,
|
||||||
|
latitudeSouth: point.lat,
|
||||||
|
latitudeNorth: point.lat,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether [bounds] is contained inside bounds
|
/// Checks whether [bounds] is contained inside bounds
|
||||||
bool containsBounds(LatLngBounds bounds) {
|
bool containsBounds(LngLatBounds bounds) {
|
||||||
final sw = bounds.southwest;
|
return (bounds.latitudeSouth >= latitudeSouth) &&
|
||||||
final ne = bounds.northeast;
|
(bounds.latitudeNorth <= latitudeNorth) &&
|
||||||
return (sw.latitude >= southwest.latitude) &&
|
(bounds.longitudeWest >= longitudeWest) &&
|
||||||
(ne.latitude <= northeast.latitude) &&
|
(bounds.longitudeEast <= longitudeEast);
|
||||||
(sw.longitude >= southwest.longitude) &&
|
|
||||||
(ne.longitude <= northeast.longitude);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
import 'package:immich_mobile/utils/map_utils.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();
|
static var _completer = Completer()..complete();
|
||||||
|
|
||||||
Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
|
Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
|
||||||
return addSource(
|
return style!.addSource(
|
||||||
MapUtils.defaultSourceId,
|
GeoJsonSource(
|
||||||
GeojsonSourceProperties(data: MapUtils.generateGeoJsonForMarkers(markers.toList())),
|
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
|
// !! Make sure to remove layers before sources else the native
|
||||||
// maplibre library would crash when removing the source saying that
|
// maplibre library would crash when removing the source saying that
|
||||||
// the source is still in use
|
// the source is still in use
|
||||||
final existingLayers = await getLayerIds();
|
try {
|
||||||
if (existingLayers.contains(MapUtils.defaultHeatMapLayerId)) {
|
await style!.removeLayer(MapUtils.defaultHeatMapLayerId);
|
||||||
await removeLayer(MapUtils.defaultHeatMapLayerId);
|
} catch (_) {
|
||||||
|
// Layer may not exist
|
||||||
}
|
}
|
||||||
|
|
||||||
final existingSources = await getSourceIds();
|
try {
|
||||||
if (existingSources.contains(MapUtils.defaultSourceId)) {
|
await style!.removeSource(MapUtils.defaultSourceId);
|
||||||
await removeSource(MapUtils.defaultSourceId);
|
} catch (_) {
|
||||||
|
// Source may not exist
|
||||||
}
|
}
|
||||||
|
|
||||||
await addGeoJSONSourceForMarkers(markers);
|
await addGeoJSONSourceForMarkers(markers);
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
await style!.addLayer(
|
||||||
await addCircleLayer(
|
const HeatmapStyleLayer(
|
||||||
MapUtils.defaultSourceId,
|
id: MapUtils.defaultHeatMapLayerId,
|
||||||
MapUtils.defaultHeatMapLayerId,
|
sourceId: MapUtils.defaultSourceId,
|
||||||
const CircleLayerProperties(
|
paint: MapUtils.defaultHeatMapLayerPaint,
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_completer.complete();
|
_completer.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Symbol?> addMarkerAtLatLng(LatLng centre) async {
|
|
||||||
// no marker is displayed if asset-path is incorrect
|
|
||||||
try {
|
|
||||||
final ByteData bytes = await rootBundle.load("assets/location-pin.png");
|
|
||||||
await addImage("mapMarker", bytes.buffer.asUint8List());
|
|
||||||
return addSymbol(SymbolOptions(geometry: centre, iconImage: "mapMarker", iconSize: 0.15, iconAnchor: "bottom"));
|
|
||||||
} finally {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<LatLngBounds> getBoundsFromPoint(Point<double> point, double distance) async {
|
|
||||||
final southWestPx = Point(point.x - distance, point.y + distance);
|
|
||||||
final northEastPx = Point(point.x + distance, point.y - distance);
|
|
||||||
|
|
||||||
final southWest = await toLatLng(southWestPx);
|
|
||||||
final northEast = await toLatLng(northEastPx);
|
|
||||||
|
|
||||||
return LatLngBounds(southwest: southWest, northeast: northEast);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// ignore_for_file: experimental_member_use
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.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/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
import 'package: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 {
|
class DriftMapRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
@@ -42,7 +42,7 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
Future<List<Marker>> _watchMapMarker({
|
Future<List<Marker>> _watchMapMarker({
|
||||||
Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter,
|
Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter,
|
||||||
LatLngBounds? bounds,
|
LngLatBounds? bounds,
|
||||||
}) async {
|
}) async {
|
||||||
final assetId = _db.remoteExifEntity.assetId;
|
final assetId = _db.remoteExifEntity.assetId;
|
||||||
final latitude = _db.remoteExifEntity.latitude;
|
final latitude = _db.remoteExifEntity.latitude;
|
||||||
@@ -66,20 +66,21 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
|||||||
final rows = await query.get();
|
final rows = await query.get();
|
||||||
return List.generate(rows.length, (i) {
|
return List.generate(rows.length, (i) {
|
||||||
final row = rows[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);
|
}, growable: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MapBounds on $RemoteExifEntityTable {
|
extension MapBounds on $RemoteExifEntityTable {
|
||||||
Expression<bool> inBounds(LatLngBounds bounds) {
|
Expression<bool> inBounds(LngLatBounds bounds) {
|
||||||
final southwest = bounds.southwest;
|
final latInBounds = latitude.isBetweenValues(bounds.latitudeSouth, bounds.latitudeNorth);
|
||||||
final northeast = bounds.northeast;
|
final longInBounds = bounds.longitudeWest <= bounds.longitudeEast
|
||||||
|
? longitude.isBetweenValues(bounds.longitudeWest, bounds.longitudeEast)
|
||||||
final latInBounds = latitude.isBetweenValues(southwest.latitude, northeast.latitude);
|
: (longitude.isBiggerOrEqualValue(bounds.longitudeWest) |
|
||||||
final longInBounds = southwest.longitude <= northeast.longitude
|
longitude.isSmallerOrEqualValue(bounds.longitudeEast));
|
||||||
? longitude.isBetweenValues(southwest.longitude, northeast.longitude)
|
|
||||||
: (longitude.isBiggerOrEqualValue(southwest.longitude) | longitude.isSmallerOrEqualValue(northeast.longitude));
|
|
||||||
return latInBounds & longInBounds;
|
return latInBounds & longInBounds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.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 {
|
class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
@@ -170,12 +170,12 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, LatLng location) {
|
Future<void> updateLocation(List<String> ids, Geographic location) {
|
||||||
return _db.batch((batch) async {
|
return _db.batch((batch) async {
|
||||||
for (final id in ids) {
|
for (final id in ids) {
|
||||||
batch.update(
|
batch.update(
|
||||||
_db.remoteExifEntity,
|
_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),
|
where: (e) => e.assetId.equals(id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/map.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';
|
import 'package:stream_transform/stream_transform.dart';
|
||||||
|
|
||||||
class TimelineMapOptions {
|
class TimelineMapOptions {
|
||||||
final LatLngBounds bounds;
|
final LngLatBounds bounds;
|
||||||
final bool onlyFavorites;
|
final bool onlyFavorites;
|
||||||
final bool includeArchived;
|
final bool includeArchived;
|
||||||
final bool withPartners;
|
final bool withPartners;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class MapMarker {
|
class MapMarker {
|
||||||
final LatLng latLng;
|
final Geographic latLng;
|
||||||
final String assetRemoteId;
|
final String assetRemoteId;
|
||||||
const MapMarker({required this.latLng, required this.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);
|
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
|
@override
|
||||||
String toString() => 'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)';
|
String toString() => 'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)';
|
||||||
|
|||||||
@@ -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/immich_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class LibraryPage extends ConsumerWidget {
|
class LibraryPage extends ConsumerWidget {
|
||||||
@@ -325,7 +325,7 @@ class PlacesCollectionCard extends StatelessWidget {
|
|||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
centre: const LatLng(21.44950, -157.91959),
|
centre: const Geographic(lat: 21.44950, lon: -157.91959),
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class PlacesCollectionPage extends HookConsumerWidget {
|
class PlacesCollectionPage extends HookConsumerWidget {
|
||||||
const PlacesCollectionPage({super.key, this.currentLocation});
|
const PlacesCollectionPage({super.key, this.currentLocation});
|
||||||
final LatLng? currentLocation;
|
final Geographic? currentLocation;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final places = ref.watch(getAllPlacesProvider);
|
final places = ref.watch(getAllPlacesProvider);
|
||||||
@@ -61,7 +61,7 @@ class PlacesCollectionPage extends HookConsumerWidget {
|
|||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)),
|
onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)),
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
centre: currentLocation ?? const LatLng(21.44950, -157.91959),
|
centre: currentLocation ?? const Geographic(lat: 21.44950, lon: -157.91959),
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:collection/collection.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/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
|
||||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.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/models/map/map_marker.model.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.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/utils/map_utils.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.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/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_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_asset_grid.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_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.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/maplibre.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MapPage extends HookConsumerWidget {
|
class MapPage extends HookConsumerWidget {
|
||||||
const MapPage({super.key, this.initialLocation});
|
const MapPage({super.key, this.initialLocation});
|
||||||
final LatLng? initialLocation;
|
final Geographic? initialLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final mapController = useRef<MapLibreMapController?>(null);
|
final mapController = useRef<MapController?>(null);
|
||||||
final markers = useRef<List<MapMarker>>([]);
|
final markers = useRef<List<MapMarker>>([]);
|
||||||
final markersInBounds = useRef<List<MapMarker>>([]);
|
final markersInBounds = useRef<List<MapMarker>>([]);
|
||||||
final bottomSheetStreamController = useStreamController<MapEvent>();
|
final bottomSheetStreamController = useStreamController<app.MapEvent>();
|
||||||
final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null);
|
final selectedMarker = useValueNotifier<MapMarker?>(null);
|
||||||
final assetsDebouncer = useDebouncer();
|
final assetsDebouncer = useDebouncer();
|
||||||
final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1));
|
final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1));
|
||||||
final isLoading = useProcessingOverlay();
|
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
|
// updates the markersInBounds value with the map markers that are visible in the current
|
||||||
// map camera bounds
|
// map camera bounds
|
||||||
Future<void> updateAssetsInBounds() async {
|
void updateAssetsInBounds() {
|
||||||
// Guard map not created
|
if (mapController.value == null) return;
|
||||||
if (mapController.value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final bounds = await mapController.value!.getVisibleRegion();
|
final bounds = mapController.value!.getVisibleRegion();
|
||||||
final inBounds = markers.value
|
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();
|
.toList();
|
||||||
|
|
||||||
// Notify bottom sheet to update asset grid only when there are new assets
|
// Notify bottom sheet to update asset grid only when there are new assets
|
||||||
if (markersInBounds.value.length != inBounds.length) {
|
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;
|
markersInBounds.value = inBounds;
|
||||||
}
|
}
|
||||||
@@ -99,57 +97,67 @@ class MapPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Refetch markers when map state is changed
|
// Refetch markers when map state is changed
|
||||||
ref.listen(mapStateNotifierProvider, (_, current) {
|
ref.listen(mapStateNotifierProvider, (_, current) {
|
||||||
if (current.shouldRefetchMarkers) {
|
if (!current.shouldRefetchMarkers) return;
|
||||||
markerDebouncer.run(() {
|
|
||||||
ref.invalidate(mapMarkersProvider);
|
markerDebouncer.run(() {
|
||||||
// Reset marker
|
ref.invalidate(mapMarkersProvider);
|
||||||
selectedMarker.value = null;
|
// Reset marker
|
||||||
loadMarkers();
|
selectedMarker.value = null;
|
||||||
ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false);
|
loadMarkers();
|
||||||
});
|
ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// updates the selected markers position based on the current map camera
|
void selectMarker(MapMarker marker) {
|
||||||
Future<void> updateAssetMarkerPosition(MapMarker marker, {bool shouldAnimate = true}) async {
|
selectedMarker.value = marker;
|
||||||
final assetPoint = await mapController.value!.toScreenLocation(marker.latLng);
|
|
||||||
selectedMarker.value = _AssetMarkerMeta(point: assetPoint, marker: marker, shouldAnimate: shouldAnimate);
|
|
||||||
(assetPoint, marker, shouldAnimate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// finds the nearest asset marker from the tap point and store it as the selectedMarker
|
// finds the nearest asset marker from the tap point and store it as the selectedMarker
|
||||||
Future<void> onMarkerClicked(Point<double> point, LatLng _) async {
|
void onMarkerClicked(Offset point) {
|
||||||
// Guard map not created
|
if (mapController.value == null) return;
|
||||||
if (mapController.value == null) {
|
|
||||||
return;
|
final features = mapController.value!.featuresInRect(
|
||||||
}
|
Rect.fromCircle(center: point, radius: 50),
|
||||||
final latlngBound = await mapController.value!.getBoundsFromPoint(point, 50);
|
layerIds: [MapUtils.defaultHeatMapLayerId],
|
||||||
final marker = markersInBounds.value.firstWhereOrNull(
|
|
||||||
(m) => latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final featureId = features.firstOrNull?.id?.toString();
|
||||||
|
|
||||||
|
final marker = featureId != null
|
||||||
|
? markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == featureId)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (marker != null) {
|
if (marker != null) {
|
||||||
await updateAssetMarkerPosition(marker);
|
selectMarker(marker);
|
||||||
} else {
|
return;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
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<void> onMarkerTapped() async {
|
Future<void> onMarkerTapped() async {
|
||||||
final assetId = selectedMarker.value?.marker.assetRemoteId;
|
final assetId = selectedMarker.value?.assetRemoteId;
|
||||||
if (assetId == null) {
|
if (assetId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -171,14 +179,10 @@ class MapPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
/// BOTTOM SHEET CALLBACKS
|
/// BOTTOM SHEET CALLBACKS
|
||||||
|
|
||||||
Future<void> onMapMoved() async {
|
|
||||||
assetsDebouncer.run(updateAssetsInBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onBottomSheetScrolled(String assetRemoteId) {
|
void onBottomSheetScrolled(String assetRemoteId) {
|
||||||
final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId);
|
final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId);
|
||||||
if (assetMarker != null) {
|
if (assetMarker != null) {
|
||||||
updateAssetMarkerPosition(assetMarker);
|
selectMarker(assetMarker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,10 +191,11 @@ class MapPage extends HookConsumerWidget {
|
|||||||
if (mapController.value != null && assetMarker != null) {
|
if (mapController.value != null && assetMarker != null) {
|
||||||
// Offset the latitude a little to show the marker just above the viewports center
|
// Offset the latitude a little to show the marker just above the viewports center
|
||||||
final offset = context.isMobile ? 0.02 : 0;
|
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(
|
mapController.value!.animateCamera(
|
||||||
CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel),
|
center: latlng,
|
||||||
duration: const Duration(milliseconds: 800),
|
zoom: mapZoomToAssetLevel,
|
||||||
|
nativeDuration: Durations.extralong2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,8 +216,9 @@ class MapPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (mapController.value != null && location != null) {
|
if (mapController.value != null && location != null) {
|
||||||
await mapController.value!.animateCamera(
|
await mapController.value!.animateCamera(
|
||||||
CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel),
|
center: Geographic(lat: location.latitude, lon: location.longitude),
|
||||||
duration: const Duration(milliseconds: 800),
|
zoom: mapZoomToAssetLevel,
|
||||||
|
nativeDuration: Durations.extralong2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,9 +240,8 @@ class MapPage extends HookConsumerWidget {
|
|||||||
style: style,
|
style: style,
|
||||||
selectedMarker: selectedMarker,
|
selectedMarker: selectedMarker,
|
||||||
onMapCreated: onMapCreated,
|
onMapCreated: onMapCreated,
|
||||||
onMapMoved: onMapMoved,
|
onMapEvent: onMapEvent,
|
||||||
onMapClicked: onMarkerClicked,
|
onStyleLoaded: (_) => reloadLayers(),
|
||||||
onStyleLoaded: reloadLayers,
|
|
||||||
onMarkerTapped: onMarkerTapped,
|
onMarkerTapped: onMarkerTapped,
|
||||||
),
|
),
|
||||||
// Should be a part of the body and not scaffold::bottomsheet for the
|
// Should be a part of the body and not scaffold::bottomsheet for the
|
||||||
@@ -266,9 +271,8 @@ class MapPage extends HookConsumerWidget {
|
|||||||
style: style,
|
style: style,
|
||||||
selectedMarker: selectedMarker,
|
selectedMarker: selectedMarker,
|
||||||
onMapCreated: onMapCreated,
|
onMapCreated: onMapCreated,
|
||||||
onMapMoved: onMapMoved,
|
onMapEvent: onMapEvent,
|
||||||
onMapClicked: onMarkerClicked,
|
onStyleLoaded: (_) => reloadLayers(),
|
||||||
onStyleLoaded: reloadLayers,
|
|
||||||
onMarkerTapped: onMarkerTapped,
|
onMarkerTapped: onMarkerTapped,
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -302,32 +306,19 @@ class MapPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AssetMarkerMeta {
|
|
||||||
final Point<num> 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 {
|
class _MapWithMarker extends StatelessWidget {
|
||||||
final AsyncValue<String> style;
|
final AsyncValue<String> style;
|
||||||
final MapCreatedCallback onMapCreated;
|
final void Function(MapController) onMapCreated;
|
||||||
final OnCameraIdleCallback onMapMoved;
|
final void Function(MapEvent) onMapEvent;
|
||||||
final OnMapClickCallback onMapClicked;
|
final void Function(StyleController) onStyleLoaded;
|
||||||
final OnStyleLoadedCallback onStyleLoaded;
|
|
||||||
final Function()? onMarkerTapped;
|
final Function()? onMarkerTapped;
|
||||||
final ValueNotifier<_AssetMarkerMeta?> selectedMarker;
|
final ValueNotifier<MapMarker?> selectedMarker;
|
||||||
final LatLng? initialLocation;
|
final Geographic? initialLocation;
|
||||||
|
|
||||||
const _MapWithMarker({
|
const _MapWithMarker({
|
||||||
required this.style,
|
required this.style,
|
||||||
required this.onMapCreated,
|
required this.onMapCreated,
|
||||||
required this.onMapMoved,
|
required this.onMapEvent,
|
||||||
required this.onMapClicked,
|
|
||||||
required this.onStyleLoaded,
|
required this.onStyleLoaded,
|
||||||
required this.selectedMarker,
|
required this.selectedMarker,
|
||||||
this.onMarkerTapped,
|
this.onMarkerTapped,
|
||||||
@@ -336,48 +327,44 @@ class _MapWithMarker extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return style.widgetWhen(
|
||||||
builder: (ctx, constraints) => SizedBox(
|
onData: (style) => MapLibreMap(
|
||||||
height: constraints.maxHeight,
|
options: MapOptions(
|
||||||
width: constraints.maxWidth,
|
initCenter: initialLocation ?? const Geographic(lat: 0, lon: 0),
|
||||||
child: Stack(
|
initZoom: initialLocation != null ? 12 : 0,
|
||||||
children: [
|
initStyle: style,
|
||||||
style.widgetWhen(
|
gestures: const MapGestures.all(pitch: false, rotate: false),
|
||||||
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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
onMapCreated: onMapCreated,
|
||||||
|
onStyleLoaded: onStyleLoaded,
|
||||||
|
onEvent: onMapEvent,
|
||||||
|
children: [
|
||||||
|
ValueListenableBuilder<MapMarker?>(
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
|
||||||
import 'package:immich_mobile/utils/map_utils.dart';
|
import 'package:immich_mobile/utils/map_utils.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MapLocationPickerPage extends HookConsumerWidget {
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectedLatLng = useValueNotifier<LatLng>(initialLatLng);
|
final selectedLatLng = useValueNotifier<Geographic>(initialLatLng);
|
||||||
final controller = useRef<MapLibreMapController?>(null);
|
final currentLatLng = useValueListenable(selectedLatLng);
|
||||||
final marker = useRef<Symbol?>(null);
|
final controller = useRef<MapController?>(null);
|
||||||
|
|
||||||
Future<void> onStyleLoaded() async {
|
Future<void> onStyleLoaded(StyleController style) async {
|
||||||
marker.value = await controller.value?.addMarkerAtLatLng(initialLatLng);
|
await style.addImageFromAssets(id: 'mapMarker', asset: 'assets/location-pin.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onMapClick(Point<num> _, LatLng centre) async {
|
void onEvent(MapEvent event) {
|
||||||
selectedLatLng.value = centre;
|
if (event is! MapEventClick) return;
|
||||||
await controller.value?.animateCamera(CameraUpdate.newLatLng(centre));
|
|
||||||
if (marker.value != null) {
|
selectedLatLng.value = event.point;
|
||||||
await controller.value?.updateSymbol(marker.value!, SymbolOptions(geometry: centre));
|
controller.value?.animateCamera(center: event.point);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onClose([LatLng? selected]) {
|
void onClose([Geographic? selected]) {
|
||||||
context.maybePop(selected);
|
context.maybePop(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +43,9 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude);
|
var currentLatLng = Geographic(lat: currentLocation.latitude, lon: currentLocation.longitude);
|
||||||
selectedLatLng.value = currentLatLng;
|
selectedLatLng.value = currentLatLng;
|
||||||
await controller.value?.animateCamera(CameraUpdate.newLatLngZoom(currentLatLng, 12));
|
await controller.value?.animateCamera(center: currentLatLng, zoom: 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapThemeOverride(
|
return MapThemeOverride(
|
||||||
@@ -66,18 +62,24 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)),
|
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)),
|
||||||
),
|
),
|
||||||
child: MapLibreMap(
|
child: MapLibreMap(
|
||||||
initialCameraPosition: CameraPosition(
|
options: MapOptions(
|
||||||
target: initialLatLng,
|
initCenter: initialLatLng,
|
||||||
zoom: (initialLatLng.latitude == 0 && initialLatLng.longitude == 0) ? 1 : 12,
|
initZoom: (initialLatLng.lat == 0 && initialLatLng.lon == 0) ? 1 : 12,
|
||||||
|
initStyle: style,
|
||||||
|
gestures: const MapGestures.all(pitch: false),
|
||||||
),
|
),
|
||||||
styleString: style,
|
|
||||||
onMapCreated: (mapController) => controller.value = mapController,
|
onMapCreated: (mapController) => controller.value = mapController,
|
||||||
onStyleLoadedCallback: onStyleLoaded,
|
onStyleLoaded: onStyleLoaded,
|
||||||
onMapClick: onMapClick,
|
onEvent: onEvent,
|
||||||
dragEnabled: false,
|
layers: [
|
||||||
tiltGesturesEnabled: false,
|
MarkerLayer(
|
||||||
myLocationEnabled: false,
|
points: [Feature(geometry: Point(currentLatLng))],
|
||||||
attributionButtonMargins: const Point(20, 15),
|
iconImage: 'mapMarker',
|
||||||
|
iconSize: 0.15,
|
||||||
|
iconAnchor: IconAnchor.bottom,
|
||||||
|
iconAllowOverlap: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -117,7 +119,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BottomBar extends StatelessWidget {
|
class _BottomBar extends StatelessWidget {
|
||||||
final ValueNotifier<LatLng> selectedLatLng;
|
final ValueNotifier<Geographic> selectedLatLng;
|
||||||
final Function() onUseLocation;
|
final Function() onUseLocation;
|
||||||
final Function() onGetCurrentLocation;
|
final Function() onGetCurrentLocation;
|
||||||
|
|
||||||
@@ -140,8 +142,7 @@ class _BottomBar extends StatelessWidget {
|
|||||||
const SizedBox(width: 15),
|
const SizedBox(width: 15),
|
||||||
ValueListenableBuilder(
|
ValueListenableBuilder(
|
||||||
valueListenable: selectedLatLng,
|
valueListenable: selectedLatLng,
|
||||||
builder: (_, value, __) =>
|
builder: (_, value, __) => Text("${value.lat.toStringAsFixed(4)}, ${value.lon.toStringAsFixed(4)}"),
|
||||||
Text("${value.latitude.toStringAsFixed(4)}, ${value.longitude.toStringAsFixed(4)}"),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/utils/image_url_builder.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/common/immich_sliver_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftLibraryPage extends ConsumerWidget {
|
class DriftLibraryPage extends ConsumerWidget {
|
||||||
@@ -230,7 +230,7 @@ class _PlacesCollectionCard extends StatelessWidget {
|
|||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
centre: const LatLng(21.44950, -157.91959),
|
centre: const Geographic(lat: 21.44950, lon: -157.91959),
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/map/map.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/map/map.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart';
|
import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftMapPage extends StatelessWidget {
|
class DriftMapPage extends StatelessWidget {
|
||||||
final LatLng? initialLocation;
|
final Geographic? initialLocation;
|
||||||
|
|
||||||
const DriftMapPage({super.key, this.initialLocation});
|
const DriftMapPage({super.key, this.initialLocation});
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftPlacePage extends StatelessWidget {
|
class DriftPlacePage extends StatelessWidget {
|
||||||
const DriftPlacePage({super.key, this.currentLocation});
|
const DriftPlacePage({super.key, this.currentLocation});
|
||||||
|
|
||||||
final LatLng? currentLocation;
|
final Geographic? currentLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -82,7 +82,7 @@ class _Map extends StatelessWidget {
|
|||||||
const _Map({required this.search, this.currentLocation});
|
const _Map({required this.search, this.currentLocation});
|
||||||
|
|
||||||
final ValueNotifier<String?> search;
|
final ValueNotifier<String?> search;
|
||||||
final LatLng? currentLocation;
|
final Geographic? currentLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -96,7 +96,7 @@ class _Map extends StatelessWidget {
|
|||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
onTap: (_, __) => context.pushRoute(DriftMapRoute(initialLocation: currentLocation)),
|
onTap: (_, __) => context.pushRoute(DriftMapRoute(initialLocation: currentLocation)),
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
centre: currentLocation ?? const LatLng(21.44950, -157.91959),
|
centre: currentLocation ?? const Geographic(lat: 21.44950, lon: -157.91959),
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.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: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 {
|
class LocationDetails extends ConsumerStatefulWidget {
|
||||||
const LocationDetails({super.key});
|
const LocationDetails({super.key});
|
||||||
@@ -20,7 +20,7 @@ class LocationDetails extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LocationDetailsState extends ConsumerState<LocationDetails> {
|
class _LocationDetailsState extends ConsumerState<LocationDetails> {
|
||||||
MapLibreMapController? _mapController;
|
MapController? _mapController;
|
||||||
|
|
||||||
String? _getLocationName(ExifInfo? exifInfo) {
|
String? _getLocationName(ExifInfo? exifInfo) {
|
||||||
if (exifInfo == null) {
|
if (exifInfo == null) {
|
||||||
@@ -36,14 +36,16 @@ class _LocationDetailsState extends ConsumerState<LocationDetails> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMapCreated(MapLibreMapController controller) {
|
void _onMapCreated(MapController controller) {
|
||||||
_mapController = controller;
|
_mapController = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onExifChanged(AsyncValue<ExifInfo?>? previous, AsyncValue<ExifInfo?> current) {
|
void _onExifChanged(AsyncValue<ExifInfo?>? previous, AsyncValue<ExifInfo?> current) {
|
||||||
final currentExif = current.valueOrNull;
|
final currentExif = current.valueOrNull;
|
||||||
if (currentExif != null && currentExif.hasCoordinates) {
|
if (currentExif != null && currentExif.hasCoordinates) {
|
||||||
_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentExif.latitude!, currentExif.longitude!)));
|
_mapController?.moveCamera(
|
||||||
|
center: Geographic(lat: currentExif.latitude!, lon: currentExif.longitude!),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/infrastructure/map.provider.dart';
|
||||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
class MapState {
|
class MapState {
|
||||||
final ThemeMode themeMode;
|
final ThemeMode themeMode;
|
||||||
final LatLngBounds bounds;
|
final LngLatBounds bounds;
|
||||||
final bool onlyFavorites;
|
final bool onlyFavorites;
|
||||||
final bool includeArchived;
|
final bool includeArchived;
|
||||||
final bool withPartners;
|
final bool withPartners;
|
||||||
@@ -35,7 +35,7 @@ class MapState {
|
|||||||
int get hashCode => bounds.hashCode;
|
int get hashCode => bounds.hashCode;
|
||||||
|
|
||||||
MapState copyWith({
|
MapState copyWith({
|
||||||
LatLngBounds? bounds,
|
LngLatBounds? bounds,
|
||||||
ThemeMode? themeMode,
|
ThemeMode? themeMode,
|
||||||
bool? onlyFavorites,
|
bool? onlyFavorites,
|
||||||
bool? includeArchived,
|
bool? includeArchived,
|
||||||
@@ -64,7 +64,7 @@ class MapState {
|
|||||||
class MapStateNotifier extends Notifier<MapState> {
|
class MapStateNotifier extends Notifier<MapState> {
|
||||||
MapStateNotifier();
|
MapStateNotifier();
|
||||||
|
|
||||||
bool setBounds(LatLngBounds bounds) {
|
bool setBounds(LngLatBounds bounds) {
|
||||||
if (state.bounds == bounds) {
|
if (state.bounds == bounds) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -113,14 +113,14 @@ class MapStateNotifier extends Notifier<MapState> {
|
|||||||
includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived),
|
includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived),
|
||||||
withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners),
|
withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners),
|
||||||
relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate),
|
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.
|
// 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
|
// It should be used only after the map service provider is overridden
|
||||||
final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LatLngBounds?>((ref, bounds) async {
|
final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LngLatBounds?>((ref, bounds) async {
|
||||||
final mapService = ref.watch(mapServiceProvider);
|
final mapService = ref.watch(mapServiceProvider);
|
||||||
final markers = await mapService.getMarkers(bounds);
|
final markers = await mapService.getMarkers(bounds);
|
||||||
final features = List.filled(markers.length, const <String, dynamic>{});
|
final features = List.filled(markers.length, const <String, dynamic>{});
|
||||||
@@ -131,7 +131,7 @@ final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LatLngBoun
|
|||||||
'id': marker.assetId,
|
'id': marker.assetId,
|
||||||
'geometry': {
|
'geometry': {
|
||||||
'type': 'Point',
|
'type': 'Point',
|
||||||
'coordinates': [marker.location.longitude, marker.location.latitude],
|
'coordinates': [marker.location.lon, marker.location.lat],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.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/utils/debounce.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
class CustomSourceProperties implements SourceProperties {
|
|
||||||
final Map<String, dynamic> data;
|
|
||||||
const CustomSourceProperties({required this.data});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
"type": "geojson",
|
|
||||||
"data": data,
|
|
||||||
// "cluster": true,
|
|
||||||
// "clusterRadius": 1,
|
|
||||||
// "clusterMinPoints": 5,
|
|
||||||
// "tolerance": 0.1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DriftMap extends ConsumerStatefulWidget {
|
class DriftMap extends ConsumerStatefulWidget {
|
||||||
final LatLng? initialLocation;
|
final Geographic? initialLocation;
|
||||||
|
|
||||||
const DriftMap({super.key, this.initialLocation});
|
const DriftMap({super.key, this.initialLocation});
|
||||||
|
|
||||||
@@ -49,7 +31,7 @@ class DriftMap extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DriftMapState extends ConsumerState<DriftMap> {
|
class _DriftMapState extends ConsumerState<DriftMap> {
|
||||||
MapLibreMapController? mapController;
|
MapController? mapController;
|
||||||
final _reloadMutex = AsyncMutex();
|
final _reloadMutex = AsyncMutex();
|
||||||
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
||||||
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
||||||
@@ -69,7 +51,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onMapCreated(MapLibreMapController controller) {
|
void onMapCreated(MapController controller) {
|
||||||
mapController = controller;
|
mapController = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,43 +63,23 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await controller.addSource(
|
await controller.style!.addSource(
|
||||||
MapUtils.defaultSourceId,
|
GeoJsonSource(id: MapUtils.defaultSourceId, data: jsonEncode({'type': 'FeatureCollection', 'features': []})),
|
||||||
const CustomSourceProperties(data: {'type': 'FeatureCollection', 'features': []}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
await controller.style!.addLayer(
|
||||||
await controller.addCircleLayer(
|
const HeatmapStyleLayer(
|
||||||
MapUtils.defaultSourceId,
|
id: MapUtils.defaultHeatMapLayerId,
|
||||||
MapUtils.defaultHeatMapLayerId,
|
sourceId: MapUtils.defaultSourceId,
|
||||||
const CircleLayerProperties(
|
paint: MapUtils.defaultHeatmapLayerPaint,
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_debouncer.run(() => setBounds(forceReload: true));
|
_debouncer.run(() => setBounds(forceReload: true));
|
||||||
controller.addListener(onMapMoved);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onMapMoved() {
|
void onMapEvent(MapEvent event) {
|
||||||
if (mapController!.isCameraMoving || !mounted) {
|
if (event is! MapEventCameraIdle || !mounted) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_debouncer.run(setBounds);
|
_debouncer.run(setBounds);
|
||||||
}
|
}
|
||||||
@@ -136,7 +98,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final bounds = await controller.getVisibleRegion();
|
final bounds = controller.getVisibleRegion();
|
||||||
unawaited(
|
unawaited(
|
||||||
_reloadMutex.run(() async {
|
_reloadMutex.run(() async {
|
||||||
if (mounted && (ref.read(mapStateProvider.notifier).setBounds(bounds) || forceReload)) {
|
if (mounted && (ref.read(mapStateProvider.notifier).setBounds(bounds) || forceReload)) {
|
||||||
@@ -153,7 +115,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await controller.setGeoJsonSource(MapUtils.defaultSourceId, markers);
|
await controller.style!.updateGeoJsonSource(id: MapUtils.defaultSourceId, data: jsonEncode(markers));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onZoomToLocation() async {
|
Future<void> onZoomToLocation() async {
|
||||||
@@ -173,8 +135,9 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
final controller = mapController;
|
final controller = mapController;
|
||||||
if (controller != null && location != null) {
|
if (controller != null && location != null) {
|
||||||
await controller.animateCamera(
|
await controller.animateCamera(
|
||||||
CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), MapUtils.mapZoomToAssetLevel),
|
center: Geographic(lat: location.latitude, lon: location.longitude),
|
||||||
duration: const Duration(milliseconds: 800),
|
zoom: MapUtils.mapZoomToAssetLevel,
|
||||||
|
nativeDuration: Durations.extralong2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +146,12 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
_Map(
|
||||||
|
initialLocation: widget.initialLocation,
|
||||||
|
onMapCreated: onMapCreated,
|
||||||
|
onMapReady: onMapReady,
|
||||||
|
onMapEvent: onMapEvent,
|
||||||
|
),
|
||||||
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
|
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
|
||||||
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
|
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
|
||||||
],
|
],
|
||||||
@@ -192,13 +160,13 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _Map extends StatelessWidget {
|
class _Map extends StatelessWidget {
|
||||||
final LatLng? initialLocation;
|
final Geographic? initialLocation;
|
||||||
|
|
||||||
const _Map({this.initialLocation, required this.onMapCreated, required this.onMapReady});
|
const _Map({this.initialLocation, required this.onMapCreated, required this.onMapReady, required this.onMapEvent});
|
||||||
|
|
||||||
final MapCreatedCallback onMapCreated;
|
|
||||||
|
|
||||||
|
final void Function(MapController) onMapCreated;
|
||||||
final VoidCallback onMapReady;
|
final VoidCallback onMapReady;
|
||||||
|
final void Function(MapEvent) onMapEvent;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -206,16 +174,15 @@ class _Map extends StatelessWidget {
|
|||||||
return MapThemeOverride(
|
return MapThemeOverride(
|
||||||
mapBuilder: (style) => style.widgetWhen(
|
mapBuilder: (style) => style.widgetWhen(
|
||||||
onData: (style) => MapLibreMap(
|
onData: (style) => MapLibreMap(
|
||||||
initialCameraPosition: initialLocation == null
|
options: MapOptions(
|
||||||
? const CameraPosition(target: LatLng(0, 0), zoom: 0)
|
initCenter: initialLocation ?? const Geographic(lat: 0, lon: 0),
|
||||||
: CameraPosition(target: initialLocation, zoom: MapUtils.mapZoomToAssetLevel),
|
initZoom: initialLocation == null ? 0 : MapUtils.mapZoomToAssetLevel,
|
||||||
compassEnabled: false,
|
initStyle: style,
|
||||||
rotateGesturesEnabled: false,
|
gestures: const MapGestures.all(rotate: false),
|
||||||
styleString: style,
|
),
|
||||||
onMapCreated: onMapCreated,
|
onMapCreated: onMapCreated,
|
||||||
onStyleLoadedCallback: onMapReady,
|
onStyleLoaded: (_) => onMapReady(),
|
||||||
attributionButtonPosition: AttributionButtonPosition.topRight,
|
onEvent: onMapEvent,
|
||||||
attributionButtonMargins: const Point(8, kToolbarHeight),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:geolocator/geolocator.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
|
||||||
|
|
||||||
class MapUtils {
|
class MapUtils {
|
||||||
static final Logger _logger = Logger("MapUtils");
|
static final Logger _logger = Logger("MapUtils");
|
||||||
@@ -13,49 +12,37 @@ class MapUtils {
|
|||||||
static const mapZoomToAssetLevel = 12.0;
|
static const mapZoomToAssetLevel = 12.0;
|
||||||
static const defaultSourceId = 'asset-map-markers';
|
static const defaultSourceId = 'asset-map-markers';
|
||||||
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
||||||
static var markerCompleter = Completer()..complete();
|
static const defaultHeatmapLayerPaint = <String, Object>{
|
||||||
|
'heatmap-color': [
|
||||||
static const defaultCircleLayerLayerProperties = CircleLayerProperties(
|
'interpolate',
|
||||||
circleRadius: 10,
|
['linear'],
|
||||||
circleColor: "rgba(150,86,34,0.7)",
|
['heatmap-density'],
|
||||||
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"],
|
|
||||||
0.0,
|
0.0,
|
||||||
"rgba(103,58,183,0.0)",
|
'rgba(103,58,183,0.0)',
|
||||||
0.3,
|
0.3,
|
||||||
"rgb(103,58,183)",
|
'rgb(103,58,183)',
|
||||||
0.5,
|
0.5,
|
||||||
"rgb(33,149,243)",
|
'rgb(33,149,243)',
|
||||||
0.7,
|
0.7,
|
||||||
"rgb(76,175,79)",
|
'rgb(76,175,79)',
|
||||||
0.95,
|
0.95,
|
||||||
"rgb(255,235,59)",
|
'rgb(255,235,59)',
|
||||||
1.0,
|
1.0,
|
||||||
"rgb(255,86,34)",
|
'rgb(255,86,34)',
|
||||||
],
|
],
|
||||||
heatmapIntensity: [
|
'heatmap-intensity': [
|
||||||
Expressions.interpolate,
|
'interpolate',
|
||||||
["linear"],
|
['linear'],
|
||||||
[Expressions.zoom],
|
['zoom'],
|
||||||
0,
|
0,
|
||||||
0.5,
|
0.5,
|
||||||
9,
|
9,
|
||||||
2,
|
2,
|
||||||
],
|
],
|
||||||
heatmapRadius: [
|
'heatmap-radius': [
|
||||||
Expressions.interpolate,
|
'interpolate',
|
||||||
["linear"],
|
['linear'],
|
||||||
[Expressions.zoom],
|
['zoom'],
|
||||||
0,
|
0,
|
||||||
4,
|
4,
|
||||||
4,
|
4,
|
||||||
@@ -63,8 +50,8 @@ class MapUtils {
|
|||||||
9,
|
9,
|
||||||
16,
|
16,
|
||||||
],
|
],
|
||||||
heatmapOpacity: 0.7,
|
'heatmap-opacity': 0.7,
|
||||||
);
|
};
|
||||||
|
|
||||||
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
|
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
|||||||
@@ -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/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/api.repository.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';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final assetApiRepositoryProvider = Provider(
|
final assetApiRepositoryProvider = Provider(
|
||||||
@@ -62,8 +62,8 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite));
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
Future<void> updateLocation(List<String> ids, Geographic location) async {
|
||||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude));
|
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.lat, longitude: location.lon));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
|
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
|
||||||
|
|||||||
@@ -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/local_auth.service.dart';
|
||||||
import 'package:immich_mobile/services/secure_storage.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: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';
|
part 'router.gr.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1226,7 +1226,7 @@ class DriftLockedFolderRoute extends PageRouteInfo<void> {
|
|||||||
class DriftMapRoute extends PageRouteInfo<DriftMapRouteArgs> {
|
class DriftMapRoute extends PageRouteInfo<DriftMapRouteArgs> {
|
||||||
DriftMapRoute({
|
DriftMapRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
LatLng? initialLocation,
|
Geographic? initialLocation,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
DriftMapRoute.name,
|
DriftMapRoute.name,
|
||||||
@@ -1252,7 +1252,7 @@ class DriftMapRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final LatLng? initialLocation;
|
final Geographic? initialLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -1461,7 +1461,7 @@ class DriftPlaceDetailRouteArgs {
|
|||||||
class DriftPlaceRoute extends PageRouteInfo<DriftPlaceRouteArgs> {
|
class DriftPlaceRoute extends PageRouteInfo<DriftPlaceRouteArgs> {
|
||||||
DriftPlaceRoute({
|
DriftPlaceRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
LatLng? currentLocation,
|
Geographic? currentLocation,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
DriftPlaceRoute.name,
|
DriftPlaceRoute.name,
|
||||||
@@ -1490,7 +1490,7 @@ class DriftPlaceRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final LatLng? currentLocation;
|
final Geographic? currentLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -2011,7 +2011,7 @@ class MainTimelineRoute extends PageRouteInfo<void> {
|
|||||||
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
||||||
MapLocationPickerRoute({
|
MapLocationPickerRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
LatLng initialLatLng = const LatLng(0, 0),
|
Geographic initialLatLng = const Geographic(lat: 0, lon: 0),
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
MapLocationPickerRoute.name,
|
MapLocationPickerRoute.name,
|
||||||
@@ -2041,12 +2041,12 @@ class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
|||||||
class MapLocationPickerRouteArgs {
|
class MapLocationPickerRouteArgs {
|
||||||
const MapLocationPickerRouteArgs({
|
const MapLocationPickerRouteArgs({
|
||||||
this.key,
|
this.key,
|
||||||
this.initialLatLng = const LatLng(0, 0),
|
this.initialLatLng = const Geographic(lat: 0, lon: 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final LatLng initialLatLng;
|
final Geographic initialLatLng;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -2057,12 +2057,15 @@ class MapLocationPickerRouteArgs {
|
|||||||
/// generated route for
|
/// generated route for
|
||||||
/// [MapPage]
|
/// [MapPage]
|
||||||
class MapRoute extends PageRouteInfo<MapRouteArgs> {
|
class MapRoute extends PageRouteInfo<MapRouteArgs> {
|
||||||
MapRoute({Key? key, LatLng? initialLocation, List<PageRouteInfo>? children})
|
MapRoute({
|
||||||
: super(
|
Key? key,
|
||||||
MapRoute.name,
|
Geographic? initialLocation,
|
||||||
args: MapRouteArgs(key: key, initialLocation: initialLocation),
|
List<PageRouteInfo>? children,
|
||||||
initialChildren: children,
|
}) : super(
|
||||||
);
|
MapRoute.name,
|
||||||
|
args: MapRouteArgs(key: key, initialLocation: initialLocation),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
static const String name = 'MapRoute';
|
static const String name = 'MapRoute';
|
||||||
|
|
||||||
@@ -2082,7 +2085,7 @@ class MapRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final LatLng? initialLocation;
|
final Geographic? initialLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -2403,7 +2406,7 @@ class PinAuthRouteArgs {
|
|||||||
class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
||||||
PlacesCollectionRoute({
|
PlacesCollectionRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
LatLng? currentLocation,
|
Geographic? currentLocation,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
PlacesCollectionRoute.name,
|
PlacesCollectionRoute.name,
|
||||||
@@ -2435,7 +2438,7 @@ class PlacesCollectionRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final LatLng? currentLocation;
|
final Geographic? currentLocation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/utils/timezone.dart';
|
import 'package:immich_mobile/utils/timezone.dart';
|
||||||
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
||||||
import 'package:immich_mobile/widgets/common/location_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';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
final actionServiceProvider = Provider<ActionService>(
|
final actionServiceProvider = Provider<ActionService>(
|
||||||
@@ -131,12 +131,12 @@ class ActionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> editLocation(List<String> remoteIds, BuildContext context) async {
|
Future<bool> editLocation(List<String> remoteIds, BuildContext context) async {
|
||||||
maplibre.LatLng? initialLatLng;
|
maplibre.Geographic? initialLatLng;
|
||||||
if (remoteIds.length == 1) {
|
if (remoteIds.length == 1) {
|
||||||
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
||||||
|
|
||||||
if (exif?.latitude != null && exif?.longitude != null) {
|
if (exif?.latitude != null && exif?.longitude != null) {
|
||||||
initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!);
|
initialLatLng = maplibre.Geographic(lat: exif!.latitude!, lon: exif.longitude!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/backup.service.dart';
|
||||||
import 'package:immich_mobile/services/sync.service.dart';
|
import 'package:immich_mobile/services/sync.service.dart';
|
||||||
import 'package:logging/logging.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:openapi/api.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
|
|
||||||
@@ -236,12 +236,12 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Asset>?> changeLocation(List<Asset> assets, LatLng location) async {
|
Future<List<Asset>?> changeLocation(List<Asset> assets, Geographic location) async {
|
||||||
try {
|
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) {
|
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);
|
await _syncService.upsertAssetsWithExif(assets);
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
|
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
|
||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/user_agent.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
|
||||||
|
|
||||||
class MapService with ErrorLoggerMixin {
|
class MapService with ErrorLoggerMixin {
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
@override
|
@override
|
||||||
final logger = Logger("MapService");
|
final logger = Logger("MapService");
|
||||||
|
|
||||||
MapService(this._apiService) {
|
MapService(this._apiService);
|
||||||
_setMapUserAgentHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _setMapUserAgentHeader() async {
|
|
||||||
final userAgent = await getUserAgentString();
|
|
||||||
await setHttpHeaders({'User-Agent': userAgent});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Iterable<MapMarker>> getMapMarkers({
|
Future<Iterable<MapMarker>> getMapMarkers({
|
||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:geolocator/geolocator.dart';
|
|||||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
|
||||||
|
|
||||||
class MapUtils {
|
class MapUtils {
|
||||||
const MapUtils._();
|
const MapUtils._();
|
||||||
@@ -15,46 +14,53 @@ class MapUtils {
|
|||||||
static const defaultSourceId = 'asset-map-markers';
|
static const defaultSourceId = 'asset-map-markers';
|
||||||
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
||||||
|
|
||||||
static const defaultHeatMapLayerProperties = HeatmapLayerProperties(
|
static const defaultHeatMapLayerPaint = <String, Object>{
|
||||||
heatmapColor: [
|
'heatmap-color': [
|
||||||
Expressions.interpolate,
|
'interpolate',
|
||||||
["linear"],
|
['linear'],
|
||||||
["heatmap-density"],
|
['heatmap-density'],
|
||||||
0.0,
|
0.0,
|
||||||
"rgba(103,58,183,0.0)",
|
'rgba(103,58,183,0.0)',
|
||||||
0.3,
|
0.3,
|
||||||
"rgb(103,58,183)",
|
'rgb(103,58,183)',
|
||||||
0.5,
|
0.5,
|
||||||
"rgb(33,149,243)",
|
'rgb(33,149,243)',
|
||||||
0.7,
|
0.7,
|
||||||
"rgb(76,175,79)",
|
'rgb(76,175,79)',
|
||||||
0.95,
|
0.95,
|
||||||
"rgb(255,235,59)",
|
'rgb(255,235,59)',
|
||||||
1.0,
|
1.0,
|
||||||
"rgb(255,86,34)",
|
'rgb(255,86,34)',
|
||||||
],
|
],
|
||||||
heatmapIntensity: [
|
'heatmap-intensity': [
|
||||||
Expressions.interpolate, ["linear"], //
|
'interpolate',
|
||||||
[Expressions.zoom],
|
['linear'],
|
||||||
0, 0.5,
|
['zoom'],
|
||||||
9, 2,
|
0,
|
||||||
|
0.5,
|
||||||
|
9,
|
||||||
|
2,
|
||||||
],
|
],
|
||||||
heatmapRadius: [
|
'heatmap-radius': [
|
||||||
Expressions.interpolate, ["linear"], //
|
'interpolate',
|
||||||
[Expressions.zoom],
|
['linear'],
|
||||||
0, 4,
|
['zoom'],
|
||||||
4, 8,
|
0,
|
||||||
9, 16,
|
4,
|
||||||
|
4,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
16,
|
||||||
],
|
],
|
||||||
heatmapOpacity: 0.7,
|
'heatmap-opacity': 0.7,
|
||||||
);
|
};
|
||||||
|
|
||||||
static Map<String, dynamic> _addFeature(MapMarker marker) => {
|
static Map<String, dynamic> _addFeature(MapMarker marker) => {
|
||||||
'type': 'Feature',
|
'type': 'Feature',
|
||||||
'id': marker.assetRemoteId,
|
'id': marker.assetRemoteId,
|
||||||
'geometry': {
|
'geometry': {
|
||||||
'type': 'Point',
|
'type': 'Point',
|
||||||
'coordinates': [marker.latLng.longitude, marker.latLng.latitude],
|
'coordinates': [marker.latLng.lon, marker.latLng.lat],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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/immich_toast.dart';
|
||||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||||
import 'package:immich_mobile/widgets/common/share_dialog.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<Asset> selection) {
|
void handleShareAssets(WidgetRef ref, BuildContext context, Iterable<Asset> selection) {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -105,12 +105,12 @@ Future<void> handleEditDateTime(WidgetRef ref, BuildContext context, List<Asset>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleEditLocation(WidgetRef ref, BuildContext context, List<Asset> selection) async {
|
Future<void> handleEditLocation(WidgetRef ref, BuildContext context, List<Asset> selection) async {
|
||||||
LatLng? initialLatLng;
|
Geographic? initialLatLng;
|
||||||
if (selection.length == 1) {
|
if (selection.length == 1) {
|
||||||
final asset = selection.first;
|
final asset = selection.first;
|
||||||
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
|
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
|
||||||
if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) {
|
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!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class ExifMap extends StatelessWidget {
|
class ExifMap extends StatelessWidget {
|
||||||
@@ -15,7 +15,7 @@ class ExifMap extends StatelessWidget {
|
|||||||
// reusing this component
|
// reusing this component
|
||||||
final String? markerId;
|
final String? markerId;
|
||||||
final String? markerAssetThumbhash;
|
final String? markerAssetThumbhash;
|
||||||
final MapCreatedCallback? onMapCreated;
|
final void Function(MapController)? onMapCreated;
|
||||||
|
|
||||||
const ExifMap({
|
const ExifMap({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -66,7 +66,7 @@ class ExifMap extends StatelessWidget {
|
|||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return MapThumbnail(
|
return MapThumbnail(
|
||||||
centre: LatLng(exifInfo.latitude ?? 0, exifInfo.longitude ?? 0),
|
centre: Geographic(lat: exifInfo.latitude ?? 0, lon: exifInfo.longitude ?? 0),
|
||||||
height: 150,
|
height: 150,
|
||||||
width: constraints.maxWidth,
|
width: constraints.maxWidth,
|
||||||
zoom: 12.0,
|
zoom: 12.0,
|
||||||
|
|||||||
@@ -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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
Future<LatLng?> showLocationPicker({required BuildContext context, LatLng? initialLatLng}) {
|
Future<Geographic?> showLocationPicker({required BuildContext context, Geographic? initialLatLng}) {
|
||||||
return showDialog<LatLng?>(
|
return showDialog<Geographic?>(
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
builder: (ctx) => _LocationPicker(initialLatLng: initialLatLng),
|
builder: (ctx) => _LocationPicker(initialLatLng: initialLatLng),
|
||||||
@@ -17,7 +17,7 @@ Future<LatLng?> showLocationPicker({required BuildContext context, LatLng? initi
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LocationPicker extends HookWidget {
|
class _LocationPicker extends HookWidget {
|
||||||
final LatLng? initialLatLng;
|
final Geographic? initialLatLng;
|
||||||
|
|
||||||
const _LocationPicker({this.initialLatLng});
|
const _LocationPicker({this.initialLatLng});
|
||||||
|
|
||||||
@@ -33,9 +33,9 @@ class _LocationPicker extends HookWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final latitude = useState(initialLatLng?.latitude ?? 0.0);
|
final latitude = useState(initialLatLng?.lat ?? 0.0);
|
||||||
final longitude = useState(initialLatLng?.longitude ?? 0.0);
|
final longitude = useState(initialLatLng?.lon ?? 0.0);
|
||||||
final latlng = LatLng(latitude.value, longitude.value);
|
final latlng = Geographic(lat: latitude.value, lon: longitude.value);
|
||||||
final latitiudeFocusNode = useFocusNode();
|
final latitiudeFocusNode = useFocusNode();
|
||||||
final longitudeFocusNode = useFocusNode();
|
final longitudeFocusNode = useFocusNode();
|
||||||
final latitudeController = useTextEditingController(text: latitude.value.toStringAsFixed(4));
|
final latitudeController = useTextEditingController(text: latitude.value.toStringAsFixed(4));
|
||||||
@@ -48,10 +48,10 @@ class _LocationPicker extends HookWidget {
|
|||||||
}, [latitude.value, longitude.value]);
|
}, [latitude.value, longitude.value]);
|
||||||
|
|
||||||
Future<void> onMapTap() async {
|
Future<void> onMapTap() async {
|
||||||
final newLatLng = await context.pushRoute<LatLng?>(MapLocationPickerRoute(initialLatLng: latlng));
|
final newLatLng = await context.pushRoute<Geographic?>(MapLocationPickerRoute(initialLatLng: latlng));
|
||||||
if (newLatLng != null) {
|
if (newLatLng != null) {
|
||||||
latitude.value = newLatLng.latitude;
|
latitude.value = newLatLng.lat;
|
||||||
longitude.value = newLatLng.longitude;
|
longitude.value = newLatLng.lon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,36 +51,35 @@ class MapAssetGrid extends HookConsumerWidget {
|
|||||||
final assetCache = useRef<Map<String, Asset>>({});
|
final assetCache = useRef<Map<String, Asset>>({});
|
||||||
|
|
||||||
void handleMapEvents(MapEvent event) async {
|
void handleMapEvents(MapEvent event) async {
|
||||||
if (event is MapAssetsInBoundsUpdated) {
|
if (event is! MapAssetsInBoundsUpdated) return;
|
||||||
final assetIds = event.assetRemoteIds;
|
|
||||||
final missingIds = <String>[];
|
|
||||||
final currentAssets = <Asset>[];
|
|
||||||
|
|
||||||
for (final id in assetIds) {
|
final assetIds = event.assetRemoteIds;
|
||||||
final asset = assetCache.value[id];
|
final missingIds = <String>[];
|
||||||
if (asset != null) {
|
final currentAssets = <Asset>[];
|
||||||
currentAssets.add(asset);
|
|
||||||
} else {
|
for (final id in assetIds) {
|
||||||
missingIds.add(id);
|
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<MapEvent>(mapEventStream, onData: handleMapEvents);
|
useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
|
||||||
|
|||||||
@@ -33,13 +33,9 @@ class MapBottomSheet extends HookConsumerWidget {
|
|||||||
final isBottomSheetOpened = useRef(false);
|
final isBottomSheetOpened = useRef(false);
|
||||||
|
|
||||||
void handleMapEvents(MapEvent event) async {
|
void handleMapEvents(MapEvent event) async {
|
||||||
if (event is MapCloseBottomSheet) {
|
if (event is! MapCloseBottomSheet) return;
|
||||||
await sheetController.animateTo(
|
|
||||||
0.1,
|
await sheetController.animateTo(0.1, duration: const Duration(milliseconds: 200), curve: Curves.linearToEaseOut);
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
curve: Curves.linearToEaseOut,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
|
useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre/maplibre.dart';
|
||||||
|
|
||||||
class MapThemePicker extends StatelessWidget {
|
class MapThemePicker extends StatelessWidget {
|
||||||
final ThemeMode themeMode;
|
final ThemeMode themeMode;
|
||||||
@@ -78,7 +78,7 @@ class _BorderedMapThumbnail extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: MapThumbnail(
|
child: MapThumbnail(
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
centre: const LatLng(47, 5),
|
centre: const Geographic(lat: 47, lon: 5),
|
||||||
onTap: (_, __) => onThemeChange(mode),
|
onTap: (_, __) => onThemeChange(mode),
|
||||||
themeMode: mode,
|
themeMode: mode,
|
||||||
showAttribution: false,
|
showAttribution: false,
|
||||||
|
|||||||
@@ -84,8 +84,13 @@ class _MapThemeOverrideState extends ConsumerState<MapThemeOverride> with Widget
|
|||||||
data: _isDarkTheme
|
data: _isDarkTheme
|
||||||
? getThemeData(colorScheme: appTheme.dark, locale: locale)
|
? getThemeData(colorScheme: appTheme.dark, locale: locale)
|
||||||
: getThemeData(colorScheme: appTheme.light, locale: locale),
|
: getThemeData(colorScheme: appTheme.light, locale: locale),
|
||||||
child: widget.mapBuilder.call(
|
// Key on _isDarkTheme to force MapLibreMap recreation on theme change,
|
||||||
ref.watch(mapStateNotifierProvider.select((v) => _isDarkTheme ? v.darkStyleFetched : v.lightStyleFetched)),
|
// 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)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:immich_mobile/widgets/map/asset_marker_icon.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
|
/// 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,
|
/// [showMarkerPin] to true which would display a marker pin instead. If both are provided,
|
||||||
/// [assetMarkerRemoteId] will take precedence
|
/// [assetMarkerRemoteId] will take precedence
|
||||||
class MapThumbnail extends HookConsumerWidget {
|
class MapThumbnail extends HookConsumerWidget {
|
||||||
final Function(Point<double>, LatLng)? onTap;
|
final Function(Offset, Geographic)? onTap;
|
||||||
final LatLng centre;
|
final Geographic centre;
|
||||||
final String? assetMarkerRemoteId;
|
final String? assetMarkerRemoteId;
|
||||||
final String? assetThumbhash;
|
final String? assetThumbhash;
|
||||||
final bool showMarkerPin;
|
final bool showMarkerPin;
|
||||||
@@ -26,7 +23,7 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
final double width;
|
final double width;
|
||||||
final ThemeMode? themeMode;
|
final ThemeMode? themeMode;
|
||||||
final bool showAttribution;
|
final bool showAttribution;
|
||||||
final MapCreatedCallback? onCreated;
|
final void Function(MapController)? onCreated;
|
||||||
|
|
||||||
const MapThumbnail({
|
const MapThumbnail({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -45,28 +42,21 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final controller = useRef<MapLibreMapController?>(null);
|
|
||||||
final styleLoaded = useState(false);
|
final styleLoaded = useState(false);
|
||||||
|
|
||||||
Future<void> onMapCreated(MapLibreMapController mapController) async {
|
Future<void> onStyleLoaded(StyleController style) async {
|
||||||
controller.value = mapController;
|
if (showMarkerPin) {
|
||||||
styleLoaded.value = false;
|
await style.addImageFromAssets(id: 'mapMarker', asset: 'assets/location-pin.png');
|
||||||
onCreated?.call(mapController);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> 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
|
|
||||||
}
|
}
|
||||||
styleLoaded.value = true;
|
styleLoaded.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onEvent(MapEvent event) {
|
||||||
|
if (event is MapEventClick && onTap != null) {
|
||||||
|
onTap!(event.screenPoint, event.point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return MapThemeOverride(
|
return MapThemeOverride(
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
mapBuilder: (style) => AnimatedContainer(
|
mapBuilder: (style) => AnimatedContainer(
|
||||||
@@ -80,37 +70,41 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
width: width,
|
width: width,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
child: Stack(
|
child: style.widgetWhen(
|
||||||
alignment: AlignmentGeometry.topCenter,
|
onData: (style) => MapLibreMap(
|
||||||
children: [
|
options: MapOptions(
|
||||||
style.widgetWhen(
|
initCenter: Geographic(lat: centre.lat + 0.002, lon: centre.lon),
|
||||||
onData: (style) => MapLibreMap(
|
initZoom: zoom,
|
||||||
initialCameraPosition: CameraPosition(target: centre, zoom: zoom),
|
initStyle: style,
|
||||||
styleString: style,
|
gestures: const MapGestures.none(),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (assetMarkerRemoteId != null && assetThumbhash != null)
|
onMapCreated: onCreated,
|
||||||
Container(
|
onStyleLoaded: onStyleLoaded,
|
||||||
width: width,
|
onEvent: onEvent,
|
||||||
height: height / 2,
|
layers: [
|
||||||
alignment: Alignment.bottomCenter,
|
if (showMarkerPin)
|
||||||
child: SizedBox.square(
|
MarkerLayer(
|
||||||
dimension: height / 2.5,
|
points: [Feature(geometry: Point(centre))],
|
||||||
child: AssetMarkerIcon(id: assetMarkerRemoteId!, thumbhash: assetThumbhash!),
|
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!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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<num> 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)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.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 {
|
class SearchMapThumbnail extends StatelessWidget {
|
||||||
const SearchMapThumbnail({super.key, this.size = 60.0});
|
const SearchMapThumbnail({super.key, this.size = 60.0});
|
||||||
@@ -20,7 +20,13 @@ class SearchMapThumbnail extends StatelessWidget {
|
|||||||
context.pushRoute(MapRoute());
|
context.pushRoute(MapRoute());
|
||||||
},
|
},
|
||||||
child: IgnorePointer(
|
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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[tools]
|
[tools]
|
||||||
flutter = "3.35.7"
|
flutter = "3.41.2"
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
version = "1.30.0"
|
version = "1.30.0"
|
||||||
|
|||||||
@@ -229,10 +229,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
charcode:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -273,6 +273,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
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:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -776,6 +784,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
geobase:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geobase
|
||||||
|
sha256: "3a4e2eb17a7ab452dda78fb45ee498a3ab02469ded749a5f4a9abea21fc95919"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
geoclue:
|
geoclue:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -872,6 +888,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.1"
|
version: "0.8.1"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
hooks_riverpod:
|
hooks_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1125,6 +1149,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "5.1.1"
|
||||||
|
lists:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lists
|
||||||
|
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
local_auth:
|
local_auth:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1173,54 +1205,78 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
maplibre_gl:
|
maplibre:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: maplibre_gl
|
name: maplibre
|
||||||
sha256: "5c7b1008396b2a321bada7d986ed60f9423406fbc7bd16f7ce91b385dfa054cd"
|
sha256: "03aad98086ef8e24caf9abcbbacf43f7ceb6267a6b914d907f57fb05ccb65e09"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.22.0"
|
version: "0.3.4"
|
||||||
maplibre_gl_platform_interface:
|
maplibre_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: maplibre_gl_platform_interface
|
name: maplibre_android
|
||||||
sha256: "08ee0a2d0853ea945a0ab619d52c0c714f43144145cd67478fc6880b52f37509"
|
sha256: be8a9c29b20c10f4b2207790e8ab35489955a29cb69df10e38bdf993b44f1547
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.22.0"
|
version: "0.3.4"
|
||||||
maplibre_gl_web:
|
maplibre_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: maplibre_gl_web
|
name: maplibre_ios
|
||||||
sha256: "2b13d4b1955a9a54e38a718f2324e56e4983c080fc6de316f6f4b5458baacb58"
|
sha256: "3e261d99697cc191e64ceb256acec5d96662d429059057bb6c1740dd11eaa7c3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.18"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1237,6 +1293,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
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:
|
native_video_player:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1477,6 +1541,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
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:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1501,6 +1597,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.3"
|
version: "5.0.3"
|
||||||
|
proj4dart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: proj4dart
|
||||||
|
sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
protobuf:
|
protobuf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1910,10 +2014,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.9"
|
||||||
thumbhash:
|
thumbhash:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1954,6 +2058,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
unicode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: unicode
|
||||||
|
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
universal_io:
|
universal_io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2162,6 +2274,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.3"
|
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:
|
worker_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -2203,5 +2323,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.0 <4.0.0"
|
dart: ">=3.11.0 <4.0.0"
|
||||||
flutter: ">=3.35.7"
|
flutter: "3.41.2"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ publish_to: 'none'
|
|||||||
version: 2.5.6+3037
|
version: 2.5.6+3037
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.8.0 <4.0.0'
|
sdk: '>=3.11.0 <4.0.0'
|
||||||
flutter: 3.35.7
|
flutter: 3.41.2
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
async: ^2.13.0
|
async: ^2.13.0
|
||||||
@@ -52,7 +52,7 @@ dependencies:
|
|||||||
isar_community_flutter_libs: 3.3.0-dev.3
|
isar_community_flutter_libs: 3.3.0-dev.3
|
||||||
local_auth: ^2.3.0
|
local_auth: ^2.3.0
|
||||||
logging: ^1.3.0
|
logging: ^1.3.0
|
||||||
maplibre_gl: ^0.22.0
|
maplibre: ^0.3.4
|
||||||
|
|
||||||
native_video_player:
|
native_video_player:
|
||||||
git:
|
git:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/services/asset.service.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:mocktail/mocktail.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -85,9 +85,9 @@ void main() {
|
|||||||
expect(receivedDatetime.every((d) => d == dateTime), isTrue);
|
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 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);
|
await sut.changeLocation(assets, latLng);
|
||||||
|
|
||||||
verify(() => assetsApi.updateAssets(any())).called(1);
|
verify(() => assetsApi.updateAssets(any())).called(1);
|
||||||
@@ -95,7 +95,7 @@ void main() {
|
|||||||
upsertExifCallback.called(1);
|
upsertExifCallback.called(1);
|
||||||
final receivedAssets = upsertExifCallback.captured.firstOrNull as List<Object>? ?? [];
|
final receivedAssets = upsertExifCallback.captured.firstOrNull as List<Object>? ?? [];
|
||||||
final receivedCoords = receivedAssets.cast<Asset>().map(
|
final receivedCoords = receivedAssets.cast<Asset>().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);
|
expect(receivedCoords.every((l) => l == latLng), isTrue);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
|
|||||||
# Flutter SDK
|
# Flutter SDK
|
||||||
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
||||||
ENV FLUTTER_CHANNEL="stable"
|
ENV FLUTTER_CHANNEL="stable"
|
||||||
ENV FLUTTER_VERSION="3.35.7"
|
ENV FLUTTER_VERSION="3.41.2"
|
||||||
ENV FLUTTER_HOME=/flutter
|
ENV FLUTTER_HOME=/flutter
|
||||||
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
|
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user