+
+ {$t('people')}
+
+
+
+
-
+
-
{user.name}
-
{$t('owner')}
+
{$user.name}
+
+
+
{#each album.albumUsers as { user, role } (user.id)}
-
-
-
+
+
-
{user.name}
- {#if role === AlbumUserRole.Viewer}
- {$t('role_viewer')}
- {:else}
- {$t('role_editor')}
- {/if}
- {#if user.id !== album.ownerId}
-
- {#if role === AlbumUserRole.Viewer}
- handleUpdateSharedUserRole(user, AlbumUserRole.Editor)}
- text={$t('allow_edits')}
- />
- {:else}
- handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)}
- text={$t('disallow_edits')}
- />
- {/if}
-
- handleRemoveUser(user)} text={$t('remove')} />
-
- {/if}
+
+
{/each}
-
+
+
+ {$t('shared_links')}
+
+
+
+
+ {#each sharedLinks as sharedLink (sharedLink.id)}
+
+ {/each}
+
+
+
diff --git a/web/src/lib/modals/AlbumShareModal.svelte b/web/src/lib/modals/AlbumShareModal.svelte
deleted file mode 100644
index 4ee612af06..0000000000
--- a/web/src/lib/modals/AlbumShareModal.svelte
+++ /dev/null
@@ -1,192 +0,0 @@
-
-
-
-
- {#if Object.keys(selectedUsers).length > 0}
-
-
{$t('selected')}
-
- {#each Object.values(selectedUsers) as { user } (user.id)}
- {#key user.id}
-
-
-
-
-
-
-
-
- {user.name}
-
-
- {user.email}
-
-
-
-
({ title, icon })}
- onSelect={({ value }) => handleChangeRole(user, value)}
- />
-
- {/key}
- {/each}
-
-
- {/if}
-
- {#if users.length + Object.keys(selectedUsers).length === 0}
-
- {$t('album_share_no_users')}
-
- {/if}
-
-
-
- {#if users.length > 0}
-
-
-
- {/if}
-
-
-
-
- {#if sharedLinks.length > 0}
-
- {$t('shared_links')}
- onClose()} class="text-sm">{$t('view_all')}
-
-
-
- {#each sharedLinks as sharedLink (sharedLink.id)}
-
- {/each}
-
- {/if}
-
-
-
-
-
diff --git a/web/src/lib/modals/AlbumUsersModal.svelte b/web/src/lib/modals/AlbumUsersModal.svelte
deleted file mode 100644
index d1b009cb91..0000000000
--- a/web/src/lib/modals/AlbumUsersModal.svelte
+++ /dev/null
@@ -1,148 +0,0 @@
-
-
-
-
-
-
-
diff --git a/web/src/lib/services/album.service.ts b/web/src/lib/services/album.service.ts
index e1ad368cf7..670c7f8fd2 100644
--- a/web/src/lib/services/album.service.ts
+++ b/web/src/lib/services/album.service.ts
@@ -3,7 +3,9 @@ import ToastAction from '$lib/components/ToastAction.svelte';
import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
-import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
+import AlbumAddUsersModal from '$lib/modals/AlbumAddUsersModal.svelte';
+import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte';
+import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { user } from '$lib/stores/user.store';
import { downloadArchive } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
@@ -12,14 +14,17 @@ import { getFormatter } from '$lib/utils/i18n';
import {
addAssetsToAlbum,
addUsersToAlbum,
+ AlbumUserRole,
deleteAlbum,
+ removeUserFromAlbum,
updateAlbumInfo,
+ updateAlbumUser,
type AlbumResponseDto,
- type AlbumUserAddDto,
type UpdateAlbumDto,
+ type UserResponseDto,
} from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
-import { mdiPlusBoxOutline, mdiShareVariantOutline, mdiUpload } from '@mdi/js';
+import { mdiLink, mdiPlus, mdiPlusBoxOutline, mdiShareVariantOutline, mdiUpload } from '@mdi/js';
import { type MessageFormatter } from 'svelte-i18n';
import { get } from 'svelte/store';
@@ -31,10 +36,24 @@ export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) =
type: $t('command'),
icon: mdiShareVariantOutline,
$if: () => isOwned,
- onAction: () => modalManager.show(AlbumShareModal, { album }),
+ onAction: () => modalManager.show(AlbumOptionsModal, { album }),
};
- return { Share };
+ const AddUsers: ActionItem = {
+ title: $t('invite_people'),
+ type: $t('command'),
+ icon: mdiPlus,
+ onAction: () => modalManager.show(AlbumAddUsersModal, { album }),
+ };
+
+ const CreateSharedLink: ActionItem = {
+ title: $t('create_link'),
+ type: $t('command'),
+ icon: mdiLink,
+ onAction: () => modalManager.show(SharedLinkCreateModal, { albumId: album.id }),
+ };
+
+ return { Share, AddUsers, CreateSharedLink };
};
export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponseDto, assets: TimelineAsset[]) => {
@@ -72,18 +91,56 @@ const addAssets = async (album: AlbumResponseDto, assets: TimelineAsset[]) => {
}
};
-export const handleAddUsersToAlbum = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => {
+export const handleUpdateUserAlbumRole = async ({
+ albumId,
+ userId,
+ role,
+}: {
+ albumId: string;
+ userId: string;
+ role: AlbumUserRole;
+}) => {
const $t = await getFormatter();
try {
- await addUsersToAlbum({ id: album.id, addUsersDto: { albumUsers } });
+ 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'));
}
+};
- return false;
+export const handleRemoveUserFromAlbum = async (album: AlbumResponseDto, albumUser: UserResponseDto) => {
+ const $t = await getFormatter();
+
+ const confirmed = await modalManager.showDialog({
+ title: $t('album_remove_user'),
+ prompt: $t('album_remove_user_confirmation', { values: { user: albumUser.name } }),
+ confirmText: $t('remove_user'),
+ });
+
+ if (!confirmed) {
+ 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'));
+ }
};
export const handleUpdateAlbum = async ({ id }: { id: string }, dto: UpdateAlbumDto) => {
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 afd0ace65b..04becc8d85 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
@@ -36,8 +36,6 @@
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte';
- import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
- import AlbumUsersModal from '$lib/modals/AlbumUsersModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import {
getAlbumActions,
@@ -60,14 +58,7 @@
navigate,
type AssetGridRouteSearchParams,
} from '$lib/utils/navigation';
- import {
- AlbumUserRole,
- AssetOrder,
- AssetVisibility,
- getAlbumInfo,
- updateAlbumInfo,
- type AlbumResponseDto,
- } from '@immich/sdk';
+ import { AlbumUserRole, AssetVisibility, getAlbumInfo, updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk';
import { CommandPaletteDefaultProvider, Icon, IconButton, modalManager, toastManager } from '@immich/ui';
import {
mdiAccountEye,
@@ -101,7 +92,6 @@
let backUrl: string = $state(AppRoute.ALBUMS);
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
- let albumOrder: AssetOrder | undefined = $state(data.album.order);
let timelineManager = $state
() as TimelineManager;
let showAlbumUsers = $derived(timelineManager?.showAssetOwners ?? false);
@@ -266,7 +256,7 @@
timelineAlbumId: albumId,
};
}
- return { albumId, order: albumOrder };
+ return { albumId, order: album.order };
});
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
@@ -319,37 +309,6 @@
}
};
- const handleEditUsers = async () => {
- const changed = await modalManager.show(AlbumUsersModal, { album });
-
- if (changed) {
- await refreshAlbum();
- }
- };
-
- const handleOptions = async () => {
- const result = await modalManager.show(AlbumOptionsModal, { album, order: albumOrder, user: $user });
-
- if (!result) {
- return;
- }
-
- switch (result.action) {
- case 'changeOrder': {
- albumOrder = result.order;
- break;
- }
- case 'shareUser': {
- await modalManager.show(AlbumShareModal, { album });
- break;
- }
- case 'refreshAlbum': {
- await refreshAlbum();
- break;
- }
- }
- };
-
const onAlbumAddAssets = async () => {
await refreshAlbum();
timelineInteraction.clearMultiselect();
@@ -361,12 +320,31 @@
await setModeToView();
};
+ const onAlbumUserUpdate = ({ albumId, userId, role }: { albumId: string; userId: string; role: AlbumUserRole }) => {
+ if (albumId !== album.id) {
+ return;
+ }
+
+ album.albumUsers = album.albumUsers.map((albumUser) =>
+ albumUser.user.id === userId ? { ...albumUser, role } : albumUser,
+ );
+ };
+
const { Cast } = $derived(getGlobalActions($t));
const { Share } = $derived(getAlbumActions($t, album));
const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineInteraction.selectedAssets));
-
+ (album = newAlbum)}
+/>
@@ -417,13 +395,13 @@
{/if}
-