feat(web): view assets in map viewport (#27492)

This commit is contained in:
rickytrevor 2026-08-27 20:39:32 +02:00 committed by GitHub
parent 1db4677cf8
commit 25c60ef99e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 75 additions and 6 deletions

View file

@ -2038,6 +2038,7 @@
"show_albums": "Show albums",
"show_all_people": "Show all people",
"show_and_hide_people": "Show & hide people",
"show_asset_panel_on_map": "Show asset panel by default",
"show_file_location": "Show file location",
"show_gallery": "Show gallery",
"show_hidden_people": "Show hidden people",
@ -2050,6 +2051,7 @@
"show_or_hide_info": "Show or hide info",
"show_password": "Show password",
"show_person_options": "Show person options",
"show_photos_in_area": "Show photos in this area",
"show_progress_bar": "Show Progress Bar",
"show_schema": "Show schema",
"show_search_options": "Show search options",

View file

@ -11,13 +11,14 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import OnEvents from '$lib/components/OnEvents.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import MapSettingsModal from '$lib/modals/MapSettingsModal.svelte';
import { mapSettings } from '$lib/stores/preferences.store';
import { getAssetMediaUrl, handlePromiseError } from '$lib/utils';
import { getMapMarkers, type MapMarkerResponseDto } from '@immich/sdk';
import { Alert, Container, Icon, modalManager, Text, Theme, themeManager } from '@immich/ui';
import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
import { mdiCog, mdiImageMultiple, mdiMap, mdiMapMarker } from '@mdi/js';
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
import { isEqual, omit } from 'lodash-es';
import { DateTime, Duration } from 'luxon';
@ -31,7 +32,7 @@
type Map,
type MapMouseEvent,
} from 'maplibre-gl';
import { onDestroy, onMount, untrack } from 'svelte';
import { onDestroy, onMount, tick, untrack } from 'svelte';
import { t } from 'svelte-i18n';
import {
AttributionControl,
@ -60,6 +61,9 @@
onOpenInMapView?: (() => Promise<void> | void) | undefined;
onSelect?: (assetIds: string[]) => void;
onClusterSelect?: (assetIds: string[], bbox: SelectionBBox) => void;
onViewportClose?: () => void;
viewportGridActive?: boolean;
autoOpenPanel?: boolean;
onClickPoint?: ({ lat, lng }: { lat: number; lng: number }) => void;
popup?: import('svelte').Snippet<[{ marker: MapMarkerResponseDto }]>;
rounded?: boolean;
@ -79,6 +83,9 @@
onOpenInMapView = undefined,
onSelect = () => {},
onClusterSelect,
onViewportClose,
viewportGridActive = false,
autoOpenPanel = false,
onClickPoint = () => {},
popup,
rounded = false,
@ -276,6 +283,15 @@
if (!mapMarkers) {
mapMarkers = await loadMapMarkers();
}
if (autoOpenPanel) {
// Wait for the map to finish rendering before opening the panel
await tick();
if (map) {
map.resize();
await map.once('idle');
handleViewportSelect();
}
}
});
onDestroy(() => {
@ -317,15 +333,43 @@
untrack(() => map?.jumpTo({ center, zoom }));
});
const handleViewportSelect = () => {
if (!map || !onClusterSelect || !mapMarkers) {
return;
}
const bounds = map.getBounds();
const west = bounds.getWest();
const east = bounds.getEast();
// When zoomed out enough to see the whole world, show all markers
const showAll = east - west >= 360;
const visibleIds = showAll
? mapMarkers.map(({ id }) => id)
: mapMarkers.filter(({ lon, lat }) => bounds.contains([lon, lat])).map(({ id }) => id);
const bbox: SelectionBBox = {
west: showAll ? -180 : west,
south: showAll ? -90 : bounds.getSouth(),
east: showAll ? 180 : east,
north: showAll ? 90 : bounds.getNorth(),
};
onClusterSelect(visibleIds, bbox);
};
const handleMoveEnd = () => {
if (viewportGridActive && !assetViewerManager.isViewing) {
handleViewportSelect();
}
};
const onAssetsChanged = async () => {
mapMarkers = await loadMapMarkers();
};
</script>
<OnEvents onAssetsDelete={onAssetsChanged} onAssetsArchive={onAssetsChanged} onAssetsUnarchive={onAssetsChanged} />
<!-- We handle style loading ourselves so we set style blank here -->
<svelte:boundary>
<!-- We handle style loading ourselves so we set style blank here -->
<MapLibre
{hash}
style=""
@ -339,6 +383,7 @@
onload={(event: Map) => {
event.setMaxZoom(18);
event.on('click', handleMapClick);
event.on('moveend', handleMoveEnd);
if (!simplified) {
event.addControl(new GlobeControl(), 'top-left');
}
@ -351,6 +396,15 @@
{#if !simplified}
<GeolocateControl position="top-left" />
{#if onClusterSelect}
<Control position="top-left">
<ControlGroup>
<ControlButton onclick={() => (viewportGridActive ? onViewportClose?.() : handleViewportSelect())}>
<Icon title={$t('show_photos_in_area')} icon={mdiImageMultiple} size="100%" class="text-black/80" />
</ControlButton>
</ControlGroup>
</Control>
{/if}
<ScaleControl />
<AttributionControl compact={false} />
{/if}

View file

@ -36,6 +36,9 @@
<Field label={$t('include_shared_albums')}>
<Switch bind:checked={settings.withSharedAlbums} />
</Field>
<Field label={$t('show_asset_panel_on_map')}>
<Switch bind:checked={settings.showAssetPanel} />
</Field>
{#if customDateRange}
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">

View file

@ -25,6 +25,7 @@ export interface MapSettings {
onlyFavorites: boolean;
withPartners: boolean;
withSharedAlbums: boolean;
showAssetPanel: boolean;
relativeDate: string;
dateAfter?: string;
dateBefore?: string;
@ -36,6 +37,7 @@ const defaultMapSettings = {
onlyFavorites: false,
withPartners: false,
withSharedAlbums: false,
showAssetPanel: false,
relativeDate: '',
};

View file

@ -11,6 +11,7 @@
import { handlePromiseError } from '$lib/utils';
import { delay } from '$lib/utils/asset-utils';
import { navigate } from '$lib/utils/navigation';
import { mapSettings } from '$lib/stores/preferences.store';
import { LoadingSpinner } from '@immich/ui';
import { onDestroy } from 'svelte';
import type { PageData } from './$types';
@ -69,7 +70,14 @@
</div>
{/await}
{:then { default: Map }}
<Map hash onSelect={onViewAssets} {onClusterSelect} />
<Map
hash
onSelect={onViewAssets}
{onClusterSelect}
onViewportClose={closeTimelinePanel}
viewportGridActive={isTimelinePanelVisible}
autoOpenPanel={$mapSettings.showAssetPanel}
/>
{/await}
</div>

View file

@ -86,7 +86,7 @@
? AssetVisibility.Timeline
: $mapSettings.includeArchived
? undefined
: AssetVisibility.Archive,
: AssetVisibility.Timeline,
isFavorite: $mapSettings.onlyFavorites || undefined,
withPartners: $mapSettings.withPartners || undefined,
assetFilter: selectedClusterIds,