mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
feat: touched fields dont reset on change source image
This commit is contained in:
+2
-6
@@ -6,13 +6,9 @@
|
|||||||
|
|
||||||
## UX — приоритетная очередь
|
## UX — приоритетная очередь
|
||||||
|
|
||||||
Порядок согласован: A (сброс параметров) → C (контролы полей) → B (загрузка
|
Порядок согласован: C (контролы полей) → B (загрузка исходника) → E (download) →
|
||||||
исходника) → E (download) → D (pipeline). Делать по одной, сверху вниз.
|
D (pipeline). Делать по одной, сверху вниз.
|
||||||
|
|
||||||
- [ ] **A. Сброс параметров при загрузке нового изображения** — crop, resize (но
|
|
||||||
скорее всего все инструменты): при загрузке нового файла сбрасываются
|
|
||||||
размеры, настроенные пользователем. Решить, что сбрасывать (размеры под
|
|
||||||
новый исходник), а что сохранять (толерансы, цвета, форматы)
|
|
||||||
- [ ] **C1. range slider field** — добавить поле для ввода цифр и кнопку сброса
|
- [ ] **C1. range slider field** — добавить поле для ввода цифр и кнопку сброса
|
||||||
значения
|
значения
|
||||||
- [ ] **C2. keep Aspect field** — пересчитывать второе поле, учитывать какое
|
- [ ] **C2. keep Aspect field** — пересчитывать второе поле, учитывать какое
|
||||||
|
|||||||
@@ -36,6 +36,20 @@ test("resize-png uses the source dimensions by default", async ({ page }) => {
|
|||||||
expectNoErrors(sink);
|
expectNoErrors(sink);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("resize-png keeps user-set size when another image is uploaded", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await openTool(page, "resize-png");
|
||||||
|
await uploadImage(page, opaquePng);
|
||||||
|
const width = page.getByRole("spinbutton", { name: "Width" });
|
||||||
|
await width.fill("25");
|
||||||
|
await uploadImage(page, landscapePng);
|
||||||
|
await expect(width).toHaveValue("25");
|
||||||
|
await expect(page.getByRole("spinbutton", { name: "Height" })).toHaveValue(
|
||||||
|
"48",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("crop-png starts with the full source area", async ({ page }) => {
|
test("crop-png starts with the full source area", async ({ page }) => {
|
||||||
const sink = trackErrors(page);
|
const sink = trackErrors(page);
|
||||||
await openTool(page, "crop-png");
|
await openTool(page, "crop-png");
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
type ToolSchema,
|
type ToolSchema,
|
||||||
} from "$lib/registry-schema";
|
} from "$lib/registry-schema";
|
||||||
import { downloadZip } from "$lib/zip";
|
import { downloadZip } from "$lib/zip";
|
||||||
|
import { SvelteSet } from "svelte/reactivity";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page: Page;
|
page: Page;
|
||||||
@@ -38,6 +39,9 @@
|
|||||||
const resultKind = $derived(tool.result ?? "image");
|
const resultKind = $derived(tool.result ?? "image");
|
||||||
|
|
||||||
let values = $state<Record<string, unknown>>({});
|
let values = $state<Record<string, unknown>>({});
|
||||||
|
// Поля, изменённые пользователем: при загрузке нового файла им не даём
|
||||||
|
// перезаписаться source-дефолтами. Reset снимает пометку.
|
||||||
|
const touched = new SvelteSet<string>();
|
||||||
let source = $state<PixelImage | null>(null);
|
let source = $state<PixelImage | null>(null);
|
||||||
let result = $state<PixelImage | null>(null);
|
let result = $state<PixelImage | null>(null);
|
||||||
let fileResult = $state<FileResult | null>(null);
|
let fileResult = $state<FileResult | null>(null);
|
||||||
@@ -67,10 +71,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setValue(id: string, value: unknown) {
|
function setValue(id: string, value: unknown) {
|
||||||
|
touched.add(id);
|
||||||
values = { ...values, [id]: value };
|
values = { ...values, [id]: value };
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
|
touched.clear();
|
||||||
values = schema
|
values = schema
|
||||||
? defaultSchemaParams(schema, { source: source ?? undefined })
|
? defaultSchemaParams(schema, { source: source ?? undefined })
|
||||||
: {};
|
: {};
|
||||||
@@ -87,7 +93,12 @@
|
|||||||
const decoded = await decodeFile(file);
|
const decoded = await decodeFile(file);
|
||||||
source = decoded;
|
source = decoded;
|
||||||
if (schema) {
|
if (schema) {
|
||||||
values = applySourceDefaults(schema, values, { source: decoded });
|
values = applySourceDefaults(
|
||||||
|
schema,
|
||||||
|
values,
|
||||||
|
{ source: decoded },
|
||||||
|
touched,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
displayError = toDisplayError(e);
|
displayError = toDisplayError(e);
|
||||||
|
|||||||
@@ -89,6 +89,16 @@ describe("registry-schema: source-aware dimension defaults", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("touched-поля не перезаписываются source-дефолтами", () => {
|
||||||
|
const manual = { size: { width: 5, height: 6 } };
|
||||||
|
expect(
|
||||||
|
applySourceDefaults(sourceSchema, manual, { source }, new Set(["size"])),
|
||||||
|
).toEqual({
|
||||||
|
size: { width: 5, height: 6 },
|
||||||
|
fixed: { width: 10, height: 20 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("uses source dimensions when sanitizing missing values", () => {
|
it("uses source dimensions when sanitizing missing values", () => {
|
||||||
expect(sanitizeSchemaParams(sourceSchema, {}, { source })).toEqual({
|
expect(sanitizeSchemaParams(sourceSchema, {}, { source })).toEqual({
|
||||||
size: source,
|
size: source,
|
||||||
|
|||||||
@@ -435,15 +435,22 @@ export function defaultSchemaParams<P>(
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пересчитывает дефолты от нового исходника, сохраняя значения пользователя.
|
||||||
|
* Поля из touched (изменённые вручную) не перезаписываются даже если у них
|
||||||
|
* source-дефолт — иначе загрузка нового файла сносила бы настроенные размеры.
|
||||||
|
*/
|
||||||
export function applySourceDefaults<P>(
|
export function applySourceDefaults<P>(
|
||||||
schema: ToolSchema<P>,
|
schema: ToolSchema<P>,
|
||||||
values: Record<string, unknown>,
|
values: Record<string, unknown>,
|
||||||
context: SchemaContext,
|
context: SchemaContext,
|
||||||
|
touched?: ReadonlySet<string>,
|
||||||
): Record<keyof P, unknown> {
|
): Record<keyof P, unknown> {
|
||||||
const defaults = defaultSchemaParams(schema, context);
|
const defaults = defaultSchemaParams(schema, context);
|
||||||
for (const key of Object.keys(schema.fields) as (keyof P)[]) {
|
for (const key of Object.keys(schema.fields) as (keyof P)[]) {
|
||||||
const spec = schema.fields[key].spec;
|
const spec = schema.fields[key].spec;
|
||||||
if (spec.kind === "dimension" && spec.defaultFromSource) continue;
|
const fromSource = spec.kind === "dimension" && spec.defaultFromSource;
|
||||||
|
if (fromSource && !touched?.has(key as string)) continue;
|
||||||
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
||||||
defaults[key] = values[key as string];
|
defaults[key] = values[key as string];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user