diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index fd1338532bee2..2e5a4641fdd18 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -1041,12 +1041,10 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getMapMarkers** -> List getMapMarkers(isFavorite, isArchived, skip) +> List getMapMarkers(isFavorite) -Get all assets that have GPS information embedded - ### Example ```dart import 'package:openapi/api.dart'; @@ -1067,11 +1065,9 @@ import 'package:openapi/api.dart'; final api_instance = AssetApi(); final isFavorite = true; // bool | -final isArchived = true; // bool | -final skip = 8.14; // num | try { - final result = api_instance.getMapMarkers(isFavorite, isArchived, skip); + final result = api_instance.getMapMarkers(isFavorite); print(result); } catch (e) { print('Exception when calling AssetApi->getMapMarkers: $e\n'); @@ -1083,8 +1079,6 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **isFavorite** | **bool**| | [optional] - **isArchived** | **bool**| | [optional] - **skip** | **num**| | [optional] ### Return type diff --git a/mobile/openapi/doc/MapMarkerResponseDto.md b/mobile/openapi/doc/MapMarkerResponseDto.md index 5994a8ac3554d..94f253d380249 100644 --- a/mobile/openapi/doc/MapMarkerResponseDto.md +++ b/mobile/openapi/doc/MapMarkerResponseDto.md @@ -8,10 +8,9 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | +**id** | **String** | | **lat** | **double** | | **lon** | **double** | | -**id** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 9972700920703..e6cde800db0cc 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -979,18 +979,11 @@ class AssetApi { return null; } - /// Get all assets that have GPS information embedded - /// - /// Note: This method returns the HTTP [Response]. - /// + /// Performs an HTTP 'GET /asset/map-marker' operation and returns the [Response]. /// Parameters: /// /// * [bool] isFavorite: - /// - /// * [bool] isArchived: - /// - /// * [num] skip: - Future getMapMarkersWithHttpInfo({ bool? isFavorite, bool? isArchived, num? skip, }) async { + Future getMapMarkersWithHttpInfo({ bool? isFavorite, }) async { // ignore: prefer_const_declarations final path = r'/asset/map-marker'; @@ -1004,12 +997,6 @@ class AssetApi { if (isFavorite != null) { queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); } - if (isArchived != null) { - queryParams.addAll(_queryParams('', 'isArchived', isArchived)); - } - if (skip != null) { - queryParams.addAll(_queryParams('', 'skip', skip)); - } const contentTypes = []; @@ -1025,17 +1012,11 @@ class AssetApi { ); } - /// Get all assets that have GPS information embedded - /// /// Parameters: /// /// * [bool] isFavorite: - /// - /// * [bool] isArchived: - /// - /// * [num] skip: - Future?> getMapMarkers({ bool? isFavorite, bool? isArchived, num? skip, }) async { - final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, isArchived: isArchived, skip: skip, ); + Future?> getMapMarkers({ bool? isFavorite, }) async { + final response = await getMapMarkersWithHttpInfo( isFavorite: isFavorite, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/map_marker_response_dto.dart b/mobile/openapi/lib/model/map_marker_response_dto.dart index e8d0001acf9ed..f094c8be76adc 100644 --- a/mobile/openapi/lib/model/map_marker_response_dto.dart +++ b/mobile/openapi/lib/model/map_marker_response_dto.dart @@ -13,44 +13,38 @@ part of openapi.api; class MapMarkerResponseDto { /// Returns a new [MapMarkerResponseDto] instance. MapMarkerResponseDto({ - required this.type, + required this.id, required this.lat, required this.lon, - required this.id, }); - AssetTypeEnum type; + String id; double lat; double lon; - String id; - @override bool operator ==(Object other) => identical(this, other) || other is MapMarkerResponseDto && - other.type == type && + other.id == id && other.lat == lat && - other.lon == lon && - other.id == id; + other.lon == lon; @override int get hashCode => // ignore: unnecessary_parenthesis - (type.hashCode) + + (id.hashCode) + (lat.hashCode) + - (lon.hashCode) + - (id.hashCode); + (lon.hashCode); @override - String toString() => 'MapMarkerResponseDto[type=$type, lat=$lat, lon=$lon, id=$id]'; + String toString() => 'MapMarkerResponseDto[id=$id, lat=$lat, lon=$lon]'; Map toJson() { final json = {}; - json[r'type'] = this.type; + json[r'id'] = this.id; json[r'lat'] = this.lat; json[r'lon'] = this.lon; - json[r'id'] = this.id; return json; } @@ -73,10 +67,9 @@ class MapMarkerResponseDto { }()); return MapMarkerResponseDto( - type: AssetTypeEnum.fromJson(json[r'type'])!, + id: mapValueOfType(json, r'id')!, lat: mapValueOfType(json, r'lat')!, lon: mapValueOfType(json, r'lon')!, - id: mapValueOfType(json, r'id')!, ); } return null; @@ -124,10 +117,9 @@ class MapMarkerResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'type', + 'id', 'lat', 'lon', - 'id', }; } diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index b9091fbeea78b..085f1560f822c 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -117,9 +117,7 @@ void main() { // TODO }); - // Get all assets that have GPS information embedded - // - //Future> getMapMarkers({ bool isFavorite, bool isArchived, num skip }) async + //Future> getMapMarkers({ bool isFavorite }) async test('test getMapMarkers', () async { // TODO }); diff --git a/mobile/openapi/test/map_marker_response_dto_test.dart b/mobile/openapi/test/map_marker_response_dto_test.dart index 312d4052dd8c0..f8308116ffd24 100644 --- a/mobile/openapi/test/map_marker_response_dto_test.dart +++ b/mobile/openapi/test/map_marker_response_dto_test.dart @@ -16,8 +16,8 @@ void main() { // final instance = MapMarkerResponseDto(); group('test MapMarkerResponseDto', () { - // AssetTypeEnum type - test('to test the property `type`', () async { + // String id + test('to test the property `id`', () async { // TODO }); @@ -31,11 +31,6 @@ void main() { // TODO }); - // String id - test('to test the property `id`', () async { - // TODO - }); - }); 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 2a20b3dd48e47..6088b8122217a 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 b9697a1faf193..12bfbc7007462 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 3d40b7984fd60..e130b6b07b7d9 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 7d78292aee750..a9168975b4922 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 0000000000000..ee71e814f78b1 --- /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 1ffb3b79cb8c3..d86db2bebcb86 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 64c5effbf0a73..7c2ed1bdccb47 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 ed3a66a29c8d2..20edc50b7c0f3 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 debf488afa01d..3a023a1d10f82 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 89cb15e95e7d4..42e301a21b1a5 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 0000000000000..8d39ebf22a7d0 --- /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 b695b055a8d6a..48c7b01495196 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 cde4daaf58369..44fd7bf7b26a9 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 eb948550691df..9752ccee6cd04 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 dda9572f562a5..24b7f4edb8c42 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 41646a48f5816..e135c3d50fe26 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 80e520761446f..8544fe1c46e24 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 9c3109d922150..bd9428d2793c0 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 0000000000000..126a566d35bee --- /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 b31e3a2ae147d..53de7d296f694 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 b7afca2a638de..ad587c43715b9 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 bcbaff1d7a6f1..60a5662eb52be 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 fd65b73954dce..cda82d1818625 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 d4a7ff2e8a934..6bcb95791eea3 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 0bdf0f863a7d4..e0a369e6a4c2a 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}