refactor: group props in view models

This commit is contained in:
2026-10-10 05:55:05 +05:00
parent 731f4ba90e
commit e51d6f3239
4 changed files with 143 additions and 150 deletions
+8 -6
View File
@@ -67,13 +67,15 @@ handleFiles, эффекты, workspace-разметка). Декомпозици
Переработка самого паттерна `$effect(() => init())` — часть H1, отдельно не
делалась.
### M3. `SchemaPreview.svelte` — интерфейс из ~40 пропсов
### M3. `SchemaPreview.svelte` — интерфейс из ~40 пропсов — СДЕЛАНО (2026-10)
`web/src/lib/components/schema/SchemaPreview.svelte:22-103`: весь стейт
оркестратора проброшен вниз по одному (`stepResults`, `stepMaskable`,
`stepMaskOn`, `stepMasks`, `format`, `quality`, `limitKb`, `alphaLoss` + 11
колбэков `on*`). Нужен группирующий view-model-объект или разбиение на
подкомпоненты.
Пропсы сгруппированы в 4 view-model объекта по внутреннему потребителю: `head`
(SchemaActions), `source` (SchemaSourceTile), `steps` (промежуточные тайлы),
`result` (SchemaResultTile). Интерфейсы — в `schema-preview-model.ts`
(`PreviewHeadModel` и др.); имена полей зеркалят пропсы потребителей, поэтому
внутри работает спред (`<SchemaActions {...head}>`). Плоскими остались 7
идентифицирующих пропсов (inputMode, resultKind, toolId, running, error,
aligned, ontogglealign).
### M4. `runMask` обходил санитизацию параметров — СДЕЛАНО (2026-10)
@@ -1,5 +1,11 @@
<script lang="ts">
import { hasPreviewResult } from "./schema-preview-model";
import {
hasPreviewResult,
type PreviewHeadModel,
type PreviewResultModel,
type PreviewSourceModel,
type PreviewStepsModel,
} from "./schema-preview-model";
import SchemaActions from "./SchemaActions.svelte";
import PartsGrid from "./PartsGrid.svelte";
import SchemaResultTile from "./SchemaResultTile.svelte";
@@ -9,101 +15,50 @@
import ChipButton from "$lib/components/ui/ChipButton.svelte";
import { toDataUrl } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types";
import type { OutputMime } from "$lib/core/io";
import { t } from "$lib/i18n/t";
import type {
FileResult,
InputMode,
ResultKind,
ResultNote,
ToolImageFile,
} from "$lib/registry";
import type { InputMode, ResultKind } from "$lib/registry";
interface Props {
inputMode: InputMode;
resultKind?: ResultKind;
toolId: string;
source: PixelImage | null;
sources?: ToolImageFile[];
fileCount?: number;
result: PixelImage | null;
resultNote?: ResultNote | null;
fileResult?: FileResult | null;
textSource?: string;
textResult?: string | null;
textVars?: Record<string, string | number>;
mask?: PixelImage | null;
maskOn?: boolean;
ontogglemask?: () => void;
running?: boolean;
error?: string;
stepResults?: (PixelImage | null)[];
stepFileSets?: ToolImageFile[][];
stepMaskable?: boolean[];
stepMaskOn?: boolean[];
stepMasks?: (PixelImage | null)[];
ontogglestepmask?: (i: number) => void;
aligned?: boolean;
ontogglealign?: () => void;
format?: OutputMime;
quality?: number;
limitKb?: number;
alphaLoss?: boolean;
onupload: (file: File) => void;
onuploadmany?: (files: File[]) => void;
ontextsource?: (text: string) => void;
onrendertext?: () => void;
oncopytext?: () => void;
ondownloadtxt?: () => void;
ondownload: () => void;
onformat?: (mime: OutputMime) => void;
onquality?: (value: number) => void;
onlimit?: (kb: number | undefined) => void;
head: PreviewHeadModel;
source: PreviewSourceModel;
steps?: PreviewStepsModel;
result: PreviewResultModel;
}
let {
inputMode,
resultKind = "image",
toolId,
source,
sources = [],
fileCount = 0,
result,
resultNote = null,
fileResult = null,
textSource = "",
textResult = null,
textVars = undefined,
mask = null,
maskOn = false,
ontogglemask,
running = false,
error = "",
stepResults = [],
stepFileSets = [],
stepMaskable = [],
stepMaskOn = [],
stepMasks = [],
ontogglestepmask,
aligned = false,
ontogglealign,
format = "image/png",
quality = undefined,
limitKb = undefined,
alphaLoss = false,
onupload,
onuploadmany,
ontextsource,
onrendertext,
oncopytext,
ondownloadtxt,
ondownload,
onformat,
onquality,
onlimit,
head,
source,
steps = {},
result,
}: Props = $props();
const stepResults = $derived(steps.results ?? []);
const stepFileSets = $derived(steps.fileSets ?? []);
const stepMaskable = $derived(steps.maskable ?? []);
const stepMaskOn = $derived(steps.maskOn ?? []);
const stepMasks = $derived(steps.masks ?? []);
const hasResult = $derived(
hasPreviewResult({ inputMode, resultKind, result, fileResult, textResult }),
hasPreviewResult({
inputMode,
resultKind,
result: result.result,
fileResult: result.fileResult ?? null,
textResult: result.textResult ?? null,
}),
);
</script>
@@ -124,15 +79,7 @@
{resultKind}
canDownload={hasResult}
{running}
{format}
{quality}
{limitKb}
{alphaLoss}
{onupload}
{ondownload}
{onformat}
{onquality}
{onlimit}
{...head}
/>
</div>
@@ -143,15 +90,9 @@
<div class="pair">
<SchemaSourceTile
mode={inputMode}
{source}
{sources}
{textSource}
{running}
{fileCount}
ontextinput={ontextsource}
{onrendertext}
{onupload}
{onuploadmany}
onupload={head.onupload}
{...source}
/>
{#each stepResults.slice(0, -1) as img, i (i)}
{@const set = stepFileSets[i] ?? []}
@@ -167,7 +108,7 @@
<ChipButton
label={t("resultCard.maskToggle")}
active={stepMaskOn[i] ?? false}
onclick={() => ontogglestepmask?.(i)}
onclick={() => steps.ontogglestepmask?.(i)}
/>
{/if}
{/snippet}
@@ -184,7 +125,7 @@
<ChipButton
label={t("resultCard.maskToggle")}
active={stepMaskOn[i] ?? false}
onclick={() => ontogglestepmask?.(i)}
onclick={() => steps.ontogglestepmask?.(i)}
/>
{/if}
{/snippet}
@@ -197,21 +138,7 @@
</PreviewTile>
{/if}
{/each}
<SchemaResultTile
{resultKind}
{result}
{resultNote}
{fileResult}
{textResult}
{textVars}
{toolId}
{running}
{mask}
{maskOn}
{ontogglemask}
oncopy={oncopytext}
{ondownloadtxt}
/>
<SchemaResultTile {resultKind} {toolId} {running} {...result} />
</div>
<style>
@@ -500,48 +500,54 @@
<section class="panel">
<SchemaPreview
toolId={lastTool.id}
{source}
sources={sourceFiles}
fileCount={sourceFiles.length}
{result}
{resultNote}
{fileResult}
{textSource}
{textResult}
textVars={verdictVars}
{inputMode}
resultKind={displayResultKind}
{running}
error={errorText}
{format}
quality={currentQuality}
{limitKb}
{alphaLoss}
{stepResults}
{stepFileSets}
{aligned}
{stepMaskable}
{stepMaskOn}
{stepMasks}
ontogglestepmask={toggleStepMaskByIndex}
mask={stepMasks.at(-1) ?? null}
maskOn={stepMaskOn.at(-1) ?? false}
ontogglemask={stepMaskable.at(-1)
? () => toggleStepMask(steps[steps.length - 1].key)
: undefined}
ontogglealign={() => (aligned = !aligned)}
onformat={(v) => (format = v)}
onquality={setFormatQuality}
onlimit={(v) => (limitKb = v)}
onupload={handleFile}
onuploadmany={handleFiles}
ontextsource={(textValue) => {
textSource = textValue;
head={{
format,
quality: currentQuality,
limitKb,
alphaLoss,
onupload: handleFile,
ondownload: download,
onformat: (v) => (format = v),
onquality: setFormatQuality,
onlimit: (v) => (limitKb = v),
}}
source={{
source,
sources: sourceFiles,
fileCount: sourceFiles.length,
textSource,
ontextinput: (v) => (textSource = v),
onrendertext: run,
onuploadmany: handleFiles,
}}
steps={{
results: stepResults,
fileSets: stepFileSets,
maskable: stepMaskable,
maskOn: stepMaskOn,
masks: stepMasks,
ontogglestepmask: toggleStepMaskByIndex,
}}
result={{
result,
resultNote,
fileResult,
textResult,
textVars: verdictVars,
mask: stepMasks.at(-1) ?? null,
maskOn: stepMaskOn.at(-1) ?? false,
ontogglemask: stepMaskable.at(-1)
? () => toggleStepMask(steps[steps.length - 1].key)
: undefined,
oncopy: copyText,
ondownloadtxt: downloadText,
}}
onrendertext={run}
oncopytext={copyText}
ondownloadtxt={downloadText}
ondownload={download}
/>
</section>
</div>
@@ -1,5 +1,12 @@
import type { OutputMime } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types";
import type { FileResult, InputMode, ResultKind } from "$lib/registry";
import type {
FileResult,
InputMode,
ResultKind,
ResultNote,
ToolImageFile,
} from "$lib/registry";
export type SchemaPreviewModelInput = {
inputMode: InputMode;
@@ -18,3 +25,54 @@ export function hasPreviewResult(input: SchemaPreviewModelInput): boolean {
}
return Boolean(textResult);
}
// Grouped view-models of SchemaPreview: one object per inner consumer
// (field names mirror the consumer's own props, so they spread directly).
/** SchemaActions: download policy and format settings. */
export interface PreviewHeadModel {
format: OutputMime;
quality?: number;
limitKb?: number;
alphaLoss?: boolean;
onupload: (file: File) => void;
ondownload: () => void;
onformat?: (mime: OutputMime) => void;
onquality?: (value: number) => void;
onlimit?: (kb: number | undefined) => void;
}
/** SchemaSourceTile minus mode/running/onupload (top-level/head props). */
export interface PreviewSourceModel {
source: PixelImage | null;
sources?: ToolImageFile[];
fileCount?: number;
textSource?: string;
ontextinput?: (text: string) => void;
onrendertext?: () => void;
onuploadmany?: (files: File[]) => void;
}
/** Intermediate step tiles (chain mode). */
export interface PreviewStepsModel {
results?: (PixelImage | null)[];
fileSets?: ToolImageFile[][];
maskable?: boolean[];
maskOn?: boolean[];
masks?: (PixelImage | null)[];
ontogglestepmask?: (i: number) => void;
}
/** SchemaResultTile minus resultKind/toolId/running (top-level props). */
export interface PreviewResultModel {
result: PixelImage | null;
resultNote?: ResultNote | null;
fileResult?: FileResult | null;
textResult?: string | null;
textVars?: Record<string, string | number>;
mask?: PixelImage | null;
maskOn?: boolean;
ontogglemask?: () => void;
oncopy?: () => void;
ondownloadtxt?: () => void;
}