From e3bf54e2bade4feab627b9ea627a77c983cf6512 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 22 Oct 2026 18:37:03 -0400 Subject: [PATCH] refactor: album queries (#32668) --- .../lib/components/album-page/AlbumMap.svelte | 29 +-- .../album-page/AlbumThumbnail.svelte | 19 +- .../components/album-page/AlbumsList.svelte | 26 +- .../asset-viewer/DetailPanel.svelte | 89 +++---- .../side-bar/RecentAlbums.spec.ts | 8 +- .../side-bar/RecentAlbums.svelte | 26 +- web/src/lib/managers/event-manager.svelte.ts | 8 - web/src/lib/modals/AlbumEditModal.svelte | 12 +- web/src/lib/modals/AlbumOptionsModal.svelte | 49 ++-- web/src/lib/modals/AlbumPickerModal.svelte | 33 ++- .../lib/modals/AssetAddToAlbumModal.svelte | 8 +- web/src/lib/queries/album.query.ts | 222 ++++++++++++++++- web/src/lib/queries/shared-link.query.ts | 14 +- web/src/lib/services/album.service.ts | 223 ++++-------------- web/src/lib/services/asset.service.ts | 24 +- web/src/lib/stores/user.svelte.ts | 28 --- web/src/lib/utils/album-utils.ts | 32 +-- web/src/lib/utils/file-uploader.ts | 8 +- web/src/routes/(user)/albums/+page.svelte | 11 +- web/src/routes/(user)/albums/+page.ts | 8 +- .../[[assetId=id]]/+page.svelte | 123 +++------- .../[[assetId=id]]/AlbumDescription.svelte | 29 +-- .../[[assetId=id]]/AlbumTitle.svelte | 20 +- web/src/routes/(user)/sharing/+page.svelte | 7 +- web/src/routes/(user)/sharing/+page.ts | 7 +- .../user-settings/UserUsageStatistic.svelte | 13 +- .../[[assetId=id]]/DuplicateAsset.svelte | 25 +- .../[workflowId]/WorkflowStepCard.svelte | 27 +-- 28 files changed, 508 insertions(+), 620 deletions(-) delete mode 100644 web/src/lib/stores/user.svelte.ts diff --git a/web/src/lib/components/album-page/AlbumMap.svelte b/web/src/lib/components/album-page/AlbumMap.svelte index 0a6a79eb6..50448cc60 100644 --- a/web/src/lib/components/album-page/AlbumMap.svelte +++ b/web/src/lib/components/album-page/AlbumMap.svelte @@ -1,13 +1,13 @@
- {#await getAlbumInfo({ ...authManager.params, id: albumId })} + {#if query.isPending} - {:then album} + {:else if album}

- {:catch} + {:else}
{$t('unknown')} @@ -63,5 +70,5 @@ />
- {/await} + {/if} diff --git a/web/src/lib/components/album-page/AlbumsList.svelte b/web/src/lib/components/album-page/AlbumsList.svelte index 1370c8332..b8c759a96 100644 --- a/web/src/lib/components/album-page/AlbumsList.svelte +++ b/web/src/lib/components/album-page/AlbumsList.svelte @@ -1,7 +1,6 @@ - - {#if albums.length > 0} {#if userSettings.view === AlbumViewMode.Cover} diff --git a/web/src/lib/components/asset-viewer/DetailPanel.svelte b/web/src/lib/components/asset-viewer/DetailPanel.svelte index 9f210419a..345b3f8ef 100644 --- a/web/src/lib/components/asset-viewer/DetailPanel.svelte +++ b/web/src/lib/components/asset-viewer/DetailPanel.svelte @@ -13,26 +13,20 @@ import { getAssetMediaUrl } from '$lib/utils'; import { delay, getDimensions } from '$lib/utils/asset-utils'; import { getByteUnitString } from '$lib/utils/byte-units'; - import { handleError } from '$lib/utils/handle-error'; import { getParentPath } from '$lib/utils/tree-utils'; - import { - AssetMediaSize, - getAllAlbums, - getAssetInfo, - type AlbumResponseDto, - type AssetResponseDto, - } from '@immich/sdk'; + import { AssetMediaSize, getAssetInfo, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk'; + import { createQuery } from '@tanstack/svelte-query'; import { Icon, IconButton, Link, LoadingSpinner, Text } from '@immich/ui'; import { mdiCamera, mdiCameraIris, mdiClose, mdiImageOutline, mdiInformationOutline } from '@mdi/js'; import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; import { slide } from 'svelte/transition'; import PersonSidePanel from '../faces-page/PersonSidePanel.svelte'; - import OnEvents from '../OnEvents.svelte'; import UserAvatar from '../shared-components/UserAvatar.svelte'; import AlbumListItemDetails from './AlbumListItemDetails.svelte'; import DetailPanelPeople from '$lib/components/asset-viewer/DetailPanelPeople.svelte'; import { faceManager } from '$lib/stores/face.svelte'; + import { getAlbumsByAssetQuery } from '$lib/queries/album.query'; import { createPreloadedQuery } from '$lib/queries/client'; import { getPublicConfigQuery } from '$lib/queries/config-public.query'; @@ -60,20 +54,11 @@ let previousId: string | undefined = $state(); let previousRoute = $derived(currentAlbum?.id ? Route.viewAlbum(currentAlbum) : Route.photos()); - const refreshAlbums = async () => { - if (authManager.isSharedLink) { - return []; - } - - try { - return await getAllAlbums({ assetId: asset.id }); - } catch (error) { - handleError(error, 'Error getting asset album membership'); - return []; - } - }; - - let albums = $derived(refreshAlbums()); + const albumsQuery = createQuery(() => ({ + ...getAlbumsByAssetQuery(asset.id), + enabled: !authManager.isSharedLink, + })); + const albums = $derived(albumsQuery.data ?? []); $effect(() => { if (!previousId) { @@ -116,8 +101,6 @@ }); - (albums = refreshAlbums())} /> - {#if !assetViewerManager.isEditFacesPanelOpen}
@@ -345,39 +328,37 @@
{/if} - {#await albums then albums} - {#if albums.length > 0} -
-
- {$t('appears_in')} -
- {#each albums as album (album.id)} - - + + {/each} +
+ {/if} {#if authManager.authenticated && authManager.preferences.tags.enabled}
diff --git a/web/src/lib/components/shared-components/side-bar/RecentAlbums.spec.ts b/web/src/lib/components/shared-components/side-bar/RecentAlbums.spec.ts index bc6cb3578..ffd607441 100644 --- a/web/src/lib/components/shared-components/side-bar/RecentAlbums.spec.ts +++ b/web/src/lib/components/shared-components/side-bar/RecentAlbums.spec.ts @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/svelte'; -import { tick } from 'svelte'; import { sdkMock } from '$lib/__mocks__/sdk.mock'; import RecentAlbums from '$lib/components/shared-components/side-bar/RecentAlbums.svelte'; import { albumFactory } from '@test-data/factories/album-factory'; @@ -16,13 +15,8 @@ describe('RecentAlbums component', () => { sdkMock.getAllAlbums.mockResolvedValueOnce([...albums]); render(RecentAlbums); + const links = await screen.findAllByRole('link'); expect(sdkMock.getAllAlbums).toHaveBeenCalledOnce(); - - // wtf - await tick(); - await tick(); - - const links = screen.getAllByRole('link'); expect(links).toHaveLength(3); // @ts-expect-error pending a type fix: https://github.com/testing-library/jest-dom/pull/742/changes expect(links[0]).toHaveAttribute('href', `/albums/${albums[2].id}`); diff --git a/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte b/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte index 10c772d8b..6435c4d5b 100644 --- a/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte +++ b/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte @@ -1,28 +1,12 @@ {#each albums as album (album.id)} diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts index b2e1c9aa2..28f88af73 100644 --- a/web/src/lib/managers/event-manager.svelte.ts +++ b/web/src/lib/managers/event-manager.svelte.ts @@ -1,6 +1,4 @@ import type { - AlbumResponseDto, - AlbumUserRole, ApiKeyResponseDto, AssetResponseDto, IntegrityReport, @@ -47,12 +45,6 @@ export type Events = { AlbumAddAssets: [{ assetIds: string[]; albumIds: string[] }]; AlbumRemoveAssets: [{ assetIds: string[]; albumIds: string[] }]; - AlbumCreate: [AlbumResponseDto]; - AlbumUpdate: [AlbumResponseDto]; - AlbumDelete: [AlbumResponseDto]; - AlbumShare: []; - AlbumUserUpdate: [{ albumId: string; userId: string; role: AlbumUserRole }]; - AlbumUserDelete: [{ albumId: string; userId: string }]; PersonUpdate: [PersonResponseDto]; PersonShare: [{ personId: string }]; diff --git a/web/src/lib/modals/AlbumEditModal.svelte b/web/src/lib/modals/AlbumEditModal.svelte index c5386a6a0..6b3fa26c2 100644 --- a/web/src/lib/modals/AlbumEditModal.svelte +++ b/web/src/lib/modals/AlbumEditModal.svelte @@ -1,6 +1,7 @@ diff --git a/web/src/lib/modals/AlbumOptionsModal.svelte b/web/src/lib/modals/AlbumOptionsModal.svelte index 5982c4a45..ce883296f 100644 --- a/web/src/lib/modals/AlbumOptionsModal.svelte +++ b/web/src/lib/modals/AlbumOptionsModal.svelte @@ -1,19 +1,20 @@ - (album = newAlbum)} /> - @@ -67,14 +66,14 @@ { label: $t('newest_first'), value: AssetOrder.Desc }, { label: $t('oldest_first'), value: AssetOrder.Asc }, ]} - onChange={(value) => handleUpdateAlbum(album, { order: value })} + onChange={(value) => updateAlbum({ order: value })} /> {/if} handleUpdateAlbum(album, { isActivityEnabled: checked })} + onCheckedChange={(checked) => updateAlbum({ isActivityEnabled: checked })} /> diff --git a/web/src/lib/modals/AlbumPickerModal.svelte b/web/src/lib/modals/AlbumPickerModal.svelte index 19430e271..688daf3a6 100644 --- a/web/src/lib/modals/AlbumPickerModal.svelte +++ b/web/src/lib/modals/AlbumPickerModal.svelte @@ -5,19 +5,25 @@ AlbumModalRowType, isSelectableRowType, } from '$lib/components/shared-components/album-selection/album-selection-utils'; - import { eventManager } from '$lib/managers/event-manager.svelte'; + import { createAlbumMutation, getAllAlbumsQuery, getRecentAlbumsQuery } from '$lib/queries/album.query'; + import { executeMutation, queryClient } from '$lib/queries/client'; import { albumViewSettings } from '$lib/stores/preferences.store'; - import { createAlbum, getAllAlbums, type AlbumResponseDto } from '@immich/sdk'; + import { type AlbumResponseDto } from '@immich/sdk'; import { Button, Icon, Modal, ModalBody, ModalFooter, Text } from '@immich/ui'; import { mdiKeyboardReturn } from '@mdi/js'; - import { onMount } from 'svelte'; + import { createQuery } from '@tanstack/svelte-query'; import { t } from 'svelte-i18n'; import AlbumListItem from '../components/asset-viewer/AlbumListItem.svelte'; import NewAlbumListItem from '../components/shared-components/album-selection/NewAlbumListItem.svelte'; - let albums: AlbumResponseDto[] = $state([]); - let recentAlbums: AlbumResponseDto[] = $state([]); - let loading = $state(true); + const queries = { + albums: createQuery(getAllAlbumsQuery, () => queryClient), + recentAlbums: createQuery(getRecentAlbumsQuery, () => queryClient), + }; + + const albums = $derived(queries.albums.data ?? []); + const recentAlbums = $derived(queries.recentAlbums.data ?? []); + const loading = $derived(queries.albums.isPending); let search = $state(''); let selectedRowIndex: number = $state(-1); @@ -28,12 +34,6 @@ let { onClose, selectedItemsCount }: Props = $props(); - onMount(async () => { - albums = await getAllAlbums({}); - recentAlbums = [...albums].sort((a, b) => (new Date(a.updatedAt) > new Date(b.updatedAt) ? -1 : 1)).slice(0, 3); - loading = false; - }); - const multiSelectedAlbumIds: string[] = $state([]); const multiSelectActive = $derived(multiSelectedAlbumIds.length > 0); @@ -43,11 +43,10 @@ ); const selectableRowCount = $derived(albumModalRows.filter((row) => isSelectableRowType(row.type)).length); - const onNewAlbum = async (name: string) => { - const album = await createAlbum({ createAlbumDto: { albumName: name } }); - eventManager.emit('AlbumCreate', album); - onClose([album]); - }; + const onNewAlbum = (name: string) => + executeMutation(createAlbumMutation, [{ createAlbumDto: { albumName: name } }], { + onSuccess: (album) => onClose([album]), + }); const handleAlbumClick = (album?: AlbumResponseDto) => { if (multiSelectActive) { diff --git a/web/src/lib/modals/AssetAddToAlbumModal.svelte b/web/src/lib/modals/AssetAddToAlbumModal.svelte index 7259dd824..37fccf354 100644 --- a/web/src/lib/modals/AssetAddToAlbumModal.svelte +++ b/web/src/lib/modals/AssetAddToAlbumModal.svelte @@ -1,6 +1,7 @@ diff --git a/web/src/lib/queries/album.query.ts b/web/src/lib/queries/album.query.ts index 5120a6163..7f36d8226 100644 --- a/web/src/lib/queries/album.query.ts +++ b/web/src/lib/queries/album.query.ts @@ -1,8 +1,224 @@ -import { getAlbumInfo } from '@immich/sdk'; -import { queryOptions } from '@tanstack/svelte-query'; +import { + addAssetsToAlbum, + addAssetsToAlbums, + addUsersToAlbum, + AlbumUserRole, + BulkIdErrorReason, + createAlbum, + deleteAlbum, + getAlbumInfo, + getAlbumMapMarkers, + getAlbumStatistics, + getAllAlbums, + removeAssetFromAlbum, + removeUserFromAlbum, + updateAlbumInfo, + updateAlbumUser, + type AlbumResponseDto, + type AlbumsAddAssetsResponseDto, + type BulkIdResponseDto, +} from '@immich/sdk'; +import { toastManager } from '@immich/ui'; +import { mutationOptions, queryOptions, type MutationMeta } from '@tanstack/svelte-query'; +import type { MessageFormatter } from 'svelte-i18n'; +import { goto } from '$app/navigation'; +import { authManager } from '$lib/managers/auth-manager.svelte'; +import { eventManager } from '$lib/managers/event-manager.svelte'; +import { asMutation, queryClient } from '$lib/queries/client'; +import { Route } from '$lib/route'; +import { getFormatter } from '$lib/utils/i18n'; + +type AlbumsFilter = { isOwned?: boolean; isShared?: boolean }; + +export const albumsQueryKey = ['albums']; + +const isOwned = (album: AlbumResponseDto) => + authManager.authenticated && + album.albumUsers.some(({ role, user }) => role === AlbumUserRole.Owner && user.id === authManager.user.id); + +const matchesFilter = (filter: AlbumsFilter) => (album: AlbumResponseDto) => + (filter.isOwned === undefined || isOwned(album) === filter.isOwned) && + (filter.isShared === undefined || album.shared === filter.isShared); + +const findOrFail = (albums: AlbumResponseDto[], id: string) => { + const album = albums.find((album) => album.id === id); + if (!album) { + throw new Error(`Album ${id} not found`); + } + return album; +}; + +export const getAllAlbumsQuery = (filter: AlbumsFilter = {}) => + queryOptions({ + queryKey: albumsQueryKey, + queryFn: ({ signal }) => getAllAlbums({}, { signal }), + select: (albums) => albums.filter(matchesFilter(filter)), + }); + +export const getRecentAlbumsQuery = () => + queryOptions({ + ...getAllAlbumsQuery(), + select: (albums) => + [...albums].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()).slice(0, 3), + }); + +export const getAlbumQuery = (id: string) => + queryOptions({ + ...getAllAlbumsQuery(), + select: (albums) => findOrFail(albums, id), + }); + +export const getAlbumsByAssetQuery = (assetId: string) => + queryOptions({ + queryKey: [...albumsQueryKey, 'asset', assetId], + queryFn: ({ signal }) => getAllAlbums({ assetId }, { signal }), + }); + +export const getAlbumStatisticsQuery = () => + queryOptions({ + queryKey: [...albumsQueryKey, 'statistics'], + queryFn: ({ signal }) => getAlbumStatistics({ signal }), + }); export const getAlbumInfoQuery = (id: string) => queryOptions({ queryKey: ['album', id], - queryFn: ({ signal }) => getAlbumInfo({ id }, { signal }), + queryFn: ({ signal }) => getAlbumInfo({ ...authManager.params, id }, { signal }), + }); + +export const getAlbumMapMarkersQuery = (id: string) => + queryOptions({ + queryKey: ['album', id, 'map-markers'], + queryFn: ({ signal }) => getAlbumMapMarkers({ ...authManager.params, id }, { signal }), + }); + +export const invalidateAlbums = () => queryClient.invalidateQueries({ queryKey: albumsQueryKey }); + +export const invalidateAlbum = (id: string) => + queryClient.invalidateQueries({ queryKey: getAlbumInfoQuery(id).queryKey, exact: true }); + +const onAlbumChange = async (album: AlbumResponseDto) => { + queryClient.setQueryData(getAlbumInfoQuery(album.id).queryKey, album); + await Promise.all([invalidateAlbum(album.id), invalidateAlbums()]); +}; + +const onAlbumAssetsChange = (ids: string[]) => + Promise.all([...ids.map((id) => invalidateAlbum(id)), invalidateAlbums()]); + +export const createAlbumMutation = () => + asMutation(createAlbum, { + onSuccess: onAlbumChange, + meta: { error: { toast: 'errors.failed_to_create_album' } }, + }); + +const defaultMeta: MutationMeta = { + success: { toast: 'album_info_updated' }, + error: { toast: 'errors.unable_to_update_album_info' }, +}; + +export const updateAlbumInfoMutation = (meta?: MutationMeta) => + asMutation(updateAlbumInfo, { onSuccess: onAlbumChange, meta: meta ?? defaultMeta }); + +export const deleteAlbumMutation = (meta?: MutationMeta) => + asMutation(deleteAlbum, { + onSuccess: (_, [{ id }]) => { + queryClient.removeQueries({ queryKey: ['album', id] }); + return invalidateAlbums(); + }, + meta: meta ?? { error: { toast: 'errors.unable_to_delete_album' } }, + }); + +const notifyAddToAlbum = ($t: MessageFormatter, albumId: string, assetIds: string[], results: BulkIdResponseDto[]) => { + const successCount = results.filter(({ success }) => success).length; + const duplicateCount = results.filter(({ error }) => error === 'duplicate').length; + let description: string | undefined; + + if (duplicateCount === assetIds.length) { + description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } }); + } else if (successCount === assetIds.length) { + description = $t('assets_added_to_album_count', { values: { count: successCount } }); + } else if (successCount > 0) { + description = $t('assets_added_to_album_partial_count', { values: { successCount, totalCount: assetIds.length } }); + } + + const button = { label: $t('view_album'), onclick: () => goto(Route.viewAlbum({ id: albumId })) }; + if (description) { + toastManager.primary({ description, button }, { timeout: 5000 }); + return; + } + + toastManager.danger( + { description: $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } }), button }, + { timeout: 5000 }, + ); +}; + +const notifyAddToAlbums = ( + $t: MessageFormatter, + albumIds: string[], + assetIds: string[], + results: AlbumsAddAssetsResponseDto, +) => { + if (results.error === BulkIdErrorReason.Duplicate) { + toastManager.info($t('assets_were_part_of_albums_count', { values: { count: assetIds.length } })); + } else if (results.error) { + toastManager.warning($t('assets_cannot_be_added_to_albums', { values: { count: assetIds.length } })); + } else { + toastManager.primary( + $t('assets_added_to_albums_count', { + values: { albumTotal: albumIds.length, assetTotal: assetIds.length }, + }), + ); + } +}; + +export const addAssetsToAlbumsMutation = ({ notify = true }: { notify?: boolean } = {}) => + mutationOptions({ + mutationFn: ({ + albumIds, + assetIds, + }: { + albumIds: string[]; + assetIds: string[]; + }): Promise => + albumIds.length === 1 + ? addAssetsToAlbum({ ...authManager.params, id: albumIds[0], bulkIdsDto: { ids: assetIds } }) + : addAssetsToAlbums({ ...authManager.params, albumsAddAssetsDto: { albumIds, assetIds } }), + onSuccess: async (results, { albumIds, assetIds }) => { + eventManager.emit('AlbumAddAssets', { assetIds, albumIds }); + if (notify) { + const $t = await getFormatter(); + if (Array.isArray(results)) { + notifyAddToAlbum($t, albumIds[0], assetIds, results); + } else { + notifyAddToAlbums($t, albumIds, assetIds, results); + } + } + await onAlbumAssetsChange(albumIds); + }, + meta: { error: { toast: 'errors.error_adding_assets_to_album' } }, + }); + +export const removeAssetFromAlbumMutation = () => + asMutation(removeAssetFromAlbum, { + onSuccess: (_, [{ id }]) => onAlbumAssetsChange([id]), + meta: { error: { toast: 'errors.error_removing_assets_from_album' } }, + }); + +export const addUsersToAlbumMutation = () => + asMutation(addUsersToAlbum, { + onSuccess: onAlbumChange, + meta: { error: { toast: 'errors.error_adding_users_to_album' } }, + }); + +export const updateAlbumUserMutation = () => + asMutation(updateAlbumUser, { + onSuccess: (_, [{ id }]) => onAlbumAssetsChange([id]), + meta: { error: { toast: 'errors.unable_to_change_album_user_role' } }, + }); + +export const removeUserFromAlbumMutation = () => + asMutation(removeUserFromAlbum, { + onSuccess: (_, [{ id }]) => onAlbumAssetsChange([id]), + meta: { error: { toast: 'errors.unable_to_remove_album_users' } }, }); diff --git a/web/src/lib/queries/shared-link.query.ts b/web/src/lib/queries/shared-link.query.ts index 59c14c389..ba471fb6e 100644 --- a/web/src/lib/queries/shared-link.query.ts +++ b/web/src/lib/queries/shared-link.query.ts @@ -7,7 +7,7 @@ import { type SharedLinkResponseDto, } from '@immich/sdk'; import { queryOptions } from '@tanstack/svelte-query'; -import { getAlbumInfoQuery } from '$lib/queries/album.query'; +import { invalidateAlbum, invalidateAlbums } from '$lib/queries/album.query'; import { asMutation, queryClient } from '$lib/queries/client'; const findOrFail = (sharedLinks: SharedLinkResponseDto[], id: string) => { @@ -31,15 +31,19 @@ export const getSharedLinkQuery = (id: string) => select: (sharedLinks) => findOrFail(sharedLinks, id), }); -const invalidateAlbum = (albumId?: string | null) => - albumId ? queryClient.invalidateQueries({ queryKey: getAlbumInfoQuery(albumId).queryKey }) : undefined; +const onAlbumSharingChange = async (albumId?: string | null) => { + await invalidateAlbums(); + if (albumId) { + await invalidateAlbum(albumId); + } +}; export const createSharedLinkMutation = () => asMutation(createSharedLink, { onSuccess: async (_, [{ sharedLinkCreateDto }]) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: getAllSharedLinksQuery().queryKey, exact: true }), - invalidateAlbum(sharedLinkCreateDto.albumId), + onAlbumSharingChange(sharedLinkCreateDto.albumId), ]); }, meta: { error: { toast: 'errors.failed_to_create_shared_link' } }, @@ -60,7 +64,7 @@ export const removeSharedLinkMutation = () => queryClient.setQueryData(getAllSharedLinksQuery().queryKey, (sharedLinks) => sharedLinks?.filter((sharedLink) => sharedLink.id !== id), ); - await invalidateAlbum(albumId); + await onAlbumSharingChange(albumId); }, meta: { success: { toast: 'deleted_shared_link' }, error: { toast: 'errors.unable_to_delete_shared_link' } }, }); diff --git a/web/src/lib/services/album.service.ts b/web/src/lib/services/album.service.ts index 64b7d2378..68f470d34 100644 --- a/web/src/lib/services/album.service.ts +++ b/web/src/lib/services/album.service.ts @@ -1,21 +1,5 @@ -import { - addAssetsToAlbum as addToAlbum, - addAssetsToAlbums as addToAlbums, - addUsersToAlbum, - AlbumUserRole, - BulkIdErrorReason, - deleteAlbum, - removeUserFromAlbum, - updateAlbumInfo, - updateAlbumUser, - type AlbumResponseDto, - type AlbumsAddAssetsResponseDto, - type AssetResponseDto, - type BulkIdResponseDto, - type UpdateAlbumDto, - type UserResponseDto, -} from '@immich/sdk'; -import { modalManager, toastManager, type ActionItem } from '@immich/ui'; +import { AlbumUserRole, type AlbumResponseDto, type UserResponseDto } from '@immich/sdk'; +import { modalManager, type ActionItem } from '@immich/ui'; import { mdiDownload, mdiImageOutline, @@ -31,19 +15,26 @@ import { } from '@mdi/js'; import { type MessageFormatter } from 'svelte-i18n'; import { goto } from '$app/navigation'; +import { page } from '$app/state'; import { authManager } from '$lib/managers/auth-manager.svelte'; -import { eventManager } from '$lib/managers/event-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import AddUsersModal from '$lib/modals/AddUsersModal.svelte'; import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte'; import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; +import { + addAssetsToAlbumsMutation, + addUsersToAlbumMutation, + deleteAlbumMutation, + removeUserFromAlbumMutation, + updateAlbumInfoMutation, + updateAlbumUserMutation, +} from '$lib/queries/album.query'; +import { executeMutation } from '$lib/queries/client'; import { Route } from '$lib/route'; -import { userInteraction } from '$lib/stores/user.svelte'; import { createAlbumAndRedirect } from '$lib/utils/album-utils'; import { downloadArchive } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; -import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; export const isAlbumOwner = (album: AlbumResponseDto) => album.albumUsers[0].user.id === authManager.user.id; @@ -129,7 +120,7 @@ export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) = return { AddUsers, CreateSharedLink, Delete, Download, Edit, Leave, Options, Share }; }; -export const getAlbumAssetActions = ($t: MessageFormatter, album: AlbumResponseDto, asset: AssetResponseDto) => { +export const getAlbumAssetActions = ($t: MessageFormatter, album: AlbumResponseDto, asset: { id: string }) => { const SetCover: ActionItem = { title: $t('set_as_album_cover'), icon: mdiImageOutline, @@ -146,11 +137,7 @@ export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponse icon: mdiPlusBoxOutline, $if: () => assets.length > 0, onAction: () => - addAssetsToAlbums( - [album.id], - assets.map(({ id }) => id), - { notify: true }, - ).then(() => undefined), + executeMutation(addAssetsToAlbumsMutation, { albumIds: [album.id], assetIds: assets.map(({ id }) => id) }), }; const Upload: ActionItem = { @@ -163,78 +150,13 @@ export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponse return { AddAssets, Upload }; }; -export const addAssetsToAlbums = async (albumIds: string[], assetIds: string[], { notify }: { notify: boolean }) => { - const $t = await getFormatter(); - - try { - if (albumIds.length === 1) { - const albumId = albumIds[0]; - const results = await addToAlbum({ ...authManager.params, id: albumId, bulkIdsDto: { ids: assetIds } }); - if (notify) { - notifyAddToAlbum($t, albumId, assetIds, results); - } - } - - if (albumIds.length > 1) { - const results = await addToAlbums({ ...authManager.params, albumsAddAssetsDto: { albumIds, assetIds } }); - if (notify) { - notifyAddToAlbums($t, albumIds, assetIds, results); - } - } - - eventManager.emit('AlbumAddAssets', { assetIds, albumIds }); - return true; - } catch (error) { - handleError(error, $t('errors.error_adding_assets_to_album')); - return false; +const leaveAlbumPage = (album: AlbumResponseDto) => { + if (page.params.albumId === album.id) { + void goto(Route.albums()); } }; -const notifyAddToAlbum = ($t: MessageFormatter, albumId: string, assetIds: string[], results: BulkIdResponseDto[]) => { - const successCount = results.filter(({ success }) => success).length; - const duplicateCount = results.filter(({ error }) => error === 'duplicate').length; - let description: string | undefined; - - if (duplicateCount === assetIds.length) { - description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } }); - } else if (successCount === assetIds.length) { - description = $t('assets_added_to_album_count', { values: { count: successCount } }); - } else if (successCount > 0) { - description = $t('assets_added_to_album_partial_count', { values: { successCount, totalCount: assetIds.length } }); - } - - const button = { label: $t('view_album'), onclick: () => goto(Route.viewAlbum({ id: albumId })) }; - if (description) { - toastManager.primary({ description, button }, { timeout: 5000 }); - return; - } - - toastManager.danger( - { description: $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } }), button }, - { timeout: 5000 }, - ); -}; - -const notifyAddToAlbums = ( - $t: MessageFormatter, - albumIds: string[], - assetIds: string[], - results: AlbumsAddAssetsResponseDto, -) => { - if (results.error === BulkIdErrorReason.Duplicate) { - toastManager.info($t('assets_were_part_of_albums_count', { values: { count: assetIds.length } })); - } else if (results.error) { - toastManager.warning($t('assets_cannot_be_added_to_albums', { values: { count: assetIds.length } })); - } else { - toastManager.primary( - $t('assets_added_to_albums_count', { - values: { albumTotal: albumIds.length, assetTotal: assetIds.length }, - }), - ); - } -}; - -export const handleUpdateUserAlbumRole = async ({ +export const handleUpdateUserAlbumRole = ({ albumId, userId, role, @@ -242,28 +164,14 @@ export const handleUpdateUserAlbumRole = async ({ albumId: string; userId: string; role: AlbumUserRole; -}) => { - const $t = await getFormatter(); +}) => executeMutation(updateAlbumUserMutation, [{ id: albumId, userId, updateAlbumUserDto: { role } }]); - try { - await updateAlbumUser({ id: albumId, userId, updateAlbumUserDto: { role } }); - eventManager.emit('AlbumUserUpdate', { albumId, userId, role }); - } catch (error) { - handleError(error, $t('errors.unable_to_change_album_user_role')); - } -}; - -export const handleAddUsersToAlbum = async (album: AlbumResponseDto, users: UserResponseDto[]) => { - const $t = await getFormatter(); - - try { - await addUsersToAlbum({ id: album.id, addUsersDto: { albumUsers: users.map(({ id }) => ({ userId: id })) } }); - eventManager.emit('AlbumShare'); - return true; - } catch (error) { - handleError(error, $t('errors.error_adding_users_to_album')); - } -}; +export const handleAddUsersToAlbum = (album: AlbumResponseDto, users: UserResponseDto[], onSuccess?: () => void) => + executeMutation( + addUsersToAlbumMutation, + [{ id: album.id, addUsersDto: { albumUsers: users.map(({ id }) => ({ userId: id })) } }], + { onSuccess }, + ); export const handleRemoveUserFromAlbum = async (album: AlbumResponseDto, albumUser: UserResponseDto) => { const $t = await getFormatter(); @@ -278,12 +186,7 @@ export const handleRemoveUserFromAlbum = async (album: AlbumResponseDto, albumUs return; } - try { - await removeUserFromAlbum({ id: album.id, userId: albumUser.id }); - eventManager.emit('AlbumUserDelete', { albumId: album.id, userId: albumUser.id }); - } catch (error) { - handleError(error, $t('errors.unable_to_remove_album_users')); - } + await executeMutation(removeUserFromAlbumMutation, [{ id: album.id, userId: albumUser.id }]); }; export const handleLeaveAlbum = async (album: AlbumResponseDto) => { @@ -299,49 +202,20 @@ export const handleLeaveAlbum = async (album: AlbumResponseDto) => { return; } - try { - await removeUserFromAlbum({ id: album.id, userId: 'me' }); - userInteraction.recentAlbums = undefined; - eventManager.emit('AlbumDelete', album); - return true; - } catch (error) { - handleError(error, $t('errors.unable_to_remove_album_users')); - } + await executeMutation(removeUserFromAlbumMutation, [{ id: album.id, userId: 'me' }], { + onSuccess: () => leaveAlbumPage(album), + }); }; -const handleUpdateThumbnail = async (album: AlbumResponseDto, assetId: string) => { - const $t = await getFormatter(); - - try { - const response = await updateAlbumInfo({ - id: album.id, - updateAlbumDto: { - albumThumbnailAssetId: assetId, - }, - }); - eventManager.emit('AlbumUpdate', response); - toastManager.primary($t('album_cover_updated')); - } catch (error) { - handleError(error, $t('errors.unable_to_update_album_cover')); - } -}; - -export const handleUpdateAlbum = async ({ id }: { id: string }, dto: UpdateAlbumDto) => { - const $t = await getFormatter(); - - try { - const response = await updateAlbumInfo({ id, updateAlbumDto: dto }); - eventManager.emit('AlbumUpdate', response); - toastManager.primary({ - description: $t('album_info_updated'), - button: { label: $t('view_album'), onclick: () => goto(Route.viewAlbum({ id })) }, - }); - - return true; - } catch (error) { - handleError(error, $t('errors.unable_to_update_album_info')); - } -}; +const handleUpdateThumbnail = (album: AlbumResponseDto, assetId: string) => + executeMutation( + () => + updateAlbumInfoMutation({ + success: { toast: 'album_cover_updated' }, + error: { toast: 'errors.unable_to_update_album_cover' }, + }), + [{ id: album.id, updateAlbumDto: { albumThumbnailAssetId: assetId } }], + ); export const handleDeleteAlbum = async (album: AlbumResponseDto, options?: { prompt?: boolean; notify?: boolean }) => { const $t = await getFormatter(); @@ -355,21 +229,18 @@ export const handleDeleteAlbum = async (album: AlbumResponseDto, options?: { pro const description = $t('album_delete_confirmation_description'); const success = await modalManager.showDialog({ prompt: `${confirmation} ${description}` }); if (!success) { - return false; + return; } } - try { - await deleteAlbum({ id: album.id }); - eventManager.emit('AlbumDelete', album); - if (notify) { - toastManager.primary(); - } - return true; - } catch (error) { - handleError(error, $t('errors.unable_to_delete_album'), { notify }); - return false; - } + await executeMutation( + () => + deleteAlbumMutation( + notify ? { success: { toast: true }, error: { toast: 'errors.unable_to_delete_album' } } : {}, + ), + [{ id: album.id }], + { onSuccess: () => leaveAlbumPage(album) }, + ); }; export const handleDownloadAlbum = async (album: AlbumResponseDto) => { diff --git a/web/src/lib/services/asset.service.ts b/web/src/lib/services/asset.service.ts index c7dae7968..30c468244 100644 --- a/web/src/lib/services/asset.service.ts +++ b/web/src/lib/services/asset.service.ts @@ -5,7 +5,6 @@ import { AssetVisibility, bulkTagAssets, getAssetInfo, - removeAssetFromAlbum, runAssetJobs, updateAsset, type AlbumResponseDto, @@ -52,7 +51,8 @@ import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte'; import AssetTagModal from '$lib/modals/AssetTagModal.svelte'; import ProfileImageCropperModal from '$lib/modals/ProfileImageCropperModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; -import { readPreloadedQuery } from '$lib/queries/client'; +import { removeAssetFromAlbumMutation } from '$lib/queries/album.query'; +import { executeMutation, readPreloadedQuery } from '$lib/queries/client'; import { getUserConfigQuery } from '$lib/queries/config-user.query'; import { Route } from '$lib/route'; import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; @@ -518,19 +518,13 @@ const handleBulkRemoveAssetsFromAlbum = async (assetIds: string[], album: AlbumR const handleRemoveAssetsFromAlbum = async (assetIds: string[], album: AlbumResponseDto) => { const $t = await getFormatter(); - try { - const results = await removeAssetFromAlbum({ - id: album.id, - bulkIdsDto: { ids: assetIds }, - }); - - const count = results.filter(({ success }) => success).length; - - toastManager.primary($t('assets_removed_count', { values: { count } })); - eventManager.emit('AlbumRemoveAssets', { assetIds, albumIds: [album.id] }); - } catch (error) { - handleError(error, $t('errors.error_removing_assets_from_album')); - } + await executeMutation(removeAssetFromAlbumMutation, [{ id: album.id, bulkIdsDto: { ids: assetIds } }], { + onSuccess: (results) => { + const count = results.filter(({ success }) => success).length; + toastManager.primary($t('assets_removed_count', { values: { count } })); + eventManager.emit('AlbumRemoveAssets', { assetIds, albumIds: [album.id] }); + }, + }); }; const getAssetJobMessage = ($t: MessageFormatter, job: AssetJobName) => { diff --git a/web/src/lib/stores/user.svelte.ts b/web/src/lib/stores/user.svelte.ts deleted file mode 100644 index f1ae7f5a4..000000000 --- a/web/src/lib/stores/user.svelte.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { AlbumResponseDto } from '@immich/sdk'; -import { eventManager } from '$lib/managers/event-manager.svelte'; - -interface UserInteractions { - recentAlbums?: AlbumResponseDto[]; -} - -const defaultUserInteraction: UserInteractions = { - recentAlbums: undefined, -}; - -export const userInteraction = $state(defaultUserInteraction); - -const resetRecentAlbums = () => { - userInteraction.recentAlbums = undefined; -}; - -const reset = () => { - Object.assign(userInteraction, defaultUserInteraction); -}; - -// eslint-disable-next-line unicorn/no-top-level-side-effects -eventManager.on({ - AlbumCreate: () => resetRecentAlbums(), - AlbumUpdate: () => resetRecentAlbums(), - AlbumDelete: () => resetRecentAlbums(), - AuthLogout: () => reset(), -}); diff --git a/web/src/lib/utils/album-utils.ts b/web/src/lib/utils/album-utils.ts index 97546f0b0..377143836 100644 --- a/web/src/lib/utils/album-utils.ts +++ b/web/src/lib/utils/album-utils.ts @@ -1,10 +1,9 @@ import type { AlbumResponseDto } from '@immich/sdk'; -import * as sdk from '@immich/sdk'; import { orderBy } from 'lodash-es'; -import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; import { goto } from '$app/navigation'; -import { eventManager } from '$lib/managers/event-manager.svelte'; +import { createAlbumMutation } from '$lib/queries/album.query'; +import { executeMutation } from '$lib/queries/client'; import { Route } from '$lib/route'; import { AlbumFilter, @@ -15,35 +14,16 @@ import { locale, type AlbumViewSettings, } from '$lib/stores/preferences.store'; -import { handleError } from '$lib/utils/handle-error'; /** * ------------------------- * Albums General Management * ------------------------- */ -export const createAlbum = async (name?: string, assetIds?: string[]) => { - try { - const newAlbum: AlbumResponseDto = await sdk.createAlbum({ - createAlbumDto: { - albumName: name ?? '', - assetIds, - }, - }); - eventManager.emit('AlbumCreate', newAlbum); - return newAlbum; - } catch (error) { - const $t = get(t); - handleError(error, $t('errors.failed_to_create_album')); - } -}; - -export const createAlbumAndRedirect = async (name?: string, assetIds?: string[]) => { - const newAlbum = await createAlbum(name, assetIds); - if (newAlbum) { - await goto(Route.viewAlbum(newAlbum)); - } -}; +export const createAlbumAndRedirect = (name?: string, assetIds?: string[]) => + executeMutation(createAlbumMutation, [{ createAlbumDto: { albumName: name ?? '', assetIds } }], { + onSuccess: (album) => void goto(Route.viewAlbum(album)), + }); /** * ------------- diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index c926fd9cb..3e4e369f0 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -12,7 +12,8 @@ import { t } from 'svelte-i18n'; import { get } from 'svelte/store'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { uploadManager } from '$lib/managers/upload-manager.svelte'; -import { addAssetsToAlbums } from '$lib/services/album.service'; +import { addAssetsToAlbumsMutation } from '$lib/queries/album.query'; +import { executeMutation } from '$lib/queries/client'; import { uploadAssetsStore } from '$lib/stores/upload'; import { UploadState } from '$lib/types'; import { uploadRequest } from '$lib/utils'; @@ -221,7 +222,10 @@ async function fileUploader({ if (albumId && !authManager.isSharedLink) { uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_adding_to_album') }); - await addAssetsToAlbums([albumId], [responseData.id], { notify: false }); + await executeMutation(() => addAssetsToAlbumsMutation({ notify: false }), { + albumIds: [albumId], + assetIds: [responseData.id], + }); uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_added_to_album') }); } diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index 0548d882c..4bb90c03d 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -6,6 +6,8 @@ import EmptyPlaceholder from '$lib/components/shared-components/EmptyPlaceholder.svelte'; import GroupTab from '$lib/elements/GroupTab.svelte'; import SearchBar from '$lib/elements/SearchBar.svelte'; + import { getAllAlbumsQuery } from '$lib/queries/album.query'; + import { createPreloadedQuery } from '$lib/queries/client'; import { Route } from '$lib/route'; import { AlbumFilter, albumViewSettings } from '$lib/stores/preferences.store'; import { createAlbumAndRedirect } from '$lib/utils/album-utils'; @@ -18,6 +20,11 @@ let { data }: Props = $props(); + const queries = { + ownedAlbums: createPreloadedQuery(() => getAllAlbumsQuery({ isOwned: true })), + sharedAlbums: createPreloadedQuery(() => getAllAlbumsQuery({ isShared: true })), + }; + let searchQuery = $state(''); let albumGroups: string[] = $state([]); @@ -44,8 +51,8 @@ { await authenticate(url); - const sharedAlbums = await getAllAlbums({ isShared: true }); - const albums = await getAllAlbums({ isOwned: true }); + await queryClient.query(getAllAlbumsQuery()); const $t = await getFormatter(); return { - albums, - sharedAlbums, meta: { title: $t('albums'), }, diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 4739dca7b..e04ee20cb 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -27,27 +27,18 @@ import { assetMultiSelectManager, AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; - import { eventManager } from '$lib/managers/event-manager.svelte'; - import { getAlbumInfoQuery } from '$lib/queries/album.query'; - import { createPreloadedQuery, queryClient } from '$lib/queries/client'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { Route } from '$lib/route'; - import { - getAlbumActions, - getAlbumAssetsActions, - handleDeleteAlbum, - isAlbumEditor, - isAlbumOwner, - } from '$lib/services/album.service'; + import { getAlbumActions, getAlbumAssetsActions, isAlbumEditor, isAlbumOwner } from '$lib/services/album.service'; import { getGlobalActions } from '$lib/services/app.service'; import { getAssetBulkActions } from '$lib/services/asset.service'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { handlePromiseError } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { isAlbumsRoute, navigate, type AssetGridRouteSearchParams } from '$lib/utils/navigation'; - import { AlbumUserRole, AssetVisibility, updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk'; - import { ActionButton, CommandPaletteDefaultProvider, Icon, IconButton, toastManager } from '@immich/ui'; + import { AlbumUserRole, AssetVisibility } from '@immich/sdk'; + import { ActionButton, CommandPaletteDefaultProvider, Icon, IconButton } from '@immich/ui'; import { mdiAccountEye, mdiAccountEyeOutline, @@ -68,12 +59,23 @@ import AlbumTitle from './AlbumTitle.svelte'; import ActionMenuItem from '$lib/components/ActionMenuItem.svelte'; import { delay } from '$lib/utils/asset-utils'; + import { + deleteAlbumMutation, + getAlbumInfoQuery, + invalidateAlbum, + updateAlbumInfoMutation, + } from '$lib/queries/album.query'; + import { createPreloadedQuery, executeMutation } from '$lib/queries/client'; import { getUserConfigQuery } from '$lib/queries/config-user.query'; - const query = createPreloadedQuery(getUserConfigQuery); - const config = $derived(query.data); + let { params }: PageProps = $props(); - const { params }: PageProps = $props(); + const queries = { + config: createPreloadedQuery(getUserConfigQuery), + album: createPreloadedQuery(() => getAlbumInfoQuery(params.albumId)), + }; + const config = $derived(queries.config.data); + const album = $derived(queries.album.data); let { slideshowState, slideshowNavigation } = slideshowStore; let oldAt: AssetGridRouteSearchParams | null | undefined = $state(); let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW); @@ -122,16 +124,7 @@ await goto(Route.albums()); }; - const refreshAlbum = async (updated?: AlbumResponseDto) => { - if (updated) { - if (updated.id !== album.id) { - return; - } - queryClient.setQueryData(getAlbumInfoQuery(album.id).queryKey, updated); - } - - await queryClient.invalidateQueries({ queryKey: getAlbumInfoQuery(album.id).queryKey }); - }; + const refreshAlbum = () => invalidateAlbum(album.id); const setModeToView = async () => { timelineManager.suspendTransitions = true; @@ -191,29 +184,22 @@ await updateThumbnail(firstAsset.id); }; - const updateThumbnail = async (assetId: string) => { - try { - const response = await updateAlbumInfo({ - id: album.id, - updateAlbumDto: { - albumThumbnailAssetId: assetId, - }, - }); - eventManager.emit('AlbumUpdate', response); - toastManager.primary($t('album_cover_updated')); - } catch (error) { - handleError(error, $t('errors.unable_to_update_album_cover')); - } - }; + const updateThumbnail = (assetId: string) => + executeMutation( + () => + updateAlbumInfoMutation({ + success: { toast: 'album_cover_updated' }, + error: { toast: 'errors.unable_to_update_album_cover' }, + }), + [{ id: album.id, updateAlbumDto: { albumThumbnailAssetId: assetId } }], + ); onNavigate(async ({ to }) => { if (!isAlbumsRoute(to?.route.id) && album.assetCount === 0 && !album.albumName) { - await handleDeleteAlbum(album, { notify: false, prompt: false }); + await executeMutation(() => deleteAlbumMutation({}), [{ id: album.id }]); } }); - const albumQuery = createPreloadedQuery(() => getAlbumInfoQuery(params.albumId)); - let album = $derived(albumQuery.data); const albumId = $derived(album.id); const albumHasViewers = $derived(album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer)); @@ -274,42 +260,15 @@ viewMode === AlbumPageViewMode.SELECT_ASSETS ? timelineMultiSelectManager : assetMultiSelectManager, ); - const onAlbumDelete = async ({ id }: AlbumResponseDto) => { - if (id !== album.id) { - return; - } - - await goto(Route.albums()); - viewMode = AlbumPageViewMode.VIEW; - }; - - const onAlbumAddAssets = async ({ albumIds, assetIds }: { albumIds: string[]; assetIds: string[] }) => { + const onAlbumAddAssets = async ({ albumIds }: { albumIds: string[]; assetIds: string[] }) => { if (!albumIds.includes(album.id)) { return; } - album = { ...album, assetCount: album.assetCount + assetIds.length }; - timelineMultiSelectManager.clear(); await setModeToView(); }; - const onAlbumShare = async () => { - await refreshAlbum(); - await setModeToView(); - }; - - const onAlbumUserUpdate = ({ albumId, userId, role }: { albumId: string; userId: string; role: AlbumUserRole }) => { - if (albumId !== album.id) { - return; - } - - const albumUsers = album.albumUsers.map((albumUser) => - albumUser.user.id === userId ? { ...albumUser, role } : albumUser, - ); - album = { ...album, albumUsers }; - }; - const { Cast } = $derived(getGlobalActions($t)); const Actions = $derived(getAlbumActions($t, album)); const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineMultiSelectManager.assets)); @@ -323,16 +282,7 @@ }); - refreshAlbum()} - onAlbumUpdate={(album) => refreshAlbum(album)} - onAssetsDelete={() => refreshAlbum()} -/> + refreshAlbum()} />
@@ -357,12 +307,7 @@ {#if viewMode !== AlbumPageViewMode.SELECT_THUMBNAIL}
- (album = { ...album, albumName })} - /> + {#if album.assetCount > 0} @@ -406,11 +351,7 @@
{/if} - album.description, (description) => (album = { ...album, description })} - /> +
{/if} diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte index c476a18bd..d8556a0c3 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte @@ -1,8 +1,7 @@ {#if isEditor}