refactor: separate tool page

This commit is contained in:
2026-08-22 20:52:03 +05:00
parent dcc7c346c4
commit 5560435b55
4 changed files with 213 additions and 101 deletions
+24 -98
View File
@@ -2,15 +2,10 @@
import { imageInfo, type ImageInfo } from '$lib/core/analyze'; import { imageInfo, type ImageInfo } from '$lib/core/analyze';
import { decodeFile, isSupportedImage, unsupportedImageMessage } from '$lib/core/io'; import { decodeFile, isSupportedImage, unsupportedImageMessage } from '$lib/core/io';
import type { PixelImage } from '$lib/core/types'; import type { PixelImage } from '$lib/core/types';
import { defaultParams, outputOf, sanitizeParams, type ToolEntry } from '$lib/registry'; import { defaultParams, sanitizeParams, type ToolEntry } from '$lib/registry';
import Button from './ui/Button.svelte'; import ParamsCard from './tool/ParamsCard.svelte';
import DownloadButton from './DownloadButton.svelte'; import ResultCard from './tool/ResultCard.svelte';
import DropOverlay from './DropOverlay.svelte'; import SourceCard from './tool/SourceCard.svelte';
import DropZone from './DropZone.svelte';
import EmptyState from './ui/EmptyState.svelte';
import InfoPanel from './InfoPanel.svelte';
import ParamForm from './ParamForm.svelte';
import Preview from './Preview.svelte';
let { tool }: { tool: ToolEntry } = $props(); let { tool }: { tool: ToolEntry } = $props();
@@ -105,71 +100,30 @@
{/if} {/if}
<div class="stage panel"> <div class="stage panel">
<div class="side"> <div class="cell">
<h2 class="heading-section">Исходник</h2> <SourceCard
{#if !source} {source}
<DropZone onFile={handleFile} onError={(message) => (errorText = message)} /> onFile={handleFile}
{:else} onError={(message) => (errorText = message)}
<DropOverlay onFile={handleFile} onError={(message) => (errorText = message)}> onReset={reset}
<div class="media">
<Preview image={source} />
</div>
<Button variant="secondary" onclick={reset}>Заменить изображение</Button>
</DropOverlay>
{/if}
</div>
<div class="side">
<h2 class="heading-section">{isInfo ? 'Сводка' : 'Результат'}</h2>
{#if !source}
<div class="media">
<EmptyState
title="Результат появится здесь"
hint="Сначала загрузите исходное изображение слева"
/> />
</div> </div>
{:else if !isInfo && status === 'processing' && !result} <div class="cell">
<div class="media"> <ResultCard
<EmptyState {tool}
title="Обработка…" sourceLoaded={!!source}
hint="Изображение обрабатывается, это займёт немного времени" {status}
/> {result}
</div> {info}
{:else if isInfo} {isInfo}
<div class="media">
{#if info}
<InfoPanel {info} />
{/if}
</div>
{:else}
<div class="media">
<Preview image={result} />
</div>
<DownloadButton
image={result}
format={outputOf(tool)}
baseName={tool.id}
params={sanitized} params={sanitized}
onError={showError} onDownloadError={showError}
/> />
{/if}
</div> </div>
</div> </div>
{#if source && !isInfo} {#if source && !isInfo}
<div class="params-card panel"> <ParamsCard params={tool.params} bind:values busy={status === 'processing'} onApply={apply} />
<h2 class="heading-section">Параметры</h2>
{#if tool.params.length > 0}
<ParamForm params={tool.params} bind:values />
<Button onclick={apply} busy={status === 'processing'} busyText="Обработка…" fullWidth>
Применить
</Button>
{:else}
<p class="hint text-caption text-muted">
У этого инструмента нет параметров — результат уже готов.
</p>
{/if}
</div>
{/if} {/if}
</section> </section>
@@ -194,54 +148,26 @@
padding: var(--space-4); padding: var(--space-4);
} }
.side { .cell {
display: flex;
flex-direction: column;
gap: var(--space-2);
min-width: 0; min-width: 0;
display: flex;
} }
.side + .side { .cell + .cell {
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
padding-left: var(--space-4); padding-left: var(--space-4);
} }
.side h2 {
margin-bottom: var(--space-1);
}
.media {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 16rem;
}
.params-card {
margin-top: var(--space-4);
padding: var(--space-4);
}
@media (max-width: 48rem) { @media (max-width: 48rem) {
.stage { .stage {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.side + .side { .cell + .cell {
border-left: none; border-left: none;
padding-left: 0; padding-left: 0;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
padding-top: var(--space-4); padding-top: var(--space-4);
} }
} }
.hint {
margin-bottom: var(--space-3);
}
.hint {
margin-bottom: var(--space-3);
}
</style> </style>
@@ -0,0 +1,41 @@
<script lang="ts">
import type { ParamDef } from '$lib/registry';
import ParamForm from '../ParamForm.svelte';
import Button from '../ui/Button.svelte';
interface Props {
params: ParamDef[];
values: Record<string, any>;
busy: boolean;
onApply: () => void;
}
let { params, values = $bindable(), busy, onApply }: Props = $props();
</script>
<div class="panel root">
<h2 class="heading-section">Параметры</h2>
{#if params.length > 0}
<ParamForm {params} bind:values />
<Button onclick={onApply} {busy} busyText="Обработка…" fullWidth>Применить</Button>
{:else}
<p class="hint text-caption text-muted">
У этого инструмента нет параметров — результат уже готов.
</p>
{/if}
</div>
<style>
.root {
margin-top: var(--space-4);
padding: var(--space-4);
}
h2 {
margin-bottom: var(--space-2);
}
.hint {
margin: 0;
}
</style>
@@ -0,0 +1,92 @@
<script lang="ts">
import type { ImageInfo } from '$lib/core/analyze';
import type { PixelImage } from '$lib/core/types';
import { outputOf, type ToolEntry } from '$lib/registry';
import DownloadButton from '../DownloadButton.svelte';
import InfoPanel from '../InfoPanel.svelte';
import Preview from '../Preview.svelte';
import EmptyState from '../ui/EmptyState.svelte';
type Status = 'idle' | 'loaded' | 'processing' | 'error';
interface Props {
tool: ToolEntry;
sourceLoaded: boolean;
status: Status;
result: PixelImage | null;
info: ImageInfo | null;
isInfo: boolean;
params: Record<string, unknown>;
onDownloadError: (e: unknown) => void;
}
let {
tool,
sourceLoaded,
status,
result,
info,
isInfo,
params,
onDownloadError
}: Props = $props();
</script>
<div class="container">
<h2 class="heading-section">{isInfo ? 'Сводка' : 'Результат'}</h2>
{#if !sourceLoaded}
<div class="media">
<EmptyState
title="Результат появится здесь"
hint="Сначала загрузите исходное изображение слева"
/>
</div>
{:else if !isInfo && status === 'processing' && !result}
<div class="media">
<EmptyState
title="Обработка…"
hint="Изображение обрабатывается, это займёт немного времени"
/>
</div>
{:else if isInfo}
<div class="media">
{#if info}
<InfoPanel {info} />
{/if}
</div>
{:else}
<div class="media">
<Preview image={result} />
</div>
<DownloadButton
image={result}
format={outputOf(tool)}
baseName={tool.id}
{params}
onError={onDownloadError}
/>
{/if}
</div>
<style>
.container {
display: flex;
flex-direction: column;
gap: var(--space-2);
min-width: 0;
height: 100%;
}
h2 {
margin-bottom: var(--space-1);
}
.media {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 16rem;
}
</style>
@@ -0,0 +1,53 @@
<script lang="ts">
import type { PixelImage } from '$lib/core/types';
import DropOverlay from '../DropOverlay.svelte';
import DropZone from '../DropZone.svelte';
import Preview from '../Preview.svelte';
import Button from '../ui/Button.svelte';
interface Props {
source: PixelImage | null;
onFile: (file: File) => void;
onError: (message: string) => void;
onReset: () => void;
}
let { source, onFile, onError, onReset }: Props = $props();
</script>
<div class="container">
<h2 class="heading-section">Исходник</h2>
{#if !source}
<DropZone {onFile} {onError} />
{:else}
<DropOverlay {onFile} {onError}>
<div class="media">
<Preview image={source} />
</div>
<Button variant="secondary" onclick={onReset}>Заменить изображение</Button>
</DropOverlay>
{/if}
</div>
<style>
.container {
display: flex;
flex-direction: column;
gap: var(--space-2);
min-width: 0;
height: 100%;
}
h2 {
margin-bottom: var(--space-1);
}
.media {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 16rem;
}
</style>