mirror of
https://github.com/immich-app/immich.git
synced 2026-03-09 19:57:41 +03:00
feat: tags (#11980)
* feat: tags * fix: folder tree icons * navigate to tag from detail panel * delete tag * Tag position and add tag button * Tag asset in detail panel * refactor form * feat: navigate to tag page from clicking on a tag * feat: delete tags from the tag page * refactor: moving tag section in detail panel and add + tag button * feat: tag asset action in detail panel * refactor add tag form * fdisable add tag button when there is no selection * feat: tag bulk endpoint * feat: tag colors * chore: clean up * chore: unit tests * feat: write tags to sidecar * Remove tag and auto focus on tag creation form opened * chore: regenerate migration * chore: linting * add color picker to tag edit form * fix: force render tags timeline on navigating back from asset viewer * feat: read tags from keywords * chore: clean up --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
47
web/src/lib/components/photos-page/actions/tag-action.svelte
Normal file
47
web/src/lib/components/photos-page/actions/tag-action.svelte
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { tagAssets } from '$lib/utils/asset-utils';
|
||||
import { mdiTagMultipleOutline, mdiTimerSand } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte';
|
||||
|
||||
export let menuItem = false;
|
||||
|
||||
const text = $t('tag');
|
||||
const icon = mdiTagMultipleOutline;
|
||||
|
||||
let loading = false;
|
||||
let isOpen = false;
|
||||
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
const handleOpen = () => (isOpen = true);
|
||||
const handleCancel = () => (isOpen = false);
|
||||
const handleTag = async (tagIds: string[]) => {
|
||||
const assets = [...getOwnedAssets()];
|
||||
loading = true;
|
||||
const ids = await tagAssets({ tagIds, assetIds: assets.map((asset) => asset.id) });
|
||||
if (ids) {
|
||||
clearSelect();
|
||||
}
|
||||
loading = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if menuItem}
|
||||
<MenuOption {text} {icon} onClick={handleOpen} />
|
||||
{/if}
|
||||
|
||||
{#if !menuItem}
|
||||
{#if loading}
|
||||
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
|
||||
{:else}
|
||||
<CircleIconButton title={text} {icon} on:click={handleOpen} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isOpen}
|
||||
<TagAssetForm onTag={(tagIds) => handleTag(tagIds)} onCancel={handleCancel} />
|
||||
{/if}
|
||||
@@ -109,7 +109,7 @@
|
||||
);
|
||||
},
|
||||
onSeparate: () => {
|
||||
$assetStore.taskManager.seperatedDateGroup(componentId, dateGroup, () =>
|
||||
$assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () =>
|
||||
assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: false }),
|
||||
);
|
||||
},
|
||||
@@ -186,9 +186,9 @@
|
||||
<div
|
||||
use:intersectionObserver={{
|
||||
onIntersect: () => onAssetInGrid?.(asset),
|
||||
top: `-${TITLE_HEIGHT}px`,
|
||||
bottom: `-${viewport.height - TITLE_HEIGHT - 1}px`,
|
||||
right: `-${viewport.width - 1}px`,
|
||||
top: `${-TITLE_HEIGHT}px`,
|
||||
bottom: `${-(viewport.height - TITLE_HEIGHT - 1)}px`,
|
||||
right: `${-(viewport.width - 1)}px`,
|
||||
root: assetGridElement,
|
||||
}}
|
||||
data-asset-id={asset.id}
|
||||
|
||||
@@ -498,21 +498,21 @@
|
||||
}
|
||||
};
|
||||
|
||||
function intersectedHandler(bucket: AssetBucket) {
|
||||
function handleIntersect(bucket: AssetBucket) {
|
||||
updateLastIntersectedBucketDate();
|
||||
const intersectedTask = () => {
|
||||
const task = () => {
|
||||
$assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
|
||||
void $assetStore.loadBucket(bucket.bucketDate);
|
||||
};
|
||||
$assetStore.taskManager.intersectedBucket(componentId, bucket, intersectedTask);
|
||||
$assetStore.taskManager.intersectedBucket(componentId, bucket, task);
|
||||
}
|
||||
|
||||
function seperatedHandler(bucket: AssetBucket) {
|
||||
const seperatedTask = () => {
|
||||
function handleSeparate(bucket: AssetBucket) {
|
||||
const task = () => {
|
||||
$assetStore.updateBucket(bucket.bucketDate, { intersecting: false });
|
||||
bucket.cancel();
|
||||
};
|
||||
$assetStore.taskManager.seperatedBucket(componentId, bucket, seperatedTask);
|
||||
$assetStore.taskManager.separatedBucket(componentId, bucket, task);
|
||||
}
|
||||
|
||||
const handlePrevious = async () => {
|
||||
@@ -809,8 +809,8 @@
|
||||
<div
|
||||
id="bucket"
|
||||
use:intersectionObserver={{
|
||||
onIntersect: () => intersectedHandler(bucket),
|
||||
onSeparate: () => seperatedHandler(bucket),
|
||||
onIntersect: () => handleIntersect(bucket),
|
||||
onSeparate: () => handleSeparate(bucket),
|
||||
top: BUCKET_INTERSECTION_ROOT_TOP,
|
||||
bottom: BUCKET_INTERSECTION_ROOT_BOTTOM,
|
||||
root: element,
|
||||
|
||||
Reference in New Issue
Block a user