1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-28 09:33:27 +02:00

feat (server, web): Implement Archive (#2225)

* feat (server, web): add archive

* chore: generate api

* feat (web): add empty placeholder for archive page

* chore: remove title on favorites page

Duplicates sidebar selection. Two pages (Archive and Favorites)
are consistent now

* refactor (web): create EmptyPlaceholder component for empty pages

* fixed menu close button not close:

* fix (web): remove not necessary store call

* test (web): simplify asset tests code

* test (web): simplify asset tests code

* chore (server): remove isArchived while uploading

* chore (server): remove isArchived from typesense schema

* chore: generate api

* fix (web): delete asset from archive page

* chore: change archive asset count endpoint

old endpoint: /asset/archived-count-by-user-id
new endpoint: /asset/stat/archive

* chore: generate api

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Sergey Kondrikov 2023-04-12 18:37:52 +03:00 committed by GitHub
parent eb9481b668
commit d314805caf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 672 additions and 75 deletions

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -36,6 +36,7 @@ export interface IAssetRepository {
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>; getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum): Promise<AssetCountByTimeBucket[]>; getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum): Promise<AssetCountByTimeBucket[]>;
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>; getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
getArchivedAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>; getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>; getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
getExistingAssets( getExistingAssets(
@ -83,26 +84,22 @@ export class AssetRepository implements IAssetRepository {
.groupBy('asset.type') .groupBy('asset.type')
.getRawMany(); .getRawMany();
const assetCountByUserId = new AssetCountByUserIdResponseDto(); return this.getAssetCount(items);
}
// asset type to dto property mapping async getArchivedAssetCountByUserId(ownerId: string): Promise<AssetCountByUserIdResponseDto> {
const map: Record<AssetType, keyof AssetCountByUserIdResponseDto> = { // Get archived asset count by AssetType
[AssetType.AUDIO]: 'audio', const items = await this.assetRepository
[AssetType.IMAGE]: 'photos', .createQueryBuilder('asset')
[AssetType.VIDEO]: 'videos', .select(`COUNT(asset.id)`, 'count')
[AssetType.OTHER]: 'other', .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) { return this.getAssetCount(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;
} }
async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> { async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
@ -115,6 +112,7 @@ export class AssetRepository implements IAssetRepository {
}) })
.andWhere('asset.resizePath is not NULL') .andWhere('asset.resizePath is not NULL')
.andWhere('asset.isVisible = true') .andWhere('asset.isVisible = true')
.andWhere('asset.isArchived = false')
.orderBy('asset.fileCreatedAt', 'DESC') .orderBy('asset.fileCreatedAt', 'DESC')
.getMany(); .getMany();
} }
@ -130,6 +128,7 @@ export class AssetRepository implements IAssetRepository {
.where('"ownerId" = :userId', { userId: userId }) .where('"ownerId" = :userId', { userId: userId })
.andWhere('asset.resizePath is not NULL') .andWhere('asset.resizePath is not NULL')
.andWhere('asset.isVisible = true') .andWhere('asset.isVisible = true')
.andWhere('asset.isArchived = false')
.groupBy(`date_trunc('month', "fileCreatedAt")`) .groupBy(`date_trunc('month', "fileCreatedAt")`)
.orderBy(`date_trunc('month', "fileCreatedAt")`, 'DESC') .orderBy(`date_trunc('month', "fileCreatedAt")`, 'DESC')
.getRawMany(); .getRawMany();
@ -141,6 +140,7 @@ export class AssetRepository implements IAssetRepository {
.where('"ownerId" = :userId', { userId: userId }) .where('"ownerId" = :userId', { userId: userId })
.andWhere('asset.resizePath is not NULL') .andWhere('asset.resizePath is not NULL')
.andWhere('asset.isVisible = true') .andWhere('asset.isVisible = true')
.andWhere('asset.isArchived = false')
.groupBy(`date_trunc('day', "fileCreatedAt")`) .groupBy(`date_trunc('day', "fileCreatedAt")`)
.orderBy(`date_trunc('day', "fileCreatedAt")`, 'DESC') .orderBy(`date_trunc('day', "fileCreatedAt")`, 'DESC')
.getRawMany(); .getRawMany();
@ -224,6 +224,7 @@ export class AssetRepository implements IAssetRepository {
resizePath: Not(IsNull()), resizePath: Not(IsNull()),
isVisible: true, isVisible: true,
isFavorite: dto.isFavorite, isFavorite: dto.isFavorite,
isArchived: dto.isArchived,
}, },
relations: { relations: {
exifInfo: true, exifInfo: true,
@ -260,6 +261,7 @@ export class AssetRepository implements IAssetRepository {
*/ */
async update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity> { async update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity> {
asset.isFavorite = dto.isFavorite ?? asset.isFavorite; asset.isFavorite = dto.isFavorite ?? asset.isFavorite;
asset.isArchived = dto.isArchived ?? asset.isArchived;
if (dto.tagIds) { if (dto.tagIds) {
const tags = await this._tagRepository.getByIds(userId, 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, keyof AssetCountByUserIdResponseDto> = {
[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;
}
} }

View File

@ -228,6 +228,11 @@ export class AssetController {
return this.assetService.getAssetCountByUserId(authUser); return this.assetService.getAssetCountByUserId(authUser);
} }
@Authenticated()
@Get('/stat/archive')
async getArchivedAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this.assetService.getArchivedAssetCountByUserId(authUser);
}
/** /**
* Get all AssetEntity belong to the user * Get all AssetEntity belong to the user
*/ */

View File

@ -28,6 +28,7 @@ export class AssetCore {
type: dto.assetType, type: dto.assetType,
isFavorite: dto.isFavorite, isFavorite: dto.isFavorite,
isArchived: dto.isArchived ?? false,
duration: dto.duration || null, duration: dto.duration || null,
isVisible: dto.isVisible ?? true, isVisible: dto.isVisible ?? true,
livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null, livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null,

View File

@ -32,6 +32,7 @@ const _getCreateAssetDto = (): CreateAssetDto => {
createAssetDto.fileCreatedAt = '2022-06-19T23:41:36.910Z'; createAssetDto.fileCreatedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.fileModifiedAt = '2022-06-19T23:41:36.910Z'; createAssetDto.fileModifiedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.isFavorite = false; createAssetDto.isFavorite = false;
createAssetDto.isArchived = false;
createAssetDto.duration = '0:00:00.000000'; createAssetDto.duration = '0:00:00.000000';
return createAssetDto; return createAssetDto;
@ -51,6 +52,7 @@ const _getAsset_1 = () => {
asset_1.fileCreatedAt = '2022-06-19T23:41:36.910Z'; asset_1.fileCreatedAt = '2022-06-19T23:41:36.910Z';
asset_1.updatedAt = '2022-06-19T23:41:36.910Z'; asset_1.updatedAt = '2022-06-19T23:41:36.910Z';
asset_1.isFavorite = false; asset_1.isFavorite = false;
asset_1.isArchived = false;
asset_1.mimeType = 'image/jpeg'; asset_1.mimeType = 'image/jpeg';
asset_1.webpPath = ''; asset_1.webpPath = '';
asset_1.encodedVideoPath = ''; asset_1.encodedVideoPath = '';
@ -72,6 +74,7 @@ const _getAsset_2 = () => {
asset_2.fileCreatedAt = '2022-06-19T23:41:36.910Z'; asset_2.fileCreatedAt = '2022-06-19T23:41:36.910Z';
asset_2.updatedAt = '2022-06-19T23:41:36.910Z'; asset_2.updatedAt = '2022-06-19T23:41:36.910Z';
asset_2.isFavorite = false; asset_2.isFavorite = false;
asset_2.isArchived = false;
asset_2.mimeType = 'image/jpeg'; asset_2.mimeType = 'image/jpeg';
asset_2.webpPath = ''; asset_2.webpPath = '';
asset_2.encodedVideoPath = ''; asset_2.encodedVideoPath = '';
@ -105,6 +108,15 @@ const _getAssetCountByUserId = (): AssetCountByUserIdResponseDto => {
return result; return result;
}; };
const _getArchivedAssetsCountByUserId = (): AssetCountByUserIdResponseDto => {
const result = new AssetCountByUserIdResponseDto();
result.videos = 1;
result.photos = 2;
return result;
};
describe('AssetService', () => { describe('AssetService', () => {
let sut: AssetService; let sut: AssetService;
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
@ -136,6 +148,7 @@ describe('AssetService', () => {
getAssetByTimeBucket: jest.fn(), getAssetByTimeBucket: jest.fn(),
getAssetByChecksum: jest.fn(), getAssetByChecksum: jest.fn(),
getAssetCountByUserId: jest.fn(), getAssetCountByUserId: jest.fn(),
getArchivedAssetCountByUserId: jest.fn(),
getExistingAssets: jest.fn(), getExistingAssets: jest.fn(),
countByIdAndUser: jest.fn(), countByIdAndUser: jest.fn(),
}; };
@ -350,14 +363,16 @@ describe('AssetService', () => {
it('get asset count by user id', async () => { it('get asset count by user id', async () => {
const assetCount = _getAssetCountByUserId(); const assetCount = _getAssetCountByUserId();
assetRepositoryMock.getAssetCountByUserId.mockResolvedValue(assetCount);
assetRepositoryMock.getAssetCountByUserId.mockImplementation(() => await expect(sut.getAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount);
Promise.resolve<AssetCountByUserIdResponseDto>(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', () => { describe('deleteAll', () => {

View File

@ -466,6 +466,10 @@ export class AssetService {
return this._assetRepository.getAssetCountByUserId(authUser.id); return this._assetRepository.getAssetCountByUserId(authUser.id);
} }
getArchivedAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this._assetRepository.getArchivedAssetCountByUserId(authUser.id);
}
async checkAssetsAccess(authUser: AuthUserDto, assetIds: string[], mustBeOwner = false) { async checkAssetsAccess(authUser: AuthUserDto, assetIds: string[], mustBeOwner = false) {
for (const assetId of assetIds) { for (const assetId of assetIds) {
// Step 1: Check if asset is part of a public shared // Step 1: Check if asset is part of a public shared

View File

@ -9,6 +9,12 @@ export class AssetSearchDto {
@Transform(toBoolean) @Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;
@IsOptional()
@IsNotEmpty()
@IsBoolean()
@Transform(toBoolean)
isArchived?: boolean;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
skip?: number; skip?: number;

View File

@ -24,6 +24,10 @@ export class CreateAssetDto {
@IsNotEmpty() @IsNotEmpty()
isFavorite!: boolean; isFavorite!: boolean;
@IsOptional()
@IsBoolean()
isArchived?: boolean;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
isVisible?: boolean; isVisible?: boolean;

View File

@ -6,6 +6,10 @@ export class UpdateAssetDto {
@IsBoolean() @IsBoolean()
isFavorite?: boolean; isFavorite?: boolean;
@IsOptional()
@IsBoolean()
isArchived?: boolean;
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@IsString({ each: true }) @IsString({ each: true })

View File

@ -767,6 +767,14 @@
"type": "boolean" "type": "boolean"
} }
}, },
{
"name": "isArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{ {
"name": "exifInfo.city", "name": "exifInfo.city",
"required": false, "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": { "/asset": {
"get": { "get": {
"operationId": "getAllAssets", "operationId": "getAllAssets",
@ -2295,6 +2333,14 @@
"type": "boolean" "type": "boolean"
} }
}, },
{
"name": "isArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{ {
"name": "skip", "name": "skip",
"required": false, "required": false,
@ -3726,6 +3772,9 @@
"isFavorite": { "isFavorite": {
"type": "boolean" "type": "boolean"
}, },
"isArchived": {
"type": "boolean"
},
"mimeType": { "mimeType": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@ -3771,6 +3820,7 @@
"fileModifiedAt", "fileModifiedAt",
"updatedAt", "updatedAt",
"isFavorite", "isFavorite",
"isArchived",
"mimeType", "mimeType",
"duration", "duration",
"webpPath" "webpPath"
@ -4984,6 +5034,9 @@
"isFavorite": { "isFavorite": {
"type": "boolean" "type": "boolean"
}, },
"isArchived": {
"type": "boolean"
},
"isVisible": { "isVisible": {
"type": "boolean" "type": "boolean"
}, },
@ -5227,6 +5280,9 @@
}, },
"isFavorite": { "isFavorite": {
"type": "boolean" "type": "boolean"
},
"isArchived": {
"type": "boolean"
} }
} }
}, },

View File

@ -19,6 +19,7 @@ export class AssetResponseDto {
fileModifiedAt!: string; fileModifiedAt!: string;
updatedAt!: string; updatedAt!: string;
isFavorite!: boolean; isFavorite!: boolean;
isArchived!: boolean;
mimeType!: string | null; mimeType!: string | null;
duration!: string; duration!: string;
webpPath!: string | null; webpPath!: string | null;
@ -43,6 +44,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
fileModifiedAt: entity.fileModifiedAt, fileModifiedAt: entity.fileModifiedAt,
updatedAt: entity.updatedAt, updatedAt: entity.updatedAt,
isFavorite: entity.isFavorite, isFavorite: entity.isFavorite,
isArchived: entity.isArchived,
mimeType: entity.mimeType, mimeType: entity.mimeType,
webpPath: entity.webpPath, webpPath: entity.webpPath,
encodedVideoPath: entity.encodedVideoPath, encodedVideoPath: entity.encodedVideoPath,
@ -68,6 +70,7 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
fileModifiedAt: entity.fileModifiedAt, fileModifiedAt: entity.fileModifiedAt,
updatedAt: entity.updatedAt, updatedAt: entity.updatedAt,
isFavorite: entity.isFavorite, isFavorite: entity.isFavorite,
isArchived: entity.isArchived,
mimeType: entity.mimeType, mimeType: entity.mimeType,
webpPath: entity.webpPath, webpPath: entity.webpPath,
encodedVideoPath: entity.encodedVideoPath, encodedVideoPath: entity.encodedVideoPath,

View File

@ -28,6 +28,11 @@ export class SearchDto {
@Transform(toBoolean) @Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;
@IsBoolean()
@IsOptional()
@Transform(toBoolean)
isArchived?: boolean;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsOptional() @IsOptional()

View File

@ -15,6 +15,7 @@ export interface SearchFilter {
userId: string; userId: string;
type?: AssetType; type?: AssetType;
isFavorite?: boolean; isFavorite?: boolean;
isArchived?: boolean;
city?: string; city?: string;
state?: string; state?: string;
country?: string; country?: string;

View File

@ -134,6 +134,7 @@ export const assetEntityStub = {
updatedAt: '2023-02-23T05:06:29.716Z', updatedAt: '2023-02-23T05:06:29.716Z',
mimeType: null, mimeType: null,
isFavorite: true, isFavorite: true,
isArchived: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
@ -158,6 +159,7 @@ export const assetEntityStub = {
updatedAt: '2023-02-23T05:06:29.716Z', updatedAt: '2023-02-23T05:06:29.716Z',
mimeType: null, mimeType: null,
isFavorite: true, isFavorite: true,
isArchived: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
@ -184,6 +186,7 @@ export const assetEntityStub = {
updatedAt: '2023-02-23T05:06:29.716Z', updatedAt: '2023-02-23T05:06:29.716Z',
mimeType: null, mimeType: null,
isFavorite: true, isFavorite: true,
isArchived: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
@ -355,6 +358,7 @@ const assetResponse: AssetResponseDto = {
fileCreatedAt: today.toISOString(), fileCreatedAt: today.toISOString(),
updatedAt: today.toISOString(), updatedAt: today.toISOString(),
isFavorite: false, isFavorite: false,
isArchived: false,
mimeType: 'image/jpeg', mimeType: 'image/jpeg',
smartInfo: { smartInfo: {
tags: [], tags: [],
@ -591,6 +595,7 @@ export const sharedLinkStub = {
createdAt: today.toISOString(), createdAt: today.toISOString(),
updatedAt: today.toISOString(), updatedAt: today.toISOString(),
isFavorite: false, isFavorite: false,
isArchived: false,
mimeType: 'image/jpeg', mimeType: 'image/jpeg',
smartInfo: { smartInfo: {
assetId: 'id_1', assetId: 'id_1',

View File

@ -67,6 +67,9 @@ export class AssetEntity {
@Column({ type: 'boolean', default: false }) @Column({ type: 'boolean', default: false })
isFavorite!: boolean; isFavorite!: boolean;
@Column({ type: 'boolean', default: false })
isArchived!: boolean;
@Column({ type: 'varchar', nullable: true }) @Column({ type: 'varchar', nullable: true })
mimeType!: string | null; mimeType!: string | null;

View File

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddIsArchivedColumn1680632845740 implements MigrationInterface {
name = 'AddIsArchivedColumn1680632845740'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" ADD "isArchived" boolean NOT NULL DEFAULT false`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "isArchived"`);
}
}

View File

@ -1,6 +1,6 @@
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections'; import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
export const assetSchemaVersion = 4; export const assetSchemaVersion = 5;
export const assetSchema: CollectionCreateSchema = { export const assetSchema: CollectionCreateSchema = {
name: `assets-v${assetSchemaVersion}`, name: `assets-v${assetSchemaVersion}`,
fields: [ fields: [
@ -14,8 +14,6 @@ export const assetSchema: CollectionCreateSchema = {
{ name: 'fileModifiedAt', type: 'string', facet: false, sort: true }, { name: 'fileModifiedAt', type: 'string', facet: false, sort: true },
{ name: 'isFavorite', type: 'bool', facet: true }, { name: 'isFavorite', type: 'bool', facet: true },
{ name: 'originalFileName', type: 'string', facet: false, optional: true }, { name: 'originalFileName', type: 'string', facet: false, optional: true },
// { name: 'checksum', type: 'string', facet: true },
// { name: 'tags', type: 'string[]', facet: true, optional: true },
// exif // exif
{ name: 'exifInfo.city', type: 'string', facet: true, optional: true }, { name: 'exifInfo.city', type: 'string', facet: true, optional: true },

View File

@ -512,6 +512,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto * @memberof AssetResponseDto
*/ */
'isFavorite': boolean; 'isFavorite': boolean;
/**
*
* @type {boolean}
* @memberof AssetResponseDto
*/
'isArchived': boolean;
/** /**
* *
* @type {string} * @type {string}
@ -2329,6 +2335,12 @@ export interface UpdateAssetDto {
* @memberof UpdateAssetDto * @memberof UpdateAssetDto
*/ */
'isFavorite'?: boolean; '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 * Get all AssetEntity belong to the user
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived]
* @param {number} [skip] * @param {number} [skip]
* @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {string} [ifNoneMatch] ETag of data already cached on the client
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getAllAssets: async (isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getAllAssets: async (isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset`; const localVarPath = `/asset`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -4302,6 +4315,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['isFavorite'] = isFavorite; localVarQueryParameter['isFavorite'] = isFavorite;
} }
if (isArchived !== undefined) {
localVarQueryParameter['isArchived'] = isArchived;
}
if (skip !== undefined) { if (skip !== undefined) {
localVarQueryParameter['skip'] = skip; 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<RequestArgs> => {
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); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -4873,12 +4925,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {string} fileExtension * @param {string} fileExtension
* @param {string} [key] * @param {string} [key]
* @param {File} [livePhotoData] * @param {File} [livePhotoData]
* @param {boolean} [isArchived]
* @param {boolean} [isVisible] * @param {boolean} [isVisible]
* @param {string} [duration] * @param {string} [duration]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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<RequestArgs> => { 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<RequestArgs> => {
// verify required parameter 'assetType' is not null or undefined // verify required parameter 'assetType' is not null or undefined
assertParamExists('uploadFile', 'assetType', assetType) assertParamExists('uploadFile', 'assetType', assetType)
// verify required parameter 'assetData' is not null or undefined // 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); localVarFormParams.append('isFavorite', isFavorite as any);
} }
if (isArchived !== undefined) {
localVarFormParams.append('isArchived', isArchived as any);
}
if (isVisible !== undefined) { if (isVisible !== undefined) {
localVarFormParams.append('isVisible', isVisible as any); localVarFormParams.append('isVisible', isVisible as any);
} }
@ -5075,13 +5132,23 @@ export const AssetApiFp = function(configuration?: Configuration) {
/** /**
* Get all AssetEntity belong to the user * Get all AssetEntity belong to the user
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived]
* @param {number} [skip] * @param {number} [skip]
* @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {string} [ifNoneMatch] ETag of data already cached on the client
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { async getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(isFavorite, skip, ifNoneMatch, options); 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<AssetCountByUserIdResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getArchivedAssetCountByUserId(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -5230,13 +5297,14 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {string} fileExtension * @param {string} fileExtension
* @param {string} [key] * @param {string} [key]
* @param {File} [livePhotoData] * @param {File} [livePhotoData]
* @param {boolean} [isArchived]
* @param {boolean} [isVisible] * @param {boolean} [isVisible]
* @param {string} [duration] * @param {string} [duration]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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<AssetFileUploadResponseDto>> { 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<AssetFileUploadResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isVisible, duration, options); 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); 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 * Get all AssetEntity belong to the user
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived]
* @param {number} [skip] * @param {number} [skip]
* @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {string} [ifNoneMatch] ETag of data already cached on the client
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> { getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> {
return localVarFp.getAllAssets(isFavorite, skip, ifNoneMatch, options).then((request) => request(axios, basePath)); 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<AssetCountByUserIdResponseDto> {
return localVarFp.getArchivedAssetCountByUserId(options).then((request) => request(axios, basePath));
}, },
/** /**
* Get a single asset\'s information * Get a single asset\'s information
@ -5471,13 +5548,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @param {string} fileExtension * @param {string} fileExtension
* @param {string} [key] * @param {string} [key]
* @param {File} [livePhotoData] * @param {File} [livePhotoData]
* @param {boolean} [isArchived]
* @param {boolean} [isVisible] * @param {boolean} [isVisible]
* @param {string} [duration] * @param {string} [duration]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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<AssetFileUploadResponseDto> { 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<AssetFileUploadResponseDto> {
return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, isVisible, duration, options).then((request) => request(axios, basePath)); 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 * Get all AssetEntity belong to the user
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived]
* @param {number} [skip] * @param {number} [skip]
* @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {string} [ifNoneMatch] ETag of data already cached on the client
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof AssetApi * @memberof AssetApi
*/ */
public getAllAssets(isFavorite?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) { public getAllAssets(isFavorite?: boolean, isArchived?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAllAssets(isFavorite, skip, ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); 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} fileExtension
* @param {string} [key] * @param {string} [key]
* @param {File} [livePhotoData] * @param {File} [livePhotoData]
* @param {boolean} [isArchived]
* @param {boolean} [isVisible] * @param {boolean} [isVisible]
* @param {string} [duration] * @param {string} [duration]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof AssetApi * @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) { 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, isVisible, duration, options).then((request) => request(this.axios, this.basePath)); 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 {boolean} [clip]
* @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type]
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived]
* @param {string} [exifInfoCity] * @param {string} [exifInfoCity]
* @param {string} [exifInfoState] * @param {string} [exifInfoState]
* @param {string} [exifInfoCountry] * @param {string} [exifInfoCountry]
@ -6869,7 +6960,7 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { 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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/search`; const localVarPath = `/search`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -6908,6 +6999,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
localVarQueryParameter['isFavorite'] = isFavorite; localVarQueryParameter['isFavorite'] = isFavorite;
} }
if (isArchived !== undefined) {
localVarQueryParameter['isArchived'] = isArchived;
}
if (exifInfoCity !== undefined) { if (exifInfoCity !== undefined) {
localVarQueryParameter['exifInfo.city'] = exifInfoCity; localVarQueryParameter['exifInfo.city'] = exifInfoCity;
} }
@ -6990,6 +7085,7 @@ export const SearchApiFp = function(configuration?: Configuration) {
* @param {boolean} [clip] * @param {boolean} [clip]
* @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type]
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived]
* @param {string} [exifInfoCity] * @param {string} [exifInfoCity]
* @param {string} [exifInfoState] * @param {string} [exifInfoState]
* @param {string} [exifInfoCountry] * @param {string} [exifInfoCountry]
@ -7002,8 +7098,8 @@ export const SearchApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> { 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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options); 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); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
} }
@ -7039,6 +7135,7 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
* @param {boolean} [clip] * @param {boolean} [clip]
* @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type]
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived]
* @param {string} [exifInfoCity] * @param {string} [exifInfoCity]
* @param {string} [exifInfoState] * @param {string} [exifInfoState]
* @param {string} [exifInfoCountry] * @param {string} [exifInfoCountry]
@ -7051,8 +7148,8 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: any): AxiosPromise<SearchResponseDto> { 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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: any): AxiosPromise<SearchResponseDto> {
return localVarFp.search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(axios, basePath)); 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 {boolean} [clip]
* @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type]
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived]
* @param {string} [exifInfoCity] * @param {string} [exifInfoCity]
* @param {string} [exifInfoState] * @param {string} [exifInfoState]
* @param {string} [exifInfoCountry] * @param {string} [exifInfoCountry]
@ -7104,8 +7202,8 @@ export class SearchApi extends BaseAPI {
* @throws {RequiredError} * @throws {RequiredError}
* @memberof SearchApi * @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<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig) { 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<string>, smartInfoTags?: Array<string>, 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)); 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));
} }
} }

View File

@ -0,0 +1,28 @@
<script lang="ts">
import empty1Url from '$lib/assets/empty-1.svg';
export let actionHandler: undefined | (() => Promise<void>) = undefined;
export let text = '';
export let alt = '';
let hoverClasses =
'hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer';
</script>
{#if actionHandler}
<div
on:click={actionHandler}
on:keydown={actionHandler}
class="border dark:border-immich-dark-gray {hoverClasses} p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center"
>
<img src={empty1Url} {alt} width="500" draggable="false" />
<p class="text-center text-immich-text-gray-500 dark:text-immich-dark-fg">{text}</p>
</div>
{:else}
<div
class="border dark:border-immich-dark-gray p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center"
>
<img src={empty1Url} {alt} width="500" draggable="false" />
<p class="text-center text-immich-text-gray-500 dark:text-immich-dark-fg">{text}</p>
</div>
{/if}

View File

@ -4,6 +4,7 @@
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte'; import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte'; import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
import ImageOutline from 'svelte-material-icons/ImageOutline.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 Magnify from 'svelte-material-icons/Magnify.svelte';
import StarOutline from 'svelte-material-icons/StarOutline.svelte'; import StarOutline from 'svelte-material-icons/StarOutline.svelte';
import { AppRoute } from '../../../constants'; import { AppRoute } from '../../../constants';
@ -13,16 +14,17 @@
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
const getAssetCount = async () => { 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 { return {
videos: assetCount.videos, videos: allAssetCount.videos - archivedCount.videos,
photos: assetCount.photos photos: allAssetCount.photos - archivedCount.photos
}; };
}; };
const getFavoriteCount = async () => { const getFavoriteCount = async () => {
const { data: assets } = await api.assetApi.getAllAssets(true); const { data: assets } = await api.assetApi.getAllAssets(true, undefined);
return { return {
favorites: assets.length favorites: assets.length
@ -37,6 +39,15 @@
owned: albumCount.owned owned: albumCount.owned
}; };
}; };
const getArchivedAssetsCount = async () => {
const { data: assetCount } = await api.assetApi.getArchivedAssetCountByUserId();
return {
videos: assetCount.videos,
photos: assetCount.photos
};
};
</script> </script>
<section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6 bg-immich-bg dark:bg-immich-dark-bg"> <section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6 bg-immich-bg dark:bg-immich-dark-bg">
@ -130,6 +141,24 @@
</svelte:fragment> </svelte:fragment>
</SideBarButton> </SideBarButton>
</a> </a>
<a data-sveltekit-preload-data="hover" href={AppRoute.ARCHIVE} draggable="false">
<SideBarButton
title="Archive"
logo={ArchiveArrowDownOutline}
isSelected={$page.route.id === '/(user)/archive'}
>
<svelte:fragment slot="moreInformation">
{#await getArchivedAssetsCount()}
<LoadingSpinner />
{:then data}
<div>
<p>{data.videos.toLocaleString($locale)} Videos</p>
<p>{data.photos.toLocaleString($locale)} Photos</p>
</div>
{/await}
</svelte:fragment>
</SideBarButton>
</a>
<!-- Status Box --> <!-- Status Box -->
<div class="mb-6 mt-auto"> <div class="mb-6 mt-auto">

View File

@ -8,6 +8,7 @@ export enum AppRoute {
ADMIN_JOBS = '/admin/jobs-status', ADMIN_JOBS = '/admin/jobs-status',
ALBUMS = '/albums', ALBUMS = '/albums',
ARCHIVE = '/archive',
FAVORITES = '/favorites', FAVORITES = '/favorites',
PHOTOS = '/photos', PHOTOS = '/photos',
EXPLORE = '/explore', EXPLORE = '/explore',

View File

@ -7,7 +7,7 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte'; import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
import { useAlbums } from './albums.bloc'; import { useAlbums } from './albums.bloc';
import empty1Url from '$lib/assets/empty-1.svg'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
@ -57,17 +57,11 @@
<!-- Empty Message --> <!-- Empty Message -->
{#if $albums.length === 0} {#if $albums.length === 0}
<div <EmptyPlaceholder
on:click={handleCreateAlbum} text="Create an album to organize your photos and videos"
on:keydown={handleCreateAlbum} actionHandler={handleCreateAlbum}
class="border dark:border-immich-dark-gray hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center" alt="Empty albums"
> />
<img src={empty1Url} alt="Empty shared album" width="500" draggable="false" />
<p class="text-center text-immich-text-gray-500 dark:text-immich-dark-fg">
Create an album to organize your photos and videos
</p>
</div>
{/if} {/if}
</UserPageLayout> </UserPageLayout>

View File

@ -0,0 +1,16 @@
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { AppRoute } from '$lib/constants';
export const load = (async ({ locals: { user } }) => {
if (!user) {
throw redirect(302, AppRoute.AUTH_LOGIN);
}
return {
user,
meta: {
title: 'Archive'
}
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,259 @@
<script lang="ts">
import { goto } from '$app/navigation';
import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
import {
notificationController,
NotificationType
} from '$lib/components/shared-components/notification/notification';
import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils';
import { AlbumResponseDto, api, AssetResponseDto, SharedLinkType } from '@api';
import Close from 'svelte-material-icons/Close.svelte';
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import Plus from 'svelte-material-icons/Plus.svelte';
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
import { locale } from '$lib/stores/preferences.store';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import type { PageData } from './$types';
import { onMount } from 'svelte';
import { handleError } from '$lib/utils/handle-error';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
export let data: PageData;
onMount(async () => {
try {
const { data: assets } = await api.assetApi.getAllAssets(undefined, true);
archived = assets;
} catch {
handleError(Error, 'Unable to load archived assets');
}
});
const clearMultiSelectAssetAssetHandler = () => {
selectedAssets = new Set();
};
const deleteSelectedAssetHandler = async () => {
try {
if (
window.confirm(
`Caution! Are you sure you want to delete ${selectedAssets.size} assets? This step also deletes assets in the album(s) to which they belong. You can not undo this action!`
)
) {
const { data: deletedAssets } = await api.assetApi.deleteAsset({
ids: Array.from(selectedAssets).map((a) => a.id)
});
for (const asset of deletedAssets) {
if (asset.status == 'SUCCESS') {
archived = archived.filter((a) => a.id != asset.id);
}
}
clearMultiSelectAssetAssetHandler();
}
} catch (e) {
notificationController.show({
type: NotificationType.Error,
message: 'Error deleting assets, check console for more details'
});
console.error('Error deleteSelectedAssetHandler', e);
}
};
$: isMultiSelectionMode = selectedAssets.size > 0;
let selectedAssets: Set<AssetResponseDto> = new Set();
let archived: AssetResponseDto[] = [];
let contextMenuPosition = { x: 0, y: 0 };
let isShowCreateSharedLinkModal = false;
let isShowAddMenu = false;
let isShowAlbumPicker = false;
let addToSharedAlbum = false;
const handleShowMenu = ({ x, y }: MouseEvent) => {
contextMenuPosition = { x, y };
isShowAddMenu = !isShowAddMenu;
};
const handleAddToFavorites = () => {
isShowAddMenu = false;
let cnt = 0;
for (const asset of selectedAssets) {
if (!asset.isFavorite) {
api.assetApi.updateAsset(asset.id, {
isFavorite: true
});
cnt = cnt + 1;
}
}
notificationController.show({
message: `Added ${cnt} to favorites`,
type: NotificationType.Info
});
clearMultiSelectAssetAssetHandler();
};
const handleShowAlbumPicker = (shared: boolean) => {
isShowAddMenu = false;
isShowAlbumPicker = true;
addToSharedAlbum = shared;
};
const handleAddToNewAlbum = (event: CustomEvent) => {
isShowAlbumPicker = false;
const { albumName }: { albumName: string } = event.detail;
const assetIds = Array.from(selectedAssets).map((asset) => asset.id);
api.albumApi.createAlbum({ albumName, assetIds }).then((response) => {
const { id, albumName } = response.data;
notificationController.show({
message: `Added ${assetIds.length} to ${albumName}`,
type: NotificationType.Info
});
clearMultiSelectAssetAssetHandler();
goto('/albums/' + id);
});
};
const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => {
isShowAlbumPicker = false;
const album = event.detail.album;
const assetIds = Array.from(selectedAssets).map((asset) => asset.id);
addAssetsToAlbum(album.id, assetIds).then(() => {
clearMultiSelectAssetAssetHandler();
});
};
const handleDownloadFiles = async () => {
await bulkDownload('immich', Array.from(selectedAssets), () => {
clearMultiSelectAssetAssetHandler();
});
};
const handleUnarchive = async () => {
let cnt = 0;
for (const asset of selectedAssets) {
if (asset.isArchived) {
api.assetApi.updateAsset(asset.id, {
isArchived: false
});
cnt = cnt + 1;
archived = archived.filter((a) => a.id != asset.id);
}
}
notificationController.show({
message: `Removed ${cnt} from archive`,
type: NotificationType.Info
});
clearMultiSelectAssetAssetHandler();
};
const handleCreateSharedLink = async () => {
isShowCreateSharedLinkModal = true;
};
const handleCloseSharedLinkModal = () => {
clearMultiSelectAssetAssetHandler();
isShowCreateSharedLinkModal = false;
};
</script>
<UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode}>
<!-- Empty Message -->
{#if archived.length === 0}
<EmptyPlaceholder
text="Archive photos and videos to hide them from your Photos view"
alt="Empty archive"
/>
{/if}
<svelte:fragment slot="header">
{#if isMultiSelectionMode}
<ControlAppBar
on:close-button-click={clearMultiSelectAssetAssetHandler}
backIcon={Close}
tailwindClasses={'bg-white shadow-md'}
>
<svelte:fragment slot="leading">
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">
Selected {selectedAssets.size.toLocaleString($locale)}
</p>
</svelte:fragment>
<svelte:fragment slot="trailing">
<CircleIconButton
title="Share"
logo={ShareVariantOutline}
on:click={handleCreateSharedLink}
/>
<CircleIconButton
title="Unarchive"
logo={ArchiveArrowUpOutline}
on:click={handleUnarchive}
/>
<CircleIconButton
title="Download"
logo={CloudDownloadOutline}
on:click={handleDownloadFiles}
/>
<CircleIconButton title="Add" logo={Plus} on:click={handleShowMenu} />
<CircleIconButton
title="Delete"
logo={DeleteOutline}
on:click={deleteSelectedAssetHandler}
/>
</svelte:fragment>
</ControlAppBar>
{/if}
{#if isShowAddMenu}
<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowAddMenu = false)}>
<div class="flex flex-col rounded-lg ">
<MenuOption on:click={handleAddToFavorites} text="Add to Favorites" />
<MenuOption on:click={() => handleShowAlbumPicker(false)} text="Add to Album" />
<MenuOption on:click={() => handleShowAlbumPicker(true)} text="Add to Shared Album" />
</div>
</ContextMenu>
{/if}
{#if isShowAlbumPicker}
<AlbumSelectionModal
shared={addToSharedAlbum}
on:newAlbum={handleAddToNewAlbum}
on:newSharedAlbum={handleAddToNewAlbum}
on:album={handleAddToAlbum}
on:close={() => (isShowAlbumPicker = false)}
/>
{/if}
{#if isShowCreateSharedLinkModal}
<CreateSharedLinkModal
sharedAssets={Array.from(selectedAssets)}
shareType={SharedLinkType.Individual}
on:close={handleCloseSharedLinkModal}
/>
{/if}
</svelte:fragment>
<GalleryViewer assets={archived} bind:selectedAssets />
</UserPageLayout>

View File

@ -10,7 +10,7 @@
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
import StarMinusOutline from 'svelte-material-icons/StarMinusOutline.svelte'; import StarMinusOutline from 'svelte-material-icons/StarMinusOutline.svelte';
import Error from '../../+error.svelte'; import Error from '../../+error.svelte';
import empty1Url from '$lib/assets/empty-1.svg'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -24,7 +24,7 @@
onMount(async () => { onMount(async () => {
try { try {
const { data: assets } = await api.assetApi.getAllAssets(true); const { data: assets } = await api.assetApi.getAllAssets(true, undefined);
favorites = assets; favorites = assets;
} catch { } catch {
handleError(Error, 'Unable to load favorites'); handleError(Error, 'Unable to load favorites');
@ -96,19 +96,14 @@
/> />
{/if} {/if}
<UserPageLayout user={data.user} title={data.meta.title} hideNavbar={isMultiSelectionMode}> <UserPageLayout user={data.user} hideNavbar={isMultiSelectionMode}>
<section> <section>
<!-- Empty Message --> <!-- Empty Message -->
{#if favorites.length === 0} {#if favorites.length === 0}
<div <EmptyPlaceholder
class="border dark:border-immich-dark-gray hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center" text="Add favorites to quickly find your best pictures and videos"
> alt="Empty favorites"
<img src={empty1Url} alt="Empty shared album" width="500" draggable="false" /> />
<p class="text-center text-immich-text-gray-500 dark:text-immich-dark-fg">
Add favorites to quickly find your best pictures and videos
</p>
</div>
{/if} {/if}
<GalleryViewer assets={favorites} bind:selectedAssets /> <GalleryViewer assets={favorites} bind:selectedAssets />

View File

@ -19,6 +19,7 @@
import { assetStore } from '$lib/stores/assets.store'; import { assetStore } from '$lib/stores/assets.store';
import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils'; import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils';
import { AlbumResponseDto, api, SharedLinkType } from '@api'; import { AlbumResponseDto, api, SharedLinkType } from '@api';
import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte';
import Close from 'svelte-material-icons/Close.svelte'; import Close from 'svelte-material-icons/Close.svelte';
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
@ -69,6 +70,27 @@
isShowAddMenu = !isShowAddMenu; isShowAddMenu = !isShowAddMenu;
}; };
const handleArchive = async () => {
let cnt = 0;
for (const asset of $selectedAssets) {
if (!asset.isArchived) {
api.assetApi.updateAsset(asset.id, {
isArchived: true
});
assetStore.removeAsset(asset.id);
cnt = cnt + 1;
}
}
notificationController.show({
message: `Archived ${cnt}`,
type: NotificationType.Info
});
assetInteractionStore.clearMultiselect();
};
const handleAddToFavorites = () => { const handleAddToFavorites = () => {
isShowAddMenu = false; isShowAddMenu = false;
@ -162,6 +184,11 @@
logo={ShareVariantOutline} logo={ShareVariantOutline}
on:click={handleCreateSharedLink} on:click={handleCreateSharedLink}
/> />
<CircleIconButton
title="Archive"
logo={ArchiveArrowDownOutline}
on:click={handleArchive}
/>
<CircleIconButton <CircleIconButton
title="Download" title="Download"
logo={CloudDownloadOutline} logo={CloudDownloadOutline}

View File

@ -24,6 +24,7 @@ export const load = (async ({ locals, parent, url }) => {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
{ params: url.searchParams } { params: url.searchParams }
); );