feat: add converter chaining for multi-step format conversions

This commit is contained in:
Nick DeMarchis 2026-06-29 14:10:28 -04:00
parent 916e67357d
commit 3f4a42b907
2 changed files with 82 additions and 1 deletions

View File

@ -93,3 +93,61 @@ export class Converter {
return this.supportedFormats.map((f) => f.name);
}
}
export interface ChainStep {
converter: Converter;
to: string; // intermediate format (last step uses the final target)
}
export class ChainedConverter extends Converter {
public override name: string;
private steps: ChainStep[];
constructor(steps: ChainStep[]) {
super(0);
if (steps.length < 2) throw new Error("Chain requires at least 2 steps");
this.steps = steps;
this.name = steps.map((s) => s.converter.name).join("+");
this.clearTimeout();
this.status = this.deriveStatus();
this.supportedFormats = [
...steps[0].converter.supportedFormats.filter((f) => f.fromSupported),
...steps[steps.length - 1].converter.supportedFormats.filter((f) => f.toSupported),
];
}
private deriveStatus(): WorkerStatus {
const statuses = this.steps.map((s) => s.converter.status);
if (statuses.some((s) => s === "error")) return "error";
if (statuses.every((s) => s === "ready")) return "ready";
if (statuses.some((s) => s === "downloading")) return "downloading";
return "not-ready";
}
public override async convert(input: VertFile, to: string): Promise<VertFile> {
this.status = this.deriveStatus();
const { VertFile: VF } = await import("$lib/types");
let current: VertFile = input;
for (let i = 0; i < this.steps.length; i++) {
const { converter, to: stepTo } = this.steps[i];
const isLast = i === this.steps.length - 1;
const target = isLast ? to : stepTo;
const result = await converter.convert(current, target);
if (!isLast) {
current = new VF(
new File([await result.file.arrayBuffer()], input.name.replace(/\.[^/.]+$/, target)),
target,
);
} else {
current = result;
}
}
return current;
}
public override async cancel(input: VertFile): Promise<void> {
await Promise.allSettled(this.steps.map((s) => s.converter.cancel(input)));
}
}

View File

@ -1,5 +1,6 @@
import { byNative, converters } from "$lib/converters";
import type { Converter } from "$lib/converters/converter.svelte";
import { ChainedConverter } from "$lib/converters/converter.svelte";
import { m } from "$lib/paraglide/messages";
import { ToastManager } from "$lib/util/toast.svelte";
import type { Component } from "svelte";
@ -65,7 +66,29 @@ export class VertFile {
if (!theirFrom.isNative && !theirTo.isNative) return false;
return true;
});
return converter;
if (converter) return converter;
for (const a of this.converters) {
for (const fmt of a.supportedFormats) {
if (!fmt.toSupported) continue;
const b = converters.find((c) => {
const cFrom = c.supportedFormats.find(
(f) => f.name === fmt.name && f.fromSupported,
);
const cTo = c.supportedFormats.find(
(f) => f.name === this.to && f.toSupported,
);
return cFrom && cTo;
});
if (b)
return new ChainedConverter([
{ converter: a, to: fmt.name },
{ converter: b, to: this.to },
]);
}
}
return undefined;
}
public isLarge(): boolean {