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

refactor(server): asset stats (#3253)

* refactor(server): asset stats

* chore: open api
This commit is contained in:
Jason Rasmussen 2023-07-14 09:30:17 -04:00 committed by GitHub
parent 05e1a6d949
commit f952bc0b64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 351 additions and 490 deletions

View File

@ -486,43 +486,6 @@ export interface AssetCountByTimeBucketResponseDto {
*/
'buckets': Array<AssetCountByTimeBucket>;
}
/**
*
* @export
* @interface AssetCountByUserIdResponseDto
*/
export interface AssetCountByUserIdResponseDto {
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'audio': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'photos': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'videos': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'other': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'total': number;
}
/**
*
* @export
@ -724,6 +687,31 @@ export interface AssetResponseDto {
}
/**
*
* @export
* @interface AssetStatsResponseDto
*/
export interface AssetStatsResponseDto {
/**
*
* @type {number}
* @memberof AssetStatsResponseDto
*/
'images': number;
/**
*
* @type {number}
* @memberof AssetStatsResponseDto
*/
'videos': number;
/**
*
* @type {number}
* @memberof AssetStatsResponseDto
*/
'total': number;
}
/**
*
* @export
@ -4892,44 +4880,6 @@ 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 api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// 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};
@ -5079,8 +5029,8 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/count-by-user-id`;
getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/search-terms`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@ -5114,11 +5064,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/search-terms`;
getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/statistics`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@ -5139,6 +5091,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (isArchived !== undefined) {
localVarQueryParameter['isArchived'] = isArchived;
}
if (isFavorite !== undefined) {
localVarQueryParameter['isFavorite'] = isFavorite;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -5887,15 +5847,6 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, 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);
},
/**
* Get a single asset\'s information
* @param {string} id
@ -5932,17 +5883,19 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByUserIdResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByUserId(options);
async getAssetSearchTerms(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetSearchTerms(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetSearchTerms(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetSearchTerms(options);
async getAssetStats(isArchived?: boolean, isFavorite?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetStatsResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetStats(isArchived, isFavorite, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -6160,14 +6113,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getArchivedAssetCountByUserId(options?: AxiosRequestConfig): AxiosPromise<AssetCountByUserIdResponseDto> {
return localVarFp.getArchivedAssetCountByUserId(options).then((request) => request(axios, basePath));
},
/**
* Get a single asset\'s information
* @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
@ -6200,16 +6145,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByUserId(options?: AxiosRequestConfig): AxiosPromise<AssetCountByUserIdResponseDto> {
return localVarFp.getAssetCountByUserId(options).then((request) => request(axios, basePath));
getAssetSearchTerms(options?: AxiosRequestConfig): AxiosPromise<Array<string>> {
return localVarFp.getAssetSearchTerms(options).then((request) => request(axios, basePath));
},
/**
*
* @param {AssetApiGetAssetStatsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetSearchTerms(options?: AxiosRequestConfig): AxiosPromise<Array<string>> {
return localVarFp.getAssetSearchTerms(options).then((request) => request(axios, basePath));
getAssetStats(requestParameters: AssetApiGetAssetStatsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<AssetStatsResponseDto> {
return localVarFp.getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, options).then((request) => request(axios, basePath));
},
/**
*
@ -6523,6 +6469,27 @@ export interface AssetApiGetAssetCountByTimeBucketRequest {
readonly getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto
}
/**
* Request parameters for getAssetStats operation in AssetApi.
* @export
* @interface AssetApiGetAssetStatsRequest
*/
export interface AssetApiGetAssetStatsRequest {
/**
*
* @type {boolean}
* @memberof AssetApiGetAssetStats
*/
readonly isArchived?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetAssetStats
*/
readonly isFavorite?: boolean
}
/**
* Request parameters for getAssetThumbnail operation in AssetApi.
* @export
@ -6915,16 +6882,6 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.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));
}
/**
* Get a single asset\'s information
* @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
@ -6964,18 +6921,19 @@ export class AssetApi extends BaseAPI {
* @throws {RequiredError}
* @memberof AssetApi
*/
public getAssetCountByUserId(options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetCountByUserId(options).then((request) => request(this.axios, this.basePath));
public getAssetSearchTerms(options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetSearchTerms(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {AssetApiGetAssetStatsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public getAssetSearchTerms(options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetSearchTerms(options).then((request) => request(this.axios, this.basePath));
public getAssetStats(requestParameters: AssetApiGetAssetStatsRequest = {}, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, options).then((request) => request(this.axios, this.basePath));
}
/**

View File

@ -23,11 +23,11 @@ doc/AssetBulkUploadCheckResponseDto.md
doc/AssetBulkUploadCheckResult.md
doc/AssetCountByTimeBucket.md
doc/AssetCountByTimeBucketResponseDto.md
doc/AssetCountByUserIdResponseDto.md
doc/AssetFileUploadResponseDto.md
doc/AssetIdsDto.md
doc/AssetIdsResponseDto.md
doc/AssetResponseDto.md
doc/AssetStatsResponseDto.md
doc/AssetTypeEnum.md
doc/AudioCodec.md
doc/AuthDeviceResponseDto.md
@ -163,11 +163,11 @@ lib/model/asset_bulk_upload_check_response_dto.dart
lib/model/asset_bulk_upload_check_result.dart
lib/model/asset_count_by_time_bucket.dart
lib/model/asset_count_by_time_bucket_response_dto.dart
lib/model/asset_count_by_user_id_response_dto.dart
lib/model/asset_file_upload_response_dto.dart
lib/model/asset_ids_dto.dart
lib/model/asset_ids_response_dto.dart
lib/model/asset_response_dto.dart
lib/model/asset_stats_response_dto.dart
lib/model/asset_type_enum.dart
lib/model/audio_codec.dart
lib/model/auth_device_response_dto.dart
@ -272,11 +272,11 @@ test/asset_bulk_upload_check_response_dto_test.dart
test/asset_bulk_upload_check_result_test.dart
test/asset_count_by_time_bucket_response_dto_test.dart
test/asset_count_by_time_bucket_test.dart
test/asset_count_by_user_id_response_dto_test.dart
test/asset_file_upload_response_dto_test.dart
test/asset_ids_dto_test.dart
test/asset_ids_response_dto_test.dart
test/asset_response_dto_test.dart
test/asset_stats_response_dto_test.dart
test/asset_type_enum_test.dart
test/audio_codec_test.dart
test/auth_device_response_dto_test.dart

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.

View File

@ -984,38 +984,6 @@
]
}
},
"/asset/count-by-user-id": {
"get": {
"operationId": "getAssetCountByUserId",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetCountByUserIdResponseDto"
}
}
}
}
},
"tags": [
"Asset"
],
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
]
}
},
"/asset/curated-locations": {
"get": {
"operationId": "getCuratedLocations",
@ -1608,17 +1576,34 @@
]
}
},
"/asset/stat/archive": {
"/asset/statistics": {
"get": {
"operationId": "getArchivedAssetCountByUserId",
"parameters": [],
"operationId": "getAssetStats",
"parameters": [
{
"name": "isArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isFavorite",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetCountByUserIdResponseDto"
"$ref": "#/components/schemas/AssetStatsResponseDto"
}
}
}
@ -4786,38 +4771,6 @@
"buckets"
]
},
"AssetCountByUserIdResponseDto": {
"type": "object",
"properties": {
"audio": {
"type": "integer",
"default": 0
},
"photos": {
"type": "integer",
"default": 0
},
"videos": {
"type": "integer",
"default": 0
},
"other": {
"type": "integer",
"default": 0
},
"total": {
"type": "integer",
"default": 0
}
},
"required": [
"audio",
"photos",
"videos",
"other",
"total"
]
},
"AssetFileUploadResponseDto": {
"type": "object",
"properties": {
@ -4970,6 +4923,25 @@
"checksum"
]
},
"AssetStatsResponseDto": {
"type": "object",
"properties": {
"images": {
"type": "integer"
},
"videos": {
"type": "integer"
},
"total": {
"type": "integer"
}
},
"required": [
"images",
"videos",
"total"
]
},
"AssetTypeEnum": {
"type": "string",
"enum": [

View File

@ -1,6 +1,13 @@
import { AssetEntity, AssetType } from '@app/infra/entities';
import { Paginated, PaginationOptions } from '../domain.util';
export type AssetStats = Record<AssetType, number>;
export interface AssetStatsOptions {
isFavorite?: boolean;
isArchived?: boolean;
}
export interface AssetSearchOptions {
isVisible?: boolean;
type?: AssetType;
@ -55,4 +62,5 @@ export interface IAssetRepository {
save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
}

View File

@ -1,3 +1,4 @@
import { AssetType } from '@app/infra/entities';
import { BadRequestException } from '@nestjs/common';
import {
assetEntityStub,
@ -10,9 +11,9 @@ import {
import { when } from 'jest-when';
import { Readable } from 'stream';
import { IStorageRepository } from '../storage';
import { IAssetRepository } from './asset.repository';
import { AssetStats, IAssetRepository } from './asset.repository';
import { AssetService } from './asset.service';
import { DownloadResponseDto } from './index';
import { AssetStatsResponseDto, DownloadResponseDto } from './dto';
import { mapAsset } from './response-dto';
const downloadResponse: DownloadResponseDto = {
@ -25,6 +26,19 @@ const downloadResponse: DownloadResponseDto = {
],
};
const stats: AssetStats = {
[AssetType.IMAGE]: 10,
[AssetType.VIDEO]: 23,
[AssetType.AUDIO]: 0,
[AssetType.OTHER]: 0,
};
const statResponse: AssetStatsResponseDto = {
images: 10,
videos: 23,
total: 33,
};
describe(AssetService.name, () => {
let sut: AssetService;
let accessMock: IAccessRepositoryMock;
@ -287,4 +301,30 @@ describe(AssetService.name, () => {
});
});
});
describe('getStatistics', () => {
it('should get the statistics for a user, excluding archived assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: false });
});
it('should get the statistics for a user for archived assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isArchived: true });
});
it('should get the statistics for a user for favorite assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, { isFavorite: true });
});
it('should get the statistics for a user for all assets', async () => {
assetMock.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse);
expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, {});
});
});
});

View File

@ -9,6 +9,7 @@ import { HumanReadableSize, usePagination } from '../domain.util';
import { ImmichReadStream, IStorageRepository } from '../storage';
import { IAssetRepository } from './asset.repository';
import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto';
import { MapMarkerDto } from './dto/map-marker.dto';
import { mapAsset, MapMarkerResponseDto } from './response-dto';
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
@ -155,4 +156,9 @@ export class AssetService {
throw new BadRequestException('assetIds, albumId, or userId is required');
}
async getStatistics(authUser: AuthUserDto, dto: AssetStatsDto) {
const stats = await this.assetRepository.getStatistics(authUser.id, dto);
return mapStats(stats);
}
}

View File

@ -0,0 +1,37 @@
import { AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
import { toBoolean } from '../../domain.util';
import { AssetStats } from '../asset.repository';
export class AssetStatsDto {
@IsBoolean()
@Transform(toBoolean)
@IsOptional()
isArchived?: boolean;
@IsBoolean()
@Transform(toBoolean)
@IsOptional()
isFavorite?: boolean;
}
export class AssetStatsResponseDto {
@ApiProperty({ type: 'integer' })
images!: number;
@ApiProperty({ type: 'integer' })
videos!: number;
@ApiProperty({ type: 'integer' })
total!: number;
}
export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
return {
images: stats[AssetType.IMAGE],
videos: stats[AssetType.VIDEO],
total: Object.values(stats).reduce((total, value) => total + value, 0),
};
};

View File

@ -1,4 +1,5 @@
export * from './asset-ids.dto';
export * from './asset-statistics.dto';
export * from './download.dto';
export * from './map-marker.dto';
export * from './memory-lane.dto';

View File

@ -1,4 +1,4 @@
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { AssetEntity, ExifEntity } from '@app/infra/entities';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Not } from 'typeorm';
@ -11,7 +11,6 @@ import { GetAssetCountByTimeBucketDto, TimeGroupEnum } from './dto/get-asset-cou
import { SearchPropertiesDto } from './dto/search-properties.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
@ -38,8 +37,6 @@ export interface IAssetRepository {
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
getAssetCountByTimeBucket(userId: string, dto: GetAssetCountByTimeBucketDto): Promise<AssetCountByTimeBucket[]>;
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
getArchivedAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
@ -55,35 +52,6 @@ export class AssetRepository implements IAssetRepository {
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
) {}
async getAssetCountByUserId(ownerId: string): Promise<AssetCountByUserIdResponseDto> {
// Get 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')
.groupBy('asset.type')
.getRawMany();
return this.getAssetCount(items);
}
async getArchivedAssetCountByUserId(ownerId: string): Promise<AssetCountByUserIdResponseDto> {
// 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();
return this.getAssetCount(items);
}
async getAssetByTimeBucket(userId: string, dto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
// Get asset entity from a list of time buckets
let builder = this.assetRepository
@ -337,29 +305,6 @@ export class AssetRepository implements IAssetRepository {
return assets.map((asset) => asset.deviceAssetId);
}
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;
}
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null> {
return this.assetRepository.findOne({
select: {

View File

@ -38,7 +38,6 @@ import { ServeFileDto } from './dto/serve-file.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto';
import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by-time-group-response.dto';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
@ -173,15 +172,6 @@ export class AssetController {
return this.assetService.getAssetCountByTimeBucket(authUser, dto);
}
@Get('/count-by-user-id')
getAssetCountByUserId(@AuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this.assetService.getAssetCountByUserId(authUser);
}
@Get('/stat/archive')
getArchivedAssetCountByUserId(@AuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this.assetService.getArchivedAssetCountByUserId(authUser);
}
/**
* Get all AssetEntity belong to the user
*/

View File

@ -26,7 +26,6 @@ import { CreateAssetDto } from './dto/create-asset.dto';
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
import { AssetRejectReason, AssetUploadAction } from './response-dto/asset-check-response.dto';
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
const _getCreateAssetDto = (): CreateAssetDto => {
const createAssetDto = new CreateAssetDto();
@ -103,24 +102,6 @@ const _getAssetCountByTimeBucket = (): AssetCountByTimeBucket[] => {
return [result1, result2];
};
const _getAssetCountByUserId = (): AssetCountByUserIdResponseDto => {
const result = new AssetCountByUserIdResponseDto();
result.videos = 2;
result.photos = 2;
return result;
};
const _getArchivedAssetsCountByUserId = (): AssetCountByUserIdResponseDto => {
const result = new AssetCountByUserIdResponseDto();
result.videos = 1;
result.photos = 2;
return result;
};
const uploadFile = {
nullAuth: {
authUser: null,
@ -197,8 +178,6 @@ describe('AssetService', () => {
getSearchPropertiesByUserId: jest.fn(),
getAssetByTimeBucket: jest.fn(),
getAssetsByChecksums: jest.fn(),
getAssetCountByUserId: jest.fn(),
getArchivedAssetCountByUserId: jest.fn(),
getExistingAssets: jest.fn(),
getByOriginalPath: jest.fn(),
};
@ -467,20 +446,6 @@ describe('AssetService', () => {
expect(result.buckets.length).toEqual(2);
});
it('get asset count by user id', async () => {
const assetCount = _getAssetCountByUserId();
assetRepositoryMock.getAssetCountByUserId.mockResolvedValue(assetCount);
await expect(sut.getAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount);
});
it('get archived asset count by user id', async () => {
const assetCount = _getArchivedAssetsCountByUserId();
assetRepositoryMock.getArchivedAssetCountByUserId.mockResolvedValue(assetCount);
await expect(sut.getArchivedAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount);
});
describe('deleteAll', () => {
it('should return failed status when an asset is missing', async () => {
assetRepositoryMock.get.mockResolvedValue(null);

View File

@ -58,7 +58,6 @@ import {
AssetCountByTimeBucketResponseDto,
mapAssetCountByTimeBucket,
} from './response-dto/asset-count-by-time-group-response.dto';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
@ -536,14 +535,6 @@ export class AssetService {
return mapAssetCountByTimeBucket(result);
}
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this._assetRepository.getAssetCountByUserId(authUser.id);
}
getArchivedAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this._assetRepository.getArchivedAssetCountByUserId(authUser.id);
}
getExifPermission(authUser: AuthUserDto) {
return !authUser.isPublicUser || authUser.isShowExif;
}

View File

@ -1,18 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
export class AssetCountByUserIdResponseDto {
@ApiProperty({ type: 'integer' })
audio = 0;
@ApiProperty({ type: 'integer' })
photos = 0;
@ApiProperty({ type: 'integer' })
videos = 0;
@ApiProperty({ type: 'integer' })
other = 0;
@ApiProperty({ type: 'integer' })
total = 0;
}

View File

@ -1,6 +1,8 @@
import {
AssetIdsDto,
AssetService,
AssetStatsDto,
AssetStatsResponseDto,
AuthUserDto,
DownloadDto,
DownloadResponseDto,
@ -53,4 +55,9 @@ export class AssetController {
downloadFile(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) {
return this.service.downloadFile(authUser, id).then(asStreamableFile);
}
@Get('statistics')
getAssetStats(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
return this.service.getStatistics(authUser, dto);
}
}

View File

@ -1,5 +1,7 @@
import {
AssetSearchOptions,
AssetStats,
AssetStatsOptions,
IAssetRepository,
LivePhotoSearchOptions,
MapMarker,
@ -321,4 +323,38 @@ export class AssetRepository implements IAssetRepository {
lon: asset.exifInfo!.longitude!,
}));
}
async getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats> {
let builder = await this.repository
.createQueryBuilder('asset')
.select(`COUNT(asset.id)`, 'count')
.addSelect(`asset.type`, 'type')
.where('"ownerId" = :ownerId', { ownerId })
.andWhere('asset.isVisible = true')
.groupBy('asset.type');
const { isArchived, isFavorite } = options;
if (isArchived !== undefined) {
builder = builder.andWhere(`asset.isArchived = :isArchived`, { isArchived });
}
if (isFavorite !== undefined) {
builder = builder.andWhere(`asset.isFavorite = :isFavorite`, { isFavorite });
}
const items = await builder.getRawMany();
const result: AssetStats = {
[AssetType.AUDIO]: 0,
[AssetType.IMAGE]: 0,
[AssetType.VIDEO]: 0,
[AssetType.OTHER]: 0,
};
for (const item of items) {
result[item.type as AssetType] = Number(item.count) || 0;
}
return result;
}
}

View File

@ -18,5 +18,6 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
save: jest.fn(),
findLivePhotoMatch: jest.fn(),
getMapMarkers: jest.fn(),
getStatistics: jest.fn(),
};
};

View File

@ -486,43 +486,6 @@ export interface AssetCountByTimeBucketResponseDto {
*/
'buckets': Array<AssetCountByTimeBucket>;
}
/**
*
* @export
* @interface AssetCountByUserIdResponseDto
*/
export interface AssetCountByUserIdResponseDto {
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'audio': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'photos': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'videos': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'other': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'total': number;
}
/**
*
* @export
@ -724,6 +687,31 @@ export interface AssetResponseDto {
}
/**
*
* @export
* @interface AssetStatsResponseDto
*/
export interface AssetStatsResponseDto {
/**
*
* @type {number}
* @memberof AssetStatsResponseDto
*/
'images': number;
/**
*
* @type {number}
* @memberof AssetStatsResponseDto
*/
'videos': number;
/**
*
* @type {number}
* @memberof AssetStatsResponseDto
*/
'total': number;
}
/**
*
* @export
@ -4901,44 +4889,6 @@ 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 api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// 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};
@ -5088,8 +5038,8 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/count-by-user-id`;
getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/search-terms`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@ -5123,11 +5073,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/search-terms`;
getAssetStats: async (isArchived?: boolean, isFavorite?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/statistics`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@ -5148,6 +5100,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (isArchived !== undefined) {
localVarQueryParameter['isArchived'] = isArchived;
}
if (isFavorite !== undefined) {
localVarQueryParameter['isFavorite'] = isFavorite;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -5896,15 +5856,6 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, 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);
},
/**
* Get a single asset\'s information
* @param {string} id
@ -5941,17 +5892,19 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByUserIdResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByUserId(options);
async getAssetSearchTerms(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetSearchTerms(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetSearchTerms(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<string>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetSearchTerms(options);
async getAssetStats(isArchived?: boolean, isFavorite?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetStatsResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetStats(isArchived, isFavorite, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -6177,14 +6130,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> {
return localVarFp.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, 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
* @param {string} id
@ -6218,16 +6163,18 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByUserId(options?: any): AxiosPromise<AssetCountByUserIdResponseDto> {
return localVarFp.getAssetCountByUserId(options).then((request) => request(axios, basePath));
getAssetSearchTerms(options?: any): AxiosPromise<Array<string>> {
return localVarFp.getAssetSearchTerms(options).then((request) => request(axios, basePath));
},
/**
*
* @param {boolean} [isArchived]
* @param {boolean} [isFavorite]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetSearchTerms(options?: any): AxiosPromise<Array<string>> {
return localVarFp.getAssetSearchTerms(options).then((request) => request(axios, basePath));
getAssetStats(isArchived?: boolean, isFavorite?: boolean, options?: any): AxiosPromise<AssetStatsResponseDto> {
return localVarFp.getAssetStats(isArchived, isFavorite, options).then((request) => request(axios, basePath));
},
/**
*
@ -6565,6 +6512,27 @@ export interface AssetApiGetAssetCountByTimeBucketRequest {
readonly getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto
}
/**
* Request parameters for getAssetStats operation in AssetApi.
* @export
* @interface AssetApiGetAssetStatsRequest
*/
export interface AssetApiGetAssetStatsRequest {
/**
*
* @type {boolean}
* @memberof AssetApiGetAssetStats
*/
readonly isArchived?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetAssetStats
*/
readonly isFavorite?: boolean
}
/**
* Request parameters for getAssetThumbnail operation in AssetApi.
* @export
@ -6957,16 +6925,6 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.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));
}
/**
* Get a single asset\'s information
* @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters.
@ -7006,18 +6964,19 @@ export class AssetApi extends BaseAPI {
* @throws {RequiredError}
* @memberof AssetApi
*/
public getAssetCountByUserId(options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetCountByUserId(options).then((request) => request(this.axios, this.basePath));
public getAssetSearchTerms(options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetSearchTerms(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {AssetApiGetAssetStatsRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public getAssetSearchTerms(options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetSearchTerms(options).then((request) => request(this.axios, this.basePath));
public getAssetStats(requestParameters: AssetApiGetAssetStatsRequest = {}, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetStats(requestParameters.isArchived, requestParameters.isFavorite, options).then((request) => request(this.axios, this.basePath));
}
/**

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { api } from '@api';
import { AssetApiGetAssetStatsRequest, api } from '@api';
import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
import AccountMultiple from 'svelte-material-icons/AccountMultiple.svelte';
import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
@ -18,31 +18,9 @@
import { locale } from '$lib/stores/preferences.store';
import SideBarSection from './side-bar-section.svelte';
const getAssetCount = async () => {
const { data: allAssetCount } = await api.assetApi.getAssetCountByUserId();
const { data: archivedCount } = await api.assetApi.getArchivedAssetCountByUserId();
return {
videos: allAssetCount.videos - archivedCount.videos,
photos: allAssetCount.photos - archivedCount.photos,
};
};
const getFavoriteCount = async () => {
try {
const { data: assets } = await api.assetApi.getAllAssets({
isFavorite: true,
withoutThumbs: true,
});
return {
favorites: assets.length,
};
} catch {
return {
favorites: 0,
};
}
const getStats = async (dto: AssetApiGetAssetStatsRequest) => {
const { data: stats } = await api.assetApi.getAssetStats(dto);
return stats;
};
const getAlbumCount = async () => {
@ -54,22 +32,6 @@
}
};
const getArchivedAssetsCount = async () => {
try {
const { data: assetCount } = await api.assetApi.getArchivedAssetCountByUserId();
return {
videos: assetCount.videos,
photos: assetCount.photos,
};
} catch {
return {
videos: 0,
photos: 0,
};
}
};
const isFavoritesSelected = $page.route.id === '/(user)/favorites';
const isPhotosSelected = $page.route.id === '/(user)/photos';
const isSharingSelected = $page.route.id === '/(user)/sharing';
@ -83,12 +45,12 @@
isSelected={isPhotosSelected}
>
<svelte:fragment slot="moreInformation">
{#await getAssetCount()}
{#await getStats({ isArchived: false })}
<LoadingSpinner />
{:then data}
<div>
<p>{data.videos.toLocaleString($locale)} Videos</p>
<p>{data.photos.toLocaleString($locale)} Photos</p>
<p>{data.images.toLocaleString($locale)} Photos</p>
</div>
{/await}
</svelte:fragment>
@ -129,11 +91,12 @@
isSelected={isFavoritesSelected}
>
<svelte:fragment slot="moreInformation">
{#await getFavoriteCount()}
{#await getStats({ isFavorite: true })}
<LoadingSpinner />
{:then data}
<div>
<p>{data.favorites} Favorites</p>
<p>{data.videos.toLocaleString($locale)} Videos</p>
<p>{data.images.toLocaleString($locale)} Photos</p>
</div>
{/await}
</svelte:fragment>
@ -155,12 +118,12 @@
<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()}
{#await getStats({ isArchived: true })}
<LoadingSpinner />
{:then data}
<div>
<p>{data.videos.toLocaleString($locale)} Videos</p>
<p>{data.photos.toLocaleString($locale)} Photos</p>
<p>{data.images.toLocaleString($locale)} Photos</p>
</div>
{/await}
</svelte:fragment>

View File

@ -10,22 +10,22 @@
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { assetInteractionStore, isMultiSelectStoreState, selectedAssets } from '$lib/stores/asset-interaction.store';
import { assetStore } from '$lib/stores/assets.store';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { api } from '@api';
import { onDestroy, onMount } from 'svelte';
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
import Plus from 'svelte-material-icons/Plus.svelte';
import type { PageData } from './$types';
import { api } from '@api';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
export let data: PageData;
let assetCount = 1;
onMount(async () => {
const { data: allAssetCount } = await api.assetApi.getAssetCountByUserId();
assetCount = allAssetCount.total;
const { data: stats } = await api.assetApi.getAssetStats();
assetCount = stats.total;
});
onDestroy(() => {