feat(web+server): map date filters + small changes (#2565)

This commit is contained in:
Michel Heusschen
2023-05-25 18:47:52 +02:00
committed by GitHub
parent bcc2c34eef
commit 062e2eca6f
18 changed files with 429 additions and 178 deletions

View File

@@ -2,16 +2,24 @@
export interface MapSettings {
allowDarkMode: boolean;
onlyFavorites: boolean;
relativeDate: string;
dateAfter: string;
dateBefore: string;
}
</script>
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { Duration } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { fly } from 'svelte/transition';
import SettingSelect from '../admin-page/settings/setting-select.svelte';
import SettingSwitch from '../admin-page/settings/setting-switch.svelte';
import Button from '../elements/buttons/button.svelte';
import LinkButton from '../elements/buttons/link-button.svelte';
export let settings: MapSettings;
let customDateRange = !!settings.dateAfter || !!settings.dateBefore;
const dispatch = createEventDispatcher<{
close: void;
@@ -27,9 +35,90 @@
Map Settings
</h1>
<form on:submit|preventDefault={() => dispatch('save', settings)} class="flex flex-col gap-4">
<form
on:submit|preventDefault={() => dispatch('save', settings)}
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
>
<SettingSwitch title="Allow dark mode" bind:checked={settings.allowDarkMode} />
<SettingSwitch title="Show only favorites" bind:checked={settings.onlyFavorites} />
<SettingSwitch title="Only favorites" bind:checked={settings.onlyFavorites} />
{#if customDateRange}
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
<div class="flex justify-between items-center gap-8">
<label class="immich-form-label text-sm shrink-0" for="date-after">Date after</label>
<input
class="immich-form-input w-40"
type="date"
id="date-after"
max={settings.dateBefore}
bind:value={settings.dateAfter}
/>
</div>
<div class="flex justify-between items-center gap-8">
<label class="immich-form-label text-sm shrink-0" for="date-before">Date before</label>
<input
class="immich-form-input w-40"
type="date"
id="date-before"
bind:value={settings.dateBefore}
/>
</div>
<div class="flex justify-center text-xs">
<LinkButton
on:click={() => {
customDateRange = false;
settings.dateAfter = '';
settings.dateBefore = '';
}}
>
Remove custom date range
</LinkButton>
</div>
</div>
{:else}
<div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
<SettingSelect
label="Date range"
name="date-range"
bind:value={settings.relativeDate}
options={[
{
value: '',
text: 'All'
},
{
value: Duration.fromObject({ hours: 24 }).toISO(),
text: 'Past 24 hours'
},
{
value: Duration.fromObject({ days: 7 }).toISO(),
text: 'Past 7 days'
},
{
value: Duration.fromObject({ days: 30 }).toISO(),
text: 'Past 30 days'
},
{
value: Duration.fromObject({ years: 1 }).toISO(),
text: 'Past year'
},
{
value: Duration.fromObject({ years: 3 }).toISO(),
text: 'Past 3 years'
}
]}
/>
<div class="text-xs">
<LinkButton
on:click={() => {
customDateRange = true;
settings.relativeDate = '';
}}
>
Use custom date range instead
</LinkButton>
</div>
</div>
{/if}
<div class="flex w-full gap-4 mt-4">
<Button color="gray" size="sm" fullwidth on:click={() => dispatch('close')}>Cancel</Button>

View File

@@ -1,39 +0,0 @@
.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;
}

View File

@@ -1,95 +0,0 @@
<script lang="ts" context="module">
import { createContext } from '$lib/utils/context';
import { Icon, LeafletEvent, Marker, MarkerClusterGroup } from 'leaflet';
const { get: getContext, set: setClusterContext } = createContext<() => MarkerClusterGroup>();
export const getClusterContext = () => {
return getContext()();
};
</script>
<script lang="ts">
import { MapMarkerResponseDto, api } from '@api';
import 'leaflet.markercluster';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import './asset-marker-cluster.css';
import { getMapContext } from './map.svelte';
class AssetMarker extends Marker {
constructor(private marker: MapMarkerResponseDto) {
super([marker.lat, marker.lon], {
icon: new Icon({
iconUrl: api.getAssetThumbnailUrl(marker.id),
iconRetinaUrl: api.getAssetThumbnailUrl(marker.id),
iconSize: [60, 60],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41],
className: 'asset-marker-icon'
})
});
this.on('click', this.onClick);
}
onClick() {
dispatch('view', { assets: [this.marker.id] });
}
getAssetId(): string {
return this.marker.id;
}
}
const dispatch = createEventDispatcher<{ view: { assets: string[] } }>();
export let markers: MapMarkerResponseDto[];
const map = getMapContext();
let cluster: MarkerClusterGroup;
setClusterContext(() => cluster);
onMount(() => {
cluster = new MarkerClusterGroup({
showCoverageOnHover: false,
zoomToBoundsOnClick: false,
spiderfyOnMaxZoom: false,
maxClusterRadius: 30,
spiderLegPolylineOptions: { opacity: 0 },
spiderfyDistanceMultiplier: 3
});
cluster.on('clusterclick', (event: LeafletEvent) => {
const ids = event.sourceTarget
.getAllChildMarkers()
.map((marker: AssetMarker) => marker.getAssetId());
dispatch('view', { assets: ids });
});
cluster.on('clustermouseover', (event: LeafletEvent) => {
if (event.sourceTarget.getChildCount() <= 10) {
event.sourceTarget.spiderfy();
}
});
cluster.on('clustermouseout', (event: LeafletEvent) => {
event.sourceTarget.unspiderfy();
});
map.addLayer(cluster);
});
$: if (cluster) {
const leafletMarkers = markers.map((marker) => new AssetMarker(marker));
cluster.clearLayers();
cluster.addLayers(leafletMarkers);
}
onDestroy(() => {
if (cluster) cluster.remove();
});
</script>

View File

@@ -1,4 +1,4 @@
export { default as AssetMarkerCluster } from './asset-marker-cluster.svelte';
export { default as AssetMarkerCluster } from './marker-cluster/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';

View File

@@ -0,0 +1,32 @@
.asset-marker-icon {
@apply rounded-full;
@apply object-cover;
@apply border;
@apply border-immich-primary;
@apply transition-all;
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-icon {
@apply h-full;
@apply w-full;
@apply flex;
@apply justify-center;
@apply items-center;
@apply rounded-full;
@apply font-bold;
@apply bg-violet-50;
@apply border;
@apply border-immich-primary;
@apply text-immich-primary;
box-shadow: rgba(5, 5, 122, 0.12) 0px 2px 4px 0px, rgba(4, 4, 230, 0.32) 0px 2px 16px 0px;
}
.dark .map-dark .marker-cluster-icon {
@apply bg-blue-200;
@apply text-black;
@apply border-blue-200;
box-shadow: none;
}

View File

@@ -0,0 +1,104 @@
<script lang="ts" context="module">
import { createContext } from '$lib/utils/context';
import { MarkerClusterGroup } from 'leaflet';
const { get: getContext, set: setClusterContext } = createContext<() => MarkerClusterGroup>();
export const getClusterContext = () => {
return getContext()();
};
</script>
<script lang="ts">
import { MapMarkerResponseDto } from '@api';
import { DivIcon, LeafletEvent, LeafletMouseEvent, MarkerCluster, Point } from 'leaflet';
import 'leaflet.markercluster';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { getMapContext } from '../map.svelte';
import AssetMarker from './asset-marker';
import './asset-marker-cluster.css';
export let markers: MapMarkerResponseDto[];
export let spiderfyLimit = 10;
let cluster: MarkerClusterGroup;
const map = getMapContext();
const dispatch = createEventDispatcher<{
view: { assetIds: string[]; activeAssetIndex: number };
}>();
setClusterContext(() => cluster);
onMount(() => {
cluster = new MarkerClusterGroup({
showCoverageOnHover: false,
zoomToBoundsOnClick: false,
spiderfyOnMaxZoom: false,
maxClusterRadius: (zoom) => 80 - zoom * 2,
spiderLegPolylineOptions: { opacity: 0 },
spiderfyDistanceMultiplier: 3,
iconCreateFunction: (options) => {
const childCount = options.getChildCount();
const iconSize = childCount > spiderfyLimit ? 45 : 40;
return new DivIcon({
html: `<div class="marker-cluster-icon">${childCount}</div>`,
className: '',
iconSize: new Point(iconSize, iconSize)
});
}
});
cluster.on('clusterclick', (event: LeafletEvent) => {
const markerCluster: MarkerCluster = event.sourceTarget;
const childCount = markerCluster.getChildCount();
if (childCount > spiderfyLimit) {
const markers = markerCluster.getAllChildMarkers() as AssetMarker[];
onView(markers, markers[0].id);
} else {
markerCluster.spiderfy();
}
});
cluster.on('click', (event: LeafletMouseEvent) => {
const marker: AssetMarker = event.sourceTarget;
const markerCluster = getClusterByMarker(marker);
const markers = markerCluster
? (markerCluster.getAllChildMarkers() as AssetMarker[])
: [marker];
onView(markers, marker.id);
});
map.addLayer(cluster);
});
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const getClusterByMarker = (marker: any): MarkerCluster | undefined => {
const mapZoom = map.getZoom();
while (marker && marker._zoom !== mapZoom) {
marker = marker.__parent;
}
return marker;
};
const onView = (markers: AssetMarker[], activeAssetId: string) => {
const assetIds = markers.map((marker) => marker.id);
const activeAssetIndex = assetIds.indexOf(activeAssetId) || 0;
dispatch('view', { assetIds, activeAssetIndex });
};
$: if (cluster) {
const leafletMarkers = markers.map((marker) => new AssetMarker(marker));
cluster.clearLayers();
cluster.addLayers(leafletMarkers);
}
onDestroy(() => {
if (cluster) cluster.remove();
});
</script>

View File

@@ -0,0 +1,37 @@
import { MapMarkerResponseDto, api } from '@api';
import { Marker, Map, Icon } from 'leaflet';
export default class AssetMarker extends Marker {
id: string;
private iconCreated = false;
constructor(marker: MapMarkerResponseDto) {
super([marker.lat, marker.lon]);
this.id = marker.id;
}
onAdd(map: Map) {
// Set icon when the marker gets actually added to the map. This only
// gets called for individual assets and when selecting a cluster, so
// creating an icon for every marker in advance is pretty wasteful.
if (!this.iconCreated) {
this.iconCreated = true;
this.setIcon(this.getIcon());
}
return super.onAdd(map);
}
getIcon() {
return new Icon({
iconUrl: api.getAssetThumbnailUrl(this.id),
iconRetinaUrl: api.getAssetThumbnailUrl(this.id),
iconSize: [60, 60],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41],
className: 'asset-marker-icon'
});
}
}