mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10: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:
@@ -1,7 +1,7 @@
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
class Marker {
|
||||
final LatLng location;
|
||||
final Geographic location;
|
||||
final String assetId;
|
||||
|
||||
const Marker({required this.location, required this.assetId});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:immich_mobile/domain/models/map.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart' hide Marker;
|
||||
|
||||
typedef MapMarkerSource = Future<List<Marker>> Function(LatLngBounds? bounds);
|
||||
typedef MapMarkerSource = Future<List<Marker>> Function(LngLatBounds? bounds);
|
||||
|
||||
typedef MapQuery = ({MapMarkerSource markerSource});
|
||||
|
||||
@@ -21,5 +21,5 @@ class MapService {
|
||||
|
||||
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
|
||||
bool contains(LatLng point) {
|
||||
final sw = point;
|
||||
final ne = point;
|
||||
return containsBounds(LatLngBounds(southwest: sw, northeast: ne));
|
||||
bool contains(Geographic point) {
|
||||
return containsBounds(
|
||||
LngLatBounds(
|
||||
longitudeWest: point.lon,
|
||||
longitudeEast: point.lon,
|
||||
latitudeSouth: point.lat,
|
||||
latitudeNorth: point.lat,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks whether [bounds] is contained inside bounds
|
||||
bool containsBounds(LatLngBounds bounds) {
|
||||
final sw = bounds.southwest;
|
||||
final ne = bounds.northeast;
|
||||
return (sw.latitude >= southwest.latitude) &&
|
||||
(ne.latitude <= northeast.latitude) &&
|
||||
(sw.longitude >= southwest.longitude) &&
|
||||
(ne.longitude <= northeast.longitude);
|
||||
bool containsBounds(LngLatBounds bounds) {
|
||||
return (bounds.latitudeSouth >= latitudeSouth) &&
|
||||
(bounds.latitudeNorth <= latitudeNorth) &&
|
||||
(bounds.longitudeWest >= longitudeWest) &&
|
||||
(bounds.longitudeEast <= longitudeEast);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||
import 'package:immich_mobile/utils/map_utils.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
extension MapMarkers on MapLibreMapController {
|
||||
extension MapMarkers on MapController {
|
||||
static var _completer = Completer()..complete();
|
||||
|
||||
Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
|
||||
return addSource(
|
||||
MapUtils.defaultSourceId,
|
||||
GeojsonSourceProperties(data: MapUtils.generateGeoJsonForMarkers(markers.toList())),
|
||||
return style!.addSource(
|
||||
GeoJsonSource(
|
||||
id: MapUtils.defaultSourceId,
|
||||
data: jsonEncode(MapUtils.generateGeoJsonForMarkers(markers.toList())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,63 +27,28 @@ extension MapMarkers on MapLibreMapController {
|
||||
// !! Make sure to remove layers before sources else the native
|
||||
// maplibre library would crash when removing the source saying that
|
||||
// the source is still in use
|
||||
final existingLayers = await getLayerIds();
|
||||
if (existingLayers.contains(MapUtils.defaultHeatMapLayerId)) {
|
||||
await removeLayer(MapUtils.defaultHeatMapLayerId);
|
||||
try {
|
||||
await style!.removeLayer(MapUtils.defaultHeatMapLayerId);
|
||||
} catch (_) {
|
||||
// Layer may not exist
|
||||
}
|
||||
|
||||
final existingSources = await getSourceIds();
|
||||
if (existingSources.contains(MapUtils.defaultSourceId)) {
|
||||
await removeSource(MapUtils.defaultSourceId);
|
||||
try {
|
||||
await style!.removeSource(MapUtils.defaultSourceId);
|
||||
} catch (_) {
|
||||
// Source may not exist
|
||||
}
|
||||
|
||||
await addGeoJSONSourceForMarkers(markers);
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
await addCircleLayer(
|
||||
MapUtils.defaultSourceId,
|
||||
MapUtils.defaultHeatMapLayerId,
|
||||
const CircleLayerProperties(
|
||||
circleRadius: 10,
|
||||
circleColor: "rgba(150,86,34,0.7)",
|
||||
circleBlur: 1.0,
|
||||
circleOpacity: 0.7,
|
||||
circleStrokeWidth: 0.1,
|
||||
circleStrokeColor: "rgba(203,46,19,0.5)",
|
||||
circleStrokeOpacity: 0.7,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
await addHeatmapLayer(
|
||||
MapUtils.defaultSourceId,
|
||||
MapUtils.defaultHeatMapLayerId,
|
||||
MapUtils.defaultHeatMapLayerProperties,
|
||||
);
|
||||
}
|
||||
await style!.addLayer(
|
||||
const HeatmapStyleLayer(
|
||||
id: MapUtils.defaultHeatMapLayerId,
|
||||
sourceId: MapUtils.defaultSourceId,
|
||||
paint: MapUtils.defaultHeatMapLayerPaint,
|
||||
),
|
||||
);
|
||||
|
||||
_completer.complete();
|
||||
}
|
||||
|
||||
Future<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 '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/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart' hide Marker;
|
||||
|
||||
class DriftMapRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
@@ -42,7 +42,7 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
||||
|
||||
Future<List<Marker>> _watchMapMarker({
|
||||
Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter,
|
||||
LatLngBounds? bounds,
|
||||
LngLatBounds? bounds,
|
||||
}) async {
|
||||
final assetId = _db.remoteExifEntity.assetId;
|
||||
final latitude = _db.remoteExifEntity.latitude;
|
||||
@@ -66,20 +66,21 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
||||
final rows = await query.get();
|
||||
return List.generate(rows.length, (i) {
|
||||
final row = rows[i];
|
||||
return Marker(assetId: row.read(assetId)!, location: LatLng(row.read(latitude)!, row.read(longitude)!));
|
||||
return Marker(
|
||||
assetId: row.read(assetId)!,
|
||||
location: Geographic(lat: row.read(latitude)!, lon: row.read(longitude)!),
|
||||
);
|
||||
}, growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
extension MapBounds on $RemoteExifEntityTable {
|
||||
Expression<bool> inBounds(LatLngBounds bounds) {
|
||||
final southwest = bounds.southwest;
|
||||
final northeast = bounds.northeast;
|
||||
|
||||
final latInBounds = latitude.isBetweenValues(southwest.latitude, northeast.latitude);
|
||||
final longInBounds = southwest.longitude <= northeast.longitude
|
||||
? longitude.isBetweenValues(southwest.longitude, northeast.longitude)
|
||||
: (longitude.isBiggerOrEqualValue(southwest.longitude) | longitude.isSmallerOrEqualValue(northeast.longitude));
|
||||
Expression<bool> inBounds(LngLatBounds bounds) {
|
||||
final latInBounds = latitude.isBetweenValues(bounds.latitudeSouth, bounds.latitudeNorth);
|
||||
final longInBounds = bounds.longitudeWest <= bounds.longitudeEast
|
||||
? longitude.isBetweenValues(bounds.longitudeWest, bounds.longitudeEast)
|
||||
: (longitude.isBiggerOrEqualValue(bounds.longitudeWest) |
|
||||
longitude.isSmallerOrEqualValue(bounds.longitudeEast));
|
||||
return latInBounds & longInBounds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
@@ -170,12 +170,12 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateLocation(List<String> ids, LatLng location) {
|
||||
Future<void> updateLocation(List<String> ids, Geographic location) {
|
||||
return _db.batch((batch) async {
|
||||
for (final id in ids) {
|
||||
batch.update(
|
||||
_db.remoteExifEntity,
|
||||
RemoteExifEntityCompanion(latitude: Value(location.latitude), longitude: Value(location.longitude)),
|
||||
RemoteExifEntityCompanion(latitude: Value(location.lat), longitude: Value(location.lon)),
|
||||
where: (e) => e.assetId.equals(id),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
import 'package:stream_transform/stream_transform.dart';
|
||||
|
||||
class TimelineMapOptions {
|
||||
final LatLngBounds bounds;
|
||||
final LngLatBounds bounds;
|
||||
final bool onlyFavorites;
|
||||
final bool includeArchived;
|
||||
final bool withPartners;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class MapMarker {
|
||||
final LatLng latLng;
|
||||
final Geographic latLng;
|
||||
final String assetRemoteId;
|
||||
const MapMarker({required this.latLng, required this.assetRemoteId});
|
||||
|
||||
MapMarker copyWith({LatLng? latLng, String? assetRemoteId}) {
|
||||
MapMarker copyWith({Geographic? latLng, String? assetRemoteId}) {
|
||||
return MapMarker(latLng: latLng ?? this.latLng, assetRemoteId: assetRemoteId ?? this.assetRemoteId);
|
||||
}
|
||||
|
||||
MapMarker.fromDto(MapMarkerResponseDto dto) : latLng = LatLng(dto.lat, dto.lon), assetRemoteId = dto.id;
|
||||
MapMarker.fromDto(MapMarkerResponseDto dto) : latLng = Geographic(lat: dto.lat, lon: dto.lon), assetRemoteId = dto.id;
|
||||
|
||||
@override
|
||||
String toString() => 'MapMarker(latLng: $latLng, assetRemoteId: $assetRemoteId)';
|
||||
|
||||
@@ -17,7 +17,7 @@ import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LibraryPage extends ConsumerWidget {
|
||||
@@ -325,7 +325,7 @@ class PlacesCollectionCard extends StatelessWidget {
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(
|
||||
zoom: 8,
|
||||
centre: const LatLng(21.44950, -157.91959),
|
||||
centre: const Geographic(lat: 21.44950, lon: -157.91959),
|
||||
showAttribution: false,
|
||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
|
||||
@@ -15,12 +15,12 @@ import 'package:immich_mobile/providers/search/search_page_state.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PlacesCollectionPage extends HookConsumerWidget {
|
||||
const PlacesCollectionPage({super.key, this.currentLocation});
|
||||
final LatLng? currentLocation;
|
||||
final Geographic? currentLocation;
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final places = ref.watch(getAllPlacesProvider);
|
||||
@@ -61,7 +61,7 @@ class PlacesCollectionPage extends HookConsumerWidget {
|
||||
child: MapThumbnail(
|
||||
onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)),
|
||||
zoom: 8,
|
||||
centre: currentLocation ?? const LatLng(21.44950, -157.91959),
|
||||
centre: currentLocation ?? const Geographic(lat: 21.44950, lon: -157.91959),
|
||||
showAttribution: false,
|
||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -12,8 +11,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
|
||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||
import 'package:immich_mobile/models/map/map_event.model.dart';
|
||||
import 'package:immich_mobile/models/map/map_event.model.dart' as app;
|
||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
@@ -26,25 +26,25 @@ import 'package:immich_mobile/utils/immich_loading_overlay.dart';
|
||||
import 'package:immich_mobile/utils/map_utils.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/map/asset_marker_icon.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MapPage extends HookConsumerWidget {
|
||||
const MapPage({super.key, this.initialLocation});
|
||||
final LatLng? initialLocation;
|
||||
final Geographic? initialLocation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mapController = useRef<MapLibreMapController?>(null);
|
||||
final mapController = useRef<MapController?>(null);
|
||||
final markers = useRef<List<MapMarker>>([]);
|
||||
final markersInBounds = useRef<List<MapMarker>>([]);
|
||||
final bottomSheetStreamController = useStreamController<MapEvent>();
|
||||
final selectedMarker = useValueNotifier<_AssetMarkerMeta?>(null);
|
||||
final bottomSheetStreamController = useStreamController<app.MapEvent>();
|
||||
final selectedMarker = useValueNotifier<MapMarker?>(null);
|
||||
final assetsDebouncer = useDebouncer();
|
||||
final layerDebouncer = useDebouncer(interval: const Duration(seconds: 1));
|
||||
final isLoading = useProcessingOverlay();
|
||||
@@ -55,19 +55,17 @@ class MapPage extends HookConsumerWidget {
|
||||
|
||||
// updates the markersInBounds value with the map markers that are visible in the current
|
||||
// map camera bounds
|
||||
Future<void> updateAssetsInBounds() async {
|
||||
// Guard map not created
|
||||
if (mapController.value == null) {
|
||||
return;
|
||||
}
|
||||
void updateAssetsInBounds() {
|
||||
if (mapController.value == null) return;
|
||||
|
||||
final bounds = await mapController.value!.getVisibleRegion();
|
||||
final bounds = mapController.value!.getVisibleRegion();
|
||||
final inBounds = markers.value
|
||||
.where((m) => bounds.contains(LatLng(m.latLng.latitude, m.latLng.longitude)))
|
||||
.where((m) => bounds.contains(Geographic(lat: m.latLng.lat, lon: m.latLng.lon)))
|
||||
.toList();
|
||||
|
||||
// Notify bottom sheet to update asset grid only when there are new assets
|
||||
if (markersInBounds.value.length != inBounds.length) {
|
||||
bottomSheetStreamController.add(MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList()));
|
||||
bottomSheetStreamController.add(app.MapAssetsInBoundsUpdated(inBounds.map((e) => e.assetRemoteId).toList()));
|
||||
}
|
||||
markersInBounds.value = inBounds;
|
||||
}
|
||||
@@ -99,57 +97,67 @@ class MapPage extends HookConsumerWidget {
|
||||
|
||||
// Refetch markers when map state is changed
|
||||
ref.listen(mapStateNotifierProvider, (_, current) {
|
||||
if (current.shouldRefetchMarkers) {
|
||||
markerDebouncer.run(() {
|
||||
ref.invalidate(mapMarkersProvider);
|
||||
// Reset marker
|
||||
selectedMarker.value = null;
|
||||
loadMarkers();
|
||||
ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false);
|
||||
});
|
||||
}
|
||||
if (!current.shouldRefetchMarkers) return;
|
||||
|
||||
markerDebouncer.run(() {
|
||||
ref.invalidate(mapMarkersProvider);
|
||||
// Reset marker
|
||||
selectedMarker.value = null;
|
||||
loadMarkers();
|
||||
ref.read(mapStateNotifierProvider.notifier).setRefetchMarkers(false);
|
||||
});
|
||||
});
|
||||
|
||||
// updates the selected markers position based on the current map camera
|
||||
Future<void> updateAssetMarkerPosition(MapMarker marker, {bool shouldAnimate = true}) async {
|
||||
final assetPoint = await mapController.value!.toScreenLocation(marker.latLng);
|
||||
selectedMarker.value = _AssetMarkerMeta(point: assetPoint, marker: marker, shouldAnimate: shouldAnimate);
|
||||
(assetPoint, marker, shouldAnimate);
|
||||
void selectMarker(MapMarker marker) {
|
||||
selectedMarker.value = marker;
|
||||
}
|
||||
|
||||
// finds the nearest asset marker from the tap point and store it as the selectedMarker
|
||||
Future<void> onMarkerClicked(Point<double> point, LatLng _) async {
|
||||
// Guard map not created
|
||||
if (mapController.value == null) {
|
||||
return;
|
||||
}
|
||||
final latlngBound = await mapController.value!.getBoundsFromPoint(point, 50);
|
||||
final marker = markersInBounds.value.firstWhereOrNull(
|
||||
(m) => latlngBound.contains(LatLng(m.latLng.latitude, m.latLng.longitude)),
|
||||
void onMarkerClicked(Offset point) {
|
||||
if (mapController.value == null) return;
|
||||
|
||||
final features = mapController.value!.featuresInRect(
|
||||
Rect.fromCircle(center: point, radius: 50),
|
||||
layerIds: [MapUtils.defaultHeatMapLayerId],
|
||||
);
|
||||
|
||||
final featureId = features.firstOrNull?.id?.toString();
|
||||
|
||||
final marker = featureId != null
|
||||
? markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == featureId)
|
||||
: null;
|
||||
|
||||
if (marker != null) {
|
||||
await updateAssetMarkerPosition(marker);
|
||||
} else {
|
||||
// If no asset was previously selected and no new asset is available, close the bottom sheet
|
||||
if (selectedMarker.value == null) {
|
||||
bottomSheetStreamController.add(const MapCloseBottomSheet());
|
||||
}
|
||||
selectedMarker.value = null;
|
||||
selectMarker(marker);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedMarker.value == null) {
|
||||
// If no asset was previously selected and no new asset is available,
|
||||
// close the bottom sheet.
|
||||
bottomSheetStreamController.add(const app.MapCloseBottomSheet());
|
||||
return;
|
||||
}
|
||||
|
||||
selectedMarker.value = null;
|
||||
}
|
||||
|
||||
void onMapCreated(MapLibreMapController controller) async {
|
||||
void onMapCreated(MapController controller) {
|
||||
mapController.value = controller;
|
||||
controller.addListener(() {
|
||||
if (controller.isCameraMoving && selectedMarker.value != null) {
|
||||
updateAssetMarkerPosition(selectedMarker.value!.marker, shouldAnimate: false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onMapEvent(MapEvent event) {
|
||||
switch (event) {
|
||||
case MapEventClick():
|
||||
onMarkerClicked(event.screenPoint);
|
||||
case MapEventCameraIdle():
|
||||
assetsDebouncer.run(updateAssetsInBounds);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onMarkerTapped() async {
|
||||
final assetId = selectedMarker.value?.marker.assetRemoteId;
|
||||
final assetId = selectedMarker.value?.assetRemoteId;
|
||||
if (assetId == null) {
|
||||
return;
|
||||
}
|
||||
@@ -171,14 +179,10 @@ class MapPage extends HookConsumerWidget {
|
||||
|
||||
/// BOTTOM SHEET CALLBACKS
|
||||
|
||||
Future<void> onMapMoved() async {
|
||||
assetsDebouncer.run(updateAssetsInBounds);
|
||||
}
|
||||
|
||||
void onBottomSheetScrolled(String assetRemoteId) {
|
||||
final assetMarker = markersInBounds.value.firstWhereOrNull((m) => m.assetRemoteId == assetRemoteId);
|
||||
if (assetMarker != null) {
|
||||
updateAssetMarkerPosition(assetMarker);
|
||||
selectMarker(assetMarker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,10 +191,11 @@ class MapPage extends HookConsumerWidget {
|
||||
if (mapController.value != null && assetMarker != null) {
|
||||
// Offset the latitude a little to show the marker just above the viewports center
|
||||
final offset = context.isMobile ? 0.02 : 0;
|
||||
final latlng = LatLng(assetMarker.latLng.latitude - offset, assetMarker.latLng.longitude);
|
||||
final latlng = Geographic(lat: assetMarker.latLng.lat - offset, lon: assetMarker.latLng.lon);
|
||||
mapController.value!.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(latlng, mapZoomToAssetLevel),
|
||||
duration: const Duration(milliseconds: 800),
|
||||
center: latlng,
|
||||
zoom: mapZoomToAssetLevel,
|
||||
nativeDuration: Durations.extralong2,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -211,8 +216,9 @@ class MapPage extends HookConsumerWidget {
|
||||
|
||||
if (mapController.value != null && location != null) {
|
||||
await mapController.value!.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel),
|
||||
duration: const Duration(milliseconds: 800),
|
||||
center: Geographic(lat: location.latitude, lon: location.longitude),
|
||||
zoom: mapZoomToAssetLevel,
|
||||
nativeDuration: Durations.extralong2,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -234,9 +240,8 @@ class MapPage extends HookConsumerWidget {
|
||||
style: style,
|
||||
selectedMarker: selectedMarker,
|
||||
onMapCreated: onMapCreated,
|
||||
onMapMoved: onMapMoved,
|
||||
onMapClicked: onMarkerClicked,
|
||||
onStyleLoaded: reloadLayers,
|
||||
onMapEvent: onMapEvent,
|
||||
onStyleLoaded: (_) => reloadLayers(),
|
||||
onMarkerTapped: onMarkerTapped,
|
||||
),
|
||||
// Should be a part of the body and not scaffold::bottomsheet for the
|
||||
@@ -266,9 +271,8 @@ class MapPage extends HookConsumerWidget {
|
||||
style: style,
|
||||
selectedMarker: selectedMarker,
|
||||
onMapCreated: onMapCreated,
|
||||
onMapMoved: onMapMoved,
|
||||
onMapClicked: onMarkerClicked,
|
||||
onStyleLoaded: reloadLayers,
|
||||
onMapEvent: onMapEvent,
|
||||
onStyleLoaded: (_) => reloadLayers(),
|
||||
onMarkerTapped: onMarkerTapped,
|
||||
),
|
||||
Positioned(
|
||||
@@ -302,32 +306,19 @@ class MapPage extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetMarkerMeta {
|
||||
final Point<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 {
|
||||
final AsyncValue<String> style;
|
||||
final MapCreatedCallback onMapCreated;
|
||||
final OnCameraIdleCallback onMapMoved;
|
||||
final OnMapClickCallback onMapClicked;
|
||||
final OnStyleLoadedCallback onStyleLoaded;
|
||||
final void Function(MapController) onMapCreated;
|
||||
final void Function(MapEvent) onMapEvent;
|
||||
final void Function(StyleController) onStyleLoaded;
|
||||
final Function()? onMarkerTapped;
|
||||
final ValueNotifier<_AssetMarkerMeta?> selectedMarker;
|
||||
final LatLng? initialLocation;
|
||||
final ValueNotifier<MapMarker?> selectedMarker;
|
||||
final Geographic? initialLocation;
|
||||
|
||||
const _MapWithMarker({
|
||||
required this.style,
|
||||
required this.onMapCreated,
|
||||
required this.onMapMoved,
|
||||
required this.onMapClicked,
|
||||
required this.onMapEvent,
|
||||
required this.onStyleLoaded,
|
||||
required this.selectedMarker,
|
||||
this.onMarkerTapped,
|
||||
@@ -336,48 +327,44 @@ class _MapWithMarker extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (ctx, constraints) => SizedBox(
|
||||
height: constraints.maxHeight,
|
||||
width: constraints.maxWidth,
|
||||
child: Stack(
|
||||
children: [
|
||||
style.widgetWhen(
|
||||
onData: (style) => MapLibreMap(
|
||||
attributionButtonMargins: const Point(8, kToolbarHeight),
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: initialLocation ?? const LatLng(0, 0),
|
||||
zoom: initialLocation != null ? 12 : 0,
|
||||
),
|
||||
styleString: style,
|
||||
// This is needed to update the selectedMarker's position on map camera updates
|
||||
// The changes are notified through the mapController ValueListener which is added in [onMapCreated]
|
||||
trackCameraPosition: true,
|
||||
onMapCreated: onMapCreated,
|
||||
onCameraIdle: onMapMoved,
|
||||
onMapClick: onMapClicked,
|
||||
onStyleLoadedCallback: onStyleLoaded,
|
||||
tiltGesturesEnabled: false,
|
||||
dragEnabled: false,
|
||||
myLocationEnabled: false,
|
||||
attributionButtonPosition: AttributionButtonPosition.topRight,
|
||||
rotateGesturesEnabled: false,
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: selectedMarker,
|
||||
builder: (ctx, value, _) => value != null
|
||||
? PositionedAssetMarkerIcon(
|
||||
point: value.point,
|
||||
assetRemoteId: value.marker.assetRemoteId,
|
||||
assetThumbhash: '',
|
||||
durationInMilliseconds: value.shouldAnimate ? 100 : 0,
|
||||
onTap: onMarkerTapped,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
return style.widgetWhen(
|
||||
onData: (style) => MapLibreMap(
|
||||
options: MapOptions(
|
||||
initCenter: initialLocation ?? const Geographic(lat: 0, lon: 0),
|
||||
initZoom: initialLocation != null ? 12 : 0,
|
||||
initStyle: style,
|
||||
gestures: const MapGestures.all(pitch: false, rotate: false),
|
||||
),
|
||||
onMapCreated: onMapCreated,
|
||||
onStyleLoaded: onStyleLoaded,
|
||||
onEvent: onMapEvent,
|
||||
children: [
|
||||
ValueListenableBuilder<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:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -7,36 +5,34 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||
import 'package:immich_mobile/utils/map_utils.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MapLocationPickerPage extends HookConsumerWidget {
|
||||
final LatLng initialLatLng;
|
||||
final Geographic initialLatLng;
|
||||
|
||||
const MapLocationPickerPage({super.key, this.initialLatLng = const LatLng(0, 0)});
|
||||
const MapLocationPickerPage({super.key, this.initialLatLng = const Geographic(lat: 0, lon: 0)});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedLatLng = useValueNotifier<LatLng>(initialLatLng);
|
||||
final controller = useRef<MapLibreMapController?>(null);
|
||||
final marker = useRef<Symbol?>(null);
|
||||
final selectedLatLng = useValueNotifier<Geographic>(initialLatLng);
|
||||
final currentLatLng = useValueListenable(selectedLatLng);
|
||||
final controller = useRef<MapController?>(null);
|
||||
|
||||
Future<void> onStyleLoaded() async {
|
||||
marker.value = await controller.value?.addMarkerAtLatLng(initialLatLng);
|
||||
Future<void> onStyleLoaded(StyleController style) async {
|
||||
await style.addImageFromAssets(id: 'mapMarker', asset: 'assets/location-pin.png');
|
||||
}
|
||||
|
||||
Future<void> onMapClick(Point<num> _, LatLng centre) async {
|
||||
selectedLatLng.value = centre;
|
||||
await controller.value?.animateCamera(CameraUpdate.newLatLng(centre));
|
||||
if (marker.value != null) {
|
||||
await controller.value?.updateSymbol(marker.value!, SymbolOptions(geometry: centre));
|
||||
}
|
||||
void onEvent(MapEvent event) {
|
||||
if (event is! MapEventClick) return;
|
||||
|
||||
selectedLatLng.value = event.point;
|
||||
controller.value?.animateCamera(center: event.point);
|
||||
}
|
||||
|
||||
void onClose([LatLng? selected]) {
|
||||
void onClose([Geographic? selected]) {
|
||||
context.maybePop(selected);
|
||||
}
|
||||
|
||||
@@ -47,9 +43,9 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude);
|
||||
var currentLatLng = Geographic(lat: currentLocation.latitude, lon: currentLocation.longitude);
|
||||
selectedLatLng.value = currentLatLng;
|
||||
await controller.value?.animateCamera(CameraUpdate.newLatLngZoom(currentLatLng, 12));
|
||||
await controller.value?.animateCamera(center: currentLatLng, zoom: 12);
|
||||
}
|
||||
|
||||
return MapThemeOverride(
|
||||
@@ -66,18 +62,24 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)),
|
||||
),
|
||||
child: MapLibreMap(
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: initialLatLng,
|
||||
zoom: (initialLatLng.latitude == 0 && initialLatLng.longitude == 0) ? 1 : 12,
|
||||
options: MapOptions(
|
||||
initCenter: initialLatLng,
|
||||
initZoom: (initialLatLng.lat == 0 && initialLatLng.lon == 0) ? 1 : 12,
|
||||
initStyle: style,
|
||||
gestures: const MapGestures.all(pitch: false),
|
||||
),
|
||||
styleString: style,
|
||||
onMapCreated: (mapController) => controller.value = mapController,
|
||||
onStyleLoadedCallback: onStyleLoaded,
|
||||
onMapClick: onMapClick,
|
||||
dragEnabled: false,
|
||||
tiltGesturesEnabled: false,
|
||||
myLocationEnabled: false,
|
||||
attributionButtonMargins: const Point(20, 15),
|
||||
onStyleLoaded: onStyleLoaded,
|
||||
onEvent: onEvent,
|
||||
layers: [
|
||||
MarkerLayer(
|
||||
points: [Feature(geometry: Point(currentLatLng))],
|
||||
iconImage: 'mapMarker',
|
||||
iconSize: 0.15,
|
||||
iconAnchor: IconAnchor.bottom,
|
||||
iconAllowOverlap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -117,7 +119,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
}
|
||||
|
||||
class _BottomBar extends StatelessWidget {
|
||||
final ValueNotifier<LatLng> selectedLatLng;
|
||||
final ValueNotifier<Geographic> selectedLatLng;
|
||||
final Function() onUseLocation;
|
||||
final Function() onGetCurrentLocation;
|
||||
|
||||
@@ -140,8 +142,7 @@ class _BottomBar extends StatelessWidget {
|
||||
const SizedBox(width: 15),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: selectedLatLng,
|
||||
builder: (_, value, __) =>
|
||||
Text("${value.latitude.toStringAsFixed(4)}, ${value.longitude.toStringAsFixed(4)}"),
|
||||
builder: (_, value, __) => Text("${value.lat.toStringAsFixed(4)}, ${value.lon.toStringAsFixed(4)}"),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftLibraryPage extends ConsumerWidget {
|
||||
@@ -230,7 +230,7 @@ class _PlacesCollectionCard extends StatelessWidget {
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(
|
||||
zoom: 8,
|
||||
centre: const LatLng(21.44950, -157.91959),
|
||||
centre: const Geographic(lat: 21.44950, lon: -157.91959),
|
||||
showAttribution: false,
|
||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
|
||||
@@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftMapPage extends StatelessWidget {
|
||||
final LatLng? initialLocation;
|
||||
final Geographic? initialLocation;
|
||||
|
||||
const DriftMapPage({super.key, this.initialLocation});
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DriftPlacePage extends StatelessWidget {
|
||||
const DriftPlacePage({super.key, this.currentLocation});
|
||||
|
||||
final LatLng? currentLocation;
|
||||
final Geographic? currentLocation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -82,7 +82,7 @@ class _Map extends StatelessWidget {
|
||||
const _Map({required this.search, this.currentLocation});
|
||||
|
||||
final ValueNotifier<String?> search;
|
||||
final LatLng? currentLocation;
|
||||
final Geographic? currentLocation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -96,7 +96,7 @@ class _Map extends StatelessWidget {
|
||||
child: MapThumbnail(
|
||||
onTap: (_, __) => context.pushRoute(DriftMapRoute(initialLocation: currentLocation)),
|
||||
zoom: 8,
|
||||
centre: currentLocation ?? const LatLng(21.44950, -157.91959),
|
||||
centre: currentLocation ?? const Geographic(lat: 21.44950, lon: -157.91959),
|
||||
showAttribution: false,
|
||||
themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
|
||||
+6
-4
@@ -10,7 +10,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widge
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
class LocationDetails extends ConsumerStatefulWidget {
|
||||
const LocationDetails({super.key});
|
||||
@@ -20,7 +20,7 @@ class LocationDetails extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _LocationDetailsState extends ConsumerState<LocationDetails> {
|
||||
MapLibreMapController? _mapController;
|
||||
MapController? _mapController;
|
||||
|
||||
String? _getLocationName(ExifInfo? exifInfo) {
|
||||
if (exifInfo == null) {
|
||||
@@ -36,14 +36,16 @@ class _LocationDetailsState extends ConsumerState<LocationDetails> {
|
||||
return null;
|
||||
}
|
||||
|
||||
void _onMapCreated(MapLibreMapController controller) {
|
||||
void _onMapCreated(MapController controller) {
|
||||
_mapController = controller;
|
||||
}
|
||||
|
||||
void _onExifChanged(AsyncValue<ExifInfo?>? previous, AsyncValue<ExifInfo?> current) {
|
||||
final currentExif = current.valueOrNull;
|
||||
if (currentExif != null && currentExif.hasCoordinates) {
|
||||
_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentExif.latitude!, currentExif.longitude!)));
|
||||
_mapController?.moveCamera(
|
||||
center: Geographic(lat: currentExif.latitude!, lon: currentExif.longitude!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
|
||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
class MapState {
|
||||
final ThemeMode themeMode;
|
||||
final LatLngBounds bounds;
|
||||
final LngLatBounds bounds;
|
||||
final bool onlyFavorites;
|
||||
final bool includeArchived;
|
||||
final bool withPartners;
|
||||
@@ -35,7 +35,7 @@ class MapState {
|
||||
int get hashCode => bounds.hashCode;
|
||||
|
||||
MapState copyWith({
|
||||
LatLngBounds? bounds,
|
||||
LngLatBounds? bounds,
|
||||
ThemeMode? themeMode,
|
||||
bool? onlyFavorites,
|
||||
bool? includeArchived,
|
||||
@@ -64,7 +64,7 @@ class MapState {
|
||||
class MapStateNotifier extends Notifier<MapState> {
|
||||
MapStateNotifier();
|
||||
|
||||
bool setBounds(LatLngBounds bounds) {
|
||||
bool setBounds(LngLatBounds bounds) {
|
||||
if (state.bounds == bounds) {
|
||||
return false;
|
||||
}
|
||||
@@ -113,14 +113,14 @@ class MapStateNotifier extends Notifier<MapState> {
|
||||
includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived),
|
||||
withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners),
|
||||
relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate),
|
||||
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
|
||||
bounds: const LngLatBounds(longitudeWest: 0, longitudeEast: 0, latitudeSouth: 0, latitudeNorth: 0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This provider watches the markers from the map service and serves the markers.
|
||||
// It should be used only after the map service provider is overridden
|
||||
final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LatLngBounds?>((ref, bounds) async {
|
||||
final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LngLatBounds?>((ref, bounds) async {
|
||||
final mapService = ref.watch(mapServiceProvider);
|
||||
final markers = await mapService.getMarkers(bounds);
|
||||
final features = List.filled(markers.length, const <String, dynamic>{});
|
||||
@@ -131,7 +131,7 @@ final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LatLngBoun
|
||||
'id': marker.assetId,
|
||||
'geometry': {
|
||||
'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:io';
|
||||
import 'dart:math';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
@@ -20,27 +19,10 @@ import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
class CustomSourceProperties implements SourceProperties {
|
||||
final Map<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,
|
||||
};
|
||||
}
|
||||
}
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
class DriftMap extends ConsumerStatefulWidget {
|
||||
final LatLng? initialLocation;
|
||||
final Geographic? initialLocation;
|
||||
|
||||
const DriftMap({super.key, this.initialLocation});
|
||||
|
||||
@@ -49,7 +31,7 @@ class DriftMap extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
MapLibreMapController? mapController;
|
||||
MapController? mapController;
|
||||
final _reloadMutex = AsyncMutex();
|
||||
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
|
||||
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
|
||||
@@ -69,7 +51,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onMapCreated(MapLibreMapController controller) {
|
||||
void onMapCreated(MapController controller) {
|
||||
mapController = controller;
|
||||
}
|
||||
|
||||
@@ -81,43 +63,23 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
return;
|
||||
}
|
||||
|
||||
await controller.addSource(
|
||||
MapUtils.defaultSourceId,
|
||||
const CustomSourceProperties(data: {'type': 'FeatureCollection', 'features': []}),
|
||||
await controller.style!.addSource(
|
||||
GeoJsonSource(id: MapUtils.defaultSourceId, data: jsonEncode({'type': 'FeatureCollection', 'features': []})),
|
||||
);
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
await controller.addCircleLayer(
|
||||
MapUtils.defaultSourceId,
|
||||
MapUtils.defaultHeatMapLayerId,
|
||||
const CircleLayerProperties(
|
||||
circleRadius: 10,
|
||||
circleColor: "rgba(150,86,34,0.7)",
|
||||
circleBlur: 1.0,
|
||||
circleOpacity: 0.7,
|
||||
circleStrokeWidth: 0.1,
|
||||
circleStrokeColor: "rgba(203,46,19,0.5)",
|
||||
circleStrokeOpacity: 0.7,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
await controller.addHeatmapLayer(
|
||||
MapUtils.defaultSourceId,
|
||||
MapUtils.defaultHeatMapLayerId,
|
||||
MapUtils.defaultHeatmapLayerProperties,
|
||||
);
|
||||
}
|
||||
await controller.style!.addLayer(
|
||||
const HeatmapStyleLayer(
|
||||
id: MapUtils.defaultHeatMapLayerId,
|
||||
sourceId: MapUtils.defaultSourceId,
|
||||
paint: MapUtils.defaultHeatmapLayerPaint,
|
||||
),
|
||||
);
|
||||
|
||||
_debouncer.run(() => setBounds(forceReload: true));
|
||||
controller.addListener(onMapMoved);
|
||||
}
|
||||
|
||||
void onMapMoved() {
|
||||
if (mapController!.isCameraMoving || !mounted) {
|
||||
return;
|
||||
}
|
||||
void onMapEvent(MapEvent event) {
|
||||
if (event is! MapEventCameraIdle || !mounted) return;
|
||||
|
||||
_debouncer.run(setBounds);
|
||||
}
|
||||
@@ -136,7 +98,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
return;
|
||||
}
|
||||
|
||||
final bounds = await controller.getVisibleRegion();
|
||||
final bounds = controller.getVisibleRegion();
|
||||
unawaited(
|
||||
_reloadMutex.run(() async {
|
||||
if (mounted && (ref.read(mapStateProvider.notifier).setBounds(bounds) || forceReload)) {
|
||||
@@ -153,7 +115,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
return;
|
||||
}
|
||||
|
||||
await controller.setGeoJsonSource(MapUtils.defaultSourceId, markers);
|
||||
await controller.style!.updateGeoJsonSource(id: MapUtils.defaultSourceId, data: jsonEncode(markers));
|
||||
}
|
||||
|
||||
Future<void> onZoomToLocation() async {
|
||||
@@ -173,8 +135,9 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
final controller = mapController;
|
||||
if (controller != null && location != null) {
|
||||
await controller.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), MapUtils.mapZoomToAssetLevel),
|
||||
duration: const Duration(milliseconds: 800),
|
||||
center: Geographic(lat: location.latitude, lon: location.longitude),
|
||||
zoom: MapUtils.mapZoomToAssetLevel,
|
||||
nativeDuration: Durations.extralong2,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -183,7 +146,12 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
_Map(initialLocation: widget.initialLocation, onMapCreated: onMapCreated, onMapReady: onMapReady),
|
||||
_Map(
|
||||
initialLocation: widget.initialLocation,
|
||||
onMapCreated: onMapCreated,
|
||||
onMapReady: onMapReady,
|
||||
onMapEvent: onMapEvent,
|
||||
),
|
||||
_DynamicBottomSheet(bottomSheetOffset: bottomSheetOffset),
|
||||
_DynamicMyLocationButton(onZoomToLocation: onZoomToLocation, bottomSheetOffset: bottomSheetOffset),
|
||||
],
|
||||
@@ -192,13 +160,13 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
||||
}
|
||||
|
||||
class _Map extends StatelessWidget {
|
||||
final LatLng? initialLocation;
|
||||
final Geographic? initialLocation;
|
||||
|
||||
const _Map({this.initialLocation, required this.onMapCreated, required this.onMapReady});
|
||||
|
||||
final MapCreatedCallback onMapCreated;
|
||||
const _Map({this.initialLocation, required this.onMapCreated, required this.onMapReady, required this.onMapEvent});
|
||||
|
||||
final void Function(MapController) onMapCreated;
|
||||
final VoidCallback onMapReady;
|
||||
final void Function(MapEvent) onMapEvent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -206,16 +174,15 @@ class _Map extends StatelessWidget {
|
||||
return MapThemeOverride(
|
||||
mapBuilder: (style) => style.widgetWhen(
|
||||
onData: (style) => MapLibreMap(
|
||||
initialCameraPosition: initialLocation == null
|
||||
? const CameraPosition(target: LatLng(0, 0), zoom: 0)
|
||||
: CameraPosition(target: initialLocation, zoom: MapUtils.mapZoomToAssetLevel),
|
||||
compassEnabled: false,
|
||||
rotateGesturesEnabled: false,
|
||||
styleString: style,
|
||||
options: MapOptions(
|
||||
initCenter: initialLocation ?? const Geographic(lat: 0, lon: 0),
|
||||
initZoom: initialLocation == null ? 0 : MapUtils.mapZoomToAssetLevel,
|
||||
initStyle: style,
|
||||
gestures: const MapGestures.all(rotate: false),
|
||||
),
|
||||
onMapCreated: onMapCreated,
|
||||
onStyleLoadedCallback: onMapReady,
|
||||
attributionButtonPosition: AttributionButtonPosition.topRight,
|
||||
attributionButtonMargins: const Point(8, kToolbarHeight),
|
||||
onStyleLoaded: (_) => onMapReady(),
|
||||
onEvent: onMapEvent,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:geolocator/geolocator.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
class MapUtils {
|
||||
static final Logger _logger = Logger("MapUtils");
|
||||
@@ -13,49 +12,37 @@ class MapUtils {
|
||||
static const mapZoomToAssetLevel = 12.0;
|
||||
static const defaultSourceId = 'asset-map-markers';
|
||||
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
||||
static var markerCompleter = Completer()..complete();
|
||||
|
||||
static const defaultCircleLayerLayerProperties = CircleLayerProperties(
|
||||
circleRadius: 10,
|
||||
circleColor: "rgba(150,86,34,0.7)",
|
||||
circleBlur: 1.0,
|
||||
circleOpacity: 0.7,
|
||||
circleStrokeWidth: 0.1,
|
||||
circleStrokeColor: "rgba(203,46,19,0.5)",
|
||||
circleStrokeOpacity: 0.7,
|
||||
);
|
||||
|
||||
static const defaultHeatmapLayerProperties = HeatmapLayerProperties(
|
||||
heatmapColor: [
|
||||
Expressions.interpolate,
|
||||
["linear"],
|
||||
["heatmap-density"],
|
||||
static const defaultHeatmapLayerPaint = <String, Object>{
|
||||
'heatmap-color': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['heatmap-density'],
|
||||
0.0,
|
||||
"rgba(103,58,183,0.0)",
|
||||
'rgba(103,58,183,0.0)',
|
||||
0.3,
|
||||
"rgb(103,58,183)",
|
||||
'rgb(103,58,183)',
|
||||
0.5,
|
||||
"rgb(33,149,243)",
|
||||
'rgb(33,149,243)',
|
||||
0.7,
|
||||
"rgb(76,175,79)",
|
||||
'rgb(76,175,79)',
|
||||
0.95,
|
||||
"rgb(255,235,59)",
|
||||
'rgb(255,235,59)',
|
||||
1.0,
|
||||
"rgb(255,86,34)",
|
||||
'rgb(255,86,34)',
|
||||
],
|
||||
heatmapIntensity: [
|
||||
Expressions.interpolate,
|
||||
["linear"],
|
||||
[Expressions.zoom],
|
||||
'heatmap-intensity': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
0,
|
||||
0.5,
|
||||
9,
|
||||
2,
|
||||
],
|
||||
heatmapRadius: [
|
||||
Expressions.interpolate,
|
||||
["linear"],
|
||||
[Expressions.zoom],
|
||||
'heatmap-radius': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
0,
|
||||
4,
|
||||
4,
|
||||
@@ -63,8 +50,8 @@ class MapUtils {
|
||||
9,
|
||||
16,
|
||||
],
|
||||
heatmapOpacity: 0.7,
|
||||
);
|
||||
'heatmap-opacity': 0.7,
|
||||
};
|
||||
|
||||
static Future<(Position?, LocationPermission?)> checkPermAndGetLocation({
|
||||
required BuildContext context,
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/stack.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final assetApiRepositoryProvider = Provider(
|
||||
@@ -62,8 +62,8 @@ class AssetApiRepository extends ApiRepository {
|
||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite));
|
||||
}
|
||||
|
||||
Future<void> updateLocation(List<String> ids, LatLng location) async {
|
||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude));
|
||||
Future<void> updateLocation(List<String> ids, Geographic location) async {
|
||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.lat, longitude: location.lon));
|
||||
}
|
||||
|
||||
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/secure_storage.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
part 'router.gr.dart';
|
||||
|
||||
|
||||
@@ -1226,7 +1226,7 @@ class DriftLockedFolderRoute extends PageRouteInfo<void> {
|
||||
class DriftMapRoute extends PageRouteInfo<DriftMapRouteArgs> {
|
||||
DriftMapRoute({
|
||||
Key? key,
|
||||
LatLng? initialLocation,
|
||||
Geographic? initialLocation,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
DriftMapRoute.name,
|
||||
@@ -1252,7 +1252,7 @@ class DriftMapRouteArgs {
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng? initialLocation;
|
||||
final Geographic? initialLocation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -1461,7 +1461,7 @@ class DriftPlaceDetailRouteArgs {
|
||||
class DriftPlaceRoute extends PageRouteInfo<DriftPlaceRouteArgs> {
|
||||
DriftPlaceRoute({
|
||||
Key? key,
|
||||
LatLng? currentLocation,
|
||||
Geographic? currentLocation,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
DriftPlaceRoute.name,
|
||||
@@ -1490,7 +1490,7 @@ class DriftPlaceRouteArgs {
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng? currentLocation;
|
||||
final Geographic? currentLocation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -2011,7 +2011,7 @@ class MainTimelineRoute extends PageRouteInfo<void> {
|
||||
class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
||||
MapLocationPickerRoute({
|
||||
Key? key,
|
||||
LatLng initialLatLng = const LatLng(0, 0),
|
||||
Geographic initialLatLng = const Geographic(lat: 0, lon: 0),
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
MapLocationPickerRoute.name,
|
||||
@@ -2041,12 +2041,12 @@ class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
|
||||
class MapLocationPickerRouteArgs {
|
||||
const MapLocationPickerRouteArgs({
|
||||
this.key,
|
||||
this.initialLatLng = const LatLng(0, 0),
|
||||
this.initialLatLng = const Geographic(lat: 0, lon: 0),
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng initialLatLng;
|
||||
final Geographic initialLatLng;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -2057,12 +2057,15 @@ class MapLocationPickerRouteArgs {
|
||||
/// generated route for
|
||||
/// [MapPage]
|
||||
class MapRoute extends PageRouteInfo<MapRouteArgs> {
|
||||
MapRoute({Key? key, LatLng? initialLocation, List<PageRouteInfo>? children})
|
||||
: super(
|
||||
MapRoute.name,
|
||||
args: MapRouteArgs(key: key, initialLocation: initialLocation),
|
||||
initialChildren: children,
|
||||
);
|
||||
MapRoute({
|
||||
Key? key,
|
||||
Geographic? initialLocation,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
MapRoute.name,
|
||||
args: MapRouteArgs(key: key, initialLocation: initialLocation),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'MapRoute';
|
||||
|
||||
@@ -2082,7 +2085,7 @@ class MapRouteArgs {
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng? initialLocation;
|
||||
final Geographic? initialLocation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -2403,7 +2406,7 @@ class PinAuthRouteArgs {
|
||||
class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
||||
PlacesCollectionRoute({
|
||||
Key? key,
|
||||
LatLng? currentLocation,
|
||||
Geographic? currentLocation,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
PlacesCollectionRoute.name,
|
||||
@@ -2435,7 +2438,7 @@ class PlacesCollectionRouteArgs {
|
||||
|
||||
final Key? key;
|
||||
|
||||
final LatLng? currentLocation;
|
||||
final Geographic? currentLocation;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
||||
@@ -22,7 +22,7 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/timezone.dart';
|
||||
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
||||
import 'package:maplibre/maplibre.dart' as maplibre;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
final actionServiceProvider = Provider<ActionService>(
|
||||
@@ -131,12 +131,12 @@ class ActionService {
|
||||
}
|
||||
|
||||
Future<bool> editLocation(List<String> remoteIds, BuildContext context) async {
|
||||
maplibre.LatLng? initialLatLng;
|
||||
maplibre.Geographic? initialLatLng;
|
||||
if (remoteIds.length == 1) {
|
||||
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
||||
|
||||
if (exif?.latitude != null && exif?.longitude != null) {
|
||||
initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!);
|
||||
initialLatLng = maplibre.Geographic(lat: exif!.latitude!, lon: exif.longitude!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/backup.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
@@ -236,12 +236,12 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>?> changeLocation(List<Asset> assets, LatLng location) async {
|
||||
Future<List<Asset>?> changeLocation(List<Asset> assets, Geographic location) async {
|
||||
try {
|
||||
await updateAssets(assets, UpdateAssetDto(latitude: location.latitude, longitude: location.longitude));
|
||||
await updateAssets(assets, UpdateAssetDto(latitude: location.lat, longitude: location.lon));
|
||||
|
||||
for (var element in assets) {
|
||||
element.exifInfo = element.exifInfo?.copyWith(latitude: location.latitude, longitude: location.longitude);
|
||||
element.exifInfo = element.exifInfo?.copyWith(latitude: location.lat, longitude: location.lon);
|
||||
}
|
||||
|
||||
await _syncService.upsertAssetsWithExif(assets);
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
|
||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/user_agent.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
class MapService with ErrorLoggerMixin {
|
||||
final ApiService _apiService;
|
||||
@override
|
||||
final logger = Logger("MapService");
|
||||
|
||||
MapService(this._apiService) {
|
||||
_setMapUserAgentHeader();
|
||||
}
|
||||
|
||||
Future<void> _setMapUserAgentHeader() async {
|
||||
final userAgent = await getUserAgentString();
|
||||
await setHttpHeaders({'User-Agent': userAgent});
|
||||
}
|
||||
MapService(this._apiService);
|
||||
|
||||
Future<Iterable<MapMarker>> getMapMarkers({
|
||||
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/widgets/common/confirm_dialog.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
class MapUtils {
|
||||
const MapUtils._();
|
||||
@@ -15,46 +14,53 @@ class MapUtils {
|
||||
static const defaultSourceId = 'asset-map-markers';
|
||||
static const defaultHeatMapLayerId = 'asset-heatmap-layer';
|
||||
|
||||
static const defaultHeatMapLayerProperties = HeatmapLayerProperties(
|
||||
heatmapColor: [
|
||||
Expressions.interpolate,
|
||||
["linear"],
|
||||
["heatmap-density"],
|
||||
static const defaultHeatMapLayerPaint = <String, Object>{
|
||||
'heatmap-color': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['heatmap-density'],
|
||||
0.0,
|
||||
"rgba(103,58,183,0.0)",
|
||||
'rgba(103,58,183,0.0)',
|
||||
0.3,
|
||||
"rgb(103,58,183)",
|
||||
'rgb(103,58,183)',
|
||||
0.5,
|
||||
"rgb(33,149,243)",
|
||||
'rgb(33,149,243)',
|
||||
0.7,
|
||||
"rgb(76,175,79)",
|
||||
'rgb(76,175,79)',
|
||||
0.95,
|
||||
"rgb(255,235,59)",
|
||||
'rgb(255,235,59)',
|
||||
1.0,
|
||||
"rgb(255,86,34)",
|
||||
'rgb(255,86,34)',
|
||||
],
|
||||
heatmapIntensity: [
|
||||
Expressions.interpolate, ["linear"], //
|
||||
[Expressions.zoom],
|
||||
0, 0.5,
|
||||
9, 2,
|
||||
'heatmap-intensity': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
0,
|
||||
0.5,
|
||||
9,
|
||||
2,
|
||||
],
|
||||
heatmapRadius: [
|
||||
Expressions.interpolate, ["linear"], //
|
||||
[Expressions.zoom],
|
||||
0, 4,
|
||||
4, 8,
|
||||
9, 16,
|
||||
'heatmap-radius': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
0,
|
||||
4,
|
||||
4,
|
||||
8,
|
||||
9,
|
||||
16,
|
||||
],
|
||||
heatmapOpacity: 0.7,
|
||||
);
|
||||
'heatmap-opacity': 0.7,
|
||||
};
|
||||
|
||||
static Map<String, dynamic> _addFeature(MapMarker marker) => {
|
||||
'type': 'Feature',
|
||||
'id': marker.assetRemoteId,
|
||||
'geometry': {
|
||||
'type': 'Point',
|
||||
'coordinates': [marker.latLng.longitude, marker.latLng.latitude],
|
||||
'coordinates': [marker.latLng.lon, marker.latLng.lat],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||
import 'package:immich_mobile/widgets/common/share_dialog.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
void handleShareAssets(WidgetRef ref, BuildContext context, Iterable<Asset> selection) {
|
||||
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 {
|
||||
LatLng? initialLatLng;
|
||||
Geographic? initialLatLng;
|
||||
if (selection.length == 1) {
|
||||
final asset = selection.first;
|
||||
final assetWithExif = await ref.watch(assetServiceProvider).loadExif(asset);
|
||||
if (assetWithExif.exifInfo?.latitude != null && assetWithExif.exifInfo?.longitude != null) {
|
||||
initialLatLng = LatLng(assetWithExif.exifInfo!.latitude!, assetWithExif.exifInfo!.longitude!);
|
||||
initialLatLng = Geographic(lat: assetWithExif.exifInfo!.latitude!, lon: assetWithExif.exifInfo!.longitude!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ExifMap extends StatelessWidget {
|
||||
@@ -15,7 +15,7 @@ class ExifMap extends StatelessWidget {
|
||||
// reusing this component
|
||||
final String? markerId;
|
||||
final String? markerAssetThumbhash;
|
||||
final MapCreatedCallback? onMapCreated;
|
||||
final void Function(MapController)? onMapCreated;
|
||||
|
||||
const ExifMap({
|
||||
super.key,
|
||||
@@ -66,7 +66,7 @@ class ExifMap extends StatelessWidget {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return MapThumbnail(
|
||||
centre: LatLng(exifInfo.latitude ?? 0, exifInfo.longitude ?? 0),
|
||||
centre: Geographic(lat: exifInfo.latitude ?? 0, lon: exifInfo.longitude ?? 0),
|
||||
height: 150,
|
||||
width: constraints.maxWidth,
|
||||
zoom: 12.0,
|
||||
|
||||
@@ -6,10 +6,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
Future<LatLng?> showLocationPicker({required BuildContext context, LatLng? initialLatLng}) {
|
||||
return showDialog<LatLng?>(
|
||||
Future<Geographic?> showLocationPicker({required BuildContext context, Geographic? initialLatLng}) {
|
||||
return showDialog<Geographic?>(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (ctx) => _LocationPicker(initialLatLng: initialLatLng),
|
||||
@@ -17,7 +17,7 @@ Future<LatLng?> showLocationPicker({required BuildContext context, LatLng? initi
|
||||
}
|
||||
|
||||
class _LocationPicker extends HookWidget {
|
||||
final LatLng? initialLatLng;
|
||||
final Geographic? initialLatLng;
|
||||
|
||||
const _LocationPicker({this.initialLatLng});
|
||||
|
||||
@@ -33,9 +33,9 @@ class _LocationPicker extends HookWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final latitude = useState(initialLatLng?.latitude ?? 0.0);
|
||||
final longitude = useState(initialLatLng?.longitude ?? 0.0);
|
||||
final latlng = LatLng(latitude.value, longitude.value);
|
||||
final latitude = useState(initialLatLng?.lat ?? 0.0);
|
||||
final longitude = useState(initialLatLng?.lon ?? 0.0);
|
||||
final latlng = Geographic(lat: latitude.value, lon: longitude.value);
|
||||
final latitiudeFocusNode = useFocusNode();
|
||||
final longitudeFocusNode = useFocusNode();
|
||||
final latitudeController = useTextEditingController(text: latitude.value.toStringAsFixed(4));
|
||||
@@ -48,10 +48,10 @@ class _LocationPicker extends HookWidget {
|
||||
}, [latitude.value, longitude.value]);
|
||||
|
||||
Future<void> onMapTap() async {
|
||||
final newLatLng = await context.pushRoute<LatLng?>(MapLocationPickerRoute(initialLatLng: latlng));
|
||||
final newLatLng = await context.pushRoute<Geographic?>(MapLocationPickerRoute(initialLatLng: latlng));
|
||||
if (newLatLng != null) {
|
||||
latitude.value = newLatLng.latitude;
|
||||
longitude.value = newLatLng.longitude;
|
||||
latitude.value = newLatLng.lat;
|
||||
longitude.value = newLatLng.lon;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,36 +51,35 @@ class MapAssetGrid extends HookConsumerWidget {
|
||||
final assetCache = useRef<Map<String, Asset>>({});
|
||||
|
||||
void handleMapEvents(MapEvent event) async {
|
||||
if (event is MapAssetsInBoundsUpdated) {
|
||||
final assetIds = event.assetRemoteIds;
|
||||
final missingIds = <String>[];
|
||||
final currentAssets = <Asset>[];
|
||||
if (event is! MapAssetsInBoundsUpdated) return;
|
||||
|
||||
for (final id in assetIds) {
|
||||
final asset = assetCache.value[id];
|
||||
if (asset != null) {
|
||||
currentAssets.add(asset);
|
||||
} else {
|
||||
missingIds.add(id);
|
||||
}
|
||||
final assetIds = event.assetRemoteIds;
|
||||
final missingIds = <String>[];
|
||||
final currentAssets = <Asset>[];
|
||||
|
||||
for (final id in assetIds) {
|
||||
final asset = assetCache.value[id];
|
||||
if (asset != null) {
|
||||
currentAssets.add(asset);
|
||||
} else {
|
||||
missingIds.add(id);
|
||||
}
|
||||
|
||||
// Only fetch missing assets
|
||||
if (missingIds.isNotEmpty) {
|
||||
final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds);
|
||||
|
||||
// Add new assets to cache and current list
|
||||
for (final asset in newAssets) {
|
||||
if (asset.remoteId != null) {
|
||||
assetCache.value[asset.remoteId!] = asset;
|
||||
currentAssets.add(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assetsInBounds.value = currentAssets;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only fetch missing assets
|
||||
if (missingIds.isNotEmpty) {
|
||||
final newAssets = await ref.read(dbProvider).assets.getAllByRemoteId(missingIds);
|
||||
|
||||
// Add new assets to cache and current list
|
||||
for (final asset in newAssets) {
|
||||
if (asset.remoteId != null) {
|
||||
assetCache.value[asset.remoteId!] = asset;
|
||||
currentAssets.add(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assetsInBounds.value = currentAssets;
|
||||
}
|
||||
|
||||
useOnStreamChange<MapEvent>(mapEventStream, onData: handleMapEvents);
|
||||
|
||||
@@ -33,13 +33,9 @@ class MapBottomSheet extends HookConsumerWidget {
|
||||
final isBottomSheetOpened = useRef(false);
|
||||
|
||||
void handleMapEvents(MapEvent event) async {
|
||||
if (event is MapCloseBottomSheet) {
|
||||
await sheetController.animateTo(
|
||||
0.1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.linearToEaseOut,
|
||||
);
|
||||
}
|
||||
if (event is! MapCloseBottomSheet) return;
|
||||
|
||||
await sheetController.animateTo(0.1, duration: const Duration(milliseconds: 200), curve: Curves.linearToEaseOut);
|
||||
}
|
||||
|
||||
useOnStreamChange<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/translate_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
class MapThemePicker extends StatelessWidget {
|
||||
final ThemeMode themeMode;
|
||||
@@ -78,7 +78,7 @@ class _BorderedMapThumbnail extends StatelessWidget {
|
||||
),
|
||||
child: MapThumbnail(
|
||||
zoom: 2,
|
||||
centre: const LatLng(47, 5),
|
||||
centre: const Geographic(lat: 47, lon: 5),
|
||||
onTap: (_, __) => onThemeChange(mode),
|
||||
themeMode: mode,
|
||||
showAttribution: false,
|
||||
|
||||
@@ -84,8 +84,13 @@ class _MapThemeOverrideState extends ConsumerState<MapThemeOverride> with Widget
|
||||
data: _isDarkTheme
|
||||
? getThemeData(colorScheme: appTheme.dark, locale: locale)
|
||||
: getThemeData(colorScheme: appTheme.light, locale: locale),
|
||||
child: widget.mapBuilder.call(
|
||||
ref.watch(mapStateNotifierProvider.select((v) => _isDarkTheme ? v.darkStyleFetched : v.lightStyleFetched)),
|
||||
// Key on _isDarkTheme to force MapLibreMap recreation on theme change,
|
||||
// since initStyle is only applied on creation.
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(_isDarkTheme),
|
||||
child: widget.mapBuilder.call(
|
||||
ref.watch(mapStateNotifierProvider.select((v) => _isDarkTheme ? v.darkStyleFetched : v.lightStyleFetched)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||
import 'package:immich_mobile/widgets/map/asset_marker_icon.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
/// A non-interactive thumbnail of a map in the given coordinates with optional markers
|
||||
///
|
||||
@@ -16,8 +13,8 @@ import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
/// [showMarkerPin] to true which would display a marker pin instead. If both are provided,
|
||||
/// [assetMarkerRemoteId] will take precedence
|
||||
class MapThumbnail extends HookConsumerWidget {
|
||||
final Function(Point<double>, LatLng)? onTap;
|
||||
final LatLng centre;
|
||||
final Function(Offset, Geographic)? onTap;
|
||||
final Geographic centre;
|
||||
final String? assetMarkerRemoteId;
|
||||
final String? assetThumbhash;
|
||||
final bool showMarkerPin;
|
||||
@@ -26,7 +23,7 @@ class MapThumbnail extends HookConsumerWidget {
|
||||
final double width;
|
||||
final ThemeMode? themeMode;
|
||||
final bool showAttribution;
|
||||
final MapCreatedCallback? onCreated;
|
||||
final void Function(MapController)? onCreated;
|
||||
|
||||
const MapThumbnail({
|
||||
super.key,
|
||||
@@ -45,28 +42,21 @@ class MapThumbnail extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = useRef<MapLibreMapController?>(null);
|
||||
final styleLoaded = useState(false);
|
||||
|
||||
Future<void> onMapCreated(MapLibreMapController mapController) async {
|
||||
controller.value = mapController;
|
||||
styleLoaded.value = false;
|
||||
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
|
||||
Future<void> onStyleLoaded(StyleController style) async {
|
||||
if (showMarkerPin) {
|
||||
await style.addImageFromAssets(id: 'mapMarker', asset: 'assets/location-pin.png');
|
||||
}
|
||||
styleLoaded.value = true;
|
||||
}
|
||||
|
||||
void onEvent(MapEvent event) {
|
||||
if (event is MapEventClick && onTap != null) {
|
||||
onTap!(event.screenPoint, event.point);
|
||||
}
|
||||
}
|
||||
|
||||
return MapThemeOverride(
|
||||
themeMode: themeMode,
|
||||
mapBuilder: (style) => AnimatedContainer(
|
||||
@@ -80,37 +70,41 @@ class MapThumbnail extends HookConsumerWidget {
|
||||
width: width,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||
child: Stack(
|
||||
alignment: AlignmentGeometry.topCenter,
|
||||
children: [
|
||||
style.widgetWhen(
|
||||
onData: (style) => MapLibreMap(
|
||||
initialCameraPosition: CameraPosition(target: centre, zoom: zoom),
|
||||
styleString: style,
|
||||
onMapCreated: onMapCreated,
|
||||
onStyleLoadedCallback: onStyleLoaded,
|
||||
onMapClick: onTap,
|
||||
doubleClickZoomEnabled: false,
|
||||
dragEnabled: false,
|
||||
zoomGesturesEnabled: false,
|
||||
tiltGesturesEnabled: false,
|
||||
scrollGesturesEnabled: false,
|
||||
rotateGesturesEnabled: false,
|
||||
myLocationEnabled: false,
|
||||
attributionButtonMargins: showAttribution == false ? const Point(-100, 0) : null,
|
||||
),
|
||||
child: style.widgetWhen(
|
||||
onData: (style) => MapLibreMap(
|
||||
options: MapOptions(
|
||||
initCenter: Geographic(lat: centre.lat + 0.002, lon: centre.lon),
|
||||
initZoom: zoom,
|
||||
initStyle: style,
|
||||
gestures: const MapGestures.none(),
|
||||
),
|
||||
if (assetMarkerRemoteId != null && assetThumbhash != null)
|
||||
Container(
|
||||
width: width,
|
||||
height: height / 2,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: SizedBox.square(
|
||||
dimension: height / 2.5,
|
||||
child: AssetMarkerIcon(id: assetMarkerRemoteId!, thumbhash: assetThumbhash!),
|
||||
onMapCreated: onCreated,
|
||||
onStyleLoaded: onStyleLoaded,
|
||||
onEvent: onEvent,
|
||||
layers: [
|
||||
if (showMarkerPin)
|
||||
MarkerLayer(
|
||||
points: [Feature(geometry: Point(centre))],
|
||||
iconImage: 'mapMarker',
|
||||
iconSize: 0.15,
|
||||
iconAnchor: IconAnchor.bottom,
|
||||
iconAllowOverlap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
children: [
|
||||
if (assetMarkerRemoteId != null && assetThumbhash != null)
|
||||
WidgetLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
point: centre,
|
||||
size: Size.square(height / 2),
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: AssetMarkerIcon(id: assetMarkerRemoteId!, thumbhash: assetThumbhash!),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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/widgets/map/map_thumbnail.dart';
|
||||
import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:maplibre/maplibre.dart';
|
||||
|
||||
class SearchMapThumbnail extends StatelessWidget {
|
||||
const SearchMapThumbnail({super.key, this.size = 60.0});
|
||||
@@ -20,7 +20,13 @@ class SearchMapThumbnail extends StatelessWidget {
|
||||
context.pushRoute(MapRoute());
|
||||
},
|
||||
child: IgnorePointer(
|
||||
child: MapThumbnail(zoom: 2, centre: const LatLng(47, 5), height: size, width: size, showAttribution: false),
|
||||
child: MapThumbnail(
|
||||
zoom: 2,
|
||||
centre: const Geographic(lat: 47, lon: 5),
|
||||
height: size,
|
||||
width: size,
|
||||
showAttribution: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user