feat: choose converter, clean up logic

This commit is contained in:
Maya 2026-03-08 13:46:32 +03:00
parent 9164dc8824
commit 9b0470e4de
No known key found for this signature in database
8 changed files with 449 additions and 196 deletions

View File

@ -31,9 +31,9 @@ export default ts.config(
ignores: ["build/", ".svelte-kit/", "dist/"], ignores: ["build/", ".svelte-kit/", "dist/"],
}, },
{ {
files: ["**/*.ts", "**/*.svelte.ts"], files: ["**/*.ts", "**/*.svelte.ts", "**/*.svelte"],
rules: { rules: {
"no-at-html-tags": "off", "svelte/no-at-html-tags": "off",
}, },
}, },
); );

View File

@ -85,8 +85,9 @@
"settings": { "settings": {
"settings": "Settings", "settings": "Settings",
"title": "File conversion settings", "title": "File conversion settings",
"description": "Change the conversion settings for <b>{filename}</b>, which is using <b>{converter}</b>. These settings may not be available for all formats. This is an early beta and may have some issues.", "description": "Change the conversion settings for <b>{filename}</b> with the selected converter. These settings may not be available for all formats. <b>This is an early beta and may have some issues.</b>",
"none": "No settings available for this format.", "none": "No settings available for this format.",
"converter": "Converter",
"image": { "image": {
"quality": "Quality", "quality": "Quality",
"depth": "Color depth", "depth": "Color depth",

View File

@ -17,19 +17,40 @@
let { file, onclose }: Props = $props(); let { file, onclose }: Props = $props();
let settings = $state<ConversionSettings>({}); const getCurrentConverter = (
vertFile: VertFile,
converterOverride?: string,
) => {
const converterName =
converterOverride || vertFile.conversionSettings.converter;
const availableConverters = vertFile.isZip()
? vertFile.converters
: vertFile.findConverters();
if (converterName) {
const selectedConverter =
availableConverters.find((c) => c.name === converterName) ||
vertFile.converters.find((c) => c.name === converterName);
if (selectedConverter) return selectedConverter;
}
return vertFile.isZip()
? vertFile.converters[0]
: vertFile.findConverters()[0];
};
let settings = $derived<ConversionSettings>({
converter: file ? getCurrentConverter(file)?.name : undefined,
});
const handleSettingChange = (key: string, value: any) => { const handleSettingChange = (key: string, value: any) => {
if (!file) return; if (!file) return;
settings[key] = value; settings[key] = value;
}; };
const applySettings = async () => { const applySettings = async (converterName: string) => {
onclose?.();
if (!file) return; if (!file) return;
const converter = file.isZip() const converter = getCurrentConverter(file, converterName);
? file.converters[0]
: file.findConverters()[0];
if (!converter) { if (!converter) {
log( log(
["settings", "modal"], ["settings", "modal"],
@ -61,7 +82,10 @@
}, },
{ {
text: "Apply", text: "Apply",
action: applySettings, action: () => {
applySettings(settings.converter!);
onclose?.();
},
primary: true, primary: true,
}, },
]} ]}
@ -69,171 +93,196 @@
> >
<div class="flex flex-col gap-8 max-h-[calc(100vh-225px)] overflow-y-auto"> <div class="flex flex-col gap-8 max-h-[calc(100vh-225px)] overflow-y-auto">
{#if file} {#if file}
{@const currentConverter = getCurrentConverter(file)}
{@const availableConverters = file.isZip()
? file.converters
: file.findConverters()}
<p class="text-base">
{@html sanitize(
m["convert.settings.description"]({
converter: currentConverter?.name || "unknown",
filename: file.name,
}),
)}
</p>
<div class="flex flex-col gap-2">
<p class="text-sm font-bold mb-1">
{m["convert.settings.converter"]()}
</p>
<Dropdown
options={availableConverters.map((converter) => ({
value: converter.name,
label: converter.name,
}))}
selected={settings.converter || currentConverter?.name}
settingsStyle
onselect={(value) => {
settings = { converter: value }; // TODO: dont think i need to add the converter here
}}
/>
</div>
<!-- FIXME: modal loads before settings is finished for some reason --> <!-- FIXME: modal loads before settings is finished for some reason -->
{#await file.getAvailableSettings(file) then availableSettings} {#key settings}
<div class="flex flex-col gap-4"> {#await file.getAvailableSettings(file, settings.converter) then availableSettings}
<p class="text-base"> <div class="flex flex-col gap-4">
{@html sanitize( {#if availableSettings.length === 0}
m["convert.settings.description"]({ <p class="text-sm text-muted">
converter: file.isZip() {m["convert.settings.none"]()}
? file.converters[0].name </p>
: file.findConverters()[0].name || {:else}
"unknown", <div class="grid grid-cols-2 gap-4">
filename: file.name, {#each availableSettings as setting (setting.key)}
}), <div
)} class={setting.forceFullWidth
</p> ? "col-span-2"
: "flex flex-col gap-2"}
{#if availableSettings.length === 0} >
<p class="text-sm text-muted"> <p class="text-sm font-bold">
{m["convert.settings.none"]()} {setting.label}
</p>
{:else}
<div class="grid grid-cols-2 gap-4">
{#each availableSettings as setting (setting.key)}
<div
class={setting.forceFullWidth
? "col-span-2"
: "flex flex-col gap-2"}
>
<p class="text-sm font-bold">
{setting.label}
</p>
<!-- prob unneeded -->
{#if setting.description}
<p class="text-xs text-muted mt-1">
{setting.description}
</p> </p>
{/if} <!-- prob unneeded -->
{#if setting.description}
<p class="text-xs text-muted mt-1">
{setting.description}
</p>
{/if}
{#if setting.type === "select"} {#if setting.type === "select"}
<Dropdown <Dropdown
options={setting.options?.map( options={setting.options?.map(
(opt) => (opt) =>
typeof opt === "string" typeof opt === "string"
? { ? {
value: opt, value: opt,
label: opt, label: opt,
} }
: opt, : opt,
) || []} ) || []}
selected={settings[setting.key] ?? selected={settings[
file.conversionSettings[
setting.key setting.key
] ?? ] ??
setting.default}
settingsStyle
onselect={(value) =>
handleSettingChange(
setting.key,
value,
)}
disabled={setting.disabled}
/>
{#if setting.hasCustomInput}
{@const disabled =
(settings[setting.key] ??
file.conversionSettings[ file.conversionSettings[
setting.key setting.key
]) !== "custom"}
<FancyInput
type="text"
value={settings[
setting.customInputKey!
] ??
file.conversionSettings[
setting.customInputKey!
] ?? ] ??
""} setting.default}
placeholder={setting.placeholder} settingsStyle
disabled={disabled || onselect={(value) =>
setting.disabled}
oninput={(e) =>
handleSettingChange(
setting.customInputKey!,
e.currentTarget.value,
)}
/>
{/if}
{:else if setting.type === "boolean"}
<FancyInput
type="checkbox"
checked={settings[setting.key] ??
file.conversionSettings[
setting.key
] ??
setting.default}
placeholder={setting.placeholder}
onchange={(e) =>
handleSettingChange(
setting.key,
e.currentTarget.checked,
)}
disabled={setting.disabled}
/>
{:else if setting.type === "range"}
{@const rangeValue = (settings[
setting.key
] ??
file.conversionSettings[
setting.key
] ??
setting.default ??
setting.min ??
0) as number}
{@const rangeLabel =
setting.options?.[rangeValue]
?.label ?? rangeValue}
<div
class="flex items-center mt-2 gap-2"
>
<input
type="range"
min={setting.min}
max={setting.max}
step={setting.step}
value={rangeValue}
class="range-slider w-full"
oninput={(e) => {
const nextValue =
e.currentTarget
.valueAsNumber;
handleSettingChange( handleSettingChange(
setting.key, setting.key,
nextValue, value,
); )}
}}
disabled={setting.disabled} disabled={setting.disabled}
/> />
<span {#if setting.hasCustomInput}
class="text-sm max-w-28 w-full text-right" {@const disabled =
> (settings[setting.key] ??
{rangeLabel} file.conversionSettings[
</span> setting.key
</div> ]) !== "custom"}
{:else} <FancyInput
<FancyInput type="text"
type={setting.type} value={settings[
value={settings[setting.key] ?? setting.customInputKey!
] ??
file.conversionSettings[
setting
.customInputKey!
] ??
""}
placeholder={setting.placeholder}
disabled={disabled ||
setting.disabled}
oninput={(e) =>
handleSettingChange(
setting.customInputKey!,
e.currentTarget
.value,
)}
/>
{/if}
{:else if setting.type === "boolean"}
<FancyInput
type="checkbox"
checked={settings[
setting.key
] ??
file.conversionSettings[
setting.key
] ??
setting.default}
placeholder={setting.placeholder}
onchange={(e) =>
handleSettingChange(
setting.key,
e.currentTarget.checked,
)}
disabled={setting.disabled}
/>
{:else if setting.type === "range"}
{@const rangeValue = (settings[
setting.key
] ??
file.conversionSettings[ file.conversionSettings[
setting.key setting.key
] ?? ] ??
setting.default} setting.default ??
placeholder={setting.placeholder} setting.min ??
oninput={(e) => 0) as number}
handleSettingChange( {@const rangeLabel =
setting.key, setting.options?.[rangeValue]
e.currentTarget.value, ?.label ?? rangeValue}
)} <div
disabled={setting.disabled} class="flex items-center mt-2 gap-2"
/> >
{/if} <input
</div> type="range"
{/each} min={setting.min}
</div> max={setting.max}
{/if} step={setting.step}
</div> value={rangeValue}
{/await} class="range-slider w-full"
oninput={(e) => {
const nextValue =
e.currentTarget
.valueAsNumber;
handleSettingChange(
setting.key,
nextValue,
);
}}
disabled={setting.disabled}
/>
<span
class="text-sm max-w-28 w-full text-right"
>
{rangeLabel}
</span>
</div>
{:else}
<FancyInput
type={setting.type}
value={settings[setting.key] ??
file.conversionSettings[
setting.key
] ??
setting.default}
placeholder={setting.placeholder}
oninput={(e) =>
handleSettingChange(
setting.key,
e.currentTarget.value,
)}
disabled={setting.disabled}
/>
{/if}
</div>
{/each}
</div>
{/if}
</div>
{/await}
{/key}
{/if} {/if}
</div> </div>
</Modal> </Modal>

View File

@ -7,6 +7,7 @@ import { MagickConverter } from "./magick.svelte";
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts"; import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts";
import { MediabunnyConverter } from "./mediabunny.svelte"; import { MediabunnyConverter } from "./mediabunny.svelte";
// TODO: change this to include category with initialization to replace converterCategories and maybe categories as well
const getConverters = (): Converter[] => { const getConverters = (): Converter[] => {
const converters: Converter[] = [ const converters: Converter[] = [
new MagickConverter(), new MagickConverter(),
@ -21,6 +22,12 @@ const getConverters = (): Converter[] => {
}; };
export const converters = getConverters(); export const converters = getConverters();
export const converterCategories = {
image: ["imagemagick"],
video: ["mediabunny", "vertd"],
audio: ["ffmpeg"],
doc: ["pandoc"],
}
export function getConverterByFormat(format: string) { export function getConverterByFormat(format: string) {
for (const converter of converters) { for (const converter of converters) {
@ -40,13 +47,13 @@ export const categories: Categories = {
categories.audio.formats = categories.audio.formats =
converters converters
.find((c) => c.name === "ffmpeg") .find((c) => converterCategories.audio.includes(c.name))
?.supportedFormats.filter((f) => f.toSupported && f.isNative) ?.supportedFormats.filter((f) => f.toSupported && f.isNative)
.map((f) => f.name) || []; .map((f) => f.name) || [];
categories.video.formats = [ categories.video.formats = [
...new Set( ...new Set(
converters converters
.filter((c) => c.name === "mediabunny" || c.name === "vertd") .filter((c) => converterCategories.video.includes(c.name))
.flatMap((c) => .flatMap((c) =>
c.supportedFormats c.supportedFormats
.filter((f) => f.toSupported && f.isNative) .filter((f) => f.toSupported && f.isNative)
@ -56,11 +63,11 @@ categories.video.formats = [
]; ];
categories.image.formats = categories.image.formats =
converters converters
.find((c) => c.name === "imagemagick") .find((c) => converterCategories.image.includes(c.name))
?.formatStrings((f) => f.toSupported) || []; ?.formatStrings((f) => f.toSupported) || [];
categories.doc.formats = categories.doc.formats =
converters converters
.find((c) => c.name === "pandoc") .find((c) => converterCategories.doc.includes(c.name))
?.supportedFormats.filter((f) => f.toSupported && f.isNative) ?.supportedFormats.filter((f) => f.toSupported && f.isNative)
.map((f) => f.name) || []; .map((f) => f.name) || [];

View File

@ -23,6 +23,12 @@ import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte";
import { ToastManager } from "$lib/util/toast.svelte"; import { ToastManager } from "$lib/util/toast.svelte";
import { error, log } from "$lib/util/logger"; import { error, log } from "$lib/util/logger";
import { registerFlacEncoder } from "@mediabunny/flac-encoder"; import { registerFlacEncoder } from "@mediabunny/flac-encoder";
import { m } from "$lib/paraglide/messages";
import type {
SettingDefinition,
ConversionSettings,
} from "$lib/types/conversion-settings";
import { CONVERSION_BITRATES, SAMPLE_RATES } from "./ffmpeg.svelte";
// codec compatibility object, based on docs // codec compatibility object, based on docs
// https://mediabunny.dev/guide/supported-formats-and-codecs#compatibility-table // https://mediabunny.dev/guide/supported-formats-and-codecs#compatibility-table
@ -209,6 +215,152 @@ export class MediabunnyConverter extends Converter {
registerAc3Encoder(); registerAc3Encoder();
} }
public async getAvailableSettings(): Promise<SettingDefinition[]> {
// TODO: maybe have a slider for conversion speed/quality like vertd
const fps: SettingDefinition = {
key: "fps",
label: m["convert.settings.video.fps"](),
type: "select",
default: "auto",
options: [
{ value: "auto", label: m["convert.settings.common.auto"]() },
{
value: "custom",
label: m["convert.settings.common.custom"](),
},
{ value: "24", label: "24" },
{ value: "30", label: "30" },
{ value: "60", label: "60" },
{ value: "120", label: "120" },
{ value: "144", label: "144" },
{ value: "240", label: "240" },
],
hasCustomInput: true,
customInputKey: "customFps",
placeholder: m["convert.settings.video.fps_placeholder"](),
};
const resolution: SettingDefinition = {
key: "resolution",
label: m["convert.settings.video.resolution"](),
type: "select",
default: "auto",
options: [
{ value: "auto", label: m["convert.settings.common.auto"]() },
{
value: "custom",
label: m["convert.settings.common.custom"](),
},
{ value: "426x240", label: "426x240" },
{ value: "640x360", label: "640x360" },
{ value: "854x480", label: "854x480" },
{ value: "1280x720", label: "1280x720" },
{ value: "1920x1080", label: "1920x1080" },
{ value: "2560x1440", label: "2560x1440" },
{ value: "3840x2160", label: "3840x2160" },
],
hasCustomInput: true,
customInputKey: "customResolution",
placeholder: m["convert.settings.video.resolution_placeholder"](),
};
// TODO: allow CRF for consistent quality?
const videoBitrate: SettingDefinition = {
key: "videoBitrate",
label: m["convert.settings.video.video_bitrate"](),
type: "select",
default: "auto",
options: [
{ value: "auto", label: m["convert.settings.common.auto"]() },
{
value: "custom",
label: m["convert.settings.common.custom"](),
},
{ value: "1000", label: "1000 kbps" },
{ value: "2500", label: "2500 kbps" },
{ value: "5000", label: "5000 kbps" },
{ value: "8000", label: "8000 kbps" },
{ value: "12000", label: "12000 kbps" },
{ value: "18000", label: "18000 kbps" },
],
hasCustomInput: true,
customInputKey: "customVideoBitrate",
placeholder: m["convert.settings.video.bitrate_placeholder"](),
};
/*
* audio settings
*/
const audioBitrate: SettingDefinition = {
key: "audioBitrate",
label: m["convert.settings.video.audio_bitrate"](),
type: "select",
default: "auto",
options: CONVERSION_BITRATES.map((b) => ({
value: b.toString(),
label:
b === "auto"
? m["convert.settings.common.auto"]()
: b === "custom"
? m["convert.settings.common.custom"]()
: `${b} kbps`,
})),
hasCustomInput: true,
customInputKey: "customAudioBitrate",
placeholder: m["convert.settings.audio.bitrate_placeholder"](),
};
const sampleRate: SettingDefinition = {
key: "sampleRate",
label: m["convert.settings.audio.sample_rate"](),
type: "select",
default: "auto",
options: SAMPLE_RATES.map((r) => ({
value: r.toString(),
label:
r === "auto"
? m["convert.settings.common.auto"]()
: r === "custom"
? m["convert.settings.common.custom"]()
: `${r} Hz`,
})),
hasCustomInput: true,
customInputKey: "customSampleRate",
placeholder: m["convert.settings.audio.sample_rate_placeholder"](),
};
/*
* common
*/
const metadata: SettingDefinition = {
key: "metadata",
label: m["convert.settings.common.metadata"](),
type: "boolean",
default: true,
};
// trim/crop/rotate - also have another ui for this prob
return [
videoBitrate,
resolution,
fps,
metadata,
audioBitrate,
sampleRate,
];
}
public async getDefaultSettings(): Promise<ConversionSettings> {
const defaults: ConversionSettings = {};
const settings = await this.getAvailableSettings();
settings.forEach((setting) => {
defaults[setting.key] = setting.default;
});
return defaults;
}
public async convert(file: VertFile, to: string): Promise<VertFile> { public async convert(file: VertFile, to: string): Promise<VertFile> {
const input = new Input({ const input = new Input({
// TODO: add settings & special handling for certain formats & codecs // TODO: add settings & special handling for certain formats & codecs

View File

@ -1,5 +1,5 @@
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { byNative, converters } from "$lib/converters"; import { byNative, converterCategories, converters } from "$lib/converters";
import { error, log } from "$lib/util/logger"; import { error, log } from "$lib/util/logger";
import { VertFile } from "$lib/types"; import { VertFile } from "$lib/types";
import { parseBlob, selectCover } from "music-metadata"; import { parseBlob, selectCover } from "music-metadata";
@ -36,12 +36,12 @@ class Files {
private _addThumbnail = async (file: VertFile) => { private _addThumbnail = async (file: VertFile) => {
this.thumbnailQueue.add(async () => { this.thumbnailQueue.add(async () => {
const isAudio = converters const isAudio = converters
.find((c) => c.name === "ffmpeg") .find((c) => converterCategories.audio.includes(c.name))
?.supportedFormats.filter((f) => f.isNative) ?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name) .map((f) => f.name)
?.includes(file.from.toLowerCase()); ?.includes(file.from.toLowerCase());
const isVideo = converters const isVideo = converters
.find((c) => c.name === "vertd") .find((c) => converterCategories.video.includes(c.name))
?.supportedFormats.filter((f) => f.isNative) ?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name) .map((f) => f.name)
?.includes(file.from.toLowerCase()); ?.includes(file.from.toLowerCase());
@ -291,7 +291,7 @@ class Files {
this._addThumbnail(vf); this._addThumbnail(vf);
const convName = converter.name; const convName = converter.name;
if (file.size > MAX_ARRAY_BUFFER_SIZE && convName === "vertd") { if (file.size > MAX_ARRAY_BUFFER_SIZE && (converterCategories.video.includes(convName))) {
ToastManager.add({ ToastManager.add({
type: "warning", type: "warning",
message: m["convert.large_file_warning"]({ message: m["convert.large_file_warning"]({
@ -303,10 +303,11 @@ class Files {
}); });
} }
const isVideo = convName === "vertd"; // TODO: only show if vertd is needed/requested
const isServerVideo = convName === "vertd";
const acceptedExternalWarning = const acceptedExternalWarning =
localStorage.getItem("acceptedExternalWarning") === "true"; localStorage.getItem("acceptedExternalWarning") === "true";
if (isVideo && !acceptedExternalWarning && !this._warningShown) { if (isServerVideo && !acceptedExternalWarning && !this._warningShown) {
this._warningShown = true; this._warningShown = true;
const title = m["convert.external_warning.title"](); const title = m["convert.external_warning.title"]();
const message = m["convert.external_warning.text"](); const message = m["convert.external_warning.text"]();

View File

@ -40,10 +40,15 @@ export class VertFile {
public isZip = $state(() => this.from === ".zip"); public isZip = $state(() => this.from === ".zip");
public getAvailableSettings(input: VertFile): Promise<SettingDefinition[]> { public getAvailableSettings(
const converter = this.findConverters()[0]; input: VertFile,
if (!converter) return Promise.resolve([]); converter: string | undefined = this.conversionSettings.converter,
return converter.getAvailableSettings(input); ): Promise<SettingDefinition[]> {
const converterInstance = this.converters.find(
(c) => c.name === converter,
);
if (!converterInstance) return Promise.resolve([]);
return converterInstance.getAvailableSettings(input);
} }
public findConverters(supportedFormats: string[] = [this.from]) { public findConverters(supportedFormats: string[] = [this.from]) {
@ -133,10 +138,19 @@ export class VertFile {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public async convert(...args: any[]) { public async convert(...args: any[]) {
if (!this.converters.length) throw new Error("No converters found"); if (!this.converters.length) throw new Error("No converters found");
const converter = this.isZip()
? this.converters[0] const customConverter = this.converters.find(
: this.findConverters()[0]; (c) => c.name === this.conversionSettings.converter,
);
const converter =
customConverter ||
(this.isZip() // TODO: not sure if the zip needs to be changed now
? this.converters[0]
: this.findConverters([this.from, this.to])[0]);
log(["file", "convert"], `using converter: ${converter.name}`);
if (!converter) throw new Error("No converter found"); if (!converter) throw new Error("No converter found");
this.result = null; this.result = null;
this.progress = 0; this.progress = 0;
this.processing = true; this.processing = true;
@ -255,9 +269,9 @@ export class VertFile {
public async cancel() { public async cancel() {
if (!this.processing) return; if (!this.processing) return;
const converter = this.isZip() const converter = this.converters.find(
? this.converters[0] (c) => c.name === this.conversionSettings.converter,
: this.findConverters()[0]; );
if (!converter) throw new Error("No converter found"); if (!converter) throw new Error("No converter found");
this.cancelled = true; this.cancelled = true;
try { try {

View File

@ -5,7 +5,11 @@
import Panel from "$lib/components/visual/Panel.svelte"; import Panel from "$lib/components/visual/Panel.svelte";
import ProgressBar from "$lib/components/visual/ProgressBar.svelte"; import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
import Tooltip from "$lib/components/visual/Tooltip.svelte"; import Tooltip from "$lib/components/visual/Tooltip.svelte";
import { categories, converters } from "$lib/converters"; import {
categories,
converterCategories,
converters,
} from "$lib/converters";
import { import {
effects, effects,
files, files,
@ -35,6 +39,22 @@
let processedFileIds = $state(new Set<string>()); let processedFileIds = $state(new Set<string>());
const getCurrentConverter = (file: VertFile) => {
const converterName = file.conversionSettings.converter;
const availableConverters = file.isZip()
? file.converters
: file.findConverters();
if (converterName) {
const selectedConverter =
availableConverters.find((c) => c.name === converterName) ||
file.converters.find((c) => c.name === converterName);
if (selectedConverter) return selectedConverter;
}
return file.isZip() ? file.converters[0] : file.findConverters()[0];
};
$effect(() => { $effect(() => {
if (!Settings.instance.settings || files.files.length === 0) return; if (!Settings.instance.settings || files.files.length === 0) return;
@ -42,14 +62,19 @@
const settings = Settings.instance.settings; const settings = Settings.instance.settings;
if (processedFileIds.has(file.id)) return; if (processedFileIds.has(file.id)) return;
const converter = file.isZip() ? file.converters[0] : file.findConverters()[0]; const converter = getCurrentConverter(file);
if (!converter) return; if (!converter) return;
// Initialize converter in settings if not already set
if (!file.conversionSettings.converter)
file.conversionSettings.converter = converter.name;
let category: string | undefined; let category: string | undefined;
const isImage = converter.name === "imagemagick"; const isImage = converterCategories.image.includes(converter.name);
const isAudio = converter.name === "ffmpeg"; const isAudio = converterCategories.audio.includes(converter.name);
const isVideo = converter.name === "vertd"; const isVideo = converterCategories.video.includes(converter.name);
const isDocument = converter.name === "pandoc"; const isDocument = converterCategories.doc.includes(converter.name);
if (isImage) category = "image"; if (isImage) category = "image";
else if (isAudio) category = "audio"; else if (isAudio) category = "audio";
@ -108,16 +133,19 @@
let type = ""; let type = "";
if (files.files.length) { if (files.files.length) {
const converters = files.files.map( const converters = files.files.map(
(file) => (file.isZip() ? file.converters[0] : file.findConverters()[0])?.name, (file) => getCurrentConverter(file)?.name,
); );
const uniqueTypes = new Set(converters); const uniqueTypes = new Set(converters);
if (uniqueTypes.size === 1) { if (uniqueTypes.size === 1) {
const onlyType = converters[0]; const onlyType = converters[0];
if (onlyType === "imagemagick") type = "blue"; if (converterCategories.image.includes(onlyType)) type = "blue";
else if (onlyType === "ffmpeg") type = "purple"; else if (converterCategories.audio.includes(onlyType))
else if (onlyType === "vertd") type = "red"; type = "purple";
else if (onlyType === "pandoc") type = "green"; else if (converterCategories.video.includes(onlyType))
type = "red";
else if (converterCategories.doc.includes(onlyType))
type = "green";
} }
} }
@ -130,11 +158,12 @@
</script> </script>
{#snippet fileItem(file: VertFile, index: number)} {#snippet fileItem(file: VertFile, index: number)}
{@const currentConverter = file.isZip() ? file.converters[0] : file.findConverters()[0]} {@const currentConverter = getCurrentConverter(file)}
{@const isImage = currentConverter?.name === "imagemagick"} {@const name = currentConverter?.name || "unknown"}
{@const isAudio = currentConverter?.name === "ffmpeg"} {@const isImage = converterCategories.image.includes(name)}
{@const isVideo = currentConverter?.name === "vertd"} {@const isAudio = converterCategories.audio.includes(name)}
{@const isDocument = currentConverter?.name === "pandoc"} {@const isVideo = converterCategories.video.includes(name)}
{@const isDocument = converterCategories.doc.includes(name)}
<Panel class="p-5 flex flex-col min-w-0 gap-4 relative"> <Panel class="p-5 flex flex-col min-w-0 gap-4 relative">
<div class="flex-shrink-0 h-8 w-full flex items-center gap-2"> <div class="flex-shrink-0 h-8 w-full flex items-center gap-2">
{#if !converters.length} {#if !converters.length}