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/"],
|
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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) || [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]();
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue