vert/src/lib/components/functional/popups/SettingsModal.svelte

280 lines
7.6 KiB
Svelte

<script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */
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 { sanitize } from "$lib/store/index.svelte";
import { log } from "$lib/util/logger";
import { type ConversionSettings } from "$lib/types/conversion-settings";
type Props = {
file: VertFile | null;
onclose?: () => void;
};
let { file, onclose }: Props = $props();
const getAvailableConverters = (vertFile: VertFile) => {
return vertFile.isZip()
? vertFile.converters
: vertFile.findConverters([vertFile.from, vertFile.to]);
};
const getValidConverter = (vertFile: VertFile, converterName?: string) => {
const available = getAvailableConverters(vertFile);
const name = converterName || vertFile.conversionSettings.converter;
return available.find((c) => c.name === name) || available[0];
};
let settings = $derived<ConversionSettings>({
converter: file ? getValidConverter(file)?.name : undefined,
});
const handleSettingChange = (key: string, value: any) => {
if (!file) return;
settings[key] = value;
};
const applySettings = async (converterName: string) => {
if (!file) return;
const converter = getValidConverter(file, converterName);
if (!converter) {
log(
["settings", "modal"],
`No converter found for ${file.name}, cannot apply settings`,
);
return;
}
// apply defaults, then existing settings, then new settings on top
file.conversionSettings = {
...(await converter.getDefaultSettings(file)),
...file.conversionSettings,
...settings,
};
log(
["settings", "modal"],
`Applied settings for ${file.name}: ${JSON.stringify(file.conversionSettings, null, 2)}`,
);
};
</script>
<Modal
icon={SearchIcon}
title={m["convert.settings.title"]()}
color="purple"
buttons={[
{
text: "Cancel",
action: () => onclose?.(),
},
{
text: "Apply",
action: () => {
applySettings(settings.converter!);
onclose?.();
},
primary: true,
},
]}
onclose={() => onclose?.()}
>
<div class="flex flex-col gap-8 max-h-[calc(100vh-225px)] overflow-y-auto">
{#if file}
{@const availableConverters = getAvailableConverters(file)}
{@const validConverter = getValidConverter(
file,
settings.converter,
)}
<p class="text-base">
{@html sanitize(
m["convert.settings.description"]({
converter: validConverter?.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={validConverter?.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 -->
{#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>
<!-- 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[
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;
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>