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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 429 additions and 178 deletions

View file

@ -1101,7 +1101,7 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getMapMarkers**
> List<MapMarkerResponseDto> getMapMarkers(isFavorite)
> List<MapMarkerResponseDto> getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore)
@ -1125,9 +1125,11 @@ import 'package:openapi/api.dart';
final api_instance = AssetApi();
final isFavorite = true; // bool |
final fileCreatedAfter = 2013-10-20T19:20:30+01:00; // DateTime |
final fileCreatedBefore = 2013-10-20T19:20:30+01:00; // DateTime |
try {
final result = api_instance.getMapMarkers(isFavorite);
final result = api_instance.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getMapMarkers: $e\n');
@ -1139,6 +1141,8 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**isFavorite** | **bool**| | [optional]
**fileCreatedAfter** | **DateTime**| | [optional]
**fileCreatedBefore** | **DateTime**| | [optional]
### Return type

View file

@ -1042,7 +1042,11 @@ class AssetApi {
/// Parameters:
///
/// * [bool] isFavorite:
Future<Response> getMapMarkersWithHttpInfo({ bool? isFavorite, }) async {
///
/// * [DateTime] fileCreatedAfter:
///
/// * [DateTime] fileCreatedBefore:
Future<Response> getMapMarkersWithHttpInfo({ bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
// ignore: prefer_const_declarations
final path = r'/asset/map-marker';
@ -1056,6 +1060,12 @@ class AssetApi {
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (fileCreatedAfter != null) {
queryParams.addAll(_queryParams('', 'fileCreatedAfter', fileCreatedAfter));
}
if (fileCreatedBefore != null) {
queryParams.addAll(_queryParams('', 'fileCreatedBefore', fileCreatedBefore));
}
const contentTypes = <String>[];
@ -1074,8 +1084,12 @@ class AssetApi {
/// Parameters:
///
/// * [bool] isFavorite:
Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isFavorite, }) async {
final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, );
///
/// * [DateTime] fileCreatedAfter:
///
/// * [DateTime] fileCreatedBefore:
Future<List<MapMarkerResponseDto>?> getMapMarkers({ bool? isFavorite, DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, }) async {
final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View file

@ -124,7 +124,7 @@ void main() {
// TODO
});
//Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite }) async
//Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite, DateTime fileCreatedAfter, DateTime fileCreatedBefore }) async
test('test getMapMarkers', () async {
// TODO
});

View file

@ -306,6 +306,24 @@
"schema": {
"type": "boolean"
}
},
{
"name": "fileCreatedAfter",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "fileCreatedBefore",
"required": false,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
}
],
"responses": {

View file

@ -15,6 +15,8 @@ export interface LivePhotoSearchOptions {
export interface MapMarkerSearchOptions {
isFavorite?: boolean;
fileCreatedBefore?: string;
fileCreatedAfter?: string;
}
export interface MapMarker {

View file

@ -1,10 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { toBoolean } from 'apps/immich/src/utils/transform.util';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
import { IsBoolean, IsISO8601, IsOptional } from 'class-validator';
export class MapMarkerDto {
@ApiProperty()
@IsOptional()
@IsBoolean()
@Transform(toBoolean)
isFavorite?: boolean;
@ApiProperty({ format: 'date-time' })
@IsOptional()
@IsISO8601({ strict: true, strictSeparator: true })
fileCreatedAfter?: string;
@ApiProperty({ format: 'date-time' })
@IsOptional()
@IsISO8601({ strict: true, strictSeparator: true })
fileCreatedBefore?: string;
}

View file

@ -14,6 +14,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
import { AssetEntity, AssetType } from '../entities';
import { paginate } from '../utils/pagination.util';
import OptionalBetween from '../utils/optional-between.util';
@Injectable()
export class AssetRepository implements IAssetRepository {
@ -212,7 +213,7 @@ export class AssetRepository implements IAssetRepository {
}
async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
const { isFavorite } = options;
const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
const assets = await this.repository.find({
select: {
@ -231,6 +232,7 @@ export class AssetRepository implements IAssetRepository {
longitude: Not(IsNull()),
},
isFavorite,
fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore),
},
relations: {
exifInfo: true,

View file

@ -0,0 +1,15 @@
import { Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
/**
* Allows optional values unlike the regular Between and uses MoreThanOrEqual
* or LessThanOrEqual when only one parameter is specified.
*/
export default function OptionalBetween<T>(from?: T, to?: T) {
if (from && to) {
return Between(from, to);
} else if (from) {
return MoreThanOrEqual(from);
} else if (to) {
return LessThanOrEqual(to);
}
}

View file

@ -5040,10 +5040,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
/**
*
* @param {boolean} [isFavorite]
* @param {string} [fileCreatedAfter]
* @param {string} [fileCreatedBefore]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMapMarkers: async (isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getMapMarkers: async (isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/map-marker`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -5069,6 +5071,18 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['isFavorite'] = isFavorite;
}
if (fileCreatedAfter !== undefined) {
localVarQueryParameter['fileCreatedAfter'] = (fileCreatedAfter as any instanceof Date) ?
(fileCreatedAfter as any).toISOString() :
fileCreatedAfter;
}
if (fileCreatedBefore !== undefined) {
localVarQueryParameter['fileCreatedBefore'] = (fileCreatedBefore as any instanceof Date) ?
(fileCreatedBefore as any).toISOString() :
fileCreatedBefore;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -5659,11 +5673,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
/**
*
* @param {boolean} [isFavorite]
* @param {string} [fileCreatedAfter]
* @param {string} [fileCreatedBefore]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getMapMarkers(isFavorite?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, options);
async getMapMarkers(isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -5936,11 +5952,13 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
/**
*
* @param {boolean} [isFavorite]
* @param {string} [fileCreatedAfter]
* @param {string} [fileCreatedBefore]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMapMarkers(isFavorite?: boolean, options?: any): AxiosPromise<Array<MapMarkerResponseDto>> {
return localVarFp.getMapMarkers(isFavorite, options).then((request) => request(axios, basePath));
getMapMarkers(isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: any): AxiosPromise<Array<MapMarkerResponseDto>> {
return localVarFp.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore, options).then((request) => request(axios, basePath));
},
/**
* Get all asset of a device that are in the database, ID only.
@ -6244,12 +6262,14 @@ export class AssetApi extends BaseAPI {
/**
*
* @param {boolean} [isFavorite]
* @param {string} [fileCreatedAfter]
* @param {string} [fileCreatedBefore]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public getMapMarkers(isFavorite?: boolean, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getMapMarkers(isFavorite, options).then((request) => request(this.axios, this.basePath));
public getMapMarkers(isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore, options).then((request) => request(this.axios, this.basePath));
}
/**

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'
});
}
}

View file

@ -23,5 +23,8 @@ export const locale = persisted<string | undefined>('locale', undefined, {
export const mapSettings = persisted<MapSettings>('map-settings', {
allowDarkMode: true,
onlyFavorites: false
onlyFavorites: false,
relativeDate: '',
dateAfter: '',
dateBefore: ''
});

View file

@ -10,15 +10,17 @@
} from '$lib/stores/asset-interaction.store';
import { mapSettings } from '$lib/stores/preferences.store';
import { MapMarkerResponseDto, api } from '@api';
import { isEqual, omit } from 'lodash-es';
import { onDestroy, onMount } from 'svelte';
import Cog from 'svelte-material-icons/Cog.svelte';
import type { PageData } from './$types';
import { DateTime, Duration } from 'luxon';
export let data: PageData;
let leaflet: typeof import('$lib/components/shared-components/leaflet');
let mapMarkers: MapMarkerResponseDto[];
let abortController = new AbortController();
let mapMarkers: MapMarkerResponseDto[] = [];
let abortController: AbortController;
let viewingAssets: string[] = [];
let viewingAssetCursor = 0;
let showSettingsModal = false;
@ -29,22 +31,59 @@
});
onDestroy(() => {
if (abortController) {
abortController.abort();
}
assetInteractionStore.clearMultiselect();
assetInteractionStore.setIsViewingAsset(false);
});
async function loadMapMarkers() {
const { data } = await api.assetApi.getMapMarkers($mapSettings.onlyFavorites || undefined, {
if (abortController) {
abortController.abort();
}
abortController = new AbortController();
const { onlyFavorites } = $mapSettings;
const { fileCreatedAfter, fileCreatedBefore } = getFileCreatedDates();
const { data } = await api.assetApi.getMapMarkers(
onlyFavorites || undefined,
fileCreatedAfter,
fileCreatedBefore,
{
signal: abortController.signal
});
}
);
return data;
}
function onViewAssets(assets: string[]) {
assetInteractionStore.setViewingAssetId(assets[0]);
viewingAssets = assets;
viewingAssetCursor = 0;
function getFileCreatedDates() {
const { relativeDate, dateAfter, dateBefore } = $mapSettings;
if (relativeDate) {
const duration = Duration.fromISO(relativeDate);
return {
fileCreatedAfter: duration.isValid ? DateTime.now().minus(duration).toISO() : undefined
};
}
try {
return {
fileCreatedAfter: dateAfter ? new Date(dateAfter).toISOString() : undefined,
fileCreatedBefore: dateBefore ? new Date(dateBefore).toISOString() : undefined
};
} catch {
$mapSettings.dateAfter = '';
$mapSettings.dateBefore = '';
return {};
}
}
function onViewAssets(assetIds: string[], activeAssetIndex: number) {
assetInteractionStore.setViewingAssetId(assetIds[activeAssetIndex]);
viewingAssets = assetIds;
viewingAssetCursor = activeAssetIndex;
}
function navigateNext() {
@ -58,31 +97,22 @@
assetInteractionStore.setViewingAssetId(viewingAssets[--viewingAssetCursor]);
}
}
function getMapCenter(mapMarkers: MapMarkerResponseDto[]): [number, number] {
const marker = mapMarkers[0];
if (marker) {
return [marker.lat, marker.lon];
}
return [48, 11];
}
</script>
<UserPageLayout user={data.user} title={data.meta.title}>
<div class="h-full w-full isolate">
{#if leaflet && mapMarkers}
{#if leaflet}
{@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet}
<Map
center={getMapCenter(mapMarkers)}
zoom={7}
center={[30, 0]}
zoom={3}
allowDarkMode={$mapSettings.allowDarkMode}
options={{
maxBounds: [
[-90, -180],
[90, 180]
],
minZoom: 3
minZoom: 2.5
}}
>
<TileLayer
@ -94,7 +124,7 @@
/>
<AssetMarkerCluster
markers={mapMarkers}
on:view={(event) => onViewAssets(event.detail.assets)}
on:view={({ detail }) => onViewAssets(detail.assetIds, detail.activeAssetIndex)}
/>
<Control>
<button
@ -129,7 +159,10 @@
settings={{ ...$mapSettings }}
on:close={() => (showSettingsModal = false)}
on:save={async ({ detail }) => {
const shouldUpdate = detail.onlyFavorites !== $mapSettings.onlyFavorites;
const shouldUpdate = !isEqual(
omit(detail, 'allowDarkMode'),
omit($mapSettings, 'allowDarkMode')
);
showSettingsModal = false;
$mapSettings = detail;