diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index fd1338532b..2e5a4641fd 100644 Binary files a/mobile/openapi/doc/AssetApi.md and b/mobile/openapi/doc/AssetApi.md differ diff --git a/mobile/openapi/doc/MapMarkerResponseDto.md b/mobile/openapi/doc/MapMarkerResponseDto.md index 5994a8ac35..94f253d380 100644 Binary files a/mobile/openapi/doc/MapMarkerResponseDto.md and b/mobile/openapi/doc/MapMarkerResponseDto.md differ diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 9972700920..e6cde800db 100644 Binary files a/mobile/openapi/lib/api/asset_api.dart and b/mobile/openapi/lib/api/asset_api.dart differ diff --git a/mobile/openapi/lib/model/map_marker_response_dto.dart b/mobile/openapi/lib/model/map_marker_response_dto.dart index e8d0001acf..f094c8be76 100644 Binary files a/mobile/openapi/lib/model/map_marker_response_dto.dart and b/mobile/openapi/lib/model/map_marker_response_dto.dart differ diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index b9091fbeea..085f1560f8 100644 Binary files a/mobile/openapi/test/asset_api_test.dart and b/mobile/openapi/test/asset_api_test.dart differ diff --git a/mobile/openapi/test/map_marker_response_dto_test.dart b/mobile/openapi/test/map_marker_response_dto_test.dart index 312d4052dd..f8308116ff 100644 Binary files a/mobile/openapi/test/map_marker_response_dto_test.dart and b/mobile/openapi/test/map_marker_response_dto_test.dart differ diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 2a20b3dd48..6088b81222 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -31,7 +31,7 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; -import { AssetResponseDto, ImmichReadStream, MapMarkerResponseDto } from '@app/domain'; +import { AssetResponseDto, ImmichReadStream } from '@app/domain'; import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto'; import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto'; import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; @@ -260,18 +260,6 @@ export class AssetController { return await this.assetService.getAssetByTimeBucket(authUser, getAssetByTimeBucketDto); } - /** - * Get all assets that have GPS information embedded - */ - @Authenticated() - @Get('/map-marker') - getMapMarkers( - @GetAuthUser() authUser: AuthUserDto, - @Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto, - ): Promise { - return this.assetService.getMapMarkers(authUser, dto); - } - /** * Get all asset of a device that are in the database, ID only. */ diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index b9697a1faf..12bfbc7007 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -504,17 +504,4 @@ describe('AssetService', () => { expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg'); }); }); - - describe('get map markers', () => { - it('should get geo information of assets', async () => { - assetRepositoryMock.getAllByUserId.mockResolvedValue(_getAssets()); - - const markers = await sut.getMapMarkers(authStub.admin, {}); - - expect(markers).toHaveLength(1); - expect(markers[0].lat).toBe(_getAsset_1().exifInfo?.latitude); - expect(markers[0].lon).toBe(_getAsset_1().exifInfo?.longitude); - expect(markers[0].id).toBe(_getAsset_1().id); - }); - }); }); diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 3d40b7984f..e130b6b07b 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -30,8 +30,6 @@ import { JobName, mapAsset, mapAssetWithoutExif, - MapMarkerResponseDto, - mapAssetMapMarker, PartnerCore, } from '@app/domain'; import { CreateAssetDto, UploadFile } from './dto/create-asset.dto'; @@ -149,12 +147,6 @@ export class AssetService { return assets.map((asset) => mapAsset(asset)); } - public async getMapMarkers(authUser: AuthUserDto, dto: AssetSearchDto): Promise { - const assets = await this._assetRepository.getAllByUserId(authUser.id, dto); - - return assets.map((asset) => mapAssetMapMarker(asset)).filter((marker) => marker != null) as MapMarkerResponseDto[]; - } - public async getAssetByTimeBucket( authUser: AuthUserDto, getAssetByTimeBucketDto: GetAssetByTimeBucketDto, diff --git a/server/apps/immich/src/app.module.ts b/server/apps/immich/src/app.module.ts index 7d78292aee..a9168975b4 100644 --- a/server/apps/immich/src/app.module.ts +++ b/server/apps/immich/src/app.module.ts @@ -9,6 +9,7 @@ import { InfraModule } from '@app/infra'; import { AlbumController, APIKeyController, + AssetController, AuthController, PersonController, JobController, @@ -36,6 +37,7 @@ import { AppCronJobs } from './app.cron-jobs'; AppController, AlbumController, APIKeyController, + AssetController, AuthController, JobController, OAuthController, diff --git a/server/apps/immich/src/controllers/asset.controller.ts b/server/apps/immich/src/controllers/asset.controller.ts new file mode 100644 index 0000000000..ee71e814f7 --- /dev/null +++ b/server/apps/immich/src/controllers/asset.controller.ts @@ -0,0 +1,20 @@ +import { AssetService, AuthUserDto, MapMarkerResponseDto } from '@app/domain'; +import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto'; +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { GetAuthUser } from '../decorators/auth-user.decorator'; +import { Authenticated } from '../decorators/authenticated.decorator'; +import { UseValidation } from '../decorators/use-validation.decorator'; + +@ApiTags('Asset') +@Controller('asset') +@Authenticated() +@UseValidation() +export class AssetController { + constructor(private service: AssetService) {} + + @Get('/map-marker') + getMapMarkers(@GetAuthUser() authUser: AuthUserDto, @Query() options: MapMarkerDto): Promise { + return this.service.getMapMarkers(authUser, options); + } +} diff --git a/server/apps/immich/src/controllers/index.ts b/server/apps/immich/src/controllers/index.ts index 1ffb3b79cb..d86db2bebc 100644 --- a/server/apps/immich/src/controllers/index.ts +++ b/server/apps/immich/src/controllers/index.ts @@ -1,5 +1,6 @@ export * from './album.controller'; export * from './api-key.controller'; +export * from './asset.controller'; export * from './auth.controller'; export * from './job.controller'; export * from './oauth.controller'; diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 64c5effbf0..7c2ed1bdcc 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -295,6 +295,50 @@ ] } }, + "/asset/map-marker": { + "get": { + "operationId": "getMapMarkers", + "parameters": [ + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MapMarkerResponseDto" + } + } + } + } + } + }, + "tags": [ + "Asset" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ] + } + }, "/auth/login": { "post": { "operationId": "login", @@ -2962,67 +3006,6 @@ ] } }, - "/asset/map-marker": { - "get": { - "operationId": "getMapMarkers", - "description": "Get all assets that have GPS information embedded", - "parameters": [ - { - "name": "isFavorite", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isArchived", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "skip", - "required": false, - "in": "query", - "schema": { - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MapMarkerResponseDto" - } - } - } - } - } - }, - "tags": [ - "Asset" - ], - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ] - } - }, "/asset/{deviceId}": { "get": { "operationId": "getUserAssetsByDeviceId", @@ -4579,6 +4562,27 @@ "name" ] }, + "MapMarkerResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "lat": { + "type": "number", + "format": "double" + }, + "lon": { + "type": "number", + "format": "double" + } + }, + "required": [ + "id", + "lat", + "lon" + ] + }, "LoginCredentialDto": { "type": "object", "properties": { @@ -5897,31 +5901,6 @@ "timeBucket" ] }, - "MapMarkerResponseDto": { - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/AssetTypeEnum" - }, - "lat": { - "type": "number", - "format": "double" - }, - "lon": { - "type": "number", - "format": "double" - }, - "id": { - "type": "string" - } - }, - "required": [ - "type", - "lat", - "lon", - "id" - ] - }, "UpdateAssetDto": { "type": "object", "properties": { diff --git a/server/libs/domain/src/asset/asset.repository.ts b/server/libs/domain/src/asset/asset.repository.ts index ed3a66a29c..20edc50b7c 100644 --- a/server/libs/domain/src/asset/asset.repository.ts +++ b/server/libs/domain/src/asset/asset.repository.ts @@ -12,6 +12,16 @@ export interface LivePhotoSearchOptions { type: AssetType; } +export interface MapMarkerSearchOptions { + isFavorite?: boolean; +} + +export interface MapMarker { + id: string; + lat: number; + lon: number; +} + export enum WithoutProperty { THUMBNAIL = 'thumbnail', ENCODED_VIDEO = 'encoded-video', @@ -31,4 +41,5 @@ export interface IAssetRepository { getAll(options?: AssetSearchOptions): Promise; save(asset: Partial): Promise; findLivePhotoMatch(options: LivePhotoSearchOptions): Promise; + getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise; } diff --git a/server/libs/domain/src/asset/asset.service.spec.ts b/server/libs/domain/src/asset/asset.service.spec.ts index debf488afa..3a023a1d10 100644 --- a/server/libs/domain/src/asset/asset.service.spec.ts +++ b/server/libs/domain/src/asset/asset.service.spec.ts @@ -1,5 +1,5 @@ import { AssetEntity, AssetType } from '@app/infra/entities'; -import { assetEntityStub, newAssetRepositoryMock, newJobRepositoryMock } from '../../test'; +import { assetEntityStub, authStub, newAssetRepositoryMock, newJobRepositoryMock } from '../../test'; import { AssetService, IAssetRepository } from '../asset'; import { IJobRepository, JobName } from '../job'; @@ -58,4 +58,29 @@ describe(AssetService.name, () => { }); }); }); + + describe('get map markers', () => { + it('should get geo information of assets', async () => { + assetMock.getMapMarkers.mockResolvedValue( + [assetEntityStub.withLocation].map((asset) => ({ + id: asset.id, + + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + lat: asset.exifInfo!.latitude!, + + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + lon: asset.exifInfo!.longitude!, + })), + ); + + const markers = await sut.getMapMarkers(authStub.user1, {}); + + expect(markers).toHaveLength(1); + expect(markers[0]).toEqual({ + id: assetEntityStub.withLocation.id, + lat: 100, + lon: 100, + }); + }); + }); }); diff --git a/server/libs/domain/src/asset/asset.service.ts b/server/libs/domain/src/asset/asset.service.ts index 89cb15e95e..42e301a21b 100644 --- a/server/libs/domain/src/asset/asset.service.ts +++ b/server/libs/domain/src/asset/asset.service.ts @@ -1,14 +1,17 @@ import { AssetEntity, AssetType } from '@app/infra/entities'; import { Inject } from '@nestjs/common'; +import { AuthUserDto } from '../auth'; import { IAssetUploadedJob, IJobRepository, JobName } from '../job'; import { AssetCore } from './asset.core'; import { IAssetRepository } from './asset.repository'; +import { MapMarkerDto } from './dto/map-marker.dto'; +import { MapMarkerResponseDto } from './response-dto'; export class AssetService { private assetCore: AssetCore; constructor( - @Inject(IAssetRepository) assetRepository: IAssetRepository, + @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, ) { this.assetCore = new AssetCore(assetRepository, jobRepository); @@ -28,4 +31,8 @@ export class AssetService { save(asset: Partial) { return this.assetCore.save(asset); } + + getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise { + return this.assetRepository.getMapMarkers(authUser.id, options); + } } diff --git a/server/libs/domain/src/asset/dto/map-marker.dto.ts b/server/libs/domain/src/asset/dto/map-marker.dto.ts new file mode 100644 index 0000000000..8d39ebf22a --- /dev/null +++ b/server/libs/domain/src/asset/dto/map-marker.dto.ts @@ -0,0 +1,10 @@ +import { toBoolean } from 'apps/immich/src/utils/transform.util'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class MapMarkerDto { + @IsOptional() + @IsBoolean() + @Transform(toBoolean) + isFavorite?: boolean; +} diff --git a/server/libs/domain/src/asset/response-dto/map-marker-response.dto.ts b/server/libs/domain/src/asset/response-dto/map-marker-response.dto.ts index b695b055a8..48c7b01495 100644 --- a/server/libs/domain/src/asset/response-dto/map-marker-response.dto.ts +++ b/server/libs/domain/src/asset/response-dto/map-marker-response.dto.ts @@ -1,35 +1,12 @@ -import { AssetEntity, AssetType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; export class MapMarkerResponseDto { + @ApiProperty() id!: string; - @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) - type!: AssetType; - - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ format: 'double' }) lat!: number; - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ format: 'double' }) lon!: number; } - -export function mapAssetMapMarker(entity: AssetEntity): MapMarkerResponseDto | null { - if (!entity.exifInfo) { - return null; - } - - const lat = entity.exifInfo.latitude; - const lon = entity.exifInfo.longitude; - - if (!lat || !lon) { - return null; - } - - return { - id: entity.id, - type: entity.type, - lon, - lat, - }; -} diff --git a/server/libs/domain/test/asset.repository.mock.ts b/server/libs/domain/test/asset.repository.mock.ts index cde4daaf58..44fd7bf7b2 100644 --- a/server/libs/domain/test/asset.repository.mock.ts +++ b/server/libs/domain/test/asset.repository.mock.ts @@ -9,5 +9,6 @@ export const newAssetRepositoryMock = (): jest.Mocked => { deleteAll: jest.fn(), save: jest.fn(), findLivePhotoMatch: jest.fn(), + getMapMarkers: jest.fn(), }; }; diff --git a/server/libs/domain/test/fixtures.ts b/server/libs/domain/test/fixtures.ts index eb94855069..9752ccee6c 100644 --- a/server/libs/domain/test/fixtures.ts +++ b/server/libs/domain/test/fixtures.ts @@ -12,6 +12,7 @@ import { UserEntity, UserTokenEntity, AssetFaceEntity, + ExifEntity, } from '@app/infra/entities'; import { AlbumResponseDto, @@ -220,6 +221,38 @@ export const assetEntityStub = { fileModifiedAt: '2022-06-19T23:41:36.910Z', fileCreatedAt: '2022-06-19T23:41:36.910Z', } as AssetEntity), + + withLocation: Object.freeze({ + id: 'asset-with-favorite-id', + deviceAssetId: 'device-asset-id', + fileModifiedAt: '2023-02-23T05:06:29.716Z', + fileCreatedAt: '2023-02-23T05:06:29.716Z', + owner: userEntityStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/original/path.ext', + resizePath: '/uploads/user-id/thumbs/path.ext', + type: AssetType.IMAGE, + webpPath: null, + encodedVideoPath: null, + createdAt: '2023-02-23T05:06:29.716Z', + updatedAt: '2023-02-23T05:06:29.716Z', + mimeType: null, + isFavorite: false, + isArchived: false, + duration: null, + isVisible: true, + livePhotoVideo: null, + livePhotoVideoId: null, + tags: [], + sharedLinks: [], + originalFileName: 'asset-id.ext', + faces: [], + exifInfo: { + latitude: 100, + longitude: 100, + } as ExifEntity, + }), }; export const albumStub = { diff --git a/server/libs/infra/src/repositories/asset.repository.ts b/server/libs/infra/src/repositories/asset.repository.ts index dda9572f56..24b7f4edb8 100644 --- a/server/libs/infra/src/repositories/asset.repository.ts +++ b/server/libs/infra/src/repositories/asset.repository.ts @@ -1,4 +1,11 @@ -import { AssetSearchOptions, IAssetRepository, LivePhotoSearchOptions, WithoutProperty } from '@app/domain'; +import { + AssetSearchOptions, + IAssetRepository, + LivePhotoSearchOptions, + MapMarker, + MapMarkerSearchOptions, + WithoutProperty, +} from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm'; @@ -21,7 +28,6 @@ export class AssetRepository implements IAssetRepository { }, }); } - async deleteAll(ownerId: string): Promise { await this.repository.delete({ ownerId }); } @@ -166,4 +172,44 @@ export class AssetRepository implements IAssetRepository { order: { fileCreatedAt: 'DESC' }, }); } + + async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise { + const { isFavorite } = options; + + const assets = await this.repository.find({ + select: { + id: true, + exifInfo: { + latitude: true, + longitude: true, + }, + }, + where: { + ownerId, + isVisible: true, + isArchived: false, + exifInfo: { + latitude: Not(IsNull()), + longitude: Not(IsNull()), + }, + isFavorite, + }, + relations: { + exifInfo: true, + }, + order: { + fileCreatedAt: 'DESC', + }, + }); + + return assets.map((asset) => ({ + id: asset.id, + + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + lat: asset.exifInfo!.latitude!, + + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + lon: asset.exifInfo!.longitude!, + })); + } } diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 41646a48f5..e135c3d50f 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1471,10 +1471,10 @@ export interface LogoutResponseDto { export interface MapMarkerResponseDto { /** * - * @type {AssetTypeEnum} + * @type {string} * @memberof MapMarkerResponseDto */ - 'type': AssetTypeEnum; + 'id': string; /** * * @type {number} @@ -1487,15 +1487,7 @@ export interface MapMarkerResponseDto { * @memberof MapMarkerResponseDto */ 'lon': number; - /** - * - * @type {string} - * @memberof MapMarkerResponseDto - */ - 'id': string; } - - /** * * @export @@ -4858,14 +4850,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }; }, /** - * Get all assets that have GPS information embedded + * * @param {boolean} [isFavorite] - * @param {boolean} [isArchived] - * @param {number} [skip] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getMapMarkers: async (isFavorite?: boolean, isArchived?: boolean, skip?: number, options: AxiosRequestConfig = {}): Promise => { + getMapMarkers: async (isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/asset/map-marker`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -4891,14 +4881,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['isFavorite'] = isFavorite; } - if (isArchived !== undefined) { - localVarQueryParameter['isArchived'] = isArchived; - } - - if (skip !== undefined) { - localVarQueryParameter['skip'] = skip; - } - setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -5471,15 +5453,13 @@ export const AssetApiFp = function(configuration?: Configuration) { return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** - * Get all assets that have GPS information embedded + * * @param {boolean} [isFavorite] - * @param {boolean} [isArchived] - * @param {number} [skip] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getMapMarkers(isFavorite?: boolean, isArchived?: boolean, skip?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, isArchived, skip, options); + async getMapMarkers(isFavorite?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMapMarkers(isFavorite, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -5739,15 +5719,13 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath return localVarFp.getCuratedObjects(options).then((request) => request(axios, basePath)); }, /** - * Get all assets that have GPS information embedded + * * @param {boolean} [isFavorite] - * @param {boolean} [isArchived] - * @param {number} [skip] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getMapMarkers(isFavorite?: boolean, isArchived?: boolean, skip?: number, options?: any): AxiosPromise> { - return localVarFp.getMapMarkers(isFavorite, isArchived, skip, options).then((request) => request(axios, basePath)); + getMapMarkers(isFavorite?: boolean, options?: any): AxiosPromise> { + return localVarFp.getMapMarkers(isFavorite, options).then((request) => request(axios, basePath)); }, /** * Get all asset of a device that are in the database, ID only. @@ -6036,16 +6014,14 @@ export class AssetApi extends BaseAPI { } /** - * Get all assets that have GPS information embedded + * * @param {boolean} [isFavorite] - * @param {boolean} [isArchived] - * @param {number} [skip] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public getMapMarkers(isFavorite?: boolean, isArchived?: boolean, skip?: number, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getMapMarkers(isFavorite, isArchived, skip, options).then((request) => request(this.axios, this.basePath)); + public getMapMarkers(isFavorite?: boolean, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getMapMarkers(isFavorite, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 80e5207614..8544fe1c46 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -237,7 +237,7 @@ {#if latlng}
{#await import('../shared-components/leaflet') then { Map, TileLayer, Marker }} - + + export interface MapSettings { + allowDarkMode: boolean; + onlyFavorites: boolean; + } + + + + + dispatch('close')}> +
+

+ Map Settings +

+ +
dispatch('save', settings)} class="flex flex-col gap-4"> + + + +
+ + +
+ +
+
diff --git a/web/src/lib/components/shared-components/full-screen-modal.svelte b/web/src/lib/components/shared-components/full-screen-modal.svelte index 9c3109d922..bd9428d279 100644 --- a/web/src/lib/components/shared-components/full-screen-modal.svelte +++ b/web/src/lib/components/shared-components/full-screen-modal.svelte @@ -3,7 +3,7 @@ import { createEventDispatcher } from 'svelte'; import { fade } from 'svelte/transition'; - const dispatch = createEventDispatcher(); + const dispatch = createEventDispatcher<{ clickOutside: void }>();
import { createContext } from '$lib/utils/context'; - import { MarkerClusterGroup, Marker, Icon, LeafletEvent } from 'leaflet'; + import { Icon, LeafletEvent, Marker, MarkerClusterGroup } from 'leaflet'; const { get: getContext, set: setClusterContext } = createContext<() => MarkerClusterGroup>(); @@ -10,11 +10,11 @@ - -{#if cluster} - -{/if} - - diff --git a/web/src/lib/components/shared-components/leaflet/control.svelte b/web/src/lib/components/shared-components/leaflet/control.svelte new file mode 100644 index 0000000000..126a566d35 --- /dev/null +++ b/web/src/lib/components/shared-components/leaflet/control.svelte @@ -0,0 +1,35 @@ + + +
+ +
diff --git a/web/src/lib/components/shared-components/leaflet/index.ts b/web/src/lib/components/shared-components/leaflet/index.ts index b31e3a2ae1..53de7d296f 100644 --- a/web/src/lib/components/shared-components/leaflet/index.ts +++ b/web/src/lib/components/shared-components/leaflet/index.ts @@ -1,4 +1,5 @@ +export { default as AssetMarkerCluster } from './asset-marker-cluster.svelte'; +export { default as Control } from './control.svelte'; export { default as Map } from './map.svelte'; export { default as Marker } from './marker.svelte'; export { default as TileLayer } from './tile-layer.svelte'; -export { default as AssetMarkerCluster } from './asset-marker-cluster.svelte'; diff --git a/web/src/lib/components/shared-components/leaflet/map.svelte b/web/src/lib/components/shared-components/leaflet/map.svelte index b7afca2a63..ad587c4371 100644 --- a/web/src/lib/components/shared-components/leaflet/map.svelte +++ b/web/src/lib/components/shared-components/leaflet/map.svelte @@ -12,11 +12,13 @@ -
+
{#if map} {/if}
+ + diff --git a/web/src/lib/components/shared-components/leaflet/tile-layer.svelte b/web/src/lib/components/shared-components/leaflet/tile-layer.svelte index bcbaff1d7a..60a5662eb5 100644 --- a/web/src/lib/components/shared-components/leaflet/tile-layer.svelte +++ b/web/src/lib/components/shared-components/leaflet/tile-layer.svelte @@ -5,30 +5,16 @@ export let urlTemplate: string; export let options: TileLayerOptions | undefined = undefined; - export let allowDarkMode = false; let tileLayer: TileLayer; const map = getMapContext(); onMount(() => { - tileLayer = new TileLayer(urlTemplate, { - className: allowDarkMode ? 'leaflet-layer-dynamic' : 'leaflet-layer', - ...options - }).addTo(map); + tileLayer = new TileLayer(urlTemplate, options).addTo(map); }); onDestroy(() => { if (tileLayer) tileLayer.remove(); }); - - diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index fd65b73954..cda82d1818 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -1,4 +1,5 @@ import { browser } from '$app/environment'; +import { MapSettings } from '$lib/components/map-page/map-settings-modal.svelte'; import { persisted } from 'svelte-local-storage-store'; const initialTheme = @@ -19,3 +20,8 @@ export const locale = persisted('locale', undefined, { stringify: (obj) => obj ?? '' } }); + +export const mapSettings = persisted('map-settings', { + allowDarkMode: true, + onlyFavorites: false +}); diff --git a/web/src/routes/(user)/map/+page.server.ts b/web/src/routes/(user)/map/+page.server.ts index d4a7ff2e8a..6bcb95791e 100644 --- a/web/src/routes/(user)/map/+page.server.ts +++ b/web/src/routes/(user)/map/+page.server.ts @@ -2,22 +2,15 @@ import { AppRoute } from '$lib/constants'; import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -export const load = (async ({ locals: { api, user } }) => { +export const load = (async ({ locals: { user } }) => { if (!user) { throw redirect(302, AppRoute.AUTH_LOGIN); } - try { - const { data: mapMarkers } = await api.assetApi.getMapMarkers(); - - return { - user, - mapMarkers, - meta: { - title: 'Map' - } - }; - } catch (e) { - throw redirect(302, AppRoute.AUTH_LOGIN); - } + return { + user, + meta: { + title: 'Map' + } + }; }) satisfies PageServerLoad; diff --git a/web/src/routes/(user)/map/+page.svelte b/web/src/routes/(user)/map/+page.svelte index 0bdf0f863a..e0a369e6a4 100644 --- a/web/src/routes/(user)/map/+page.svelte +++ b/web/src/routes/(user)/map/+page.svelte @@ -1,27 +1,43 @@ -
- -
- {#await import('$lib/components/shared-components/leaflet') then { Map, TileLayer, AssetMarkerCluster }} - - + {#await import('$lib/components/shared-components/leaflet') then { Map, TileLayer, AssetMarkerCluster, Control }} + {#await mapMarkersPromise then mapMarkers} + OpenStreetMap' + maxBounds: [ + [-90, -180], + [90, 180] + ], + minZoom: 3 }} - /> - onViewAssets(event.detail.assets)} - /> - + > + OpenStreetMap' + }} + /> + onViewAssets(event.detail.assets)} + /> + + + + + {/await} {/await}
@@ -78,3 +122,20 @@ /> {/if} + +{#if showSettingsModal} + (showSettingsModal = false)} + on:save={async ({ detail }) => { + const shouldUpdate = detail.onlyFavorites !== $mapSettings.onlyFavorites; + showSettingsModal = false; + $mapSettings = detail; + + if (shouldUpdate) { + const markers = await loadMapMarkers(); + mapMarkersPromise = Promise.resolve(markers); + } + }} + /> +{/if}