1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

fix(server): album perf query (#5232)

* Revert "fix: album performances (#5224)"

This reverts commit c438e17954.

* Revert "fix: album sorting options (#5127)"

This reverts commit 725f30c494.
This commit is contained in:
Alex 2023-11-21 10:07:49 -06:00 committed by GitHub
parent a13052e24c
commit f094ff2aa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 108 additions and 184 deletions

View File

@ -37,6 +37,15 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
const hasSharedLink = entity.sharedLinks?.length > 0; const hasSharedLink = entity.sharedLinks?.length > 0;
const hasSharedUser = sharedUsers.length > 0; const hasSharedUser = sharedUsers.length > 0;
let startDate = assets.at(0)?.fileCreatedAt || undefined;
let endDate = assets.at(-1)?.fileCreatedAt || undefined;
// Swap dates if start date is greater than end date.
if (startDate && endDate && startDate > endDate) {
const temp = startDate;
startDate = endDate;
endDate = temp;
}
return { return {
albumName: entity.albumName, albumName: entity.albumName,
description: entity.description, description: entity.description,
@ -49,10 +58,10 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
sharedUsers, sharedUsers,
shared: hasSharedUser || hasSharedLink, shared: hasSharedUser || hasSharedLink,
hasSharedLink, hasSharedLink,
startDate: entity.startDate ? entity.startDate : undefined, startDate,
endDate: entity.endDate ? entity.endDate : undefined, endDate,
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)), assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
assetCount: entity.assetCount, assetCount: entity.assets?.length || 0,
isActivityEnabled: entity.isActivityEnabled, isActivityEnabled: entity.isActivityEnabled,
}; };
}; };

View File

@ -58,6 +58,10 @@ describe(AlbumService.name, () => {
describe('getAll', () => { describe('getAll', () => {
it('gets list of albums for auth user', async () => { it('gets list of albums for auth user', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]); albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
albumMock.getAssetCountForIds.mockResolvedValue([
{ albumId: albumStub.empty.id, assetCount: 0 },
{ albumId: albumStub.sharedWithUser.id, assetCount: 0 },
]);
albumMock.getInvalidThumbnail.mockResolvedValue([]); albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, {}); const result = await sut.getAll(authStub.admin, {});
@ -68,6 +72,7 @@ describe(AlbumService.name, () => {
it('gets list of albums that have a specific asset', async () => { it('gets list of albums that have a specific asset', async () => {
albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]); albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]);
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]); albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id }); const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id });
@ -78,6 +83,7 @@ describe(AlbumService.name, () => {
it('gets list of albums that are shared', async () => { it('gets list of albums that are shared', async () => {
albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]); albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.sharedWithUser.id, assetCount: 0 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]); albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, { shared: true }); const result = await sut.getAll(authStub.admin, { shared: true });
@ -88,6 +94,7 @@ describe(AlbumService.name, () => {
it('gets list of albums that are NOT shared', async () => { it('gets list of albums that are NOT shared', async () => {
albumMock.getNotShared.mockResolvedValue([albumStub.empty]); albumMock.getNotShared.mockResolvedValue([albumStub.empty]);
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.empty.id, assetCount: 0 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]); albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, { shared: false }); const result = await sut.getAll(authStub.admin, { shared: false });
@ -99,6 +106,7 @@ describe(AlbumService.name, () => {
it('counts assets correctly', async () => { it('counts assets correctly', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]); albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]);
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]); albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAll(authStub.admin, {}); const result = await sut.getAll(authStub.admin, {});
@ -110,6 +118,9 @@ describe(AlbumService.name, () => {
it('updates the album thumbnail by listing all albums', async () => { it('updates the album thumbnail by listing all albums', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]); albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]);
albumMock.getAssetCountForIds.mockResolvedValue([
{ albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 },
]);
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]); albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail); albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail);
assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]); assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
@ -123,6 +134,9 @@ describe(AlbumService.name, () => {
it('removes the thumbnail for an empty album', async () => { it('removes the thumbnail for an empty album', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]); albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]);
albumMock.getAssetCountForIds.mockResolvedValue([
{ albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 },
]);
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]); albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail); albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail);
assetMock.getFirstAssetForAlbumId.mockResolvedValue(null); assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);

View File

@ -66,12 +66,21 @@ export class AlbumService {
albums = await this.albumRepository.getOwned(ownerId); albums = await this.albumRepository.getOwned(ownerId);
} }
// Get asset count for each album. Then map the result to an object:
// { [albumId]: assetCount }
const albumsAssetCount = await this.albumRepository.getAssetCountForIds(albums.map((album) => album.id));
const albumsAssetCountObj = albumsAssetCount.reduce((obj: Record<string, number>, { albumId, assetCount }) => {
obj[albumId] = assetCount;
return obj;
}, {});
return Promise.all( return Promise.all(
albums.map(async (album) => { albums.map(async (album) => {
const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id); const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
return { return {
...mapAlbumWithoutAssets(album), ...mapAlbumWithoutAssets(album),
sharedLinks: undefined, sharedLinks: undefined,
assetCount: albumsAssetCountObj[album.id],
lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt, lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
}; };
}), }),
@ -81,8 +90,7 @@ export class AlbumService {
async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> { async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
await this.albumRepository.updateThumbnails(); await this.albumRepository.updateThumbnails();
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; return mapAlbum(await this.findOrFail(id, { withAssets: true }), !dto.withoutAssets);
return mapAlbum(await this.findOrFail(id, { withAssets }), !dto.withoutAssets);
} }
async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> { async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {

View File

@ -30,6 +30,7 @@ export interface IAlbumRepository {
hasAsset(asset: AlbumAsset): Promise<boolean>; hasAsset(asset: AlbumAsset): Promise<boolean>;
removeAsset(assetId: string): Promise<void>; removeAsset(assetId: string): Promise<void>;
removeAssets(assets: AlbumAssets): Promise<void>; removeAssets(assets: AlbumAssets): Promise<void>;
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
getInvalidThumbnail(): Promise<string[]>; getInvalidThumbnail(): Promise<string[]>;
getOwned(ownerId: string): Promise<AlbumEntity[]>; getOwned(ownerId: string): Promise<AlbumEntity[]>;
getShared(ownerId: string): Promise<AlbumEntity[]>; getShared(ownerId: string): Promise<AlbumEntity[]>;

View File

@ -40,7 +40,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
createdAt: sharedLink.createdAt, createdAt: sharedLink.createdAt,
expiresAt: sharedLink.expiresAt, expiresAt: sharedLink.expiresAt,
assets: assets.map((asset) => mapAsset(asset)), assets: assets.map((asset) => mapAsset(asset)),
album: sharedLink.album?.id ? mapAlbumWithoutAssets(sharedLink.album) : undefined, album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
allowUpload: sharedLink.allowUpload, allowUpload: sharedLink.allowUpload,
allowDownload: sharedLink.allowDownload, allowDownload: sharedLink.allowDownload,
showMetadata: sharedLink.showExif, showMetadata: sharedLink.showExif,

View File

@ -9,7 +9,6 @@ import {
OneToMany, OneToMany,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
VirtualColumn,
} from 'typeorm'; } from 'typeorm';
import { AssetEntity } from './asset.entity'; import { AssetEntity } from './asset.entity';
import { SharedLinkEntity } from './shared-link.entity'; import { SharedLinkEntity } from './shared-link.entity';
@ -60,34 +59,4 @@ export class AlbumEntity {
@Column({ default: true }) @Column({ default: true })
isActivityEnabled!: boolean; isActivityEnabled!: boolean;
@VirtualColumn({
query: (alias) => `
SELECT MIN(assets."fileCreatedAt")
FROM "assets" assets
JOIN "albums_assets_assets" aa ON aa."assetsId" = assets.id
WHERE aa."albumsId" = ${alias}.id
`,
})
startDate!: Date | null;
@VirtualColumn({
query: (alias) => `
SELECT MAX(assets."fileCreatedAt")
FROM "assets" assets
JOIN "albums_assets_assets" aa ON aa."assetsId" = assets.id
WHERE aa."albumsId" = ${alias}.id
`,
})
endDate!: Date | null;
@VirtualColumn({
query: (alias) => `
SELECT COUNT(assets."id")
FROM "assets" assets
JOIN "albums_assets_assets" aa ON aa."assetsId" = assets.id
WHERE aa."albumsId" = ${alias}.id
`,
})
assetCount!: number;
} }

View File

@ -1,4 +1,4 @@
import { AlbumAsset, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain'; import { AlbumAsset, AlbumAssetCount, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
@ -56,10 +56,31 @@ export class AlbumRepository implements IAlbumRepository {
], ],
relations: { owner: true, sharedUsers: true }, relations: { owner: true, sharedUsers: true },
order: { createdAt: 'DESC' }, order: { createdAt: 'DESC' },
relationLoadStrategy: 'query',
}); });
} }
async getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]> {
// Guard against running invalid query when ids list is empty.
if (!ids.length) {
return [];
}
// Only possible with query builder because of GROUP BY.
const countByAlbums = await this.repository
.createQueryBuilder('album')
.select('album.id')
.addSelect('COUNT(albums_assets.assetsId)', 'asset_count')
.leftJoin('albums_assets_assets', 'albums_assets', 'albums_assets.albumsId = album.id')
.where('album.id IN (:...ids)', { ids })
.groupBy('album.id')
.getRawMany();
return countByAlbums.map<AlbumAssetCount>((albumCount) => ({
albumId: albumCount['album_id'],
assetCount: Number(albumCount['asset_count']),
}));
}
/** /**
* Returns the album IDs that have an invalid thumbnail, when: * Returns the album IDs that have an invalid thumbnail, when:
* - Thumbnail references an asset outside the album * - Thumbnail references an asset outside the album
@ -92,7 +113,6 @@ export class AlbumRepository implements IAlbumRepository {
relations: { sharedUsers: true, sharedLinks: true, owner: true }, relations: { sharedUsers: true, sharedLinks: true, owner: true },
where: { ownerId }, where: { ownerId },
order: { createdAt: 'DESC' }, order: { createdAt: 'DESC' },
relationLoadStrategy: 'query',
}); });
} }
@ -108,7 +128,6 @@ export class AlbumRepository implements IAlbumRepository {
{ ownerId, sharedUsers: { id: Not(IsNull()) } }, { ownerId, sharedUsers: { id: Not(IsNull()) } },
], ],
order: { createdAt: 'DESC' }, order: { createdAt: 'DESC' },
relationLoadStrategy: 'query',
}); });
} }
@ -120,7 +139,6 @@ export class AlbumRepository implements IAlbumRepository {
relations: { sharedUsers: true, sharedLinks: true, owner: true }, relations: { sharedUsers: true, sharedLinks: true, owner: true },
where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } }, where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } },
order: { createdAt: 'DESC' }, order: { createdAt: 'DESC' },
relationLoadStrategy: 'query',
}); });
} }

View File

@ -19,9 +19,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}), }),
sharedWithUser: Object.freeze<AlbumEntity>({ sharedWithUser: Object.freeze<AlbumEntity>({
id: 'album-2', id: 'album-2',
@ -38,9 +35,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [userStub.user1], sharedUsers: [userStub.user1],
isActivityEnabled: true, isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}), }),
sharedWithMultiple: Object.freeze<AlbumEntity>({ sharedWithMultiple: Object.freeze<AlbumEntity>({
id: 'album-3', id: 'album-3',
@ -57,9 +51,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [userStub.user1, userStub.user2], sharedUsers: [userStub.user1, userStub.user2],
isActivityEnabled: true, isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}), }),
sharedWithAdmin: Object.freeze<AlbumEntity>({ sharedWithAdmin: Object.freeze<AlbumEntity>({
id: 'album-3', id: 'album-3',
@ -76,9 +67,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [userStub.admin], sharedUsers: [userStub.admin],
isActivityEnabled: true, isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}), }),
oneAsset: Object.freeze<AlbumEntity>({ oneAsset: Object.freeze<AlbumEntity>({
id: 'album-4', id: 'album-4',
@ -95,9 +83,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
startDate: assetStub.image.fileCreatedAt,
endDate: assetStub.image.fileCreatedAt,
assetCount: 1,
}), }),
twoAssets: Object.freeze<AlbumEntity>({ twoAssets: Object.freeze<AlbumEntity>({
id: 'album-4a', id: 'album-4a',
@ -114,9 +99,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
startDate: assetStub.withLocation.fileCreatedAt,
endDate: assetStub.image.fileCreatedAt,
assetCount: 2,
}), }),
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({ emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5', id: 'album-5',
@ -133,9 +115,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}), }),
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({ emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5', id: 'album-5',
@ -152,9 +131,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
startDate: null,
endDate: null,
assetCount: 0,
}), }),
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({ oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6', id: 'album-6',
@ -171,9 +147,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
startDate: assetStub.image.fileCreatedAt,
endDate: assetStub.image.fileCreatedAt,
assetCount: 1,
}), }),
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({ oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6', id: 'album-6',
@ -190,8 +163,5 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
startDate: assetStub.image.fileCreatedAt,
endDate: assetStub.image.fileCreatedAt,
assetCount: 1,
}), }),
}; };

View File

@ -181,9 +181,6 @@ export const sharedLinkStub = {
sharedUsers: [], sharedUsers: [],
sharedLinks: [], sharedLinks: [],
isActivityEnabled: true, isActivityEnabled: true,
startDate: today,
endDate: today,
assetCount: 1,
assets: [ assets: [
{ {
id: 'id_1', id: 'id_1',

View File

@ -5,6 +5,7 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
getById: jest.fn(), getById: jest.fn(),
getByIds: jest.fn(), getByIds: jest.fn(),
getByAssetId: jest.fn(), getByAssetId: jest.fn(),
getAssetCountForIds: jest.fn(),
getInvalidThumbnail: jest.fn(), getInvalidThumbnail: jest.fn(),
getOwned: jest.fn(), getOwned: jest.fn(),
getShared: jest.fn(), getShared: jest.fn(),

View File

@ -5,10 +5,10 @@
export let option: Sort; export let option: Sort;
const handleSort = () => { const handleSort = () => {
if (albumViewSettings === option.title) { if (albumViewSettings === option.sortTitle) {
option.sortDesc = !option.sortDesc; option.sortDesc = !option.sortDesc;
} else { } else {
albumViewSettings = option.title; albumViewSettings = option.sortTitle;
} }
}; };
</script> </script>
@ -18,12 +18,12 @@
class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50" class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
on:click={() => handleSort()} on:click={() => handleSort()}
> >
{#if albumViewSettings === option.title} {#if albumViewSettings === option.sortTitle}
{#if option.sortDesc} {#if option.sortDesc}
&#8595; &#8595;
{:else} {:else}
&#8593; &#8593;
{/if} {/if}
{/if}{option.title}</button {/if}{option.table}</button
></th ></th
> >

View File

@ -7,7 +7,6 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js'; import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
export let link: SharedLinkResponseDto; export let link: SharedLinkResponseDto;
@ -61,28 +60,18 @@
class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary" class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
> >
<div> <div>
{#if (link?.album?.assetCount && link?.album?.assetCount > 0) || link.assets.length > 0} {#await getAssetInfo()}
{#await getAssetInfo()} <LoadingSpinner />
<LoadingSpinner /> {:then asset}
{:then asset}
<img
id={asset.id}
src={api.getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)}
alt={asset.id}
class="h-[100px] w-[100px] rounded-lg object-cover"
loading="lazy"
draggable="false"
/>
{/await}
{:else}
<img <img
src={noThumbnailUrl} id={asset.id}
alt={'Album without assets'} src={api.getAssetThumbnailUrl(asset.id, ThumbnailFormat.Webp)}
alt={asset.id}
class="h-[100px] w-[100px] rounded-lg object-cover" class="h-[100px] w-[100px] rounded-lg object-cover"
loading="lazy" loading="lazy"
draggable="false" draggable="false"
/> />
{/if} {/await}
</div> </div>
<div class="flex flex-col justify-between"> <div class="flex flex-col justify-between">

View File

@ -1,6 +1,9 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
// table is the text printed in the table and sortTitle is the text printed in the dropDow menu
export interface Sort { export interface Sort {
title: string; table: string;
sortTitle: string;
sortDesc: boolean; sortDesc: boolean;
widthClass: string; widthClass: string;
sortFn: (reverse: boolean, albums: AlbumResponseDto[]) => AlbumResponseDto[]; sortFn: (reverse: boolean, albums: AlbumResponseDto[]) => AlbumResponseDto[];
@ -51,75 +54,46 @@
let sortByOptions: Record<string, Sort> = { let sortByOptions: Record<string, Sort> = {
albumTitle: { albumTitle: {
title: 'Album title', table: 'Album title',
sortTitle: 'Album title',
sortDesc: $albumViewSettings.sortDesc, // Load Sort Direction sortDesc: $albumViewSettings.sortDesc, // Load Sort Direction
widthClass: 'text-left w-8/12 sm:w-4/12 md:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]', widthClass: 'w-8/12 text-left sm:w-4/12 md:w-4/12 md:w-4/12 2xl:w-6/12',
sortFn: (reverse, albums) => { sortFn: (reverse, albums) => {
return orderBy(albums, 'albumName', [reverse ? 'desc' : 'asc']); return orderBy(albums, 'albumName', [reverse ? 'desc' : 'asc']);
}, },
}, },
numberOfAssets: { numberOfAssets: {
title: 'Number of assets', table: 'Assets',
sortTitle: 'Number of assets',
sortDesc: $albumViewSettings.sortDesc, sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center w-4/12 m:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]', widthClass: 'w-4/12 text-center sm:w-2/12 2xl:w-1/12',
sortFn: (reverse, albums) => { sortFn: (reverse, albums) => {
return orderBy(albums, 'assetCount', [reverse ? 'desc' : 'asc']); return orderBy(albums, 'assetCount', [reverse ? 'desc' : 'asc']);
}, },
}, },
lastModified: { lastModified: {
title: 'Last modified', table: 'Updated date',
sortTitle: 'Last modified',
sortDesc: $albumViewSettings.sortDesc, sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]', widthClass: 'text-center hidden sm:block w-3/12 lg:w-2/12',
sortFn: (reverse, albums) => { sortFn: (reverse, albums) => {
return orderBy(albums, [(album) => new Date(album.updatedAt)], [reverse ? 'desc' : 'asc']); return orderBy(albums, [(album) => new Date(album.updatedAt)], [reverse ? 'desc' : 'asc']);
}, },
}, },
created: {
title: 'Created date',
sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]',
sortFn: (reverse, albums) => {
return orderBy(albums, [(album) => new Date(album.createdAt)], [reverse ? 'desc' : 'asc']);
},
},
mostRecent: { mostRecent: {
title: 'Most recent photo', table: 'Created date',
sortTitle: 'Most recent photo',
sortDesc: $albumViewSettings.sortDesc, sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]', widthClass: 'text-center hidden sm:block w-3/12 lg:w-2/12',
sortFn: (reverse, albums) => { sortFn: (reverse, albums) => {
return orderBy( return orderBy(
albums, albums,
[(album) => (album.endDate ? new Date(album.endDate) : '')], [
(album) =>
album.lastModifiedAssetTimestamp ? new Date(album.lastModifiedAssetTimestamp) : new Date(album.updatedAt),
],
[reverse ? 'desc' : 'asc'], [reverse ? 'desc' : 'asc'],
).sort((a, b) => { );
if (a.endDate === undefined) {
return 1;
}
if (b.endDate === undefined) {
return -1;
}
return 0;
});
},
},
mostOld: {
title: 'Oldest photo',
sortDesc: $albumViewSettings.sortDesc,
widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]',
sortFn: (reverse, albums) => {
return orderBy(
albums,
[(album) => (album.startDate ? new Date(album.startDate) : null)],
[reverse ? 'desc' : 'asc'],
).sort((a, b) => {
if (a.startDate === undefined) {
return 1;
}
if (b.startDate === undefined) {
return -1;
}
return 0;
});
}, },
}, },
}; };
@ -170,25 +144,16 @@
}; };
$: { $: {
const { sortBy } = $albumViewSettings;
for (const key in sortByOptions) { for (const key in sortByOptions) {
if (sortByOptions[key].title === $albumViewSettings.sortBy) { if (sortByOptions[key].sortTitle === sortBy) {
$albums = sortByOptions[key].sortFn(sortByOptions[key].sortDesc, $unsortedAlbums); $albums = sortByOptions[key].sortFn(sortByOptions[key].sortDesc, $unsortedAlbums);
$albumViewSettings.sortDesc = sortByOptions[key].sortDesc; // "Save" sortDesc $albumViewSettings.sortDesc = sortByOptions[key].sortDesc; // "Save" sortDesc
$albumViewSettings.sortBy = sortByOptions[key].title;
break; break;
} }
} }
} }
const test = (searched: string): Sort => {
for (const key in sortByOptions) {
if (sortByOptions[key].title === searched) {
return sortByOptions[key];
}
}
return sortByOptions[0];
};
const handleCreateAlbum = async () => { const handleCreateAlbum = async () => {
const newAlbum = await createAlbum(); const newAlbum = await createAlbum();
if (newAlbum) { if (newAlbum) {
@ -255,20 +220,19 @@
<Dropdown <Dropdown
options={Object.values(sortByOptions)} options={Object.values(sortByOptions)}
selectedOption={test($albumViewSettings.sortBy)}
render={(option) => { render={(option) => {
return { return {
title: option.title, title: option.sortTitle,
icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin, icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin,
}; };
}} }}
on:select={(event) => { on:select={(event) => {
for (const key in sortByOptions) { for (const key in sortByOptions) {
if (sortByOptions[key].title === event.detail.title) { if (sortByOptions[key].sortTitle === event.detail.sortTitle) {
sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc; sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc;
$albumViewSettings.sortBy = sortByOptions[key].title;
} }
} }
$albumViewSettings.sortBy = event.detail.sortTitle;
}} }}
/> />
@ -307,7 +271,7 @@
{#each Object.keys(sortByOptions) as key (key)} {#each Object.keys(sortByOptions) as key (key)}
<TableHeader bind:albumViewSettings={$albumViewSettings.sortBy} bind:option={sortByOptions[key]} /> <TableHeader bind:albumViewSettings={$albumViewSettings.sortBy} bind:option={sortByOptions[key]} />
{/each} {/each}
<th class="hidden text-center text-sm font-medium 2xl:block 2xl:w-[12%]">Action</th> <th class="hidden w-2/12 text-center text-sm font-medium lg:block 2xl:w-1/12">Action</th>
</tr> </tr>
</thead> </thead>
<tbody <tbody
@ -320,34 +284,18 @@
on:keydown={(event) => event.key === 'Enter' && goto(`albums/${album.id}`)} on:keydown={(event) => event.key === 'Enter' && goto(`albums/${album.id}`)}
tabindex="0" tabindex="0"
> >
<td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]" <td class="text-md w-8/12 text-ellipsis text-left sm:w-4/12 md:w-4/12 2xl:w-6/12">{album.albumName}</td>
>{album.albumName}</td <td class="text-md w-4/12 text-ellipsis text-center sm:w-2/12 md:w-2/12 2xl:w-1/12">
>
<td class="text-md text-ellipsis text-center sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]">
{album.assetCount} {album.assetCount}
{album.assetCount > 1 ? `items` : `item`} {album.assetCount == 1 ? `item` : `items`}
</td> </td>
<td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]" <td class="text-md hidden w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
>{dateLocaleString(album.updatedAt)} >{dateLocaleString(album.updatedAt)}</td
</td> >
<td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]" <td class="text-md hidden w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
>{dateLocaleString(album.createdAt)}</td >{dateLocaleString(album.createdAt)}</td
> >
<td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]"> <td class="text-md hidden w-2/12 text-ellipsis text-center lg:block 2xl:w-1/12">
{#if album.endDate}
{dateLocaleString(album.endDate)}
{:else}
&#10060;
{/if}</td
>
<td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]"
>{#if album.startDate}
{dateLocaleString(album.startDate)}
{:else}
&#10060;
{/if}</td
>
<td class="text-md hidden text-ellipsis text-center 2xl:block xl:w-[15%] 2xl:w-[12%]">
<button <button
on:click|stopPropagation={() => handleEdit(album)} on:click|stopPropagation={() => handleEdit(album)}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700" class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"