mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-01 02:17:43 +09:00 
			
		
		
		
	feat(server): visibility column (#17939)
* feat: private view * pr feedback * sql generation * feat: visibility column * fix: set visibility value as the same as the still part after unlinked live photos * fix: test * pr feedback
This commit is contained in:
		| @@ -3,6 +3,7 @@ import { | ||||
|   AssetMediaStatus, | ||||
|   AssetResponseDto, | ||||
|   AssetTypeEnum, | ||||
|   AssetVisibility, | ||||
|   getAssetInfo, | ||||
|   getMyUser, | ||||
|   LoginResponseDto, | ||||
| @@ -119,9 +120,9 @@ describe('/asset', () => { | ||||
|       // stats | ||||
|       utils.createAsset(statsUser.accessToken), | ||||
|       utils.createAsset(statsUser.accessToken, { isFavorite: true }), | ||||
|       utils.createAsset(statsUser.accessToken, { isArchived: true }), | ||||
|       utils.createAsset(statsUser.accessToken, { visibility: AssetVisibility.Archive }), | ||||
|       utils.createAsset(statsUser.accessToken, { | ||||
|         isArchived: true, | ||||
|         visibility: AssetVisibility.Archive, | ||||
|         isFavorite: true, | ||||
|         assetData: { filename: 'example.mp4' }, | ||||
|       }), | ||||
| @@ -309,7 +310,7 @@ describe('/asset', () => { | ||||
|       }); | ||||
|  | ||||
|       it('disallows viewing archived assets', async () => { | ||||
|         const asset = await utils.createAsset(user1.accessToken, { isArchived: true }); | ||||
|         const asset = await utils.createAsset(user1.accessToken, { visibility: AssetVisibility.Archive }); | ||||
|  | ||||
|         const { status } = await request(app) | ||||
|           .get(`/assets/${asset.id}`) | ||||
| @@ -353,7 +354,7 @@ describe('/asset', () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .get('/assets/statistics') | ||||
|         .set('Authorization', `Bearer ${statsUser.accessToken}`) | ||||
|         .query({ isArchived: true }); | ||||
|         .query({ visibility: AssetVisibility.Archive }); | ||||
|  | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ images: 1, videos: 1, total: 2 }); | ||||
| @@ -363,7 +364,7 @@ describe('/asset', () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .get('/assets/statistics') | ||||
|         .set('Authorization', `Bearer ${statsUser.accessToken}`) | ||||
|         .query({ isFavorite: true, isArchived: true }); | ||||
|         .query({ isFavorite: true, visibility: AssetVisibility.Archive }); | ||||
|  | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ images: 0, videos: 1, total: 1 }); | ||||
| @@ -373,7 +374,7 @@ describe('/asset', () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .get('/assets/statistics') | ||||
|         .set('Authorization', `Bearer ${statsUser.accessToken}`) | ||||
|         .query({ isFavorite: false, isArchived: false }); | ||||
|         .query({ isFavorite: false, visibility: AssetVisibility.Timeline }); | ||||
|  | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toEqual({ images: 1, videos: 0, total: 1 }); | ||||
| @@ -459,7 +460,7 @@ describe('/asset', () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .put(`/assets/${user1Assets[0].id}`) | ||||
|         .set('Authorization', `Bearer ${user1.accessToken}`) | ||||
|         .send({ isArchived: true }); | ||||
|         .send({ visibility: AssetVisibility.Archive }); | ||||
|       expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true }); | ||||
|       expect(status).toEqual(200); | ||||
|     }); | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { LoginResponseDto } from '@immich/sdk'; | ||||
| import { AssetVisibility, LoginResponseDto } from '@immich/sdk'; | ||||
| import { readFile } from 'node:fs/promises'; | ||||
| import { basename, join } from 'node:path'; | ||||
| import { Socket } from 'socket.io-client'; | ||||
| @@ -44,7 +44,7 @@ describe('/map', () => { | ||||
|     it('should get map markers for all non-archived assets', async () => { | ||||
|       const { status, body } = await request(app) | ||||
|         .get('/map/markers') | ||||
|         .query({ isArchived: false }) | ||||
|         .query({ visibility: AssetVisibility.Timeline }) | ||||
|         .set('Authorization', `Bearer ${admin.accessToken}`); | ||||
|  | ||||
|       expect(status).toBe(200); | ||||
|   | ||||
| @@ -1,4 +1,11 @@ | ||||
| import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk'; | ||||
| import { | ||||
|   AssetMediaResponseDto, | ||||
|   AssetResponseDto, | ||||
|   AssetVisibility, | ||||
|   deleteAssets, | ||||
|   LoginResponseDto, | ||||
|   updateAsset, | ||||
| } from '@immich/sdk'; | ||||
| import { DateTime } from 'luxon'; | ||||
| import { readFile } from 'node:fs/promises'; | ||||
| import { join } from 'node:path'; | ||||
| @@ -49,7 +56,7 @@ describe('/search', () => { | ||||
|       { filename: '/formats/motionphoto/samsung-one-ui-6.heic' }, | ||||
|       { filename: '/formats/motionphoto/samsung-one-ui-5.jpg' }, | ||||
|  | ||||
|       { filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } }, | ||||
|       { filename: '/metadata/gps-position/thompson-springs.jpg', dto: { visibility: AssetVisibility.Archive } }, | ||||
|  | ||||
|       // used for search suggestions | ||||
|       { filename: '/formats/png/density_plot.png' }, | ||||
| @@ -171,12 +178,12 @@ describe('/search', () => { | ||||
|         deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }), | ||||
|       }, | ||||
|       { | ||||
|         should: 'should search by isArchived (true)', | ||||
|         deferred: () => ({ dto: { isArchived: true }, assets: [assetSprings] }), | ||||
|         should: 'should search by visibility (AssetVisibility.Archive)', | ||||
|         deferred: () => ({ dto: { visibility: AssetVisibility.Archive }, assets: [assetSprings] }), | ||||
|       }, | ||||
|       { | ||||
|         should: 'should search by isArchived (false)', | ||||
|         deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }), | ||||
|         should: 'should search by visibility (AssetVisibility.Timeline)', | ||||
|         deferred: () => ({ dto: { size: 1, visibility: AssetVisibility.Timeline }, assets: [assetLast] }), | ||||
|       }, | ||||
|       { | ||||
|         should: 'should search by type (image)', | ||||
| @@ -185,7 +192,7 @@ describe('/search', () => { | ||||
|       { | ||||
|         should: 'should search by type (video)', | ||||
|         deferred: () => ({ | ||||
|           dto: { type: 'VIDEO' }, | ||||
|           dto: { type: 'VIDEO', visibility: AssetVisibility.Hidden }, | ||||
|           assets: [ | ||||
|             // the three live motion photos | ||||
|             { id: expect.any(String) }, | ||||
| @@ -229,13 +236,6 @@ describe('/search', () => { | ||||
|         should: 'should search by takenAfter (no results)', | ||||
|         deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }), | ||||
|       }, | ||||
|       //   { | ||||
|       //     should: 'should search by originalPath', | ||||
|       //     deferred: () => ({ | ||||
|       //       dto: { originalPath: asset1.originalPath }, | ||||
|       //       assets: [asset1], | ||||
|       //     }), | ||||
|       //   }, | ||||
|       { | ||||
|         should: 'should search by originalFilename', | ||||
|         deferred: () => ({ | ||||
| @@ -265,7 +265,7 @@ describe('/search', () => { | ||||
|         deferred: () => ({ | ||||
|           dto: { | ||||
|             city: '', | ||||
|             isVisible: true, | ||||
|             visibility: AssetVisibility.Timeline, | ||||
|             includeNull: true, | ||||
|           }, | ||||
|           assets: [assetLast], | ||||
| @@ -276,7 +276,7 @@ describe('/search', () => { | ||||
|         deferred: () => ({ | ||||
|           dto: { | ||||
|             city: null, | ||||
|             isVisible: true, | ||||
|             visibility: AssetVisibility.Timeline, | ||||
|             includeNull: true, | ||||
|           }, | ||||
|           assets: [assetLast], | ||||
| @@ -297,7 +297,7 @@ describe('/search', () => { | ||||
|         deferred: () => ({ | ||||
|           dto: { | ||||
|             state: '', | ||||
|             isVisible: true, | ||||
|             visibility: AssetVisibility.Timeline, | ||||
|             withExif: true, | ||||
|             includeNull: true, | ||||
|           }, | ||||
| @@ -309,7 +309,7 @@ describe('/search', () => { | ||||
|         deferred: () => ({ | ||||
|           dto: { | ||||
|             state: null, | ||||
|             isVisible: true, | ||||
|             visibility: AssetVisibility.Timeline, | ||||
|             includeNull: true, | ||||
|           }, | ||||
|           assets: [assetLast, assetNotocactus], | ||||
| @@ -330,7 +330,7 @@ describe('/search', () => { | ||||
|         deferred: () => ({ | ||||
|           dto: { | ||||
|             country: '', | ||||
|             isVisible: true, | ||||
|             visibility: AssetVisibility.Timeline, | ||||
|             includeNull: true, | ||||
|           }, | ||||
|           assets: [assetLast], | ||||
| @@ -341,7 +341,7 @@ describe('/search', () => { | ||||
|         deferred: () => ({ | ||||
|           dto: { | ||||
|             country: null, | ||||
|             isVisible: true, | ||||
|             visibility: AssetVisibility.Timeline, | ||||
|             includeNull: true, | ||||
|           }, | ||||
|           assets: [assetLast], | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk'; | ||||
| import { AssetMediaResponseDto, AssetVisibility, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk'; | ||||
| import { DateTime } from 'luxon'; | ||||
| import { createUserDto } from 'src/fixtures'; | ||||
| import { errorDto } from 'src/responses'; | ||||
| @@ -104,7 +104,7 @@ describe('/timeline', () => { | ||||
|       const req1 = await request(app) | ||||
|         .get('/timeline/buckets') | ||||
|         .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) | ||||
|         .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true }); | ||||
|         .query({ size: TimeBucketSize.Month, withPartners: true, visibility: AssetVisibility.Archive }); | ||||
|  | ||||
|       expect(req1.status).toBe(400); | ||||
|       expect(req1.body).toEqual(errorDto.badRequest()); | ||||
| @@ -112,7 +112,7 @@ describe('/timeline', () => { | ||||
|       const req2 = await request(app) | ||||
|         .get('/timeline/buckets') | ||||
|         .set('Authorization', `Bearer ${user.accessToken}`) | ||||
|         .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined }); | ||||
|         .query({ size: TimeBucketSize.Month, withPartners: true, visibility: undefined }); | ||||
|  | ||||
|       expect(req2.status).toBe(400); | ||||
|       expect(req2.body).toEqual(errorDto.badRequest()); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { | ||||
|   AssetMediaCreateDto, | ||||
|   AssetMediaResponseDto, | ||||
|   AssetResponseDto, | ||||
|   AssetVisibility, | ||||
|   CheckExistingAssetsDto, | ||||
|   CreateAlbumDto, | ||||
|   CreateLibraryDto, | ||||
| @@ -429,7 +430,10 @@ export const utils = { | ||||
|   }, | ||||
|  | ||||
|   archiveAssets: (accessToken: string, ids: string[]) => | ||||
|     updateAssets({ assetBulkUpdateDto: { ids, isArchived: true } }, { headers: asBearerAuth(accessToken) }), | ||||
|     updateAssets( | ||||
|       { assetBulkUpdateDto: { ids, visibility: AssetVisibility.Archive } }, | ||||
|       { headers: asBearerAuth(accessToken) }, | ||||
|     ), | ||||
|  | ||||
|   deleteAssets: (accessToken: string, ids: string[]) => | ||||
|     deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }), | ||||
|   | ||||
| @@ -197,7 +197,7 @@ class AssetService { | ||||
|         ids: assets.map((e) => e.remoteId!).toList(), | ||||
|         dateTimeOriginal: updateAssetDto.dateTimeOriginal, | ||||
|         isFavorite: updateAssetDto.isFavorite, | ||||
|         isArchived: updateAssetDto.isArchived, | ||||
|         visibility: updateAssetDto.visibility, | ||||
|         latitude: updateAssetDto.latitude, | ||||
|         longitude: updateAssetDto.longitude, | ||||
|       ), | ||||
| @@ -229,7 +229,13 @@ class AssetService { | ||||
|     bool isArchived, | ||||
|   ) async { | ||||
|     try { | ||||
|       await updateAssets(assets, UpdateAssetDto(isArchived: isArchived)); | ||||
|       await updateAssets( | ||||
|         assets, | ||||
|         UpdateAssetDto( | ||||
|           visibility: | ||||
|               isArchived ? AssetVisibility.archive : AssetVisibility.timeline, | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       for (var element in assets) { | ||||
|         element.isArchived = isArchived; | ||||
|   | ||||
| @@ -68,7 +68,9 @@ class SearchService { | ||||
|             model: filter.camera.model, | ||||
|             takenAfter: filter.date.takenAfter, | ||||
|             takenBefore: filter.date.takenBefore, | ||||
|             isArchived: filter.display.isArchive ? true : null, | ||||
|             visibility: filter.display.isArchive | ||||
|                 ? AssetVisibility.archive | ||||
|                 : AssetVisibility.timeline, | ||||
|             isFavorite: filter.display.isFavorite ? true : null, | ||||
|             isNotInAlbum: filter.display.isNotInAlbum ? true : null, | ||||
|             personIds: filter.people.map((e) => e.id).toList(), | ||||
| @@ -95,7 +97,9 @@ class SearchService { | ||||
|             model: filter.camera.model, | ||||
|             takenAfter: filter.date.takenAfter, | ||||
|             takenBefore: filter.date.takenBefore, | ||||
|             isArchived: filter.display.isArchive ? true : null, | ||||
|             visibility: filter.display.isArchive | ||||
|                 ? AssetVisibility.archive | ||||
|                 : AssetVisibility.timeline, | ||||
|             isFavorite: filter.display.isFavorite ? true : null, | ||||
|             isNotInAlbum: filter.display.isNotInAlbum ? true : null, | ||||
|             personIds: filter.people.map((e) => e.id).toList(), | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -302,6 +302,7 @@ Class | Method | HTTP request | Description | ||||
|  - [AssetStackResponseDto](doc//AssetStackResponseDto.md) | ||||
|  - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) | ||||
|  - [AssetTypeEnum](doc//AssetTypeEnum.md) | ||||
|  - [AssetVisibility](doc//AssetVisibility.md) | ||||
|  - [AudioCodec](doc//AudioCodec.md) | ||||
|  - [AvatarUpdate](doc//AvatarUpdate.md) | ||||
|  - [BulkIdResponseDto](doc//BulkIdResponseDto.md) | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -106,6 +106,7 @@ part 'model/asset_response_dto.dart'; | ||||
| part 'model/asset_stack_response_dto.dart'; | ||||
| part 'model/asset_stats_response_dto.dart'; | ||||
| part 'model/asset_type_enum.dart'; | ||||
| part 'model/asset_visibility.dart'; | ||||
| part 'model/audio_codec.dart'; | ||||
| part 'model/avatar_update.dart'; | ||||
| part 'model/bulk_id_response_dto.dart'; | ||||
|   | ||||
							
								
								
									
										50
									
								
								mobile/openapi/lib/api/assets_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										50
									
								
								mobile/openapi/lib/api/assets_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -342,12 +342,12 @@ class AssetsApi { | ||||
|   /// Performs an HTTP 'GET /assets/statistics' operation and returns the [Response]. | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [bool] isTrashed: | ||||
|   Future<Response> getAssetStatisticsWithHttpInfo({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async { | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   Future<Response> getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/assets/statistics'; | ||||
| 
 | ||||
| @@ -358,15 +358,15 @@ class AssetsApi { | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     if (isArchived != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isArchived', isArchived)); | ||||
|     } | ||||
|     if (isFavorite != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); | ||||
|     } | ||||
|     if (isTrashed != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); | ||||
|     } | ||||
|     if (visibility != null) { | ||||
|       queryParams.addAll(_queryParams('', 'visibility', visibility)); | ||||
|     } | ||||
| 
 | ||||
|     const contentTypes = <String>[]; | ||||
| 
 | ||||
| @@ -384,13 +384,13 @@ class AssetsApi { | ||||
| 
 | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [bool] isTrashed: | ||||
|   Future<AssetStatsResponseDto?> getAssetStatistics({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async { | ||||
|     final response = await getAssetStatisticsWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, ); | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   Future<AssetStatsResponseDto?> getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { | ||||
|     final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
| @@ -788,16 +788,14 @@ class AssetsApi { | ||||
|   /// | ||||
|   /// * [String] duration: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [bool] isVisible: | ||||
|   /// | ||||
|   /// * [String] livePhotoVideoId: | ||||
|   /// | ||||
|   /// * [MultipartFile] sidecarData: | ||||
|   Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async { | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/assets'; | ||||
| 
 | ||||
| @@ -845,18 +843,10 @@ class AssetsApi { | ||||
|       hasFields = true; | ||||
|       mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt); | ||||
|     } | ||||
|     if (isArchived != null) { | ||||
|       hasFields = true; | ||||
|       mp.fields[r'isArchived'] = parameterToString(isArchived); | ||||
|     } | ||||
|     if (isFavorite != null) { | ||||
|       hasFields = true; | ||||
|       mp.fields[r'isFavorite'] = parameterToString(isFavorite); | ||||
|     } | ||||
|     if (isVisible != null) { | ||||
|       hasFields = true; | ||||
|       mp.fields[r'isVisible'] = parameterToString(isVisible); | ||||
|     } | ||||
|     if (livePhotoVideoId != null) { | ||||
|       hasFields = true; | ||||
|       mp.fields[r'livePhotoVideoId'] = parameterToString(livePhotoVideoId); | ||||
| @@ -866,6 +856,10 @@ class AssetsApi { | ||||
|       mp.fields[r'sidecarData'] = sidecarData.field; | ||||
|       mp.files.add(sidecarData); | ||||
|     } | ||||
|     if (visibility != null) { | ||||
|       hasFields = true; | ||||
|       mp.fields[r'visibility'] = parameterToString(visibility); | ||||
|     } | ||||
|     if (hasFields) { | ||||
|       postBody = mp; | ||||
|     } | ||||
| @@ -900,17 +894,15 @@ class AssetsApi { | ||||
|   /// | ||||
|   /// * [String] duration: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [bool] isVisible: | ||||
|   /// | ||||
|   /// * [String] livePhotoVideoId: | ||||
|   /// | ||||
|   /// * [MultipartFile] sidecarData: | ||||
|   Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async { | ||||
|     final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt,  key: key, xImmichChecksum: xImmichChecksum, duration: duration, isArchived: isArchived, isFavorite: isFavorite, isVisible: isVisible, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, ); | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { | ||||
|     final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt,  key: key, xImmichChecksum: xImmichChecksum, duration: duration, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										40
									
								
								mobile/openapi/lib/api/timeline_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										40
									
								
								mobile/openapi/lib/api/timeline_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -25,8 +25,6 @@ class TimelineApi { | ||||
|   /// | ||||
|   /// * [String] albumId: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [bool] isTrashed: | ||||
| @@ -41,10 +39,12 @@ class TimelineApi { | ||||
|   /// | ||||
|   /// * [String] userId: | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   /// | ||||
|   /// * [bool] withPartners: | ||||
|   /// | ||||
|   /// * [bool] withStacked: | ||||
|   Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { | ||||
|   Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/timeline/bucket'; | ||||
| 
 | ||||
| @@ -58,9 +58,6 @@ class TimelineApi { | ||||
|     if (albumId != null) { | ||||
|       queryParams.addAll(_queryParams('', 'albumId', albumId)); | ||||
|     } | ||||
|     if (isArchived != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isArchived', isArchived)); | ||||
|     } | ||||
|     if (isFavorite != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); | ||||
|     } | ||||
| @@ -84,6 +81,9 @@ class TimelineApi { | ||||
|     if (userId != null) { | ||||
|       queryParams.addAll(_queryParams('', 'userId', userId)); | ||||
|     } | ||||
|     if (visibility != null) { | ||||
|       queryParams.addAll(_queryParams('', 'visibility', visibility)); | ||||
|     } | ||||
|     if (withPartners != null) { | ||||
|       queryParams.addAll(_queryParams('', 'withPartners', withPartners)); | ||||
|     } | ||||
| @@ -113,8 +113,6 @@ class TimelineApi { | ||||
|   /// | ||||
|   /// * [String] albumId: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [bool] isTrashed: | ||||
| @@ -129,11 +127,13 @@ class TimelineApi { | ||||
|   /// | ||||
|   /// * [String] userId: | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   /// | ||||
|   /// * [bool] withPartners: | ||||
|   /// | ||||
|   /// * [bool] withStacked: | ||||
|   Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { | ||||
|     final response = await getTimeBucketWithHttpInfo(size, timeBucket,  albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); | ||||
|   Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { | ||||
|     final response = await getTimeBucketWithHttpInfo(size, timeBucket,  albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
| @@ -157,8 +157,6 @@ class TimelineApi { | ||||
|   /// | ||||
|   /// * [String] albumId: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [bool] isTrashed: | ||||
| @@ -173,10 +171,12 @@ class TimelineApi { | ||||
|   /// | ||||
|   /// * [String] userId: | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   /// | ||||
|   /// * [bool] withPartners: | ||||
|   /// | ||||
|   /// * [bool] withStacked: | ||||
|   Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { | ||||
|   Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/timeline/buckets'; | ||||
| 
 | ||||
| @@ -190,9 +190,6 @@ class TimelineApi { | ||||
|     if (albumId != null) { | ||||
|       queryParams.addAll(_queryParams('', 'albumId', albumId)); | ||||
|     } | ||||
|     if (isArchived != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isArchived', isArchived)); | ||||
|     } | ||||
|     if (isFavorite != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); | ||||
|     } | ||||
| @@ -215,6 +212,9 @@ class TimelineApi { | ||||
|     if (userId != null) { | ||||
|       queryParams.addAll(_queryParams('', 'userId', userId)); | ||||
|     } | ||||
|     if (visibility != null) { | ||||
|       queryParams.addAll(_queryParams('', 'visibility', visibility)); | ||||
|     } | ||||
|     if (withPartners != null) { | ||||
|       queryParams.addAll(_queryParams('', 'withPartners', withPartners)); | ||||
|     } | ||||
| @@ -242,8 +242,6 @@ class TimelineApi { | ||||
|   /// | ||||
|   /// * [String] albumId: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [bool] isTrashed: | ||||
| @@ -258,11 +256,13 @@ class TimelineApi { | ||||
|   /// | ||||
|   /// * [String] userId: | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   /// | ||||
|   /// * [bool] withPartners: | ||||
|   /// | ||||
|   /// * [bool] withStacked: | ||||
|   Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { | ||||
|     final response = await getTimeBucketsWithHttpInfo(size,  albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); | ||||
|   Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async { | ||||
|     final response = await getTimeBucketsWithHttpInfo(size,  albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -268,6 +268,8 @@ class ApiClient { | ||||
|           return AssetStatsResponseDto.fromJson(value); | ||||
|         case 'AssetTypeEnum': | ||||
|           return AssetTypeEnumTypeTransformer().decode(value); | ||||
|         case 'AssetVisibility': | ||||
|           return AssetVisibilityTypeTransformer().decode(value); | ||||
|         case 'AudioCodec': | ||||
|           return AudioCodecTypeTransformer().decode(value); | ||||
|         case 'AvatarUpdate': | ||||
|   | ||||
							
								
								
									
										3
									
								
								mobile/openapi/lib/api_helper.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/lib/api_helper.dart
									
									
									
										generated
									
									
									
								
							| @@ -73,6 +73,9 @@ String parameterToString(dynamic value) { | ||||
|   if (value is AssetTypeEnum) { | ||||
|     return AssetTypeEnumTypeTransformer().encode(value).toString(); | ||||
|   } | ||||
|   if (value is AssetVisibility) { | ||||
|     return AssetVisibilityTypeTransformer().encode(value).toString(); | ||||
|   } | ||||
|   if (value is AudioCodec) { | ||||
|     return AudioCodecTypeTransformer().encode(value).toString(); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										40
									
								
								mobile/openapi/lib/model/asset_bulk_update_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										40
									
								
								mobile/openapi/lib/model/asset_bulk_update_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -16,11 +16,11 @@ class AssetBulkUpdateDto { | ||||
|     this.dateTimeOriginal, | ||||
|     this.duplicateId, | ||||
|     this.ids = const [], | ||||
|     this.isArchived, | ||||
|     this.isFavorite, | ||||
|     this.latitude, | ||||
|     this.longitude, | ||||
|     this.rating, | ||||
|     this.visibility, | ||||
|   }); | ||||
| 
 | ||||
|   /// | ||||
| @@ -35,14 +35,6 @@ class AssetBulkUpdateDto { | ||||
| 
 | ||||
|   List<String> ids; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isArchived; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
| @@ -77,16 +69,24 @@ class AssetBulkUpdateDto { | ||||
|   /// | ||||
|   num? rating; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   AssetVisibility? visibility; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto && | ||||
|     other.dateTimeOriginal == dateTimeOriginal && | ||||
|     other.duplicateId == duplicateId && | ||||
|     _deepEquality.equals(other.ids, ids) && | ||||
|     other.isArchived == isArchived && | ||||
|     other.isFavorite == isFavorite && | ||||
|     other.latitude == latitude && | ||||
|     other.longitude == longitude && | ||||
|     other.rating == rating; | ||||
|     other.rating == rating && | ||||
|     other.visibility == visibility; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
| @@ -94,14 +94,14 @@ class AssetBulkUpdateDto { | ||||
|     (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) + | ||||
|     (duplicateId == null ? 0 : duplicateId!.hashCode) + | ||||
|     (ids.hashCode) + | ||||
|     (isArchived == null ? 0 : isArchived!.hashCode) + | ||||
|     (isFavorite == null ? 0 : isFavorite!.hashCode) + | ||||
|     (latitude == null ? 0 : latitude!.hashCode) + | ||||
|     (longitude == null ? 0 : longitude!.hashCode) + | ||||
|     (rating == null ? 0 : rating!.hashCode); | ||||
|     (rating == null ? 0 : rating!.hashCode) + | ||||
|     (visibility == null ? 0 : visibility!.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]'; | ||||
|   String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, visibility=$visibility]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -116,11 +116,6 @@ class AssetBulkUpdateDto { | ||||
|     //  json[r'duplicateId'] = null; | ||||
|     } | ||||
|       json[r'ids'] = this.ids; | ||||
|     if (this.isArchived != null) { | ||||
|       json[r'isArchived'] = this.isArchived; | ||||
|     } else { | ||||
|     //  json[r'isArchived'] = null; | ||||
|     } | ||||
|     if (this.isFavorite != null) { | ||||
|       json[r'isFavorite'] = this.isFavorite; | ||||
|     } else { | ||||
| @@ -141,6 +136,11 @@ class AssetBulkUpdateDto { | ||||
|     } else { | ||||
|     //  json[r'rating'] = null; | ||||
|     } | ||||
|     if (this.visibility != null) { | ||||
|       json[r'visibility'] = this.visibility; | ||||
|     } else { | ||||
|     //  json[r'visibility'] = null; | ||||
|     } | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
| @@ -158,11 +158,11 @@ class AssetBulkUpdateDto { | ||||
|         ids: json[r'ids'] is Iterable | ||||
|             ? (json[r'ids'] as Iterable).cast<String>().toList(growable: false) | ||||
|             : const [], | ||||
|         isArchived: mapValueOfType<bool>(json, r'isArchived'), | ||||
|         isFavorite: mapValueOfType<bool>(json, r'isFavorite'), | ||||
|         latitude: num.parse('${json[r'latitude']}'), | ||||
|         longitude: num.parse('${json[r'longitude']}'), | ||||
|         rating: num.parse('${json[r'rating']}'), | ||||
|         visibility: AssetVisibility.fromJson(json[r'visibility']), | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   | ||||
							
								
								
									
										88
									
								
								mobile/openapi/lib/model/asset_visibility.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								mobile/openapi/lib/model/asset_visibility.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.18 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| 
 | ||||
| class AssetVisibility { | ||||
|   /// Instantiate a new enum with the provided [value]. | ||||
|   const AssetVisibility._(this.value); | ||||
| 
 | ||||
|   /// The underlying value of this enum member. | ||||
|   final String value; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => value; | ||||
| 
 | ||||
|   String toJson() => value; | ||||
| 
 | ||||
|   static const archive = AssetVisibility._(r'archive'); | ||||
|   static const timeline = AssetVisibility._(r'timeline'); | ||||
|   static const hidden = AssetVisibility._(r'hidden'); | ||||
| 
 | ||||
|   /// List of all possible values in this [enum][AssetVisibility]. | ||||
|   static const values = <AssetVisibility>[ | ||||
|     archive, | ||||
|     timeline, | ||||
|     hidden, | ||||
|   ]; | ||||
| 
 | ||||
|   static AssetVisibility? fromJson(dynamic value) => AssetVisibilityTypeTransformer().decode(value); | ||||
| 
 | ||||
|   static List<AssetVisibility> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AssetVisibility>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AssetVisibility.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// Transformation class that can [encode] an instance of [AssetVisibility] to String, | ||||
| /// and [decode] dynamic data back to [AssetVisibility]. | ||||
| class AssetVisibilityTypeTransformer { | ||||
|   factory AssetVisibilityTypeTransformer() => _instance ??= const AssetVisibilityTypeTransformer._(); | ||||
| 
 | ||||
|   const AssetVisibilityTypeTransformer._(); | ||||
| 
 | ||||
|   String encode(AssetVisibility data) => data.value; | ||||
| 
 | ||||
|   /// Decodes a [dynamic value][data] to a AssetVisibility. | ||||
|   /// | ||||
|   /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, | ||||
|   /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] | ||||
|   /// cannot be decoded successfully, then an [UnimplementedError] is thrown. | ||||
|   /// | ||||
|   /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, | ||||
|   /// and users are still using an old app with the old code. | ||||
|   AssetVisibility? decode(dynamic data, {bool allowNull = true}) { | ||||
|     if (data != null) { | ||||
|       switch (data) { | ||||
|         case r'archive': return AssetVisibility.archive; | ||||
|         case r'timeline': return AssetVisibility.timeline; | ||||
|         case r'hidden': return AssetVisibility.hidden; | ||||
|         default: | ||||
|           if (!allowNull) { | ||||
|             throw ArgumentError('Unknown enum value to decode: $data'); | ||||
|           } | ||||
|       } | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Singleton [AssetVisibilityTypeTransformer] instance. | ||||
|   static AssetVisibilityTypeTransformer? _instance; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										58
									
								
								mobile/openapi/lib/model/metadata_search_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										58
									
								
								mobile/openapi/lib/model/metadata_search_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -23,13 +23,11 @@ class MetadataSearchDto { | ||||
|     this.deviceId, | ||||
|     this.encodedVideoPath, | ||||
|     this.id, | ||||
|     this.isArchived, | ||||
|     this.isEncoded, | ||||
|     this.isFavorite, | ||||
|     this.isMotion, | ||||
|     this.isNotInAlbum, | ||||
|     this.isOffline, | ||||
|     this.isVisible, | ||||
|     this.lensModel, | ||||
|     this.libraryId, | ||||
|     this.make, | ||||
| @@ -52,7 +50,7 @@ class MetadataSearchDto { | ||||
|     this.type, | ||||
|     this.updatedAfter, | ||||
|     this.updatedBefore, | ||||
|     this.withArchived = false, | ||||
|     this.visibility, | ||||
|     this.withDeleted, | ||||
|     this.withExif, | ||||
|     this.withPeople, | ||||
| @@ -127,14 +125,6 @@ class MetadataSearchDto { | ||||
|   /// | ||||
|   String? id; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isArchived; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
| @@ -175,14 +165,6 @@ class MetadataSearchDto { | ||||
|   /// | ||||
|   bool? isOffline; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isVisible; | ||||
| 
 | ||||
|   String? lensModel; | ||||
| 
 | ||||
|   String? libraryId; | ||||
| @@ -322,7 +304,13 @@ class MetadataSearchDto { | ||||
|   /// | ||||
|   DateTime? updatedBefore; | ||||
| 
 | ||||
|   bool withArchived; | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   AssetVisibility? visibility; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
| @@ -368,13 +356,11 @@ class MetadataSearchDto { | ||||
|     other.deviceId == deviceId && | ||||
|     other.encodedVideoPath == encodedVideoPath && | ||||
|     other.id == id && | ||||
|     other.isArchived == isArchived && | ||||
|     other.isEncoded == isEncoded && | ||||
|     other.isFavorite == isFavorite && | ||||
|     other.isMotion == isMotion && | ||||
|     other.isNotInAlbum == isNotInAlbum && | ||||
|     other.isOffline == isOffline && | ||||
|     other.isVisible == isVisible && | ||||
|     other.lensModel == lensModel && | ||||
|     other.libraryId == libraryId && | ||||
|     other.make == make && | ||||
| @@ -397,7 +383,7 @@ class MetadataSearchDto { | ||||
|     other.type == type && | ||||
|     other.updatedAfter == updatedAfter && | ||||
|     other.updatedBefore == updatedBefore && | ||||
|     other.withArchived == withArchived && | ||||
|     other.visibility == visibility && | ||||
|     other.withDeleted == withDeleted && | ||||
|     other.withExif == withExif && | ||||
|     other.withPeople == withPeople && | ||||
| @@ -416,13 +402,11 @@ class MetadataSearchDto { | ||||
|     (deviceId == null ? 0 : deviceId!.hashCode) + | ||||
|     (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + | ||||
|     (id == null ? 0 : id!.hashCode) + | ||||
|     (isArchived == null ? 0 : isArchived!.hashCode) + | ||||
|     (isEncoded == null ? 0 : isEncoded!.hashCode) + | ||||
|     (isFavorite == null ? 0 : isFavorite!.hashCode) + | ||||
|     (isMotion == null ? 0 : isMotion!.hashCode) + | ||||
|     (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) + | ||||
|     (isOffline == null ? 0 : isOffline!.hashCode) + | ||||
|     (isVisible == null ? 0 : isVisible!.hashCode) + | ||||
|     (lensModel == null ? 0 : lensModel!.hashCode) + | ||||
|     (libraryId == null ? 0 : libraryId!.hashCode) + | ||||
|     (make == null ? 0 : make!.hashCode) + | ||||
| @@ -445,14 +429,14 @@ class MetadataSearchDto { | ||||
|     (type == null ? 0 : type!.hashCode) + | ||||
|     (updatedAfter == null ? 0 : updatedAfter!.hashCode) + | ||||
|     (updatedBefore == null ? 0 : updatedBefore!.hashCode) + | ||||
|     (withArchived.hashCode) + | ||||
|     (visibility == null ? 0 : visibility!.hashCode) + | ||||
|     (withDeleted == null ? 0 : withDeleted!.hashCode) + | ||||
|     (withExif == null ? 0 : withExif!.hashCode) + | ||||
|     (withPeople == null ? 0 : withPeople!.hashCode) + | ||||
|     (withStacked == null ? 0 : withStacked!.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; | ||||
|   String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -506,11 +490,6 @@ class MetadataSearchDto { | ||||
|     } else { | ||||
|     //  json[r'id'] = null; | ||||
|     } | ||||
|     if (this.isArchived != null) { | ||||
|       json[r'isArchived'] = this.isArchived; | ||||
|     } else { | ||||
|     //  json[r'isArchived'] = null; | ||||
|     } | ||||
|     if (this.isEncoded != null) { | ||||
|       json[r'isEncoded'] = this.isEncoded; | ||||
|     } else { | ||||
| @@ -536,11 +515,6 @@ class MetadataSearchDto { | ||||
|     } else { | ||||
|     //  json[r'isOffline'] = null; | ||||
|     } | ||||
|     if (this.isVisible != null) { | ||||
|       json[r'isVisible'] = this.isVisible; | ||||
|     } else { | ||||
|     //  json[r'isVisible'] = null; | ||||
|     } | ||||
|     if (this.lensModel != null) { | ||||
|       json[r'lensModel'] = this.lensModel; | ||||
|     } else { | ||||
| @@ -639,7 +613,11 @@ class MetadataSearchDto { | ||||
|     } else { | ||||
|     //  json[r'updatedBefore'] = null; | ||||
|     } | ||||
|       json[r'withArchived'] = this.withArchived; | ||||
|     if (this.visibility != null) { | ||||
|       json[r'visibility'] = this.visibility; | ||||
|     } else { | ||||
|     //  json[r'visibility'] = null; | ||||
|     } | ||||
|     if (this.withDeleted != null) { | ||||
|       json[r'withDeleted'] = this.withDeleted; | ||||
|     } else { | ||||
| @@ -682,13 +660,11 @@ class MetadataSearchDto { | ||||
|         deviceId: mapValueOfType<String>(json, r'deviceId'), | ||||
|         encodedVideoPath: mapValueOfType<String>(json, r'encodedVideoPath'), | ||||
|         id: mapValueOfType<String>(json, r'id'), | ||||
|         isArchived: mapValueOfType<bool>(json, r'isArchived'), | ||||
|         isEncoded: mapValueOfType<bool>(json, r'isEncoded'), | ||||
|         isFavorite: mapValueOfType<bool>(json, r'isFavorite'), | ||||
|         isMotion: mapValueOfType<bool>(json, r'isMotion'), | ||||
|         isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'), | ||||
|         isOffline: mapValueOfType<bool>(json, r'isOffline'), | ||||
|         isVisible: mapValueOfType<bool>(json, r'isVisible'), | ||||
|         lensModel: mapValueOfType<String>(json, r'lensModel'), | ||||
|         libraryId: mapValueOfType<String>(json, r'libraryId'), | ||||
|         make: mapValueOfType<String>(json, r'make'), | ||||
| @@ -715,7 +691,7 @@ class MetadataSearchDto { | ||||
|         type: AssetTypeEnum.fromJson(json[r'type']), | ||||
|         updatedAfter: mapDateTime(json, r'updatedAfter', r''), | ||||
|         updatedBefore: mapDateTime(json, r'updatedBefore', r''), | ||||
|         withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false, | ||||
|         visibility: AssetVisibility.fromJson(json[r'visibility']), | ||||
|         withDeleted: mapValueOfType<bool>(json, r'withDeleted'), | ||||
|         withExif: mapValueOfType<bool>(json, r'withExif'), | ||||
|         withPeople: mapValueOfType<bool>(json, r'withPeople'), | ||||
|   | ||||
							
								
								
									
										58
									
								
								mobile/openapi/lib/model/random_search_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										58
									
								
								mobile/openapi/lib/model/random_search_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -18,13 +18,11 @@ class RandomSearchDto { | ||||
|     this.createdAfter, | ||||
|     this.createdBefore, | ||||
|     this.deviceId, | ||||
|     this.isArchived, | ||||
|     this.isEncoded, | ||||
|     this.isFavorite, | ||||
|     this.isMotion, | ||||
|     this.isNotInAlbum, | ||||
|     this.isOffline, | ||||
|     this.isVisible, | ||||
|     this.lensModel, | ||||
|     this.libraryId, | ||||
|     this.make, | ||||
| @@ -41,7 +39,7 @@ class RandomSearchDto { | ||||
|     this.type, | ||||
|     this.updatedAfter, | ||||
|     this.updatedBefore, | ||||
|     this.withArchived = false, | ||||
|     this.visibility, | ||||
|     this.withDeleted, | ||||
|     this.withExif, | ||||
|     this.withPeople, | ||||
| @@ -76,14 +74,6 @@ class RandomSearchDto { | ||||
|   /// | ||||
|   String? deviceId; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isArchived; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
| @@ -124,14 +114,6 @@ class RandomSearchDto { | ||||
|   /// | ||||
|   bool? isOffline; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isVisible; | ||||
| 
 | ||||
|   String? lensModel; | ||||
| 
 | ||||
|   String? libraryId; | ||||
| @@ -228,7 +210,13 @@ class RandomSearchDto { | ||||
|   /// | ||||
|   DateTime? updatedBefore; | ||||
| 
 | ||||
|   bool withArchived; | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   AssetVisibility? visibility; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
| @@ -269,13 +257,11 @@ class RandomSearchDto { | ||||
|     other.createdAfter == createdAfter && | ||||
|     other.createdBefore == createdBefore && | ||||
|     other.deviceId == deviceId && | ||||
|     other.isArchived == isArchived && | ||||
|     other.isEncoded == isEncoded && | ||||
|     other.isFavorite == isFavorite && | ||||
|     other.isMotion == isMotion && | ||||
|     other.isNotInAlbum == isNotInAlbum && | ||||
|     other.isOffline == isOffline && | ||||
|     other.isVisible == isVisible && | ||||
|     other.lensModel == lensModel && | ||||
|     other.libraryId == libraryId && | ||||
|     other.make == make && | ||||
| @@ -292,7 +278,7 @@ class RandomSearchDto { | ||||
|     other.type == type && | ||||
|     other.updatedAfter == updatedAfter && | ||||
|     other.updatedBefore == updatedBefore && | ||||
|     other.withArchived == withArchived && | ||||
|     other.visibility == visibility && | ||||
|     other.withDeleted == withDeleted && | ||||
|     other.withExif == withExif && | ||||
|     other.withPeople == withPeople && | ||||
| @@ -306,13 +292,11 @@ class RandomSearchDto { | ||||
|     (createdAfter == null ? 0 : createdAfter!.hashCode) + | ||||
|     (createdBefore == null ? 0 : createdBefore!.hashCode) + | ||||
|     (deviceId == null ? 0 : deviceId!.hashCode) + | ||||
|     (isArchived == null ? 0 : isArchived!.hashCode) + | ||||
|     (isEncoded == null ? 0 : isEncoded!.hashCode) + | ||||
|     (isFavorite == null ? 0 : isFavorite!.hashCode) + | ||||
|     (isMotion == null ? 0 : isMotion!.hashCode) + | ||||
|     (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) + | ||||
|     (isOffline == null ? 0 : isOffline!.hashCode) + | ||||
|     (isVisible == null ? 0 : isVisible!.hashCode) + | ||||
|     (lensModel == null ? 0 : lensModel!.hashCode) + | ||||
|     (libraryId == null ? 0 : libraryId!.hashCode) + | ||||
|     (make == null ? 0 : make!.hashCode) + | ||||
| @@ -329,14 +313,14 @@ class RandomSearchDto { | ||||
|     (type == null ? 0 : type!.hashCode) + | ||||
|     (updatedAfter == null ? 0 : updatedAfter!.hashCode) + | ||||
|     (updatedBefore == null ? 0 : updatedBefore!.hashCode) + | ||||
|     (withArchived.hashCode) + | ||||
|     (visibility == null ? 0 : visibility!.hashCode) + | ||||
|     (withDeleted == null ? 0 : withDeleted!.hashCode) + | ||||
|     (withExif == null ? 0 : withExif!.hashCode) + | ||||
|     (withPeople == null ? 0 : withPeople!.hashCode) + | ||||
|     (withStacked == null ? 0 : withStacked!.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; | ||||
|   String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -365,11 +349,6 @@ class RandomSearchDto { | ||||
|     } else { | ||||
|     //  json[r'deviceId'] = null; | ||||
|     } | ||||
|     if (this.isArchived != null) { | ||||
|       json[r'isArchived'] = this.isArchived; | ||||
|     } else { | ||||
|     //  json[r'isArchived'] = null; | ||||
|     } | ||||
|     if (this.isEncoded != null) { | ||||
|       json[r'isEncoded'] = this.isEncoded; | ||||
|     } else { | ||||
| @@ -395,11 +374,6 @@ class RandomSearchDto { | ||||
|     } else { | ||||
|     //  json[r'isOffline'] = null; | ||||
|     } | ||||
|     if (this.isVisible != null) { | ||||
|       json[r'isVisible'] = this.isVisible; | ||||
|     } else { | ||||
|     //  json[r'isVisible'] = null; | ||||
|     } | ||||
|     if (this.lensModel != null) { | ||||
|       json[r'lensModel'] = this.lensModel; | ||||
|     } else { | ||||
| @@ -472,7 +446,11 @@ class RandomSearchDto { | ||||
|     } else { | ||||
|     //  json[r'updatedBefore'] = null; | ||||
|     } | ||||
|       json[r'withArchived'] = this.withArchived; | ||||
|     if (this.visibility != null) { | ||||
|       json[r'visibility'] = this.visibility; | ||||
|     } else { | ||||
|     //  json[r'visibility'] = null; | ||||
|     } | ||||
|     if (this.withDeleted != null) { | ||||
|       json[r'withDeleted'] = this.withDeleted; | ||||
|     } else { | ||||
| @@ -510,13 +488,11 @@ class RandomSearchDto { | ||||
|         createdAfter: mapDateTime(json, r'createdAfter', r''), | ||||
|         createdBefore: mapDateTime(json, r'createdBefore', r''), | ||||
|         deviceId: mapValueOfType<String>(json, r'deviceId'), | ||||
|         isArchived: mapValueOfType<bool>(json, r'isArchived'), | ||||
|         isEncoded: mapValueOfType<bool>(json, r'isEncoded'), | ||||
|         isFavorite: mapValueOfType<bool>(json, r'isFavorite'), | ||||
|         isMotion: mapValueOfType<bool>(json, r'isMotion'), | ||||
|         isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'), | ||||
|         isOffline: mapValueOfType<bool>(json, r'isOffline'), | ||||
|         isVisible: mapValueOfType<bool>(json, r'isVisible'), | ||||
|         lensModel: mapValueOfType<String>(json, r'lensModel'), | ||||
|         libraryId: mapValueOfType<String>(json, r'libraryId'), | ||||
|         make: mapValueOfType<String>(json, r'make'), | ||||
| @@ -537,7 +513,7 @@ class RandomSearchDto { | ||||
|         type: AssetTypeEnum.fromJson(json[r'type']), | ||||
|         updatedAfter: mapDateTime(json, r'updatedAfter', r''), | ||||
|         updatedBefore: mapDateTime(json, r'updatedBefore', r''), | ||||
|         withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false, | ||||
|         visibility: AssetVisibility.fromJson(json[r'visibility']), | ||||
|         withDeleted: mapValueOfType<bool>(json, r'withDeleted'), | ||||
|         withExif: mapValueOfType<bool>(json, r'withExif'), | ||||
|         withPeople: mapValueOfType<bool>(json, r'withPeople'), | ||||
|   | ||||
							
								
								
									
										58
									
								
								mobile/openapi/lib/model/smart_search_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										58
									
								
								mobile/openapi/lib/model/smart_search_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -18,13 +18,11 @@ class SmartSearchDto { | ||||
|     this.createdAfter, | ||||
|     this.createdBefore, | ||||
|     this.deviceId, | ||||
|     this.isArchived, | ||||
|     this.isEncoded, | ||||
|     this.isFavorite, | ||||
|     this.isMotion, | ||||
|     this.isNotInAlbum, | ||||
|     this.isOffline, | ||||
|     this.isVisible, | ||||
|     this.language, | ||||
|     this.lensModel, | ||||
|     this.libraryId, | ||||
| @@ -44,7 +42,7 @@ class SmartSearchDto { | ||||
|     this.type, | ||||
|     this.updatedAfter, | ||||
|     this.updatedBefore, | ||||
|     this.withArchived = false, | ||||
|     this.visibility, | ||||
|     this.withDeleted, | ||||
|     this.withExif, | ||||
|   }); | ||||
| @@ -77,14 +75,6 @@ class SmartSearchDto { | ||||
|   /// | ||||
|   String? deviceId; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isArchived; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
| @@ -125,14 +115,6 @@ class SmartSearchDto { | ||||
|   /// | ||||
|   bool? isOffline; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isVisible; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
| @@ -248,7 +230,13 @@ class SmartSearchDto { | ||||
|   /// | ||||
|   DateTime? updatedBefore; | ||||
| 
 | ||||
|   bool withArchived; | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   AssetVisibility? visibility; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
| @@ -273,13 +261,11 @@ class SmartSearchDto { | ||||
|     other.createdAfter == createdAfter && | ||||
|     other.createdBefore == createdBefore && | ||||
|     other.deviceId == deviceId && | ||||
|     other.isArchived == isArchived && | ||||
|     other.isEncoded == isEncoded && | ||||
|     other.isFavorite == isFavorite && | ||||
|     other.isMotion == isMotion && | ||||
|     other.isNotInAlbum == isNotInAlbum && | ||||
|     other.isOffline == isOffline && | ||||
|     other.isVisible == isVisible && | ||||
|     other.language == language && | ||||
|     other.lensModel == lensModel && | ||||
|     other.libraryId == libraryId && | ||||
| @@ -299,7 +285,7 @@ class SmartSearchDto { | ||||
|     other.type == type && | ||||
|     other.updatedAfter == updatedAfter && | ||||
|     other.updatedBefore == updatedBefore && | ||||
|     other.withArchived == withArchived && | ||||
|     other.visibility == visibility && | ||||
|     other.withDeleted == withDeleted && | ||||
|     other.withExif == withExif; | ||||
| 
 | ||||
| @@ -311,13 +297,11 @@ class SmartSearchDto { | ||||
|     (createdAfter == null ? 0 : createdAfter!.hashCode) + | ||||
|     (createdBefore == null ? 0 : createdBefore!.hashCode) + | ||||
|     (deviceId == null ? 0 : deviceId!.hashCode) + | ||||
|     (isArchived == null ? 0 : isArchived!.hashCode) + | ||||
|     (isEncoded == null ? 0 : isEncoded!.hashCode) + | ||||
|     (isFavorite == null ? 0 : isFavorite!.hashCode) + | ||||
|     (isMotion == null ? 0 : isMotion!.hashCode) + | ||||
|     (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) + | ||||
|     (isOffline == null ? 0 : isOffline!.hashCode) + | ||||
|     (isVisible == null ? 0 : isVisible!.hashCode) + | ||||
|     (language == null ? 0 : language!.hashCode) + | ||||
|     (lensModel == null ? 0 : lensModel!.hashCode) + | ||||
|     (libraryId == null ? 0 : libraryId!.hashCode) + | ||||
| @@ -337,12 +321,12 @@ class SmartSearchDto { | ||||
|     (type == null ? 0 : type!.hashCode) + | ||||
|     (updatedAfter == null ? 0 : updatedAfter!.hashCode) + | ||||
|     (updatedBefore == null ? 0 : updatedBefore!.hashCode) + | ||||
|     (withArchived.hashCode) + | ||||
|     (visibility == null ? 0 : visibility!.hashCode) + | ||||
|     (withDeleted == null ? 0 : withDeleted!.hashCode) + | ||||
|     (withExif == null ? 0 : withExif!.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]'; | ||||
|   String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -371,11 +355,6 @@ class SmartSearchDto { | ||||
|     } else { | ||||
|     //  json[r'deviceId'] = null; | ||||
|     } | ||||
|     if (this.isArchived != null) { | ||||
|       json[r'isArchived'] = this.isArchived; | ||||
|     } else { | ||||
|     //  json[r'isArchived'] = null; | ||||
|     } | ||||
|     if (this.isEncoded != null) { | ||||
|       json[r'isEncoded'] = this.isEncoded; | ||||
|     } else { | ||||
| @@ -401,11 +380,6 @@ class SmartSearchDto { | ||||
|     } else { | ||||
|     //  json[r'isOffline'] = null; | ||||
|     } | ||||
|     if (this.isVisible != null) { | ||||
|       json[r'isVisible'] = this.isVisible; | ||||
|     } else { | ||||
|     //  json[r'isVisible'] = null; | ||||
|     } | ||||
|     if (this.language != null) { | ||||
|       json[r'language'] = this.language; | ||||
|     } else { | ||||
| @@ -489,7 +463,11 @@ class SmartSearchDto { | ||||
|     } else { | ||||
|     //  json[r'updatedBefore'] = null; | ||||
|     } | ||||
|       json[r'withArchived'] = this.withArchived; | ||||
|     if (this.visibility != null) { | ||||
|       json[r'visibility'] = this.visibility; | ||||
|     } else { | ||||
|     //  json[r'visibility'] = null; | ||||
|     } | ||||
|     if (this.withDeleted != null) { | ||||
|       json[r'withDeleted'] = this.withDeleted; | ||||
|     } else { | ||||
| @@ -517,13 +495,11 @@ class SmartSearchDto { | ||||
|         createdAfter: mapDateTime(json, r'createdAfter', r''), | ||||
|         createdBefore: mapDateTime(json, r'createdBefore', r''), | ||||
|         deviceId: mapValueOfType<String>(json, r'deviceId'), | ||||
|         isArchived: mapValueOfType<bool>(json, r'isArchived'), | ||||
|         isEncoded: mapValueOfType<bool>(json, r'isEncoded'), | ||||
|         isFavorite: mapValueOfType<bool>(json, r'isFavorite'), | ||||
|         isMotion: mapValueOfType<bool>(json, r'isMotion'), | ||||
|         isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'), | ||||
|         isOffline: mapValueOfType<bool>(json, r'isOffline'), | ||||
|         isVisible: mapValueOfType<bool>(json, r'isVisible'), | ||||
|         language: mapValueOfType<String>(json, r'language'), | ||||
|         lensModel: mapValueOfType<String>(json, r'lensModel'), | ||||
|         libraryId: mapValueOfType<String>(json, r'libraryId'), | ||||
| @@ -547,7 +523,7 @@ class SmartSearchDto { | ||||
|         type: AssetTypeEnum.fromJson(json[r'type']), | ||||
|         updatedAfter: mapDateTime(json, r'updatedAfter', r''), | ||||
|         updatedBefore: mapDateTime(json, r'updatedBefore', r''), | ||||
|         withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false, | ||||
|         visibility: AssetVisibility.fromJson(json[r'visibility']), | ||||
|         withDeleted: mapValueOfType<bool>(json, r'withDeleted'), | ||||
|         withExif: mapValueOfType<bool>(json, r'withExif'), | ||||
|       ); | ||||
|   | ||||
							
								
								
									
										99
									
								
								mobile/openapi/lib/model/sync_asset_v1.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										99
									
								
								mobile/openapi/lib/model/sync_asset_v1.dart
									
									
									
										generated
									
									
									
								
							| @@ -19,11 +19,11 @@ class SyncAssetV1 { | ||||
|     required this.fileModifiedAt, | ||||
|     required this.id, | ||||
|     required this.isFavorite, | ||||
|     required this.isVisible, | ||||
|     required this.localDateTime, | ||||
|     required this.ownerId, | ||||
|     required this.thumbhash, | ||||
|     required this.type, | ||||
|     required this.visibility, | ||||
|   }); | ||||
| 
 | ||||
|   String checksum; | ||||
| @@ -38,8 +38,6 @@ class SyncAssetV1 { | ||||
| 
 | ||||
|   bool isFavorite; | ||||
| 
 | ||||
|   bool isVisible; | ||||
| 
 | ||||
|   DateTime? localDateTime; | ||||
| 
 | ||||
|   String ownerId; | ||||
| @@ -48,6 +46,8 @@ class SyncAssetV1 { | ||||
| 
 | ||||
|   SyncAssetV1TypeEnum type; | ||||
| 
 | ||||
|   SyncAssetV1VisibilityEnum visibility; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 && | ||||
|     other.checksum == checksum && | ||||
| @@ -56,11 +56,11 @@ class SyncAssetV1 { | ||||
|     other.fileModifiedAt == fileModifiedAt && | ||||
|     other.id == id && | ||||
|     other.isFavorite == isFavorite && | ||||
|     other.isVisible == isVisible && | ||||
|     other.localDateTime == localDateTime && | ||||
|     other.ownerId == ownerId && | ||||
|     other.thumbhash == thumbhash && | ||||
|     other.type == type; | ||||
|     other.type == type && | ||||
|     other.visibility == visibility; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
| @@ -71,14 +71,14 @@ class SyncAssetV1 { | ||||
|     (fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) + | ||||
|     (id.hashCode) + | ||||
|     (isFavorite.hashCode) + | ||||
|     (isVisible.hashCode) + | ||||
|     (localDateTime == null ? 0 : localDateTime!.hashCode) + | ||||
|     (ownerId.hashCode) + | ||||
|     (thumbhash == null ? 0 : thumbhash!.hashCode) + | ||||
|     (type.hashCode); | ||||
|     (type.hashCode) + | ||||
|     (visibility.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, isVisible=$isVisible, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type]'; | ||||
|   String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -100,7 +100,6 @@ class SyncAssetV1 { | ||||
|     } | ||||
|       json[r'id'] = this.id; | ||||
|       json[r'isFavorite'] = this.isFavorite; | ||||
|       json[r'isVisible'] = this.isVisible; | ||||
|     if (this.localDateTime != null) { | ||||
|       json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String(); | ||||
|     } else { | ||||
| @@ -113,6 +112,7 @@ class SyncAssetV1 { | ||||
|     //  json[r'thumbhash'] = null; | ||||
|     } | ||||
|       json[r'type'] = this.type; | ||||
|       json[r'visibility'] = this.visibility; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
| @@ -131,11 +131,11 @@ class SyncAssetV1 { | ||||
|         fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), | ||||
|         id: mapValueOfType<String>(json, r'id')!, | ||||
|         isFavorite: mapValueOfType<bool>(json, r'isFavorite')!, | ||||
|         isVisible: mapValueOfType<bool>(json, r'isVisible')!, | ||||
|         localDateTime: mapDateTime(json, r'localDateTime', r''), | ||||
|         ownerId: mapValueOfType<String>(json, r'ownerId')!, | ||||
|         thumbhash: mapValueOfType<String>(json, r'thumbhash'), | ||||
|         type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!, | ||||
|         visibility: SyncAssetV1VisibilityEnum.fromJson(json[r'visibility'])!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
| @@ -189,11 +189,11 @@ class SyncAssetV1 { | ||||
|     'fileModifiedAt', | ||||
|     'id', | ||||
|     'isFavorite', | ||||
|     'isVisible', | ||||
|     'localDateTime', | ||||
|     'ownerId', | ||||
|     'thumbhash', | ||||
|     'type', | ||||
|     'visibility', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| @@ -277,3 +277,80 @@ class SyncAssetV1TypeEnumTypeTransformer { | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| class SyncAssetV1VisibilityEnum { | ||||
|   /// Instantiate a new enum with the provided [value]. | ||||
|   const SyncAssetV1VisibilityEnum._(this.value); | ||||
| 
 | ||||
|   /// The underlying value of this enum member. | ||||
|   final String value; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => value; | ||||
| 
 | ||||
|   String toJson() => value; | ||||
| 
 | ||||
|   static const archive = SyncAssetV1VisibilityEnum._(r'archive'); | ||||
|   static const timeline = SyncAssetV1VisibilityEnum._(r'timeline'); | ||||
|   static const hidden = SyncAssetV1VisibilityEnum._(r'hidden'); | ||||
| 
 | ||||
|   /// List of all possible values in this [enum][SyncAssetV1VisibilityEnum]. | ||||
|   static const values = <SyncAssetV1VisibilityEnum>[ | ||||
|     archive, | ||||
|     timeline, | ||||
|     hidden, | ||||
|   ]; | ||||
| 
 | ||||
|   static SyncAssetV1VisibilityEnum? fromJson(dynamic value) => SyncAssetV1VisibilityEnumTypeTransformer().decode(value); | ||||
| 
 | ||||
|   static List<SyncAssetV1VisibilityEnum> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <SyncAssetV1VisibilityEnum>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = SyncAssetV1VisibilityEnum.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// Transformation class that can [encode] an instance of [SyncAssetV1VisibilityEnum] to String, | ||||
| /// and [decode] dynamic data back to [SyncAssetV1VisibilityEnum]. | ||||
| class SyncAssetV1VisibilityEnumTypeTransformer { | ||||
|   factory SyncAssetV1VisibilityEnumTypeTransformer() => _instance ??= const SyncAssetV1VisibilityEnumTypeTransformer._(); | ||||
| 
 | ||||
|   const SyncAssetV1VisibilityEnumTypeTransformer._(); | ||||
| 
 | ||||
|   String encode(SyncAssetV1VisibilityEnum data) => data.value; | ||||
| 
 | ||||
|   /// Decodes a [dynamic value][data] to a SyncAssetV1VisibilityEnum. | ||||
|   /// | ||||
|   /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, | ||||
|   /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] | ||||
|   /// cannot be decoded successfully, then an [UnimplementedError] is thrown. | ||||
|   /// | ||||
|   /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, | ||||
|   /// and users are still using an old app with the old code. | ||||
|   SyncAssetV1VisibilityEnum? decode(dynamic data, {bool allowNull = true}) { | ||||
|     if (data != null) { | ||||
|       switch (data) { | ||||
|         case r'archive': return SyncAssetV1VisibilityEnum.archive; | ||||
|         case r'timeline': return SyncAssetV1VisibilityEnum.timeline; | ||||
|         case r'hidden': return SyncAssetV1VisibilityEnum.hidden; | ||||
|         default: | ||||
|           if (!allowNull) { | ||||
|             throw ArgumentError('Unknown enum value to decode: $data'); | ||||
|           } | ||||
|       } | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Singleton [SyncAssetV1VisibilityEnumTypeTransformer] instance. | ||||
|   static SyncAssetV1VisibilityEnumTypeTransformer? _instance; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										40
									
								
								mobile/openapi/lib/model/update_asset_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										40
									
								
								mobile/openapi/lib/model/update_asset_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -15,12 +15,12 @@ class UpdateAssetDto { | ||||
|   UpdateAssetDto({ | ||||
|     this.dateTimeOriginal, | ||||
|     this.description, | ||||
|     this.isArchived, | ||||
|     this.isFavorite, | ||||
|     this.latitude, | ||||
|     this.livePhotoVideoId, | ||||
|     this.longitude, | ||||
|     this.rating, | ||||
|     this.visibility, | ||||
|   }); | ||||
| 
 | ||||
|   /// | ||||
| @@ -39,14 +39,6 @@ class UpdateAssetDto { | ||||
|   /// | ||||
|   String? description; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isArchived; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
| @@ -83,31 +75,39 @@ class UpdateAssetDto { | ||||
|   /// | ||||
|   num? rating; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   AssetVisibility? visibility; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto && | ||||
|     other.dateTimeOriginal == dateTimeOriginal && | ||||
|     other.description == description && | ||||
|     other.isArchived == isArchived && | ||||
|     other.isFavorite == isFavorite && | ||||
|     other.latitude == latitude && | ||||
|     other.livePhotoVideoId == livePhotoVideoId && | ||||
|     other.longitude == longitude && | ||||
|     other.rating == rating; | ||||
|     other.rating == rating && | ||||
|     other.visibility == visibility; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) + | ||||
|     (description == null ? 0 : description!.hashCode) + | ||||
|     (isArchived == null ? 0 : isArchived!.hashCode) + | ||||
|     (isFavorite == null ? 0 : isFavorite!.hashCode) + | ||||
|     (latitude == null ? 0 : latitude!.hashCode) + | ||||
|     (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + | ||||
|     (longitude == null ? 0 : longitude!.hashCode) + | ||||
|     (rating == null ? 0 : rating!.hashCode); | ||||
|     (rating == null ? 0 : rating!.hashCode) + | ||||
|     (visibility == null ? 0 : visibility!.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating]'; | ||||
|   String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating, visibility=$visibility]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -121,11 +121,6 @@ class UpdateAssetDto { | ||||
|     } else { | ||||
|     //  json[r'description'] = null; | ||||
|     } | ||||
|     if (this.isArchived != null) { | ||||
|       json[r'isArchived'] = this.isArchived; | ||||
|     } else { | ||||
|     //  json[r'isArchived'] = null; | ||||
|     } | ||||
|     if (this.isFavorite != null) { | ||||
|       json[r'isFavorite'] = this.isFavorite; | ||||
|     } else { | ||||
| @@ -151,6 +146,11 @@ class UpdateAssetDto { | ||||
|     } else { | ||||
|     //  json[r'rating'] = null; | ||||
|     } | ||||
|     if (this.visibility != null) { | ||||
|       json[r'visibility'] = this.visibility; | ||||
|     } else { | ||||
|     //  json[r'visibility'] = null; | ||||
|     } | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
| @@ -165,12 +165,12 @@ class UpdateAssetDto { | ||||
|       return UpdateAssetDto( | ||||
|         dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'), | ||||
|         description: mapValueOfType<String>(json, r'description'), | ||||
|         isArchived: mapValueOfType<bool>(json, r'isArchived'), | ||||
|         isFavorite: mapValueOfType<bool>(json, r'isFavorite'), | ||||
|         latitude: num.parse('${json[r'latitude']}'), | ||||
|         livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'), | ||||
|         longitude: num.parse('${json[r'longitude']}'), | ||||
|         rating: num.parse('${json[r'rating']}'), | ||||
|         visibility: AssetVisibility.fromJson(json[r'visibility']), | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   | ||||
| @@ -1781,14 +1781,6 @@ | ||||
|       "get": { | ||||
|         "operationId": "getAssetStatistics", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "isArchived", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "isFavorite", | ||||
|             "required": false, | ||||
| @@ -1804,6 +1796,14 @@ | ||||
|             "schema": { | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "visibility", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "$ref": "#/components/schemas/AssetVisibility" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
| @@ -6909,14 +6909,6 @@ | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "isArchived", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "isFavorite", | ||||
|             "required": false, | ||||
| @@ -6992,6 +6984,14 @@ | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "visibility", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "$ref": "#/components/schemas/AssetVisibility" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "withPartners", | ||||
|             "required": false, | ||||
| @@ -7053,14 +7053,6 @@ | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "isArchived", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "isFavorite", | ||||
|             "required": false, | ||||
| @@ -7128,6 +7120,14 @@ | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "visibility", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "$ref": "#/components/schemas/AssetVisibility" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "withPartners", | ||||
|             "required": false, | ||||
| @@ -8273,9 +8273,6 @@ | ||||
|             }, | ||||
|             "type": "array" | ||||
|           }, | ||||
|           "isArchived": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isFavorite": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
| @@ -8289,6 +8286,13 @@ | ||||
|             "maximum": 5, | ||||
|             "minimum": -1, | ||||
|             "type": "number" | ||||
|           }, | ||||
|           "visibility": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetVisibility" | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
| @@ -8713,15 +8717,9 @@ | ||||
|             "format": "date-time", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "isArchived": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isFavorite": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isVisible": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "livePhotoVideoId": { | ||||
|             "format": "uuid", | ||||
|             "type": "string" | ||||
| @@ -8729,6 +8727,13 @@ | ||||
|           "sidecarData": { | ||||
|             "format": "binary", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "visibility": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetVisibility" | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
| @@ -9009,6 +9014,14 @@ | ||||
|         ], | ||||
|         "type": "string" | ||||
|       }, | ||||
|       "AssetVisibility": { | ||||
|         "enum": [ | ||||
|           "archive", | ||||
|           "timeline", | ||||
|           "hidden" | ||||
|         ], | ||||
|         "type": "string" | ||||
|       }, | ||||
|       "AudioCodec": { | ||||
|         "enum": [ | ||||
|           "mp3", | ||||
| @@ -10204,9 +10217,6 @@ | ||||
|             "format": "uuid", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "isArchived": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isEncoded": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
| @@ -10222,9 +10232,6 @@ | ||||
|           "isOffline": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isVisible": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "lensModel": { | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
| @@ -10324,9 +10331,12 @@ | ||||
|             "format": "date-time", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "withArchived": { | ||||
|             "default": false, | ||||
|             "type": "boolean" | ||||
|           "visibility": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetVisibility" | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|           "withDeleted": { | ||||
|             "type": "boolean" | ||||
| @@ -11041,9 +11051,6 @@ | ||||
|           "deviceId": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "isArchived": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isEncoded": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
| @@ -11059,9 +11066,6 @@ | ||||
|           "isOffline": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isVisible": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "lensModel": { | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
| @@ -11137,9 +11141,12 @@ | ||||
|             "format": "date-time", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "withArchived": { | ||||
|             "default": false, | ||||
|             "type": "boolean" | ||||
|           "visibility": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetVisibility" | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|           "withDeleted": { | ||||
|             "type": "boolean" | ||||
| @@ -11989,9 +11996,6 @@ | ||||
|           "deviceId": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "isArchived": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isEncoded": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
| @@ -12007,9 +12011,6 @@ | ||||
|           "isOffline": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isVisible": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "language": { | ||||
|             "type": "string" | ||||
|           }, | ||||
| @@ -12095,9 +12096,12 @@ | ||||
|             "format": "date-time", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "withArchived": { | ||||
|             "default": false, | ||||
|             "type": "boolean" | ||||
|           "visibility": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetVisibility" | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|           "withDeleted": { | ||||
|             "type": "boolean" | ||||
| @@ -12381,9 +12385,6 @@ | ||||
|           "isFavorite": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isVisible": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "localDateTime": { | ||||
|             "format": "date-time", | ||||
|             "nullable": true, | ||||
| @@ -12404,6 +12405,14 @@ | ||||
|               "OTHER" | ||||
|             ], | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "visibility": { | ||||
|             "enum": [ | ||||
|               "archive", | ||||
|               "timeline", | ||||
|               "hidden" | ||||
|             ], | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
| @@ -12413,11 +12422,11 @@ | ||||
|           "fileModifiedAt", | ||||
|           "id", | ||||
|           "isFavorite", | ||||
|           "isVisible", | ||||
|           "localDateTime", | ||||
|           "ownerId", | ||||
|           "thumbhash", | ||||
|           "type" | ||||
|           "type", | ||||
|           "visibility" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
| @@ -13671,9 +13680,6 @@ | ||||
|           "description": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "isArchived": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isFavorite": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
| @@ -13692,6 +13698,13 @@ | ||||
|             "maximum": 5, | ||||
|             "minimum": -1, | ||||
|             "type": "number" | ||||
|           }, | ||||
|           "visibility": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetVisibility" | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         }, | ||||
|         "type": "object" | ||||
|   | ||||
| @@ -413,11 +413,10 @@ export type AssetMediaCreateDto = { | ||||
|     duration?: string; | ||||
|     fileCreatedAt: string; | ||||
|     fileModifiedAt: string; | ||||
|     isArchived?: boolean; | ||||
|     isFavorite?: boolean; | ||||
|     isVisible?: boolean; | ||||
|     livePhotoVideoId?: string; | ||||
|     sidecarData?: Blob; | ||||
|     visibility?: AssetVisibility; | ||||
| }; | ||||
| export type AssetMediaResponseDto = { | ||||
|     id: string; | ||||
| @@ -427,11 +426,11 @@ export type AssetBulkUpdateDto = { | ||||
|     dateTimeOriginal?: string; | ||||
|     duplicateId?: string | null; | ||||
|     ids: string[]; | ||||
|     isArchived?: boolean; | ||||
|     isFavorite?: boolean; | ||||
|     latitude?: number; | ||||
|     longitude?: number; | ||||
|     rating?: number; | ||||
|     visibility?: AssetVisibility; | ||||
| }; | ||||
| export type AssetBulkUploadCheckItem = { | ||||
|     /** base64 or hex encoded sha1 hash */ | ||||
| @@ -470,12 +469,12 @@ export type AssetStatsResponseDto = { | ||||
| export type UpdateAssetDto = { | ||||
|     dateTimeOriginal?: string; | ||||
|     description?: string; | ||||
|     isArchived?: boolean; | ||||
|     isFavorite?: boolean; | ||||
|     latitude?: number; | ||||
|     livePhotoVideoId?: string | null; | ||||
|     longitude?: number; | ||||
|     rating?: number; | ||||
|     visibility?: AssetVisibility; | ||||
| }; | ||||
| export type AssetMediaReplaceDto = { | ||||
|     assetData: Blob; | ||||
| @@ -815,13 +814,11 @@ export type MetadataSearchDto = { | ||||
|     deviceId?: string; | ||||
|     encodedVideoPath?: string; | ||||
|     id?: string; | ||||
|     isArchived?: boolean; | ||||
|     isEncoded?: boolean; | ||||
|     isFavorite?: boolean; | ||||
|     isMotion?: boolean; | ||||
|     isNotInAlbum?: boolean; | ||||
|     isOffline?: boolean; | ||||
|     isVisible?: boolean; | ||||
|     lensModel?: string | null; | ||||
|     libraryId?: string | null; | ||||
|     make?: string; | ||||
| @@ -844,7 +841,7 @@ export type MetadataSearchDto = { | ||||
|     "type"?: AssetTypeEnum; | ||||
|     updatedAfter?: string; | ||||
|     updatedBefore?: string; | ||||
|     withArchived?: boolean; | ||||
|     visibility?: AssetVisibility; | ||||
|     withDeleted?: boolean; | ||||
|     withExif?: boolean; | ||||
|     withPeople?: boolean; | ||||
| @@ -888,13 +885,11 @@ export type RandomSearchDto = { | ||||
|     createdAfter?: string; | ||||
|     createdBefore?: string; | ||||
|     deviceId?: string; | ||||
|     isArchived?: boolean; | ||||
|     isEncoded?: boolean; | ||||
|     isFavorite?: boolean; | ||||
|     isMotion?: boolean; | ||||
|     isNotInAlbum?: boolean; | ||||
|     isOffline?: boolean; | ||||
|     isVisible?: boolean; | ||||
|     lensModel?: string | null; | ||||
|     libraryId?: string | null; | ||||
|     make?: string; | ||||
| @@ -911,7 +906,7 @@ export type RandomSearchDto = { | ||||
|     "type"?: AssetTypeEnum; | ||||
|     updatedAfter?: string; | ||||
|     updatedBefore?: string; | ||||
|     withArchived?: boolean; | ||||
|     visibility?: AssetVisibility; | ||||
|     withDeleted?: boolean; | ||||
|     withExif?: boolean; | ||||
|     withPeople?: boolean; | ||||
| @@ -923,13 +918,11 @@ export type SmartSearchDto = { | ||||
|     createdAfter?: string; | ||||
|     createdBefore?: string; | ||||
|     deviceId?: string; | ||||
|     isArchived?: boolean; | ||||
|     isEncoded?: boolean; | ||||
|     isFavorite?: boolean; | ||||
|     isMotion?: boolean; | ||||
|     isNotInAlbum?: boolean; | ||||
|     isOffline?: boolean; | ||||
|     isVisible?: boolean; | ||||
|     language?: string; | ||||
|     lensModel?: string | null; | ||||
|     libraryId?: string | null; | ||||
| @@ -949,7 +942,7 @@ export type SmartSearchDto = { | ||||
|     "type"?: AssetTypeEnum; | ||||
|     updatedAfter?: string; | ||||
|     updatedBefore?: string; | ||||
|     withArchived?: boolean; | ||||
|     visibility?: AssetVisibility; | ||||
|     withDeleted?: boolean; | ||||
|     withExif?: boolean; | ||||
| }; | ||||
| @@ -1877,18 +1870,18 @@ export function getRandom({ count }: { | ||||
|         ...opts | ||||
|     })); | ||||
| } | ||||
| export function getAssetStatistics({ isArchived, isFavorite, isTrashed }: { | ||||
|     isArchived?: boolean; | ||||
| export function getAssetStatistics({ isFavorite, isTrashed, visibility }: { | ||||
|     isFavorite?: boolean; | ||||
|     isTrashed?: boolean; | ||||
|     visibility?: AssetVisibility; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
|     return oazapfts.ok(oazapfts.fetchJson<{ | ||||
|         status: 200; | ||||
|         data: AssetStatsResponseDto; | ||||
|     }>(`/assets/statistics${QS.query(QS.explode({ | ||||
|         isArchived, | ||||
|         isFavorite, | ||||
|         isTrashed | ||||
|         isTrashed, | ||||
|         visibility | ||||
|     }))}`, { | ||||
|         ...opts | ||||
|     })); | ||||
| @@ -3242,9 +3235,8 @@ export function tagAssets({ id, bulkIdsDto }: { | ||||
|         body: bulkIdsDto | ||||
|     }))); | ||||
| } | ||||
| export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, withPartners, withStacked }: { | ||||
| export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, visibility, withPartners, withStacked }: { | ||||
|     albumId?: string; | ||||
|     isArchived?: boolean; | ||||
|     isFavorite?: boolean; | ||||
|     isTrashed?: boolean; | ||||
|     key?: string; | ||||
| @@ -3254,6 +3246,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, | ||||
|     tagId?: string; | ||||
|     timeBucket: string; | ||||
|     userId?: string; | ||||
|     visibility?: AssetVisibility; | ||||
|     withPartners?: boolean; | ||||
|     withStacked?: boolean; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
| @@ -3262,7 +3255,6 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, | ||||
|         data: AssetResponseDto[]; | ||||
|     }>(`/timeline/bucket${QS.query(QS.explode({ | ||||
|         albumId, | ||||
|         isArchived, | ||||
|         isFavorite, | ||||
|         isTrashed, | ||||
|         key, | ||||
| @@ -3272,15 +3264,15 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, | ||||
|         tagId, | ||||
|         timeBucket, | ||||
|         userId, | ||||
|         visibility, | ||||
|         withPartners, | ||||
|         withStacked | ||||
|     }))}`, { | ||||
|         ...opts | ||||
|     })); | ||||
| } | ||||
| export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, userId, withPartners, withStacked }: { | ||||
| export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, userId, visibility, withPartners, withStacked }: { | ||||
|     albumId?: string; | ||||
|     isArchived?: boolean; | ||||
|     isFavorite?: boolean; | ||||
|     isTrashed?: boolean; | ||||
|     key?: string; | ||||
| @@ -3289,6 +3281,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key | ||||
|     size: TimeBucketSize; | ||||
|     tagId?: string; | ||||
|     userId?: string; | ||||
|     visibility?: AssetVisibility; | ||||
|     withPartners?: boolean; | ||||
|     withStacked?: boolean; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
| @@ -3297,7 +3290,6 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key | ||||
|         data: TimeBucketResponseDto[]; | ||||
|     }>(`/timeline/buckets${QS.query(QS.explode({ | ||||
|         albumId, | ||||
|         isArchived, | ||||
|         isFavorite, | ||||
|         isTrashed, | ||||
|         key, | ||||
| @@ -3306,6 +3298,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key | ||||
|         size, | ||||
|         tagId, | ||||
|         userId, | ||||
|         visibility, | ||||
|         withPartners, | ||||
|         withStacked | ||||
|     }))}`, { | ||||
| @@ -3620,6 +3613,11 @@ export enum Permission { | ||||
|     AdminUserUpdate = "admin.user.update", | ||||
|     AdminUserDelete = "admin.user.delete" | ||||
| } | ||||
| export enum AssetVisibility { | ||||
|     Archive = "archive", | ||||
|     Timeline = "timeline", | ||||
|     Hidden = "hidden" | ||||
| } | ||||
| export enum AssetMediaStatus { | ||||
|     Created = "created", | ||||
|     Replaced = "replaced", | ||||
|   | ||||
| @@ -100,38 +100,29 @@ describe(AssetMediaController.name, () => { | ||||
|       expect(body).toEqual(factory.responses.badRequest()); | ||||
|     }); | ||||
|  | ||||
|     it('should throw if `isVisible` is not a boolean', async () => { | ||||
|     it('should throw if `visibility` is not an enum', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()) | ||||
|         .post('/assets') | ||||
|         .attach('assetData', assetData, filename) | ||||
|         .field({ ...makeUploadDto(), isVisible: 'not-a-boolean' }); | ||||
|         .field({ ...makeUploadDto(), visibility: 'not-a-boolean' }); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(factory.responses.badRequest()); | ||||
|     }); | ||||
|  | ||||
|     it('should throw if `isArchived` is not a boolean', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()) | ||||
|         .post('/assets') | ||||
|         .attach('assetData', assetData, filename) | ||||
|         .field({ ...makeUploadDto(), isArchived: 'not-a-boolean' }); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(factory.responses.badRequest()); | ||||
|     // TODO figure out how to deal with `sendFile` | ||||
|     describe.skip('GET /assets/:id/original', () => { | ||||
|       it('should be an authenticated route', async () => { | ||||
|         await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`); | ||||
|         expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // TODO figure out how to deal with `sendFile` | ||||
|   describe.skip('GET /assets/:id/original', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // TODO figure out how to deal with `sendFile` | ||||
|   describe.skip('GET /assets/:id/thumbnail', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     // TODO figure out how to deal with `sendFile` | ||||
|     describe.skip('GET /assets/:id/thumbnail', () => { | ||||
|       it('should be an authenticated route', async () => { | ||||
|         await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`); | ||||
|         expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -60,12 +60,14 @@ describe(SearchController.name, () => { | ||||
|       expect(body).toEqual(errorDto.badRequest(['size must not be less than 1', 'size must be an integer number'])); | ||||
|     }); | ||||
|  | ||||
|     it('should reject an isArchived as not a boolean', async () => { | ||||
|     it('should reject an visibility as not an enum', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()) | ||||
|         .post('/search/metadata') | ||||
|         .send({ isArchived: 'immich' }); | ||||
|         .send({ visibility: 'immich' }); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest(['isArchived must be a boolean value'])); | ||||
|       expect(body).toEqual( | ||||
|         errorDto.badRequest(['visibility must be one of the following values: archive, timeline, hidden']), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should reject an isFavorite as not a boolean', async () => { | ||||
| @@ -98,104 +100,98 @@ describe(SearchController.name, () => { | ||||
|       expect(body).toEqual(errorDto.badRequest(['isMotion must be a boolean value'])); | ||||
|     }); | ||||
|  | ||||
|     it('should reject an isVisible as not a boolean', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()) | ||||
|         .post('/search/metadata') | ||||
|         .send({ isVisible: 'immich' }); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest(['isVisible must be a boolean value'])); | ||||
|     }); | ||||
|   }); | ||||
|     describe('POST /search/random', () => { | ||||
|       it('should be an authenticated route', async () => { | ||||
|         await request(ctx.getHttpServer()).post('/search/random'); | ||||
|         expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       }); | ||||
|  | ||||
|   describe('POST /search/random', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).post('/search/random'); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       it('should reject if withStacked is not a boolean', async () => { | ||||
|         const { status, body } = await request(ctx.getHttpServer()) | ||||
|           .post('/search/random') | ||||
|           .send({ withStacked: 'immich' }); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value'])); | ||||
|       }); | ||||
|  | ||||
|       it('should reject if withPeople is not a boolean', async () => { | ||||
|         const { status, body } = await request(ctx.getHttpServer()) | ||||
|           .post('/search/random') | ||||
|           .send({ withPeople: 'immich' }); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value'])); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should reject if withStacked is not a boolean', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()) | ||||
|         .post('/search/random') | ||||
|         .send({ withStacked: 'immich' }); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value'])); | ||||
|     describe('POST /search/smart', () => { | ||||
|       it('should be an authenticated route', async () => { | ||||
|         await request(ctx.getHttpServer()).post('/search/smart'); | ||||
|         expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       }); | ||||
|  | ||||
|       it('should require a query', async () => { | ||||
|         const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({}); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string'])); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should reject if withPeople is not a boolean', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).post('/search/random').send({ withPeople: 'immich' }); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value'])); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /search/smart', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).post('/search/smart'); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     describe('GET /search/explore', () => { | ||||
|       it('should be an authenticated route', async () => { | ||||
|         await request(ctx.getHttpServer()).get('/search/explore'); | ||||
|         expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should require a query', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({}); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string'])); | ||||
|     }); | ||||
|   }); | ||||
|     describe('POST /search/person', () => { | ||||
|       it('should be an authenticated route', async () => { | ||||
|         await request(ctx.getHttpServer()).get('/search/person'); | ||||
|         expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       }); | ||||
|  | ||||
|   describe('GET /search/explore', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).get('/search/explore'); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('POST /search/person', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).get('/search/person'); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       it('should require a name', async () => { | ||||
|         const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({}); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should require a name', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({}); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); | ||||
|     }); | ||||
|   }); | ||||
|     describe('GET /search/places', () => { | ||||
|       it('should be an authenticated route', async () => { | ||||
|         await request(ctx.getHttpServer()).get('/search/places'); | ||||
|         expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       }); | ||||
|  | ||||
|   describe('GET /search/places', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).get('/search/places'); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       it('should require a name', async () => { | ||||
|         const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({}); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should require a name', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({}); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /search/cities', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).get('/search/cities'); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /search/suggestions', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).get('/search/suggestions'); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     describe('GET /search/cities', () => { | ||||
|       it('should be an authenticated route', async () => { | ||||
|         await request(ctx.getHttpServer()).get('/search/cities'); | ||||
|         expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should require a type', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({}); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual( | ||||
|         errorDto.badRequest([ | ||||
|           'type should not be empty', | ||||
|           expect.stringContaining('type must be one of the following values:'), | ||||
|         ]), | ||||
|       ); | ||||
|     describe('GET /search/suggestions', () => { | ||||
|       it('should be an authenticated route', async () => { | ||||
|         await request(ctx.getHttpServer()).get('/search/suggestions'); | ||||
|         expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|       }); | ||||
|  | ||||
|       it('should require a type', async () => { | ||||
|         const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({}); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual( | ||||
|           errorDto.badRequest([ | ||||
|             'type should not be empty', | ||||
|             expect.stringContaining('type must be one of the following values:'), | ||||
|           ]), | ||||
|         ); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { | ||||
|   AlbumUserRole, | ||||
|   AssetFileType, | ||||
|   AssetType, | ||||
|   AssetVisibility, | ||||
|   MemoryType, | ||||
|   Permission, | ||||
|   SharedLinkType, | ||||
| @@ -108,7 +109,7 @@ export type Asset = { | ||||
|   fileCreatedAt: Date; | ||||
|   fileModifiedAt: Date; | ||||
|   isExternal: boolean; | ||||
|   isVisible: boolean; | ||||
|   visibility: AssetVisibility; | ||||
|   libraryId: string | null; | ||||
|   livePhotoVideoId: string | null; | ||||
|   localDateTime: Date; | ||||
| @@ -285,7 +286,7 @@ export const columns = { | ||||
|     'assets.fileCreatedAt', | ||||
|     'assets.fileModifiedAt', | ||||
|     'assets.isExternal', | ||||
|     'assets.isVisible', | ||||
|     'assets.visibility', | ||||
|     'assets.libraryId', | ||||
|     'assets.livePhotoVideoId', | ||||
|     'assets.localDateTime', | ||||
| @@ -345,7 +346,7 @@ export const columns = { | ||||
|     'type', | ||||
|     'deletedAt', | ||||
|     'isFavorite', | ||||
|     'isVisible', | ||||
|     'visibility', | ||||
|     'updateId', | ||||
|   ], | ||||
|   stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'], | ||||
|   | ||||
							
								
								
									
										4
									
								
								server/src/db.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								server/src/db.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -10,6 +10,7 @@ import { | ||||
|   AssetOrder, | ||||
|   AssetStatus, | ||||
|   AssetType, | ||||
|   AssetVisibility, | ||||
|   MemoryType, | ||||
|   NotificationLevel, | ||||
|   NotificationType, | ||||
| @@ -148,11 +149,10 @@ export interface Assets { | ||||
|   fileCreatedAt: Timestamp; | ||||
|   fileModifiedAt: Timestamp; | ||||
|   id: Generated<string>; | ||||
|   isArchived: Generated<boolean>; | ||||
|   isExternal: Generated<boolean>; | ||||
|   isFavorite: Generated<boolean>; | ||||
|   isOffline: Generated<boolean>; | ||||
|   isVisible: Generated<boolean>; | ||||
|   visibility: Generated<AssetVisibility>; | ||||
|   libraryId: string | null; | ||||
|   livePhotoVideoId: string | null; | ||||
|   localDateTime: Timestamp; | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Type } from 'class-transformer'; | ||||
| import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; | ||||
| import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; | ||||
| import { AssetVisibility } from 'src/enum'; | ||||
| import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; | ||||
|  | ||||
| export enum AssetMediaSize { | ||||
|   /** | ||||
| @@ -55,11 +56,8 @@ export class AssetMediaCreateDto extends AssetMediaBase { | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isFavorite?: boolean; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isArchived?: boolean; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isVisible?: boolean; | ||||
|   @ValidateAssetVisibility({ optional: true }) | ||||
|   visibility?: AssetVisibility; | ||||
|  | ||||
|   @ValidateUUID({ optional: true }) | ||||
|   livePhotoVideoId?: string; | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import { | ||||
| } from 'src/dtos/person.dto'; | ||||
| import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; | ||||
| import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; | ||||
| import { AssetStatus, AssetType } from 'src/enum'; | ||||
| import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { mimeTypes } from 'src/utils/mime-types'; | ||||
|  | ||||
| export class SanitizedAssetResponseDto { | ||||
| @@ -74,11 +74,10 @@ export type MapAsset = { | ||||
|   fileCreatedAt: Date; | ||||
|   fileModifiedAt: Date; | ||||
|   files?: AssetFile[]; | ||||
|   isArchived: boolean; | ||||
|   isExternal: boolean; | ||||
|   isFavorite: boolean; | ||||
|   isOffline: boolean; | ||||
|   isVisible: boolean; | ||||
|   visibility: AssetVisibility; | ||||
|   libraryId: string | null; | ||||
|   livePhotoVideoId: string | null; | ||||
|   localDateTime: Date; | ||||
| @@ -183,7 +182,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset | ||||
|     localDateTime: entity.localDateTime, | ||||
|     updatedAt: entity.updatedAt, | ||||
|     isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false, | ||||
|     isArchived: entity.isArchived, | ||||
|     isArchived: entity.visibility === AssetVisibility.ARCHIVE, | ||||
|     isTrashed: !!entity.deletedAt, | ||||
|     duration: entity.duration ?? '0:00:00.00000', | ||||
|     exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, | ||||
|   | ||||
| @@ -14,9 +14,9 @@ import { | ||||
|   ValidateIf, | ||||
| } from 'class-validator'; | ||||
| import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; | ||||
| import { AssetType } from 'src/enum'; | ||||
| import { AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { AssetStats } from 'src/repositories/asset.repository'; | ||||
| import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; | ||||
| import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation'; | ||||
|  | ||||
| export class DeviceIdDto { | ||||
|   @IsNotEmpty() | ||||
| @@ -32,8 +32,8 @@ export class UpdateAssetBase { | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isFavorite?: boolean; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isArchived?: boolean; | ||||
|   @ValidateAssetVisibility({ optional: true }) | ||||
|   visibility?: AssetVisibility; | ||||
|  | ||||
|   @Optional() | ||||
|   @IsDateString() | ||||
| @@ -105,8 +105,8 @@ export class AssetJobsDto extends AssetIdsDto { | ||||
| } | ||||
|  | ||||
| export class AssetStatsDto { | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isArchived?: boolean; | ||||
|   @ValidateAssetVisibility({ optional: true }) | ||||
|   visibility?: AssetVisibility; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isFavorite?: boolean; | ||||
|   | ||||
| @@ -5,8 +5,8 @@ import { Place } from 'src/database'; | ||||
| import { PropertyLifecycle } from 'src/decorators'; | ||||
| import { AlbumResponseDto } from 'src/dtos/album.dto'; | ||||
| import { AssetResponseDto } from 'src/dtos/asset-response.dto'; | ||||
| import { AssetOrder, AssetType } from 'src/enum'; | ||||
| import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; | ||||
| import { AssetOrder, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; | ||||
|  | ||||
| class BaseSearchDto { | ||||
|   @ValidateUUID({ optional: true, nullable: true }) | ||||
| @@ -22,13 +22,6 @@ class BaseSearchDto { | ||||
|   @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) | ||||
|   type?: AssetType; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isArchived?: boolean; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   @ApiProperty({ default: false }) | ||||
|   withArchived?: boolean; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isEncoded?: boolean; | ||||
|  | ||||
| @@ -41,8 +34,8 @@ class BaseSearchDto { | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isOffline?: boolean; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isVisible?: boolean; | ||||
|   @ValidateAssetVisibility({ optional: true }) | ||||
|   visibility?: AssetVisibility; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   withDeleted?: boolean; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator'; | ||||
| import { AssetResponseDto } from 'src/dtos/asset-response.dto'; | ||||
| import { AssetType, SyncEntityType, SyncRequestType } from 'src/enum'; | ||||
| import { AssetType, AssetVisibility, SyncEntityType, SyncRequestType } from 'src/enum'; | ||||
| import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; | ||||
|  | ||||
| export class AssetFullSyncDto { | ||||
| @@ -67,7 +67,7 @@ export class SyncAssetV1 { | ||||
|   type!: AssetType; | ||||
|   deletedAt!: Date | null; | ||||
|   isFavorite!: boolean; | ||||
|   isVisible!: boolean; | ||||
|   visibility!: AssetVisibility; | ||||
| } | ||||
|  | ||||
| export class SyncAssetDeleteV1 { | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; | ||||
| import { AssetOrder } from 'src/enum'; | ||||
| import { AssetOrder, AssetVisibility } from 'src/enum'; | ||||
| import { TimeBucketSize } from 'src/repositories/asset.repository'; | ||||
| import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; | ||||
| import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation'; | ||||
|  | ||||
| export class TimeBucketDto { | ||||
|   @IsNotEmpty() | ||||
| @@ -22,9 +22,6 @@ export class TimeBucketDto { | ||||
|   @ValidateUUID({ optional: true }) | ||||
|   tagId?: string; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isArchived?: boolean; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isFavorite?: boolean; | ||||
|  | ||||
| @@ -41,6 +38,9 @@ export class TimeBucketDto { | ||||
|   @Optional() | ||||
|   @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) | ||||
|   order?: AssetOrder; | ||||
|  | ||||
|   @ValidateAssetVisibility({ optional: true }) | ||||
|   visibility?: AssetVisibility; | ||||
| } | ||||
|  | ||||
| export class TimeBucketAssetDto extends TimeBucketDto { | ||||
|   | ||||
| @@ -618,3 +618,13 @@ export enum DatabaseSslMode { | ||||
|   Require = 'require', | ||||
|   VerifyFull = 'verify-full', | ||||
| } | ||||
|  | ||||
| export enum AssetVisibility { | ||||
|   ARCHIVE = 'archive', | ||||
|   TIMELINE = 'timeline', | ||||
|  | ||||
|   /** | ||||
|    * Video part of the LivePhotos and MotionPhotos | ||||
|    */ | ||||
|   HIDDEN = 'hidden', | ||||
| } | ||||
|   | ||||
| @@ -110,8 +110,11 @@ from | ||||
|   and "assets"."deletedAt" is null | ||||
| where | ||||
|   "partner"."sharedWithId" = $1 | ||||
|   and "assets"."isArchived" = $2 | ||||
|   and "assets"."id" in ($3) | ||||
|   and ( | ||||
|     "assets"."visibility" = 'timeline' | ||||
|     or "assets"."visibility" = 'hidden' | ||||
|   ) | ||||
|   and "assets"."id" in ($2) | ||||
|  | ||||
| -- AccessRepository.asset.checkSharedLinkAccess | ||||
| select | ||||
|   | ||||
| @@ -7,7 +7,7 @@ select | ||||
|   "ownerId", | ||||
|   "duplicateId", | ||||
|   "stackId", | ||||
|   "isVisible", | ||||
|   "visibility", | ||||
|   "smart_search"."embedding", | ||||
|   ( | ||||
|     select | ||||
| @@ -83,7 +83,7 @@ from | ||||
|   inner join "asset_job_status" on "asset_job_status"."assetId" = "assets"."id" | ||||
| where | ||||
|   "assets"."deletedAt" is null | ||||
|   and "assets"."isVisible" = $1 | ||||
|   and "assets"."visibility" != $1 | ||||
|   and ( | ||||
|     "asset_job_status"."previewAt" is null | ||||
|     or "asset_job_status"."thumbnailAt" is null | ||||
| @@ -118,7 +118,7 @@ where | ||||
| -- AssetJobRepository.getForGenerateThumbnailJob | ||||
| select | ||||
|   "assets"."id", | ||||
|   "assets"."isVisible", | ||||
|   "assets"."visibility", | ||||
|   "assets"."originalFileName", | ||||
|   "assets"."originalPath", | ||||
|   "assets"."ownerId", | ||||
| @@ -155,7 +155,7 @@ select | ||||
|   "assets"."fileCreatedAt", | ||||
|   "assets"."fileModifiedAt", | ||||
|   "assets"."isExternal", | ||||
|   "assets"."isVisible", | ||||
|   "assets"."visibility", | ||||
|   "assets"."libraryId", | ||||
|   "assets"."livePhotoVideoId", | ||||
|   "assets"."localDateTime", | ||||
| @@ -201,7 +201,7 @@ from | ||||
|   "assets" | ||||
|   inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id" | ||||
| where | ||||
|   "assets"."isVisible" = $1 | ||||
|   "assets"."visibility" != $1 | ||||
|   and "assets"."deletedAt" is null | ||||
|   and "job_status"."previewAt" is not null | ||||
|   and not exists ( | ||||
| @@ -220,7 +220,7 @@ from | ||||
|   "assets" | ||||
|   inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id" | ||||
| where | ||||
|   "assets"."isVisible" = $1 | ||||
|   "assets"."visibility" != $1 | ||||
|   and "assets"."deletedAt" is null | ||||
|   and "job_status"."previewAt" is not null | ||||
|   and not exists ( | ||||
| @@ -234,7 +234,7 @@ where | ||||
| -- AssetJobRepository.getForClipEncoding | ||||
| select | ||||
|   "assets"."id", | ||||
|   "assets"."isVisible", | ||||
|   "assets"."visibility", | ||||
|   ( | ||||
|     select | ||||
|       coalesce(json_agg(agg), '[]') | ||||
| @@ -259,7 +259,7 @@ where | ||||
| -- AssetJobRepository.getForDetectFacesJob | ||||
| select | ||||
|   "assets"."id", | ||||
|   "assets"."isVisible", | ||||
|   "assets"."visibility", | ||||
|   to_json("exif") as "exifInfo", | ||||
|   ( | ||||
|     select | ||||
| @@ -312,7 +312,7 @@ where | ||||
| -- AssetJobRepository.getForAssetDeletion | ||||
| select | ||||
|   "assets"."id", | ||||
|   "assets"."isVisible", | ||||
|   "assets"."visibility", | ||||
|   "assets"."libraryId", | ||||
|   "assets"."ownerId", | ||||
|   "assets"."livePhotoVideoId", | ||||
| @@ -372,7 +372,7 @@ from | ||||
|       "assets" as "stacked" | ||||
|     where | ||||
|       "stacked"."deletedAt" is not null | ||||
|       and "stacked"."isArchived" = $1 | ||||
|       and "stacked"."visibility" != $1 | ||||
|       and "stacked"."stackId" = "asset_stack"."id" | ||||
|     group by | ||||
|       "asset_stack"."id" | ||||
| @@ -391,7 +391,7 @@ where | ||||
|     "assets"."encodedVideoPath" is null | ||||
|     or "assets"."encodedVideoPath" = $2 | ||||
|   ) | ||||
|   and "assets"."isVisible" = $3 | ||||
|   and "assets"."visibility" != $3 | ||||
|   and "assets"."deletedAt" is null | ||||
|  | ||||
| -- AssetJobRepository.getForVideoConversion | ||||
| @@ -417,7 +417,7 @@ where | ||||
|     "asset_job_status"."metadataExtractedAt" is null | ||||
|     or "asset_job_status"."assetId" is null | ||||
|   ) | ||||
|   and "assets"."isVisible" = $1 | ||||
|   and "assets"."visibility" != $1 | ||||
|   and "assets"."deletedAt" is null | ||||
|  | ||||
| -- AssetJobRepository.getForStorageTemplateJob | ||||
| @@ -480,7 +480,7 @@ where | ||||
|     "assets"."sidecarPath" = $1 | ||||
|     or "assets"."sidecarPath" is null | ||||
|   ) | ||||
|   and "assets"."isVisible" = $2 | ||||
|   and "assets"."visibility" != $2 | ||||
|  | ||||
| -- AssetJobRepository.streamForDetectFacesJob | ||||
| select | ||||
| @@ -489,7 +489,7 @@ from | ||||
|   "assets" | ||||
|   inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id" | ||||
| where | ||||
|   "assets"."isVisible" = $1 | ||||
|   "assets"."visibility" != $1 | ||||
|   and "assets"."deletedAt" is null | ||||
|   and "job_status"."previewAt" is not null | ||||
|   and "job_status"."facesRecognizedAt" is null | ||||
|   | ||||
| @@ -43,21 +43,20 @@ with | ||||
|           "asset_job_status"."previewAt" is not null | ||||
|           and (assets."localDateTime" at time zone 'UTC')::date = today.date | ||||
|           and "assets"."ownerId" = any ($3::uuid[]) | ||||
|           and "assets"."isVisible" = $4 | ||||
|           and "assets"."isArchived" = $5 | ||||
|           and "assets"."visibility" = $4 | ||||
|           and exists ( | ||||
|             select | ||||
|             from | ||||
|               "asset_files" | ||||
|             where | ||||
|               "assetId" = "assets"."id" | ||||
|               and "asset_files"."type" = $6 | ||||
|               and "asset_files"."type" = $5 | ||||
|           ) | ||||
|           and "assets"."deletedAt" is null | ||||
|         order by | ||||
|           (assets."localDateTime" at time zone 'UTC')::date desc | ||||
|         limit | ||||
|           $7 | ||||
|           $6 | ||||
|       ) as "a" on true | ||||
|       inner join "exif" on "a"."id" = "exif"."assetId" | ||||
|   ) | ||||
| @@ -159,7 +158,7 @@ from | ||||
| where | ||||
|   "ownerId" = $1::uuid | ||||
|   and "deviceId" = $2 | ||||
|   and "isVisible" = $3 | ||||
|   and "visibility" != $3 | ||||
|   and "deletedAt" is null | ||||
|  | ||||
| -- AssetRepository.getLivePhotoCount | ||||
| @@ -241,7 +240,10 @@ with | ||||
|       "assets" | ||||
|     where | ||||
|       "assets"."deletedAt" is null | ||||
|       and "assets"."isVisible" = $2 | ||||
|       and ( | ||||
|         "assets"."visibility" = $2 | ||||
|         or "assets"."visibility" = $3 | ||||
|       ) | ||||
|   ) | ||||
| select | ||||
|   "timeBucket", | ||||
| @@ -271,7 +273,7 @@ from | ||||
|     where | ||||
|       "stacked"."stackId" = "asset_stack"."id" | ||||
|       and "stacked"."deletedAt" is null | ||||
|       and "stacked"."isArchived" = $1 | ||||
|       and "stacked"."visibility" != $1 | ||||
|     group by | ||||
|       "asset_stack"."id" | ||||
|   ) as "stacked_assets" on "asset_stack"."id" is not null | ||||
| @@ -281,8 +283,11 @@ where | ||||
|     or "assets"."stackId" is null | ||||
|   ) | ||||
|   and "assets"."deletedAt" is null | ||||
|   and "assets"."isVisible" = $2 | ||||
|   and date_trunc($3, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4 | ||||
|   and ( | ||||
|     "assets"."visibility" = $2 | ||||
|     or "assets"."visibility" = $3 | ||||
|   ) | ||||
|   and date_trunc($4, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $5 | ||||
| order by | ||||
|   "assets"."localDateTime" desc | ||||
|  | ||||
| @@ -307,7 +312,7 @@ with | ||||
|       "assets"."ownerId" = $1::uuid | ||||
|       and "assets"."duplicateId" is not null | ||||
|       and "assets"."deletedAt" is null | ||||
|       and "assets"."isVisible" = $2 | ||||
|       and "assets"."visibility" != $2 | ||||
|       and "assets"."stackId" is null | ||||
|     group by | ||||
|       "assets"."duplicateId" | ||||
| @@ -365,12 +370,11 @@ from | ||||
|   inner join "cities" on "exif"."city" = "cities"."city" | ||||
| where | ||||
|   "ownerId" = $2::uuid | ||||
|   and "isVisible" = $3 | ||||
|   and "isArchived" = $4 | ||||
|   and "type" = $5 | ||||
|   and "visibility" = $3 | ||||
|   and "type" = $4 | ||||
|   and "deletedAt" is null | ||||
| limit | ||||
|   $6 | ||||
|   $5 | ||||
|  | ||||
| -- AssetRepository.getAllForUserFullSync | ||||
| select | ||||
| @@ -394,7 +398,7 @@ from | ||||
|   ) as "stacked_assets" on "asset_stack"."id" is not null | ||||
| where | ||||
|   "assets"."ownerId" = $1::uuid | ||||
|   and "assets"."isVisible" = $2 | ||||
|   and "assets"."visibility" != $2 | ||||
|   and "assets"."updatedAt" <= $3 | ||||
|   and "assets"."id" > $4 | ||||
| order by | ||||
| @@ -424,7 +428,7 @@ from | ||||
|   ) as "stacked_assets" on "asset_stack"."id" is not null | ||||
| where | ||||
|   "assets"."ownerId" = any ($1::uuid[]) | ||||
|   and "assets"."isVisible" = $2 | ||||
|   and "assets"."visibility" != $2 | ||||
|   and "assets"."updatedAt" > $3 | ||||
| limit | ||||
|   $4 | ||||
|   | ||||
| @@ -35,14 +35,14 @@ select | ||||
|     where | ||||
|       ( | ||||
|         "assets"."type" = $1 | ||||
|         and "assets"."isVisible" = $2 | ||||
|         and "assets"."visibility" != $2 | ||||
|       ) | ||||
|   ) as "photos", | ||||
|   count(*) filter ( | ||||
|     where | ||||
|       ( | ||||
|         "assets"."type" = $3 | ||||
|         and "assets"."isVisible" = $4 | ||||
|         and "assets"."visibility" != $4 | ||||
|       ) | ||||
|   ) as "videos", | ||||
|   coalesce(sum("exif"."fileSizeInByte"), $5) as "usage" | ||||
|   | ||||
| @@ -14,7 +14,7 @@ from | ||||
|   and "exif"."latitude" is not null | ||||
|   and "exif"."longitude" is not null | ||||
| where | ||||
|   "isVisible" = $1 | ||||
|   "assets"."visibility" = $1 | ||||
|   and "deletedAt" is null | ||||
|   and ( | ||||
|     "ownerId" in ($2) | ||||
|   | ||||
| @@ -107,7 +107,7 @@ select | ||||
|       ( | ||||
|         select | ||||
|           "assets"."ownerId", | ||||
|           "assets"."isArchived", | ||||
|           "assets"."visibility", | ||||
|           "assets"."fileCreatedAt" | ||||
|         from | ||||
|           "assets" | ||||
| @@ -203,7 +203,7 @@ from | ||||
|   "asset_faces" | ||||
|   left join "assets" on "assets"."id" = "asset_faces"."assetId" | ||||
|   and "asset_faces"."personId" = $1 | ||||
|   and "assets"."isArchived" = $2 | ||||
|   and "assets"."visibility" != $2 | ||||
|   and "assets"."deletedAt" is null | ||||
| where | ||||
|   "asset_faces"."deletedAt" is null | ||||
| @@ -220,7 +220,7 @@ from | ||||
|   inner join "asset_faces" on "asset_faces"."personId" = "person"."id" | ||||
|   inner join "assets" on "assets"."id" = "asset_faces"."assetId" | ||||
|   and "assets"."deletedAt" is null | ||||
|   and "assets"."isArchived" = $2 | ||||
|   and "assets"."visibility" != $2 | ||||
| where | ||||
|   "person"."ownerId" = $3 | ||||
|   and "asset_faces"."deletedAt" is null | ||||
|   | ||||
| @@ -7,11 +7,11 @@ from | ||||
|   "assets" | ||||
|   inner join "exif" on "assets"."id" = "exif"."assetId" | ||||
| where | ||||
|   "assets"."fileCreatedAt" >= $1 | ||||
|   and "exif"."lensModel" = $2 | ||||
|   and "assets"."ownerId" = any ($3::uuid[]) | ||||
|   and "assets"."isFavorite" = $4 | ||||
|   and "assets"."isArchived" = $5 | ||||
|   "assets"."visibility" = $1 | ||||
|   and "assets"."fileCreatedAt" >= $2 | ||||
|   and "exif"."lensModel" = $3 | ||||
|   and "assets"."ownerId" = any ($4::uuid[]) | ||||
|   and "assets"."isFavorite" = $5 | ||||
|   and "assets"."deletedAt" is null | ||||
| order by | ||||
|   "assets"."fileCreatedAt" desc | ||||
| @@ -28,11 +28,11 @@ offset | ||||
|     "assets" | ||||
|     inner join "exif" on "assets"."id" = "exif"."assetId" | ||||
|   where | ||||
|     "assets"."fileCreatedAt" >= $1 | ||||
|     and "exif"."lensModel" = $2 | ||||
|     and "assets"."ownerId" = any ($3::uuid[]) | ||||
|     and "assets"."isFavorite" = $4 | ||||
|     and "assets"."isArchived" = $5 | ||||
|     "assets"."visibility" = $1 | ||||
|     and "assets"."fileCreatedAt" >= $2 | ||||
|     and "exif"."lensModel" = $3 | ||||
|     and "assets"."ownerId" = any ($4::uuid[]) | ||||
|     and "assets"."isFavorite" = $5 | ||||
|     and "assets"."deletedAt" is null | ||||
|     and "assets"."id" < $6 | ||||
|   order by | ||||
| @@ -48,11 +48,11 @@ union all | ||||
|     "assets" | ||||
|     inner join "exif" on "assets"."id" = "exif"."assetId" | ||||
|   where | ||||
|     "assets"."fileCreatedAt" >= $8 | ||||
|     and "exif"."lensModel" = $9 | ||||
|     and "assets"."ownerId" = any ($10::uuid[]) | ||||
|     and "assets"."isFavorite" = $11 | ||||
|     and "assets"."isArchived" = $12 | ||||
|     "assets"."visibility" = $8 | ||||
|     and "assets"."fileCreatedAt" >= $9 | ||||
|     and "exif"."lensModel" = $10 | ||||
|     and "assets"."ownerId" = any ($11::uuid[]) | ||||
|     and "assets"."isFavorite" = $12 | ||||
|     and "assets"."deletedAt" is null | ||||
|     and "assets"."id" > $13 | ||||
|   order by | ||||
| @@ -71,11 +71,11 @@ from | ||||
|   inner join "exif" on "assets"."id" = "exif"."assetId" | ||||
|   inner join "smart_search" on "assets"."id" = "smart_search"."assetId" | ||||
| where | ||||
|   "assets"."fileCreatedAt" >= $1 | ||||
|   and "exif"."lensModel" = $2 | ||||
|   and "assets"."ownerId" = any ($3::uuid[]) | ||||
|   and "assets"."isFavorite" = $4 | ||||
|   and "assets"."isArchived" = $5 | ||||
|   "assets"."visibility" = $1 | ||||
|   and "assets"."fileCreatedAt" >= $2 | ||||
|   and "exif"."lensModel" = $3 | ||||
|   and "assets"."ownerId" = any ($4::uuid[]) | ||||
|   and "assets"."isFavorite" = $5 | ||||
|   and "assets"."deletedAt" is null | ||||
| order by | ||||
|   smart_search.embedding <=> $6 | ||||
| @@ -97,7 +97,7 @@ with | ||||
|     where | ||||
|       "assets"."ownerId" = any ($2::uuid[]) | ||||
|       and "assets"."deletedAt" is null | ||||
|       and "assets"."isVisible" = $3 | ||||
|       and "assets"."visibility" != $3 | ||||
|       and "assets"."type" = $4 | ||||
|       and "assets"."id" != $5::uuid | ||||
|       and "assets"."stackId" is null | ||||
| @@ -176,14 +176,13 @@ with recursive | ||||
|         inner join "assets" on "assets"."id" = "exif"."assetId" | ||||
|       where | ||||
|         "assets"."ownerId" = any ($1::uuid[]) | ||||
|         and "assets"."isVisible" = $2 | ||||
|         and "assets"."isArchived" = $3 | ||||
|         and "assets"."type" = $4 | ||||
|         and "assets"."visibility" = $2 | ||||
|         and "assets"."type" = $3 | ||||
|         and "assets"."deletedAt" is null | ||||
|       order by | ||||
|         "city" | ||||
|       limit | ||||
|         $5 | ||||
|         $4 | ||||
|     ) | ||||
|     union all | ||||
|     ( | ||||
| @@ -200,16 +199,15 @@ with recursive | ||||
|             "exif" | ||||
|             inner join "assets" on "assets"."id" = "exif"."assetId" | ||||
|           where | ||||
|             "assets"."ownerId" = any ($6::uuid[]) | ||||
|             and "assets"."isVisible" = $7 | ||||
|             and "assets"."isArchived" = $8 | ||||
|             and "assets"."type" = $9 | ||||
|             "assets"."ownerId" = any ($5::uuid[]) | ||||
|             and "assets"."visibility" = $6 | ||||
|             and "assets"."type" = $7 | ||||
|             and "assets"."deletedAt" is null | ||||
|             and "exif"."city" > "cte"."city" | ||||
|           order by | ||||
|             "city" | ||||
|           limit | ||||
|             $10 | ||||
|             $8 | ||||
|         ) as "l" on true | ||||
|     ) | ||||
|   ) | ||||
| @@ -231,7 +229,7 @@ from | ||||
|   inner join "assets" on "assets"."id" = "exif"."assetId" | ||||
| where | ||||
|   "ownerId" = any ($1::uuid[]) | ||||
|   and "isVisible" = $2 | ||||
|   and "visibility" != $2 | ||||
|   and "deletedAt" is null | ||||
|   and "state" is not null | ||||
|  | ||||
| @@ -243,7 +241,7 @@ from | ||||
|   inner join "assets" on "assets"."id" = "exif"."assetId" | ||||
| where | ||||
|   "ownerId" = any ($1::uuid[]) | ||||
|   and "isVisible" = $2 | ||||
|   and "visibility" != $2 | ||||
|   and "deletedAt" is null | ||||
|   and "city" is not null | ||||
|  | ||||
| @@ -255,7 +253,7 @@ from | ||||
|   inner join "assets" on "assets"."id" = "exif"."assetId" | ||||
| where | ||||
|   "ownerId" = any ($1::uuid[]) | ||||
|   and "isVisible" = $2 | ||||
|   and "visibility" != $2 | ||||
|   and "deletedAt" is null | ||||
|   and "make" is not null | ||||
|  | ||||
| @@ -267,6 +265,6 @@ from | ||||
|   inner join "assets" on "assets"."id" = "exif"."assetId" | ||||
| where | ||||
|   "ownerId" = any ($1::uuid[]) | ||||
|   and "isVisible" = $2 | ||||
|   and "visibility" != $2 | ||||
|   and "deletedAt" is null | ||||
|   and "model" is not null | ||||
|   | ||||
| @@ -84,7 +84,7 @@ select | ||||
|   "type", | ||||
|   "deletedAt", | ||||
|   "isFavorite", | ||||
|   "isVisible", | ||||
|   "visibility", | ||||
|   "updateId" | ||||
| from | ||||
|   "assets" | ||||
| @@ -106,7 +106,7 @@ select | ||||
|   "type", | ||||
|   "deletedAt", | ||||
|   "isFavorite", | ||||
|   "isVisible", | ||||
|   "visibility", | ||||
|   "updateId" | ||||
| from | ||||
|   "assets" | ||||
|   | ||||
| @@ -285,14 +285,14 @@ select | ||||
|     where | ||||
|       ( | ||||
|         "assets"."type" = 'IMAGE' | ||||
|         and "assets"."isVisible" = true | ||||
|         and "assets"."visibility" != 'hidden' | ||||
|       ) | ||||
|   ) as "photos", | ||||
|   count(*) filter ( | ||||
|     where | ||||
|       ( | ||||
|         "assets"."type" = 'VIDEO' | ||||
|         and "assets"."isVisible" = true | ||||
|         and "assets"."visibility" != 'hidden' | ||||
|       ) | ||||
|   ) as "videos", | ||||
|   coalesce( | ||||
|   | ||||
| @@ -7,8 +7,7 @@ from | ||||
|   "assets" | ||||
| where | ||||
|   "ownerId" = $2::uuid | ||||
|   and "isVisible" = $3 | ||||
|   and "isArchived" = $4 | ||||
|   and "visibility" = $3 | ||||
|   and "deletedAt" is null | ||||
|   and "fileCreatedAt" is not null | ||||
|   and "fileModifiedAt" is not null | ||||
| @@ -23,13 +22,12 @@ from | ||||
|   left join "exif" on "assets"."id" = "exif"."assetId" | ||||
| where | ||||
|   "ownerId" = $1::uuid | ||||
|   and "isVisible" = $2 | ||||
|   and "isArchived" = $3 | ||||
|   and "visibility" = $2 | ||||
|   and "deletedAt" is null | ||||
|   and "fileCreatedAt" is not null | ||||
|   and "fileModifiedAt" is not null | ||||
|   and "localDateTime" is not null | ||||
|   and "originalPath" like $4 | ||||
|   and "originalPath" not like $5 | ||||
|   and "originalPath" like $3 | ||||
|   and "originalPath" not like $4 | ||||
| order by | ||||
|   regexp_replace("assets"."originalPath", $6, $7) asc | ||||
|   regexp_replace("assets"."originalPath", $5, $6) asc | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { Kysely, sql } from 'kysely'; | ||||
| import { InjectKysely } from 'nestjs-kysely'; | ||||
| import { DB } from 'src/db'; | ||||
| import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { AlbumUserRole } from 'src/enum'; | ||||
| import { AlbumUserRole, AssetVisibility } from 'src/enum'; | ||||
| import { asUuid } from 'src/utils/database'; | ||||
|  | ||||
| class ActivityAccess { | ||||
| @@ -199,7 +199,13 @@ class AssetAccess { | ||||
|       ) | ||||
|       .select('assets.id') | ||||
|       .where('partner.sharedWithId', '=', userId) | ||||
|       .where('assets.isArchived', '=', false) | ||||
|       .where((eb) => | ||||
|         eb.or([ | ||||
|           eb('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)), | ||||
|           eb('assets.visibility', '=', sql.lit(AssetVisibility.HIDDEN)), | ||||
|         ]), | ||||
|       ) | ||||
|  | ||||
|       .where('assets.id', 'in', [...assetIds]) | ||||
|       .execute() | ||||
|       .then((assets) => new Set(assets.map((asset) => asset.id))); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { InjectKysely } from 'nestjs-kysely'; | ||||
| import { Asset, columns } from 'src/database'; | ||||
| import { DB } from 'src/db'; | ||||
| import { DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { AssetFileType, AssetType } from 'src/enum'; | ||||
| import { AssetFileType, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { StorageAsset } from 'src/types'; | ||||
| import { | ||||
|   anyUuid, | ||||
| @@ -34,7 +34,7 @@ export class AssetJobRepository { | ||||
|         'ownerId', | ||||
|         'duplicateId', | ||||
|         'stackId', | ||||
|         'isVisible', | ||||
|         'visibility', | ||||
|         'smart_search.embedding', | ||||
|         withFiles(eb, AssetFileType.PREVIEW), | ||||
|       ]) | ||||
| @@ -70,7 +70,7 @@ export class AssetJobRepository { | ||||
|       .select(['assets.id', 'assets.thumbhash']) | ||||
|       .select(withFiles) | ||||
|       .where('assets.deletedAt', 'is', null) | ||||
|       .where('assets.isVisible', '=', true) | ||||
|       .where('assets.visibility', '!=', AssetVisibility.HIDDEN) | ||||
|       .$if(!force, (qb) => | ||||
|         qb | ||||
|           // If there aren't any entries, metadata extraction hasn't run yet which is required for thumbnails | ||||
| @@ -102,7 +102,7 @@ export class AssetJobRepository { | ||||
|       .selectFrom('assets') | ||||
|       .select([ | ||||
|         'assets.id', | ||||
|         'assets.isVisible', | ||||
|         'assets.visibility', | ||||
|         'assets.originalFileName', | ||||
|         'assets.originalPath', | ||||
|         'assets.ownerId', | ||||
| @@ -138,7 +138,7 @@ export class AssetJobRepository { | ||||
|   private assetsWithPreviews() { | ||||
|     return this.db | ||||
|       .selectFrom('assets') | ||||
|       .where('assets.isVisible', '=', true) | ||||
|       .where('assets.visibility', '!=', AssetVisibility.HIDDEN) | ||||
|       .where('assets.deletedAt', 'is', null) | ||||
|       .innerJoin('asset_job_status as job_status', 'assetId', 'assets.id') | ||||
|       .where('job_status.previewAt', 'is not', null); | ||||
| @@ -169,7 +169,7 @@ export class AssetJobRepository { | ||||
|   getForClipEncoding(id: string) { | ||||
|     return this.db | ||||
|       .selectFrom('assets') | ||||
|       .select(['assets.id', 'assets.isVisible']) | ||||
|       .select(['assets.id', 'assets.visibility']) | ||||
|       .select((eb) => withFiles(eb, AssetFileType.PREVIEW)) | ||||
|       .where('assets.id', '=', id) | ||||
|       .executeTakeFirst(); | ||||
| @@ -179,7 +179,7 @@ export class AssetJobRepository { | ||||
|   getForDetectFacesJob(id: string) { | ||||
|     return this.db | ||||
|       .selectFrom('assets') | ||||
|       .select(['assets.id', 'assets.isVisible']) | ||||
|       .select(['assets.id', 'assets.visibility']) | ||||
|       .$call(withExifInner) | ||||
|       .select((eb) => withFaces(eb, true)) | ||||
|       .select((eb) => withFiles(eb, AssetFileType.PREVIEW)) | ||||
| @@ -209,7 +209,7 @@ export class AssetJobRepository { | ||||
|       .selectFrom('assets') | ||||
|       .select([ | ||||
|         'assets.id', | ||||
|         'assets.isVisible', | ||||
|         'assets.visibility', | ||||
|         'assets.libraryId', | ||||
|         'assets.ownerId', | ||||
|         'assets.livePhotoVideoId', | ||||
| @@ -228,7 +228,7 @@ export class AssetJobRepository { | ||||
|             .select(['asset_stack.id', 'asset_stack.primaryAssetId']) | ||||
|             .select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets')) | ||||
|             .where('stacked.deletedAt', 'is not', null) | ||||
|             .where('stacked.isArchived', '=', false) | ||||
|             .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE) | ||||
|             .whereRef('stacked.stackId', '=', 'asset_stack.id') | ||||
|             .groupBy('asset_stack.id') | ||||
|             .as('stacked_assets'), | ||||
| @@ -248,7 +248,7 @@ export class AssetJobRepository { | ||||
|       .$if(!force, (qb) => | ||||
|         qb | ||||
|           .where((eb) => eb.or([eb('assets.encodedVideoPath', 'is', null), eb('assets.encodedVideoPath', '=', '')])) | ||||
|           .where('assets.isVisible', '=', true), | ||||
|           .where('assets.visibility', '!=', AssetVisibility.HIDDEN), | ||||
|       ) | ||||
|       .where('assets.deletedAt', 'is', null) | ||||
|       .stream(); | ||||
| @@ -275,7 +275,7 @@ export class AssetJobRepository { | ||||
|           .where((eb) => | ||||
|             eb.or([eb('asset_job_status.metadataExtractedAt', 'is', null), eb('asset_job_status.assetId', 'is', null)]), | ||||
|           ) | ||||
|           .where('assets.isVisible', '=', true), | ||||
|           .where('assets.visibility', '!=', AssetVisibility.HIDDEN), | ||||
|       ) | ||||
|       .where('assets.deletedAt', 'is', null) | ||||
|       .stream(); | ||||
| @@ -331,7 +331,7 @@ export class AssetJobRepository { | ||||
|       .$if(!force, (qb) => | ||||
|         qb.where((eb) => eb.or([eb('assets.sidecarPath', '=', ''), eb('assets.sidecarPath', 'is', null)])), | ||||
|       ) | ||||
|       .where('assets.isVisible', '=', true) | ||||
|       .where('assets.visibility', '!=', AssetVisibility.HIDDEN) | ||||
|       .stream(); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import { Stack } from 'src/database'; | ||||
| import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db'; | ||||
| import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { MapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum'; | ||||
| import { AssetFileType, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { | ||||
|   anyUuid, | ||||
|   asUuid, | ||||
| @@ -14,6 +14,7 @@ import { | ||||
|   removeUndefinedKeys, | ||||
|   truncatedDate, | ||||
|   unnest, | ||||
|   withDefaultVisibility, | ||||
|   withExif, | ||||
|   withFaces, | ||||
|   withFacesAndPeople, | ||||
| @@ -30,8 +31,8 @@ export type AssetStats = Record<AssetType, number>; | ||||
|  | ||||
| export interface AssetStatsOptions { | ||||
|   isFavorite?: boolean; | ||||
|   isArchived?: boolean; | ||||
|   isTrashed?: boolean; | ||||
|   visibility?: AssetVisibility; | ||||
| } | ||||
|  | ||||
| export interface LivePhotoSearchOptions { | ||||
| @@ -52,7 +53,6 @@ export enum TimeBucketSize { | ||||
| } | ||||
|  | ||||
| export interface AssetBuilderOptions { | ||||
|   isArchived?: boolean; | ||||
|   isFavorite?: boolean; | ||||
|   isTrashed?: boolean; | ||||
|   isDuplicate?: boolean; | ||||
| @@ -64,6 +64,7 @@ export interface AssetBuilderOptions { | ||||
|   exifInfo?: boolean; | ||||
|   status?: AssetStatus; | ||||
|   assetType?: AssetType; | ||||
|   visibility?: AssetVisibility; | ||||
| } | ||||
|  | ||||
| export interface TimeBucketOptions extends AssetBuilderOptions { | ||||
| @@ -258,8 +259,7 @@ export class AssetRepository { | ||||
|                 .where('asset_job_status.previewAt', 'is not', null) | ||||
|                 .where(sql`(assets."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`) | ||||
|                 .where('assets.ownerId', '=', anyUuid(ownerIds)) | ||||
|                 .where('assets.isVisible', '=', true) | ||||
|                 .where('assets.isArchived', '=', false) | ||||
|                 .where('assets.visibility', '=', AssetVisibility.TIMELINE) | ||||
|                 .where((eb) => | ||||
|                   eb.exists((qb) => | ||||
|                     qb | ||||
| @@ -348,7 +348,7 @@ export class AssetRepository { | ||||
|       .select(['deviceAssetId']) | ||||
|       .where('ownerId', '=', asUuid(ownerId)) | ||||
|       .where('deviceId', '=', deviceId) | ||||
|       .where('isVisible', '=', true) | ||||
|       .where('visibility', '!=', AssetVisibility.HIDDEN) | ||||
|       .where('deletedAt', 'is', null) | ||||
|       .execute(); | ||||
|  | ||||
| @@ -393,7 +393,7 @@ export class AssetRepository { | ||||
|                     .whereRef('stacked.stackId', '=', 'asset_stack.id') | ||||
|                     .whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId') | ||||
|                     .where('stacked.deletedAt', 'is', null) | ||||
|                     .where('stacked.isArchived', '=', false) | ||||
|                     .where('stacked.visibility', '=', AssetVisibility.TIMELINE) | ||||
|                     .groupBy('asset_stack.id') | ||||
|                     .as('stacked_assets'), | ||||
|                 (join) => join.on('asset_stack.id', 'is not', null), | ||||
| @@ -503,7 +503,7 @@ export class AssetRepository { | ||||
|       .executeTakeFirst(); | ||||
|   } | ||||
|  | ||||
|   getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> { | ||||
|   getStatistics(ownerId: string, { visibility, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> { | ||||
|     return this.db | ||||
|       .selectFrom('assets') | ||||
|       .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.AUDIO).as(AssetType.AUDIO)) | ||||
| @@ -511,8 +511,8 @@ export class AssetRepository { | ||||
|       .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO)) | ||||
|       .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER)) | ||||
|       .where('ownerId', '=', asUuid(ownerId)) | ||||
|       .where('isVisible', '=', true) | ||||
|       .$if(isArchived !== undefined, (qb) => qb.where('isArchived', '=', isArchived!)) | ||||
|       .$if(visibility === undefined, withDefaultVisibility) | ||||
|       .$if(!!visibility, (qb) => qb.where('assets.visibility', '=', visibility!)) | ||||
|       .$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!)) | ||||
|       .$if(!!isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) | ||||
|       .where('deletedAt', isTrashed ? 'is not' : 'is', null) | ||||
| @@ -525,7 +525,7 @@ export class AssetRepository { | ||||
|       .selectAll('assets') | ||||
|       .$call(withExif) | ||||
|       .where('ownerId', '=', anyUuid(userIds)) | ||||
|       .where('isVisible', '=', true) | ||||
|       .where('visibility', '!=', AssetVisibility.HIDDEN) | ||||
|       .where('deletedAt', 'is', null) | ||||
|       .orderBy((eb) => eb.fn('random')) | ||||
|       .limit(take) | ||||
| @@ -542,7 +542,8 @@ export class AssetRepository { | ||||
|             .select(truncatedDate<Date>(options.size).as('timeBucket')) | ||||
|             .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) | ||||
|             .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) | ||||
|             .where('assets.isVisible', '=', true) | ||||
|             .$if(options.visibility === undefined, withDefaultVisibility) | ||||
|             .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!)) | ||||
|             .$if(!!options.albumId, (qb) => | ||||
|               qb | ||||
|                 .innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId') | ||||
| @@ -559,7 +560,6 @@ export class AssetRepository { | ||||
|                 .where((eb) => eb.or([eb('assets.stackId', 'is', null), eb(eb.table('asset_stack'), 'is not', null)])), | ||||
|             ) | ||||
|             .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) | ||||
|             .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!)) | ||||
|             .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) | ||||
|             .$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!)) | ||||
|             .$if(options.isDuplicate !== undefined, (qb) => | ||||
| @@ -594,7 +594,6 @@ export class AssetRepository { | ||||
|       ) | ||||
|       .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!])) | ||||
|       .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) | ||||
|       .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!)) | ||||
|       .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) | ||||
|       .$if(!!options.withStacked, (qb) => | ||||
|         qb | ||||
| @@ -610,7 +609,7 @@ export class AssetRepository { | ||||
|                 .select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount')) | ||||
|                 .whereRef('stacked.stackId', '=', 'asset_stack.id') | ||||
|                 .where('stacked.deletedAt', 'is', null) | ||||
|                 .where('stacked.isArchived', '=', false) | ||||
|                 .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE) | ||||
|                 .groupBy('asset_stack.id') | ||||
|                 .as('stacked_assets'), | ||||
|             (join) => join.on('asset_stack.id', 'is not', null), | ||||
| @@ -624,7 +623,8 @@ export class AssetRepository { | ||||
|       .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) | ||||
|       .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)) | ||||
|       .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) | ||||
|       .where('assets.isVisible', '=', true) | ||||
|       .$if(options.visibility == undefined, withDefaultVisibility) | ||||
|       .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!)) | ||||
|       .where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, '')) | ||||
|       .orderBy('assets.localDateTime', options.order ?? 'desc') | ||||
|       .execute(); | ||||
| @@ -658,7 +658,7 @@ export class AssetRepository { | ||||
|             .where('assets.duplicateId', 'is not', null) | ||||
|             .$narrowType<{ duplicateId: NotNull }>() | ||||
|             .where('assets.deletedAt', 'is', null) | ||||
|             .where('assets.isVisible', '=', true) | ||||
|             .where('assets.visibility', '!=', AssetVisibility.HIDDEN) | ||||
|             .where('assets.stackId', 'is', null) | ||||
|             .groupBy('assets.duplicateId'), | ||||
|         ) | ||||
| @@ -703,8 +703,7 @@ export class AssetRepository { | ||||
|       .select(['assetId as data', 'exif.city as value']) | ||||
|       .$narrowType<{ value: NotNull }>() | ||||
|       .where('ownerId', '=', asUuid(ownerId)) | ||||
|       .where('isVisible', '=', true) | ||||
|       .where('isArchived', '=', false) | ||||
|       .where('visibility', '=', AssetVisibility.TIMELINE) | ||||
|       .where('type', '=', AssetType.IMAGE) | ||||
|       .where('deletedAt', 'is', null) | ||||
|       .limit(maxFields) | ||||
| @@ -743,7 +742,7 @@ export class AssetRepository { | ||||
|       ) | ||||
|       .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack')) | ||||
|       .where('assets.ownerId', '=', asUuid(ownerId)) | ||||
|       .where('assets.isVisible', '=', true) | ||||
|       .where('assets.visibility', '!=', AssetVisibility.HIDDEN) | ||||
|       .where('assets.updatedAt', '<=', updatedUntil) | ||||
|       .$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!)) | ||||
|       .orderBy('assets.id') | ||||
| @@ -771,7 +770,7 @@ export class AssetRepository { | ||||
|       ) | ||||
|       .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack')) | ||||
|       .where('assets.ownerId', '=', anyUuid(options.userIds)) | ||||
|       .where('assets.isVisible', '=', true) | ||||
|       .where('assets.visibility', '!=', AssetVisibility.HIDDEN) | ||||
|       .where('assets.updatedAt', '>', options.updatedAfter) | ||||
|       .limit(options.limit) | ||||
|       .execute(); | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; | ||||
| import { Kysely } from 'kysely'; | ||||
| import { InjectKysely } from 'nestjs-kysely'; | ||||
| import { DB } from 'src/db'; | ||||
| import { AssetVisibility } from 'src/enum'; | ||||
| import { anyUuid } from 'src/utils/database'; | ||||
|  | ||||
| const builder = (db: Kysely<DB>) => | ||||
| @@ -31,6 +32,9 @@ export class DownloadRepository { | ||||
|   } | ||||
|  | ||||
|   downloadUserId(userId: string) { | ||||
|     return builder(this.db).where('assets.ownerId', '=', userId).where('assets.isVisible', '=', true).stream(); | ||||
|     return builder(this.db) | ||||
|       .where('assets.ownerId', '=', userId) | ||||
|       .where('assets.visibility', '!=', AssetVisibility.HIDDEN) | ||||
|       .stream(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { InjectKysely } from 'nestjs-kysely'; | ||||
| import { DB, Libraries } from 'src/db'; | ||||
| import { DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { LibraryStatsResponseDto } from 'src/dtos/library.dto'; | ||||
| import { AssetType } from 'src/enum'; | ||||
| import { AssetType, AssetVisibility } from 'src/enum'; | ||||
|  | ||||
| export enum AssetSyncResult { | ||||
|   DO_NOTHING, | ||||
| @@ -77,13 +77,17 @@ export class LibraryRepository { | ||||
|       .select((eb) => | ||||
|         eb.fn | ||||
|           .countAll<number>() | ||||
|           .filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.isVisible', '=', true)])) | ||||
|           .filterWhere((eb) => | ||||
|             eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.visibility', '!=', AssetVisibility.HIDDEN)]), | ||||
|           ) | ||||
|           .as('photos'), | ||||
|       ) | ||||
|       .select((eb) => | ||||
|         eb.fn | ||||
|           .countAll<number>() | ||||
|           .filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.isVisible', '=', true)])) | ||||
|           .filterWhere((eb) => | ||||
|             eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.visibility', '!=', AssetVisibility.HIDDEN)]), | ||||
|           ) | ||||
|           .as('videos'), | ||||
|       ) | ||||
|       .select((eb) => eb.fn.coalesce((eb) => eb.fn.sum('exif.fileSizeInByte'), eb.val(0)).as('usage')) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import readLine from 'node:readline'; | ||||
| import { citiesFile } from 'src/constants'; | ||||
| import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db'; | ||||
| import { DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { SystemMetadataKey } from 'src/enum'; | ||||
| import { AssetVisibility, SystemMetadataKey } from 'src/enum'; | ||||
| import { ConfigRepository } from 'src/repositories/config.repository'; | ||||
| import { LoggingRepository } from 'src/repositories/logging.repository'; | ||||
| import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; | ||||
| @@ -75,9 +75,11 @@ export class MapRepository { | ||||
|   } | ||||
|  | ||||
|   @GenerateSql({ params: [[DummyValue.UUID], [DummyValue.UUID]] }) | ||||
|   getMapMarkers(ownerIds: string[], albumIds: string[], options: MapMarkerSearchOptions = {}) { | ||||
|     const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options; | ||||
|  | ||||
|   getMapMarkers( | ||||
|     ownerIds: string[], | ||||
|     albumIds: string[], | ||||
|     { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore }: MapMarkerSearchOptions = {}, | ||||
|   ) { | ||||
|     return this.db | ||||
|       .selectFrom('assets') | ||||
|       .innerJoin('exif', (builder) => | ||||
| @@ -88,8 +90,17 @@ export class MapRepository { | ||||
|       ) | ||||
|       .select(['id', 'exif.latitude as lat', 'exif.longitude as lon', 'exif.city', 'exif.state', 'exif.country']) | ||||
|       .$narrowType<{ lat: NotNull; lon: NotNull }>() | ||||
|       .where('isVisible', '=', true) | ||||
|       .$if(isArchived !== undefined, (q) => q.where('isArchived', '=', isArchived!)) | ||||
|       .$if(isArchived === true, (qb) => | ||||
|         qb.where((eb) => | ||||
|           eb.or([ | ||||
|             eb('assets.visibility', '=', AssetVisibility.TIMELINE), | ||||
|             eb('assets.visibility', '=', AssetVisibility.ARCHIVE), | ||||
|           ]), | ||||
|         ), | ||||
|       ) | ||||
|       .$if(isArchived === false || isArchived === undefined, (qb) => | ||||
|         qb.where('assets.visibility', '=', AssetVisibility.TIMELINE), | ||||
|       ) | ||||
|       .$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!)) | ||||
|       .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!)) | ||||
|       .$if(fileCreatedBefore !== undefined, (q) => q.where('fileCreatedAt', '<=', fileCreatedBefore!)) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { jsonObjectFrom } from 'kysely/helpers/postgres'; | ||||
| import { InjectKysely } from 'nestjs-kysely'; | ||||
| import { AssetFaces, DB, FaceSearch, Person } from 'src/db'; | ||||
| import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { AssetFileType, SourceType } from 'src/enum'; | ||||
| import { AssetFileType, AssetVisibility, SourceType } from 'src/enum'; | ||||
| import { removeUndefinedKeys } from 'src/utils/database'; | ||||
| import { paginationHelper, PaginationOptions } from 'src/utils/pagination'; | ||||
|  | ||||
| @@ -157,7 +157,7 @@ export class PersonRepository { | ||||
|       .innerJoin('assets', (join) => | ||||
|         join | ||||
|           .onRef('asset_faces.assetId', '=', 'assets.id') | ||||
|           .on('assets.isArchived', '=', false) | ||||
|           .on('assets.visibility', '!=', AssetVisibility.ARCHIVE) | ||||
|           .on('assets.deletedAt', 'is', null), | ||||
|       ) | ||||
|       .where('person.ownerId', '=', userId) | ||||
| @@ -248,7 +248,7 @@ export class PersonRepository { | ||||
|         jsonObjectFrom( | ||||
|           eb | ||||
|             .selectFrom('assets') | ||||
|             .select(['assets.ownerId', 'assets.isArchived', 'assets.fileCreatedAt']) | ||||
|             .select(['assets.ownerId', 'assets.visibility', 'assets.fileCreatedAt']) | ||||
|             .whereRef('assets.id', '=', 'asset_faces.assetId'), | ||||
|         ).as('asset'), | ||||
|       ) | ||||
| @@ -346,7 +346,7 @@ export class PersonRepository { | ||||
|         join | ||||
|           .onRef('assets.id', '=', 'asset_faces.assetId') | ||||
|           .on('asset_faces.personId', '=', personId) | ||||
|           .on('assets.isArchived', '=', false) | ||||
|           .on('assets.visibility', '!=', AssetVisibility.ARCHIVE) | ||||
|           .on('assets.deletedAt', 'is', null), | ||||
|       ) | ||||
|       .select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count')) | ||||
| @@ -369,7 +369,7 @@ export class PersonRepository { | ||||
|         join | ||||
|           .onRef('assets.id', '=', 'asset_faces.assetId') | ||||
|           .on('assets.deletedAt', 'is', null) | ||||
|           .on('assets.isArchived', '=', false), | ||||
|           .on('assets.visibility', '!=', AssetVisibility.ARCHIVE), | ||||
|       ) | ||||
|       .select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total')) | ||||
|       .select((eb) => | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto'; | ||||
| import { DB, Exif } from 'src/db'; | ||||
| import { DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { MapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AssetStatus, AssetType } from 'src/enum'; | ||||
| import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { ConfigRepository } from 'src/repositories/config.repository'; | ||||
| import { anyUuid, asUuid, searchAssetBuilder, vectorIndexQuery } from 'src/utils/database'; | ||||
| import { paginationHelper } from 'src/utils/pagination'; | ||||
| @@ -26,17 +26,16 @@ export interface SearchUserIdOptions { | ||||
| export type SearchIdOptions = SearchAssetIdOptions & SearchUserIdOptions; | ||||
|  | ||||
| export interface SearchStatusOptions { | ||||
|   isArchived?: boolean; | ||||
|   isEncoded?: boolean; | ||||
|   isFavorite?: boolean; | ||||
|   isMotion?: boolean; | ||||
|   isOffline?: boolean; | ||||
|   isVisible?: boolean; | ||||
|   isNotInAlbum?: boolean; | ||||
|   type?: AssetType; | ||||
|   status?: AssetStatus; | ||||
|   withArchived?: boolean; | ||||
|   withDeleted?: boolean; | ||||
|   visibility?: AssetVisibility; | ||||
| } | ||||
|  | ||||
| export interface SearchOneToOneRelationOptions { | ||||
| @@ -276,7 +275,7 @@ export class SearchRepository { | ||||
|           .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') | ||||
|           .where('assets.ownerId', '=', anyUuid(userIds)) | ||||
|           .where('assets.deletedAt', 'is', null) | ||||
|           .where('assets.isVisible', '=', true) | ||||
|           .where('assets.visibility', '!=', AssetVisibility.HIDDEN) | ||||
|           .where('assets.type', '=', type) | ||||
|           .where('assets.id', '!=', asUuid(assetId)) | ||||
|           .where('assets.stackId', 'is', null) | ||||
| @@ -367,8 +366,7 @@ export class SearchRepository { | ||||
|           .select(['city', 'assetId']) | ||||
|           .innerJoin('assets', 'assets.id', 'exif.assetId') | ||||
|           .where('assets.ownerId', '=', anyUuid(userIds)) | ||||
|           .where('assets.isVisible', '=', true) | ||||
|           .where('assets.isArchived', '=', false) | ||||
|           .where('assets.visibility', '=', AssetVisibility.TIMELINE) | ||||
|           .where('assets.type', '=', AssetType.IMAGE) | ||||
|           .where('assets.deletedAt', 'is', null) | ||||
|           .orderBy('city') | ||||
| @@ -384,8 +382,7 @@ export class SearchRepository { | ||||
|                 .select(['city', 'assetId']) | ||||
|                 .innerJoin('assets', 'assets.id', 'exif.assetId') | ||||
|                 .where('assets.ownerId', '=', anyUuid(userIds)) | ||||
|                 .where('assets.isVisible', '=', true) | ||||
|                 .where('assets.isArchived', '=', false) | ||||
|                 .where('assets.visibility', '=', AssetVisibility.TIMELINE) | ||||
|                 .where('assets.type', '=', AssetType.IMAGE) | ||||
|                 .where('assets.deletedAt', 'is', null) | ||||
|                 .whereRef('exif.city', '>', 'cte.city') | ||||
| @@ -518,7 +515,7 @@ export class SearchRepository { | ||||
|       .distinctOn(field) | ||||
|       .innerJoin('assets', 'assets.id', 'exif.assetId') | ||||
|       .where('ownerId', '=', anyUuid(userIds)) | ||||
|       .where('isVisible', '=', true) | ||||
|       .where('visibility', '!=', AssetVisibility.HIDDEN) | ||||
|       .where('deletedAt', 'is', null) | ||||
|       .where(field, 'is not', null); | ||||
|   } | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import { InjectKysely } from 'nestjs-kysely'; | ||||
| import { columns } from 'src/database'; | ||||
| import { DB, UserMetadata as DbUserMetadata } from 'src/db'; | ||||
| import { DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { AssetType, UserStatus } from 'src/enum'; | ||||
| import { AssetType, AssetVisibility, UserStatus } from 'src/enum'; | ||||
| import { UserTable } from 'src/schema/tables/user.table'; | ||||
| import { UserMetadata, UserMetadataItem } from 'src/types'; | ||||
| import { asUuid } from 'src/utils/database'; | ||||
| @@ -205,13 +205,19 @@ export class UserRepository { | ||||
|         eb.fn | ||||
|           .countAll<number>() | ||||
|           .filterWhere((eb) => | ||||
|             eb.and([eb('assets.type', '=', sql.lit(AssetType.IMAGE)), eb('assets.isVisible', '=', sql.lit(true))]), | ||||
|             eb.and([ | ||||
|               eb('assets.type', '=', sql.lit(AssetType.IMAGE)), | ||||
|               eb('assets.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), | ||||
|             ]), | ||||
|           ) | ||||
|           .as('photos'), | ||||
|         eb.fn | ||||
|           .countAll<number>() | ||||
|           .filterWhere((eb) => | ||||
|             eb.and([eb('assets.type', '=', sql.lit(AssetType.VIDEO)), eb('assets.isVisible', '=', sql.lit(true))]), | ||||
|             eb.and([ | ||||
|               eb('assets.type', '=', sql.lit(AssetType.VIDEO)), | ||||
|               eb('assets.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), | ||||
|             ]), | ||||
|           ) | ||||
|           .as('videos'), | ||||
|         eb.fn | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { Kysely } from 'kysely'; | ||||
| import { InjectKysely } from 'nestjs-kysely'; | ||||
| import { DB } from 'src/db'; | ||||
| import { DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { AssetVisibility } from 'src/enum'; | ||||
| import { asUuid, withExif } from 'src/utils/database'; | ||||
|  | ||||
| export class ViewRepository { | ||||
| @@ -14,8 +15,7 @@ export class ViewRepository { | ||||
|       .select((eb) => eb.fn<string>('substring', ['assets.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath')) | ||||
|       .distinct() | ||||
|       .where('ownerId', '=', asUuid(userId)) | ||||
|       .where('isVisible', '=', true) | ||||
|       .where('isArchived', '=', false) | ||||
|       .where('visibility', '=', AssetVisibility.TIMELINE) | ||||
|       .where('deletedAt', 'is', null) | ||||
|       .where('fileCreatedAt', 'is not', null) | ||||
|       .where('fileModifiedAt', 'is not', null) | ||||
| @@ -34,8 +34,7 @@ export class ViewRepository { | ||||
|       .selectAll('assets') | ||||
|       .$call(withExif) | ||||
|       .where('ownerId', '=', asUuid(userId)) | ||||
|       .where('isVisible', '=', true) | ||||
|       .where('isArchived', '=', false) | ||||
|       .where('visibility', '=', AssetVisibility.TIMELINE) | ||||
|       .where('deletedAt', 'is', null) | ||||
|       .where('fileCreatedAt', 'is not', null) | ||||
|       .where('fileModifiedAt', 'is not', null) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import { AssetVisibility } from 'src/enum'; | ||||
| import { asset_face_source_type, assets_status_enum } from 'src/schema/enums'; | ||||
| import { | ||||
|   assets_delete_audit, | ||||
| @@ -45,7 +46,12 @@ import { UserAuditTable } from 'src/schema/tables/user-audit.table'; | ||||
| import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; | ||||
| import { UserTable } from 'src/schema/tables/user.table'; | ||||
| import { VersionHistoryTable } from 'src/schema/tables/version-history.table'; | ||||
| import { ConfigurationParameter, Database, Extensions } from 'src/sql-tools'; | ||||
| import { ConfigurationParameter, Database, Extensions, registerEnum } from 'src/sql-tools'; | ||||
|  | ||||
| export const asset_visibility_enum = registerEnum({ | ||||
|   name: 'asset_visibility_enum', | ||||
|   values: Object.values(AssetVisibility), | ||||
| }); | ||||
|  | ||||
| @Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql']) | ||||
| @ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' }) | ||||
|   | ||||
| @@ -0,0 +1,37 @@ | ||||
| import { Kysely, sql } from 'kysely'; | ||||
|  | ||||
| export async function up(db: Kysely<any>): Promise<void> { | ||||
|   await sql`CREATE TYPE "asset_visibility_enum" AS ENUM ('archive','timeline','hidden');`.execute(db); | ||||
|   await sql`ALTER TABLE "assets" | ||||
|     ADD "visibility" asset_visibility_enum NOT NULL DEFAULT 'timeline';`.execute(db); | ||||
|  | ||||
|   await sql` | ||||
|     UPDATE "assets" | ||||
|     SET "visibility" = CASE | ||||
|                         WHEN "isArchived" THEN 'archive'::asset_visibility_enum | ||||
|                         WHEN "isVisible" THEN 'timeline'::asset_visibility_enum | ||||
|                         ELSE 'hidden'::asset_visibility_enum | ||||
|                       END; | ||||
|   `.execute(db); | ||||
|  | ||||
|   await sql`ALTER TABLE "assets" DROP COLUMN "isVisible";`.execute(db); | ||||
|   await sql`ALTER TABLE "assets" DROP COLUMN "isArchived";`.execute(db); | ||||
| } | ||||
|  | ||||
| export async function down(db: Kysely<any>): Promise<void> { | ||||
|   await sql`ALTER TABLE "assets" ADD COLUMN "isArchived" BOOLEAN NOT NULL DEFAULT FALSE;`.execute(db); | ||||
|   await sql`ALTER TABLE "assets" ADD COLUMN "isVisible" BOOLEAN NOT NULL DEFAULT TRUE;`.execute(db); | ||||
|  | ||||
|   await sql` | ||||
|       UPDATE "assets" | ||||
|       SET | ||||
|         "isArchived" = ("visibility" = 'archive'::asset_visibility_enum), | ||||
|         "isVisible" = CASE | ||||
|                         WHEN "visibility" = 'timeline'::asset_visibility_enum THEN TRUE | ||||
|                         WHEN "visibility" = 'archive'::asset_visibility_enum THEN TRUE | ||||
|                         ELSE FALSE | ||||
|                       END; | ||||
|     `.execute(db); | ||||
|   await sql`ALTER TABLE "assets" DROP COLUMN "visibility";`.execute(db); | ||||
|   await sql`DROP TYPE "asset_visibility_enum";`.execute(db); | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; | ||||
| import { AssetStatus, AssetType } from 'src/enum'; | ||||
| import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { asset_visibility_enum } from 'src/schema'; | ||||
| import { assets_status_enum } from 'src/schema/enums'; | ||||
| import { assets_delete_audit } from 'src/schema/functions'; | ||||
| import { LibraryTable } from 'src/schema/tables/library.table'; | ||||
| @@ -95,9 +96,6 @@ export class AssetTable { | ||||
|   @Column({ type: 'bytea', index: true }) | ||||
|   checksum!: Buffer; // sha1 checksum | ||||
|  | ||||
|   @Column({ type: 'boolean', default: true }) | ||||
|   isVisible!: boolean; | ||||
|  | ||||
|   @ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' }) | ||||
|   livePhotoVideoId!: string | null; | ||||
|  | ||||
| @@ -107,9 +105,6 @@ export class AssetTable { | ||||
|   @CreateDateColumn() | ||||
|   createdAt!: Date; | ||||
|  | ||||
|   @Column({ type: 'boolean', default: false }) | ||||
|   isArchived!: boolean; | ||||
|  | ||||
|   @Column({ index: true }) | ||||
|   originalFileName!: string; | ||||
|  | ||||
| @@ -145,4 +140,7 @@ export class AssetTable { | ||||
|  | ||||
|   @UpdateIdColumn({ indexName: 'IDX_assets_update_id' }) | ||||
|   updateId?: string; | ||||
|  | ||||
|   @Column({ enum: asset_visibility_enum, default: AssetVisibility.TIMELINE }) | ||||
|   visibility!: AssetVisibility; | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import { AssetFile } from 'src/database'; | ||||
| import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; | ||||
| import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto'; | ||||
| import { MapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AssetFileType, AssetStatus, AssetType, CacheControl, JobName } from 'src/enum'; | ||||
| import { AssetFileType, AssetStatus, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum'; | ||||
| import { AuthRequest } from 'src/middleware/auth.guard'; | ||||
| import { AssetMediaService } from 'src/services/asset-media.service'; | ||||
| import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; | ||||
| @@ -142,7 +142,6 @@ const createDto = Object.freeze({ | ||||
|   fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|   fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|   isFavorite: false, | ||||
|   isArchived: false, | ||||
|   duration: '0:00:00.000000', | ||||
| }) as AssetMediaCreateDto; | ||||
|  | ||||
| @@ -164,7 +163,6 @@ const assetEntity = Object.freeze({ | ||||
|   fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|   updatedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|   isFavorite: false, | ||||
|   isArchived: false, | ||||
|   encodedVideoPath: '', | ||||
|   duration: '0:00:00.000000', | ||||
|   files: [] as AssetFile[], | ||||
| @@ -437,7 +435,10 @@ describe(AssetMediaService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should hide the linked motion asset', async () => { | ||||
|       mocks.asset.getById.mockResolvedValueOnce({ ...assetStub.livePhotoMotionAsset, isVisible: true }); | ||||
|       mocks.asset.getById.mockResolvedValueOnce({ | ||||
|         ...assetStub.livePhotoMotionAsset, | ||||
|         visibility: AssetVisibility.TIMELINE, | ||||
|       }); | ||||
|       mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); | ||||
|  | ||||
|       await expect( | ||||
| @@ -452,7 +453,10 @@ describe(AssetMediaService.name, () => { | ||||
|       }); | ||||
|  | ||||
|       expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', isVisible: false }); | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ | ||||
|         id: 'live-photo-motion-asset', | ||||
|         visibility: AssetVisibility.HIDDEN, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should handle a sidecar file', async () => { | ||||
|   | ||||
| @@ -21,7 +21,7 @@ import { | ||||
|   UploadFieldName, | ||||
| } from 'src/dtos/asset-media.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { AssetStatus, AssetType, CacheControl, JobName, Permission, StorageFolder } from 'src/enum'; | ||||
| import { AssetStatus, AssetType, AssetVisibility, CacheControl, JobName, Permission, StorageFolder } from 'src/enum'; | ||||
| import { AuthRequest } from 'src/middleware/auth.guard'; | ||||
| import { BaseService } from 'src/services/base.service'; | ||||
| import { UploadFile } from 'src/types'; | ||||
| @@ -146,7 +146,6 @@ export class AssetMediaService extends BaseService { | ||||
|           { userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId }, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       const asset = await this.create(auth.user.id, dto, file, sidecarFile); | ||||
|  | ||||
|       await this.userRepository.updateUsage(auth.user.id, file.size); | ||||
| @@ -416,9 +415,8 @@ export class AssetMediaService extends BaseService { | ||||
|  | ||||
|       type: mimeTypes.assetType(file.originalPath), | ||||
|       isFavorite: dto.isFavorite, | ||||
|       isArchived: dto.isArchived ?? false, | ||||
|       duration: dto.duration || null, | ||||
|       isVisible: dto.isVisible ?? true, | ||||
|       visibility: dto.visibility ?? AssetVisibility.TIMELINE, | ||||
|       livePhotoVideoId: dto.livePhotoVideoId, | ||||
|       originalFileName: file.originalName, | ||||
|       sidecarPath: sidecarFile?.originalPath, | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common'; | ||||
| import { DateTime } from 'luxon'; | ||||
| import { MapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; | ||||
| import { AssetStatus, AssetType, JobName, JobStatus } from 'src/enum'; | ||||
| import { AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; | ||||
| import { AssetStats } from 'src/repositories/asset.repository'; | ||||
| import { AssetService } from 'src/services/asset.service'; | ||||
| import { assetStub } from 'test/fixtures/asset.stub'; | ||||
| @@ -46,14 +46,22 @@ describe(AssetService.name, () => { | ||||
|   describe('getStatistics', () => { | ||||
|     it('should get the statistics for a user, excluding archived assets', async () => { | ||||
|       mocks.asset.getStatistics.mockResolvedValue(stats); | ||||
|       await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse); | ||||
|       expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: false }); | ||||
|       await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.TIMELINE })).resolves.toEqual( | ||||
|         statResponse, | ||||
|       ); | ||||
|       expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { | ||||
|         visibility: AssetVisibility.TIMELINE, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should get the statistics for a user for archived assets', async () => { | ||||
|       mocks.asset.getStatistics.mockResolvedValue(stats); | ||||
|       await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse); | ||||
|       expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: true }); | ||||
|       await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.ARCHIVE })).resolves.toEqual( | ||||
|         statResponse, | ||||
|       ); | ||||
|       expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { | ||||
|         visibility: AssetVisibility.ARCHIVE, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should get the statistics for a user for favorite assets', async () => { | ||||
| @@ -192,9 +200,9 @@ describe(AssetService.name, () => { | ||||
|  | ||||
|   describe('update', () => { | ||||
|     it('should require asset write access for the id', async () => { | ||||
|       await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf( | ||||
|         BadRequestException, | ||||
|       ); | ||||
|       await expect( | ||||
|         sut.update(authStub.admin, 'asset-1', { visibility: AssetVisibility.TIMELINE }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
|  | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
| @@ -242,7 +250,10 @@ describe(AssetService.name, () => { | ||||
|         id: assetStub.livePhotoStillAsset.id, | ||||
|         livePhotoVideoId: assetStub.livePhotoMotionAsset.id, | ||||
|       }); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true }); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith({ | ||||
|         id: assetStub.livePhotoMotionAsset.id, | ||||
|         visibility: AssetVisibility.TIMELINE, | ||||
|       }); | ||||
|       expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', { | ||||
|         assetId: assetStub.livePhotoMotionAsset.id, | ||||
|         userId: userStub.admin.id, | ||||
| @@ -263,7 +274,10 @@ describe(AssetService.name, () => { | ||||
|         id: assetStub.livePhotoStillAsset.id, | ||||
|         livePhotoVideoId: assetStub.livePhotoMotionAsset.id, | ||||
|       }); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true }); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith({ | ||||
|         id: assetStub.livePhotoMotionAsset.id, | ||||
|         visibility: AssetVisibility.TIMELINE, | ||||
|       }); | ||||
|       expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', { | ||||
|         assetId: assetStub.livePhotoMotionAsset.id, | ||||
|         userId: userStub.admin.id, | ||||
| @@ -284,7 +298,10 @@ describe(AssetService.name, () => { | ||||
|         id: assetStub.livePhotoStillAsset.id, | ||||
|         livePhotoVideoId: assetStub.livePhotoMotionAsset.id, | ||||
|       }); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true }); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith({ | ||||
|         id: assetStub.livePhotoMotionAsset.id, | ||||
|         visibility: AssetVisibility.TIMELINE, | ||||
|       }); | ||||
|       expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', { | ||||
|         assetId: assetStub.livePhotoMotionAsset.id, | ||||
|         userId: userStub.admin.id, | ||||
| @@ -296,7 +313,7 @@ describe(AssetService.name, () => { | ||||
|       mocks.asset.getById.mockResolvedValueOnce({ | ||||
|         ...assetStub.livePhotoMotionAsset, | ||||
|         ownerId: authStub.admin.user.id, | ||||
|         isVisible: true, | ||||
|         visibility: AssetVisibility.TIMELINE, | ||||
|       }); | ||||
|       mocks.asset.getById.mockResolvedValueOnce(assetStub.image); | ||||
|       mocks.asset.update.mockResolvedValue(assetStub.image); | ||||
| @@ -305,7 +322,10 @@ describe(AssetService.name, () => { | ||||
|         livePhotoVideoId: assetStub.livePhotoMotionAsset.id, | ||||
|       }); | ||||
|  | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false }); | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ | ||||
|         id: assetStub.livePhotoMotionAsset.id, | ||||
|         visibility: AssetVisibility.HIDDEN, | ||||
|       }); | ||||
|       expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', { | ||||
|         assetId: assetStub.livePhotoMotionAsset.id, | ||||
|         userId: userStub.admin.id, | ||||
| @@ -335,7 +355,10 @@ describe(AssetService.name, () => { | ||||
|         id: assetStub.livePhotoStillAsset.id, | ||||
|         livePhotoVideoId: null, | ||||
|       }); | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true }); | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ | ||||
|         id: assetStub.livePhotoMotionAsset.id, | ||||
|         visibility: assetStub.livePhotoStillAsset.visibility, | ||||
|       }); | ||||
|       expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', { | ||||
|         assetId: assetStub.livePhotoMotionAsset.id, | ||||
|         userId: userStub.admin.id, | ||||
| @@ -361,7 +384,6 @@ describe(AssetService.name, () => { | ||||
|       await expect( | ||||
|         sut.updateAll(authStub.admin, { | ||||
|           ids: ['asset-1'], | ||||
|           isArchived: false, | ||||
|         }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
|     }); | ||||
| @@ -369,9 +391,11 @@ describe(AssetService.name, () => { | ||||
|     it('should update all assets', async () => { | ||||
|       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); | ||||
|  | ||||
|       await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true }); | ||||
|       await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.ARCHIVE }); | ||||
|  | ||||
|       expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true }); | ||||
|       expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { | ||||
|         visibility: AssetVisibility.ARCHIVE, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should not update Assets table if no relevant fields are provided', async () => { | ||||
| @@ -381,7 +405,6 @@ describe(AssetService.name, () => { | ||||
|         ids: ['asset-1'], | ||||
|         latitude: 0, | ||||
|         longitude: 0, | ||||
|         isArchived: undefined, | ||||
|         isFavorite: undefined, | ||||
|         duplicateId: undefined, | ||||
|         rating: undefined, | ||||
| @@ -389,14 +412,14 @@ describe(AssetService.name, () => { | ||||
|       expect(mocks.asset.updateAll).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should update Assets table if isArchived field is provided', async () => { | ||||
|     it('should update Assets table if visibility field is provided', async () => { | ||||
|       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); | ||||
|  | ||||
|       await sut.updateAll(authStub.admin, { | ||||
|         ids: ['asset-1'], | ||||
|         latitude: 0, | ||||
|         longitude: 0, | ||||
|         isArchived: undefined, | ||||
|         visibility: undefined, | ||||
|         isFavorite: false, | ||||
|         duplicateId: undefined, | ||||
|         rating: undefined, | ||||
| @@ -416,7 +439,6 @@ describe(AssetService.name, () => { | ||||
|         latitude: 30, | ||||
|         longitude: 50, | ||||
|         dateTimeOriginal, | ||||
|         isArchived: undefined, | ||||
|         isFavorite: false, | ||||
|         duplicateId: undefined, | ||||
|         rating: undefined, | ||||
| @@ -439,7 +461,6 @@ describe(AssetService.name, () => { | ||||
|         ids: ['asset-1'], | ||||
|         latitude: 0, | ||||
|         longitude: 0, | ||||
|         isArchived: undefined, | ||||
|         isFavorite: undefined, | ||||
|         duplicateId: null, | ||||
|         rating: undefined, | ||||
|   | ||||
| @@ -92,8 +92,12 @@ export class AssetService extends BaseService { | ||||
|  | ||||
|     const asset = await this.assetRepository.update({ id, ...rest }); | ||||
|  | ||||
|     if (previousMotion) { | ||||
|       await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id }); | ||||
|     if (previousMotion && asset) { | ||||
|       await onAfterUnlink(repos, { | ||||
|         userId: auth.user.id, | ||||
|         livePhotoVideoId: previousMotion.id, | ||||
|         visibility: asset.visibility, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (!asset) { | ||||
| @@ -115,7 +119,7 @@ export class AssetService extends BaseService { | ||||
|     } | ||||
|  | ||||
|     if ( | ||||
|       options.isArchived !== undefined || | ||||
|       options.visibility !== undefined || | ||||
|       options.isFavorite !== undefined || | ||||
|       options.duplicateId !== undefined || | ||||
|       options.rating !== undefined | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { AssetFileType, AssetType, JobName, JobStatus } from 'src/enum'; | ||||
| import { AssetFileType, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; | ||||
| import { DuplicateService } from 'src/services/duplicate.service'; | ||||
| import { SearchService } from 'src/services/search.service'; | ||||
| import { assetStub } from 'test/fixtures/asset.stub'; | ||||
| @@ -22,11 +22,11 @@ const hasEmbedding = { | ||||
|       updateId: 'update-1', | ||||
|     }, | ||||
|   ], | ||||
|   isVisible: true, | ||||
|   stackId: null, | ||||
|   type: AssetType.IMAGE, | ||||
|   duplicateId: null, | ||||
|   embedding: '[1, 2, 3, 4]', | ||||
|   visibility: AssetVisibility.TIMELINE, | ||||
| }; | ||||
|  | ||||
| const hasDupe = { | ||||
| @@ -207,7 +207,10 @@ describe(SearchService.name, () => { | ||||
|  | ||||
|     it('should skip if asset is not visible', async () => { | ||||
|       const id = assetStub.livePhotoMotionAsset.id; | ||||
|       mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, isVisible: false }); | ||||
|       mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ | ||||
|         ...hasEmbedding, | ||||
|         visibility: AssetVisibility.HIDDEN, | ||||
|       }); | ||||
|  | ||||
|       const result = await sut.handleSearchDuplicates({ id }); | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { OnJob } from 'src/decorators'; | ||||
| import { mapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { DuplicateResponseDto } from 'src/dtos/duplicate.dto'; | ||||
| import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum'; | ||||
| import { AssetFileType, AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum'; | ||||
| import { AssetDuplicateResult } from 'src/repositories/search.repository'; | ||||
| import { BaseService } from 'src/services/base.service'; | ||||
| import { JobItem, JobOf } from 'src/types'; | ||||
| @@ -65,7 +65,7 @@ export class DuplicateService extends BaseService { | ||||
|       return JobStatus.SKIPPED; | ||||
|     } | ||||
|  | ||||
|     if (!asset.isVisible) { | ||||
|     if (asset.visibility == AssetVisibility.HIDDEN) { | ||||
|       this.logger.debug(`Asset ${id} is not visible, skipping`); | ||||
|       return JobStatus.SKIPPED; | ||||
|     } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; | ||||
| import { | ||||
|   AssetType, | ||||
|   AssetVisibility, | ||||
|   BootstrapEventPriority, | ||||
|   ImmichWorker, | ||||
|   JobCommand, | ||||
| @@ -301,7 +302,7 @@ export class JobService extends BaseService { | ||||
|         } | ||||
|  | ||||
|         await this.jobRepository.queueAll(jobs); | ||||
|         if (asset.isVisible) { | ||||
|         if (asset.visibility === AssetVisibility.TIMELINE || asset.visibility === AssetVisibility.ARCHIVE) { | ||||
|           this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset)); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import { | ||||
|   AssetFileType, | ||||
|   AssetPathType, | ||||
|   AssetType, | ||||
|   AssetVisibility, | ||||
|   AudioCodec, | ||||
|   Colorspace, | ||||
|   JobName, | ||||
| @@ -152,7 +153,7 @@ export class MediaService extends BaseService { | ||||
|       return JobStatus.FAILED; | ||||
|     } | ||||
|  | ||||
|     if (!asset.isVisible) { | ||||
|     if (asset.visibility === AssetVisibility.HIDDEN) { | ||||
|       this.logger.verbose(`Thumbnail generation skipped for asset ${id}: not visible`); | ||||
|       return JobStatus.SKIPPED; | ||||
|     } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { Stats } from 'node:fs'; | ||||
| import { constants } from 'node:fs/promises'; | ||||
| import { defaults } from 'src/config'; | ||||
| import { MapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AssetType, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum'; | ||||
| import { AssetType, AssetVisibility, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum'; | ||||
| import { ImmichTags } from 'src/repositories/metadata.repository'; | ||||
| import { MetadataService } from 'src/services/metadata.service'; | ||||
| import { assetStub } from 'test/fixtures/asset.stub'; | ||||
| @@ -504,7 +504,10 @@ describe(MetadataService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should not apply motion photos if asset is video', async () => { | ||||
|       mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true }); | ||||
|       mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ | ||||
|         ...assetStub.livePhotoMotionAsset, | ||||
|         visibility: AssetVisibility.TIMELINE, | ||||
|       }); | ||||
|       mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); | ||||
|  | ||||
|       await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); | ||||
| @@ -513,7 +516,7 @@ describe(MetadataService.name, () => { | ||||
|       expect(mocks.job.queue).not.toHaveBeenCalled(); | ||||
|       expect(mocks.job.queueAll).not.toHaveBeenCalled(); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith( | ||||
|         expect.objectContaining({ assetType: AssetType.VIDEO, isVisible: false }), | ||||
|         expect.objectContaining({ assetType: AssetType.VIDEO, visibility: AssetVisibility.HIDDEN }), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
| @@ -580,7 +583,7 @@ describe(MetadataService.name, () => { | ||||
|         fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, | ||||
|         fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, | ||||
|         id: fileStub.livePhotoMotion.uuid, | ||||
|         isVisible: false, | ||||
|         visibility: AssetVisibility.HIDDEN, | ||||
|         libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, | ||||
|         localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, | ||||
|         originalFileName: 'asset_1.mp4', | ||||
| @@ -638,7 +641,7 @@ describe(MetadataService.name, () => { | ||||
|         fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, | ||||
|         fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, | ||||
|         id: fileStub.livePhotoMotion.uuid, | ||||
|         isVisible: false, | ||||
|         visibility: AssetVisibility.HIDDEN, | ||||
|         libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, | ||||
|         localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, | ||||
|         originalFileName: 'asset_1.mp4', | ||||
| @@ -696,7 +699,7 @@ describe(MetadataService.name, () => { | ||||
|         fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, | ||||
|         fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, | ||||
|         id: fileStub.livePhotoMotion.uuid, | ||||
|         isVisible: false, | ||||
|         visibility: AssetVisibility.HIDDEN, | ||||
|         libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, | ||||
|         localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, | ||||
|         originalFileName: 'asset_1.mp4', | ||||
| @@ -773,14 +776,17 @@ describe(MetadataService.name, () => { | ||||
|         MicroVideoOffset: 1, | ||||
|       }); | ||||
|       mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); | ||||
|       mocks.asset.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true }); | ||||
|       mocks.asset.getByChecksum.mockResolvedValue({ | ||||
|         ...assetStub.livePhotoMotionAsset, | ||||
|         visibility: AssetVisibility.TIMELINE, | ||||
|       }); | ||||
|       const video = randomBytes(512); | ||||
|       mocks.storage.readFile.mockResolvedValue(video); | ||||
|  | ||||
|       await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ | ||||
|         id: assetStub.livePhotoMotionAsset.id, | ||||
|         isVisible: false, | ||||
|         visibility: AssetVisibility.HIDDEN, | ||||
|       }); | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ | ||||
|         id: assetStub.livePhotoStillAsset.id, | ||||
| @@ -1301,7 +1307,9 @@ describe(MetadataService.name, () => { | ||||
|  | ||||
|       expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); | ||||
|       expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled(); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false })); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith( | ||||
|         expect.objectContaining({ visibility: AssetVisibility.HIDDEN }), | ||||
|       ); | ||||
|       expect(mocks.album.removeAsset).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
| @@ -1320,7 +1328,9 @@ describe(MetadataService.name, () => { | ||||
|         libraryId: null, | ||||
|         type: AssetType.IMAGE, | ||||
|       }); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false })); | ||||
|       expect(mocks.asset.update).not.toHaveBeenCalledWith( | ||||
|         expect.objectContaining({ visibility: AssetVisibility.HIDDEN }), | ||||
|       ); | ||||
|       expect(mocks.album.removeAsset).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
| @@ -1342,7 +1352,10 @@ describe(MetadataService.name, () => { | ||||
|         id: assetStub.livePhotoStillAsset.id, | ||||
|         livePhotoVideoId: assetStub.livePhotoMotionAsset.id, | ||||
|       }); | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false }); | ||||
|       expect(mocks.asset.update).toHaveBeenCalledWith({ | ||||
|         id: assetStub.livePhotoMotionAsset.id, | ||||
|         visibility: AssetVisibility.HIDDEN, | ||||
|       }); | ||||
|       expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import { AssetFaces, Exif, Person } from 'src/db'; | ||||
| import { OnEvent, OnJob } from 'src/decorators'; | ||||
| import { | ||||
|   AssetType, | ||||
|   AssetVisibility, | ||||
|   DatabaseLock, | ||||
|   ExifOrientation, | ||||
|   ImmichWorker, | ||||
| @@ -156,7 +157,7 @@ export class MetadataService extends BaseService { | ||||
|     const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset]; | ||||
|     await Promise.all([ | ||||
|       this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }), | ||||
|       this.assetRepository.update({ id: motionAsset.id, isVisible: false }), | ||||
|       this.assetRepository.update({ id: motionAsset.id, visibility: AssetVisibility.HIDDEN }), | ||||
|       this.albumRepository.removeAsset(motionAsset.id), | ||||
|     ]); | ||||
|  | ||||
| @@ -527,8 +528,11 @@ export class MetadataService extends BaseService { | ||||
|         }); | ||||
|  | ||||
|         // Hide the motion photo video asset if it's not already hidden to prepare for linking | ||||
|         if (motionAsset.isVisible) { | ||||
|           await this.assetRepository.update({ id: motionAsset.id, isVisible: false }); | ||||
|         if (motionAsset.visibility === AssetVisibility.TIMELINE) { | ||||
|           await this.assetRepository.update({ | ||||
|             id: motionAsset.id, | ||||
|             visibility: AssetVisibility.HIDDEN, | ||||
|           }); | ||||
|           this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`); | ||||
|         } | ||||
|       } else { | ||||
| @@ -544,7 +548,7 @@ export class MetadataService extends BaseService { | ||||
|           ownerId: asset.ownerId, | ||||
|           originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), | ||||
|           originalFileName: `${path.parse(asset.originalFileName).name}.mp4`, | ||||
|           isVisible: false, | ||||
|           visibility: AssetVisibility.HIDDEN, | ||||
|           deviceAssetId: 'NONE', | ||||
|           deviceId: 'NONE', | ||||
|         }); | ||||
| @@ -863,7 +867,7 @@ export class MetadataService extends BaseService { | ||||
|       return JobStatus.FAILED; | ||||
|     } | ||||
|  | ||||
|     if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) { | ||||
|     if (!isSync && (asset.visibility === AssetVisibility.HIDDEN || asset.sidecarPath) && !asset.isExternal) { | ||||
|       return JobStatus.FAILED; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import { | ||||
| } from 'src/dtos/person.dto'; | ||||
| import { | ||||
|   AssetType, | ||||
|   AssetVisibility, | ||||
|   CacheControl, | ||||
|   ImageFormat, | ||||
|   JobName, | ||||
| @@ -296,7 +297,7 @@ export class PersonService extends BaseService { | ||||
|       return JobStatus.FAILED; | ||||
|     } | ||||
|  | ||||
|     if (!asset.isVisible) { | ||||
|     if (asset.visibility === AssetVisibility.HIDDEN) { | ||||
|       return JobStatus.SKIPPED; | ||||
|     } | ||||
|  | ||||
| @@ -484,7 +485,9 @@ export class PersonService extends BaseService { | ||||
|  | ||||
|     this.logger.debug(`Face ${id} has ${matches.length} matches`); | ||||
|  | ||||
|     const isCore = matches.length >= machineLearning.facialRecognition.minFaces && !face.asset.isArchived; | ||||
|     const isCore = | ||||
|       matches.length >= machineLearning.facialRecognition.minFaces && | ||||
|       face.asset.visibility === AssetVisibility.TIMELINE; | ||||
|     if (!isCore && !deferred) { | ||||
|       this.logger.debug(`Deferring non-core face ${id} for later processing`); | ||||
|       await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } }); | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; | ||||
| import { SystemConfig } from 'src/config'; | ||||
| import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; | ||||
| import { OnEvent, OnJob } from 'src/decorators'; | ||||
| import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; | ||||
| import { AssetVisibility, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; | ||||
| import { ArgOf } from 'src/repositories/event.repository'; | ||||
| import { BaseService } from 'src/services/base.service'; | ||||
| import { JobItem, JobOf } from 'src/types'; | ||||
| @@ -104,7 +104,7 @@ export class SmartInfoService extends BaseService { | ||||
|       return JobStatus.FAILED; | ||||
|     } | ||||
|  | ||||
|     if (!asset.isVisible) { | ||||
|     if (asset.visibility === AssetVisibility.HIDDEN) { | ||||
|       return JobStatus.SKIPPED; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import { | ||||
|   SyncAckSetDto, | ||||
|   SyncStreamDto, | ||||
| } from 'src/dtos/sync.dto'; | ||||
| import { DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum'; | ||||
| import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum'; | ||||
| import { BaseService } from 'src/services/base.service'; | ||||
| import { SyncAck } from 'src/types'; | ||||
| import { getMyPartnerIds } from 'src/utils/asset.util'; | ||||
| @@ -262,7 +262,10 @@ export class SyncService extends BaseService { | ||||
|       needsFullSync: false, | ||||
|       upserted: upserted | ||||
|         // do not return archived assets for partner users | ||||
|         .filter((a) => a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && !a.isArchived)) | ||||
|         .filter( | ||||
|           (a) => | ||||
|             a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && a.visibility === AssetVisibility.TIMELINE), | ||||
|         ) | ||||
|         .map((a) => | ||||
|           mapAsset(a, { | ||||
|             auth, | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { BadRequestException } from '@nestjs/common'; | ||||
| import { AssetVisibility } from 'src/enum'; | ||||
| import { TimeBucketSize } from 'src/repositories/asset.repository'; | ||||
| import { TimelineService } from 'src/services/timeline.service'; | ||||
| import { assetStub } from 'test/fixtures/asset.stub'; | ||||
| @@ -54,7 +55,7 @@ describe(TimelineService.name, () => { | ||||
|         sut.getTimeBucket(authStub.admin, { | ||||
|           size: TimeBucketSize.DAY, | ||||
|           timeBucket: 'bucket', | ||||
|           isArchived: true, | ||||
|           visibility: AssetVisibility.ARCHIVE, | ||||
|           userId: authStub.admin.user.id, | ||||
|         }), | ||||
|       ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); | ||||
| @@ -63,7 +64,7 @@ describe(TimelineService.name, () => { | ||||
|         expect.objectContaining({ | ||||
|           size: TimeBucketSize.DAY, | ||||
|           timeBucket: 'bucket', | ||||
|           isArchived: true, | ||||
|           visibility: AssetVisibility.ARCHIVE, | ||||
|           userIds: [authStub.admin.user.id], | ||||
|         }), | ||||
|       ); | ||||
| @@ -77,7 +78,7 @@ describe(TimelineService.name, () => { | ||||
|         sut.getTimeBucket(authStub.admin, { | ||||
|           size: TimeBucketSize.DAY, | ||||
|           timeBucket: 'bucket', | ||||
|           isArchived: false, | ||||
|           visibility: AssetVisibility.TIMELINE, | ||||
|           userId: authStub.admin.user.id, | ||||
|           withPartners: true, | ||||
|         }), | ||||
| @@ -85,7 +86,7 @@ describe(TimelineService.name, () => { | ||||
|       expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', { | ||||
|         size: TimeBucketSize.DAY, | ||||
|         timeBucket: 'bucket', | ||||
|         isArchived: false, | ||||
|         visibility: AssetVisibility.TIMELINE, | ||||
|         withPartners: true, | ||||
|         userIds: [authStub.admin.user.id], | ||||
|       }); | ||||
| @@ -120,7 +121,7 @@ describe(TimelineService.name, () => { | ||||
|       const buckets = await sut.getTimeBucket(auth, { | ||||
|         size: TimeBucketSize.DAY, | ||||
|         timeBucket: 'bucket', | ||||
|         isArchived: true, | ||||
|         visibility: AssetVisibility.ARCHIVE, | ||||
|         albumId: 'album-id', | ||||
|       }); | ||||
|  | ||||
| @@ -129,7 +130,7 @@ describe(TimelineService.name, () => { | ||||
|       expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', { | ||||
|         size: TimeBucketSize.DAY, | ||||
|         timeBucket: 'bucket', | ||||
|         isArchived: true, | ||||
|         visibility: AssetVisibility.ARCHIVE, | ||||
|         albumId: 'album-id', | ||||
|       }); | ||||
|     }); | ||||
| @@ -154,12 +155,12 @@ describe(TimelineService.name, () => { | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should throw an error if withParners is true and isArchived true or undefined', async () => { | ||||
|     it('should throw an error if withParners is true and visibility true or undefined', async () => { | ||||
|       await expect( | ||||
|         sut.getTimeBucket(authStub.admin, { | ||||
|           size: TimeBucketSize.DAY, | ||||
|           timeBucket: 'bucket', | ||||
|           isArchived: true, | ||||
|           visibility: AssetVisibility.ARCHIVE, | ||||
|           withPartners: true, | ||||
|           userId: authStub.admin.user.id, | ||||
|         }), | ||||
| @@ -169,7 +170,7 @@ describe(TimelineService.name, () => { | ||||
|         sut.getTimeBucket(authStub.admin, { | ||||
|           size: TimeBucketSize.DAY, | ||||
|           timeBucket: 'bucket', | ||||
|           isArchived: undefined, | ||||
|           visibility: undefined, | ||||
|           withPartners: true, | ||||
|           userId: authStub.admin.user.id, | ||||
|         }), | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; | ||||
| import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; | ||||
| import { Permission } from 'src/enum'; | ||||
| import { AssetVisibility, Permission } from 'src/enum'; | ||||
| import { TimeBucketOptions } from 'src/repositories/asset.repository'; | ||||
| import { BaseService } from 'src/services/base.service'; | ||||
| import { getMyPartnerIds } from 'src/utils/asset.util'; | ||||
| @@ -55,7 +55,7 @@ export class TimelineService extends BaseService { | ||||
|  | ||||
|     if (dto.userId) { | ||||
|       await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] }); | ||||
|       if (dto.isArchived !== false) { | ||||
|       if (dto.visibility === AssetVisibility.ARCHIVE) { | ||||
|         await this.requireAccess({ auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] }); | ||||
|       } | ||||
|     } | ||||
| @@ -65,7 +65,7 @@ export class TimelineService extends BaseService { | ||||
|     } | ||||
|  | ||||
|     if (dto.withPartners) { | ||||
|       const requestedArchived = dto.isArchived === true || dto.isArchived === undefined; | ||||
|       const requestedArchived = dto.visibility === AssetVisibility.ARCHIVE || dto.visibility === undefined; | ||||
|       const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; | ||||
|       const requestedTrash = dto.isTrashed === true; | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { AssetFile } from 'src/database'; | ||||
| import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; | ||||
| import { UploadFieldName } from 'src/dtos/asset-media.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { AssetFileType, AssetType, Permission } from 'src/enum'; | ||||
| import { AssetFileType, AssetType, AssetVisibility, Permission } from 'src/enum'; | ||||
| import { AuthRequest } from 'src/middleware/auth.guard'; | ||||
| import { AccessRepository } from 'src/repositories/access.repository'; | ||||
| import { AssetRepository } from 'src/repositories/asset.repository'; | ||||
| @@ -150,8 +150,8 @@ export const onBeforeLink = async ( | ||||
|     throw new BadRequestException('Live photo video does not belong to the user'); | ||||
|   } | ||||
|  | ||||
|   if (motionAsset?.isVisible) { | ||||
|     await assetRepository.update({ id: livePhotoVideoId, isVisible: false }); | ||||
|   if (motionAsset && motionAsset.visibility === AssetVisibility.TIMELINE) { | ||||
|     await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.HIDDEN }); | ||||
|     await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId }); | ||||
|   } | ||||
| }; | ||||
| @@ -174,9 +174,9 @@ export const onBeforeUnlink = async ( | ||||
|  | ||||
| export const onAfterUnlink = async ( | ||||
|   { asset: assetRepository, event: eventRepository }: AssetHookRepositories, | ||||
|   { userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string }, | ||||
|   { userId, livePhotoVideoId, visibility }: { userId: string; livePhotoVideoId: string; visibility: AssetVisibility }, | ||||
| ) => { | ||||
|   await assetRepository.update({ id: livePhotoVideoId, isVisible: true }); | ||||
|   await assetRepository.update({ id: livePhotoVideoId, visibility }); | ||||
|   await eventRepository.emit('asset.show', { assetId: livePhotoVideoId, userId }); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ import { parse } from 'pg-connection-string'; | ||||
| import postgres, { Notice } from 'postgres'; | ||||
| import { columns, Exif, Person } from 'src/database'; | ||||
| import { DB } from 'src/db'; | ||||
| import { AssetFileType, DatabaseExtension, DatabaseSslMode } from 'src/enum'; | ||||
| import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum'; | ||||
| import { TimeBucketSize } from 'src/repositories/asset.repository'; | ||||
| import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; | ||||
| import { DatabaseConnectionParams, VectorExtension } from 'src/types'; | ||||
| @@ -155,6 +155,15 @@ export function toJson<DB, TB extends keyof DB & string, T extends TB | Expressi | ||||
| export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum'; | ||||
| // TODO come up with a better query that only selects the fields we need | ||||
|  | ||||
| export function withDefaultVisibility<O>(qb: SelectQueryBuilder<DB, 'assets', O>) { | ||||
|   return qb.where((qb) => | ||||
|     qb.or([ | ||||
|       qb('assets.visibility', '=', AssetVisibility.TIMELINE), | ||||
|       qb('assets.visibility', '=', AssetVisibility.ARCHIVE), | ||||
|     ]), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) { | ||||
|   return qb | ||||
|     .leftJoin('exif', 'assets.id', 'exif.assetId') | ||||
| @@ -280,12 +289,14 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); | ||||
| /** TODO: This should only be used for search-related queries, not as a general purpose query builder */ | ||||
|  | ||||
| export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) { | ||||
|   options.isArchived ??= options.withArchived ? undefined : false; | ||||
|   options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline); | ||||
|   const visibility = options.visibility == null ? AssetVisibility.TIMELINE : options.visibility; | ||||
|  | ||||
|   return kysely | ||||
|     .withPlugin(joinDeduplicationPlugin) | ||||
|     .selectFrom('assets') | ||||
|     .selectAll('assets') | ||||
|     .where('assets.visibility', '=', visibility) | ||||
|     .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) | ||||
|     .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) | ||||
|     .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) | ||||
| @@ -356,8 +367,6 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild | ||||
|     .$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!)) | ||||
|     .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) | ||||
|     .$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!)) | ||||
|     .$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!)) | ||||
|     .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!)) | ||||
|     .$if(options.isEncoded !== undefined, (qb) => | ||||
|       qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null), | ||||
|     ) | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import { | ||||
|   IsArray, | ||||
|   IsBoolean, | ||||
|   IsDate, | ||||
|   IsEnum, | ||||
|   IsHexColor, | ||||
|   IsNotEmpty, | ||||
|   IsOptional, | ||||
| @@ -29,6 +30,7 @@ import { | ||||
| import { CronJob } from 'cron'; | ||||
| import { DateTime } from 'luxon'; | ||||
| import sanitize from 'sanitize-filename'; | ||||
| import { AssetVisibility } from 'src/enum'; | ||||
| import { isIP, isIPRange } from 'validator'; | ||||
|  | ||||
| @Injectable() | ||||
| @@ -146,6 +148,17 @@ export const ValidateDate = (options?: DateOptions) => { | ||||
|   return applyDecorators(...decorators); | ||||
| }; | ||||
|  | ||||
| type AssetVisibilityOptions = { optional?: boolean }; | ||||
| export const ValidateAssetVisibility = (options?: AssetVisibilityOptions) => { | ||||
|   const { optional } = { optional: false, ...options }; | ||||
|   const decorators = [IsEnum(AssetVisibility), ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility })]; | ||||
|  | ||||
|   if (optional) { | ||||
|     decorators.push(Optional()); | ||||
|   } | ||||
|   return applyDecorators(...decorators); | ||||
| }; | ||||
|  | ||||
| type BooleanOptions = { optional?: boolean }; | ||||
| export const ValidateBoolean = (options?: BooleanOptions) => { | ||||
|   const { optional } = { optional: false, ...options }; | ||||
|   | ||||
							
								
								
									
										65
									
								
								server/test/fixtures/asset.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										65
									
								
								server/test/fixtures/asset.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| import { AssetFace, AssetFile, Exif } from 'src/database'; | ||||
| import { MapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AssetFileType, AssetStatus, AssetType } from 'src/enum'; | ||||
| import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { StorageAsset } from 'src/types'; | ||||
| import { authStub } from 'test/fixtures/auth.stub'; | ||||
| import { fileStub } from 'test/fixtures/file.stub'; | ||||
| @@ -74,9 +74,7 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     sharedLinks: [], | ||||
| @@ -90,6 +88,7 @@ export const assetStub = { | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     updateId: '42', | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   noWebpPath: Object.freeze({ | ||||
| @@ -111,9 +110,7 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     sharedLinks: [], | ||||
| @@ -130,6 +127,7 @@ export const assetStub = { | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     updateId: '42', | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   noThumbhash: Object.freeze({ | ||||
| @@ -151,9 +149,7 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     isExternal: false, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
| @@ -167,6 +163,7 @@ export const assetStub = { | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     updateId: '42', | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   primaryImage: Object.freeze({ | ||||
| @@ -188,9 +185,7 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     isExternal: false, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
| @@ -214,6 +209,7 @@ export const assetStub = { | ||||
|     isOffline: false, | ||||
|     updateId: '42', | ||||
|     libraryId: null, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   image: Object.freeze({ | ||||
| @@ -235,9 +231,7 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2025-01-01T01:02:03.456Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     isExternal: false, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
| @@ -257,6 +251,7 @@ export const assetStub = { | ||||
|     duplicateId: null, | ||||
|     isOffline: false, | ||||
|     stack: null, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   trashed: Object.freeze({ | ||||
| @@ -278,9 +273,7 @@ export const assetStub = { | ||||
|     deletedAt: new Date('2023-02-24T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: false, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     isExternal: false, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
| @@ -299,6 +292,7 @@ export const assetStub = { | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     updateId: '42', | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   trashedOffline: Object.freeze({ | ||||
| @@ -321,10 +315,8 @@ export const assetStub = { | ||||
|     deletedAt: new Date('2023-02-24T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: false, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     libraryId: 'library-id', | ||||
|     isVisible: true, | ||||
|     isExternal: false, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
| @@ -341,6 +333,7 @@ export const assetStub = { | ||||
|     isOffline: true, | ||||
|     stackId: null, | ||||
|     updateId: '42', | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|   archived: Object.freeze({ | ||||
|     id: 'asset-id', | ||||
| @@ -361,9 +354,7 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: true, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     isExternal: false, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
| @@ -382,6 +373,7 @@ export const assetStub = { | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     updateId: '42', | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   external: Object.freeze({ | ||||
| @@ -403,10 +395,8 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     isExternal: true, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     libraryId: 'library-id', | ||||
| @@ -423,6 +413,7 @@ export const assetStub = { | ||||
|     updateId: '42', | ||||
|     stackId: null, | ||||
|     stack: null, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   image1: Object.freeze({ | ||||
| @@ -445,9 +436,7 @@ export const assetStub = { | ||||
|     deletedAt: null, | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     isExternal: false, | ||||
| @@ -464,6 +453,7 @@ export const assetStub = { | ||||
|     stackId: null, | ||||
|     libraryId: null, | ||||
|     stack: null, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   imageFrom2015: Object.freeze({ | ||||
| @@ -485,10 +475,8 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2015-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2015-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     isExternal: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     sharedLinks: [], | ||||
| @@ -501,6 +489,7 @@ export const assetStub = { | ||||
|     deletedAt: null, | ||||
|     duplicateId: null, | ||||
|     isOffline: false, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   video: Object.freeze({ | ||||
| @@ -523,10 +512,8 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     isExternal: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     sharedLinks: [], | ||||
| @@ -543,6 +530,7 @@ export const assetStub = { | ||||
|     updateId: '42', | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   livePhotoMotionAsset: Object.freeze({ | ||||
| @@ -551,7 +539,6 @@ export const assetStub = { | ||||
|     originalPath: fileStub.livePhotoMotion.originalPath, | ||||
|     ownerId: authStub.user1.user.id, | ||||
|     type: AssetType.VIDEO, | ||||
|     isVisible: false, | ||||
|     fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|     fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|     exifInfo: { | ||||
| @@ -559,6 +546,7 @@ export const assetStub = { | ||||
|       timeZone: `America/New_York`, | ||||
|     }, | ||||
|     libraryId: null, | ||||
|     visibility: AssetVisibility.HIDDEN, | ||||
|   } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }), | ||||
|  | ||||
|   livePhotoStillAsset: Object.freeze({ | ||||
| @@ -568,7 +556,6 @@ export const assetStub = { | ||||
|     ownerId: authStub.user1.user.id, | ||||
|     type: AssetType.IMAGE, | ||||
|     livePhotoVideoId: 'live-photo-motion-asset', | ||||
|     isVisible: true, | ||||
|     fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|     fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|     exifInfo: { | ||||
| @@ -577,6 +564,7 @@ export const assetStub = { | ||||
|     }, | ||||
|     files, | ||||
|     faces: [] as AssetFace[], | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   } as MapAsset & { faces: AssetFace[] }), | ||||
|  | ||||
|   livePhotoWithOriginalFileName: Object.freeze({ | ||||
| @@ -587,7 +575,6 @@ export const assetStub = { | ||||
|     ownerId: authStub.user1.user.id, | ||||
|     type: AssetType.IMAGE, | ||||
|     livePhotoVideoId: 'live-photo-motion-asset', | ||||
|     isVisible: true, | ||||
|     fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|     fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|     exifInfo: { | ||||
| @@ -596,6 +583,7 @@ export const assetStub = { | ||||
|     }, | ||||
|     libraryId: null, | ||||
|     faces: [] as AssetFace[], | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   } as MapAsset & { faces: AssetFace[] }), | ||||
|  | ||||
|   withLocation: Object.freeze({ | ||||
| @@ -618,10 +606,8 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-22T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2020-12-31T23:59:00.000Z'), | ||||
|     isFavorite: false, | ||||
|     isArchived: false, | ||||
|     isExternal: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     updateId: 'foo', | ||||
| @@ -642,6 +628,7 @@ export const assetStub = { | ||||
|     duplicateId: null, | ||||
|     isOffline: false, | ||||
|     tags: [], | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   sidecar: Object.freeze({ | ||||
| @@ -663,10 +650,8 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     isExternal: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     sharedLinks: [], | ||||
| @@ -679,6 +664,7 @@ export const assetStub = { | ||||
|     updateId: 'foo', | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   sidecarWithoutExt: Object.freeze({ | ||||
| @@ -700,10 +686,8 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     isExternal: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     sharedLinks: [], | ||||
| @@ -713,6 +697,7 @@ export const assetStub = { | ||||
|     deletedAt: null, | ||||
|     duplicateId: null, | ||||
|     isOffline: false, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   hasEncodedVideo: Object.freeze({ | ||||
| @@ -735,10 +720,8 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     isExternal: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     sharedLinks: [], | ||||
| @@ -754,6 +737,7 @@ export const assetStub = { | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     stack: null, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   hasFileExtension: Object.freeze({ | ||||
| @@ -775,10 +759,8 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     isExternal: true, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
|     libraryId: 'library-id', | ||||
| @@ -792,6 +774,7 @@ export const assetStub = { | ||||
|     } as Exif, | ||||
|     duplicateId: null, | ||||
|     isOffline: false, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   imageDng: Object.freeze({ | ||||
| @@ -813,9 +796,7 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     isExternal: false, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
| @@ -834,6 +815,7 @@ export const assetStub = { | ||||
|     updateId: '42', | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
|  | ||||
|   imageHif: Object.freeze({ | ||||
| @@ -855,9 +837,7 @@ export const assetStub = { | ||||
|     updatedAt: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     localDateTime: new Date('2023-02-23T05:06:29.716Z'), | ||||
|     isFavorite: true, | ||||
|     isArchived: false, | ||||
|     duration: null, | ||||
|     isVisible: true, | ||||
|     isExternal: false, | ||||
|     livePhotoVideo: null, | ||||
|     livePhotoVideoId: null, | ||||
| @@ -876,5 +856,6 @@ export const assetStub = { | ||||
|     updateId: '42', | ||||
|     libraryId: null, | ||||
|     stackId: null, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }), | ||||
| }; | ||||
|   | ||||
							
								
								
									
										4
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -4,7 +4,7 @@ import { AssetResponseDto, MapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { ExifResponseDto } from 'src/dtos/exif.dto'; | ||||
| import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; | ||||
| import { mapUser } from 'src/dtos/user.dto'; | ||||
| import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum'; | ||||
| import { AssetOrder, AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum'; | ||||
| import { assetStub } from 'test/fixtures/asset.stub'; | ||||
| import { authStub } from 'test/fixtures/auth.stub'; | ||||
| import { userStub } from 'test/fixtures/user.stub'; | ||||
| @@ -206,7 +206,6 @@ export const sharedLinkStub = { | ||||
|           thumbhash: null, | ||||
|           encodedVideoPath: '', | ||||
|           duration: null, | ||||
|           isVisible: true, | ||||
|           livePhotoVideo: null, | ||||
|           livePhotoVideoId: null, | ||||
|           originalFileName: 'asset_1.jpeg', | ||||
| @@ -251,6 +250,7 @@ export const sharedLinkStub = { | ||||
|           updateId: '42', | ||||
|           libraryId: null, | ||||
|           stackId: null, | ||||
|           visibility: AssetVisibility.TIMELINE, | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { createHash, randomBytes } from 'node:crypto'; | ||||
| import { Writable } from 'node:stream'; | ||||
| import { AssetFace } from 'src/database'; | ||||
| import { AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db'; | ||||
| import { AssetType, SourceType } from 'src/enum'; | ||||
| import { AssetType, AssetVisibility, SourceType } from 'src/enum'; | ||||
| import { ActivityRepository } from 'src/repositories/activity.repository'; | ||||
| import { AlbumRepository } from 'src/repositories/album.repository'; | ||||
| import { AssetJobRepository } from 'src/repositories/asset-job.repository'; | ||||
| @@ -227,16 +227,37 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => { | ||||
|  | ||||
|     case 'database': { | ||||
|       return automock(DatabaseRepository, { | ||||
|         args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }], | ||||
|         args: [ | ||||
|           undefined, | ||||
|           { | ||||
|             setContext: () => {}, | ||||
|           }, | ||||
|           { getEnv: () => ({ database: { vectorExtension: '' } }) }, | ||||
|         ], | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     case 'email': { | ||||
|       return automock(EmailRepository, { args: [{ setContext: () => {} }] }); | ||||
|       return automock(EmailRepository, { | ||||
|         args: [ | ||||
|           { | ||||
|             setContext: () => {}, | ||||
|           }, | ||||
|         ], | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     case 'job': { | ||||
|       return automock(JobRepository, { args: [undefined, undefined, undefined, { setContext: () => {} }] }); | ||||
|       return automock(JobRepository, { | ||||
|         args: [ | ||||
|           undefined, | ||||
|           undefined, | ||||
|           undefined, | ||||
|           { | ||||
|             setContext: () => {}, | ||||
|           }, | ||||
|         ], | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     case 'logger': { | ||||
| @@ -345,11 +366,11 @@ const assetInsert = (asset: Partial<Insertable<Assets>> = {}) => { | ||||
|     type: AssetType.IMAGE, | ||||
|     originalPath: '/path/to/something.jpg', | ||||
|     ownerId: '@immich.cloud', | ||||
|     isVisible: true, | ||||
|     isFavorite: false, | ||||
|     fileCreatedAt: now, | ||||
|     fileModifiedAt: now, | ||||
|     localDateTime: now, | ||||
|     visibility: AssetVisibility.TIMELINE, | ||||
|   }; | ||||
|  | ||||
|   return { | ||||
|   | ||||
| @@ -456,9 +456,9 @@ describe(SyncService.name, () => { | ||||
|               fileCreatedAt: asset.fileCreatedAt, | ||||
|               fileModifiedAt: asset.fileModifiedAt, | ||||
|               isFavorite: asset.isFavorite, | ||||
|               isVisible: asset.isVisible, | ||||
|               localDateTime: asset.localDateTime, | ||||
|               type: asset.type, | ||||
|               visibility: asset.visibility, | ||||
|             }, | ||||
|             type: 'AssetV1', | ||||
|           }, | ||||
| @@ -573,9 +573,9 @@ describe(SyncService.name, () => { | ||||
|               fileCreatedAt: date, | ||||
|               fileModifiedAt: date, | ||||
|               isFavorite: false, | ||||
|               isVisible: true, | ||||
|               localDateTime: date, | ||||
|               type: asset.type, | ||||
|               visibility: asset.visibility, | ||||
|             }, | ||||
|             type: SyncEntityType.PartnerAssetV1, | ||||
|           }, | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import { | ||||
| } from 'src/database'; | ||||
| import { MapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum'; | ||||
| import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserStatus } from 'src/enum'; | ||||
| import { OnThisDayData } from 'src/types'; | ||||
|  | ||||
| export const newUuid = () => randomUUID() as string; | ||||
| @@ -202,11 +202,9 @@ const assetFactory = (asset: Partial<MapAsset> = {}) => ({ | ||||
|   encodedVideoPath: null, | ||||
|   fileCreatedAt: newDate(), | ||||
|   fileModifiedAt: newDate(), | ||||
|   isArchived: false, | ||||
|   isExternal: false, | ||||
|   isFavorite: false, | ||||
|   isOffline: false, | ||||
|   isVisible: true, | ||||
|   libraryId: null, | ||||
|   livePhotoVideoId: null, | ||||
|   localDateTime: newDate(), | ||||
| @@ -217,6 +215,7 @@ const assetFactory = (asset: Partial<MapAsset> = {}) => ({ | ||||
|   stackId: null, | ||||
|   thumbhash: null, | ||||
|   type: AssetType.IMAGE, | ||||
|   visibility: AssetVisibility.TIMELINE, | ||||
|   ...asset, | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <script lang="ts" module> | ||||
|   import type { SearchLocationFilter } from './search-location-section.svelte'; | ||||
|   import type { SearchDisplayFilters } from './search-display-section.svelte'; | ||||
|   import type { SearchDateFilter } from './search-date-section.svelte'; | ||||
|   import { MediaType, QueryType, validQueryTypes } from '$lib/constants'; | ||||
|   import type { SearchDateFilter } from './search-date-section.svelte'; | ||||
|   import type { SearchDisplayFilters } from './search-display-section.svelte'; | ||||
|   import type { SearchLocationFilter } from './search-location-section.svelte'; | ||||
|  | ||||
|   export type SearchFilter = { | ||||
|     query: string; | ||||
| @@ -19,24 +19,24 @@ | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||
|   import { preferences } from '$lib/stores/user.store'; | ||||
|   import { parseUtcDate } from '$lib/utils/date-time'; | ||||
|   import { generateId } from '$lib/utils/generate-id'; | ||||
|   import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk'; | ||||
|   import { Button } from '@immich/ui'; | ||||
|   import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk'; | ||||
|   import SearchPeopleSection from './search-people-section.svelte'; | ||||
|   import SearchTagsSection from './search-tags-section.svelte'; | ||||
|   import SearchLocationSection from './search-location-section.svelte'; | ||||
|   import { mdiTune } from '@mdi/js'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import { SvelteSet } from 'svelte/reactivity'; | ||||
|   import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte'; | ||||
|   import SearchDateSection from './search-date-section.svelte'; | ||||
|   import SearchMediaSection from './search-media-section.svelte'; | ||||
|   import SearchRatingsSection from './search-ratings-section.svelte'; | ||||
|   import { parseUtcDate } from '$lib/utils/date-time'; | ||||
|   import SearchDisplaySection from './search-display-section.svelte'; | ||||
|   import SearchLocationSection from './search-location-section.svelte'; | ||||
|   import SearchMediaSection from './search-media-section.svelte'; | ||||
|   import SearchPeopleSection from './search-people-section.svelte'; | ||||
|   import SearchRatingsSection from './search-ratings-section.svelte'; | ||||
|   import SearchTagsSection from './search-tags-section.svelte'; | ||||
|   import SearchTextSection from './search-text-section.svelte'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||
|   import { mdiTune } from '@mdi/js'; | ||||
|   import { generateId } from '$lib/utils/generate-id'; | ||||
|   import { SvelteSet } from 'svelte/reactivity'; | ||||
|   import { preferences } from '$lib/stores/user.store'; | ||||
|  | ||||
|   interface Props { | ||||
|     searchQuery: MetadataSearchDto | SmartSearchDto; | ||||
| @@ -83,7 +83,7 @@ | ||||
|       takenBefore: searchQuery.takenBefore ? toStartOfDayDate(searchQuery.takenBefore) : undefined, | ||||
|     }, | ||||
|     display: { | ||||
|       isArchive: searchQuery.isArchived, | ||||
|       isArchive: searchQuery.visibility === AssetVisibility.Archive, | ||||
|       isFavorite: searchQuery.isFavorite, | ||||
|       isNotInAlbum: 'isNotInAlbum' in searchQuery ? searchQuery.isNotInAlbum : undefined, | ||||
|     }, | ||||
| @@ -132,7 +132,7 @@ | ||||
|       model: filter.camera.model, | ||||
|       takenAfter: parseOptionalDate(filter.date.takenAfter)?.startOf('day').toISO() || undefined, | ||||
|       takenBefore: parseOptionalDate(filter.date.takenBefore)?.endOf('day').toISO() || undefined, | ||||
|       isArchived: filter.display.isArchive || undefined, | ||||
|       visibility: filter.display.isArchive ? AssetVisibility.Archive : undefined, | ||||
|       isFavorite: filter.display.isFavorite || undefined, | ||||
|       isNotInAlbum: filter.display.isNotInAlbum || undefined, | ||||
|       personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined, | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| <script lang="ts"> | ||||
|   import { locale } from '$lib/stores/preferences.store'; | ||||
|   import { | ||||
|     AssetVisibility, | ||||
|     getAlbumStatistics, | ||||
|     getAssetStatistics, | ||||
|     type AlbumStatisticsResponseDto, | ||||
| @@ -41,9 +42,9 @@ | ||||
|  | ||||
|   const getUsage = async () => { | ||||
|     [timelineStats, favoriteStats, archiveStats, trashStats, albumStats] = await Promise.all([ | ||||
|       getAssetStatistics({ isArchived: false }), | ||||
|       getAssetStatistics({ visibility: AssetVisibility.Timeline }), | ||||
|       getAssetStatistics({ isFavorite: true }), | ||||
|       getAssetStatistics({ isArchived: true }), | ||||
|       getAssetStatistics({ visibility: AssetVisibility.Archive }), | ||||
|       getAssetStatistics({ isTrashed: true }), | ||||
|       getAlbumStatistics(), | ||||
|     ]); | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import { formatDateGroupTitle, fromLocalDateTime } from '$lib/utils/timeline-uti | ||||
| import { TUNABLES } from '$lib/utils/tunables'; | ||||
| import { | ||||
|   AssetOrder, | ||||
|   AssetVisibility, | ||||
|   getAssetInfo, | ||||
|   getTimeBucket, | ||||
|   getTimeBuckets, | ||||
| @@ -1375,7 +1376,7 @@ export class AssetStore { | ||||
|  | ||||
|   isExcluded(asset: AssetResponseDto) { | ||||
|     return ( | ||||
|       isMismatched(this.#options.isArchived, asset.isArchived) || | ||||
|       isMismatched(this.#options.visibility === AssetVisibility.Archive, asset.isArchived) || | ||||
|       isMismatched(this.#options.isFavorite, asset.isFavorite) || | ||||
|       isMismatched(this.#options.isTrashed, asset.isTrashed) | ||||
|     ); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { goto } from '$app/navigation'; | ||||
| import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte'; | ||||
| import type { InterpolationValues } from '$lib/components/i18n/format-message'; | ||||
| import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; | ||||
| import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; | ||||
| import { AppRoute } from '$lib/constants'; | ||||
| import { authManager } from '$lib/managers/auth-manager.svelte'; | ||||
| import { downloadManager } from '$lib/managers/download-manager.svelte'; | ||||
| @@ -15,6 +15,7 @@ import { getFormatter } from '$lib/utils/i18n'; | ||||
| import { navigate } from '$lib/utils/navigation'; | ||||
| import { | ||||
|   addAssetsToAlbum as addAssets, | ||||
|   AssetVisibility, | ||||
|   createStack, | ||||
|   deleteAssets, | ||||
|   deleteStacks, | ||||
| @@ -507,7 +508,7 @@ export const toggleArchive = async (asset: AssetResponseDto) => { | ||||
|     const data = await updateAsset({ | ||||
|       id: asset.id, | ||||
|       updateAssetDto: { | ||||
|         isArchived: !asset.isArchived, | ||||
|         visibility: asset.isArchived ? AssetVisibility.Timeline : AssetVisibility.Archive, | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
| @@ -531,7 +532,9 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean | ||||
|  | ||||
|   try { | ||||
|     if (ids.length > 0) { | ||||
|       await updateAssets({ assetBulkUpdateDto: { ids, isArchived } }); | ||||
|       await updateAssets({ | ||||
|         assetBulkUpdateDto: { ids, visibility: isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline }, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     for (const asset of assets) { | ||||
|   | ||||
| @@ -372,7 +372,10 @@ | ||||
|     if (viewMode === AlbumPageViewMode.VIEW) { | ||||
|       void assetStore.updateOptions({ albumId, order: albumOrder }); | ||||
|     } else if (viewMode === AlbumPageViewMode.SELECT_ASSETS) { | ||||
|       void assetStore.updateOptions({ isArchived: false, withPartners: true, timelineAlbumId: albumId }); | ||||
|       void assetStore.updateOptions({ | ||||
|         withPartners: true, | ||||
|         timelineAlbumId: albumId, | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
| @@ -385,9 +388,6 @@ | ||||
|     activityManager.reset(); | ||||
|     assetStore.destroy(); | ||||
|   }); | ||||
|   // let timelineStore = new AssetStore(); | ||||
|   // $effect(() => void timelineStore.updateOptions({ isArchived: false, withPartners: true, timelineAlbumId: albumId })); | ||||
|   // onDestroy(() => timelineStore.destroy()); | ||||
|  | ||||
|   let isOwned = $derived($user.id == album.ownerId); | ||||
|  | ||||
|   | ||||
| @@ -8,17 +8,18 @@ | ||||
|   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; | ||||
|   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; | ||||
|   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; | ||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||
|   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||
|   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; | ||||
|   import { AssetAction } from '$lib/constants'; | ||||
|  | ||||
|   import type { PageData } from './$types'; | ||||
|   import { mdiPlus, mdiDotsVertical } from '@mdi/js'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import { onDestroy } from 'svelte'; | ||||
|   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||
|   import { AssetStore } from '$lib/stores/assets-store.svelte'; | ||||
|   import { AssetVisibility } from '@immich/sdk'; | ||||
|   import { mdiDotsVertical, mdiPlus } from '@mdi/js'; | ||||
|   import { onDestroy } from 'svelte'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import type { PageData } from './$types'; | ||||
|  | ||||
|   interface Props { | ||||
|     data: PageData; | ||||
| @@ -26,7 +27,7 @@ | ||||
|  | ||||
|   let { data }: Props = $props(); | ||||
|   const assetStore = new AssetStore(); | ||||
|   void assetStore.updateOptions({ isArchived: true }); | ||||
|   void assetStore.updateOptions({ visibility: AssetVisibility.Archive }); | ||||
|   onDestroy(() => assetStore.destroy()); | ||||
|  | ||||
|   const assetInteraction = new AssetInteraction(); | ||||
|   | ||||
| @@ -9,19 +9,19 @@ | ||||
|   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; | ||||
|   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; | ||||
|   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; | ||||
|   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; | ||||
|   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; | ||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||
|   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||
|   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; | ||||
|   import { AssetAction } from '$lib/constants'; | ||||
|   import { AssetStore } from '$lib/stores/assets-store.svelte'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import { mdiDotsVertical, mdiPlus } from '@mdi/js'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import { onDestroy } from 'svelte'; | ||||
|   import { preferences } from '$lib/stores/user.store'; | ||||
|   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; | ||||
|   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||
|   import { AssetStore } from '$lib/stores/assets-store.svelte'; | ||||
|   import { preferences } from '$lib/stores/user.store'; | ||||
|   import { mdiDotsVertical, mdiPlus } from '@mdi/js'; | ||||
|   import { onDestroy } from 'svelte'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import type { PageData } from './$types'; | ||||
|  | ||||
|   interface Props { | ||||
|     data: PageData; | ||||
|   | ||||
| @@ -4,16 +4,17 @@ | ||||
|   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; | ||||
|   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; | ||||
|   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; | ||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||
|   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||
|   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; | ||||
|   import { AppRoute } from '$lib/constants'; | ||||
|   import { AssetStore } from '$lib/stores/assets-store.svelte'; | ||||
|   import { onDestroy } from 'svelte'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import { mdiPlus, mdiArrowLeft } from '@mdi/js'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||
|   import { AssetStore } from '$lib/stores/assets-store.svelte'; | ||||
|   import { AssetVisibility } from '@immich/sdk'; | ||||
|   import { mdiArrowLeft, mdiPlus } from '@mdi/js'; | ||||
|   import { onDestroy } from 'svelte'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import type { PageData } from './$types'; | ||||
|  | ||||
|   interface Props { | ||||
|     data: PageData; | ||||
| @@ -22,7 +23,14 @@ | ||||
|   let { data }: Props = $props(); | ||||
|  | ||||
|   const assetStore = new AssetStore(); | ||||
|   $effect(() => void assetStore.updateOptions({ userId: data.partner.id, isArchived: false, withStacked: true })); | ||||
|   $effect( | ||||
|     () => | ||||
|       void assetStore.updateOptions({ | ||||
|         userId: data.partner.id, | ||||
|         visibility: AssetVisibility.Timeline, | ||||
|         withStacked: true, | ||||
|       }), | ||||
|   ); | ||||
|   onDestroy(() => assetStore.destroy()); | ||||
|   const assetInteraction = new AssetInteraction(); | ||||
|  | ||||
|   | ||||
| @@ -34,12 +34,14 @@ | ||||
|   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|   import { AssetStore } from '$lib/stores/assets-store.svelte'; | ||||
|   import { locale } from '$lib/stores/preferences.store'; | ||||
|   import { preferences } from '$lib/stores/user.store'; | ||||
|   import { websocketEvents } from '$lib/stores/websocket'; | ||||
|   import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { isExternalUrl } from '$lib/utils/navigation'; | ||||
|   import { | ||||
|     AssetVisibility, | ||||
|     getPersonStatistics, | ||||
|     mergePerson, | ||||
|     searchPerson, | ||||
| @@ -59,11 +61,10 @@ | ||||
|     mdiHeartOutline, | ||||
|     mdiPlus, | ||||
|   } from '@mdi/js'; | ||||
|   import { DateTime } from 'luxon'; | ||||
|   import { onDestroy, onMount } from 'svelte'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import { locale } from '$lib/stores/preferences.store'; | ||||
|   import { DateTime } from 'luxon'; | ||||
|  | ||||
|   interface Props { | ||||
|     data: PageData; | ||||
| @@ -75,7 +76,7 @@ | ||||
|   let { isViewing: showAssetViewer } = assetViewingStore; | ||||
|  | ||||
|   const assetStore = new AssetStore(); | ||||
|   $effect(() => void assetStore.updateOptions({ isArchived: false, personId: data.person.id })); | ||||
|   $effect(() => void assetStore.updateOptions({ visibility: AssetVisibility.Timeline, personId: data.person.id })); | ||||
|   onDestroy(() => assetStore.destroy()); | ||||
|  | ||||
|   const assetInteraction = new AssetInteraction(); | ||||
|   | ||||
| @@ -32,14 +32,14 @@ | ||||
|     type OnUnlink, | ||||
|   } from '$lib/utils/actions'; | ||||
|   import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||
|   import { AssetTypeEnum } from '@immich/sdk'; | ||||
|   import { AssetTypeEnum, AssetVisibility } from '@immich/sdk'; | ||||
|   import { mdiDotsVertical, mdiPlus } from '@mdi/js'; | ||||
|   import { onDestroy } from 'svelte'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|  | ||||
|   let { isViewing: showAssetViewer } = assetViewingStore; | ||||
|   const assetStore = new AssetStore(); | ||||
|   void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true }); | ||||
|   void assetStore.updateOptions({ visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true }); | ||||
|   onDestroy(() => assetStore.destroy()); | ||||
|  | ||||
|   const assetInteraction = new AssetInteraction(); | ||||
|   | ||||
| @@ -1,25 +1,38 @@ | ||||
| <script lang="ts"> | ||||
|   import { afterNavigate, goto } from '$app/navigation'; | ||||
|   import { page } from '$app/state'; | ||||
|   import { shortcut } from '$lib/actions/shortcut'; | ||||
|   import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte'; | ||||
|   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||
|   import Icon from '$lib/components/elements/icon.svelte'; | ||||
|   import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; | ||||
|   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; | ||||
|   import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte'; | ||||
|   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; | ||||
|   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; | ||||
|   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; | ||||
|   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; | ||||
|   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; | ||||
|   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; | ||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||
|   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; | ||||
|   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||
|   import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; | ||||
|   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; | ||||
|   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||
|   import { cancelMultiselect } from '$lib/utils/asset-utils'; | ||||
|   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; | ||||
|   import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; | ||||
|   import { AppRoute, QueryParameter } from '$lib/constants'; | ||||
|   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|   import { shortcut } from '$lib/actions/shortcut'; | ||||
|   import type { Viewport } from '$lib/stores/assets-store.svelte'; | ||||
|   import { lang, locale } from '$lib/stores/preferences.store'; | ||||
|   import { featureFlags } from '$lib/stores/server-config.store'; | ||||
|   import { preferences } from '$lib/stores/user.store'; | ||||
|   import { handlePromiseError } from '$lib/utils'; | ||||
|   import { cancelMultiselect } from '$lib/utils/asset-utils'; | ||||
|   import { parseUtcDate } from '$lib/utils/date-time'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation'; | ||||
|   import { | ||||
|     type AlbumResponseDto, | ||||
|     type AssetResponseDto, | ||||
| @@ -31,21 +44,8 @@ | ||||
|     type SmartSearchDto, | ||||
|   } from '@immich/sdk'; | ||||
|   import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; | ||||
|   import type { Viewport } from '$lib/stores/assets-store.svelte'; | ||||
|   import { lang, locale } from '$lib/stores/preferences.store'; | ||||
|   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; | ||||
|   import { handlePromiseError } from '$lib/utils'; | ||||
|   import { parseUtcDate } from '$lib/utils/date-time'; | ||||
|   import { featureFlags } from '$lib/stores/server-config.store'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte'; | ||||
|   import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import { tick } from 'svelte'; | ||||
|   import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte'; | ||||
|   import { preferences } from '$lib/stores/user.store'; | ||||
|   import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; | ||||
|   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|  | ||||
|   const MAX_ASSET_COUNT = 5000; | ||||
|   let { isViewing: showAssetViewer } = assetViewingStore; | ||||
| @@ -186,7 +186,7 @@ | ||||
|     const keyMap: Partial<Record<keyof SearchTerms, string>> = { | ||||
|       takenAfter: $t('start_date'), | ||||
|       takenBefore: $t('end_date'), | ||||
|       isArchived: $t('in_archive'), | ||||
|       visibility: $t('in_archive'), | ||||
|       isFavorite: $t('favorite'), | ||||
|       isNotInAlbum: $t('not_in_any_album'), | ||||
|       type: $t('media_type'), | ||||
| @@ -313,7 +313,7 @@ | ||||
|       <div class="flex place-content-center place-items-center text-xs"> | ||||
|         <div | ||||
|           class="bg-immich-primary py-2 px-4 text-white dark:text-black dark:bg-immich-dark-primary | ||||
|           {value === true ? 'rounded-full' : 'roudned-s-full'}" | ||||
|           {value === true ? 'rounded-full' : 'rounded-s-full'}" | ||||
|         > | ||||
|           {getHumanReadableSearchKey(key as keyof SearchTerms)} | ||||
|         </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user