mirror of https://github.com/VERT-sh/VERT.git
feat: choose converter, clean up logic
This commit is contained in:
parent
9164dc8824
commit
9b0470e4de
|
|
@ -31,9 +31,9 @@ export default ts.config(
|
|||
ignores: ["build/", ".svelte-kit/", "dist/"],
|
||||
},
|
||||
{
|
||||
files: ["**/*.ts", "**/*.svelte.ts"],
|
||||
files: ["**/*.ts", "**/*.svelte.ts", "**/*.svelte"],
|
||||
rules: {
|
||||
"no-at-html-tags": "off",
|
||||
"svelte/no-at-html-tags": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -85,8 +85,9 @@
|
|||
"settings": {
|
||||
"settings": "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.",
|
||||
"converter": "Converter",
|
||||
"image": {
|
||||
"quality": "Quality",
|
||||
"depth": "Color depth",
|
||||
|
|
|
|||
|
|
@ -17,19 +17,40 @@
|
|||
|
||||
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) => {
|
||||
if (!file) return;
|
||||
settings[key] = value;
|
||||
};
|
||||
|
||||
const applySettings = async () => {
|
||||
onclose?.();
|
||||
const applySettings = async (converterName: string) => {
|
||||
if (!file) return;
|
||||
const converter = file.isZip()
|
||||
? file.converters[0]
|
||||
: file.findConverters()[0];
|
||||
const converter = getCurrentConverter(file, converterName);
|
||||
if (!converter) {
|
||||
log(
|
||||
["settings", "modal"],
|
||||
|
|
@ -61,7 +82,10 @@
|
|||
},
|
||||
{
|
||||
text: "Apply",
|
||||
action: applySettings,
|
||||
action: () => {
|
||||
applySettings(settings.converter!);
|
||||
onclose?.();
|
||||
},
|
||||
primary: true,
|
||||
},
|
||||
]}
|
||||
|
|
@ -69,171 +93,196 @@
|
|||
>
|
||||
<div class="flex flex-col gap-8 max-h-[calc(100vh-225px)] overflow-y-auto">
|
||||
{#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 -->
|
||||
{#await file.getAvailableSettings(file) then availableSettings}
|
||||
<div class="flex flex-col gap-4">
|
||||
<p class="text-base">
|
||||
{@html sanitize(
|
||||
m["convert.settings.description"]({
|
||||
converter: file.isZip()
|
||||
? file.converters[0].name
|
||||
: file.findConverters()[0].name ||
|
||||
"unknown",
|
||||
filename: file.name,
|
||||
}),
|
||||
)}
|
||||
</p>
|
||||
|
||||
{#if availableSettings.length === 0}
|
||||
<p class="text-sm text-muted">
|
||||
{m["convert.settings.none"]()}
|
||||
</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}
|
||||
{#key settings}
|
||||
{#await file.getAvailableSettings(file, settings.converter) then availableSettings}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if availableSettings.length === 0}
|
||||
<p class="text-sm text-muted">
|
||||
{m["convert.settings.none"]()}
|
||||
</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>
|
||||
{/if}
|
||||
<!-- prob unneeded -->
|
||||
{#if setting.description}
|
||||
<p class="text-xs text-muted mt-1">
|
||||
{setting.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if setting.type === "select"}
|
||||
<Dropdown
|
||||
options={setting.options?.map(
|
||||
(opt) =>
|
||||
typeof opt === "string"
|
||||
? {
|
||||
value: opt,
|
||||
label: opt,
|
||||
}
|
||||
: opt,
|
||||
) || []}
|
||||
selected={settings[setting.key] ??
|
||||
file.conversionSettings[
|
||||
{#if setting.type === "select"}
|
||||
<Dropdown
|
||||
options={setting.options?.map(
|
||||
(opt) =>
|
||||
typeof opt === "string"
|
||||
? {
|
||||
value: opt,
|
||||
label: opt,
|
||||
}
|
||||
: opt,
|
||||
) || []}
|
||||
selected={settings[
|
||||
setting.key
|
||||
] ??
|
||||
setting.default}
|
||||
settingsStyle
|
||||
onselect={(value) =>
|
||||
handleSettingChange(
|
||||
setting.key,
|
||||
value,
|
||||
)}
|
||||
disabled={setting.disabled}
|
||||
/>
|
||||
{#if setting.hasCustomInput}
|
||||
{@const disabled =
|
||||
(settings[setting.key] ??
|
||||
file.conversionSettings[
|
||||
setting.key
|
||||
]) !== "custom"}
|
||||
<FancyInput
|
||||
type="text"
|
||||
value={settings[
|
||||
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[
|
||||
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;
|
||||
setting.default}
|
||||
settingsStyle
|
||||
onselect={(value) =>
|
||||
handleSettingChange(
|
||||
setting.key,
|
||||
nextValue,
|
||||
);
|
||||
}}
|
||||
value,
|
||||
)}
|
||||
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] ??
|
||||
{#if setting.hasCustomInput}
|
||||
{@const disabled =
|
||||
(settings[setting.key] ??
|
||||
file.conversionSettings[
|
||||
setting.key
|
||||
]) !== "custom"}
|
||||
<FancyInput
|
||||
type="text"
|
||||
value={settings[
|
||||
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[
|
||||
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}
|
||||
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(
|
||||
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}
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { MagickConverter } from "./magick.svelte";
|
|||
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts";
|
||||
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 converters: Converter[] = [
|
||||
new MagickConverter(),
|
||||
|
|
@ -21,6 +22,12 @@ const getConverters = (): Converter[] => {
|
|||
};
|
||||
|
||||
export const converters = getConverters();
|
||||
export const converterCategories = {
|
||||
image: ["imagemagick"],
|
||||
video: ["mediabunny", "vertd"],
|
||||
audio: ["ffmpeg"],
|
||||
doc: ["pandoc"],
|
||||
}
|
||||
|
||||
export function getConverterByFormat(format: string) {
|
||||
for (const converter of converters) {
|
||||
|
|
@ -40,13 +47,13 @@ export const categories: Categories = {
|
|||
|
||||
categories.audio.formats =
|
||||
converters
|
||||
.find((c) => c.name === "ffmpeg")
|
||||
.find((c) => converterCategories.audio.includes(c.name))
|
||||
?.supportedFormats.filter((f) => f.toSupported && f.isNative)
|
||||
.map((f) => f.name) || [];
|
||||
categories.video.formats = [
|
||||
...new Set(
|
||||
converters
|
||||
.filter((c) => c.name === "mediabunny" || c.name === "vertd")
|
||||
.filter((c) => converterCategories.video.includes(c.name))
|
||||
.flatMap((c) =>
|
||||
c.supportedFormats
|
||||
.filter((f) => f.toSupported && f.isNative)
|
||||
|
|
@ -56,11 +63,11 @@ categories.video.formats = [
|
|||
];
|
||||
categories.image.formats =
|
||||
converters
|
||||
.find((c) => c.name === "imagemagick")
|
||||
.find((c) => converterCategories.image.includes(c.name))
|
||||
?.formatStrings((f) => f.toSupported) || [];
|
||||
categories.doc.formats =
|
||||
converters
|
||||
.find((c) => c.name === "pandoc")
|
||||
.find((c) => converterCategories.doc.includes(c.name))
|
||||
?.supportedFormats.filter((f) => f.toSupported && f.isNative)
|
||||
.map((f) => f.name) || [];
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte";
|
|||
import { ToastManager } from "$lib/util/toast.svelte";
|
||||
import { error, log } from "$lib/util/logger";
|
||||
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
|
||||
// https://mediabunny.dev/guide/supported-formats-and-codecs#compatibility-table
|
||||
|
|
@ -209,6 +215,152 @@ export class MediabunnyConverter extends Converter {
|
|||
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> {
|
||||
const input = new Input({
|
||||
// TODO: add settings & special handling for certain formats & codecs
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { VertFile } from "$lib/types";
|
||||
import { parseBlob, selectCover } from "music-metadata";
|
||||
|
|
@ -36,12 +36,12 @@ class Files {
|
|||
private _addThumbnail = async (file: VertFile) => {
|
||||
this.thumbnailQueue.add(async () => {
|
||||
const isAudio = converters
|
||||
.find((c) => c.name === "ffmpeg")
|
||||
.find((c) => converterCategories.audio.includes(c.name))
|
||||
?.supportedFormats.filter((f) => f.isNative)
|
||||
.map((f) => f.name)
|
||||
?.includes(file.from.toLowerCase());
|
||||
const isVideo = converters
|
||||
.find((c) => c.name === "vertd")
|
||||
.find((c) => converterCategories.video.includes(c.name))
|
||||
?.supportedFormats.filter((f) => f.isNative)
|
||||
.map((f) => f.name)
|
||||
?.includes(file.from.toLowerCase());
|
||||
|
|
@ -291,7 +291,7 @@ class Files {
|
|||
this._addThumbnail(vf);
|
||||
|
||||
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({
|
||||
type: "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 =
|
||||
localStorage.getItem("acceptedExternalWarning") === "true";
|
||||
if (isVideo && !acceptedExternalWarning && !this._warningShown) {
|
||||
if (isServerVideo && !acceptedExternalWarning && !this._warningShown) {
|
||||
this._warningShown = true;
|
||||
const title = m["convert.external_warning.title"]();
|
||||
const message = m["convert.external_warning.text"]();
|
||||
|
|
|
|||
|
|
@ -40,10 +40,15 @@ export class VertFile {
|
|||
|
||||
public isZip = $state(() => this.from === ".zip");
|
||||
|
||||
public getAvailableSettings(input: VertFile): Promise<SettingDefinition[]> {
|
||||
const converter = this.findConverters()[0];
|
||||
if (!converter) return Promise.resolve([]);
|
||||
return converter.getAvailableSettings(input);
|
||||
public getAvailableSettings(
|
||||
input: VertFile,
|
||||
converter: string | undefined = this.conversionSettings.converter,
|
||||
): 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]) {
|
||||
|
|
@ -133,10 +138,19 @@ export class VertFile {
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public async convert(...args: any[]) {
|
||||
if (!this.converters.length) throw new Error("No converters found");
|
||||
const converter = this.isZip()
|
||||
? this.converters[0]
|
||||
: this.findConverters()[0];
|
||||
|
||||
const customConverter = this.converters.find(
|
||||
(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");
|
||||
|
||||
this.result = null;
|
||||
this.progress = 0;
|
||||
this.processing = true;
|
||||
|
|
@ -255,9 +269,9 @@ export class VertFile {
|
|||
|
||||
public async cancel() {
|
||||
if (!this.processing) return;
|
||||
const converter = this.isZip()
|
||||
? this.converters[0]
|
||||
: this.findConverters()[0];
|
||||
const converter = this.converters.find(
|
||||
(c) => c.name === this.conversionSettings.converter,
|
||||
);
|
||||
if (!converter) throw new Error("No converter found");
|
||||
this.cancelled = true;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@
|
|||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
|
||||
import Tooltip from "$lib/components/visual/Tooltip.svelte";
|
||||
import { categories, converters } from "$lib/converters";
|
||||
import {
|
||||
categories,
|
||||
converterCategories,
|
||||
converters,
|
||||
} from "$lib/converters";
|
||||
import {
|
||||
effects,
|
||||
files,
|
||||
|
|
@ -35,6 +39,22 @@
|
|||
|
||||
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(() => {
|
||||
if (!Settings.instance.settings || files.files.length === 0) return;
|
||||
|
||||
|
|
@ -42,14 +62,19 @@
|
|||
const settings = Settings.instance.settings;
|
||||
if (processedFileIds.has(file.id)) return;
|
||||
|
||||
const converter = file.isZip() ? file.converters[0] : file.findConverters()[0];
|
||||
const converter = getCurrentConverter(file);
|
||||
if (!converter) return;
|
||||
|
||||
// Initialize converter in settings if not already set
|
||||
if (!file.conversionSettings.converter)
|
||||
file.conversionSettings.converter = converter.name;
|
||||
|
||||
|
||||
let category: string | undefined;
|
||||
const isImage = converter.name === "imagemagick";
|
||||
const isAudio = converter.name === "ffmpeg";
|
||||
const isVideo = converter.name === "vertd";
|
||||
const isDocument = converter.name === "pandoc";
|
||||
const isImage = converterCategories.image.includes(converter.name);
|
||||
const isAudio = converterCategories.audio.includes(converter.name);
|
||||
const isVideo = converterCategories.video.includes(converter.name);
|
||||
const isDocument = converterCategories.doc.includes(converter.name);
|
||||
|
||||
if (isImage) category = "image";
|
||||
else if (isAudio) category = "audio";
|
||||
|
|
@ -108,16 +133,19 @@
|
|||
let type = "";
|
||||
if (files.files.length) {
|
||||
const converters = files.files.map(
|
||||
(file) => (file.isZip() ? file.converters[0] : file.findConverters()[0])?.name,
|
||||
(file) => getCurrentConverter(file)?.name,
|
||||
);
|
||||
const uniqueTypes = new Set(converters);
|
||||
|
||||
if (uniqueTypes.size === 1) {
|
||||
const onlyType = converters[0];
|
||||
if (onlyType === "imagemagick") type = "blue";
|
||||
else if (onlyType === "ffmpeg") type = "purple";
|
||||
else if (onlyType === "vertd") type = "red";
|
||||
else if (onlyType === "pandoc") type = "green";
|
||||
if (converterCategories.image.includes(onlyType)) type = "blue";
|
||||
else if (converterCategories.audio.includes(onlyType))
|
||||
type = "purple";
|
||||
else if (converterCategories.video.includes(onlyType))
|
||||
type = "red";
|
||||
else if (converterCategories.doc.includes(onlyType))
|
||||
type = "green";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,11 +158,12 @@
|
|||
</script>
|
||||
|
||||
{#snippet fileItem(file: VertFile, index: number)}
|
||||
{@const currentConverter = file.isZip() ? file.converters[0] : file.findConverters()[0]}
|
||||
{@const isImage = currentConverter?.name === "imagemagick"}
|
||||
{@const isAudio = currentConverter?.name === "ffmpeg"}
|
||||
{@const isVideo = currentConverter?.name === "vertd"}
|
||||
{@const isDocument = currentConverter?.name === "pandoc"}
|
||||
{@const currentConverter = getCurrentConverter(file)}
|
||||
{@const name = currentConverter?.name || "unknown"}
|
||||
{@const isImage = converterCategories.image.includes(name)}
|
||||
{@const isAudio = converterCategories.audio.includes(name)}
|
||||
{@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">
|
||||
<div class="flex-shrink-0 h-8 w-full flex items-center gap-2">
|
||||
{#if !converters.length}
|
||||
|
|
|
|||
Loading…
Reference in New Issue