feat: start how settings is defined

god kill me i fought with paraglide for a while
This commit is contained in:
Maya 2026-02-14 00:01:02 +03:00
parent d54094722f
commit 575f444abc
No known key found for this signature in database
7 changed files with 293 additions and 70 deletions

View File

@ -83,7 +83,26 @@
},
"settings": {
"settings": "Settings",
"title": "File conversion settings"
"title": "File conversion settings",
"image": {
"quality": "Quality",
"depth": "Color depth",
"color_space": "Color space",
"transparency": "Transparency",
"metadata": "Metadata"
},
"audio": {
"quality": "Quality",
"rate": "Sample rate",
"metadata": "Metadata"
},
"video": {
"quality": "Quality",
"metadata": "Metadata"
},
"document": {
"metadata": "Metadata"
}
},
"tooltips": {
"unknown_file": "Unknown file type",

View File

@ -11,6 +11,7 @@
import Modal from "./Modal.svelte";
import Dropdown from "./Dropdown.svelte";
import FancyInput from "./FancyInput.svelte";
import SettingsModal from "./SettingsModal.svelte";
type Props = {
categories: Categories;
@ -335,9 +336,9 @@
};
let showSettingsModal = $state(false);
const settings = () => {
if (!file) return;
// TODO: temporary - will have individual settings modals for each converter and show those instead
showSettingsModal = true;
};
@ -365,66 +366,8 @@
});
</script>
{#if showSettingsModal}
<Modal
icon={SearchIcon}
title="Conversion Settings"
color="purple"
buttons={[
{
text: "Cancel",
action: () => (showSettingsModal = false),
},
{
text: "Apply",
action: () => (showSettingsModal = false),
primary: true,
},
]}
onclose={() => (showSettingsModal = false)}
>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Format Settings</p>
<p class="text-sm text-muted font-normal">
Configure conversion options for {file?.name}
</p>
</div>
<div class="flex flex-col gap-2">
<p class="text-sm font-bold">Example dropdown</p>
<Dropdown
options={categories[currentCategory!]?.formats || []}
settingsStyle
{selected}
onselect={(e) => {
console.log("selected format", e);
selected = e;
}}
/>
</div>
<div class="flex flex-col gap-2">
<p class="text-sm font-bold">Resolution</p>
<FancyInput
type="text"
placeholder="1920x1080 or smth"
class="rounded-lg bg-button text-foreground p-3"
/>
</div>
<div class="flex flex-col gap-2">
<p class="text-sm font-bold">Frame Rate (FPS)</p>
<FancyInput
type="number"
placeholder="30"
class="rounded-lg bg-button text-foreground p-3"
/>
</div>
</div>
</div>
</Modal>
{#if showSettingsModal && file}
<SettingsModal {file} onclose={() => (showSettingsModal = false)} />
{/if}
<div
@ -499,7 +442,7 @@
<!-- search box -->
<div class="p-3 w-full">
<div class="relative">
<FancyInput
<input
type="text"
placeholder={m["convert.dropdown.placeholder"]()}
class="flex-grow w-full !pl-11 !pr-3 rounded-lg bg-panel text-foreground"
@ -577,8 +520,7 @@
{m["convert.archive_file.extract"]()}
</button>
</div>
{/if}
{#if file}
{:else if file}
<div class="border-t border-separator text-base p-2">
<button
class="w-full p-2 text-center rounded-lg bg-accent text-black"

View File

@ -0,0 +1,125 @@
<script lang="ts">
import { SearchIcon } from "lucide-svelte";
import Dropdown from "./Dropdown.svelte";
import FancyInput from "./FancyInput.svelte";
import Modal from "./Modal.svelte";
import { m } from "$lib/paraglide/messages";
import type { VertFile } from "$lib/types";
import type { SettingDefinition } from "$lib/types/conversion-settings";
type Props = {
file: VertFile | null;
onclose?: () => void;
};
let { file, onclose }: Props = $props();
const handleSettingChange = (key: string, value: any) => {
if (!file) return;
file.conversionSettings[key] = value;
};
const applySettings = () => {
onclose?.();
};
</script>
<Modal
icon={SearchIcon}
title="Conversion Settings"
color="purple"
buttons={[
{
text: "Cancel",
action: () => onclose?.(),
},
{
text: "Apply",
action: applySettings,
primary: true,
},
]}
onclose={() => onclose?.()}
>
<div class="flex flex-col gap-8">
{#if !file}
<p class="text-sm text-muted">No file selected</p>
{:else}
{@const settings = file.getAvailableSettings()}
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">
{m["settings.conversion.title"]?.() ||
"Conversion Settings"}
</p>
<p class="text-sm text-muted font-normal">
{m["settings.conversion.description"]?.() ||
`Configure conversion options for ${file.name}`}
</p>
</div>
{#if settings.length === 0}
<p class="text-sm text-muted">
{m["settings.conversion.no_settings"]?.() ||
"No settings available for this converter"}
</p>
{:else}
<div class="grid grid-cols-2 gap-4">
{#each settings as setting (setting.key)}
<div class="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">
{setting.description}
</p>
{/if}
{#if setting.type === "select"}
<Dropdown
options={setting.options?.map(
(opt) => opt.value,
) || []}
selected={file.conversionSettings[
setting.key
] ?? setting.default}
settingsStyle
onselect={(value) =>
handleSettingChange(setting.key, value)}
/>
{:else if setting.type === "boolean"}
<input
type="checkbox"
checked={file.conversionSettings[
setting.key
] ?? setting.default}
onchange={(e) =>
handleSettingChange(
setting.key,
e.currentTarget.checked,
)}
class="w-4 h-4"
/>
{:else}
<FancyInput
type={setting.type}
value={file.conversionSettings[
setting.key
] ?? setting.default}
oninput={(e) =>
handleSettingChange(
setting.key,
e.detail.value,
)}
/>
{/if}
</div>
{/each}
</div>
{/if}
</div>
{/if}
</div>
</Modal>

View File

@ -1,4 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { VertFile } from "$lib/types";
import type {
ConversionSettings,
SettingDefinition,
} from "$lib/types/conversion-settings";
export type WorkerStatus = "not-ready" | "downloading" | "ready" | "error";
@ -44,6 +50,27 @@ export class Converter {
this.startTimeout();
}
/**
* Get available settings for this converter.
* Can be overridden per converter for format-specific settings.
* @param input The input file.
*/
public getAvailableSettings(input: VertFile): SettingDefinition[] {
return [];
}
/**
* Get default settings for a conversion.
* @param input The input file.
*/
public getDefaultSettings(input: VertFile): ConversionSettings {
const defaults: ConversionSettings = {};
this.getAvailableSettings(input).forEach((setting) => {
defaults[setting.key] = setting.default;
});
return defaults;
}
private startTimeout() {
this.timeoutId = setTimeout(() => {
if (this.status !== "ready") this.status = "not-ready";
@ -63,11 +90,9 @@ export class Converter {
* @param to The format to convert to. Includes the dot.
*/
public async convert(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
input: VertFile,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
to: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
settings?: ConversionSettings,
...args: any[]
): Promise<VertFile> {
throw new Error("Not implemented");
@ -77,7 +102,6 @@ export class Converter {
* Cancel the active conversion of a file.
* @param input The input file.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async cancel(input: VertFile): Promise<void> {
throw new Error("Not implemented");
}

View File

@ -8,6 +8,10 @@ import { imageFormats } from "./magick-automated";
import { Settings } from "$lib/sections/settings/index.svelte";
import magickWasm from "@imagemagick/magick-wasm/magick.wasm?url";
import { ToastManager } from "$lib/util/toast.svelte";
import type {
SettingDefinition,
ConversionSettings,
} from "$lib/types/conversion-settings";
export class MagickConverter extends Converter {
public name = "imagemagick";
@ -112,6 +116,81 @@ export class MagickConverter extends Converter {
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getAvailableSettings(input: VertFile): SettingDefinition[] {
// images - quality/compression/quantize/interlace/depth-DPI, resize, crop, rotate, flip/flop, autoOrient?, color space/bit depth, transparency settings
const quality: SettingDefinition = {
key: "quality",
label: m["convert.settings.image.quality"](),
type: "number",
default: 100,
min: 0,
max: 100,
};
const depth: SettingDefinition = {
key: "depth",
label: m["convert.settings.image.depth"](),
type: "select",
default: "auto",
// somehow implement custom option
options: [
{ value: "auto", label: "Auto" },
{ value: "8", label: "8-bit" },
{ value: "16", label: "16-bit" },
{ value: "32", label: "32-bit" },
{ value: "custom", label: "Custom" },
],
};
const colorSpace: SettingDefinition = {
key: "colorSpace",
label: m["convert.settings.image.color_space"](),
type: "select",
default: "auto",
options: [
// what are these even lmao
{ value: "auto", label: "Auto" },
{ value: "srgb", label: "sRGB" },
{ value: "adobe98", label: "Adobe RGB" },
{ value: "prophoto", label: "ProPhoto RGB" },
{ value: "displayp3", label: "Display P3" },
{ value: "xyz", label: "CIEXYZ" },
{ value: "lab", label: "CIELAB" },
{ value: "gray", label: "Grayscale" },
],
};
// allow transparency or not
// TODO: disable if jpg/jpeg input/output
const transparency: SettingDefinition = {
key: "transparency",
label: m["convert.settings.image.transparency"](),
type: "boolean",
default: true,
};
const metadata: SettingDefinition = {
key: "metadata",
label: m["convert.settings.image.metadata"](),
type: "boolean",
default: true,
};
// resize, crop, rotate - prob want a ui
return [quality, depth, colorSpace, transparency, metadata];
}
public getDefaultSettings(input: VertFile): ConversionSettings {
const defaults: ConversionSettings = {};
this.getAvailableSettings(input).forEach((setting) => {
defaults[setting.key] = setting.default;
});
return defaults;
}
public async convert(
input: VertFile,
to: string,

View File

@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export type SettingType = "number" | "select" | "boolean" | "string" | "range";
export interface SettingDefinition {
key: string;
label: () => string;
type: SettingType;
default: any;
min?: number;
max?: number;
step?: number;
options?: Array<{ value: string; label: string }>; // for select types
description?: string;
}
export interface ConversionSettings {
[key: string]: any;
}

View File

@ -4,6 +4,10 @@ import { m } from "$lib/paraglide/messages";
import { ToastManager } from "$lib/util/toast.svelte";
import type { Component } from "svelte";
import { MAX_ARRAY_BUFFER_SIZE } from "$lib/store/index.svelte";
import type {
ConversionSettings,
SettingDefinition,
} from "./conversion-settings";
const MAX_BLOB_SIZE_LIMIT = 2 * 1024 * 1024 * 1024; // 2GB
@ -19,6 +23,7 @@ export class VertFile {
return this.file.name;
}
public conversionSettings = $state<ConversionSettings>({});
public progress = $state(0);
public result = $state<VertFile | null>(null);
@ -34,6 +39,12 @@ export class VertFile {
public isZip = $state(() => this.from === ".zip");
public getAvailableSettings(): SettingDefinition[] {
const converter = this.findConverter();
if (!converter) return [];
return converter.getAvailableSettings(this);
}
public findConverters(supportedFormats: string[] = [this.from]) {
const converter = this.converters
.filter((converter) =>
@ -120,7 +131,12 @@ export class VertFile {
// else convert normally
res = this.isZip()
? await this.convertZip(converter)
: await converter.convert(this, this.to, ...args);
: await converter.convert(
this,
this.to,
this.conversionSettings,
...args,
);
this.result = res;
} catch (err) {
if (!this.cancelled) this.toastErr(err);