mirror of
https://github.com/immich-app/immich
synced 2026-08-29 13:15:45 +00:00
feat(web): view assets in map viewport (#27492)
This commit is contained in:
parent
1db4677cf8
commit
25c60ef99e
6 changed files with 75 additions and 6 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
? AssetVisibility.Timeline
|
||||
: $mapSettings.includeArchived
|
||||
? undefined
|
||||
: AssetVisibility.Archive,
|
||||
: AssetVisibility.Timeline,
|
||||
isFavorite: $mapSettings.onlyFavorites || undefined,
|
||||
withPartners: $mapSettings.withPartners || undefined,
|
||||
assetFilter: selectedClusterIds,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue