1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(web): add asset and album count info (#623)

* Get asset and album count

* Generate APIs

* Added asset count for each type

* Added api on the web

* Added info button for asset and album count to trigger getting info on hover

* Remove websocket event from photo page
This commit is contained in:
Alex 2022-09-07 15:16:18 -05:00 committed by GitHub
parent 18a7ff8726
commit 566039b93f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 404 additions and 39 deletions

View File

@ -6,10 +6,12 @@ doc/AddAssetsDto.md
doc/AddUsersDto.md doc/AddUsersDto.md
doc/AdminSignupResponseDto.md doc/AdminSignupResponseDto.md
doc/AlbumApi.md doc/AlbumApi.md
doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md doc/AlbumResponseDto.md
doc/AssetApi.md doc/AssetApi.md
doc/AssetCountByTimeBucket.md doc/AssetCountByTimeBucket.md
doc/AssetCountByTimeBucketResponseDto.md doc/AssetCountByTimeBucketResponseDto.md
doc/AssetCountByUserIdResponseDto.md
doc/AssetFileUploadResponseDto.md doc/AssetFileUploadResponseDto.md
doc/AssetResponseDto.md doc/AssetResponseDto.md
doc/AssetTypeEnum.md doc/AssetTypeEnum.md
@ -70,9 +72,11 @@ lib/auth/oauth.dart
lib/model/add_assets_dto.dart lib/model/add_assets_dto.dart
lib/model/add_users_dto.dart lib/model/add_users_dto.dart
lib/model/admin_signup_response_dto.dart lib/model/admin_signup_response_dto.dart
lib/model/album_count_response_dto.dart
lib/model/album_response_dto.dart lib/model/album_response_dto.dart
lib/model/asset_count_by_time_bucket.dart lib/model/asset_count_by_time_bucket.dart
lib/model/asset_count_by_time_bucket_response_dto.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_file_upload_response_dto.dart
lib/model/asset_response_dto.dart lib/model/asset_response_dto.dart
lib/model/asset_type_enum.dart lib/model/asset_type_enum.dart
@ -111,3 +115,4 @@ lib/model/user_count_response_dto.dart
lib/model/user_response_dto.dart lib/model/user_response_dto.dart
lib/model/validate_access_token_response_dto.dart lib/model/validate_access_token_response_dto.dart
pubspec.yaml pubspec.yaml
test/asset_count_by_user_id_response_dto_test.dart

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

@ -10,6 +10,7 @@ import { CreateAlbumDto } from './dto/create-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto'; import { GetAlbumsDto } from './dto/get-albums.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import { AlbumResponseDto } from './response-dto/album-response.dto'; import { AlbumResponseDto } from './response-dto/album-response.dto';
export interface IAlbumRepository { export interface IAlbumRepository {
@ -23,6 +24,7 @@ export interface IAlbumRepository {
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>; addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>;
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>; updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>; getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>;
getCountByUserId(userId: string): Promise<AlbumCountResponseDto>;
} }
export const ALBUM_REPOSITORY = 'ALBUM_REPOSITORY'; export const ALBUM_REPOSITORY = 'ALBUM_REPOSITORY';
@ -42,6 +44,18 @@ export class AlbumRepository implements IAlbumRepository {
private dataSource: DataSource, private dataSource: DataSource,
) {} ) {}
async getCountByUserId(userId: string): Promise<AlbumCountResponseDto> {
const ownedAlbums = await this.albumRepository.find({ where: { ownerId: userId }, relations: ['sharedUsers'] });
const sharedAlbums = await this.userAlbumRepository.count({
where: { sharedUserId: userId },
});
const sharingAlbums = ownedAlbums.map((album) => album.sharedUsers?.length || 0).reduce((a, b) => a + b, 0);
return new AlbumCountResponseDto(ownedAlbums.length, sharedAlbums, sharingAlbums);
}
async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> { async create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity> {
return this.dataSource.transaction(async (transactionalEntityManager) => { return this.dataSource.transaction(async (transactionalEntityManager) => {
// Create album entity // Create album entity
@ -154,23 +168,23 @@ export class AlbumRepository implements IAlbumRepository {
let query = this.albumRepository.createQueryBuilder('album'); let query = this.albumRepository.createQueryBuilder('album');
const albums = await query const albums = await query
.where('album.ownerId = :ownerId', { ownerId: userId }) .where('album.ownerId = :ownerId', { ownerId: userId })
.andWhere((qb) => { .andWhere((qb) => {
// shared with userId // shared with userId
const subQuery = qb const subQuery = qb
.subQuery() .subQuery()
.select('assetAlbum.albumId') .select('assetAlbum.albumId')
.from(AssetAlbumEntity, 'assetAlbum') .from(AssetAlbumEntity, 'assetAlbum')
.where('assetAlbum.assetId = :assetId', {assetId: assetId}) .where('assetAlbum.assetId = :assetId', { assetId: assetId })
.getQuery(); .getQuery();
return `album.id IN ${subQuery}`; return `album.id IN ${subQuery}`;
}) })
.leftJoinAndSelect('album.assets', 'assets') .leftJoinAndSelect('album.assets', 'assets')
.leftJoinAndSelect('assets.assetInfo', 'assetInfo') .leftJoinAndSelect('assets.assetInfo', 'assetInfo')
.leftJoinAndSelect('album.sharedUsers', 'sharedUser') .leftJoinAndSelect('album.sharedUsers', 'sharedUser')
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo') .leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC') .orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
.getMany(); .getMany();
return albums; return albums;
} }
@ -228,9 +242,10 @@ export class AlbumRepository implements IAlbumRepository {
// TODO: No need to return boolean if using a singe delete query // TODO: No need to return boolean if using a singe delete query
if (deleteAssetCount == removeAssetsDto.assetIds.length) { if (deleteAssetCount == removeAssetsDto.assetIds.length) {
const retAlbum = await this.get(album.id) as AlbumEntity; const retAlbum = (await this.get(album.id)) as AlbumEntity;
if (retAlbum?.assets?.length === 0) { // is empty album if (retAlbum?.assets?.length === 0) {
// is empty album
await this.albumRepository.update(album.id, { albumThumbnailAssetId: null }); await this.albumRepository.update(album.id, { albumThumbnailAssetId: null });
retAlbum.albumThumbnailAssetId = null; retAlbum.albumThumbnailAssetId = null;
} }

View File

@ -11,6 +11,7 @@ import {
ParseUUIDPipe, ParseUUIDPipe,
Put, Put,
Query, Query,
Header,
} from '@nestjs/common'; } from '@nestjs/common';
import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe'; import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe';
import { AlbumService } from './album.service'; import { AlbumService } from './album.service';
@ -24,6 +25,7 @@ import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto'; import { GetAlbumsDto } from './dto/get-albums.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AlbumResponseDto } from './response-dto/album-response.dto'; import { AlbumResponseDto } from './response-dto/album-response.dto';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe. // TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ -33,6 +35,11 @@ import { AlbumResponseDto } from './response-dto/album-response.dto';
export class AlbumController { export class AlbumController {
constructor(private readonly albumService: AlbumService) {} constructor(private readonly albumService: AlbumService) {}
@Get('count-by-user-id')
async getAlbumCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
return this.albumService.getAlbumCountByUserId(authUser);
}
@Post() @Post()
async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) { async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
return this.albumService.create(authUser, createAlbumDto); return this.albumService.create(authUser, createAlbumDto);

View File

@ -116,7 +116,8 @@ describe('Album service', () => {
removeAssets: jest.fn(), removeAssets: jest.fn(),
removeUser: jest.fn(), removeUser: jest.fn(),
updateAlbum: jest.fn(), updateAlbum: jest.fn(),
getListByAssetId: jest.fn() getListByAssetId: jest.fn(),
getCountByUserId: jest.fn(),
}; };
sut = new AlbumService(albumRepositoryMock); sut = new AlbumService(albumRepositoryMock);
}); });

View File

@ -9,6 +9,7 @@ import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto'; import { GetAlbumsDto } from './dto/get-albums.dto';
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto'; import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository'; import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
@Injectable() @Injectable()
export class AlbumService { export class AlbumService {
@ -118,4 +119,8 @@ export class AlbumService {
const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto); const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto);
return mapAlbum(updatedAlbum); return mapAlbum(updatedAlbum);
} }
async getAlbumCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
return this._albumRepository.getCountByUserId(authUser.id);
}
} }

View File

@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
export class AlbumCountResponseDto {
@ApiProperty({ type: 'integer' })
owned!: number;
@ApiProperty({ type: 'integer' })
shared!: number;
@ApiProperty({ type: 'integer' })
sharing!: number;
constructor(owned: number, shared: number, sharing: number) {
this.owned = owned;
this.shared = shared;
this.sharing = sharing;
}
}

View File

@ -9,6 +9,7 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto'; import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto'; import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
export interface IAssetRepository { export interface IAssetRepository {
create( create(
@ -25,6 +26,7 @@ export interface IAssetRepository {
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>; getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
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>;
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>;
} }
@ -38,6 +40,28 @@ export class AssetRepository implements IAssetRepository {
private assetRepository: Repository<AssetEntity>, private assetRepository: Repository<AssetEntity>,
) {} ) {}
async getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto> {
// Get asset count by AssetType
const res = await this.assetRepository
.createQueryBuilder('asset')
.select(`COUNT(asset.id)`, 'count')
.addSelect(`asset.type`, 'type')
.where('"userId" = :userId', { userId: userId })
.groupBy('asset.type')
.getRawMany();
const assetCountByUserId = new AssetCountByUserIdResponseDto(0, 0);
res.map((item) => {
if (item.type === 'IMAGE') {
assetCountByUserId.photos = item.count;
} else if (item.type === 'VIDEO') {
assetCountByUserId.videos = item.count;
}
});
return assetCountByUserId;
}
async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> { async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
// Get asset entity from a list of time buckets // Get asset entity from a list of time buckets
return await this.assetRepository return await this.assetRepository

View File

@ -48,6 +48,7 @@ import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto'; import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
import { QueryFailedError } from 'typeorm'; import { QueryFailedError } from 'typeorm';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ -78,7 +79,13 @@ export class AssetController {
const checksum = await this.assetService.calculateChecksum(file.path); const checksum = await this.assetService.calculateChecksum(file.path);
try { try {
const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype, checksum); const savedAsset = await this.assetService.createUserAsset(
authUser,
assetInfo,
file.path,
file.mimetype,
checksum,
);
if (!savedAsset) { if (!savedAsset) {
await this.backgroundTaskService.deleteFileOnDisk([ await this.backgroundTaskService.deleteFileOnDisk([
@ -104,7 +111,7 @@ export class AssetController {
]); // simulate asset to make use of delete queue (or use fs.unlink instead) ]); // simulate asset to make use of delete queue (or use fs.unlink instead)
if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') { if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') {
const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum) const existedAsset = await this.assetService.getAssetByChecksum(authUser.id, checksum);
return new AssetFileUploadResponseDto(existedAsset.id); return new AssetFileUploadResponseDto(existedAsset.id);
} }
@ -172,6 +179,10 @@ export class AssetController {
return this.assetService.getAssetCountByTimeBucket(authUser, getAssetCountByTimeGroupDto); return this.assetService.getAssetCountByTimeBucket(authUser, getAssetCountByTimeGroupDto);
} }
@Get('/count-by-user-id')
async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this.assetService.getAssetCountByUserId(authUser);
}
/** /**
* Get all AssetEntity belong to the user * Get all AssetEntity belong to the user
*/ */

View File

@ -61,6 +61,7 @@ describe('AssetService', () => {
getSearchPropertiesByUserId: jest.fn(), getSearchPropertiesByUserId: jest.fn(),
getAssetByTimeBucket: jest.fn(), getAssetByTimeBucket: jest.fn(),
getAssetByChecksum: jest.fn(), getAssetByChecksum: jest.fn(),
getAssetCountByUserId: jest.fn(),
}; };
sui = new AssetService(assetRepositoryMock, a); sui = new AssetService(assetRepositoryMock, a);

View File

@ -35,6 +35,7 @@ import {
} from './response-dto/asset-count-by-time-group-response.dto'; } from './response-dto/asset-count-by-time-group-response.dto';
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto'; import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto'; import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
const fileInfo = promisify(stat); const fileInfo = promisify(stat);
@ -479,4 +480,8 @@ export class AssetService {
fileReadStream.pipe(sha1Hash); fileReadStream.pipe(sha1Hash);
return deferred; return deferred;
} }
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this._assetRepository.getAssetCountByUserId(authUser.id);
}
} }

View File

@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
export class AssetCountByUserIdResponseDto {
@ApiProperty({ type: 'integer' })
photos!: number;
@ApiProperty({ type: 'integer' })
videos!: number;
constructor(photos: number, videos: number) {
this.photos = photos;
this.videos = videos;
}
}

File diff suppressed because one or more lines are too long

View File

@ -84,6 +84,31 @@ export interface AdminSignupResponseDto {
*/ */
'createdAt': string; 'createdAt': string;
} }
/**
*
* @export
* @interface AlbumCountResponseDto
*/
export interface AlbumCountResponseDto {
/**
*
* @type {number}
* @memberof AlbumCountResponseDto
*/
'owned': number;
/**
*
* @type {number}
* @memberof AlbumCountResponseDto
*/
'shared': number;
/**
*
* @type {number}
* @memberof AlbumCountResponseDto
*/
'sharing': number;
}
/** /**
* *
* @export * @export
@ -183,6 +208,25 @@ export interface AssetCountByTimeBucketResponseDto {
*/ */
'buckets': Array<AssetCountByTimeBucket>; 'buckets': Array<AssetCountByTimeBucket>;
} }
/**
*
* @export
* @interface AssetCountByUserIdResponseDto
*/
export interface AssetCountByUserIdResponseDto {
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'photos': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'videos': number;
}
/** /**
* *
* @export * @export
@ -1408,6 +1452,39 @@ export const AlbumApiAxiosParamCreator = 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}
*/
getAlbumCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/album/count-by-user-id`;
// 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 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};
@ -1676,6 +1753,15 @@ export const AlbumApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAlbum(albumId, options); const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAlbum(albumId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAlbumCountByUserId(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlbumCountResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAlbumCountByUserId(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {string} albumId * @param {string} albumId
@ -1778,6 +1864,14 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
deleteAlbum(albumId: string, options?: any): AxiosPromise<void> { deleteAlbum(albumId: string, options?: any): AxiosPromise<void> {
return localVarFp.deleteAlbum(albumId, options).then((request) => request(axios, basePath)); return localVarFp.deleteAlbum(albumId, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAlbumCountByUserId(options?: any): AxiosPromise<AlbumCountResponseDto> {
return localVarFp.getAlbumCountByUserId(options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {string} albumId * @param {string} albumId
@ -1883,6 +1977,16 @@ export class AlbumApi extends BaseAPI {
return AlbumApiFp(this.configuration).deleteAlbum(albumId, options).then((request) => request(this.axios, this.basePath)); return AlbumApiFp(this.configuration).deleteAlbum(albumId, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AlbumApi
*/
public getAlbumCountByUserId(options?: AxiosRequestConfig) {
return AlbumApiFp(this.configuration).getAlbumCountByUserId(options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {string} albumId * @param {string} albumId
@ -2236,6 +2340,39 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByUserId: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/count-by-user-id`;
// 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 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};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
@ -2640,6 +2777,15 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, 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);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
@ -2800,6 +2946,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
getAssetCountByTimeBucket(getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, options?: any): AxiosPromise<AssetCountByTimeBucketResponseDto> { getAssetCountByTimeBucket(getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, options?: any): AxiosPromise<AssetCountByTimeBucketResponseDto> {
return localVarFp.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options).then((request) => request(axios, basePath)); return localVarFp.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByUserId(options?: any): AxiosPromise<AssetCountByUserIdResponseDto> {
return localVarFp.getAssetCountByUserId(options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
@ -2966,6 +3120,16 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public getAssetCountByUserId(options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetCountByUserId(options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.

View File

@ -5,11 +5,19 @@
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 AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte'; import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
import SideBarButton from './side-bar-button.svelte'; import SideBarButton from './side-bar-button.svelte';
import StatusBox from '../status-box.svelte'; import StatusBox from '../status-box.svelte';
import { AlbumCountResponseDto, api, AssetCountByUserIdResponseDto } from '@api';
import { fade } from 'svelte/transition';
import LoadingSpinner from '../loading-spinner.svelte';
let selectedAction: AppSideBarSelection; let selectedAction: AppSideBarSelection;
let showAssetCount: boolean = false;
let showSharingCount = false;
let showAlbumsCount = false;
onMount(async () => { onMount(async () => {
if ($page.routeId == 'albums') { if ($page.routeId == 'albums') {
selectedAction = AppSideBarSelection.ALBUMS; selectedAction = AppSideBarSelection.ALBUMS;
@ -19,35 +27,130 @@
selectedAction = AppSideBarSelection.SHARING; selectedAction = AppSideBarSelection.SHARING;
} }
}); });
const getAssetCount = async () => {
const { data: assetCount } = await api.assetApi.getAssetCountByUserId();
return {
videos: assetCount.videos,
photos: assetCount.photos
};
};
const getAlbumCount = async () => {
const { data: albumCount } = await api.albumApi.getAlbumCountByUserId();
return {
shared: albumCount.shared,
sharing: albumCount.sharing,
owned: albumCount.owned
};
};
</script> </script>
<section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6"> <section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6">
<a sveltekit:prefetch sveltekit:noscroll href={$page.routeId !== 'photos' ? `/photos` : null}> <a
sveltekit:prefetch
sveltekit:noscroll
href={$page.routeId !== 'photos' ? `/photos` : null}
class="relative"
>
<SideBarButton <SideBarButton
title="Photos" title={`Photos`}
logo={ImageOutline} logo={ImageOutline}
actionType={AppSideBarSelection.PHOTOS} actionType={AppSideBarSelection.PHOTOS}
isSelected={selectedAction === AppSideBarSelection.PHOTOS} isSelected={selectedAction === AppSideBarSelection.PHOTOS}
/></a />
> <div
<a sveltekit:prefetch href={$page.routeId !== 'sharing' ? `/sharing` : null}> id="asset-count-info"
class="absolute right-4 top-[15px] z-40 text-xs hover:cursor-help"
on:mouseenter={() => (showAssetCount = true)}
on:mouseleave={() => (showAssetCount = false)}
>
<InformationOutline size={18} color="#4250af" />
{#if showAssetCount}
<div
transition:fade={{ duration: 200 }}
id="asset-count-info-detail"
class="w-32 rounded-lg px-4 py-2 shadow-lg bg-white absolute -right-[135px] top-0 z-[9999] flex place-items-center place-content-center"
>
{#await getAssetCount()}
<LoadingSpinner />
{:then data}
<div>
<p>{data.videos} Videos</p>
<p>{data.photos} Photos</p>
</div>
{/await}
</div>
{/if}
</div>
</a>
<a sveltekit:prefetch href={$page.routeId !== 'sharing' ? `/sharing` : null} class="relative">
<SideBarButton <SideBarButton
title="Sharing" title="Sharing"
logo={AccountMultipleOutline} logo={AccountMultipleOutline}
actionType={AppSideBarSelection.SHARING} actionType={AppSideBarSelection.SHARING}
isSelected={selectedAction === AppSideBarSelection.SHARING} isSelected={selectedAction === AppSideBarSelection.SHARING}
/></a />
> <div
id="sharing-count-info"
class="absolute right-4 top-[15px] z-40 text-xs hover:cursor-help"
on:mouseenter={() => (showSharingCount = true)}
on:mouseleave={() => (showSharingCount = false)}
>
<InformationOutline size={18} color="#4250af" />
{#if showSharingCount}
<div
transition:fade={{ duration: 200 }}
id="asset-count-info-detail"
class="w-32 rounded-lg px-4 py-2 shadow-lg bg-white absolute -right-[135px] top-0 z-[9999] flex place-items-center place-content-center"
>
{#await getAlbumCount()}
<LoadingSpinner />
{:then data}
<div>
<p>{data.shared + data.sharing} albums</p>
</div>
{/await}
</div>
{/if}
</div>
</a>
<div class="text-xs ml-5 my-4"> <div class="text-xs ml-5 my-4">
<p>LIBRARY</p> <p>LIBRARY</p>
</div> </div>
<a sveltekit:prefetch href={$page.routeId !== 'albums' ? `/albums` : null}> <a sveltekit:prefetch href={$page.routeId !== 'albums' ? `/albums` : null} class="relative">
<SideBarButton <SideBarButton
title="Albums" title="Albums"
logo={ImageAlbum} logo={ImageAlbum}
actionType={AppSideBarSelection.ALBUMS} actionType={AppSideBarSelection.ALBUMS}
isSelected={selectedAction === AppSideBarSelection.ALBUMS} isSelected={selectedAction === AppSideBarSelection.ALBUMS}
/> />
<div
id="album-count-info"
class="absolute right-4 top-[15px] z-40 text-xs hover:cursor-help"
on:mouseenter={() => (showAlbumsCount = true)}
on:mouseleave={() => (showAlbumsCount = false)}
>
<InformationOutline size={18} color="#4250af" />
{#if showAlbumsCount}
<div
transition:fade={{ duration: 200 }}
id="asset-count-info-detail"
class="w-32 rounded-lg px-4 py-2 shadow-lg bg-white absolute -right-[135px] top-0 z-[9999] flex place-items-center place-content-center"
>
{#await getAlbumCount()}
<LoadingSpinner />
{:then data}
<div>
<p>{data.owned} albums</p>
</div>
{/await}
</div>
{/if}
</div>
</a> </a>
<!-- Status Box --> <!-- Status Box -->

View File

@ -26,14 +26,6 @@
export let data: PageData; export let data: PageData;
onMount(async () => {
openWebsocketConnection();
return () => {
closeWebsocketConnection();
};
});
const deleteSelectedAssetHandler = async () => { const deleteSelectedAssetHandler = async () => {
try { try {
if ( if (