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/AdminSignupResponseDto.md
doc/AlbumApi.md
doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md
doc/AssetApi.md
doc/AssetCountByTimeBucket.md
doc/AssetCountByTimeBucketResponseDto.md
doc/AssetCountByUserIdResponseDto.md
doc/AssetFileUploadResponseDto.md
doc/AssetResponseDto.md
doc/AssetTypeEnum.md
@ -70,9 +72,11 @@ lib/auth/oauth.dart
lib/model/add_assets_dto.dart
lib/model/add_users_dto.dart
lib/model/admin_signup_response_dto.dart
lib/model/album_count_response_dto.dart
lib/model/album_response_dto.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_response_dto.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/validate_access_token_response_dto.dart
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 { RemoveAssetsDto } from './dto/remove-assets.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';
export interface IAlbumRepository {
@ -23,6 +24,7 @@ export interface IAlbumRepository {
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>;
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>;
getCountByUserId(userId: string): Promise<AlbumCountResponseDto>;
}
export const ALBUM_REPOSITORY = 'ALBUM_REPOSITORY';
@ -42,6 +44,18 @@ export class AlbumRepository implements IAlbumRepository {
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> {
return this.dataSource.transaction(async (transactionalEntityManager) => {
// Create album entity
@ -228,9 +242,10 @@ export class AlbumRepository implements IAlbumRepository {
// TODO: No need to return boolean if using a singe delete query
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 });
retAlbum.albumThumbnailAssetId = null;
}

View File

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

View File

@ -116,7 +116,8 @@ describe('Album service', () => {
removeAssets: jest.fn(),
removeUser: jest.fn(),
updateAlbum: jest.fn(),
getListByAssetId: jest.fn()
getListByAssetId: jest.fn(),
getCountByUserId: jest.fn(),
};
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 { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
@Injectable()
export class AlbumService {
@ -118,4 +119,8 @@ export class AlbumService {
const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto);
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 { TimeGroupEnum } from './dto/get-asset-count-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 {
create(
@ -25,6 +26,7 @@ export interface IAssetRepository {
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum): Promise<AssetCountByTimeBucket[]>;
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
}
@ -38,6 +40,28 @@ export class AssetRepository implements IAssetRepository {
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[]> {
// Get asset entity from a list of time buckets
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 { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
import { QueryFailedError } from 'typeorm';
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ -78,7 +79,13 @@ export class AssetController {
const checksum = await this.assetService.calculateChecksum(file.path);
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) {
await this.backgroundTaskService.deleteFileOnDisk([
@ -104,7 +111,7 @@ export class AssetController {
]); // simulate asset to make use of delete queue (or use fs.unlink instead)
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);
}
@ -172,6 +179,10 @@ export class AssetController {
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
*/

View File

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

View File

@ -35,6 +35,7 @@ import {
} from './response-dto/asset-count-by-time-group-response.dto';
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-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);
@ -479,4 +480,8 @@ export class AssetService {
fileReadStream.pipe(sha1Hash);
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;
}
/**
*
* @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
@ -183,6 +208,25 @@ export interface AssetCountByTimeBucketResponseDto {
*/
'buckets': Array<AssetCountByTimeBucket>;
}
/**
*
* @export
* @interface AssetCountByUserIdResponseDto
*/
export interface AssetCountByUserIdResponseDto {
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'photos': number;
/**
*
* @type {number}
* @memberof AssetCountByUserIdResponseDto
*/
'videos': number;
}
/**
*
* @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);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -1676,6 +1753,15 @@ export const AlbumApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAlbum(albumId, options);
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
@ -1778,6 +1864,14 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
deleteAlbum(albumId: string, options?: any): AxiosPromise<void> {
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
@ -1883,6 +1977,16 @@ export class AlbumApi extends BaseAPI {
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
@ -2236,6 +2340,39 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
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.
@ -2640,6 +2777,15 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeBucket(getAssetCountByTimeBucketDto, options);
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.
@ -2800,6 +2946,14 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
getAssetCountByTimeBucket(getAssetCountByTimeBucketDto: GetAssetCountByTimeBucketDto, options?: any): AxiosPromise<AssetCountByTimeBucketResponseDto> {
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.
@ -2966,6 +3120,16 @@ export class AssetApi extends BaseAPI {
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.

View File

@ -5,11 +5,19 @@
import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
import ImageOutline from 'svelte-material-icons/ImageOutline.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 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 showAssetCount: boolean = false;
let showSharingCount = false;
let showAlbumsCount = false;
onMount(async () => {
if ($page.routeId == 'albums') {
selectedAction = AppSideBarSelection.ALBUMS;
@ -19,35 +27,130 @@
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>
<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
title="Photos"
title={`Photos`}
logo={ImageOutline}
actionType={AppSideBarSelection.PHOTOS}
isSelected={selectedAction === AppSideBarSelection.PHOTOS}
/></a
/>
<div
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)}
>
<a sveltekit:prefetch href={$page.routeId !== 'sharing' ? `/sharing` : null}>
<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
title="Sharing"
logo={AccountMultipleOutline}
actionType={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">
<p>LIBRARY</p>
</div>
<a sveltekit:prefetch href={$page.routeId !== 'albums' ? `/albums` : null}>
<a sveltekit:prefetch href={$page.routeId !== 'albums' ? `/albums` : null} class="relative">
<SideBarButton
title="Albums"
logo={ImageAlbum}
actionType={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>
<!-- Status Box -->

View File

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