Merge a3836ce578 into 963862b1b9
commit
61964817e6
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue