+
+

+
-
-
{album.albumName}
-
-
+
-
- {/each}
-
- {/if}
- {/await}
+
+
+ {/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 @@