feat: add watermarks and text tools

This commit is contained in:
2026-08-26 07:21:43 +05:00
parent b277f3c66d
commit 116d7c72aa
9 changed files with 296 additions and 4 deletions
+16 -1
View File
@@ -12,6 +12,7 @@
import ToolSearch from './search/ToolSearch.svelte';
import { createAutoRunner } from '$lib/tools/auto-run';
import { executeStep } from '$lib/tools/executor';
import { clearOverlay, setOverlay } from '$lib/tools/overlay-store.svelte';
import { t } from '$lib/i18n/t';
import { toolDescription, toolTitle } from '$lib/i18n/tool-strings';
import type { StageStatus } from './stage/stage-props';
@@ -51,6 +52,7 @@
let showMask = $state(false);
let info = $state<ImageInfo | null>(null);
let errorText = $state('');
let overlayImage = $state<PixelImage | null>(null);
// svelte-ignore state_referenced_locally
let values = $state<Record<string, any>>({ ...defaultParams(tool), ...presetBaseValues });
// svelte-ignore state_referenced_locally
@@ -272,7 +274,16 @@
info = null;
errorText = '';
status = 'idle';
values = defaultParams(tool);
clearOverlay();
values = { ...defaultParams(tool), ...presetBaseValues };
}
async function handleOverlayFile(file: File) {
try {
setOverlay(await decodeFile(file));
} catch (e) {
errorText = errorMessage(e);
}
}
function handlePaste(event: ClipboardEvent) {
@@ -332,6 +343,10 @@
handlePickColor={handlePickColor}
showError={showError}
errorMessage={errorMessage}
overlayImage={overlayImage}
onOverlayFile={(f) => void handleOverlayFile(f)}
onOverlayError={(e) => (errorText = errorMessage(e))}
onOverlayClear={clearOverlay}
/>
<div class="chain-stack">
@@ -9,6 +9,7 @@
import DownloadButton from '../DownloadButton.svelte';
import Button from '../ui/Button.svelte';
import EmptyState from '../ui/EmptyState.svelte';
import OverlayCard from '../tool/OverlayCard.svelte';
import ParamsCard from '../tool/ParamsCard.svelte';
import ResultCard from '../tool/ResultCard.svelte';
import SourceCard from '../tool/SourceCard.svelte';
@@ -43,6 +44,10 @@
handlePipetteToggle?: (id: string) => void;
handlePickColor?: (hex: string) => void;
showError?: (e: unknown) => void;
overlayImage?: PixelImage | null;
onOverlayFile?: (file: File) => void;
onOverlayError?: (e: unknown) => void;
onOverlayClear?: () => void;
index?: number;
input?: PixelImage | null;
result?: PixelImage | null;
@@ -81,6 +86,10 @@
handlePipetteToggle,
handlePickColor,
showError,
overlayImage = null,
onOverlayFile,
onOverlayError,
onOverlayClear,
index = 0,
input = null,
result = null,
@@ -138,6 +147,14 @@
pipetteActive={!!pipetteTargetId}
onPickColor={(hex) => handlePickColor?.(hex)}
/>
{#if tool.needsOverlaySource && source}
<OverlayCard
overlay={overlayImage}
onFile={(f) => onOverlayFile?.(f)}
onError={(e) => onOverlayError?.(e)}
onClear={() => onOverlayClear?.()}
/>
{/if}
{/if}
</div>
</section>
@@ -0,0 +1,63 @@
<script lang="ts">
import { t } from '$lib/i18n/t';
import type { PixelImage } from '$lib/core/types';
import DropZone from '../DropZone.svelte';
import Preview from '../Preview.svelte';
import Button from '../ui/Button.svelte';
interface Props {
overlay: PixelImage | null;
onFile: (file: File) => void;
onError: (e: unknown) => void;
onClear: () => void;
}
let { overlay, onFile, onError, onClear }: Props = $props();
</script>
<div class="overlay-card">
<span class="edge-legend overlay-legend" aria-hidden="true">{t('ui.overlayTitle')}</span>
{#if !overlay}
<DropZone {onFile} {onError} label={t('ui.overlayDrop')} />
{:else}
<div class="preview-wrap">
<Preview image={overlay} />
<Button variant="secondary" onclick={onClear}>{t('ui.overlayRemove')}</Button>
</div>
{/if}
</div>
<style>
.overlay-card {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-top: var(--space-3);
padding-top: var(--space-3);
border-top: 1px dashed var(--border);
}
.overlay-legend {
top: -0.65em;
left: 50%;
transform: translateX(-50%);
background: var(--surface);
}
.overlay-card :global(.dropzone) {
min-height: 7rem;
font-size: var(--text-s);
}
.preview-wrap {
display: flex;
flex-direction: column;
gap: var(--space-2);
align-items: stretch;
}
.preview-wrap :global(.media) {
max-height: 12rem;
}
</style>