From aaf667d5a7c3e7d35d36df86d25956d6e7f5f2ce Mon Sep 17 00:00:00 2001 From: Nick DeMarchis <65045396+ndemarchis@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:01:41 -0400 Subject: [PATCH] feat: add converter chaining for multi-step format conversions --- src/lib/converters/converter.svelte.ts | 58 ++++++++++++++++++++++++++ src/lib/types/file.svelte.ts | 25 ++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/lib/converters/converter.svelte.ts b/src/lib/converters/converter.svelte.ts index a2b7a12..d888cbb 100644 --- a/src/lib/converters/converter.svelte.ts +++ b/src/lib/converters/converter.svelte.ts @@ -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 { + 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 { + await Promise.allSettled(this.steps.map((s) => s.converter.cancel(input))); + } +} diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index 7561542..31f9578 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -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 {