diff --git a/web/src/lib/components/album-page/AlbumsList.svelte b/web/src/lib/components/album-page/AlbumsList.svelte index 0cbdaa62b..1370c8332 100644 --- a/web/src/lib/components/album-page/AlbumsList.svelte +++ b/web/src/lib/components/album-page/AlbumsList.svelte @@ -15,7 +15,7 @@ } from '$lib/stores/preferences.store'; import { getSelectedAlbumGroupOption, sortAlbums, stringToSortOrder, type AlbumGroup } from '$lib/utils/album-utils'; import { normalizeSearchString } from '$lib/utils/string-utils'; - import { type AlbumResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; + import { type AlbumResponseDto } from '@immich/sdk'; import { groupBy } from 'lodash-es'; import { onMount, type Snippet } from 'svelte'; import { t } from 'svelte-i18n'; @@ -185,15 +185,9 @@ ownedAlbums = ownedAlbums.filter(({ id }) => id !== album.id); sharedAlbums = sharedAlbums.filter(({ id }) => id !== album.id); }; - - const onSharedLinkCreate = (sharedLink: SharedLinkResponseDto) => { - if (sharedLink.album) { - onAlbumUpdate(sharedLink.album); - } - }; - + {#if albums.length > 0} {#if userSettings.view === AlbumViewMode.Cover} diff --git a/web/src/lib/components/timeline/actions/RemoveFromSharedLinkAction.svelte b/web/src/lib/components/timeline/actions/RemoveFromSharedLinkAction.svelte index db88593b0..b88abcf89 100644 --- a/web/src/lib/components/timeline/actions/RemoveFromSharedLinkAction.svelte +++ b/web/src/lib/components/timeline/actions/RemoveFromSharedLinkAction.svelte @@ -1,9 +1,10 @@ - + diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts index 61fd2cd82..b2e1c9aa2 100644 --- a/web/src/lib/managers/event-manager.svelte.ts +++ b/web/src/lib/managers/event-manager.svelte.ts @@ -10,7 +10,6 @@ import type { PersonResponseDto, QueueResponseDto, ReleaseEventV1, - SharedLinkResponseDto, StackResponseDto, AdminConfigDto, TagResponseDto, @@ -67,10 +66,6 @@ export type Events = { QueueUpdate: [QueueResponseDto]; - SharedLinkCreate: [SharedLinkResponseDto]; - SharedLinkUpdate: [SharedLinkResponseDto]; - SharedLinkDelete: [SharedLinkResponseDto]; - StackCreate: [StackResponseDto]; /** Unstacked, with assets to handle */ StackDelete: [{ id: string; assets: AssetResponseDto[] }]; diff --git a/web/src/lib/modals/AlbumOptionsModal.svelte b/web/src/lib/modals/AlbumOptionsModal.svelte index 1ebce5bed..5982c4a45 100644 --- a/web/src/lib/modals/AlbumOptionsModal.svelte +++ b/web/src/lib/modals/AlbumOptionsModal.svelte @@ -9,17 +9,11 @@ handleUpdateAlbum, handleUpdateUserAlbumRole, } from '$lib/services/album.service'; - import { - AlbumUserRole, - AssetOrder, - getAlbumInfo, - getAllSharedLinks, - type AlbumResponseDto, - type SharedLinkResponseDto, - type UserResponseDto, - } from '@immich/sdk'; + import { AlbumUserRole, AssetOrder, getAlbumInfo, type AlbumResponseDto, type UserResponseDto } from '@immich/sdk'; import { Field, HStack, Modal, ModalBody, Select, Stack, Switch, Text, type SelectOption } from '@immich/ui'; - import { onMount } from 'svelte'; + import { createQuery } from '@tanstack/svelte-query'; + import { queryClient } from '$lib/queries/client'; + import { getAllSharedLinksQuery } from '$lib/queries/shared-link.query'; import { t } from 'svelte-i18n'; type Props = { @@ -48,30 +42,16 @@ await refreshAlbum(); }; - const onSharedLinkCreate = (sharedLink: SharedLinkResponseDto) => { - sharedLinks.push(sharedLink); - }; - - const onSharedLinkDelete = (sharedLink: SharedLinkResponseDto) => { - sharedLinks = sharedLinks.filter(({ id }) => sharedLink.id !== id); - }; - const { AddUsers, CreateSharedLink } = $derived(getAlbumActions($t, album)); - let sharedLinks: SharedLinkResponseDto[] = $state([]); - - onMount(async () => { - sharedLinks = await getAllSharedLinks({ albumId: album.id }); - }); + const query = createQuery( + () => getAllSharedLinksQuery({ albumId: album.id }), + () => queryClient, + ); + const sharedLinks = $derived(query.data ?? []); - (album = newAlbum)} -/> + (album = newAlbum)} /> diff --git a/web/src/lib/modals/SharedLinkCreateModal.svelte b/web/src/lib/modals/SharedLinkCreateModal.svelte index 953076667..4992f83a0 100644 --- a/web/src/lib/modals/SharedLinkCreateModal.svelte +++ b/web/src/lib/modals/SharedLinkCreateModal.svelte @@ -1,6 +1,8 @@ diff --git a/web/src/lib/queries/album.query.ts b/web/src/lib/queries/album.query.ts new file mode 100644 index 000000000..5120a6163 --- /dev/null +++ b/web/src/lib/queries/album.query.ts @@ -0,0 +1,8 @@ +import { getAlbumInfo } from '@immich/sdk'; +import { queryOptions } from '@tanstack/svelte-query'; + +export const getAlbumInfoQuery = (id: string) => + queryOptions({ + queryKey: ['album', id], + queryFn: ({ signal }) => getAlbumInfo({ id }, { signal }), + }); diff --git a/web/src/lib/queries/shared-link.query.ts b/web/src/lib/queries/shared-link.query.ts new file mode 100644 index 000000000..59c14c389 --- /dev/null +++ b/web/src/lib/queries/shared-link.query.ts @@ -0,0 +1,72 @@ +import { + createSharedLink, + getAllSharedLinks, + removeSharedLink, + removeSharedLinkAssets, + updateSharedLink, + type SharedLinkResponseDto, +} from '@immich/sdk'; +import { queryOptions } from '@tanstack/svelte-query'; +import { getAlbumInfoQuery } from '$lib/queries/album.query'; +import { asMutation, queryClient } from '$lib/queries/client'; + +const findOrFail = (sharedLinks: SharedLinkResponseDto[], id: string) => { + const sharedLink = sharedLinks.find((sharedLink) => sharedLink.id === id); + if (!sharedLink) { + throw new Error(`Shared link ${id} not found`); + } + return sharedLink; +}; + +export const getAllSharedLinksQuery = ({ albumId }: { albumId?: string } = {}) => + queryOptions({ + queryKey: ['shared-links'], + queryFn: ({ signal }) => getAllSharedLinks({}, { signal }), + select: (sharedLinks) => (albumId ? sharedLinks.filter(({ album }) => album?.id === albumId) : sharedLinks), + }); + +export const getSharedLinkQuery = (id: string) => + queryOptions({ + ...getAllSharedLinksQuery(), + select: (sharedLinks) => findOrFail(sharedLinks, id), + }); + +const invalidateAlbum = (albumId?: string | null) => + albumId ? queryClient.invalidateQueries({ queryKey: getAlbumInfoQuery(albumId).queryKey }) : undefined; + +export const createSharedLinkMutation = () => + asMutation(createSharedLink, { + onSuccess: async (_, [{ sharedLinkCreateDto }]) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: getAllSharedLinksQuery().queryKey, exact: true }), + invalidateAlbum(sharedLinkCreateDto.albumId), + ]); + }, + meta: { error: { toast: 'errors.failed_to_create_shared_link' } }, + }); + +export const updateSharedLinkMutation = () => + asMutation(updateSharedLink, { + onSuccess: () => queryClient.invalidateQueries({ queryKey: getAllSharedLinksQuery().queryKey, exact: true }), + meta: { success: { toast: 'saved' }, error: { toast: 'errors.failed_to_edit_shared_link' } }, + }); + +export const removeSharedLinkMutation = () => + asMutation(removeSharedLink, { + onSuccess: async (_, [{ id }]) => { + const albumId = queryClient + .getQueryData(getAllSharedLinksQuery().queryKey) + ?.find((sharedLink) => sharedLink.id === id)?.album?.id; + queryClient.setQueryData(getAllSharedLinksQuery().queryKey, (sharedLinks) => + sharedLinks?.filter((sharedLink) => sharedLink.id !== id), + ); + await invalidateAlbum(albumId); + }, + meta: { success: { toast: 'deleted_shared_link' }, error: { toast: 'errors.unable_to_delete_shared_link' } }, + }); + +export const removeSharedLinkAssetsMutation = () => + asMutation(removeSharedLinkAssets, { + onSuccess: () => queryClient.invalidateQueries({ queryKey: getAllSharedLinksQuery().queryKey, exact: true }), + meta: { error: { toast: 'errors.unable_to_remove_assets_from_shared_link' } }, + }); diff --git a/web/src/lib/services/shared-link.service.ts b/web/src/lib/services/shared-link.service.ts index e1873e4d9..83d058207 100644 --- a/web/src/lib/services/shared-link.service.ts +++ b/web/src/lib/services/shared-link.service.ts @@ -1,24 +1,14 @@ -import { - createSharedLink, - getSharedLinkById, - removeSharedLink, - removeSharedLinkAssets, - updateSharedLink, - type SharedLinkCreateDto, - type SharedLinkEditDto, - type SharedLinkResponseDto, -} from '@immich/sdk'; -import { modalManager, toastManager, type ActionItem } from '@immich/ui'; +import { type SharedLinkResponseDto } from '@immich/sdk'; +import { modalManager, type ActionItem } from '@immich/ui'; import { mdiContentCopy, mdiLink, mdiPencilOutline, mdiQrcode, mdiTrashCanOutline } from '@mdi/js'; import type { MessageFormatter } from 'svelte-i18n'; import { goto } from '$app/navigation'; -import { eventManager } from '$lib/managers/event-manager.svelte'; import QrCodeModal from '$lib/modals/QrCodeModal.svelte'; -import { readPreloadedQuery } from '$lib/queries/client'; +import { executeMutation, readPreloadedQuery } from '$lib/queries/client'; import { getUserConfigQuery } from '$lib/queries/config-user.query'; +import { removeSharedLinkMutation } from '$lib/queries/shared-link.query'; import { Route } from '$lib/route'; import { copyToClipboard } from '$lib/utils'; -import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; export const getSharedLinksActions = ($t: MessageFormatter) => { @@ -54,7 +44,7 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin const ViewQrCode: ActionItem = { title: $t('view_qr_code'), icon: mdiQrcode, - onAction: () => handleShowSharedLinkQrCode(sharedLink), + onAction: () => showSharedLinkQrCode(sharedLink), }; return { Edit, Delete, Copy, ViewQrCode }; @@ -65,42 +55,9 @@ export const asUrl = (sharedLink: SharedLinkResponseDto) => { return new URL(path, readPreloadedQuery(getUserConfigQuery()).server.externalDomain || location.origin).href; }; -export const handleCreateSharedLink = async (dto: SharedLinkCreateDto) => { +export const showSharedLinkQrCode = async (sharedLink: SharedLinkResponseDto) => { const $t = await getFormatter(); - - try { - let sharedLink = await createSharedLink({ sharedLinkCreateDto: dto }); - if (dto.albumId) { - // fetch album details, for event - sharedLink = await getSharedLinkById({ id: sharedLink.id }); - } - - eventManager.emit('SharedLinkCreate', sharedLink); - - // prevent nested modal - void handleShowSharedLinkQrCode(sharedLink); - - return true; - } catch (error) { - handleError(error, $t('errors.failed_to_create_shared_link')); - return false; - } -}; - -export const handleUpdateSharedLink = async (sharedLink: SharedLinkResponseDto, dto: SharedLinkEditDto) => { - const $t = await getFormatter(); - - try { - const response = await updateSharedLink({ id: sharedLink.id, sharedLinkEditDto: dto }); - - eventManager.emit('SharedLinkUpdate', { album: sharedLink.album, ...response }); - toastManager.primary($t('saved')); - - return true; - } catch (error) { - handleError(error, $t('errors.failed_to_edit_shared_link')); - return false; - } + await modalManager.show(QrCodeModal, { title: $t('view_link'), value: asUrl(sharedLink) }); }; const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto) => { @@ -114,50 +71,5 @@ const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto) => { return; } - try { - await removeSharedLink({ id: sharedLink.id }); - eventManager.emit('SharedLinkDelete', sharedLink); - toastManager.primary($t('deleted_shared_link')); - } catch (error) { - handleError(error, $t('errors.unable_to_delete_shared_link')); - } -}; - -export const handleRemoveSharedLinkAssets = async (sharedLink: SharedLinkResponseDto, assetIds: string[]) => { - const $t = await getFormatter(); - const success = await modalManager.showDialog({ - title: $t('remove_assets_title'), - prompt: $t('remove_assets_shared_link_confirmation', { values: { count: assetIds.length } }), - confirmText: $t('remove'), - }); - if (!success) { - return false; - } - - try { - const results = await removeSharedLinkAssets({ - id: sharedLink.id, - assetIdsDto: { assetIds }, - }); - - for (const result of results) { - if (!result.success) { - continue; - } - - sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== result.assetId); - } - - const count = results.filter((item) => item.success).length; - toastManager.primary($t('assets_removed_count', { values: { count } })); - return true; - } catch (error) { - handleError(error, $t('errors.unable_to_remove_assets_from_shared_link')); - return false; - } -}; - -const handleShowSharedLinkQrCode = async (sharedLink: SharedLinkResponseDto) => { - const $t = await getFormatter(); - await modalManager.show(QrCodeModal, { title: $t('view_link'), value: asUrl(sharedLink) }); + await executeMutation(removeSharedLinkMutation, [{ id: sharedLink.id }]); }; 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 b9df06f26..4739dca7b 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 @@ -1,6 +1,5 @@ refreshAlbum(link.album)} - onSharedLinkDelete={() => refreshAlbum()} {onAlbumDelete} {onAlbumAddAssets} {onAlbumRemoveAssets} diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts index 13b513c32..1f4bea58a 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,16 +1,14 @@ -import { getAlbumInfo } from '@immich/sdk'; +import { getAlbumInfoQuery } from '$lib/queries/album.query'; +import { queryClient } from '$lib/queries/client'; import { authenticate } from '$lib/utils/auth'; import type { PageLoad } from './$types'; -export const load = (async ({ params, url, depends }) => { +export const load = (async ({ params, url }) => { await authenticate(url); - depends('album:data'); - - const album = await getAlbumInfo({ id: params.albumId }); + const album = await queryClient.query(getAlbumInfoQuery(params.albumId)); return { - album, meta: { title: album.albumName, }, diff --git a/web/src/routes/(user)/shared-links/(list)/+layout.svelte b/web/src/routes/(user)/shared-links/(list)/+layout.svelte index aad22feac..393a37318 100644 --- a/web/src/routes/(user)/shared-links/(list)/+layout.svelte +++ b/web/src/routes/(user)/shared-links/(list)/+layout.svelte @@ -2,14 +2,15 @@ import { goto } from '$app/navigation'; import { page } from '$app/state'; import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte'; - import OnEvents from '$lib/components/OnEvents.svelte'; import SharedLinkCard from './SharedLinkCard.svelte'; import { type SharedLinkTab } from '$lib/constants'; import GroupTab from '$lib/elements/GroupTab.svelte'; import { Route } from '$lib/route'; - import { getAllSharedLinks, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk'; + import { createPreloadedQuery } from '$lib/queries/client'; + import { getAllSharedLinksQuery } from '$lib/queries/shared-link.query'; + import { SharedLinkType } from '@immich/sdk'; import { Container } from '@immich/ui'; - import { onMount, type Snippet } from 'svelte'; + import { type Snippet } from 'svelte'; import { t } from 'svelte-i18n'; import type { LayoutData } from './$types'; @@ -20,15 +21,8 @@ const { children, data }: Props = $props(); - let sharedLinks: SharedLinkResponseDto[] = $state([]); - - const refresh = async () => { - sharedLinks = await getAllSharedLinks({}); - }; - - onMount(async () => { - await refresh(); - }); + const query = createPreloadedQuery(getAllSharedLinksQuery); + const sharedLinks = $derived(query.data); const filterMap: Record = { all: $t('all'), @@ -54,21 +48,8 @@ (type === SharedLinkType.Individual && selectedTab === 'individual'), ), ); - - const onSharedLinkUpdate = (sharedLink: SharedLinkResponseDto) => { - const index = sharedLinks.findIndex((link) => link.id === sharedLink.id); - if (index !== -1) { - sharedLinks[index] = sharedLink; - } - }; - - const onSharedLinkDelete = (sharedLink: SharedLinkResponseDto) => { - sharedLinks = sharedLinks.filter(({ id }) => id !== sharedLink.id); - }; - - {#snippet buttons()}