mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-14 17:38:19 +00:00
253 lines
6.1 KiB
Svelte
253 lines
6.1 KiB
Svelte
<script lang="ts">
|
|
import RangeControl from "$lib/components/fields/schema/RangeControl.svelte";
|
|
import Icon from "$lib/components/ui/Icon.svelte";
|
|
import {
|
|
OUTPUT_FORMATS,
|
|
outputFormatByMime,
|
|
type OutputMime,
|
|
} from "$lib/core/io";
|
|
import type { SliderSpec } from "$lib/registry-schema";
|
|
import { t } from "$lib/i18n/t";
|
|
import { ChevronDown, Download } from "@lucide/svelte";
|
|
|
|
interface Props {
|
|
format: OutputMime;
|
|
quality?: number;
|
|
limitKb?: number;
|
|
running: boolean;
|
|
ondownload: () => void;
|
|
onformat: (mime: OutputMime) => void;
|
|
onquality: (value: number) => void;
|
|
onlimit: (kb: number | undefined) => void;
|
|
}
|
|
let {
|
|
format,
|
|
quality,
|
|
limitKb,
|
|
running,
|
|
ondownload,
|
|
onformat,
|
|
onquality,
|
|
onlimit,
|
|
}: Props = $props();
|
|
|
|
const current = $derived(outputFormatByMime(format));
|
|
const qualitySetting = $derived(current.settings?.quality);
|
|
const qualitySpec = $derived(
|
|
qualitySetting
|
|
? ({
|
|
kind: "slider",
|
|
min: qualitySetting.min,
|
|
max: qualitySetting.max,
|
|
step: qualitySetting.step,
|
|
default: qualitySetting.default,
|
|
} satisfies SliderSpec)
|
|
: undefined,
|
|
);
|
|
|
|
let open = $state(false);
|
|
let root = $state<HTMLDivElement | null>(null);
|
|
|
|
function choose(mime: OutputMime) {
|
|
// Menu doubles as a format-settings panel: keep it open on select
|
|
// so quality can be tuned right away.
|
|
onformat(mime);
|
|
}
|
|
|
|
function handleLimit(e: Event) {
|
|
const raw = (e.target as HTMLInputElement).valueAsNumber;
|
|
onlimit(Number.isFinite(raw) && raw >= 1 ? Math.round(raw) : undefined);
|
|
}
|
|
|
|
$effect(() => {
|
|
if (!open) return;
|
|
function onPointerDown(e: PointerEvent) {
|
|
if (!root?.contains(e.target as Node)) open = false;
|
|
}
|
|
function onKey(e: KeyboardEvent) {
|
|
if (e.key === "Escape") open = false;
|
|
}
|
|
window.addEventListener("pointerdown", onPointerDown);
|
|
window.addEventListener("keydown", onKey);
|
|
return () => {
|
|
window.removeEventListener("pointerdown", onPointerDown);
|
|
window.removeEventListener("keydown", onKey);
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<div class="download-split" bind:this={root}>
|
|
<button
|
|
type="button"
|
|
class="main"
|
|
disabled={running}
|
|
onclick={() => {
|
|
open = false;
|
|
ondownload();
|
|
}}
|
|
>
|
|
<Icon icon={Download} size={14} />
|
|
<span>{t("actions.download")}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="toggle"
|
|
aria-haspopup="menu"
|
|
aria-expanded={open}
|
|
aria-label={t("actions.formatAria")}
|
|
disabled={running}
|
|
onclick={() => (open = !open)}
|
|
>
|
|
<span>{current.label}</span>
|
|
<Icon icon={ChevronDown} size={14} />
|
|
</button>
|
|
{#if open}
|
|
<div class="menu" role="menu">
|
|
{#each OUTPUT_FORMATS as f (f.mime)}
|
|
<button
|
|
type="button"
|
|
class="item"
|
|
class:selected={f.mime === format}
|
|
role="menuitemradio"
|
|
aria-checked={f.mime === format}
|
|
onclick={() => choose(f.mime)}
|
|
>
|
|
{f.label}
|
|
</button>
|
|
{/each}
|
|
{#if qualitySpec && quality !== undefined}
|
|
<div class="quality">
|
|
<RangeControl
|
|
label={t("fields.quality")}
|
|
value={quality}
|
|
spec={qualitySpec}
|
|
onchange={(q) => onquality(q)}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
<div class="limit">
|
|
<label class="limit-field">
|
|
<span>{t("actions.sizeLimitKb")}</span>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
step="1"
|
|
placeholder="∞"
|
|
value={limitKb ?? ""}
|
|
oninput={handleLimit}
|
|
/>
|
|
</label>
|
|
{#if limitKb !== undefined && format === "image/bmp"}
|
|
<p class="limit-warn" role="status">
|
|
{t("actions.sizeLimitBmp")}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.download-split {
|
|
position: relative;
|
|
display: inline-flex;
|
|
}
|
|
.main,
|
|
.toggle {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-m);
|
|
padding: var(--space-m) var(--space-xl);
|
|
border: var(--size-border-thick) solid var(--color-main);
|
|
background: var(--color-main);
|
|
color: var(--color-background);
|
|
font-size: var(--font-size-l);
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
}
|
|
.main {
|
|
border-right: none;
|
|
border-radius: var(--radius-s) 0 0 var(--radius-s);
|
|
}
|
|
.toggle {
|
|
border-radius: 0 var(--radius-s) var(--radius-s) 0;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--font-size-s);
|
|
font-weight: normal;
|
|
}
|
|
.main:disabled,
|
|
.toggle:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
.menu {
|
|
position: absolute;
|
|
top: calc(100% + var(--space-s));
|
|
right: 0;
|
|
z-index: var(--z-dropdown);
|
|
/* Ширина под строку качества: числовое поле + степперы + сброс. */
|
|
min-width: calc(var(--space-xxxl) * 9);
|
|
display: grid;
|
|
gap: var(--space-s);
|
|
padding: var(--space-m);
|
|
border: var(--size-border) solid var(--color-border);
|
|
border-radius: var(--radius-m);
|
|
background: var(--color-panel);
|
|
}
|
|
.item {
|
|
padding: var(--space-m) var(--space-l);
|
|
border: var(--size-border) solid transparent;
|
|
border-radius: var(--radius-s);
|
|
background: transparent;
|
|
color: var(--color-text-muted);
|
|
font: var(--font-size-s) var(--font-mono);
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.item:hover {
|
|
color: var(--color-text);
|
|
border-color: var(--color-border);
|
|
}
|
|
.item.selected {
|
|
color: var(--color-main);
|
|
border-color: var(--color-main);
|
|
}
|
|
.quality {
|
|
padding-top: var(--space-m);
|
|
border-top: var(--size-border) solid var(--color-border);
|
|
}
|
|
.limit {
|
|
padding-top: var(--space-m);
|
|
border-top: var(--size-border) solid var(--color-border);
|
|
}
|
|
.limit-field {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--space-m);
|
|
color: var(--color-text-muted);
|
|
font: var(--font-size-s) var(--font-mono);
|
|
}
|
|
.limit-field input {
|
|
width: calc(var(--space-xxxl) * 3);
|
|
padding: var(--space-m) var(--space-l);
|
|
background: var(--color-background);
|
|
border: var(--size-border) solid var(--color-border);
|
|
border-radius: var(--radius-s);
|
|
color: var(--color-text);
|
|
font: var(--font-size-m) var(--font-mono);
|
|
text-align: center;
|
|
appearance: textfield;
|
|
}
|
|
.limit-field input::-webkit-outer-spin-button,
|
|
.limit-field input::-webkit-inner-spin-button {
|
|
appearance: none;
|
|
margin: 0;
|
|
}
|
|
.limit-warn {
|
|
margin: var(--space-m) 0 0;
|
|
color: var(--color-warning);
|
|
font: var(--font-size-s) var(--font-mono);
|
|
}
|
|
</style>
|