diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 1dbcfdfb0a..80610bf6a1 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index de0ad6a8b5..2012f41c06 100644 Binary files a/mobile/openapi/doc/AssetApi.md and b/mobile/openapi/doc/AssetApi.md differ diff --git a/mobile/openapi/doc/AssetResponseDto.md b/mobile/openapi/doc/AssetResponseDto.md index 0c8bb3363e..ea9c378435 100644 Binary files a/mobile/openapi/doc/AssetResponseDto.md and b/mobile/openapi/doc/AssetResponseDto.md differ diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md index 7c351ea3b9..3cd5f5e2d6 100644 Binary files a/mobile/openapi/doc/SearchApi.md and b/mobile/openapi/doc/SearchApi.md differ diff --git a/mobile/openapi/doc/UpdateAssetDto.md b/mobile/openapi/doc/UpdateAssetDto.md index f02b6d5ed4..4b844a54f0 100644 Binary files a/mobile/openapi/doc/UpdateAssetDto.md and b/mobile/openapi/doc/UpdateAssetDto.md differ diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index ea05bf8449..4c9732518c 100644 Binary files a/mobile/openapi/lib/api/asset_api.dart and b/mobile/openapi/lib/api/asset_api.dart differ diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 645ccf876b..b4ff53e9ea 100644 Binary files a/mobile/openapi/lib/api/search_api.dart and b/mobile/openapi/lib/api/search_api.dart differ diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index fbe790190d..7fdc42559d 100644 Binary files a/mobile/openapi/lib/model/asset_response_dto.dart and b/mobile/openapi/lib/model/asset_response_dto.dart differ diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index dad7b2c3e1..2faee2f7c4 100644 Binary files a/mobile/openapi/lib/model/update_asset_dto.dart and b/mobile/openapi/lib/model/update_asset_dto.dart differ diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 3430b2e039..fdd4307958 100644 Binary files a/mobile/openapi/test/asset_api_test.dart and b/mobile/openapi/test/asset_api_test.dart differ diff --git a/mobile/openapi/test/asset_response_dto_test.dart b/mobile/openapi/test/asset_response_dto_test.dart index b5c72242d6..4a9be63cb8 100644 Binary files a/mobile/openapi/test/asset_response_dto_test.dart and b/mobile/openapi/test/asset_response_dto_test.dart differ diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index 89037f3c49..1bd93ca6b9 100644 Binary files a/mobile/openapi/test/search_api_test.dart and b/mobile/openapi/test/search_api_test.dart differ diff --git a/mobile/openapi/test/update_asset_dto_test.dart b/mobile/openapi/test/update_asset_dto_test.dart index 16f51f44b6..7f9d874afc 100644 Binary files a/mobile/openapi/test/update_asset_dto_test.dart and b/mobile/openapi/test/update_asset_dto_test.dart differ diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index 9d7fa77ad1..4bd1743dad 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -36,6 +36,7 @@ export interface IAssetRepository { getSearchPropertiesByUserId(userId: string): Promise; getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum): Promise; getAssetCountByUserId(userId: string): Promise; + getArchivedAssetCountByUserId(userId: string): Promise; getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise; getAssetByChecksum(userId: string, checksum: Buffer): Promise; getExistingAssets( @@ -83,26 +84,22 @@ export class AssetRepository implements IAssetRepository { .groupBy('asset.type') .getRawMany(); - const assetCountByUserId = new AssetCountByUserIdResponseDto(); + return this.getAssetCount(items); + } - // asset type to dto property mapping - const map: Record = { - [AssetType.AUDIO]: 'audio', - [AssetType.IMAGE]: 'photos', - [AssetType.VIDEO]: 'videos', - [AssetType.OTHER]: 'other', - }; + async getArchivedAssetCountByUserId(ownerId: string): Promise { + // Get archived asset count by AssetType + const items = await this.assetRepository + .createQueryBuilder('asset') + .select(`COUNT(asset.id)`, 'count') + .addSelect(`asset.type`, 'type') + .where('"ownerId" = :ownerId', { ownerId: ownerId }) + .andWhere('asset.isVisible = true') + .andWhere('asset.isArchived = true') + .groupBy('asset.type') + .getRawMany(); - for (const item of items) { - const count = Number(item.count) || 0; - const assetType = item.type as AssetType; - const type = map[assetType]; - - assetCountByUserId[type] = count; - assetCountByUserId.total += count; - } - - return assetCountByUserId; + return this.getAssetCount(items); } async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise { @@ -115,6 +112,7 @@ export class AssetRepository implements IAssetRepository { }) .andWhere('asset.resizePath is not NULL') .andWhere('asset.isVisible = true') + .andWhere('asset.isArchived = false') .orderBy('asset.fileCreatedAt', 'DESC') .getMany(); } @@ -130,6 +128,7 @@ export class AssetRepository implements IAssetRepository { .where('"ownerId" = :userId', { userId: userId }) .andWhere('asset.resizePath is not NULL') .andWhere('asset.isVisible = true') + .andWhere('asset.isArchived = false') .groupBy(`date_trunc('month', "fileCreatedAt")`) .orderBy(`date_trunc('month', "fileCreatedAt")`, 'DESC') .getRawMany(); @@ -141,6 +140,7 @@ export class AssetRepository implements IAssetRepository { .where('"ownerId" = :userId', { userId: userId }) .andWhere('asset.resizePath is not NULL') .andWhere('asset.isVisible = true') + .andWhere('asset.isArchived = false') .groupBy(`date_trunc('day', "fileCreatedAt")`) .orderBy(`date_trunc('day', "fileCreatedAt")`, 'DESC') .getRawMany(); @@ -224,6 +224,7 @@ export class AssetRepository implements IAssetRepository { resizePath: Not(IsNull()), isVisible: true, isFavorite: dto.isFavorite, + isArchived: dto.isArchived, }, relations: { exifInfo: true, @@ -260,6 +261,7 @@ export class AssetRepository implements IAssetRepository { */ async update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise { asset.isFavorite = dto.isFavorite ?? asset.isFavorite; + asset.isArchived = dto.isArchived ?? asset.isArchived; if (dto.tagIds) { const tags = await this._tagRepository.getByIds(userId, dto.tagIds); @@ -330,4 +332,27 @@ export class AssetRepository implements IAssetRepository { }, }); } + + private getAssetCount(items: any): AssetCountByUserIdResponseDto { + const assetCountByUserId = new AssetCountByUserIdResponseDto(); + + // asset type to dto property mapping + const map: Record = { + [AssetType.AUDIO]: 'audio', + [AssetType.IMAGE]: 'photos', + [AssetType.VIDEO]: 'videos', + [AssetType.OTHER]: 'other', + }; + + for (const item of items) { + const count = Number(item.count) || 0; + const assetType = item.type as AssetType; + const type = map[assetType]; + + assetCountByUserId[type] = count; + assetCountByUserId.total += count; + } + + return assetCountByUserId; + } } diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 0c14386157..6088b81222 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -228,6 +228,11 @@ export class AssetController { return this.assetService.getAssetCountByUserId(authUser); } + @Authenticated() + @Get('/stat/archive') + async getArchivedAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise { + return this.assetService.getArchivedAssetCountByUserId(authUser); + } /** * Get all AssetEntity belong to the user */ diff --git a/server/apps/immich/src/api-v1/asset/asset.core.ts b/server/apps/immich/src/api-v1/asset/asset.core.ts index 14261b001e..972fafd445 100644 --- a/server/apps/immich/src/api-v1/asset/asset.core.ts +++ b/server/apps/immich/src/api-v1/asset/asset.core.ts @@ -28,6 +28,7 @@ export class AssetCore { type: dto.assetType, isFavorite: dto.isFavorite, + isArchived: dto.isArchived ?? false, duration: dto.duration || null, isVisible: dto.isVisible ?? true, livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null, diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index e71bda605e..0a50832961 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -32,6 +32,7 @@ const _getCreateAssetDto = (): CreateAssetDto => { createAssetDto.fileCreatedAt = '2022-06-19T23:41:36.910Z'; createAssetDto.fileModifiedAt = '2022-06-19T23:41:36.910Z'; createAssetDto.isFavorite = false; + createAssetDto.isArchived = false; createAssetDto.duration = '0:00:00.000000'; return createAssetDto; @@ -51,6 +52,7 @@ const _getAsset_1 = () => { asset_1.fileCreatedAt = '2022-06-19T23:41:36.910Z'; asset_1.updatedAt = '2022-06-19T23:41:36.910Z'; asset_1.isFavorite = false; + asset_1.isArchived = false; asset_1.mimeType = 'image/jpeg'; asset_1.webpPath = ''; asset_1.encodedVideoPath = ''; @@ -72,6 +74,7 @@ const _getAsset_2 = () => { asset_2.fileCreatedAt = '2022-06-19T23:41:36.910Z'; asset_2.updatedAt = '2022-06-19T23:41:36.910Z'; asset_2.isFavorite = false; + asset_2.isArchived = false; asset_2.mimeType = 'image/jpeg'; asset_2.webpPath = ''; asset_2.encodedVideoPath = ''; @@ -105,6 +108,15 @@ const _getAssetCountByUserId = (): AssetCountByUserIdResponseDto => { return result; }; +const _getArchivedAssetsCountByUserId = (): AssetCountByUserIdResponseDto => { + const result = new AssetCountByUserIdResponseDto(); + + result.videos = 1; + result.photos = 2; + + return result; +}; + describe('AssetService', () => { let sut: AssetService; let a: Repository; // TO BE DELETED AFTER FINISHED REFACTORING @@ -136,6 +148,7 @@ describe('AssetService', () => { getAssetByTimeBucket: jest.fn(), getAssetByChecksum: jest.fn(), getAssetCountByUserId: jest.fn(), + getArchivedAssetCountByUserId: jest.fn(), getExistingAssets: jest.fn(), countByIdAndUser: jest.fn(), }; @@ -350,14 +363,16 @@ describe('AssetService', () => { it('get asset count by user id', async () => { const assetCount = _getAssetCountByUserId(); + assetRepositoryMock.getAssetCountByUserId.mockResolvedValue(assetCount); - assetRepositoryMock.getAssetCountByUserId.mockImplementation(() => - Promise.resolve(assetCount), - ); + await expect(sut.getAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount); + }); - const result = await sut.getAssetCountByUserId(authStub.user1); + it('get archived asset count by user id', async () => { + const assetCount = _getArchivedAssetsCountByUserId(); + assetRepositoryMock.getArchivedAssetCountByUserId.mockResolvedValue(assetCount); - expect(result).toEqual(assetCount); + await expect(sut.getArchivedAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount); }); describe('deleteAll', () => { diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 59d14e3d1c..e9b3c7893b 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -466,6 +466,10 @@ export class AssetService { return this._assetRepository.getAssetCountByUserId(authUser.id); } + getArchivedAssetCountByUserId(authUser: AuthUserDto): Promise { + return this._assetRepository.getArchivedAssetCountByUserId(authUser.id); + } + async checkAssetsAccess(authUser: AuthUserDto, assetIds: string[], mustBeOwner = false) { for (const assetId of assetIds) { // Step 1: Check if asset is part of a public shared diff --git a/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts b/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts index 0d1ba7ccfc..ab7812220c 100644 --- a/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/asset-search.dto.ts @@ -9,6 +9,12 @@ export class AssetSearchDto { @Transform(toBoolean) isFavorite?: boolean; + @IsOptional() + @IsNotEmpty() + @IsBoolean() + @Transform(toBoolean) + isArchived?: boolean; + @IsOptional() @IsNumber() skip?: number; diff --git a/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts index 4f76964e68..cf158a7e23 100644 --- a/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts @@ -24,6 +24,10 @@ export class CreateAssetDto { @IsNotEmpty() isFavorite!: boolean; + @IsOptional() + @IsBoolean() + isArchived?: boolean; + @IsOptional() @IsBoolean() isVisible?: boolean; diff --git a/server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts index 9c18c50a76..5b17b043fe 100644 --- a/server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/update-asset.dto.ts @@ -6,6 +6,10 @@ export class UpdateAssetDto { @IsBoolean() isFavorite?: boolean; + @IsOptional() + @IsBoolean() + isArchived?: boolean; + @IsOptional() @IsArray() @IsString({ each: true }) diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index c699528caa..fab096fb32 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -767,6 +767,14 @@ "type": "boolean" } }, + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "exifInfo.city", "required": false, @@ -2282,6 +2290,36 @@ ] } }, + "/asset/stat/archive": { + "get": { + "operationId": "getArchivedAssetCountByUserId", + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetCountByUserIdResponseDto" + } + } + } + } + }, + "tags": [ + "Asset" + ], + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + } + ] + } + }, "/asset": { "get": { "operationId": "getAllAssets", @@ -2295,6 +2333,14 @@ "type": "boolean" } }, + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "skip", "required": false, @@ -3726,6 +3772,9 @@ "isFavorite": { "type": "boolean" }, + "isArchived": { + "type": "boolean" + }, "mimeType": { "type": "string", "nullable": true @@ -3771,6 +3820,7 @@ "fileModifiedAt", "updatedAt", "isFavorite", + "isArchived", "mimeType", "duration", "webpPath" @@ -4984,6 +5034,9 @@ "isFavorite": { "type": "boolean" }, + "isArchived": { + "type": "boolean" + }, "isVisible": { "type": "boolean" }, @@ -5227,6 +5280,9 @@ }, "isFavorite": { "type": "boolean" + }, + "isArchived": { + "type": "boolean" } } }, diff --git a/server/libs/domain/src/asset/response-dto/asset-response.dto.ts b/server/libs/domain/src/asset/response-dto/asset-response.dto.ts index 041aef5829..e78c4ddf9c 100644 --- a/server/libs/domain/src/asset/response-dto/asset-response.dto.ts +++ b/server/libs/domain/src/asset/response-dto/asset-response.dto.ts @@ -19,6 +19,7 @@ export class AssetResponseDto { fileModifiedAt!: string; updatedAt!: string; isFavorite!: boolean; + isArchived!: boolean; mimeType!: string | null; duration!: string; webpPath!: string | null; @@ -43,6 +44,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto { fileModifiedAt: entity.fileModifiedAt, updatedAt: entity.updatedAt, isFavorite: entity.isFavorite, + isArchived: entity.isArchived, mimeType: entity.mimeType, webpPath: entity.webpPath, encodedVideoPath: entity.encodedVideoPath, @@ -68,6 +70,7 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto { fileModifiedAt: entity.fileModifiedAt, updatedAt: entity.updatedAt, isFavorite: entity.isFavorite, + isArchived: entity.isArchived, mimeType: entity.mimeType, webpPath: entity.webpPath, encodedVideoPath: entity.encodedVideoPath, diff --git a/server/libs/domain/src/search/dto/search.dto.ts b/server/libs/domain/src/search/dto/search.dto.ts index b30acb0cf7..1d65c7bfb2 100644 --- a/server/libs/domain/src/search/dto/search.dto.ts +++ b/server/libs/domain/src/search/dto/search.dto.ts @@ -28,6 +28,11 @@ export class SearchDto { @Transform(toBoolean) isFavorite?: boolean; + @IsBoolean() + @IsOptional() + @Transform(toBoolean) + isArchived?: boolean; + @IsString() @IsNotEmpty() @IsOptional() diff --git a/server/libs/domain/src/search/search.repository.ts b/server/libs/domain/src/search/search.repository.ts index ac7f211a24..9878c825d0 100644 --- a/server/libs/domain/src/search/search.repository.ts +++ b/server/libs/domain/src/search/search.repository.ts @@ -15,6 +15,7 @@ export interface SearchFilter { userId: string; type?: AssetType; isFavorite?: boolean; + isArchived?: boolean; city?: string; state?: string; country?: string; diff --git a/server/libs/domain/test/fixtures.ts b/server/libs/domain/test/fixtures.ts index 00bcfee90e..708dd39336 100644 --- a/server/libs/domain/test/fixtures.ts +++ b/server/libs/domain/test/fixtures.ts @@ -134,6 +134,7 @@ export const assetEntityStub = { updatedAt: '2023-02-23T05:06:29.716Z', mimeType: null, isFavorite: true, + isArchived: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -158,6 +159,7 @@ export const assetEntityStub = { updatedAt: '2023-02-23T05:06:29.716Z', mimeType: null, isFavorite: true, + isArchived: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -184,6 +186,7 @@ export const assetEntityStub = { updatedAt: '2023-02-23T05:06:29.716Z', mimeType: null, isFavorite: true, + isArchived: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -355,6 +358,7 @@ const assetResponse: AssetResponseDto = { fileCreatedAt: today.toISOString(), updatedAt: today.toISOString(), isFavorite: false, + isArchived: false, mimeType: 'image/jpeg', smartInfo: { tags: [], @@ -591,6 +595,7 @@ export const sharedLinkStub = { createdAt: today.toISOString(), updatedAt: today.toISOString(), isFavorite: false, + isArchived: false, mimeType: 'image/jpeg', smartInfo: { assetId: 'id_1', diff --git a/server/libs/infra/src/entities/asset.entity.ts b/server/libs/infra/src/entities/asset.entity.ts index f47e0fec39..d370f06478 100644 --- a/server/libs/infra/src/entities/asset.entity.ts +++ b/server/libs/infra/src/entities/asset.entity.ts @@ -67,6 +67,9 @@ export class AssetEntity { @Column({ type: 'boolean', default: false }) isFavorite!: boolean; + @Column({ type: 'boolean', default: false }) + isArchived!: boolean; + @Column({ type: 'varchar', nullable: true }) mimeType!: string | null; diff --git a/server/libs/infra/src/migrations/1680632845740-AddIsArchivedColumn.ts b/server/libs/infra/src/migrations/1680632845740-AddIsArchivedColumn.ts new file mode 100644 index 0000000000..325e37f489 --- /dev/null +++ b/server/libs/infra/src/migrations/1680632845740-AddIsArchivedColumn.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddIsArchivedColumn1680632845740 implements MigrationInterface { + name = 'AddIsArchivedColumn1680632845740' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "assets" ADD "isArchived" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "isArchived"`); + } + +} diff --git a/server/libs/infra/src/typesense-schemas/asset.schema.ts b/server/libs/infra/src/typesense-schemas/asset.schema.ts index 04636d4f67..0b778fbeae 100644 --- a/server/libs/infra/src/typesense-schemas/asset.schema.ts +++ b/server/libs/infra/src/typesense-schemas/asset.schema.ts @@ -1,6 +1,6 @@ import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections'; -export const assetSchemaVersion = 4; +export const assetSchemaVersion = 5; export const assetSchema: CollectionCreateSchema = { name: `assets-v${assetSchemaVersion}`, fields: [ @@ -14,8 +14,6 @@ export const assetSchema: CollectionCreateSchema = { { name: 'fileModifiedAt', type: 'string', facet: false, sort: true }, { name: 'isFavorite', type: 'bool', facet: true }, { name: 'originalFileName', type: 'string', facet: false, optional: true }, - // { name: 'checksum', type: 'string', facet: true }, - // { name: 'tags', type: 'string[]', facet: true, optional: true }, // exif { name: 'exifInfo.city', type: 'string', facet: true, optional: true }, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index f92dd2328e..7eed6db389 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -512,6 +512,12 @@ export interface AssetResponseDto { * @memberof AssetResponseDto */ 'isFavorite': boolean; + /** + * + * @type {boolean} + * @memberof AssetResponseDto + */ + 'isArchived': boolean; /** * * @type {string} @@ -2329,6 +2335,12 @@ export interface UpdateAssetDto { * @memberof UpdateAssetDto */ 'isFavorite'?: boolean; + /** + * + * @type {boolean} + * @memberof UpdateAssetDto + */ + 'isArchived'?: boolean; } /** * @@ -4274,12 +4286,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration /** * Get all AssetEntity belong to the user * @param {boolean} [isFavorite] + * @param {boolean} [isArchived] * @param {number} [skip] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets: async (isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { + getAllAssets: async (isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/asset`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -4302,6 +4315,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['isFavorite'] = isFavorite; } + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + if (skip !== undefined) { localVarQueryParameter['skip'] = skip; } @@ -4312,6 +4329,41 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getArchivedAssetCountByUserId: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/asset/stat/archive`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -4873,12 +4925,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {string} fileExtension * @param {string} [key] * @param {File} [livePhotoData] + * @param {boolean} [isArchived] * @param {boolean} [isVisible] * @param {string} [duration] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - uploadFile: async (assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise => { + uploadFile: async (assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'assetType' is not null or undefined assertParamExists('uploadFile', 'assetType', assetType) // verify required parameter 'assetData' is not null or undefined @@ -4951,6 +5004,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarFormParams.append('isFavorite', isFavorite as any); } + if (isArchived !== undefined) { + localVarFormParams.append('isArchived', isArchived as any); + } + if (isVisible !== undefined) { localVarFormParams.append('isVisible', isVisible as any); } @@ -5075,13 +5132,23 @@ export const AssetApiFp = function(configuration?: Configuration) { /** * Get all AssetEntity belong to the user * @param {boolean} [isFavorite] + * @param {boolean} [isArchived] * @param {number} [skip] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(isFavorite, skip, ifNoneMatch, options); + async getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(isFavorite, isArchived, skip, ifNoneMatch, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getArchivedAssetCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getArchivedAssetCountByUserId(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -5230,13 +5297,14 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {string} fileExtension * @param {string} [key] * @param {File} [livePhotoData] + * @param {boolean} [isArchived] * @param {boolean} [isVisible] * @param {string} [duration] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isVisible, duration, options); + async uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isArchived, isVisible, duration, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -5330,13 +5398,22 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath /** * Get all AssetEntity belong to the user * @param {boolean} [isFavorite] + * @param {boolean} [isArchived] * @param {number} [skip] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise> { - return localVarFp.getAllAssets(isFavorite, skip, ifNoneMatch, options).then((request) => request(axios, basePath)); + getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise> { + return localVarFp.getAllAssets(isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getArchivedAssetCountByUserId(options?: any): AxiosPromise { + return localVarFp.getArchivedAssetCountByUserId(options).then((request) => request(axios, basePath)); }, /** * Get a single asset\'s information @@ -5471,13 +5548,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {string} fileExtension * @param {string} [key] * @param {File} [livePhotoData] + * @param {boolean} [isArchived] * @param {boolean} [isVisible] * @param {string} [duration] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, isVisible?: boolean, duration?: string, options?: any): AxiosPromise { - return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isVisible, duration, options).then((request) => request(axios, basePath)); + uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise { + return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isArchived, isVisible, duration, options).then((request) => request(axios, basePath)); }, }; }; @@ -5586,14 +5664,25 @@ export class AssetApi extends BaseAPI { /** * Get all AssetEntity belong to the user * @param {boolean} [isFavorite] + * @param {boolean} [isArchived] * @param {number} [skip] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAllAssets(isFavorite, skip, ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); + public getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAllAssets(isFavorite, isArchived, skip, ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AssetApi + */ + public getArchivedAssetCountByUserId(options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getArchivedAssetCountByUserId(options).then((request) => request(this.axios, this.basePath)); } /** @@ -5755,14 +5844,15 @@ export class AssetApi extends BaseAPI { * @param {string} fileExtension * @param {string} [key] * @param {File} [livePhotoData] + * @param {boolean} [isArchived] * @param {boolean} [isVisible] * @param {string} [duration] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isVisible, duration, options).then((request) => request(this.axios, this.basePath)); + public uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isArchived, isVisible, duration, options).then((request) => request(this.axios, this.basePath)); } } @@ -6857,6 +6947,7 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio * @param {boolean} [clip] * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] * @param {boolean} [isFavorite] + * @param {boolean} [isArchived] * @param {string} [exifInfoCity] * @param {string} [exifInfoState] * @param {string} [exifInfoCountry] @@ -6869,7 +6960,7 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio * @param {*} [options] Override http request option. * @throws {RequiredError} */ - search: async (q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise => { + search: async (q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/search`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -6908,6 +6999,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio localVarQueryParameter['isFavorite'] = isFavorite; } + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + if (exifInfoCity !== undefined) { localVarQueryParameter['exifInfo.city'] = exifInfoCity; } @@ -6990,6 +7085,7 @@ export const SearchApiFp = function(configuration?: Configuration) { * @param {boolean} [clip] * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] * @param {boolean} [isFavorite] + * @param {boolean} [isArchived] * @param {string} [exifInfoCity] * @param {string} [exifInfoState] * @param {string} [exifInfoCountry] @@ -7002,8 +7098,8 @@ export const SearchApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options); + async search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -7039,6 +7135,7 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat * @param {boolean} [clip] * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] * @param {boolean} [isFavorite] + * @param {boolean} [isArchived] * @param {string} [exifInfoCity] * @param {string} [exifInfoState] * @param {string} [exifInfoCountry] @@ -7051,8 +7148,8 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat * @param {*} [options] Override http request option. * @throws {RequiredError} */ - search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options?: any): AxiosPromise { - return localVarFp.search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(axios, basePath)); + search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options?: any): AxiosPromise { + return localVarFp.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(axios, basePath)); }, }; }; @@ -7091,6 +7188,7 @@ export class SearchApi extends BaseAPI { * @param {boolean} [clip] * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] * @param {boolean} [isFavorite] + * @param {boolean} [isArchived] * @param {string} [exifInfoCity] * @param {string} [exifInfoState] * @param {string} [exifInfoCountry] @@ -7104,8 +7202,8 @@ export class SearchApi extends BaseAPI { * @throws {RequiredError} * @memberof SearchApi */ - public search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig) { - return SearchApiFp(this.configuration).search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(this.axios, this.basePath)); + public search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, isArchived?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array, smartInfoTags?: Array, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig) { + return SearchApiFp(this.configuration).search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/web/src/lib/components/shared-components/empty-placeholder.svelte b/web/src/lib/components/shared-components/empty-placeholder.svelte new file mode 100644 index 0000000000..55b225ac74 --- /dev/null +++ b/web/src/lib/components/shared-components/empty-placeholder.svelte @@ -0,0 +1,28 @@ + + +{#if actionHandler} +
+ +

{text}

+
+{:else} +
+ +

{text}

+
+{/if} diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index f8615f5f2f..d312f05c41 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -4,6 +4,7 @@ import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte'; import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte'; import ImageOutline from 'svelte-material-icons/ImageOutline.svelte'; + import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; import Magnify from 'svelte-material-icons/Magnify.svelte'; import StarOutline from 'svelte-material-icons/StarOutline.svelte'; import { AppRoute } from '../../../constants'; @@ -13,16 +14,17 @@ import { locale } from '$lib/stores/preferences.store'; const getAssetCount = async () => { - const { data: assetCount } = await api.assetApi.getAssetCountByUserId(); + const { data: allAssetCount } = await api.assetApi.getAssetCountByUserId(); + const { data: archivedCount } = await api.assetApi.getArchivedAssetCountByUserId(); return { - videos: assetCount.videos, - photos: assetCount.photos + videos: allAssetCount.videos - archivedCount.videos, + photos: allAssetCount.photos - archivedCount.photos }; }; const getFavoriteCount = async () => { - const { data: assets } = await api.assetApi.getAllAssets(true); + const { data: assets } = await api.assetApi.getAllAssets(true, undefined); return { favorites: assets.length @@ -37,6 +39,15 @@ owned: albumCount.owned }; }; + + const getArchivedAssetsCount = async () => { + const { data: assetCount } = await api.assetApi.getArchivedAssetCountByUserId(); + + return { + videos: assetCount.videos, + photos: assetCount.photos + }; + };