mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
refactor: separate tool page
This commit is contained in:
@@ -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>
|
||||||
|
<div class="cell">
|
||||||
<div class="side">
|
<ResultCard
|
||||||
<h2 class="heading-section">{isInfo ? 'Сводка' : 'Результат'}</h2>
|
{tool}
|
||||||
{#if !source}
|
sourceLoaded={!!source}
|
||||||
<div class="media">
|
{status}
|
||||||
<EmptyState
|
{result}
|
||||||
title="Результат появится здесь"
|
{info}
|
||||||
hint="Сначала загрузите исходное изображение слева"
|
{isInfo}
|
||||||
/>
|
params={sanitized}
|
||||||
</div>
|
onDownloadError={showError}
|
||||||
{: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={sanitized}
|
|
||||||
onError={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>
|
||||||
Reference in New Issue
Block a user