mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
chore: rework tags sidebar (#25855)
This commit is contained in:
parent
8872d2c7ae
commit
94965f6d66
6 changed files with 54 additions and 47 deletions
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue