chore: rework tags sidebar (#25855)

This commit is contained in:
Daniel Dietzler 2026-02-03 17:06:26 +01:00 committed by GitHub
parent 8872d2c7ae
commit 94965f6d66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 54 additions and 47 deletions

View file

@ -14,6 +14,7 @@
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { imageManager } from '$lib/managers/ImageManager.svelte'; import { imageManager } from '$lib/managers/ImageManager.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { getAssetActions } from '$lib/services/asset.service';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { ocrManager } from '$lib/stores/ocr.svelte'; import { ocrManager } from '$lib/stores/ocr.svelte';
import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store'; import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store';
@ -36,6 +37,7 @@
type PersonResponseDto, type PersonResponseDto,
type StackResponseDto, type StackResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { CommandPaletteDefaultProvider } from '@immich/ui';
import { onDestroy, onMount, untrack } from 'svelte'; import { onDestroy, onMount, untrack } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@ -426,8 +428,11 @@
!assetViewerManager.isShowEditor && !assetViewerManager.isShowEditor &&
ocrManager.hasOcrData, ocrManager.hasOcrData,
); );
const { Tag } = $derived(getAssetActions($t, asset));
</script> </script>
<CommandPaletteDefaultProvider name={$t('assets')} actions={[Tag]} />
<OnEvents {onAssetReplace} {onAssetUpdate} /> <OnEvents {onAssetReplace} {onAssetUpdate} />
<svelte:document bind:fullscreenElement /> <svelte:document bind:fullscreenElement />

View file

@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import { shortcut } from '$lib/actions/shortcut'; import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { getAssetActions } from '$lib/services/asset.service';
import { removeTag } from '$lib/utils/asset-utils'; import { removeTag } from '$lib/utils/asset-utils';
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
import { Icon, modalManager, Text } from '@immich/ui'; import { Badge, IconButton, Link, Text } from '@immich/ui';
import { mdiClose, mdiPlus } from '@mdi/js'; import { mdiClose } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
@ -18,22 +19,23 @@
let tags = $derived(asset.tags || []); let tags = $derived(asset.tags || []);
const handleAddTag = async () => {
const success = await modalManager.show(AssetTagModal, { assetIds: [asset.id] });
if (success) {
asset = await getAssetInfo({ id: asset.id });
}
};
const handleRemove = async (tagId: string) => { const handleRemove = async (tagId: string) => {
const ids = await removeTag({ tagIds: [tagId], assetIds: [asset.id], showNotification: false }); const ids = await removeTag({ tagIds: [tagId], assetIds: [asset.id], showNotification: false });
if (ids) { if (ids) {
asset = await getAssetInfo({ id: asset.id }); asset = await getAssetInfo({ id: asset.id });
} }
}; };
const onAssetsTag = async (ids: string[]) => {
if (ids.includes(asset.id)) {
asset = await getAssetInfo({ id: asset.id });
}
};
const { Tag } = $derived(getAssetActions($t, asset));
</script> </script>
<svelte:document use:shortcut={{ shortcut: { key: 't' }, onShortcut: handleAddTag }} /> <OnEvents {onAssetsTag} />
{#if isOwner && !authManager.isSharedLink} {#if isOwner && !authManager.isSharedLink}
<section class="px-4 mt-4"> <section class="px-4 mt-4">
@ -42,36 +44,24 @@
</div> </div>
<section class="flex flex-wrap pt-2 gap-1" data-testid="detail-panel-tags"> <section class="flex flex-wrap pt-2 gap-1" data-testid="detail-panel-tags">
{#each tags as tag (tag.id)} {#each tags as tag (tag.id)}
<div class="flex group transition-all"> <Badge size="small" class="items-center px-0" shape="round">
<a <Link
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-primary rounded-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
href={Route.tags({ path: tag.value })} href={Route.tags({ path: tag.value })}
class="text-light no-underline rounded-full hover:bg-primary-400 px-2"
> >
<p class="text-sm"> {tag.value}
{tag.value} </Link>
</p> <IconButton
</a> aria-label={$t('remove_tag')}
icon={mdiClose}
<button
type="button"
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-e-full place-items-center place-content-center pe-2 ps-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
title="Remove tag"
onclick={() => handleRemove(tag.id)} onclick={() => handleRemove(tag.id)}
> size="tiny"
<Icon icon={mdiClose} /> class="hover:bg-primary-400"
</button> shape="round"
</div> />
</Badge>
{/each} {/each}
<button <HeaderActionButton action={Tag} />
type="button"
class="rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200 flex place-items-center place-content-center gap-1 px-2 py-1"
title={$t('add_tag')}
onclick={handleAddTag}
>
<span class="text-sm px-1 flex place-items-center place-content-center gap-1"
><Icon icon={mdiPlus} />{$t('add')}</span
>
</button>
</section> </section>
</section> </section>
{/if} {/if}

View file

@ -20,11 +20,8 @@
const handleTagAssets = async () => { const handleTagAssets = async () => {
const assets = [...getOwnedAssets()]; const assets = [...getOwnedAssets()];
const success = await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) }); await modalManager.show(AssetTagModal, { assetIds: assets.map(({ id }) => id) });
clearSelect();
if (success) {
clearSelect();
}
}; };
</script> </script>

View file

@ -37,6 +37,7 @@ export type Events = {
AssetsArchive: [string[]]; AssetsArchive: [string[]];
AssetsDelete: [string[]]; AssetsDelete: [string[]];
AssetEditsApplied: [string]; AssetEditsApplied: [string];
AssetsTag: [string[]];
AlbumAddAssets: []; AlbumAddAssets: [];
AlbumUpdate: [AlbumResponseDto]; AlbumUpdate: [AlbumResponseDto];

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { eventManager } from '$lib/managers/event-manager.svelte';
import { tagAssets } from '$lib/utils/asset-utils'; import { tagAssets } from '$lib/utils/asset-utils';
import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk'; import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk';
import { FormModal, Icon } from '@immich/ui'; import { FormModal, Icon } from '@immich/ui';
@ -9,7 +10,7 @@
import Combobox, { type ComboBoxOption } from '../components/shared-components/combobox.svelte'; import Combobox, { type ComboBoxOption } from '../components/shared-components/combobox.svelte';
interface Props { interface Props {
onClose: (success?: true) => void; onClose: () => void;
assetIds: string[]; assetIds: string[];
} }
@ -30,8 +31,8 @@
return; return;
} }
await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false }); const updatedIds = await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false });
onClose(true); eventManager.emit('AssetsTag', updatedIds);
}; };
const handleSelect = async (option?: ComboBoxOption) => { const handleSelect = async (option?: ComboBoxOption) => {

View file

@ -2,6 +2,7 @@ import { ProjectionType } from '$lib/constants';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { user as authUser, preferences } from '$lib/stores/user.store'; import { user as authUser, preferences } from '$lib/stores/user.store';
import { getAssetJobName, getSharedLink, sleep } from '$lib/utils'; import { getAssetJobName, getSharedLink, sleep } from '$lib/utils';
@ -41,6 +42,7 @@ import {
mdiMotionPauseOutline, mdiMotionPauseOutline,
mdiMotionPlayOutline, mdiMotionPlayOutline,
mdiShareVariantOutline, mdiShareVariantOutline,
mdiTagPlusOutline,
mdiTune, mdiTune,
} from '@mdi/js'; } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n'; import type { MessageFormatter } from 'svelte-i18n';
@ -49,6 +51,7 @@ import { get } from 'svelte/store';
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => { export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
const sharedLink = getSharedLink(); const sharedLink = getSharedLink();
const currentAuthUser = get(authUser); const currentAuthUser = get(authUser);
const userPreferences = get(preferences);
const isOwner = !!(currentAuthUser && currentAuthUser.id === asset.ownerId); const isOwner = !!(currentAuthUser && currentAuthUser.id === asset.ownerId);
const Share: ActionItem = { const Share: ActionItem = {
@ -155,7 +158,16 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
type: $t('assets'), type: $t('assets'),
$if: () => asset.hasMetadata, $if: () => asset.hasMetadata,
onAction: () => assetViewerManager.toggleDetailPanel(), onAction: () => assetViewerManager.toggleDetailPanel(),
shortcuts: [{ key: 'i' }], shortcuts: { key: 'i' },
};
const Tag: ActionItem = {
title: $t('add_tag'),
icon: mdiTagPlusOutline,
type: $t('assets'),
$if: () => userPreferences.tags.enabled,
onAction: () => modalManager.show(AssetTagModal, { assetIds: [asset.id] }),
shortcuts: { key: 't' },
}; };
const Edit: ActionItem = { const Edit: ActionItem = {
@ -212,6 +224,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
Copy, Copy,
Tag,
Edit, Edit,
RefreshFacesJob, RefreshFacesJob,
RefreshMetadataJob, RefreshMetadataJob,