Ver código fonte

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

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

This reverts commit c438e179543fe4f079dfa4ec15a227ad89b359ae.

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

This reverts commit 725f30c49448689f781d6d25374e6d08d1874c4b.
Alex 1 ano atrás
pai
commit
f094ff2aa1

+ 12 - 3
server/src/domain/album/album-response.dto.ts

@@ -37,6 +37,15 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
   const hasSharedLink = entity.sharedLinks?.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 {
     albumName: entity.albumName,
     description: entity.description,
@@ -49,10 +58,10 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
     sharedUsers,
     shared: hasSharedUser || hasSharedLink,
     hasSharedLink,
-    startDate: entity.startDate ? entity.startDate : undefined,
-    endDate: entity.endDate ? entity.endDate : undefined,
+    startDate,
+    endDate,
     assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
-    assetCount: entity.assetCount,
+    assetCount: entity.assets?.length || 0,
     isActivityEnabled: entity.isActivityEnabled,
   };
 };

+ 14 - 0
server/src/domain/album/album.service.spec.ts

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

+ 10 - 2
server/src/domain/album/album.service.ts

@@ -66,12 +66,21 @@ export class AlbumService {
       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(
       albums.map(async (album) => {
         const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
         return {
           ...mapAlbumWithoutAssets(album),
           sharedLinks: undefined,
+          assetCount: albumsAssetCountObj[album.id],
           lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
         };
       }),
@@ -81,8 +90,7 @@ export class AlbumService {
   async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
     await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
     await this.albumRepository.updateThumbnails();
-    const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
-    return mapAlbum(await this.findOrFail(id, { withAssets }), !dto.withoutAssets);
+    return mapAlbum(await this.findOrFail(id, { withAssets: true }), !dto.withoutAssets);
   }
 
   async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {

+ 1 - 0
server/src/domain/repositories/album.repository.ts

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

+ 1 - 1
server/src/domain/shared-link/shared-link-response.dto.ts

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

+ 0 - 31
server/src/infra/entities/album.entity.ts

@@ -9,7 +9,6 @@ import {
   OneToMany,
   PrimaryGeneratedColumn,
   UpdateDateColumn,
-  VirtualColumn,
 } from 'typeorm';
 import { AssetEntity } from './asset.entity';
 import { SharedLinkEntity } from './shared-link.entity';
@@ -60,34 +59,4 @@ export class AlbumEntity {
 
   @Column({ default: true })
   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;
 }

+ 23 - 5
server/src/infra/repositories/album.repository.ts

@@ -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 { InjectDataSource, InjectRepository } from '@nestjs/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 },
       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:
    *  - Thumbnail references an asset outside the album
@@ -92,7 +113,6 @@ export class AlbumRepository implements IAlbumRepository {
       relations: { sharedUsers: true, sharedLinks: true, owner: true },
       where: { ownerId },
       order: { createdAt: 'DESC' },
-      relationLoadStrategy: 'query',
     });
   }
 
@@ -108,7 +128,6 @@ export class AlbumRepository implements IAlbumRepository {
         { ownerId, sharedUsers: { id: Not(IsNull()) } },
       ],
       order: { createdAt: 'DESC' },
-      relationLoadStrategy: 'query',
     });
   }
 
@@ -120,7 +139,6 @@ export class AlbumRepository implements IAlbumRepository {
       relations: { sharedUsers: true, sharedLinks: true, owner: true },
       where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } },
       order: { createdAt: 'DESC' },
-      relationLoadStrategy: 'query',
     });
   }
 

+ 0 - 30
server/test/fixtures/album.stub.ts

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

+ 0 - 3
server/test/fixtures/shared-link.stub.ts

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

+ 1 - 0
server/test/repositories/album.repository.mock.ts

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

+ 4 - 4
web/src/lib/components/elements/table-header.svelte

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

+ 7 - 18
web/src/lib/components/sharedlinks-page/shared-link-card.svelte

@@ -7,7 +7,6 @@
   import { createEventDispatcher } from 'svelte';
   import { goto } from '$app/navigation';
   import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
-  import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
 
   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"
 >
   <div>
-    {#if (link?.album?.assetCount && link?.album?.assetCount > 0) || link.assets.length > 0}
-      {#await getAssetInfo()}
-        <LoadingSpinner />
-      {: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}
+    {#await getAssetInfo()}
+      <LoadingSpinner />
+    {:then asset}
       <img
-        src={noThumbnailUrl}
-        alt={'Album without assets'}
+        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"
       />
-    {/if}
+    {/await}
   </div>
 
   <div class="flex flex-col justify-between">

+ 35 - 87
web/src/routes/(user)/albums/+page.svelte

@@ -1,6 +1,9 @@
 <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 {
-    title: string;
+    table: string;
+    sortTitle: string;
     sortDesc: boolean;
     widthClass: string;
     sortFn: (reverse: boolean, albums: AlbumResponseDto[]) => AlbumResponseDto[];
@@ -51,75 +54,46 @@
 
   let sortByOptions: Record<string, Sort> = {
     albumTitle: {
-      title: 'Album title',
+      table: 'Album title',
+      sortTitle: 'Album title',
       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) => {
         return orderBy(albums, 'albumName', [reverse ? 'desc' : 'asc']);
       },
     },
     numberOfAssets: {
-      title: 'Number of assets',
+      table: 'Assets',
+      sortTitle: 'Number of assets',
       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) => {
         return orderBy(albums, 'assetCount', [reverse ? 'desc' : 'asc']);
       },
     },
     lastModified: {
-      title: 'Last modified',
+      table: 'Updated date',
+      sortTitle: 'Last modified',
       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) => {
         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: {
-      title: 'Most recent photo',
-      sortDesc: $albumViewSettings.sortDesc,
-      widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]',
-      sortFn: (reverse, albums) => {
-        return orderBy(
-          albums,
-          [(album) => (album.endDate ? new Date(album.endDate) : '')],
-          [reverse ? 'desc' : 'asc'],
-        ).sort((a, b) => {
-          if (a.endDate === undefined) {
-            return 1;
-          }
-          if (b.endDate === undefined) {
-            return -1;
-          }
-          return 0;
-        });
-      },
-    },
-    mostOld: {
-      title: 'Oldest photo',
+      table: 'Created date',
+      sortTitle: 'Most recent photo',
       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) => {
         return orderBy(
           albums,
-          [(album) => (album.startDate ? new Date(album.startDate) : null)],
+          [
+            (album) =>
+              album.lastModifiedAssetTimestamp ? new Date(album.lastModifiedAssetTimestamp) : new Date(album.updatedAt),
+          ],
           [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) {
-      if (sortByOptions[key].title === $albumViewSettings.sortBy) {
+      if (sortByOptions[key].sortTitle === sortBy) {
         $albums = sortByOptions[key].sortFn(sortByOptions[key].sortDesc, $unsortedAlbums);
         $albumViewSettings.sortDesc = sortByOptions[key].sortDesc; // "Save" sortDesc
-        $albumViewSettings.sortBy = sortByOptions[key].title;
         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 newAlbum = await createAlbum();
     if (newAlbum) {
@@ -255,20 +220,19 @@
 
     <Dropdown
       options={Object.values(sortByOptions)}
-      selectedOption={test($albumViewSettings.sortBy)}
       render={(option) => {
         return {
-          title: option.title,
+          title: option.sortTitle,
           icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin,
         };
       }}
       on:select={(event) => {
         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;
-            $albumViewSettings.sortBy = sortByOptions[key].title;
           }
         }
+        $albumViewSettings.sortBy = event.detail.sortTitle;
       }}
     />
 
@@ -307,7 +271,7 @@
             {#each Object.keys(sortByOptions) as key (key)}
               <TableHeader bind:albumViewSettings={$albumViewSettings.sortBy} bind:option={sortByOptions[key]} />
             {/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>
         </thead>
         <tbody
@@ -320,34 +284,18 @@
               on:keydown={(event) => event.key === 'Enter' && goto(`albums/${album.id}`)}
               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%]"
-                >{album.albumName}</td
-              >
-              <td class="text-md text-ellipsis text-center sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]">
+              <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>
+              <td class="text-md w-4/12 text-ellipsis text-center sm:w-2/12 md:w-2/12 2xl:w-1/12">
                 {album.assetCount}
-                {album.assetCount > 1 ? `items` : `item`}
-              </td>
-              <td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]"
-                >{dateLocaleString(album.updatedAt)}
+                {album.assetCount == 1 ? `item` : `items`}
               </td>
-              <td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]"
-                >{dateLocaleString(album.createdAt)}</td
-              >
-              <td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]">
-                {#if album.endDate}
-                  {dateLocaleString(album.endDate)}
-                {:else}
-                  &#10060;
-                {/if}</td
+              <td class="text-md hidden w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
+                >{dateLocaleString(album.updatedAt)}</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 w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
+                >{dateLocaleString(album.createdAt)}</td
               >
-              <td class="text-md hidden text-ellipsis text-center 2xl: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">
                 <button
                   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"