mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-25 14:47:39 +00:00
refactor: album share and options modals (#25212)
* refactor: album share modals * chore: clean up --------- Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
parent
2190921c85
commit
56dfdfd033
9 changed files with 280 additions and 542 deletions
|
|
@ -32,7 +32,7 @@
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' • ');
|
.join(' • ');
|
||||||
|
|
||||||
const { ViewQrCode, Copy } = $derived(getSharedLinkActions($t, sharedLink));
|
const { ViewQrCode, Copy, Delete } = $derived(getSharedLinkActions($t, sharedLink));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
|
|
@ -43,5 +43,6 @@
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ActionButton action={ViewQrCode} />
|
<ActionButton action={ViewQrCode} />
|
||||||
<ActionButton action={Copy} />
|
<ActionButton action={Copy} />
|
||||||
|
<ActionButton action={Delete} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte';
|
import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte';
|
||||||
import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte';
|
import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte';
|
||||||
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
|
import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte';
|
||||||
import { handleDeleteAlbum, handleDownloadAlbum } from '$lib/services/album.service';
|
import { handleDeleteAlbum, handleDownloadAlbum } from '$lib/services/album.service';
|
||||||
import {
|
import {
|
||||||
AlbumFilter,
|
AlbumFilter,
|
||||||
|
|
@ -202,7 +202,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'share': {
|
case 'share': {
|
||||||
await modalManager.show(AlbumShareModal, { album: selectedAlbum });
|
await modalManager.show(AlbumOptionsModal, { album: selectedAlbum });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { ReleaseEvent } from '$lib/types';
|
||||||
import type { TreeNode } from '$lib/utils/tree-utils';
|
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||||
import type {
|
import type {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
|
AlbumUserRole,
|
||||||
ApiKeyResponseDto,
|
ApiKeyResponseDto,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
|
|
@ -39,6 +40,8 @@ export type Events = {
|
||||||
AlbumUpdate: [AlbumResponseDto];
|
AlbumUpdate: [AlbumResponseDto];
|
||||||
AlbumDelete: [AlbumResponseDto];
|
AlbumDelete: [AlbumResponseDto];
|
||||||
AlbumShare: [];
|
AlbumShare: [];
|
||||||
|
AlbumUserUpdate: [{ albumId: string; userId: string; role: AlbumUserRole }];
|
||||||
|
AlbumUserDelete: [{ albumId: string; userId: string }];
|
||||||
|
|
||||||
PersonUpdate: [PersonResponseDto];
|
PersonUpdate: [PersonResponseDto];
|
||||||
|
|
||||||
|
|
|
||||||
56
web/src/lib/modals/AlbumAddUsersModal.svelte
Normal file
56
web/src/lib/modals/AlbumAddUsersModal.svelte
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||||
|
import { handleAddUsersToAlbum } from '$lib/services/album.service';
|
||||||
|
import { searchUsers, type AlbumResponseDto, type UserResponseDto } from '@immich/sdk';
|
||||||
|
import { FormModal, ListButton, Stack, Text } from '@immich/ui';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
album: AlbumResponseDto;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { album, onClose }: Props = $props();
|
||||||
|
|
||||||
|
let users: UserResponseDto[] = $state([]);
|
||||||
|
const excludedUserIds = $derived([album.ownerId, ...album.albumUsers.map(({ user: { id } }) => id)]);
|
||||||
|
const filteredUsers = $derived(users.filter(({ id }) => !excludedUserIds.includes(id)));
|
||||||
|
const selectedUsers = new SvelteMap<string, UserResponseDto>();
|
||||||
|
|
||||||
|
const handleToggle = (user: UserResponseDto) => {
|
||||||
|
if (selectedUsers.has(user.id)) {
|
||||||
|
selectedUsers.delete(user.id);
|
||||||
|
} else {
|
||||||
|
selectedUsers.set(user.id, user);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
const success = await handleAddUsersToAlbum(album, [...selectedUsers.values()]);
|
||||||
|
if (success) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
users = await searchUsers();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormModal title={$t('users')} submitText={$t('add')} {onSubmit} disabled={selectedUsers.size === 0} {onClose}>
|
||||||
|
<Stack>
|
||||||
|
{#each filteredUsers as user (user.id)}
|
||||||
|
<ListButton selected={selectedUsers.has(user.id)} onclick={() => handleToggle(user)}>
|
||||||
|
<UserAvatar {user} size="md" />
|
||||||
|
<div class="text-start grow">
|
||||||
|
<Text>{user.name}</Text>
|
||||||
|
<Text size="small">{user.email}</Text>
|
||||||
|
</div>
|
||||||
|
</ListButton>
|
||||||
|
{:else}
|
||||||
|
<Text>{$t('album_share_no_users')}</Text>
|
||||||
|
{/each}
|
||||||
|
</Stack>
|
||||||
|
</FormModal>
|
||||||
|
|
@ -1,184 +1,163 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import AlbumSharedLink from '$lib/components/album-page/album-shared-link.svelte';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||||
import type { RenderedOption } from '$lib/elements/Dropdown.svelte';
|
import {
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
getAlbumActions,
|
||||||
|
handleRemoveUserFromAlbum,
|
||||||
|
handleUpdateAlbum,
|
||||||
|
handleUpdateUserAlbumRole,
|
||||||
|
} from '$lib/services/album.service';
|
||||||
|
import { user } from '$lib/stores/user.store';
|
||||||
import {
|
import {
|
||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
AssetOrder,
|
AssetOrder,
|
||||||
removeUserFromAlbum,
|
getAlbumInfo,
|
||||||
updateAlbumInfo,
|
getAllSharedLinks,
|
||||||
updateAlbumUser,
|
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
|
type SharedLinkResponseDto,
|
||||||
type UserResponseDto,
|
type UserResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { Icon, Modal, ModalBody, modalManager, toastManager } from '@immich/ui';
|
import { Field, Heading, HStack, Modal, ModalBody, Select, Stack, Switch, Text } from '@immich/ui';
|
||||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js';
|
import { onMount } from 'svelte';
|
||||||
import { findKey } from 'lodash-es';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import SettingDropdown from '../components/shared-components/settings/setting-dropdown.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
type Props = {
|
||||||
album: AlbumResponseDto;
|
album: AlbumResponseDto;
|
||||||
order: AssetOrder | undefined;
|
onClose: () => void;
|
||||||
user: UserResponseDto;
|
|
||||||
onClose: (
|
|
||||||
result?: { action: 'changeOrder'; order: AssetOrder } | { action: 'shareUser' } | { action: 'refreshAlbum' },
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { album, order, user, onClose }: Props = $props();
|
|
||||||
|
|
||||||
const options: Record<AssetOrder, RenderedOption> = {
|
|
||||||
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') },
|
|
||||||
[AssetOrder.Desc]: { icon: mdiArrowDownThin, title: $t('newest_first') },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let selectedOption = $derived(order ? options[order] : options[AssetOrder.Desc]);
|
let { album, onClose }: Props = $props();
|
||||||
|
|
||||||
const handleToggleOrder = async (returnedOption: RenderedOption): Promise<void> => {
|
const orderOptions = [
|
||||||
if (selectedOption === returnedOption) {
|
{ label: $t('newest_first'), value: AssetOrder.Desc },
|
||||||
return;
|
{ label: $t('oldest_first'), value: AssetOrder.Asc },
|
||||||
}
|
];
|
||||||
let order: AssetOrder = AssetOrder.Desc;
|
|
||||||
order = findKey(options, (option) => option === returnedOption) as AssetOrder;
|
|
||||||
|
|
||||||
try {
|
const roleOptions: Array<{ label: string; value: AlbumUserRole | 'none'; icon?: string }> = [
|
||||||
await updateAlbumInfo({
|
{ label: $t('role_editor'), value: AlbumUserRole.Editor },
|
||||||
id: album.id,
|
{ label: $t('role_viewer'), value: AlbumUserRole.Viewer },
|
||||||
updateAlbumDto: {
|
{ label: $t('remove_user'), value: 'none' },
|
||||||
order,
|
];
|
||||||
},
|
|
||||||
});
|
|
||||||
onClose({ action: 'changeOrder', order });
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_save_album'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleActivity = async () => {
|
const selectedOrderOption = $derived(
|
||||||
try {
|
album.order ? orderOptions.find(({ value }) => value === album.order) : orderOptions[0],
|
||||||
album = await updateAlbumInfo({
|
);
|
||||||
id: album.id,
|
|
||||||
updateAlbumDto: {
|
|
||||||
isActivityEnabled: !album.isActivityEnabled,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
toastManager.success($t('activity_changed', { values: { enabled: album.isActivityEnabled } }));
|
const handleRoleSelect = async (user: UserResponseDto, role: AlbumUserRole | 'none') => {
|
||||||
} catch (error) {
|
if (role === 'none') {
|
||||||
handleError(error, $t('errors.cant_change_activity', { values: { enabled: album.isActivityEnabled } }));
|
await handleRemoveUserFromAlbum(album, user);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveUser = async (user: UserResponseDto): Promise<void> => {
|
|
||||||
const confirmed = await modalManager.showDialog({
|
|
||||||
title: $t('album_remove_user'),
|
|
||||||
prompt: $t('album_remove_user_confirmation', { values: { user: user.name } }),
|
|
||||||
confirmText: $t('remove_user'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await handleUpdateUserAlbumRole({ albumId: album.id, userId: user.id, role });
|
||||||
await removeUserFromAlbum({ id: album.id, userId: user.id });
|
|
||||||
onClose({ action: 'refreshAlbum' });
|
|
||||||
toastManager.success($t('album_user_removed', { values: { user: user.name } }));
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_remove_album_users'));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateSharedUserRole = async (user: UserResponseDto, role: AlbumUserRole) => {
|
const refreshAlbum = async () => {
|
||||||
try {
|
album = await getAlbumInfo({ id: album.id, withoutAssets: true });
|
||||||
await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { role } });
|
|
||||||
const message = $t('user_role_set', {
|
|
||||||
values: { user: user.name, role: role == AlbumUserRole.Viewer ? $t('role_viewer') : $t('role_editor') },
|
|
||||||
});
|
|
||||||
onClose({ action: 'refreshAlbum' });
|
|
||||||
toastManager.success(message);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_change_album_user_role'));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onAlbumUserDelete = async ({ userId }: { userId: string }) => {
|
||||||
|
album.albumUsers = album.albumUsers.filter(({ user: { id } }) => id !== userId);
|
||||||
|
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 });
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title={$t('options')} onClose={() => onClose({ action: 'refreshAlbum' })} size="small">
|
<OnEvents
|
||||||
|
{onAlbumUserDelete}
|
||||||
|
onAlbumShare={refreshAlbum}
|
||||||
|
{onSharedLinkCreate}
|
||||||
|
{onSharedLinkDelete}
|
||||||
|
onAlbumUpdate={(newAlbum) => (album = newAlbum)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal title={$t('options')} {onClose} size="small">
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div class="items-center justify-center">
|
<Stack gap={6}>
|
||||||
<div class="py-2">
|
<div>
|
||||||
<h2 class="uppercase text-gray text-sm mb-2">{$t('settings')}</h2>
|
<Heading size="tiny" class="mb-2">{$t('settings')}</Heading>
|
||||||
<div class="grid p-2 gap-y-2">
|
<div class="grid gap-y-2 ps-2">
|
||||||
{#if order}
|
{#if album.order}
|
||||||
<SettingDropdown
|
<Field label={$t('display_order')}>
|
||||||
title={$t('display_order')}
|
<Select
|
||||||
options={Object.values(options)}
|
data={orderOptions}
|
||||||
selectedOption={options[order]}
|
value={selectedOrderOption}
|
||||||
onToggle={handleToggleOrder}
|
onChange={({ value }) => handleUpdateAlbum(album, { order: value })}
|
||||||
/>
|
/>
|
||||||
|
</Field>
|
||||||
{/if}
|
{/if}
|
||||||
<SettingSwitch
|
<Field label={$t('comments_and_likes')} description={$t('let_others_respond')}>
|
||||||
title={$t('comments_and_likes')}
|
<Switch
|
||||||
subtitle={$t('let_others_respond')}
|
checked={album.isActivityEnabled}
|
||||||
checked={album.isActivityEnabled}
|
onCheckedChange={(checked) => handleUpdateAlbum(album, { isActivityEnabled: checked })}
|
||||||
onToggle={handleToggleActivity}
|
/>
|
||||||
/>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-2">
|
<div>
|
||||||
<div class="uppercase text-gray text-sm mb-3">{$t('people')}</div>
|
<HStack fullWidth class="justify-between mb-2">
|
||||||
<div class="p-2">
|
<Heading size="tiny">{$t('people')}</Heading>
|
||||||
<button type="button" class="flex items-center gap-2" onclick={() => onClose({ action: 'shareUser' })}>
|
<HeaderActionButton action={AddUsers} />
|
||||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
</HStack>
|
||||||
<div><Icon icon={mdiPlus} size="25" /></div>
|
<div class="ps-2">
|
||||||
</div>
|
<div class="flex items-center gap-2">
|
||||||
<div>{$t('invite_people')}</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 py-2 mt-2">
|
|
||||||
<div>
|
<div>
|
||||||
<UserAvatar {user} size="md" />
|
<UserAvatar user={$user} size="md" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">{user.name}</div>
|
<div class="w-full">{$user.name}</div>
|
||||||
<div>{$t('owner')}</div>
|
<Field disabled class="w-32 shrink-0">
|
||||||
|
<Select data={[{ label: $t('owner'), value: 'owner' }]} value={{ label: $t('owner'), value: 'owner' }} />
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each album.albumUsers as { user, role } (user.id)}
|
{#each album.albumUsers as { user, role } (user.id)}
|
||||||
<div class="flex items-center gap-2 py-2">
|
<div class="flex items-center justify-between gap-4 py-2">
|
||||||
<div>
|
<div class="flex flex-row items-center gap-2">
|
||||||
<UserAvatar {user} size="md" />
|
<div>
|
||||||
|
<UserAvatar {user} size="md" />
|
||||||
|
</div>
|
||||||
|
<Text>{user.name}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">{user.name}</div>
|
<Field class="w-32">
|
||||||
{#if role === AlbumUserRole.Viewer}
|
<Select
|
||||||
{$t('role_viewer')}
|
data={roleOptions}
|
||||||
{:else}
|
value={roleOptions.find(({ value }) => value === role)}
|
||||||
{$t('role_editor')}
|
onChange={({ value }) => handleRoleSelect(user, value)}
|
||||||
{/if}
|
/>
|
||||||
{#if user.id !== album.ownerId}
|
</Field>
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} size="medium" title={$t('options')}>
|
|
||||||
{#if role === AlbumUserRole.Viewer}
|
|
||||||
<MenuOption
|
|
||||||
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Editor)}
|
|
||||||
text={$t('allow_edits')}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<MenuOption
|
|
||||||
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)}
|
|
||||||
text={$t('disallow_edits')}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<!-- Allow deletion for non-owners -->
|
|
||||||
<MenuOption onClick={() => handleRemoveUser(user)} text={$t('remove')} />
|
|
||||||
</ButtonContextMenu>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<HStack class="justify-between mb-2">
|
||||||
|
<Heading size="tiny">{$t('shared_links')}</Heading>
|
||||||
|
<HeaderActionButton action={CreateSharedLink} />
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Stack gap={4}>
|
||||||
|
{#each sharedLinks as sharedLink (sharedLink.id)}
|
||||||
|
<AlbumSharedLink {album} {sharedLink} />
|
||||||
|
{/each}
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -1,192 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import AlbumSharedLink from '$lib/components/album-page/album-shared-link.svelte';
|
|
||||||
import { AppRoute } from '$lib/constants';
|
|
||||||
import Dropdown from '$lib/elements/Dropdown.svelte';
|
|
||||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
|
||||||
import { handleAddUsersToAlbum } from '$lib/services/album.service';
|
|
||||||
import {
|
|
||||||
AlbumUserRole,
|
|
||||||
getAllSharedLinks,
|
|
||||||
searchUsers,
|
|
||||||
type AlbumResponseDto,
|
|
||||||
type SharedLinkResponseDto,
|
|
||||||
type UserResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { Button, Icon, Link, Modal, ModalBody, modalManager, Stack, Text } from '@immich/ui';
|
|
||||||
import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import UserAvatar from '../components/shared-components/user-avatar.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
album: AlbumResponseDto;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { album, onClose }: Props = $props();
|
|
||||||
|
|
||||||
let users: UserResponseDto[] = $state([]);
|
|
||||||
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = $state({});
|
|
||||||
|
|
||||||
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
|
|
||||||
{ title: $t('role_editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
|
|
||||||
{ title: $t('role_viewer'), value: AlbumUserRole.Viewer, icon: mdiEye },
|
|
||||||
{ title: $t('remove_user'), value: 'none' },
|
|
||||||
];
|
|
||||||
|
|
||||||
let sharedLinks: SharedLinkResponseDto[] = $state([]);
|
|
||||||
onMount(async () => {
|
|
||||||
sharedLinks = await getAllSharedLinks({ albumId: album.id });
|
|
||||||
const data = await searchUsers();
|
|
||||||
|
|
||||||
// remove album owner
|
|
||||||
users = data.filter((user) => user.id !== album.ownerId);
|
|
||||||
|
|
||||||
// Remove the existed shared users from the album
|
|
||||||
for (const sharedUser of album.albumUsers) {
|
|
||||||
users = users.filter((user) => user.id !== sharedUser.user.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleToggle = (user: UserResponseDto) => {
|
|
||||||
if (Object.keys(selectedUsers).includes(user.id)) {
|
|
||||||
delete selectedUsers[user.id];
|
|
||||||
} else {
|
|
||||||
selectedUsers[user.id] = { user, role: AlbumUserRole.Editor };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeRole = (user: UserResponseDto, role: AlbumUserRole | 'none') => {
|
|
||||||
if (role === 'none') {
|
|
||||||
delete selectedUsers[user.id];
|
|
||||||
} else {
|
|
||||||
selectedUsers[user.id].role = role;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onShareUser = async () => {
|
|
||||||
const success = await handleAddUsersToAlbum(
|
|
||||||
album,
|
|
||||||
Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
|
|
||||||
);
|
|
||||||
if (success) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onShareLink = () => {
|
|
||||||
void modalManager.show(SharedLinkCreateModal, { albumId: album.id });
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal size="small" title={$t('share')} {onClose}>
|
|
||||||
<ModalBody>
|
|
||||||
{#if Object.keys(selectedUsers).length > 0}
|
|
||||||
<div class="mb-2 py-2 sticky">
|
|
||||||
<p class="text-xs font-medium">{$t('selected')}</p>
|
|
||||||
<div class="my-2">
|
|
||||||
{#each Object.values(selectedUsers) as { user } (user.id)}
|
|
||||||
{#key user.id}
|
|
||||||
<div class="flex place-items-center gap-4 p-4">
|
|
||||||
<div
|
|
||||||
class="flex h-10 w-10 items-center justify-center rounded-full border bg-green-600 text-3xl text-white"
|
|
||||||
>
|
|
||||||
<Icon icon={mdiCheck} size="24" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <UserAvatar {user} size="md" /> -->
|
|
||||||
<div class="text-start grow">
|
|
||||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
|
||||||
{user.name}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs">
|
|
||||||
{user.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
title={$t('role')}
|
|
||||||
options={roleOptions}
|
|
||||||
render={({ title, icon }) => ({ title, icon })}
|
|
||||||
onSelect={({ value }) => handleChangeRole(user, value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if users.length + Object.keys(selectedUsers).length === 0}
|
|
||||||
<p class="p-5 text-sm">
|
|
||||||
{$t('album_share_no_users')}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="immich-scrollbar max-h-125 overflow-y-auto">
|
|
||||||
{#if users.length > 0 && users.length !== Object.keys(selectedUsers).length}
|
|
||||||
<Text>{$t('users')}</Text>
|
|
||||||
|
|
||||||
<div class="my-2">
|
|
||||||
{#each users as user (user.id)}
|
|
||||||
{#if !Object.keys(selectedUsers).includes(user.id)}
|
|
||||||
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={() => handleToggle(user)}
|
|
||||||
class="flex w-full place-items-center gap-4 p-4"
|
|
||||||
>
|
|
||||||
<UserAvatar {user} size="md" />
|
|
||||||
<div class="text-start grow">
|
|
||||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
|
||||||
{user.name}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs">
|
|
||||||
{user.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if users.length > 0}
|
|
||||||
<div class="py-3">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fullWidth
|
|
||||||
shape="round"
|
|
||||||
disabled={Object.keys(selectedUsers).length === 0}
|
|
||||||
onclick={onShareUser}
|
|
||||||
>
|
|
||||||
{$t('add')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<hr class="my-4" />
|
|
||||||
|
|
||||||
<Stack gap={6}>
|
|
||||||
{#if sharedLinks.length > 0}
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<Text>{$t('shared_links')}</Text>
|
|
||||||
<Link href={AppRoute.SHARED_LINKS} onclick={() => onClose()} class="text-sm">{$t('view_all')}</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Stack gap={4}>
|
|
||||||
{#each sharedLinks as sharedLink (sharedLink.id)}
|
|
||||||
<AlbumSharedLink {album} {sharedLink} />
|
|
||||||
{/each}
|
|
||||||
</Stack>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Button leadingIcon={mdiLink} size="small" shape="round" fullWidth onclick={onShareLink}>
|
|
||||||
{$t('create_link')}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</ModalBody>
|
|
||||||
</Modal>
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
|
||||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import {
|
|
||||||
AlbumUserRole,
|
|
||||||
getMyUser,
|
|
||||||
removeUserFromAlbum,
|
|
||||||
updateAlbumUser,
|
|
||||||
type AlbumResponseDto,
|
|
||||||
type UserResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { Button, Modal, ModalBody, Text, modalManager, toastManager } from '@immich/ui';
|
|
||||||
import { mdiDotsVertical } from '@mdi/js';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
album: AlbumResponseDto;
|
|
||||||
onClose: (changed?: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { album, onClose }: Props = $props();
|
|
||||||
|
|
||||||
let currentUser: UserResponseDto | undefined = $state();
|
|
||||||
|
|
||||||
let isOwned = $derived(currentUser?.id == album.ownerId);
|
|
||||||
|
|
||||||
// Build a map of contributor counts by user id; avoid casts/derived
|
|
||||||
const contributorCounts: Record<string, number> = {};
|
|
||||||
if (album.contributorCounts) {
|
|
||||||
for (const { userId, assetCount } of album.contributorCounts) {
|
|
||||||
contributorCounts[userId] = assetCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
currentUser = await getMyUser();
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_refresh_user'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleRemoveUser = async (user: UserResponseDto) => {
|
|
||||||
if (!user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = user.id === currentUser?.id ? 'me' : user.id;
|
|
||||||
let confirmed: boolean | undefined;
|
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/prefer-ternary
|
|
||||||
if (userId === 'me') {
|
|
||||||
confirmed = await modalManager.showDialog({
|
|
||||||
title: $t('album_leave'),
|
|
||||||
prompt: $t('album_leave_confirmation', { values: { album: album.albumName } }),
|
|
||||||
confirmText: $t('leave'),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
confirmed = await modalManager.showDialog({
|
|
||||||
title: $t('album_remove_user'),
|
|
||||||
prompt: $t('album_remove_user_confirmation', { values: { user: user.name } }),
|
|
||||||
confirmText: $t('remove_user'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await removeUserFromAlbum({ id: album.id, userId });
|
|
||||||
const message =
|
|
||||||
userId === 'me'
|
|
||||||
? $t('album_user_left', { values: { album: album.albumName } })
|
|
||||||
: $t('album_user_removed', { values: { user: user.name } });
|
|
||||||
toastManager.success(message);
|
|
||||||
onClose(true);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_remove_album_users'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeRole = async (user: UserResponseDto, role: AlbumUserRole) => {
|
|
||||||
try {
|
|
||||||
await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { role } });
|
|
||||||
const message = $t('user_role_set', {
|
|
||||||
values: { user: user.name, role: role == AlbumUserRole.Viewer ? $t('role_viewer') : $t('role_editor') },
|
|
||||||
});
|
|
||||||
toastManager.success(message);
|
|
||||||
onClose(true);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_change_album_user_role'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal title={$t('options')} size="small" {onClose}>
|
|
||||||
<ModalBody>
|
|
||||||
<section class="immich-scrollbar max-h-100 overflow-y-auto pb-4">
|
|
||||||
{#each [{ user: album.owner, role: 'owner' }, ...album.albumUsers] as { user, role } (user.id)}
|
|
||||||
<div class="flex w-full place-items-center justify-between gap-4 p-5 rounded-xl transition-colors">
|
|
||||||
<div class="flex place-items-center gap-4">
|
|
||||||
<UserAvatar {user} size="md" />
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<p class="font-medium">{user.name}</p>
|
|
||||||
<Text color="muted" size="tiny">
|
|
||||||
{#if role === 'owner'}
|
|
||||||
{$t('owner')}
|
|
||||||
{:else if role === AlbumUserRole.Viewer}
|
|
||||||
{$t('role_viewer')}
|
|
||||||
{:else}
|
|
||||||
{$t('role_editor')}
|
|
||||||
{/if}
|
|
||||||
{#if user.id in contributorCounts}
|
|
||||||
<span>-</span>
|
|
||||||
{$t('items_count', { values: { count: contributorCounts[user.id] } })}
|
|
||||||
{/if}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="icon-{user.id}" class="flex place-items-center">
|
|
||||||
{#if isOwned}
|
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} size="medium" title={$t('options')}>
|
|
||||||
{#if role === AlbumUserRole.Viewer}
|
|
||||||
<MenuOption onClick={() => handleChangeRole(user, AlbumUserRole.Editor)} text={$t('allow_edits')} />
|
|
||||||
{:else}
|
|
||||||
<MenuOption
|
|
||||||
onClick={() => handleChangeRole(user, AlbumUserRole.Viewer)}
|
|
||||||
text={$t('disallow_edits')}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<MenuOption onClick={() => handleRemoveUser(user)} text={$t('remove')} />
|
|
||||||
</ButtonContextMenu>
|
|
||||||
{:else if user.id == currentUser?.id}
|
|
||||||
<Button shape="round" variant="ghost" leadingIcon={undefined} onclick={() => handleRemoveUser(user)}
|
|
||||||
>{$t('leave')}</Button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
</ModalBody>
|
|
||||||
</Modal>
|
|
||||||
|
|
@ -3,7 +3,9 @@ import ToastAction from '$lib/components/ToastAction.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
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 { user } from '$lib/stores/user.store';
|
||||||
import { downloadArchive } from '$lib/utils/asset-utils';
|
import { downloadArchive } from '$lib/utils/asset-utils';
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
|
|
@ -12,14 +14,17 @@ import { getFormatter } from '$lib/utils/i18n';
|
||||||
import {
|
import {
|
||||||
addAssetsToAlbum,
|
addAssetsToAlbum,
|
||||||
addUsersToAlbum,
|
addUsersToAlbum,
|
||||||
|
AlbumUserRole,
|
||||||
deleteAlbum,
|
deleteAlbum,
|
||||||
|
removeUserFromAlbum,
|
||||||
updateAlbumInfo,
|
updateAlbumInfo,
|
||||||
|
updateAlbumUser,
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AlbumUserAddDto,
|
|
||||||
type UpdateAlbumDto,
|
type UpdateAlbumDto,
|
||||||
|
type UserResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
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 { type MessageFormatter } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
|
@ -31,10 +36,24 @@ export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) =
|
||||||
type: $t('command'),
|
type: $t('command'),
|
||||||
icon: mdiShareVariantOutline,
|
icon: mdiShareVariantOutline,
|
||||||
$if: () => isOwned,
|
$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[]) => {
|
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();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
try {
|
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');
|
eventManager.emit('AlbumShare');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.error_adding_users_to_album'));
|
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) => {
|
export const handleUpdateAlbum = async ({ id }: { id: string }, dto: UpdateAlbumDto) => {
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,6 @@
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte';
|
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 SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||||
import {
|
import {
|
||||||
getAlbumActions,
|
getAlbumActions,
|
||||||
|
|
@ -60,14 +58,7 @@
|
||||||
navigate,
|
navigate,
|
||||||
type AssetGridRouteSearchParams,
|
type AssetGridRouteSearchParams,
|
||||||
} from '$lib/utils/navigation';
|
} from '$lib/utils/navigation';
|
||||||
import {
|
import { AlbumUserRole, AssetVisibility, getAlbumInfo, updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk';
|
||||||
AlbumUserRole,
|
|
||||||
AssetOrder,
|
|
||||||
AssetVisibility,
|
|
||||||
getAlbumInfo,
|
|
||||||
updateAlbumInfo,
|
|
||||||
type AlbumResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { CommandPaletteDefaultProvider, Icon, IconButton, modalManager, toastManager } from '@immich/ui';
|
import { CommandPaletteDefaultProvider, Icon, IconButton, modalManager, toastManager } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiAccountEye,
|
mdiAccountEye,
|
||||||
|
|
@ -101,7 +92,6 @@
|
||||||
|
|
||||||
let backUrl: string = $state(AppRoute.ALBUMS);
|
let backUrl: string = $state(AppRoute.ALBUMS);
|
||||||
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
|
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
|
||||||
let albumOrder: AssetOrder | undefined = $state(data.album.order);
|
|
||||||
|
|
||||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||||
let showAlbumUsers = $derived(timelineManager?.showAssetOwners ?? false);
|
let showAlbumUsers = $derived(timelineManager?.showAssetOwners ?? false);
|
||||||
|
|
@ -266,7 +256,7 @@
|
||||||
timelineAlbumId: albumId,
|
timelineAlbumId: albumId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { albumId, order: albumOrder };
|
return { albumId, order: album.order };
|
||||||
});
|
});
|
||||||
|
|
||||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
|
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 () => {
|
const onAlbumAddAssets = async () => {
|
||||||
await refreshAlbum();
|
await refreshAlbum();
|
||||||
timelineInteraction.clearMultiselect();
|
timelineInteraction.clearMultiselect();
|
||||||
|
|
@ -361,12 +320,31 @@
|
||||||
await setModeToView();
|
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 { Cast } = $derived(getGlobalActions($t));
|
||||||
const { Share } = $derived(getAlbumActions($t, album));
|
const { Share } = $derived(getAlbumActions($t, album));
|
||||||
const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineInteraction.selectedAssets));
|
const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineInteraction.selectedAssets));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OnEvents {onSharedLinkCreate} {onAlbumDelete} {onAlbumAddAssets} {onAlbumShare} />
|
<OnEvents
|
||||||
|
{onSharedLinkCreate}
|
||||||
|
onSharedLinkDelete={refreshAlbum}
|
||||||
|
{onAlbumDelete}
|
||||||
|
{onAlbumAddAssets}
|
||||||
|
{onAlbumShare}
|
||||||
|
{onAlbumUserUpdate}
|
||||||
|
onAlbumUserDelete={refreshAlbum}
|
||||||
|
onAlbumUpdate={(newAlbum) => (album = newAlbum)}
|
||||||
|
/>
|
||||||
<CommandPaletteDefaultProvider name={$t('album')} actions={[AddAssets, Upload]} />
|
<CommandPaletteDefaultProvider name={$t('album')} actions={[AddAssets, Upload]} />
|
||||||
|
|
||||||
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
|
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
|
||||||
|
|
@ -417,13 +395,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- owner -->
|
<!-- owner -->
|
||||||
<button type="button" onclick={handleEditUsers}>
|
<button type="button" onclick={() => modalManager.show(AlbumOptionsModal, { album })}>
|
||||||
<UserAvatar user={album.owner} size="md" />
|
<UserAvatar user={album.owner} size="md" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- users with write access (collaborators) -->
|
<!-- users with write access (collaborators) -->
|
||||||
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
|
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
|
||||||
<button type="button" onclick={handleEditUsers}>
|
<button type="button" onclick={() => modalManager.show(AlbumOptionsModal, { album })}>
|
||||||
<UserAvatar {user} size="md" />
|
<UserAvatar {user} size="md" />
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -436,7 +414,7 @@
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="medium"
|
size="medium"
|
||||||
icon={mdiDotsVertical}
|
icon={mdiDotsVertical}
|
||||||
onclick={handleEditUsers}
|
onclick={() => modalManager.show(AlbumOptionsModal, { album })}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -601,7 +579,11 @@
|
||||||
text={$t('select_album_cover')}
|
text={$t('select_album_cover')}
|
||||||
onClick={() => (viewMode = AlbumPageViewMode.SELECT_THUMBNAIL)}
|
onClick={() => (viewMode = AlbumPageViewMode.SELECT_THUMBNAIL)}
|
||||||
/>
|
/>
|
||||||
<MenuOption icon={mdiCogOutline} text={$t('options')} onClick={handleOptions} />
|
<MenuOption
|
||||||
|
icon={mdiCogOutline}
|
||||||
|
text={$t('options')}
|
||||||
|
onClick={() => modalManager.show(AlbumOptionsModal, { album })}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isOwned}
|
{#if isOwned}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue