mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web+server): map improvements (#2498)
* feat(web+server): map improvements * add number format double to fix mobile
This commit is contained in:
		@@ -237,7 +237,7 @@
 | 
			
		||||
{#if latlng}
 | 
			
		||||
	<div class="h-[360px]">
 | 
			
		||||
		{#await import('../shared-components/leaflet') then { Map, TileLayer, Marker }}
 | 
			
		||||
			<Map {latlng} zoom={14}>
 | 
			
		||||
			<Map center={latlng} zoom={14}>
 | 
			
		||||
				<TileLayer
 | 
			
		||||
					urlTemplate={'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'}
 | 
			
		||||
					options={{
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								web/src/lib/components/map-page/map-settings-modal.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								web/src/lib/components/map-page/map-settings-modal.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
<script lang="ts" context="module">
 | 
			
		||||
	export interface MapSettings {
 | 
			
		||||
		allowDarkMode: boolean;
 | 
			
		||||
		onlyFavorites: boolean;
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
 | 
			
		||||
	import { createEventDispatcher } from 'svelte';
 | 
			
		||||
	import SettingSwitch from '../admin-page/settings/setting-switch.svelte';
 | 
			
		||||
	import Button from '../elements/buttons/button.svelte';
 | 
			
		||||
 | 
			
		||||
	export let settings: MapSettings;
 | 
			
		||||
 | 
			
		||||
	const dispatch = createEventDispatcher<{
 | 
			
		||||
		close: void;
 | 
			
		||||
		save: MapSettings;
 | 
			
		||||
	}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<FullScreenModal on:clickOutside={() => dispatch('close')}>
 | 
			
		||||
	<div
 | 
			
		||||
		class="flex flex-col gap-8 border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-8 shadow-sm w-96 max-w-lg rounded-3xl"
 | 
			
		||||
	>
 | 
			
		||||
		<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium self-center">
 | 
			
		||||
			Map Settings
 | 
			
		||||
		</h1>
 | 
			
		||||
 | 
			
		||||
		<form on:submit|preventDefault={() => dispatch('save', settings)} class="flex flex-col gap-4">
 | 
			
		||||
			<SettingSwitch title="Allow dark mode" bind:checked={settings.allowDarkMode} />
 | 
			
		||||
			<SettingSwitch title="Show only favorites" bind:checked={settings.onlyFavorites} />
 | 
			
		||||
 | 
			
		||||
			<div class="flex w-full gap-4 mt-4">
 | 
			
		||||
				<Button color="gray" size="sm" fullwidth on:click={() => dispatch('close')}>Cancel</Button>
 | 
			
		||||
				<Button type="submit" size="sm" fullwidth>Save</Button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</form>
 | 
			
		||||
	</div>
 | 
			
		||||
</FullScreenModal>
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
	import { createEventDispatcher } from 'svelte';
 | 
			
		||||
	import { fade } from 'svelte/transition';
 | 
			
		||||
 | 
			
		||||
	const dispatch = createEventDispatcher();
 | 
			
		||||
	const dispatch = createEventDispatcher<{ clickOutside: void }>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<section
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
.marker-cluster {
 | 
			
		||||
	background-clip: padding-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.asset-marker-icon {
 | 
			
		||||
	@apply rounded-full;
 | 
			
		||||
	object-fit: cover;
 | 
			
		||||
	border: 1px solid rgb(69, 80, 169);
 | 
			
		||||
	box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px,
 | 
			
		||||
		rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px,
 | 
			
		||||
		rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.marker-cluster div {
 | 
			
		||||
	width: 40px;
 | 
			
		||||
	height: 40px;
 | 
			
		||||
	margin-left: 5px;
 | 
			
		||||
	margin-top: 5px;
 | 
			
		||||
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	@apply rounded-full;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
 | 
			
		||||
	background-color: rgb(236, 237, 246);
 | 
			
		||||
	border: 1px solid rgb(69, 80, 169);
 | 
			
		||||
 | 
			
		||||
	color: rgb(69, 80, 169);
 | 
			
		||||
	box-shadow: rgba(5, 5, 122, 0.12) 0px 2px 4px 0px, rgba(4, 4, 230, 0.32) 0px 2px 16px 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dark .marker-cluster div {
 | 
			
		||||
	background-color: #adcbfa;
 | 
			
		||||
	border: 1px solid black;
 | 
			
		||||
	color: black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.marker-cluster span {
 | 
			
		||||
	line-height: 40px;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<script lang="ts" context="module">
 | 
			
		||||
	import { createContext } from '$lib/utils/context';
 | 
			
		||||
	import { MarkerClusterGroup, Marker, Icon, LeafletEvent } from 'leaflet';
 | 
			
		||||
	import { Icon, LeafletEvent, Marker, MarkerClusterGroup } from 'leaflet';
 | 
			
		||||
 | 
			
		||||
	const { get: getContext, set: setClusterContext } = createContext<() => MarkerClusterGroup>();
 | 
			
		||||
 | 
			
		||||
@@ -10,11 +10,11 @@
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import 'leaflet.markercluster';
 | 
			
		||||
	import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import { getMapContext } from './map.svelte';
 | 
			
		||||
	import { MapMarkerResponseDto, api } from '@api';
 | 
			
		||||
	import { createEventDispatcher } from 'svelte';
 | 
			
		||||
	import 'leaflet.markercluster';
 | 
			
		||||
	import { createEventDispatcher, onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import './asset-marker-cluster.css';
 | 
			
		||||
	import { getMapContext } from './map.svelte';
 | 
			
		||||
 | 
			
		||||
	class AssetMarker extends Marker {
 | 
			
		||||
		marker: MapMarkerResponseDto;
 | 
			
		||||
@@ -95,49 +95,3 @@
 | 
			
		||||
		if (cluster) cluster.remove();
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if cluster}
 | 
			
		||||
	<slot />
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<style lang="postcss">
 | 
			
		||||
	:global(.marker-cluster) {
 | 
			
		||||
		background-clip: padding-box;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.asset-marker-icon) {
 | 
			
		||||
		@apply rounded-full;
 | 
			
		||||
		object-fit: cover;
 | 
			
		||||
		border: 1px solid rgb(69, 80, 169);
 | 
			
		||||
		box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px,
 | 
			
		||||
			rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px,
 | 
			
		||||
			rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.marker-cluster div) {
 | 
			
		||||
		width: 40px;
 | 
			
		||||
		height: 40px;
 | 
			
		||||
		margin-left: 5px;
 | 
			
		||||
		margin-top: 5px;
 | 
			
		||||
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		@apply rounded-full;
 | 
			
		||||
		font-weight: bold;
 | 
			
		||||
 | 
			
		||||
		background-color: rgb(236, 237, 246);
 | 
			
		||||
		border: 1px solid rgb(69, 80, 169);
 | 
			
		||||
 | 
			
		||||
		color: rgb(69, 80, 169);
 | 
			
		||||
		box-shadow: rgba(5, 5, 122, 0.12) 0px 2px 4px 0px, rgba(4, 4, 230, 0.32) 0px 2px 16px 0px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.dark .marker-cluster div) {
 | 
			
		||||
		background-color: #adcbfa;
 | 
			
		||||
		border: 1px solid black;
 | 
			
		||||
		color: black;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.marker-cluster span) {
 | 
			
		||||
		line-height: 40px;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { onDestroy, onMount } from 'svelte';
 | 
			
		||||
	import { Control, type ControlPosition } from 'leaflet';
 | 
			
		||||
	import { getMapContext } from './map.svelte';
 | 
			
		||||
 | 
			
		||||
	export let position: ControlPosition | undefined = undefined;
 | 
			
		||||
	let className: string | undefined = undefined;
 | 
			
		||||
	export { className as class };
 | 
			
		||||
 | 
			
		||||
	let control: Control;
 | 
			
		||||
	let target: HTMLDivElement;
 | 
			
		||||
 | 
			
		||||
	const map = getMapContext();
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		const ControlClass = Control.extend({
 | 
			
		||||
			position,
 | 
			
		||||
			onAdd: () => target
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		control = new ControlClass().addTo(map);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		control.remove();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	$: if (control && position) {
 | 
			
		||||
		control.setPosition(position);
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div bind:this={target} class={className}>
 | 
			
		||||
	<slot />
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
export { default as AssetMarkerCluster } from './asset-marker-cluster.svelte';
 | 
			
		||||
export { default as Control } from './control.svelte';
 | 
			
		||||
export { default as Map } from './map.svelte';
 | 
			
		||||
export { default as Marker } from './marker.svelte';
 | 
			
		||||
export { default as TileLayer } from './tile-layer.svelte';
 | 
			
		||||
export { default as AssetMarkerCluster } from './asset-marker-cluster.svelte';
 | 
			
		||||
 
 | 
			
		||||
@@ -12,11 +12,13 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { onMount, onDestroy } from 'svelte';
 | 
			
		||||
	import { browser } from '$app/environment';
 | 
			
		||||
	import { Map, type LatLngExpression } from 'leaflet';
 | 
			
		||||
	import { Map, type LatLngExpression, type MapOptions } from 'leaflet';
 | 
			
		||||
	import 'leaflet/dist/leaflet.css';
 | 
			
		||||
 | 
			
		||||
	export let latlng: LatLngExpression;
 | 
			
		||||
	export let center: LatLngExpression;
 | 
			
		||||
	export let zoom: number;
 | 
			
		||||
	export let options: MapOptions | undefined = undefined;
 | 
			
		||||
	export let allowDarkMode = false;
 | 
			
		||||
	let container: HTMLDivElement;
 | 
			
		||||
	let map: Map;
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +26,7 @@
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		if (browser) {
 | 
			
		||||
			map = new Map(container);
 | 
			
		||||
			map = new Map(container, options);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -32,11 +34,17 @@
 | 
			
		||||
		if (map) map.remove();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	$: if (map) map.setView(latlng, zoom);
 | 
			
		||||
	$: if (map) map.setView(center, zoom);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div bind:this={container} class="w-full h-full">
 | 
			
		||||
<div bind:this={container} class="w-full h-full" class:map-dark={allowDarkMode}>
 | 
			
		||||
	{#if map}
 | 
			
		||||
		<slot />
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	:global(.dark) .map-dark :global(.leaflet-layer) {
 | 
			
		||||
		filter: invert(100%) brightness(130%) saturate(0%);
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,30 +5,16 @@
 | 
			
		||||
 | 
			
		||||
	export let urlTemplate: string;
 | 
			
		||||
	export let options: TileLayerOptions | undefined = undefined;
 | 
			
		||||
	export let allowDarkMode = false;
 | 
			
		||||
 | 
			
		||||
	let tileLayer: TileLayer;
 | 
			
		||||
 | 
			
		||||
	const map = getMapContext();
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		tileLayer = new TileLayer(urlTemplate, {
 | 
			
		||||
			className: allowDarkMode ? 'leaflet-layer-dynamic' : 'leaflet-layer',
 | 
			
		||||
			...options
 | 
			
		||||
		}).addTo(map);
 | 
			
		||||
		tileLayer = new TileLayer(urlTemplate, options).addTo(map);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		if (tileLayer) tileLayer.remove();
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	:global(.leaflet-layer-dynamic) {
 | 
			
		||||
		filter: brightness(100%) contrast(100%) saturate(80%);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.dark .leaflet-layer-dynamic) {
 | 
			
		||||
		filter: invert(100%) brightness(130%) saturate(0%);
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user