mirror of
https://github.com/immich-app/immich.git
synced 2025-05-30 19:54:52 -04:00
feat(web+server): map date filters + small changes (#2565)
This commit is contained in:
parent
bcc2c34eef
commit
062e2eca6f
8
mobile/openapi/doc/AssetApi.md
generated
8
mobile/openapi/doc/AssetApi.md
generated
@ -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)
|
[[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**
|
# **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 api_instance = AssetApi();
|
||||||
final isFavorite = true; // bool |
|
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 {
|
try {
|
||||||
final result = api_instance.getMapMarkers(isFavorite);
|
final result = api_instance.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore);
|
||||||
print(result);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling AssetApi->getMapMarkers: $e\n');
|
print('Exception when calling AssetApi->getMapMarkers: $e\n');
|
||||||
@ -1139,6 +1141,8 @@ try {
|
|||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------- | ------------- | ------------- | -------------
|
------------- | ------------- | ------------- | -------------
|
||||||
**isFavorite** | **bool**| | [optional]
|
**isFavorite** | **bool**| | [optional]
|
||||||
|
**fileCreatedAfter** | **DateTime**| | [optional]
|
||||||
|
**fileCreatedBefore** | **DateTime**| | [optional]
|
||||||
|
|
||||||
### Return type
|
### Return type
|
||||||
|
|
||||||
|
20
mobile/openapi/lib/api/asset_api.dart
generated
20
mobile/openapi/lib/api/asset_api.dart
generated
@ -1042,7 +1042,11 @@ class AssetApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [bool] isFavorite:
|
/// * [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
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/asset/map-marker';
|
final path = r'/asset/map-marker';
|
||||||
|
|
||||||
@ -1056,6 +1060,12 @@ class AssetApi {
|
|||||||
if (isFavorite != null) {
|
if (isFavorite != null) {
|
||||||
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||||
}
|
}
|
||||||
|
if (fileCreatedAfter != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'fileCreatedAfter', fileCreatedAfter));
|
||||||
|
}
|
||||||
|
if (fileCreatedBefore != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'fileCreatedBefore', fileCreatedBefore));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
@ -1074,8 +1084,12 @@ class AssetApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [bool] isFavorite:
|
/// * [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) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
2
mobile/openapi/test/asset_api_test.dart
generated
2
mobile/openapi/test/asset_api_test.dart
generated
@ -124,7 +124,7 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite }) async
|
//Future<List<MapMarkerResponseDto>> getMapMarkers({ bool isFavorite, DateTime fileCreatedAfter, DateTime fileCreatedBefore }) async
|
||||||
test('test getMapMarkers', () async {
|
test('test getMapMarkers', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
@ -306,6 +306,24 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "boolean"
|
"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": {
|
"responses": {
|
||||||
|
@ -15,6 +15,8 @@ export interface LivePhotoSearchOptions {
|
|||||||
|
|
||||||
export interface MapMarkerSearchOptions {
|
export interface MapMarkerSearchOptions {
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
|
fileCreatedBefore?: string;
|
||||||
|
fileCreatedAfter?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MapMarker {
|
export interface MapMarker {
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { toBoolean } from 'apps/immich/src/utils/transform.util';
|
import { toBoolean } from 'apps/immich/src/utils/transform.util';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsOptional } from 'class-validator';
|
import { IsBoolean, IsISO8601, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
export class MapMarkerDto {
|
export class MapMarkerDto {
|
||||||
|
@ApiProperty()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@Transform(toBoolean)
|
@Transform(toBoolean)
|
||||||
isFavorite?: boolean;
|
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;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
|
import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
|
||||||
import { AssetEntity, AssetType } from '../entities';
|
import { AssetEntity, AssetType } from '../entities';
|
||||||
import { paginate } from '../utils/pagination.util';
|
import { paginate } from '../utils/pagination.util';
|
||||||
|
import OptionalBetween from '../utils/optional-between.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetRepository implements IAssetRepository {
|
export class AssetRepository implements IAssetRepository {
|
||||||
@ -212,7 +213,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
|
async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
|
||||||
const { isFavorite } = options;
|
const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
|
||||||
|
|
||||||
const assets = await this.repository.find({
|
const assets = await this.repository.find({
|
||||||
select: {
|
select: {
|
||||||
@ -231,6 +232,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
longitude: Not(IsNull()),
|
longitude: Not(IsNull()),
|
||||||
},
|
},
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore),
|
||||||
},
|
},
|
||||||
relations: {
|
relations: {
|
||||||
exifInfo: true,
|
exifInfo: true,
|
||||||
|
15
server/libs/infra/src/utils/optional-between.util.ts
Normal file
15
server/libs/infra/src/utils/optional-between.util.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
34
web/src/api/open-api/api.ts
generated
34
web/src/api/open-api/api.ts
generated
@ -5040,10 +5040,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {string} [fileCreatedAfter]
|
||||||
|
* @param {string} [fileCreatedBefore]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @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`;
|
const localVarPath = `/asset/map-marker`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
@ -5069,6 +5071,18 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
localVarQueryParameter['isFavorite'] = isFavorite;
|
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);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
@ -5659,11 +5673,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {string} [fileCreatedAfter]
|
||||||
|
* @param {string} [fileCreatedBefore]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getMapMarkers(isFavorite?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
|
async getMapMarkers(isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MapMarkerResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -5936,11 +5952,13 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
|
* @param {string} [fileCreatedAfter]
|
||||||
|
* @param {string} [fileCreatedBefore]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMapMarkers(isFavorite?: boolean, options?: any): AxiosPromise<Array<MapMarkerResponseDto>> {
|
getMapMarkers(isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: any): AxiosPromise<Array<MapMarkerResponseDto>> {
|
||||||
return localVarFp.getMapMarkers(isFavorite, options).then((request) => request(axios, basePath));
|
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.
|
* 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 {boolean} [isFavorite]
|
||||||
|
* @param {string} [fileCreatedAfter]
|
||||||
|
* @param {string} [fileCreatedBefore]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getMapMarkers(isFavorite?: boolean, options?: AxiosRequestConfig) {
|
public getMapMarkers(isFavorite?: boolean, fileCreatedAfter?: string, fileCreatedBefore?: string, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getMapMarkers(isFavorite, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getMapMarkers(isFavorite, fileCreatedAfter, fileCreatedBefore, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,16 +2,24 @@
|
|||||||
export interface MapSettings {
|
export interface MapSettings {
|
||||||
allowDarkMode: boolean;
|
allowDarkMode: boolean;
|
||||||
onlyFavorites: boolean;
|
onlyFavorites: boolean;
|
||||||
|
relativeDate: string;
|
||||||
|
dateAfter: string;
|
||||||
|
dateBefore: string;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
|
import { Duration } from 'luxon';
|
||||||
import { createEventDispatcher } from 'svelte';
|
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 SettingSwitch from '../admin-page/settings/setting-switch.svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
import LinkButton from '../elements/buttons/link-button.svelte';
|
||||||
|
|
||||||
export let settings: MapSettings;
|
export let settings: MapSettings;
|
||||||
|
let customDateRange = !!settings.dateAfter || !!settings.dateBefore;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
close: void;
|
close: void;
|
||||||
@ -27,9 +35,90 @@
|
|||||||
Map Settings
|
Map Settings
|
||||||
</h1>
|
</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="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">
|
<div class="flex w-full gap-4 mt-4">
|
||||||
<Button color="gray" size="sm" fullwidth on:click={() => dispatch('close')}>Cancel</Button>
|
<Button color="gray" size="sm" fullwidth on:click={() => dispatch('close')}>Cancel</Button>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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>
|
|
@ -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 Control } from './control.svelte';
|
||||||
export { default as Map } from './map.svelte';
|
export { default as Map } from './map.svelte';
|
||||||
export { default as Marker } from './marker.svelte';
|
export { default as Marker } from './marker.svelte';
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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>
|
@ -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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -23,5 +23,8 @@ export const locale = persisted<string | undefined>('locale', undefined, {
|
|||||||
|
|
||||||
export const mapSettings = persisted<MapSettings>('map-settings', {
|
export const mapSettings = persisted<MapSettings>('map-settings', {
|
||||||
allowDarkMode: true,
|
allowDarkMode: true,
|
||||||
onlyFavorites: false
|
onlyFavorites: false,
|
||||||
|
relativeDate: '',
|
||||||
|
dateAfter: '',
|
||||||
|
dateBefore: ''
|
||||||
});
|
});
|
||||||
|
@ -10,15 +10,17 @@
|
|||||||
} from '$lib/stores/asset-interaction.store';
|
} from '$lib/stores/asset-interaction.store';
|
||||||
import { mapSettings } from '$lib/stores/preferences.store';
|
import { mapSettings } from '$lib/stores/preferences.store';
|
||||||
import { MapMarkerResponseDto, api } from '@api';
|
import { MapMarkerResponseDto, api } from '@api';
|
||||||
|
import { isEqual, omit } from 'lodash-es';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import Cog from 'svelte-material-icons/Cog.svelte';
|
import Cog from 'svelte-material-icons/Cog.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { DateTime, Duration } from 'luxon';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
let leaflet: typeof import('$lib/components/shared-components/leaflet');
|
let leaflet: typeof import('$lib/components/shared-components/leaflet');
|
||||||
let mapMarkers: MapMarkerResponseDto[];
|
let mapMarkers: MapMarkerResponseDto[] = [];
|
||||||
let abortController = new AbortController();
|
let abortController: AbortController;
|
||||||
let viewingAssets: string[] = [];
|
let viewingAssets: string[] = [];
|
||||||
let viewingAssetCursor = 0;
|
let viewingAssetCursor = 0;
|
||||||
let showSettingsModal = false;
|
let showSettingsModal = false;
|
||||||
@ -29,22 +31,59 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
abortController.abort();
|
if (abortController) {
|
||||||
|
abortController.abort();
|
||||||
|
}
|
||||||
assetInteractionStore.clearMultiselect();
|
assetInteractionStore.clearMultiselect();
|
||||||
assetInteractionStore.setIsViewingAsset(false);
|
assetInteractionStore.setIsViewingAsset(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadMapMarkers() {
|
async function loadMapMarkers() {
|
||||||
const { data } = await api.assetApi.getMapMarkers($mapSettings.onlyFavorites || undefined, {
|
if (abortController) {
|
||||||
signal: abortController.signal
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onViewAssets(assets: string[]) {
|
function getFileCreatedDates() {
|
||||||
assetInteractionStore.setViewingAssetId(assets[0]);
|
const { relativeDate, dateAfter, dateBefore } = $mapSettings;
|
||||||
viewingAssets = assets;
|
|
||||||
viewingAssetCursor = 0;
|
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() {
|
function navigateNext() {
|
||||||
@ -58,31 +97,22 @@
|
|||||||
assetInteractionStore.setViewingAssetId(viewingAssets[--viewingAssetCursor]);
|
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>
|
</script>
|
||||||
|
|
||||||
<UserPageLayout user={data.user} title={data.meta.title}>
|
<UserPageLayout user={data.user} title={data.meta.title}>
|
||||||
<div class="h-full w-full isolate">
|
<div class="h-full w-full isolate">
|
||||||
{#if leaflet && mapMarkers}
|
{#if leaflet}
|
||||||
{@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet}
|
{@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet}
|
||||||
<Map
|
<Map
|
||||||
center={getMapCenter(mapMarkers)}
|
center={[30, 0]}
|
||||||
zoom={7}
|
zoom={3}
|
||||||
allowDarkMode={$mapSettings.allowDarkMode}
|
allowDarkMode={$mapSettings.allowDarkMode}
|
||||||
options={{
|
options={{
|
||||||
maxBounds: [
|
maxBounds: [
|
||||||
[-90, -180],
|
[-90, -180],
|
||||||
[90, 180]
|
[90, 180]
|
||||||
],
|
],
|
||||||
minZoom: 3
|
minZoom: 2.5
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
@ -94,7 +124,7 @@
|
|||||||
/>
|
/>
|
||||||
<AssetMarkerCluster
|
<AssetMarkerCluster
|
||||||
markers={mapMarkers}
|
markers={mapMarkers}
|
||||||
on:view={(event) => onViewAssets(event.detail.assets)}
|
on:view={({ detail }) => onViewAssets(detail.assetIds, detail.activeAssetIndex)}
|
||||||
/>
|
/>
|
||||||
<Control>
|
<Control>
|
||||||
<button
|
<button
|
||||||
@ -129,7 +159,10 @@
|
|||||||
settings={{ ...$mapSettings }}
|
settings={{ ...$mapSettings }}
|
||||||
on:close={() => (showSettingsModal = false)}
|
on:close={() => (showSettingsModal = false)}
|
||||||
on:save={async ({ detail }) => {
|
on:save={async ({ detail }) => {
|
||||||
const shouldUpdate = detail.onlyFavorites !== $mapSettings.onlyFavorites;
|
const shouldUpdate = !isEqual(
|
||||||
|
omit(detail, 'allowDarkMode'),
|
||||||
|
omit($mapSettings, 'allowDarkMode')
|
||||||
|
);
|
||||||
showSettingsModal = false;
|
showSettingsModal = false;
|
||||||
$mapSettings = detail;
|
$mapSettings = detail;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user