mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 23:52:32 -04:00
feat: workflows & plugins (#26727)
feat: plugins chore: better types feat: plugins
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { HeaderButtonActionItem } from '$lib/types';
|
||||
import { Button } from '@immich/ui';
|
||||
import { Button, type Variants } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
action: HeaderButtonActionItem;
|
||||
variant?: Variants;
|
||||
};
|
||||
|
||||
const { action }: Props = $props();
|
||||
const { action, variant }: Props = $props();
|
||||
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||
</script>
|
||||
|
||||
{#if action.$if?.() ?? true}
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant={variant ?? 'ghost'}
|
||||
size="small"
|
||||
{color}
|
||||
leadingIcon={icon}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import AlbumThumbnail from '$lib/components/album-page/AlbumThumbnail.svelte';
|
||||
import { Button, Text, Label, modalManager } from '@immich/ui';
|
||||
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
description?: string;
|
||||
albumIds: string[];
|
||||
array?: boolean;
|
||||
};
|
||||
|
||||
let { array, label, description, albumIds = $bindable([]) }: Props = $props();
|
||||
|
||||
const onAlbums = async () => {
|
||||
const albums = await modalManager.show(AlbumPickerModal);
|
||||
if (!albums || albums.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
albumIds = array ? [...albumIds, ...albums.map((album) => album.id)] : [albums[0].id];
|
||||
};
|
||||
</script>
|
||||
|
||||
{#snippet button()}
|
||||
<Button size="small" shape="round" color="secondary" onclick={() => onAlbums()}>{$t('choose')}</Button>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<Label for="album-picker" size="small" class="font-medium" {label} />
|
||||
{#if description}
|
||||
<Text color="muted" size="small">{description}</Text>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if array}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each albumIds as albumId, i (i)}
|
||||
<AlbumThumbnail
|
||||
{albumId}
|
||||
onDelete={() => {
|
||||
albumIds.splice(i, 1);
|
||||
albumIds = [...albumIds];
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{@render button()}
|
||||
</div>
|
||||
{:else}
|
||||
{@const albumId = albumIds[0]}
|
||||
{#if albumId}
|
||||
<AlbumThumbnail {albumId} onDelete={() => (albumIds = [])} />
|
||||
{:else}
|
||||
{@render button()}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import SchemaAlbumPicker from '$lib/components/SchemaAlbumPicker.svelte';
|
||||
import Self from '$lib/components/SchemaConfiguration.svelte';
|
||||
import type { JSONSchemaProperty, SchemaConfig } from '$lib/types';
|
||||
import { CodeBlock, Field, Input, Label, MultiSelect, NumberInput, Select, Switch, Text } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
schema: JSONSchemaProperty;
|
||||
root?: boolean;
|
||||
key?: string;
|
||||
config: SchemaConfig;
|
||||
};
|
||||
|
||||
let { schema, key = '', root = false, config = $bindable() }: Props = $props();
|
||||
|
||||
const label = $derived(schema.title ?? key);
|
||||
const description = $derived(schema.description);
|
||||
|
||||
const getValue = <T,>(defaultValue?: T) => (root === true ? config : (config?.[key] ?? defaultValue)) as T;
|
||||
const setValue = <T,>(value: T) => {
|
||||
if (root === true) {
|
||||
config = value;
|
||||
} else {
|
||||
if (config === undefined) {
|
||||
config = {};
|
||||
}
|
||||
|
||||
config[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
const getUiHintValue = () => {
|
||||
if (schema.array) {
|
||||
return getValue<string[]>([]);
|
||||
}
|
||||
|
||||
const values = getValue<string>();
|
||||
return values ? [values] : [];
|
||||
};
|
||||
|
||||
const setUiHintValue = (values: string[]) => setValue(schema.array ? values : values[0]);
|
||||
|
||||
const getBoolean = (defaultValue = false) => getValue<boolean>(defaultValue);
|
||||
const getString = () => getValue<string>();
|
||||
const getEnum = () => getValue<string[]>([]);
|
||||
const getNumber = () => getValue<number>();
|
||||
</script>
|
||||
|
||||
<!-- Empty schema object -->
|
||||
{#if Object.keys(schema).length === 0}
|
||||
<!-- noop -->
|
||||
<!-- nested configuration (also top level objects) -->
|
||||
{:else if schema.type === 'object'}
|
||||
{#if !root}
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label size="small" class="font-medium" {label}></Label>
|
||||
{#if description}
|
||||
<Text color="muted" size="small">{description}</Text>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4 {root ? '' : 'border-l-4 border-gray-200 ps-2'}">
|
||||
{#each Object.entries(schema.properties ?? {}) as [childKey, childSchema] (childKey)}
|
||||
<Self schema={childSchema} key={childKey} bind:config={getValue, setValue} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if schema.uiHint === 'albumId'}
|
||||
<SchemaAlbumPicker {label} {description} array={schema.array} bind:albumIds={getUiHintValue, setUiHintValue} />
|
||||
{:else if schema.enum && schema.array}
|
||||
<Field {label} {description}>
|
||||
<MultiSelect options={schema.enum} bind:values={getEnum, setValue} />
|
||||
</Field>
|
||||
{:else if schema.enum}
|
||||
<Field {label} {description}>
|
||||
<Select options={schema.enum} bind:value={getString, setValue} />
|
||||
</Field>
|
||||
{:else if schema.array}
|
||||
<!-- {@const values = getValue<string[]>([])}
|
||||
<Field {label} {description}>
|
||||
{#each values as value, i (i)}
|
||||
<Input bind:value={() => getValue<string>(), setValue} />
|
||||
{/each}
|
||||
</Field> -->
|
||||
{:else if schema.type === 'boolean'}
|
||||
<Field {label} {description}>
|
||||
<Switch bind:checked={() => getBoolean(schema.default ?? false), setValue} />
|
||||
</Field>
|
||||
{:else if schema.type === 'number'}
|
||||
<Field {label} {description}>
|
||||
<NumberInput bind:value={getNumber, setValue} />
|
||||
</Field>
|
||||
{:else if schema.type === 'string'}
|
||||
<Field {label} {description}>
|
||||
<Input bind:value={() => getValue<string>(), setValue} />
|
||||
</Field>
|
||||
{:else}
|
||||
<Text>Unknown schema</Text>
|
||||
<CodeBlock code={JSON.stringify(schema, null, 2)} />
|
||||
{/if}
|
||||
@@ -23,7 +23,7 @@
|
||||
showDateRange = false,
|
||||
showItemCount = false,
|
||||
preload = false,
|
||||
onShowContextMenu = undefined,
|
||||
onShowContextMenu,
|
||||
}: Props = $props();
|
||||
|
||||
const showAlbumContextMenu = (e: MouseEvent) => {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import AlbumCover from '$lib/components/album-page/AlbumCover.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getAlbumInfo } from '@immich/sdk';
|
||||
import { IconButton, LoadingSpinner } from '@immich/ui';
|
||||
import { mdiTrashCanOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
albumId: string;
|
||||
onDelete: () => unknown;
|
||||
}
|
||||
|
||||
let { albumId, onDelete }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getAlbumInfo({ ...authManager.params, id: albumId })}
|
||||
<LoadingSpinner />
|
||||
{:then album}
|
||||
<div class="flex gap-2">
|
||||
<AlbumCover {album} class="size-24" />
|
||||
<p
|
||||
class="line-clamp-2 grow text-lg/6 font-semibold text-black group-hover:text-primary dark:text-white"
|
||||
data-testid="album-name"
|
||||
title={album.albumName}
|
||||
>
|
||||
{album.albumName}
|
||||
</p>
|
||||
{#if album.description}
|
||||
<p
|
||||
class="line-clamp-2 grow text-lg/6 font-semibold text-black group-hover:text-primary dark:text-white"
|
||||
data-testid="album-name"
|
||||
>
|
||||
{album.description}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="">
|
||||
<IconButton
|
||||
icon={mdiTrashCanOutline}
|
||||
shape="round"
|
||||
color="danger"
|
||||
variant="ghost"
|
||||
onclick={onDelete}
|
||||
aria-label={$t('remove')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
Reference in New Issue
Block a user