Explorar o código

feat(web): bundle and 'sveltify' leaflet (#1998)

* feat(web): bundle and 'sveltify' leaflet

* lazy load leaflet components

* add correct icon sizes
Michel Heusschen %!s(int64=2) %!d(string=hai) anos
pai
achega
b29c43d86a

+ 24 - 52
web/src/lib/components/asset-viewer/detail-panel.svelte

@@ -4,60 +4,23 @@
 	import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
 	import CameraIris from 'svelte-material-icons/CameraIris.svelte';
 	import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
-	import { createEventDispatcher, onMount } from 'svelte';
-	import { browser } from '$app/environment';
+	import { createEventDispatcher } from 'svelte';
 	import { AssetResponseDto, AlbumResponseDto } from '@api';
 	import { asByteUnitString } from '../../utils/byte-units';
 	import { locale } from '$lib/stores/preferences.store';
-
-	type Leaflet = typeof import('leaflet');
-	type LeafletMap = import('leaflet').Map;
-	type LeafletMarker = import('leaflet').Marker;
-
-	// Map Property
-	let map: LeafletMap;
-	let leaflet: Leaflet;
-	let marker: LeafletMarker;
+	import type { LatLngTuple } from 'leaflet';
 
 	export let asset: AssetResponseDto;
-	$: if (asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null) {
-		drawMap(asset.exifInfo.latitude, asset.exifInfo.longitude);
-	}
-
 	export let albums: AlbumResponseDto[] = [];
 
-	onMount(async () => {
-		if (browser) {
-			if (asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null) {
-				await drawMap(asset.exifInfo.latitude, asset.exifInfo.longitude);
-			}
-		}
-	});
-
-	async function drawMap(lat: number, lon: number) {
-		if (!leaflet) {
-			leaflet = await import('leaflet');
-		}
-
-		if (!map) {
-			map = leaflet.map('map');
-			leaflet
-				.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
-					attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
-				})
-				.addTo(map);
-		}
+	$: latlng = (() => {
+		const lat = asset.exifInfo?.latitude;
+		const lng = asset.exifInfo?.longitude;
 
-		if (marker) {
-			map.removeLayer(marker);
+		if (lat && lng) {
+			return [lat, lng] as LatLngTuple;
 		}
-
-		map.setView([lat || 0, lon || 0], 17);
-
-		marker = leaflet.marker([lat || 0, lon || 0]);
-		marker.bindPopup(`${(lat || 0).toFixed(7)},${(lon || 0).toFixed(7)}`);
-		map.addLayer(marker);
-	}
+	})();
 
 	const dispatch = createEventDispatcher();
 
@@ -186,9 +149,22 @@
 	</div>
 </section>
 
-<div class={`${asset.exifInfo?.latitude ? 'visible' : 'hidden'}`}>
-	<div class="h-[360px] w-full" id="map" />
-</div>
+{#if latlng}
+	<div class="h-[360px]">
+		{#await import('../shared-components/leaflet') then { Map, TileLayer, Marker }}
+			<Map {latlng} zoom={14}>
+				<TileLayer
+					urlTemplate={'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'}
+					options={{
+						attribution:
+							'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
+					}}
+				/>
+				<Marker {latlng} popupContent="{latlng[0].toFixed(7)},{latlng[1].toFixed(7)}" />
+			</Map>
+		{/await}
+	</div>
+{/if}
 
 <section class="p-2 dark:text-immich-dark-fg">
 	<div class="px-4 py-4">
@@ -225,7 +201,3 @@
 		{/each}
 	</div>
 </section>
-
-<style>
-	@import 'https://unpkg.com/leaflet@1.7.1/dist/leaflet.css';
-</style>

+ 3 - 0
web/src/lib/components/shared-components/leaflet/index.ts

@@ -0,0 +1,3 @@
+export { default as Map } from './map.svelte';
+export { default as Marker } from './marker.svelte';
+export { default as TileLayer } from './tile-layer.svelte';

+ 42 - 0
web/src/lib/components/shared-components/leaflet/map.svelte

@@ -0,0 +1,42 @@
+<script lang="ts" context="module">
+	import { createContext } from '$lib/utils/context';
+
+	const { get: getContext, set: setMapContext } = createContext<() => Map>();
+
+	export const getMapContext = () => {
+		const getMap = getContext();
+		return getMap();
+	};
+</script>
+
+<script lang="ts">
+	import { onMount, onDestroy } from 'svelte';
+	import { browser } from '$app/environment';
+	import { Map, type LatLngExpression } from 'leaflet';
+	import 'leaflet/dist/leaflet.css';
+
+	export let latlng: LatLngExpression;
+	export let zoom: number;
+	let container: HTMLDivElement;
+	let map: Map;
+
+	setMapContext(() => map);
+
+	onMount(() => {
+		if (browser) {
+			map = new Map(container);
+		}
+	});
+
+	onDestroy(() => {
+		if (map) map.remove();
+	});
+
+	$: if (map) map.setView(latlng, zoom);
+</script>
+
+<div bind:this={container} class="w-full h-full">
+	{#if map}
+		<slot />
+	{/if}
+</div>

+ 46 - 0
web/src/lib/components/shared-components/leaflet/marker.svelte

@@ -0,0 +1,46 @@
+<script lang="ts">
+	import { onDestroy, onMount } from 'svelte';
+	import { Marker, Icon, type LatLngExpression, type Content } from 'leaflet';
+	import { getMapContext } from './map.svelte';
+	import iconUrl from 'leaflet/dist/images/marker-icon.png';
+	import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
+	import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
+
+	export let latlng: LatLngExpression;
+	export let popupContent: Content | undefined = undefined;
+	let marker: Marker;
+
+	const defaultIcon = new Icon({
+		iconUrl,
+		iconRetinaUrl,
+		shadowUrl,
+
+		// Default values from Leaflet
+		iconSize: [25, 41],
+		iconAnchor: [12, 41],
+		popupAnchor: [1, -34],
+		tooltipAnchor: [16, -28],
+		shadowSize: [41, 41]
+	});
+	const map = getMapContext();
+
+	onMount(() => {
+		marker = new Marker(latlng, {
+			icon: defaultIcon
+		}).addTo(map);
+	});
+
+	onDestroy(() => {
+		if (marker) marker.remove();
+	});
+
+	$: if (marker) {
+		marker.setLatLng(latlng);
+
+		if (popupContent) {
+			marker.bindPopup(popupContent);
+		} else {
+			marker.unbindPopup();
+		}
+	}
+</script>

+ 19 - 0
web/src/lib/components/shared-components/leaflet/tile-layer.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	import { onDestroy, onMount } from 'svelte';
+	import { TileLayer, type TileLayerOptions } from 'leaflet';
+	import { getMapContext } from './map.svelte';
+
+	export let urlTemplate: string;
+	export let options: TileLayerOptions | undefined = undefined;
+	let tileLayer: TileLayer;
+
+	const map = getMapContext();
+
+	onMount(() => {
+		tileLayer = new TileLayer(urlTemplate, options).addTo(map);
+	});
+
+	onDestroy(() => {
+		if (tileLayer) tileLayer.remove();
+	});
+</script>

+ 8 - 0
web/src/lib/utils/context.ts

@@ -0,0 +1,8 @@
+import { getContext, setContext } from 'svelte';
+
+export function createContext<T>(key: string | symbol = Symbol()) {
+	return {
+		get: () => getContext<T>(key),
+		set: (context: T) => setContext<T>(key, context)
+	};
+}