mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05: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