mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
refactor(web): tag service (#25142)
This commit is contained in:
parent
5d1e486478
commit
8136d7fd54
5 changed files with 157 additions and 138 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import type { ThemeSetting } from '$lib/managers/theme-manager.svelte';
|
import type { ThemeSetting } from '$lib/managers/theme-manager.svelte';
|
||||||
import type { ReleaseEvent } from '$lib/types';
|
import type { ReleaseEvent } from '$lib/types';
|
||||||
|
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||||
import type {
|
import type {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
ApiKeyResponseDto,
|
ApiKeyResponseDto,
|
||||||
|
|
@ -10,6 +11,7 @@ import type {
|
||||||
QueueResponseDto,
|
QueueResponseDto,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SystemConfigDto,
|
SystemConfigDto,
|
||||||
|
TagResponseDto,
|
||||||
UserAdminResponseDto,
|
UserAdminResponseDto,
|
||||||
WorkflowResponseDto,
|
WorkflowResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
|
|
@ -42,6 +44,10 @@ export type Events = {
|
||||||
SharedLinkUpdate: [SharedLinkResponseDto];
|
SharedLinkUpdate: [SharedLinkResponseDto];
|
||||||
SharedLinkDelete: [SharedLinkResponseDto];
|
SharedLinkDelete: [SharedLinkResponseDto];
|
||||||
|
|
||||||
|
TagCreate: [TagResponseDto];
|
||||||
|
TagUpdate: [TagResponseDto];
|
||||||
|
TagDelete: [TreeNode];
|
||||||
|
|
||||||
UserAdminCreate: [UserAdminResponseDto];
|
UserAdminCreate: [UserAdminResponseDto];
|
||||||
UserAdminUpdate: [UserAdminResponseDto];
|
UserAdminUpdate: [UserAdminResponseDto];
|
||||||
UserAdminRestore: [UserAdminResponseDto];
|
UserAdminRestore: [UserAdminResponseDto];
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
import { handleCreateTag } from '$lib/services/tag.service';
|
||||||
import { SettingInputFieldType } from '$lib/constants';
|
|
||||||
import type { TreeNode } from '$lib/utils/tree-utils';
|
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||||
import { upsertTags, type TagResponseDto } from '@immich/sdk';
|
import { Field, FormModal, Input, Text } from '@immich/ui';
|
||||||
import { Button, HStack, Modal, ModalBody, ModalFooter, toastManager } from '@immich/ui';
|
|
||||||
import { mdiTag } from '@mdi/js';
|
import { mdiTag } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: (tag?: TagResponseDto) => void;
|
onClose: () => void;
|
||||||
baseTag?: TreeNode;
|
baseTag?: TreeNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -16,44 +14,17 @@
|
||||||
|
|
||||||
let tagValue = $state(baseTag?.path ? `${baseTag.path}/` : '');
|
let tagValue = $state(baseTag?.path ? `${baseTag.path}/` : '');
|
||||||
|
|
||||||
const createTag = async () => {
|
const onSubmit = async () => {
|
||||||
const [tag] = await upsertTags({ tagUpsertDto: { tags: [tagValue] } });
|
const success = await handleCreateTag(tagValue);
|
||||||
|
if (success) {
|
||||||
if (!tag) {
|
onClose();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toastManager.success($t('tag_created', { values: { tag: tag.value } }));
|
|
||||||
|
|
||||||
onClose(tag);
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal size="small" title={$t('create_tag')} icon={mdiTag} {onClose}>
|
<FormModal size="small" title={$t('create_tag')} submitText={$t('create')} icon={mdiTag} {onClose} {onSubmit}>
|
||||||
<ModalBody>
|
<Text size="small">{$t('create_tag_description')}</Text>
|
||||||
<div class="text-primary">
|
<Field label={$t('tag')} required>
|
||||||
<p class="text-sm dark:text-immich-dark-fg">
|
<Input autofocus bind:value={tagValue} />
|
||||||
{$t('create_tag_description')}
|
</Field>
|
||||||
</p>
|
</FormModal>
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onsubmit={createTag} autocomplete="off" id="create-tag-form">
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label={$t('tag')}
|
|
||||||
bind:value={tagValue}
|
|
||||||
required={true}
|
|
||||||
autofocus={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<HStack fullWidth>
|
|
||||||
<Button color="secondary" fullWidth shape="round" onclick={() => onClose()}>{$t('cancel')}</Button>
|
|
||||||
<Button type="submit" fullWidth shape="round" form="create-tag-form">{$t('create')}</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,29 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
import { SettingInputFieldType } from '$lib/constants';
|
import { SettingInputFieldType } from '$lib/constants';
|
||||||
|
import { handleUpdateTag } from '$lib/services/tag.service';
|
||||||
import type { TreeNode } from '$lib/utils/tree-utils';
|
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||||
import { updateTag, type TagResponseDto } from '@immich/sdk';
|
import { FormModal } from '@immich/ui';
|
||||||
import { Button, HStack, Modal, ModalBody, ModalFooter, toastManager } from '@immich/ui';
|
|
||||||
import { mdiTag } from '@mdi/js';
|
import { mdiTag } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tag: TreeNode;
|
tag: TreeNode;
|
||||||
onClose: (updatedTag?: TagResponseDto) => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { tag, onClose }: Props = $props();
|
const { tag, onClose }: Props = $props();
|
||||||
|
|
||||||
let tagColor = $state(tag.color ?? '');
|
let tagColor = $state(tag.color ?? '');
|
||||||
|
|
||||||
const handleEdit = async () => {
|
const onSubmit = async () => {
|
||||||
if (!tag.id) {
|
const success = await handleUpdateTag(tag, { color: tagColor });
|
||||||
return;
|
if (success) {
|
||||||
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedTag = await updateTag({ id: tag.id, tagUpdateDto: { color: tagColor } });
|
|
||||||
|
|
||||||
toastManager.success($t('tag_updated', { values: { tag: tag.value } }));
|
|
||||||
|
|
||||||
onClose(updatedTag);
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title={$t('edit_tag')} icon={mdiTag} {onClose}>
|
<FormModal title={$t('edit_tag')} size="small" icon={mdiTag} {onClose} {onSubmit}>
|
||||||
<ModalBody>
|
<SettingInputField inputType={SettingInputFieldType.COLOR} label={$t('color')} bind:value={tagColor} />
|
||||||
<form onsubmit={handleEdit} autocomplete="off" id="edit-tag-form">
|
</FormModal>
|
||||||
<div class="my-4 flex flex-col gap-2">
|
|
||||||
<SettingInputField inputType={SettingInputFieldType.COLOR} label={$t('color')} bind:value={tagColor} />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<HStack fullWidth>
|
|
||||||
<Button color="secondary" fullWidth shape="round" onclick={() => onClose()}>{$t('cancel')}</Button>
|
|
||||||
<Button type="submit" fullWidth shape="round" form="edit-tag-form">{$t('save')}</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
|
|
|
||||||
98
web/src/lib/services/tag.service.ts
Normal file
98
web/src/lib/services/tag.service.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
|
import TagCreateModal from '$lib/modals/TagCreateModal.svelte';
|
||||||
|
import TagEditModal from '$lib/modals/TagEditModal.svelte';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||||
|
import { deleteTag, updateTag, upsertTags, type TagUpdateDto } from '@immich/sdk';
|
||||||
|
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||||
|
import { mdiPencil, mdiPlus, mdiTrashCanOutline } from '@mdi/js';
|
||||||
|
import { type MessageFormatter } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export const getTagActions = ($t: MessageFormatter, tag: TreeNode) => {
|
||||||
|
const Create: ActionItem = {
|
||||||
|
title: $t('create_tag'),
|
||||||
|
icon: mdiPlus,
|
||||||
|
onAction: () => modalManager.show(TagCreateModal, { baseTag: tag }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Update: ActionItem = {
|
||||||
|
title: $t('edit_tag'),
|
||||||
|
icon: mdiPencil,
|
||||||
|
$if: () => tag.path.length > 0,
|
||||||
|
onAction: () => modalManager.show(TagEditModal, { tag }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Delete: ActionItem = {
|
||||||
|
title: $t('delete_tag'),
|
||||||
|
icon: mdiTrashCanOutline,
|
||||||
|
$if: () => tag.path.length > 0,
|
||||||
|
onAction: () => handleDeleteTag(tag),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { Create, Update, Delete };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleCreateTag = async (tagValue: string) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [tag] = await upsertTags({ tagUpsertDto: { tags: [tagValue] } });
|
||||||
|
if (!tag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toastManager.success($t('tag_created', { values: { tag: tag.value } }));
|
||||||
|
eventManager.emit('TagCreate', tag);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.something_went_wrong'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleUpdateTag = async (tag: TreeNode, dto: TagUpdateDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
if (!tag.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await updateTag({ id: tag.id, tagUpdateDto: dto });
|
||||||
|
|
||||||
|
toastManager.success($t('tag_updated', { values: { tag: tag.value } }));
|
||||||
|
eventManager.emit('TagUpdate', response);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.something_went_wrong'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTag = async (tag: TreeNode) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
const tagId = tag.id;
|
||||||
|
if (!tagId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await modalManager.showDialog({
|
||||||
|
title: $t('delete_tag'),
|
||||||
|
prompt: $t('delete_tag_confirmation_prompt', { values: { tagName: tag.value } }),
|
||||||
|
confirmText: $t('delete'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteTag({ id: tagId });
|
||||||
|
eventManager.emit('TagDelete', tag);
|
||||||
|
toastManager.success();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.something_went_wrong'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,24 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
|
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
|
||||||
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
|
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
|
||||||
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
||||||
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
|
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
|
||||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
|
||||||
import { AppRoute, AssetAction, QueryParameter } from '$lib/constants';
|
|
||||||
import SkipLink from '$lib/elements/SkipLink.svelte';
|
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
|
||||||
import TagCreateModal from '$lib/modals/TagCreateModal.svelte';
|
|
||||||
import TagEditModal from '$lib/modals/TagEditModal.svelte';
|
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
|
|
||||||
import { deleteTag, getAllTags, type TagResponseDto } from '@immich/sdk';
|
|
||||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
|
||||||
import { mdiDotsVertical, mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||||
|
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||||
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
|
||||||
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
import ArchiveAction from '$lib/components/timeline/actions/ArchiveAction.svelte';
|
||||||
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||||
|
|
@ -31,8 +21,17 @@
|
||||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import { AppRoute, AssetAction, QueryParameter } from '$lib/constants';
|
||||||
|
import SkipLink from '$lib/elements/SkipLink.svelte';
|
||||||
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
|
import { getTagActions } from '$lib/services/tag.service';
|
||||||
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
|
import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
|
||||||
|
import { getAllTags, type TagResponseDto } from '@immich/sdk';
|
||||||
|
import { mdiDotsVertical, mdiPlus, mdiTag, mdiTagMultiple } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
|
|
@ -59,49 +58,29 @@
|
||||||
|
|
||||||
const navigateToView = (path: string) => goto(getLink(path));
|
const navigateToView = (path: string) => goto(getLink(path));
|
||||||
|
|
||||||
const handleCreate = async () => {
|
|
||||||
await modalManager.show(TagCreateModal, { baseTag: tag });
|
|
||||||
tags = await getAllTags();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = async () => {
|
|
||||||
if (!tag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await modalManager.show(TagEditModal, { tag });
|
|
||||||
tags = await getAllTags();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
if (!tag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isConfirm = await modalManager.showDialog({
|
|
||||||
title: $t('delete_tag'),
|
|
||||||
prompt: $t('delete_tag_confirmation_prompt', { values: { tagName: tag.value } }),
|
|
||||||
confirmText: $t('delete'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isConfirm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteTag({ id: tag.id! });
|
|
||||||
tags = await getAllTags();
|
|
||||||
|
|
||||||
// navigate to parent
|
|
||||||
await navigateToView(tag.parent ? tag.parent.path : '');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetVisibility = (assetIds: string[]) => {
|
const handleSetVisibility = (assetIds: string[]) => {
|
||||||
timelineManager.removeAssets(assetIds);
|
timelineManager.removeAssets(assetIds);
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRefresh = async () => {
|
||||||
|
tags = await getAllTags();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTagDelete = async (response: TreeNode) => {
|
||||||
|
if (response.path === tag.path) {
|
||||||
|
await navigateToView(tag.parent ? tag.parent.path : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
await onRefresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Create, Update, Delete } = $derived(getTagActions($t, tag));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<UserPageLayout title={data.meta.title}>
|
<OnEvents onTagCreate={onRefresh} onTagUpdate={onRefresh} {onTagDelete} />
|
||||||
|
|
||||||
|
<UserPageLayout title={data.meta.title} actions={[Create, Update, Delete]}>
|
||||||
{#snippet sidebar()}
|
{#snippet sidebar()}
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" />
|
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" />
|
||||||
|
|
@ -114,23 +93,6 @@
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet buttons()}
|
|
||||||
<HStack>
|
|
||||||
<Button leadingIcon={mdiPlus} onclick={handleCreate} size="small" variant="ghost" color="secondary">
|
|
||||||
<Text class="hidden md:block">{$t('create_tag')}</Text>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{#if tag.path.length > 0}
|
|
||||||
<Button leadingIcon={mdiPencil} onclick={handleEdit} size="small" variant="ghost" color="secondary">
|
|
||||||
<Text class="hidden md:block">{$t('edit_tag')}</Text>
|
|
||||||
</Button>
|
|
||||||
<Button leadingIcon={mdiTrashCanOutline} onclick={handleDelete} size="small" variant="ghost" color="secondary">
|
|
||||||
<Text class="hidden md:block">{$t('delete_tag')}</Text>
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</HStack>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<Breadcrumbs node={tag} icon={mdiTagMultiple} title={$t('tags')} {getLink} />
|
<Breadcrumbs node={tag} icon={mdiTagMultiple} title={$t('tags')} {getLink} />
|
||||||
|
|
||||||
<section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar">
|
<section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue