From a16a7b92c737273b27fd2611a77c65b746d63231 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Fri, 28 Aug 2026 10:55:51 +0500 Subject: [PATCH] feat: add dropzone, update tool page --- docs/plan-redesign.md | 2 +- web/src/lib/components/kit/Dropzone.svelte | 94 +++++ .../routes/preview/tools/[id]/+page.svelte | 374 ++++++++++++++---- 3 files changed, 392 insertions(+), 78 deletions(-) create mode 100644 web/src/lib/components/kit/Dropzone.svelte diff --git a/docs/plan-redesign.md b/docs/plan-redesign.md index 1110bb6..1a8990a 100644 --- a/docs/plan-redesign.md +++ b/docs/plan-redesign.md @@ -331,7 +331,7 @@ src/routes/preview/tools/[id]/+page.svelte подключим на C12). - [x] **[C11]** `/preview/demo` — зеркало воркспейса (4 шага + панель превью), статичный приёмочный стенд. - [x] **[C12]** `/preview/tools/[id]` — data-driven settings-panel + preview-panel (gradient live, остальные — плейсхолдер); `NumberField`, `entries()` для prerender, ToolCard-ссылки подключены. -- [ ] **[C13]** `/preview/tools/[id]` — раскладка background-remover (comparison-grid). +- [x] **[C13]** `/preview/tools/[id]` — comparison-grid для file-source инструментов (`Dropzone` + before/after `CheckerCanvas`, `result-meta`); охватывает background-remover и прочие. - [ ] **[C14]** step-cards для chain в tool-page. ### Шаг 4. Переключатель и проверка diff --git a/web/src/lib/components/kit/Dropzone.svelte b/web/src/lib/components/kit/Dropzone.svelte new file mode 100644 index 0000000..95351c3 --- /dev/null +++ b/web/src/lib/components/kit/Dropzone.svelte @@ -0,0 +1,94 @@ + + +
{ + e.preventDefault(); + dragging = true; + }} + ondragleave={() => (dragging = false)} + ondrop={onDrop} + onclick={open} + onkeydown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + open(); + } + }} +> + handleFiles((e.target as HTMLInputElement).files)} + /> + + Drop a PNG here + or click to browse — processed locally +
+ + diff --git a/web/src/routes/preview/tools/[id]/+page.svelte b/web/src/routes/preview/tools/[id]/+page.svelte index 9d9c1cd..84d1001 100644 --- a/web/src/routes/preview/tools/[id]/+page.svelte +++ b/web/src/routes/preview/tools/[id]/+page.svelte @@ -2,7 +2,8 @@ import { browser } from "$app/environment"; import { untrack } from "svelte"; import { getTool, defaultParams } from "$lib/registry"; - import { encode, downloadBlob, toDataUrl } from "$lib/core/io"; + import { encode, downloadBlob, toDataUrl, decodeBytes } from "$lib/core/io"; + import type { PixelImage } from "$lib/core/types"; import Panel from "$lib/components/kit/Panel.svelte"; import PanelHeading from "$lib/components/kit/PanelHeading.svelte"; import SettingsFooter from "$lib/components/kit/SettingsFooter.svelte"; @@ -12,13 +13,14 @@ import Toggle from "$lib/components/kit/Toggle.svelte"; import TextField from "$lib/components/kit/TextField.svelte"; import NumberField from "$lib/components/kit/NumberField.svelte"; - import PreviewTile from "$lib/components/kit/PreviewTile.svelte"; import CheckerCanvas from "$lib/components/kit/CheckerCanvas.svelte"; import MetaList from "$lib/components/kit/MetaList.svelte"; import DownloadButton from "$lib/components/kit/DownloadButton.svelte"; import Badge from "$lib/components/kit/Badge.svelte"; import StatusLine from "$lib/components/kit/StatusLine.svelte"; import EmptyState from "$lib/components/kit/EmptyState.svelte"; + import Button from "$lib/components/kit/Button.svelte"; + import Dropzone from "$lib/components/kit/Dropzone.svelte"; import { SlidersHorizontal as ToolIcon } from "@lucide/svelte"; type ParamValue = string | number | boolean; @@ -37,6 +39,13 @@ let previewUrl = $state(""); let previewError = $state(""); + let sourceImg = $state(null); + let sourceUrl = $state(""); + let sourceName = $state(""); + let resultImg = $state(null); + let resultUrl = $state(""); + let showMask = $state(false); + const tool = $derived(getTool(data.id)); $effect(() => { @@ -71,6 +80,57 @@ } }); + $effect(() => { + const src = sourceImg; + if (!tool || !tool.run || !src) { + resultImg = null; + resultUrl = ""; + return; + } + if (!browser) return; + const snapshot = { ...params }; + const mask = showMask && !!tool.preview; + try { + const base = mask + ? tool.preview!(src, snapshot) + : tool.run(src, snapshot); + const apply = (img: PixelImage) => { + resultImg = img; + resultUrl = toDataUrl(img); + previewError = ""; + }; + if (base instanceof Promise) { + base.then(apply).catch((e) => (previewError = String(e))); + } else { + apply(base); + } + } catch (e) { + previewError = String(e); + } + }); + + async function onFile(file: File) { + if (!browser) return; + try { + const bytes = new Uint8Array(await file.arrayBuffer()); + const img = await decodeBytes(bytes); + sourceImg = img; + sourceName = file.name; + sourceUrl = toDataUrl(img); + previewError = ""; + } catch (e) { + previewError = String(e); + } + } + + function clearSource() { + sourceImg = null; + sourceUrl = ""; + sourceName = ""; + resultImg = null; + resultUrl = ""; + } + async function download() { if (!tool?.generate) return; const result = tool.generate({ ...params }); @@ -79,10 +139,21 @@ downloadBlob(blob, `${tool.id}.png`); } + async function downloadResult() { + if (!resultImg || !tool) return; + const blob = await encode(resultImg, "image/png"); + const base = sourceName.replace(/\.[^.]+$/, ""); + downloadBlob(blob, `${base}-${tool.id}.png`); + } + function setParam(id: string, value: ParamValue) { params[id] = value; } + const isFileTool = $derived( + tool ? tool.sourceMode === "file" || tool.sourceMode === undefined : false, + ); + const meta = $derived( tool ? [ @@ -108,6 +179,73 @@ +{#snippet settingsPanel()} + + + {#snippet actions()} + AUTO + {/snippet} + +

{tool!.description}

+
+ {#each tool!.params as p (p.id)} +
+ {#if p.type === "color"} + setParam(p.id, v)} + /> + {:else if p.type === "slider"} + setParam(p.id, v)} + /> + {:else if p.type === "select"} + setParam(p.id, v)} + /> + {:else if p.type === "checkbox"} + + {:else if p.type === "text"} + setParam(p.id, v)} + /> + {:else if p.type === "number"} + setParam(p.id, v)} + /> + {/if} +
+ {/each} +
+ + + +
+{/snippet} + {#if !tool}
-{:else} +{:else if tool.sourceMode === "none"}
-
- - - {#snippet actions()} - AUTO - {/snippet} - -

{tool.description}

-
- {#each tool.params as p (p.id)} -
- {#if p.type === "color"} - setParam(p.id, v)} - /> - {:else if p.type === "slider"} - setParam(p.id, v)} - /> - {:else if p.type === "select"} - setParam(p.id, v)} - /> - {:else if p.type === "checkbox"} - - {:else if p.type === "text"} - setParam(p.id, v)} - /> - {:else if p.type === "number"} - setParam(p.id, v)} - /> - {/if} -
- {/each} -
- - - -
-
- +
{@render settingsPanel()}
@@ -196,11 +268,9 @@ {#if previewError} {:else if previewUrl} - - - result - - + + result + {:else if tool.generate}
@@ -208,7 +278,7 @@ {:else} {/if} @@ -217,6 +287,89 @@
+{:else if isFileTool} +
+
{@render settingsPanel()}
+
+ + + {#snippet actions()} + {#if resultUrl} + + {/if} + {/snippet} + +
+ {#if previewError} + + {/if} + {#if !sourceImg} + + {:else} +
+
+
+ SOURCE{sourceName} +
+
+ + source + + {sourceImg.width} × {sourceImg.height} +
+
+
+
+ RESULT{sourceName} +
+
+ {#if resultUrl} + + result + + {/if} + {resultImg + ? `${resultImg.width} × ${resultImg.height}` + : "—"} +
+
+
+
+ FORMAT PNG-24 + ALPHA ENABLED + STATUS PROCESSED +
+ + {/if} +
+
+
+
+{:else} +
+
{@render settingsPanel()}
+
+ + +
+ +
+
+
+
{/if}