feat: fix tools pages, dom audit fix

This commit is contained in:
2026-08-29 09:08:09 +05:00
parent b695f5d58c
commit 9ef6ae03c6
8 changed files with 530 additions and 506 deletions
+6 -228
View File
@@ -1,211 +1,23 @@
<script lang="ts">
import { browser } from "$app/environment";
import { untrack } from "svelte";
import {
getTool,
defaultParams,
isChainable,
TOOLS,
} from "$lib/registry";
import { encode, downloadBlob, toDataUrl, decodeBytes } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types";
import WorkspaceLayout from "$lib/components/kit/WorkspaceLayout.svelte";
import { getTool } from "$lib/registry";
import ToolView from "$lib/components/kit/ToolView.svelte";
import EmptyState from "$lib/components/kit/EmptyState.svelte";
import { SlidersHorizontal as ToolIcon } from "@lucide/svelte";
import SettingsPanel from "./SettingsPanel.svelte";
import PreviewPanel from "./PreviewPanel.svelte";
type ParamValue = string | number | boolean;
interface ChainStep {
toolId: string;
params: Record<string, ParamValue>;
}
interface Props {
data: { id: string };
}
let { data }: Props = $props();
const initialTool = untrack(() => getTool(data.id));
let steps = $state<ChainStep[]>(
initialTool
? [
{
toolId: initialTool.id,
params: defaultParams(initialTool) as Record<string, ParamValue>,
},
]
: [],
);
let sourceImg = $state<PixelImage | null>(null);
let sourceUrl = $state("");
let sourceName = $state("");
let stepOutputs = $state<{ toolId: string; title: string; url: string }[]>(
[],
);
let resultImg = $state<PixelImage | null>(null);
let resultUrl = $state("");
let chainError = $state("");
let processing = $state(false);
const chainOptions = TOOLS.filter(isChainable).map((t) => ({
value: t.id,
label: t.title,
}));
let addId = $state(chainOptions[0]?.value ?? "");
const firstTool = $derived(steps[0] ? getTool(steps[0].toolId) : undefined);
const needsSource = $derived(
firstTool ? firstTool.sourceMode !== "none" : false,
);
$effect(() => {
const t = getTool(data.id);
if (t) {
steps = [
{
toolId: t.id,
params: defaultParams(t) as Record<string, ParamValue>,
},
];
sourceImg = null;
sourceUrl = "";
sourceName = "";
}
});
$effect(() => {
if (!browser) return;
const chain = steps.map((s) => ({
tool: getTool(s.toolId),
params: { ...s.params },
}));
const src = sourceImg;
if (!chain.length || chain.some((c) => !c.tool)) {
resultImg = null;
resultUrl = "";
stepOutputs = [];
return;
}
const first = chain[0].tool!;
if (first.sourceMode !== "none" && !src) {
resultImg = null;
resultUrl = "";
stepOutputs = [];
return;
}
void runPipeline(chain, src);
});
async function runPipeline(
chain: {
tool: ReturnType<typeof getTool>;
params: Record<string, ParamValue>;
}[],
src: PixelImage | null,
) {
processing = true;
try {
let current: PixelImage | null = src;
const outs: { toolId: string; title: string; url: string }[] = [];
for (const c of chain) {
const tool = c.tool!;
const resolve = (x: PixelImage | Promise<PixelImage>) =>
x instanceof Promise ? x : Promise.resolve(x);
let out: PixelImage;
if (!current && tool.sourceMode === "none" && tool.generate) {
out = await resolve(tool.generate(c.params));
} else if (current && tool.run) {
out = await resolve(tool.run(current, c.params));
} else if (!current && src && tool.run) {
out = await resolve(tool.run(src, c.params));
} else {
continue;
}
current = out;
outs.push({ toolId: tool.id, title: tool.title, url: toDataUrl(out) });
}
stepOutputs = outs;
resultImg = current;
resultUrl = current ? toDataUrl(current) : "";
chainError = "";
} catch (e) {
chainError = String(e);
} finally {
processing = false;
}
}
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);
chainError = "";
} catch (e) {
chainError = String(e);
}
}
function clearSource() {
sourceImg = null;
sourceUrl = "";
sourceName = "";
}
function setStepParam(index: number, id: string, value: ParamValue) {
steps[index].params[id] = value;
}
function addStep() {
const tool = getTool(addId);
if (!tool) return;
steps = [
...steps,
{
toolId: tool.id,
params: defaultParams(tool) as Record<string, ParamValue>,
},
];
}
function removeStep(index: number) {
steps = steps.filter((_, i) => i !== index);
}
async function downloadResult() {
if (!resultImg || !firstTool) return;
const blob = await encode(resultImg, "image/png");
const base = sourceName ? sourceName.replace(/\.[^.]+$/, "") : firstTool.id;
downloadBlob(blob, `${base}-pipeline.png`);
}
const resultMeta = $derived(
resultImg
? [
{ caption: "STEPS", value: String(steps.length) },
{
caption: "DIMENSIONS",
value: `${resultImg.width} × ${resultImg.height} px`,
},
{ caption: "FORMAT", value: "PNG-24" },
]
: [],
);
const tool = $derived(getTool(data.id));
</script>
<svelte:head>
<title>easy-png-tools / {firstTool?.title ?? "Tool"}</title>
<title>easy-png-tools / {tool?.title ?? "Tool"}</title>
<meta name="robots" content="noindex, nofollow" />
</svelte:head>
{#if !firstTool}
{#if !tool}
<div class="notfound">
<EmptyState
title="Tool not found"
@@ -214,41 +26,7 @@
/>
</div>
{:else}
<WorkspaceLayout
columns="minmax(0, 1fr) minmax(0, 1.1fr)"
gap="40px"
padding="48px clamp(24px, 4vw, 72px) 72px"
maxWidth="1680px"
stickyTop="84px"
padMobile="32px 16px 48px"
>
{#snippet settings()}
<SettingsPanel
{steps}
{chainOptions}
{addId}
onAddStep={addStep}
onSetAddId={(v) => (addId = v)}
onRemoveStep={removeStep}
onSetParam={setStepParam}
/>
{/snippet}
{#snippet preview()}
<PreviewPanel
{needsSource}
{sourceImg}
{sourceUrl}
{stepOutputs}
{resultUrl}
{resultMeta}
{processing}
{chainError}
onFile={onFile}
onClearSource={clearSource}
onDownload={downloadResult}
/>
{/snippet}
</WorkspaceLayout>
<ToolView {tool} />
{/if}
<style>