You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-09 23:17:29 +02:00
refactor: album options modal (#19177)
This commit is contained in:
@@ -1,202 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
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 SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AssetOrder,
|
||||
removeUserFromAlbum,
|
||||
updateAlbumInfo,
|
||||
updateAlbumUser,
|
||||
type AlbumResponseDto,
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Modal, ModalBody } from '@immich/ui';
|
||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||
import { findKey } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { RenderedOption } from '../elements/dropdown.svelte';
|
||||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||
import SettingDropdown from '../shared-components/settings/setting-dropdown.svelte';
|
||||
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
order: AssetOrder | undefined;
|
||||
user: UserResponseDto;
|
||||
onChangeOrder: (order: AssetOrder) => void;
|
||||
onClose: () => void;
|
||||
onToggleEnabledActivity: () => void;
|
||||
onShowSelectSharedUser: () => void;
|
||||
onRemove: (userId: string) => void;
|
||||
onRefreshAlbum: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
album,
|
||||
order,
|
||||
user,
|
||||
onChangeOrder,
|
||||
onClose,
|
||||
onToggleEnabledActivity,
|
||||
onShowSelectSharedUser,
|
||||
onRemove,
|
||||
onRefreshAlbum,
|
||||
}: Props = $props();
|
||||
|
||||
let selectedRemoveUser: UserResponseDto | null = $state(null);
|
||||
|
||||
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]);
|
||||
|
||||
const handleToggle = async (returnedOption: RenderedOption): Promise<void> => {
|
||||
if (selectedOption === returnedOption) {
|
||||
return;
|
||||
}
|
||||
let order: AssetOrder = AssetOrder.Desc;
|
||||
order = findKey(options, (option) => option === returnedOption) as AssetOrder;
|
||||
|
||||
try {
|
||||
await updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
order,
|
||||
},
|
||||
});
|
||||
onChangeOrder(order);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_album'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuRemove = (user: UserResponseDto): void => {
|
||||
selectedRemoveUser = user;
|
||||
};
|
||||
|
||||
const handleRemoveUser = async (): Promise<void> => {
|
||||
if (!selectedRemoveUser) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await removeUserFromAlbum({ id: album.id, userId: selectedRemoveUser.id });
|
||||
onRemove(selectedRemoveUser.id);
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: $t('album_user_removed', { values: { user: selectedRemoveUser.name } }),
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_remove_album_users'));
|
||||
} finally {
|
||||
selectedRemoveUser = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateSharedUserRole = 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') },
|
||||
});
|
||||
onRefreshAlbum();
|
||||
notificationController.show({ type: NotificationType.Info, message });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_change_album_user_role'));
|
||||
} finally {
|
||||
selectedRemoveUser = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if !selectedRemoveUser}
|
||||
<Modal title={$t('options')} {onClose} size="small">
|
||||
<ModalBody>
|
||||
<div class="items-center justify-center">
|
||||
<div class="py-2">
|
||||
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
|
||||
<div class="grid p-2 gap-y-2">
|
||||
{#if order}
|
||||
<SettingDropdown
|
||||
title={$t('display_order')}
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[order]}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
{/if}
|
||||
<SettingSwitch
|
||||
title={$t('comments_and_likes')}
|
||||
subtitle={$t('let_others_respond')}
|
||||
checked={album.isActivityEnabled}
|
||||
onToggle={onToggleEnabledActivity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
|
||||
<div class="p-2">
|
||||
<button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}>
|
||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||
<div><Icon path={mdiPlus} size="25" /></div>
|
||||
</div>
|
||||
<div>{$t('invite_people')}</div>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-2 py-2 mt-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{user.name}</div>
|
||||
<div>{$t('owner')}</div>
|
||||
</div>
|
||||
|
||||
{#each album.albumUsers as { user, role } (user.id)}
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{user.name}</div>
|
||||
{#if role === AlbumUserRole.Viewer}
|
||||
{$t('role_viewer')}
|
||||
{:else}
|
||||
{$t('role_editor')}
|
||||
{/if}
|
||||
{#if user.id !== album.ownerId}
|
||||
<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={() => handleMenuRemove(user)} text={$t('remove')} />
|
||||
</ButtonContextMenu>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
{#if selectedRemoveUser}
|
||||
<ConfirmModal
|
||||
title={$t('album_remove_user')}
|
||||
prompt={$t('album_remove_user_confirmation', { values: { user: selectedRemoveUser.name } })}
|
||||
confirmText={$t('remove_user')}
|
||||
onClose={(confirmed) => (confirmed ? handleRemoveUser() : (selectedRemoveUser = null))}
|
||||
/>
|
||||
{/if}
|
193
web/src/lib/modals/AlbumOptionsModal.svelte
Normal file
193
web/src/lib/modals/AlbumOptionsModal.svelte
Normal file
@@ -0,0 +1,193 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
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 SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AssetOrder,
|
||||
removeUserFromAlbum,
|
||||
updateAlbumInfo,
|
||||
updateAlbumUser,
|
||||
type AlbumResponseDto,
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Modal, ModalBody } from '@immich/ui';
|
||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||
import { findKey } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { RenderedOption } from '../components/elements/dropdown.svelte';
|
||||
import { notificationController, NotificationType } from '../components/shared-components/notification/notification';
|
||||
import SettingDropdown from '../components/shared-components/settings/setting-dropdown.svelte';
|
||||
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
order: AssetOrder | undefined;
|
||||
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]);
|
||||
|
||||
const handleToggleOrder = async (returnedOption: RenderedOption): Promise<void> => {
|
||||
if (selectedOption === returnedOption) {
|
||||
return;
|
||||
}
|
||||
let order: AssetOrder = AssetOrder.Desc;
|
||||
order = findKey(options, (option) => option === returnedOption) as AssetOrder;
|
||||
|
||||
try {
|
||||
await updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
order,
|
||||
},
|
||||
});
|
||||
onClose({ action: 'changeOrder', order });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_album'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleActivity = async () => {
|
||||
try {
|
||||
album = await updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
isActivityEnabled: !album.isActivityEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: $t('activity_changed', { values: { enabled: album.isActivityEnabled } }),
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.cant_change_activity', { values: { enabled: album.isActivityEnabled } }));
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeUserFromAlbum({ id: album.id, userId: user.id });
|
||||
onClose({ action: 'refreshAlbum' });
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: $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) => {
|
||||
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') },
|
||||
});
|
||||
onClose({ action: 'refreshAlbum' });
|
||||
notificationController.show({ type: NotificationType.Info, message });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_change_album_user_role'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal title={$t('options')} onClose={() => onClose({ action: 'refreshAlbum' })} size="small">
|
||||
<ModalBody>
|
||||
<div class="items-center justify-center">
|
||||
<div class="py-2">
|
||||
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
|
||||
<div class="grid p-2 gap-y-2">
|
||||
{#if order}
|
||||
<SettingDropdown
|
||||
title={$t('display_order')}
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[order]}
|
||||
onToggle={handleToggleOrder}
|
||||
/>
|
||||
{/if}
|
||||
<SettingSwitch
|
||||
title={$t('comments_and_likes')}
|
||||
subtitle={$t('let_others_respond')}
|
||||
checked={album.isActivityEnabled}
|
||||
onToggle={handleToggleActivity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
|
||||
<div class="p-2">
|
||||
<button type="button" class="flex items-center gap-2" onclick={() => onClose({ action: 'shareUser' })}>
|
||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||
<div><Icon path={mdiPlus} size="25" /></div>
|
||||
</div>
|
||||
<div>{$t('invite_people')}</div>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-2 py-2 mt-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{user.name}</div>
|
||||
<div>{$t('owner')}</div>
|
||||
</div>
|
||||
|
||||
{#each album.albumUsers as { user, role } (user.id)}
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{user.name}</div>
|
||||
{#if role === AlbumUserRole.Viewer}
|
||||
{$t('role_viewer')}
|
||||
{:else}
|
||||
{$t('role_editor')}
|
||||
{/if}
|
||||
{#if user.id !== album.ownerId}
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
@@ -4,7 +4,6 @@
|
||||
import CastButton from '$lib/cast/cast-button.svelte';
|
||||
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
|
||||
import AlbumMap from '$lib/components/album-page/album-map.svelte';
|
||||
import AlbumOptions from '$lib/components/album-page/album-options.svelte';
|
||||
import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
|
||||
import AlbumTitle from '$lib/components/album-page/album-title.svelte';
|
||||
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
|
||||
@@ -38,6 +37,7 @@
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
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 QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
||||
@@ -130,27 +130,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
const handleToggleEnableActivity = async () => {
|
||||
try {
|
||||
const updateAlbum = await updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
isActivityEnabled: !album.isActivityEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
album = { ...album, isActivityEnabled: updateAlbum.isActivityEnabled };
|
||||
|
||||
await refreshAlbum();
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: $t('activity_changed', { values: { enabled: album.isActivityEnabled } }),
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.cant_change_activity', { values: { enabled: album.isActivityEnabled } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
try {
|
||||
await activityManager.toggleLike();
|
||||
@@ -262,22 +241,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveUser = async (userId: string, nextViewMode: AlbumPageViewMode) => {
|
||||
if (userId == 'me' || userId === $user.id) {
|
||||
await goto(backUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshAlbum();
|
||||
|
||||
// Dynamically set the view mode based on the passed argument
|
||||
viewMode = album.albumUsers.length > 0 ? nextViewMode : AlbumPageViewMode.VIEW;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_deleting_shared_user'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadAlbum = async () => {
|
||||
await downloadAlbum(album);
|
||||
};
|
||||
@@ -453,6 +416,29 @@
|
||||
album = await getAlbumInfo({ id: album.id, withoutAssets: true });
|
||||
}
|
||||
};
|
||||
|
||||
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 handleShare();
|
||||
break;
|
||||
}
|
||||
case 'refreshAlbum': {
|
||||
await refreshAlbum();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
|
||||
@@ -697,11 +683,7 @@
|
||||
text={$t('select_album_cover')}
|
||||
onClick={() => (viewMode = AlbumPageViewMode.SELECT_THUMBNAIL)}
|
||||
/>
|
||||
<MenuOption
|
||||
icon={mdiCogOutline}
|
||||
text={$t('options')}
|
||||
onClick={() => (viewMode = AlbumPageViewMode.OPTIONS)}
|
||||
/>
|
||||
<MenuOption icon={mdiCogOutline} text={$t('options')} onClick={handleOptions} />
|
||||
{/if}
|
||||
|
||||
<MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} onClick={() => handleRemoveAlbum()} />
|
||||
@@ -773,23 +755,6 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if viewMode === AlbumPageViewMode.OPTIONS && $user}
|
||||
<AlbumOptions
|
||||
{album}
|
||||
order={albumOrder}
|
||||
user={$user}
|
||||
onChangeOrder={async (order) => {
|
||||
albumOrder = order;
|
||||
await setModeToView();
|
||||
}}
|
||||
onRemove={(userId) => handleRemoveUser(userId, AlbumPageViewMode.OPTIONS)}
|
||||
onRefreshAlbum={refreshAlbum}
|
||||
onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
|
||||
onToggleEnabledActivity={handleToggleEnableActivity}
|
||||
onShowSelectSharedUser={handleShare}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
::placeholder {
|
||||
color: rgb(60, 60, 60);
|
||||
|
Reference in New Issue
Block a user