pull/27492/merge
rickytrevor 2026-06-03 08:18:40 -05:00 committed by GitHub
commit 61964817e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 87 additions and 3 deletions

View File

@ -1720,6 +1720,8 @@
"open_in_browser": "Open in browser",
"open_in_map_view": "Open in map view",
"open_in_openstreetmap": "Open in OpenStreetMap",
"show_asset_panel_on_map": "Show asset panel by default",
"show_photos_in_area": "Show photos in this area",
"open_the_search_filters": "Open the search filters",
"options": "Options",
"or": "or",

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 { Icon, modalManager, 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,
@ -61,6 +62,10 @@
onOpenInMapView?: (() => Promise<void> | void) | undefined;
onSelect?: (assetIds: string[]) => void;
onClusterSelect?: (assetIds: string[], bbox: SelectionBBox) => void;
onViewportSelect?: (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;
@ -80,6 +85,10 @@
onOpenInMapView = undefined,
onSelect = () => {},
onClusterSelect,
onViewportSelect,
onViewportClose,
viewportGridActive = false,
autoOpenPanel = false,
onClickPoint = () => {},
popup,
rounded = false,
@ -269,6 +278,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(() => {
@ -310,6 +328,35 @@
untrack(() => map?.jumpTo({ center, zoom }));
});
const handleViewportSelect = () => {
if (!map || !onViewportSelect || !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(),
};
onViewportSelect(visibleIds, bbox);
};
const handleMoveEnd = () => {
if (viewportGridActive && !assetViewerManager.isViewing) {
handleViewportSelect();
}
};
const onAssetsDelete = async () => {
mapMarkers = await loadMapMarkers();
};
@ -331,6 +378,7 @@
onload={(event: Map) => {
event.setMaxZoom(18);
event.on('click', handleMapClick);
event.on('moveend', handleMoveEnd);
if (!simplified) {
event.addControl(new GlobeControl(), 'top-left');
}
@ -344,6 +392,15 @@
{#if !simplified}
<GeolocateControl position="top-left" />
<FullscreenControl position="top-left" />
{#if onViewportSelect}
<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

@ -37,6 +37,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

@ -26,6 +26,7 @@ export interface MapSettings {
onlyFavorites: boolean;
withPartners: boolean;
withSharedAlbums: boolean;
showAssetPanel: boolean;
relativeDate: string;
dateAfter?: DateTime<true>;
dateBefore?: DateTime<true>;
@ -37,6 +38,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';
@ -23,9 +24,11 @@
let selectedClusterIds = $state.raw(new Set<string>());
let selectedClusterBBox = $state.raw<SelectionBBox>();
let isTimelinePanelVisible = $state(false);
let isViewportMode = $state(false);
function closeTimelinePanel() {
isTimelinePanelVisible = false;
isViewportMode = false;
selectedClusterBBox = undefined;
selectedClusterIds = new Set();
}
@ -47,9 +50,18 @@
selectedClusterIds = new Set(assetIds);
selectedClusterBBox = bbox;
isTimelinePanelVisible = true;
isViewportMode = false;
assetViewerManager.showAssetViewer(false);
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}
function onViewportSelect(assetIds: string[], bbox: SelectionBBox) {
selectedClusterIds = new Set(assetIds);
selectedClusterBBox = bbox;
isTimelinePanelVisible = true;
isViewportMode = true;
assetViewerManager.showAssetViewer(false);
}
</script>
{#if featureFlagsManager.value.map}
@ -69,7 +81,15 @@
</div>
{/await}
{:then { default: Map }}
<Map hash onSelect={onViewAssets} {onClusterSelect} />
<Map
hash
onSelect={onViewAssets}
{onClusterSelect}
{onViewportSelect}
onViewportClose={closeTimelinePanel}
viewportGridActive={isViewportMode && isTimelinePanelVisible}
autoOpenPanel={$mapSettings.showAssetPanel}
/>
{/await}
</div>