mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
refactor: separate schema tool runner
This commit is contained in:
@@ -29,8 +29,12 @@
|
|||||||
`SchemaAlignedLayout.svelte` (view-only: разметка + scoped-стили, сниппет
|
`SchemaAlignedLayout.svelte` (view-only: разметка + scoped-стили, сниппет
|
||||||
`stepCard` остался в родителе и передаётся пропсом). Родитель уменьшился
|
`stepCard` остался в родителе и передаётся пропсом). Родитель уменьшился
|
||||||
примерно на 200 строк.
|
примерно на 200 строк.
|
||||||
- **H1b (далее):** runner (run/download/clipboard, `runGen`/`running`,
|
- **H1b — СДЕЛАНО (2026-10):** runner вынесен в `schema-tool-runner.svelte.ts`
|
||||||
`assignResult`, `toDisplayError`) → `schema-tool-runner.svelte.ts`.
|
(`createSchemaToolRunner(ctx)`: состояние в `$state`-объекте `RunnerState`,
|
||||||
|
действия — модульные функции run/download/
|
||||||
|
copyText/downloadText/assignResult/toDisplayError, геттеры наружу; лимит
|
||||||
|
`max-lines-per-function` не даёт держать всё в одной фабрике). Компонент
|
||||||
|
читает состояние через `$derived`-алиасы.
|
||||||
- **H1c (далее):** стейт шагов (мутации `steps`, `touchedByStep`, `lastAxis`) →
|
- **H1c (далее):** стейт шагов (мутации `steps`, `touchedByStep`, `lastAxis`) →
|
||||||
`schema-tool-state.svelte.ts`.
|
`schema-tool-state.svelte.ts`.
|
||||||
|
|
||||||
|
|||||||
@@ -8,29 +8,18 @@
|
|||||||
import { Star } from "@lucide/svelte";
|
import { Star } from "@lucide/svelte";
|
||||||
import { hasTransparency } from "$lib/core/analyze";
|
import { hasTransparency } from "$lib/core/analyze";
|
||||||
import { debounce } from "$lib/core/debounce";
|
import { debounce } from "$lib/core/debounce";
|
||||||
import { ToolError, type ErrorVars } from "$lib/core/errors";
|
import { ToolError } from "$lib/core/errors";
|
||||||
import {
|
import {
|
||||||
decodeFile,
|
decodeFile,
|
||||||
downloadBlob,
|
|
||||||
encode,
|
|
||||||
fitWithinBytes,
|
|
||||||
outputFormatByMime,
|
outputFormatByMime,
|
||||||
type OutputMime,
|
type OutputMime,
|
||||||
} from "$lib/core/io";
|
} from "$lib/core/io";
|
||||||
import type { PixelImage } from "$lib/core/types";
|
import type { PixelImage } from "$lib/core/types";
|
||||||
import { execute } from "$lib/executor";
|
import { baseName, uniqueName, type ChainWarning } from "$lib/run-chain";
|
||||||
import {
|
|
||||||
baseName,
|
|
||||||
ChainStepError,
|
|
||||||
runChain,
|
|
||||||
uniqueName,
|
|
||||||
type ChainWarning,
|
|
||||||
} from "$lib/run-chain";
|
|
||||||
import {
|
import {
|
||||||
pageDescription,
|
pageDescription,
|
||||||
pageTitle,
|
pageTitle,
|
||||||
chainStepTitle,
|
chainStepTitle,
|
||||||
verdictText,
|
|
||||||
} from "$lib/i18n/schema-tool-strings";
|
} from "$lib/i18n/schema-tool-strings";
|
||||||
import { t } from "$lib/i18n/t";
|
import { t } from "$lib/i18n/t";
|
||||||
import {
|
import {
|
||||||
@@ -52,11 +41,9 @@
|
|||||||
} from "$lib/pipeline.svelte";
|
} from "$lib/pipeline.svelte";
|
||||||
import {
|
import {
|
||||||
getTool,
|
getTool,
|
||||||
type FileResult,
|
|
||||||
type Page,
|
type Page,
|
||||||
type Tool,
|
type Tool,
|
||||||
type ToolImageFile,
|
type ToolImageFile,
|
||||||
type ToolResult,
|
|
||||||
} from "$lib/registry";
|
} from "$lib/registry";
|
||||||
import {
|
import {
|
||||||
applySourceDefaults,
|
applySourceDefaults,
|
||||||
@@ -66,7 +53,6 @@
|
|||||||
type Dimension,
|
type Dimension,
|
||||||
type ToolSchema,
|
type ToolSchema,
|
||||||
} from "$lib/registry-schema";
|
} from "$lib/registry-schema";
|
||||||
import { downloadZip } from "$lib/zip";
|
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { SvelteMap, SvelteSet } from "svelte/reactivity";
|
import { SvelteMap, SvelteSet } from "svelte/reactivity";
|
||||||
import AddStepButton from "./AddStepButton.svelte";
|
import AddStepButton from "./AddStepButton.svelte";
|
||||||
@@ -74,6 +60,7 @@
|
|||||||
import SchemaFields from "./SchemaFields.svelte";
|
import SchemaFields from "./SchemaFields.svelte";
|
||||||
import SchemaPreview from "./SchemaPreview.svelte";
|
import SchemaPreview from "./SchemaPreview.svelte";
|
||||||
import ToolPickerButton from "./ToolPickerButton.svelte";
|
import ToolPickerButton from "./ToolPickerButton.svelte";
|
||||||
|
import { createSchemaToolRunner } from "./schema-tool-runner.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page: Page;
|
page: Page;
|
||||||
@@ -81,9 +68,6 @@
|
|||||||
/** Named-chain mode: the chain id and display name; page is virtual. */
|
/** Named-chain mode: the chain id and display name; page is virtual. */
|
||||||
chainId?: string;
|
chainId?: string;
|
||||||
}
|
}
|
||||||
type DisplayError =
|
|
||||||
| { kind: "i18n"; key: string; vars?: ErrorVars }
|
|
||||||
| { kind: "plain"; text: string };
|
|
||||||
let { page, tool, chainId = undefined }: Props = $props();
|
let { page, tool, chainId = undefined }: Props = $props();
|
||||||
|
|
||||||
const schema = $derived(
|
const schema = $derived(
|
||||||
@@ -98,30 +82,13 @@
|
|||||||
const touchedByStep = new SvelteMap<string, SvelteSet<string>>();
|
const touchedByStep = new SvelteMap<string, SvelteSet<string>>();
|
||||||
// Last edited axis of a dimension field; leads under lockAspect.
|
// Last edited axis of a dimension field; leads under lockAspect.
|
||||||
const lastAxis: Record<string, "width" | "height"> = {};
|
const lastAxis: Record<string, "width" | "height"> = {};
|
||||||
let stepDims = $state<(Dimension | undefined)[]>([]);
|
|
||||||
let stepResults = $state<(PixelImage | null)[]>([]);
|
|
||||||
// Full named set per step (batch/fan-out); intermediate tiles show a grid.
|
|
||||||
let stepFileSets = $state<ToolImageFile[][]>([]);
|
|
||||||
let aligned = $state(false);
|
let aligned = $state(false);
|
||||||
let source = $state<PixelImage | null>(null);
|
let source = $state<PixelImage | null>(null);
|
||||||
// The full named input set (multi-upload); `source` above is the first
|
// The full named input set (multi-upload); `source` above is the first
|
||||||
// image for preview/defaults. Single upload = one element.
|
// image for preview/defaults. Single upload = one element.
|
||||||
let sourceFiles = $state<ToolImageFile[]>([]);
|
let sourceFiles = $state<ToolImageFile[]>([]);
|
||||||
let result = $state<PixelImage | null>(null);
|
|
||||||
let fileResult = $state<FileResult | null>(null);
|
|
||||||
let textSource = $state("");
|
let textSource = $state("");
|
||||||
let textResult = $state<string | null>(null);
|
|
||||||
let verdictVars = $state<Record<string, string | number> | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
let running = $state(false);
|
|
||||||
// Generation of the latest run(): a stale in-flight chain (params changed
|
|
||||||
// while the worker was busy) must not overwrite newer results.
|
|
||||||
let runGen = 0;
|
|
||||||
let displayError = $state<DisplayError | null>(null);
|
|
||||||
let sourceWarnings = $state<ChainWarning[]>([]);
|
let sourceWarnings = $state<ChainWarning[]>([]);
|
||||||
let runWarnings = $state<ChainWarning[]>([]);
|
|
||||||
const allWarnings = $derived([...sourceWarnings, ...runWarnings]);
|
|
||||||
let initedFor = $state("");
|
let initedFor = $state("");
|
||||||
let format = $state<OutputMime>("image/png");
|
let format = $state<OutputMime>("image/png");
|
||||||
// Lossy-format quality for tools without a quality param in the schema.
|
// Lossy-format quality for tools without a quality param in the schema.
|
||||||
@@ -154,6 +121,31 @@
|
|||||||
});
|
});
|
||||||
let dragFrom = $state<number | null>(null);
|
let dragFrom = $state<number | null>(null);
|
||||||
|
|
||||||
|
const runner = createSchemaToolRunner({
|
||||||
|
pageSlug: () => page.slug,
|
||||||
|
schema: () => schema,
|
||||||
|
steps: () => steps,
|
||||||
|
source: () => source,
|
||||||
|
sourceFiles: () => sourceFiles,
|
||||||
|
textSource: () => textSource,
|
||||||
|
format: () => format,
|
||||||
|
quality: () => currentQuality,
|
||||||
|
limitKb: () => limitKb,
|
||||||
|
lastTool: () => lastTool,
|
||||||
|
});
|
||||||
|
const { run, download, copyText, downloadText } = runner;
|
||||||
|
const running = $derived(runner.running);
|
||||||
|
const result = $derived(runner.result);
|
||||||
|
const fileResult = $derived(runner.fileResult);
|
||||||
|
const textResult = $derived(runner.textResult);
|
||||||
|
const verdictVars = $derived(runner.verdictVars);
|
||||||
|
const stepDims = $derived(runner.stepDims);
|
||||||
|
const stepResults = $derived(runner.stepResults);
|
||||||
|
const stepFileSets = $derived(runner.stepFileSets);
|
||||||
|
const runWarnings = $derived(runner.runWarnings);
|
||||||
|
const displayError = $derived(runner.displayError);
|
||||||
|
const allWarnings = $derived([...sourceWarnings, ...runWarnings]);
|
||||||
|
|
||||||
const lastTool = $derived(
|
const lastTool = $derived(
|
||||||
(steps.length > 0 ? getTool(steps[steps.length - 1].id) : undefined) ??
|
(steps.length > 0 ? getTool(steps[steps.length - 1].id) : undefined) ??
|
||||||
tool,
|
tool,
|
||||||
@@ -237,7 +229,7 @@
|
|||||||
if (host === initedFor) return;
|
if (host === initedFor) return;
|
||||||
initedFor = host;
|
initedFor = host;
|
||||||
// Invalidate any in-flight run from the previous host.
|
// Invalidate any in-flight run from the previous host.
|
||||||
runGen++;
|
runner.resetForHost();
|
||||||
if (chainId) {
|
if (chainId) {
|
||||||
const handoff = takeHandoff(chainId);
|
const handoff = takeHandoff(chainId);
|
||||||
steps = getChain(chainId)?.steps ?? createChain(page);
|
steps = getChain(chainId)?.steps ?? createChain(page);
|
||||||
@@ -260,13 +252,6 @@
|
|||||||
textSource = "";
|
textSource = "";
|
||||||
}
|
}
|
||||||
sourceWarnings = [];
|
sourceWarnings = [];
|
||||||
runWarnings = [];
|
|
||||||
stepFileSets = [];
|
|
||||||
result = null;
|
|
||||||
fileResult = null;
|
|
||||||
textResult = null;
|
|
||||||
verdictVars = undefined;
|
|
||||||
displayError = null;
|
|
||||||
maskOnKeys.clear();
|
maskOnKeys.clear();
|
||||||
// Marks are keyed by step.key from the previous host; keep them and
|
// Marks are keyed by step.key from the previous host; keep them and
|
||||||
// stale keys leak into the new chain's fields.
|
// stale keys leak into the new chain's fields.
|
||||||
@@ -332,11 +317,7 @@
|
|||||||
? defaultSchemaParams(stepSchema, { source: dims })
|
? defaultSchemaParams(stepSchema, { source: dims })
|
||||||
: {};
|
: {};
|
||||||
steps = steps.with(index, { ...step, params });
|
steps = steps.with(index, { ...step, params });
|
||||||
result = null;
|
runner.clearResults();
|
||||||
fileResult = null;
|
|
||||||
textResult = null;
|
|
||||||
verdictVars = undefined;
|
|
||||||
displayError = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addStepAt(index: number, toolId: string) {
|
function addStepAt(index: number, toolId: string) {
|
||||||
@@ -374,7 +355,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleFiles(files: File[]) {
|
async function handleFiles(files: File[]) {
|
||||||
displayError = null;
|
runner.displayError = null;
|
||||||
sourceWarnings = [];
|
sourceWarnings = [];
|
||||||
const decoded: ToolImageFile[] = [];
|
const decoded: ToolImageFile[] = [];
|
||||||
const usedNames = new SvelteSet<string>();
|
const usedNames = new SvelteSet<string>();
|
||||||
@@ -389,7 +370,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (decoded.length === 0) {
|
if (decoded.length === 0) {
|
||||||
displayError = toDisplayError(new ToolError("errors.imageDecode"));
|
runner.reportError(new ToolError("errors.imageDecode"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (skipped > 0) {
|
if (skipped > 0) {
|
||||||
@@ -421,120 +402,6 @@
|
|||||||
await handleFiles([file]);
|
await handleFiles([file]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assignResult(out: ToolResult) {
|
|
||||||
if (typeof out === "object" && out !== null && "files" in out) {
|
|
||||||
fileResult = out;
|
|
||||||
result = null;
|
|
||||||
textResult = null;
|
|
||||||
verdictVars = undefined;
|
|
||||||
} else if (typeof out === "object" && out !== null && "data" in out) {
|
|
||||||
result = out as PixelImage;
|
|
||||||
fileResult = null;
|
|
||||||
textResult = null;
|
|
||||||
verdictVars = undefined;
|
|
||||||
} else {
|
|
||||||
if (typeof out === "string") {
|
|
||||||
textResult = out;
|
|
||||||
verdictVars = undefined;
|
|
||||||
} else if (out && "key" in out) {
|
|
||||||
textResult = out.key;
|
|
||||||
verdictVars = out.vars;
|
|
||||||
} else {
|
|
||||||
textResult = null;
|
|
||||||
verdictVars = undefined;
|
|
||||||
}
|
|
||||||
result = null;
|
|
||||||
fileResult = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
if (!schema || steps.length === 0) return;
|
|
||||||
const gen = ++runGen;
|
|
||||||
displayError = null;
|
|
||||||
running = true;
|
|
||||||
try {
|
|
||||||
const input =
|
|
||||||
sourceFiles.length > 0
|
|
||||||
? sourceFiles
|
|
||||||
: source
|
|
||||||
? [{ name: `${baseName(page.slug)}.png`, image: source }]
|
|
||||||
: [];
|
|
||||||
const r = await runChain(steps, input, textSource || undefined, execute);
|
|
||||||
if (gen !== runGen) return;
|
|
||||||
stepDims = r.stepDims;
|
|
||||||
stepResults = r.stepResults;
|
|
||||||
stepFileSets = r.stepFileSets;
|
|
||||||
runWarnings = r.warnings;
|
|
||||||
assignResult(r.out);
|
|
||||||
} catch (e) {
|
|
||||||
if (gen !== runGen) return;
|
|
||||||
if (e instanceof ChainStepError) {
|
|
||||||
const base = toDisplayError(e.cause);
|
|
||||||
const msg = base.kind === "i18n" ? t(base.key, base.vars) : base.text;
|
|
||||||
displayError = {
|
|
||||||
kind: "plain",
|
|
||||||
text: t("toolPage.stepError", {
|
|
||||||
n: e.stepIndex + 1,
|
|
||||||
title: stepTitle(e.toolId),
|
|
||||||
msg,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
displayError = toDisplayError(e);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (gen === runGen) running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function download() {
|
|
||||||
if (!schema) return;
|
|
||||||
running = true;
|
|
||||||
displayError = null;
|
|
||||||
try {
|
|
||||||
if (fileResult) {
|
|
||||||
await downloadZip(fileResult.files, `${page.slug}.zip`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!result) return;
|
|
||||||
const out = outputFormatByMime(format);
|
|
||||||
const quality =
|
|
||||||
currentQuality !== undefined ? currentQuality / 100 : undefined;
|
|
||||||
const blob = limitKb
|
|
||||||
? await fitWithinBytes(result, out.mime, limitKb * 1024, quality)
|
|
||||||
: await encode(result, out.mime, quality);
|
|
||||||
downloadBlob(blob, `${page.slug}.${out.ext}`);
|
|
||||||
} catch (e) {
|
|
||||||
displayError = toDisplayError(e);
|
|
||||||
} finally {
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyText() {
|
|
||||||
if (!textResult) return;
|
|
||||||
const copyValue =
|
|
||||||
resultKind === "verdict"
|
|
||||||
? verdictText(lastTool.id, textResult, verdictVars)
|
|
||||||
: textResult;
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(copyValue);
|
|
||||||
} catch (e) {
|
|
||||||
displayError = toDisplayError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadText() {
|
|
||||||
if (!textResult) return;
|
|
||||||
try {
|
|
||||||
const blob = new Blob([textResult], { type: "text/plain" });
|
|
||||||
downloadBlob(blob, `${page.slug}.txt`);
|
|
||||||
} catch (e) {
|
|
||||||
displayError = toDisplayError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
@@ -608,14 +475,6 @@
|
|||||||
if (step) toggleStepMask(step.key);
|
if (step) toggleStepMask(step.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDisplayError(e: unknown): DisplayError {
|
|
||||||
if (e instanceof ToolError) {
|
|
||||||
return { kind: "i18n", key: e.key, vars: e.vars };
|
|
||||||
}
|
|
||||||
if (e instanceof Error) return { kind: "plain", text: e.message };
|
|
||||||
return { kind: "plain", text: String(e) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function warningText(w: ChainWarning): string {
|
function warningText(w: ChainWarning): string {
|
||||||
switch (w.kind) {
|
switch (w.kind) {
|
||||||
case "partial":
|
case "partial":
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
import { ToolError, type ErrorVars } from "$lib/core/errors";
|
||||||
|
import {
|
||||||
|
downloadBlob,
|
||||||
|
encode,
|
||||||
|
fitWithinBytes,
|
||||||
|
outputFormatByMime,
|
||||||
|
type OutputMime,
|
||||||
|
} from "$lib/core/io";
|
||||||
|
import type { PixelImage } from "$lib/core/types";
|
||||||
|
import { execute } from "$lib/executor";
|
||||||
|
import { chainStepTitle, verdictText } from "$lib/i18n/schema-tool-strings";
|
||||||
|
import { t } from "$lib/i18n/t";
|
||||||
|
import type { ChainStep } from "$lib/pipeline.svelte";
|
||||||
|
import type {
|
||||||
|
FileResult,
|
||||||
|
Tool,
|
||||||
|
ToolImageFile,
|
||||||
|
ToolResult,
|
||||||
|
} from "$lib/registry";
|
||||||
|
import type { Dimension, ToolSchema } from "$lib/registry-schema";
|
||||||
|
import {
|
||||||
|
baseName,
|
||||||
|
ChainStepError,
|
||||||
|
runChain,
|
||||||
|
type ChainWarning,
|
||||||
|
} from "$lib/run-chain";
|
||||||
|
import { downloadZip } from "$lib/zip";
|
||||||
|
|
||||||
|
export type DisplayError =
|
||||||
|
| { kind: "i18n"; key: string; vars?: ErrorVars }
|
||||||
|
| { kind: "plain"; text: string };
|
||||||
|
|
||||||
|
export interface SchemaToolRunnerCtx {
|
||||||
|
pageSlug: () => string;
|
||||||
|
schema: () => ToolSchema<Record<string, unknown>> | undefined;
|
||||||
|
steps: () => ChainStep[];
|
||||||
|
source: () => PixelImage | null;
|
||||||
|
sourceFiles: () => ToolImageFile[];
|
||||||
|
textSource: () => string;
|
||||||
|
format: () => OutputMime;
|
||||||
|
quality: () => number | undefined;
|
||||||
|
limitKb: () => number | undefined;
|
||||||
|
lastTool: () => Tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunnerState {
|
||||||
|
running: boolean;
|
||||||
|
// Generation of the latest run(): a stale in-flight chain (params changed
|
||||||
|
// while the worker was busy) must not overwrite newer results.
|
||||||
|
runGen: number;
|
||||||
|
result: PixelImage | null;
|
||||||
|
fileResult: FileResult | null;
|
||||||
|
textResult: string | null;
|
||||||
|
verdictVars: Record<string, string | number> | undefined;
|
||||||
|
stepDims: (Dimension | undefined)[];
|
||||||
|
stepResults: (PixelImage | null)[];
|
||||||
|
// Full named set per step (batch/fan-out); intermediate tiles show a grid.
|
||||||
|
stepFileSets: ToolImageFile[][];
|
||||||
|
runWarnings: ChainWarning[];
|
||||||
|
displayError: DisplayError | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDisplayError(e: unknown): DisplayError {
|
||||||
|
if (e instanceof ToolError) {
|
||||||
|
return { kind: "i18n", key: e.key, vars: e.vars };
|
||||||
|
}
|
||||||
|
if (e instanceof Error) return { kind: "plain", text: e.message };
|
||||||
|
return { kind: "plain", text: String(e) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignResult(s: RunnerState, out: ToolResult) {
|
||||||
|
if (typeof out === "object" && out !== null && "files" in out) {
|
||||||
|
s.fileResult = out;
|
||||||
|
s.result = null;
|
||||||
|
s.textResult = null;
|
||||||
|
s.verdictVars = undefined;
|
||||||
|
} else if (typeof out === "object" && out !== null && "data" in out) {
|
||||||
|
s.result = out as PixelImage;
|
||||||
|
s.fileResult = null;
|
||||||
|
s.textResult = null;
|
||||||
|
s.verdictVars = undefined;
|
||||||
|
} else {
|
||||||
|
if (typeof out === "string") {
|
||||||
|
s.textResult = out;
|
||||||
|
s.verdictVars = undefined;
|
||||||
|
} else if (out && "key" in out) {
|
||||||
|
s.textResult = out.key;
|
||||||
|
s.verdictVars = out.vars;
|
||||||
|
} else {
|
||||||
|
s.textResult = null;
|
||||||
|
s.verdictVars = undefined;
|
||||||
|
}
|
||||||
|
s.result = null;
|
||||||
|
s.fileResult = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(s: RunnerState, ctx: SchemaToolRunnerCtx) {
|
||||||
|
const schema = ctx.schema();
|
||||||
|
const steps = ctx.steps();
|
||||||
|
if (!schema || steps.length === 0) return;
|
||||||
|
const gen = ++s.runGen;
|
||||||
|
s.displayError = null;
|
||||||
|
s.running = true;
|
||||||
|
try {
|
||||||
|
const source = ctx.source();
|
||||||
|
const sourceFiles = ctx.sourceFiles();
|
||||||
|
const textSource = ctx.textSource();
|
||||||
|
const input =
|
||||||
|
sourceFiles.length > 0
|
||||||
|
? sourceFiles
|
||||||
|
: source
|
||||||
|
? [{ name: `${baseName(ctx.pageSlug())}.png`, image: source }]
|
||||||
|
: [];
|
||||||
|
const r = await runChain(steps, input, textSource || undefined, execute);
|
||||||
|
if (gen !== s.runGen) return;
|
||||||
|
s.stepDims = r.stepDims;
|
||||||
|
s.stepResults = r.stepResults;
|
||||||
|
s.stepFileSets = r.stepFileSets;
|
||||||
|
s.runWarnings = r.warnings;
|
||||||
|
assignResult(s, r.out);
|
||||||
|
} catch (e) {
|
||||||
|
if (gen !== s.runGen) return;
|
||||||
|
if (e instanceof ChainStepError) {
|
||||||
|
const base = toDisplayError(e.cause);
|
||||||
|
const msg = base.kind === "i18n" ? t(base.key, base.vars) : base.text;
|
||||||
|
s.displayError = {
|
||||||
|
kind: "plain",
|
||||||
|
text: t("toolPage.stepError", {
|
||||||
|
n: e.stepIndex + 1,
|
||||||
|
title: chainStepTitle(e.toolId),
|
||||||
|
msg,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
s.displayError = toDisplayError(e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (gen === s.runGen) s.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download(s: RunnerState, ctx: SchemaToolRunnerCtx) {
|
||||||
|
if (!ctx.schema()) return;
|
||||||
|
s.running = true;
|
||||||
|
s.displayError = null;
|
||||||
|
try {
|
||||||
|
if (s.fileResult) {
|
||||||
|
await downloadZip(s.fileResult.files, `${ctx.pageSlug()}.zip`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!s.result) return;
|
||||||
|
const out = outputFormatByMime(ctx.format());
|
||||||
|
const q = ctx.quality();
|
||||||
|
const limit = ctx.limitKb();
|
||||||
|
const quality = q !== undefined ? q / 100 : undefined;
|
||||||
|
const blob = limit
|
||||||
|
? await fitWithinBytes(s.result, out.mime, limit * 1024, quality)
|
||||||
|
: await encode(s.result, out.mime, quality);
|
||||||
|
downloadBlob(blob, `${ctx.pageSlug()}.${out.ext}`);
|
||||||
|
} catch (e) {
|
||||||
|
s.displayError = toDisplayError(e);
|
||||||
|
} finally {
|
||||||
|
s.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyText(s: RunnerState, ctx: SchemaToolRunnerCtx) {
|
||||||
|
if (!s.textResult) return;
|
||||||
|
const tool = ctx.lastTool();
|
||||||
|
const copyValue =
|
||||||
|
(tool.result ?? "image") === "verdict"
|
||||||
|
? verdictText(tool.id, s.textResult, s.verdictVars)
|
||||||
|
: s.textResult;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(copyValue);
|
||||||
|
} catch (e) {
|
||||||
|
s.displayError = toDisplayError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadText(s: RunnerState, ctx: SchemaToolRunnerCtx) {
|
||||||
|
if (!s.textResult) return;
|
||||||
|
try {
|
||||||
|
const blob = new Blob([s.textResult], { type: "text/plain" });
|
||||||
|
downloadBlob(blob, `${ctx.pageSlug()}.txt`);
|
||||||
|
} catch (e) {
|
||||||
|
s.displayError = toDisplayError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearResults(s: RunnerState) {
|
||||||
|
s.result = null;
|
||||||
|
s.fileResult = null;
|
||||||
|
s.textResult = null;
|
||||||
|
s.verdictVars = undefined;
|
||||||
|
s.displayError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execution state and actions of SchemaToolView: run/download/clipboard and
|
||||||
|
// all results. Owns the generation counter (see RunnerState.runGen).
|
||||||
|
export function createSchemaToolRunner(ctx: SchemaToolRunnerCtx) {
|
||||||
|
const s = $state<RunnerState>({
|
||||||
|
running: false,
|
||||||
|
runGen: 0,
|
||||||
|
result: null,
|
||||||
|
fileResult: null,
|
||||||
|
textResult: null,
|
||||||
|
verdictVars: undefined,
|
||||||
|
stepDims: [],
|
||||||
|
stepResults: [],
|
||||||
|
stepFileSets: [],
|
||||||
|
runWarnings: [],
|
||||||
|
displayError: null,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
get running() {
|
||||||
|
return s.running;
|
||||||
|
},
|
||||||
|
get result() {
|
||||||
|
return s.result;
|
||||||
|
},
|
||||||
|
get fileResult() {
|
||||||
|
return s.fileResult;
|
||||||
|
},
|
||||||
|
get textResult() {
|
||||||
|
return s.textResult;
|
||||||
|
},
|
||||||
|
get verdictVars() {
|
||||||
|
return s.verdictVars;
|
||||||
|
},
|
||||||
|
get stepDims() {
|
||||||
|
return s.stepDims;
|
||||||
|
},
|
||||||
|
get stepResults() {
|
||||||
|
return s.stepResults;
|
||||||
|
},
|
||||||
|
get stepFileSets() {
|
||||||
|
return s.stepFileSets;
|
||||||
|
},
|
||||||
|
get runWarnings() {
|
||||||
|
return s.runWarnings;
|
||||||
|
},
|
||||||
|
get displayError() {
|
||||||
|
return s.displayError;
|
||||||
|
},
|
||||||
|
set displayError(v: DisplayError | null) {
|
||||||
|
s.displayError = v;
|
||||||
|
},
|
||||||
|
run: () => run(s, ctx),
|
||||||
|
download: () => download(s, ctx),
|
||||||
|
copyText: () => copyText(s, ctx),
|
||||||
|
downloadText: () => downloadText(s, ctx),
|
||||||
|
// Host changed (another page/chain): drop run state and invalidate any
|
||||||
|
// in-flight run from the previous host.
|
||||||
|
resetForHost: () => {
|
||||||
|
s.runGen++;
|
||||||
|
s.runWarnings = [];
|
||||||
|
s.stepFileSets = [];
|
||||||
|
clearResults(s);
|
||||||
|
},
|
||||||
|
clearResults: () => clearResults(s),
|
||||||
|
reportError: (e: unknown) => {
|
||||||
|
s.displayError = toDisplayError(e);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user