This commit is contained in:
Flavio Fois
2026-02-04 19:57:31 +01:00
parent 0d6157b2ff
commit 0cda0a26fc
25 changed files with 1549 additions and 66 deletions

View File

@@ -1,6 +1,7 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from en!",
"layout_loading_text": "Loading...",
"error_unexpected": "An unexpected error occurred",
"sidebar_overview": "Mail Viewer",
"sidebar_settings": "Settings",
@@ -29,6 +30,10 @@
"settings_preview_pdf_builtin_label": "Use built-in viewer for PDFs",
"settings_preview_pdf_builtin_hint": "Uses EMLy's built-in viewer for PDF files.",
"settings_preview_pdf_builtin_info": "Info: If disabled, PDF files will be treated as downloads instead of being previewed within the app.",
"settings_msg_converter_title": "MSG Handling",
"settings_msg_converter_description": "Configure how MSG files are processed.",
"settings_msg_converter_label": "Use MSG to EML converter",
"settings_msg_converter_hint": "Uses an external app to convert .MSG files to .EML files. Disable it to use the OSS version (less accurate).",
"settings_danger_zone_title": "Danger Zone",
"settings_danger_zone_description": "Advanced actions. Proceed with caution.",
"settings_danger_devtools_label": "Open DevTools",
@@ -56,9 +61,9 @@
"settings_unsaved_toast_save": "Save changes",
"settings_unsaved_toast_reset": "Reset",
"mail_no_email_selected": "No email selected",
"mail_open_eml_btn": "Open EML File",
"mail_open_eml_btn": "Open EML/MSG File",
"mail_subject_no_subject": "(No Subject)",
"mail_open_btn_label": "Open EML file",
"mail_open_btn_label": "Open EML/MSG file",
"mail_open_btn_title": "Open another file",
"mail_close_btn_label": "Close",
"mail_close_btn_title": "Close",
@@ -72,8 +77,13 @@
"mail_error_image": "Failed to open image file.",
"settings_toast_language_changed": "Language changed successfully!",
"settings_toast_language_change_failed": "Failed to change language.",
"mail_open_btn_text": "Open EML File",
"mail_open_btn_text": "Open EML/MSG File",
"mail_close_btn_text": "Close",
"settings_danger_reset_dialog_description_part1": "This action cannot be undone.",
"settings_danger_reset_dialog_description_part2": "This will permanently delete your current settings and return the app to its default state."
"settings_danger_reset_dialog_description_part2": "This will permanently delete your current settings and return the app to its default state.",
"mail_error_opening": "Failed to open EML file.",
"mail_pec_signed_badge": "Signed mail",
"mail_pec_feature_warning": "PEC detected: some features may be limited.",
"mail_sign_label": "Sign:",
"mail_loading_msg_conversion": "Converting MSG file... This might take a while."
}

View File

@@ -1,6 +1,7 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Ciao, {name} in it!",
"layout_loading_text": "Caricamento...",
"error_unexpected": "Si è verificato un errore imprevisto",
"sidebar_overview": "Visualizza Mail",
"sidebar_settings": "Impostazioni",
@@ -29,6 +30,10 @@
"settings_preview_pdf_builtin_label": "Usa visualizzatore integrato per PDF",
"settings_preview_pdf_builtin_hint": "Usa il visualizzatore integrato di EMLy per i file PDF.",
"settings_preview_pdf_builtin_info": "Info: Se disabilitato, i file PDF verranno trattati come download invece di essere visualizzati nell'app.",
"settings_msg_converter_title": "Gestione MSG",
"settings_msg_converter_description": "Configura come vengono elaborati i file MSG.",
"settings_msg_converter_label": "Usa convertitore MSG in EML",
"settings_msg_converter_hint": "Usa un'applicazione esterna per convertire i file .MSG in .EML. Disabilitalo per usare la versione OSS (meno accurata).",
"settings_danger_zone_title": "Zona Pericolo",
"settings_danger_zone_description": "Azioni avanzate. Procedere con cautela.",
"settings_danger_devtools_label": "Apri DevTools",
@@ -56,9 +61,9 @@
"settings_unsaved_toast_save": "Salva",
"settings_unsaved_toast_reset": "Ripristina",
"mail_no_email_selected": "Nessuna email selezionata",
"mail_open_eml_btn": "Apri File EML",
"mail_open_eml_btn": "Apri File EML/MSG",
"mail_subject_no_subject": "(Nessun Oggetto)",
"mail_open_btn_label": "Apri file EML",
"mail_open_btn_label": "Apri file EML/MSG",
"mail_open_btn_title": "Apri un altro file",
"mail_close_btn_label": "Chiudi",
"mail_close_btn_title": "Chiudi",
@@ -72,8 +77,13 @@
"mail_error_image": "Impossibile aprire il file immagine.",
"settings_toast_language_changed": "Lingua cambiata con successo!",
"settings_toast_language_change_failed": "Impossibile cambiare lingua.",
"mail_open_btn_text": "Apri File EML",
"mail_open_btn_text": "Apri file EML/MSG",
"mail_close_btn_text": "Chiudi",
"settings_danger_reset_dialog_description_part1": "Questa azione non può essere annullata.",
"settings_danger_reset_dialog_description_part2": "Questo eliminerà permanentemente le tue impostazioni attuali e riporterà l'app allo stato predefinito."
"settings_danger_reset_dialog_description_part2": "Questo eliminerà permanentemente le tue impostazioni attuali e riporterà l'app allo stato predefinito.",
"mail_error_opening": "Impossibile aprire il file EML.",
"mail_pec_signed_badge": "Mail firmata",
"mail_pec_feature_warning": "PEC rilevata: alcune funzionalità potrebbero essere limitate.",
"mail_sign_label": "Firma:",
"mail_loading_msg_conversion": "Conversione file MSG in corso... Potrebbe richiedere del tempo."
}

View File

@@ -67,7 +67,7 @@
<body data-sveltekit-preload-data="hover">
<div id="app-loading">
<div class="loader-spinner"></div>
<div>Loading, please wait.</div>
<div>Loading, please wait...</div>
</div>
<div style="display: contents;">%sveltekit.body%</div>
</body>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { X, MailOpen, Image, FileText, File } from "@lucide/svelte";
import { ShowOpenFileDialog, ReadEML, OpenPDF, OpenImageWindow, OpenPDFWindow, OpenImage } from "$lib/wailsjs/go/main/App";
import { X, MailOpen, Image, FileText, File, ShieldCheck, Shield, Signature, FileUser, Loader2 } from "@lucide/svelte";
import { ShowOpenFileDialog, ReadEML, OpenPDF, OpenImageWindow, OpenPDFWindow, OpenImage, ReadMSG, ReadPEC, OpenEMLWindow } from "$lib/wailsjs/go/main/App";
import type { internal } from "$lib/wailsjs/go/models";
import { sidebarOpen } from "$lib/stores/app";
import { onDestroy, onMount } from "svelte";
@@ -13,6 +13,7 @@
let unregisterEvents = () => {};
let isLoading = $state(false);
let loadingText = $state("");
function onClear() {
mailState.clear();
@@ -33,19 +34,60 @@
onMount(async () => {
// Listen for second instance args
unregisterEvents = EventsOn("launchArgs", async (args: string[]) => {
console.log("got event launchArgs:", args);
if (args && args.length > 0) {
for (const arg of args) {
if (arg.toLowerCase().endsWith(".eml")) {
console.log("Loading EML from second instance:", arg);
const lowerArg = arg.toLowerCase();
if (lowerArg.endsWith(".eml") || lowerArg.endsWith(".msg")) {
console.log("Loading file from second instance:", arg);
isLoading = true;
loadingText = m.layout_loading_text();
try {
const emlContent = await ReadEML(arg);
let emlContent;
if (lowerArg.endsWith(".msg")) {
const useExt = settingsStore.settings.useMsgConverter ?? true;
if (useExt) {
loadingText = m.mail_loading_msg_conversion();
}
emlContent = await ReadMSG(arg, useExt);
if(emlContent.isPec) {
toast.warning(m.mail_pec_feature_warning());
}
} else {
// EML handling
try {
emlContent = await ReadPEC(arg);
if(emlContent.isPec) {
toast.warning(m.mail_pec_feature_warning());
}
} catch (e) {
console.warn("ReadPEC failed, trying ReadEML:", e);
emlContent = await ReadEML(arg);
}
if (emlContent && emlContent.body) {
const trimmed = emlContent.body.trim();
const clean = trimmed.replace(/[\s\r\n]+/g, '');
if (clean.length > 0 && clean.length % 4 === 0 && /^[A-Za-z0-9+/]+=*$/.test(clean)) {
try {
emlContent.body = window.atob(clean);
} catch (e) { }
}
}
}
mailState.setParams(emlContent);
sidebarOpen.set(false);
WindowUnminimise();
WindowShow();
} catch (error) {
console.error("Failed to load email:", error);
toast.error("Failed to load email file");
} finally {
isLoading = false;
loadingText = "";
}
break;
}
@@ -80,23 +122,49 @@
}
}
async function openEMLHandler(base64Data: string, filename: string) {
try {
await OpenEMLWindow(base64Data, filename);
} catch (error) {
console.error("Failed to open EML:", error);
toast.error("Failed to open EML attachment");
}
}
async function onOpenMail() {
isLoading = true;
loadingText = m.layout_loading_text();
const result = await ShowOpenFileDialog();
if (result && result.length > 0) {
// Handle opening the mail file
try {
const email: internal.EmailData = await ReadEML(result);
// If the file is .eml, otherwise if is .msg, read accordingly
let email: internal.EmailData;
if(result.toLowerCase().endsWith(".msg")) {
const useExt = settingsStore.settings.useMsgConverter ?? true;
if (useExt) {
loadingText = m.mail_loading_msg_conversion();
}
email = await ReadMSG(result, useExt);
} else {
email = await ReadEML(result);
}
if(email.isPec) {
toast.warning(m.mail_pec_feature_warning(), {duration: 10000});
}
mailState.setParams(email);
sidebarOpen.set(false);
} catch (error) {
console.error("Failed to read EML file:", error);
toast.error(m.mail_error_opening());
} finally {
isLoading = false;
loadingText = "";
}
} else {
isLoading = false;
loadingText = "";
}
}
@@ -127,6 +195,12 @@
</script>
<div class="panel fill" aria-label="Events">
{#if isLoading}
<div class="loading-overlay">
<Loader2 class="spinner" size="48" />
<div class="loading-text">{loadingText}</div>
</div>
{/if}
<div class="events" role="log" aria-live="polite">
{#if mailState.currentEmail === null}
<div class="empty-state">
@@ -187,6 +261,14 @@
<span class="label">{m.mail_bcc()}</span>
<span class="value">{mailState.currentEmail.bcc.join(", ")}</span>
{/if}
{#if mailState.currentEmail.isPec}
<span class="label">{m.mail_sign_label()}</span>
<span class="value"><span class="pec-badge" title="Posta Elettronica Certificata">
<ShieldCheck size="14" />
PEC
</span></span>
{/if}
</div>
</div>
@@ -211,6 +293,32 @@
<FileText size="15" />
<span class="att-name">{att.filename}</span>
</button>
{:else if att.filename.toLowerCase().endsWith(".eml")}
<button
class="att-btn eml"
onclick={() => openEMLHandler(arrayBufferToBase64(att.data), att.filename)}
>
<MailOpen size="14" />
<span class="att-name">{att.filename}</span>
</button>
{:else if mailState.currentEmail.isPec && att.filename.toLowerCase().endsWith(".p7s")}
<a
class="att-btn file"
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
download={att.filename}
>
<Signature size="14" />
<span class="att-name">{att.filename}</span>
</a>
{:else if mailState.currentEmail.isPec && att.filename.toLowerCase() === "daticert.xml"}
<a
class="att-btn file"
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
download={att.filename}
>
<FileUser size="14" />
<span class="att-name">{att.filename}</span>
</a>
{:else}
<a
class="att-btn file"
@@ -247,6 +355,38 @@
</div>
<style>
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 50;
backdrop-filter: blur(4px);
gap: 16px;
}
/* Make sure internal loader spins if not using class-based animation library like Tailwind */
:global(.spinner) {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loading-text {
color: white;
font-size: 16px;
font-weight: 500;
}
.panel {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
@@ -403,6 +543,9 @@
.att-btn.pdf { color: #f87171; border-color: rgba(248, 113, 113, 0.3); }
.att-btn.pdf:hover { color: #fca5a5; }
.att-btn.eml { color: hsl(49, 80%, 49%); border-color: rgba(224, 206, 39, 0.3); }
.att-btn.eml:hover { color: hsl(49, 80%, 65%); }
.att-name {
white-space: nowrap;
overflow: hidden;
@@ -497,4 +640,42 @@
color: rgba(255, 255, 255, 0.4);
font-style: italic;
}
.badged-row {
display: flex;
gap: 8px;
align-items: center;
}
.signed-badge {
display: inline-flex;
align-items: center;
gap: 4px;
background: rgba(239, 68, 68, 0.15);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.3);
padding: 2px 6px;
border-radius: 6px;
font-size: 11px;
font-weight: 700;
vertical-align: middle;
user-select: none;
width: fit-content;
}
.pec-badge {
display: inline-flex;
align-items: center;
gap: 4px;
background: rgba(16, 185, 129, 0.15);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.3);
padding: 2px 6px;
border-radius: 6px;
font-size: 11px;
font-weight: 700;
vertical-align: middle;
user-select: none;
width: fit-content;
}
</style>

View File

@@ -6,6 +6,7 @@ interface EMLy_GUI_Settings {
selectedLanguage: SupportedLanguages = "en" | "it";
useBuiltinPreview: boolean;
useBuiltinPDFViewer?: boolean;
useMsgConverter?: boolean;
previewFileSupportedTypes?: SupportedFileTypePreview[];
}

View File

@@ -20,6 +20,8 @@ export function GetViewerData():Promise<main.ViewerData>;
export function OpenDefaultAppsSettings():Promise<void>;
export function OpenEMLWindow(arg1:string,arg2:string):Promise<void>;
export function OpenImage(arg1:string,arg2:string):Promise<void>;
export function OpenImageWindow(arg1:string,arg2:string):Promise<void>;
@@ -32,6 +34,12 @@ export function QuitApp():Promise<void>;
export function ReadEML(arg1:string):Promise<internal.EmailData>;
export function ReadMSG(arg1:string,arg2:boolean):Promise<internal.EmailData>;
export function ReadMSGOSS(arg1:string):Promise<internal.EmailData>;
export function ReadPEC(arg1:string):Promise<internal.EmailData>;
export function SaveConfig(arg1:utils.Config):Promise<void>;
export function ShowOpenFileDialog():Promise<string>;

View File

@@ -34,6 +34,10 @@ export function OpenDefaultAppsSettings() {
return window['go']['main']['App']['OpenDefaultAppsSettings']();
}
export function OpenEMLWindow(arg1, arg2) {
return window['go']['main']['App']['OpenEMLWindow'](arg1, arg2);
}
export function OpenImage(arg1, arg2) {
return window['go']['main']['App']['OpenImage'](arg1, arg2);
}
@@ -58,6 +62,18 @@ export function ReadEML(arg1) {
return window['go']['main']['App']['ReadEML'](arg1);
}
export function ReadMSG(arg1, arg2) {
return window['go']['main']['App']['ReadMSG'](arg1, arg2);
}
export function ReadMSGOSS(arg1) {
return window['go']['main']['App']['ReadMSGOSS'](arg1);
}
export function ReadPEC(arg1) {
return window['go']['main']['App']['ReadPEC'](arg1);
}
export function SaveConfig(arg1) {
return window['go']['main']['App']['SaveConfig'](arg1);
}

View File

@@ -199,6 +199,8 @@ export namespace internal {
subject: string;
body: string;
attachments: EmailAttachment[];
isPec: boolean;
hasInnerEmail: boolean;
static createFrom(source: any = {}) {
return new EmailData(source);
@@ -213,6 +215,8 @@ export namespace internal {
this.subject = source["subject"];
this.body = source["body"];
this.attachments = this.convertValues(source["attachments"], EmailAttachment);
this.isPec = source["isPec"];
this.hasInnerEmail = source["hasInnerEmail"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {

View File

@@ -7,11 +7,11 @@
import "../layout.css";
import { onMount } from "svelte";
import * as m from "$lib/paraglide/messages.js";
import { GetConfig } from "$lib/wailsjs/go/main/App";
import type { utils } from "$lib/wailsjs/go/models";
import { Toaster } from "$lib/components/ui/sonner/index.js";
import AppSidebar from "$lib/components/SidebarApp.svelte";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import { dev } from '$app/environment';
import {
PanelRightClose,
PanelRightOpen,
@@ -20,6 +20,7 @@
} from "@lucide/svelte";
import { Separator } from "$lib/components/ui/separator/index.js";
import { toast } from "svelte-sonner";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import {
WindowMinimise,
@@ -28,6 +29,7 @@
WindowIsMaximised,
Quit,
} from "$lib/wailsjs/runtime/runtime";
import { RefreshCcwDot } from "@lucide/svelte";
let versionInfo: utils.Config | null = $state(null);
let isMaximized = $state(false);
@@ -65,10 +67,10 @@
}
onMount(async () => {
versionInfo = await GetConfig();
versionInfo = data.data as utils.Config;
});
let { children } = $props();
let { data, children } = $props();
const THEME_KEY = "emly_theme";
let theme = $state<"dark" | "light">("dark");
@@ -109,24 +111,24 @@
<div class="title">
<bold>EMLy</bold>
<div class="version-wrapper">
<version
>v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy
.GUIReleaseChannel}</version
>
<version>
{#if dev}
v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy.GUIReleaseChannel} <debug>(DEBUG BUILD)</debug>
{:else}
v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy.GUIReleaseChannel}
{/if}
</version>
{#if versionInfo}
<div class="version-tooltip">
<div class="tooltip-item">
<span class="label">GUI:</span>
<span class="value">v{versionInfo.EMLy.GUISemver}</span>
<span class="channel">({versionInfo.EMLy.GUIReleaseChannel})</span
>
<span class="channel">({versionInfo.EMLy.GUIReleaseChannel})</span>
</div>
<div class="tooltip-item">
<span class="label">SDK:</span>
<span class="value">v{versionInfo.EMLy.SDKDecoderSemver}</span>
<span class="channel"
>({versionInfo.EMLy.SDKDecoderReleaseChannel})</span
>
<span class="channel">({versionInfo.EMLy.SDKDecoderReleaseChannel})</span>
</div>
</div>
{/if}
@@ -159,8 +161,10 @@
{#await navigating?.complete}
<div class="loading-overlay">
<div class="spinner"></div>
<span style="opacity: 0.5; font-size: 13px">Loading...</span>
</div>
<span style="opacity: 0.5; font-size: 13px"
>{m.layout_loading_text()}</span
>
</div>
{:then}
{@render children()}
{/await}
@@ -192,7 +196,7 @@
<House
size="16"
onclick={() => {
if(page.url.pathname !== "/") goto("/");
if (page.url.pathname !== "/") goto("/");
}}
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
@@ -200,11 +204,26 @@
<Settings
size="16"
onclick={() => {
if (page.url.pathname !== "/settings" && page.url.pathname !== "/settings/") goto("/settings");
if (
page.url.pathname !== "/settings" &&
page.url.pathname !== "/settings/"
)
goto("/settings");
}}
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
<a
data-sveltekit-reload
href="/"
class={`${buttonVariants({ variant: "destructive" })} cursor-pointer hover:cursor-pointer`}
style="text-decoration: none; margin-left: auto; height: 24px; font-size: 12px; padding: 0 8px;"
aria-label={m.settings_danger_reload_button()}
title={m.settings_danger_reload_button() + " app"}
>
<RefreshCcwDot />
</a>
</div>
<div style="display:none">
@@ -275,6 +294,12 @@
opacity: 0.4;
}
.title version debug{
color: #e11d48;
opacity: 1;
font-weight: 600;
}
.version-wrapper {
position: relative;
display: inline-block;

View File

@@ -0,0 +1,12 @@
import type { LayoutLoad } from './$types';
import { GetConfig } from "$lib/wailsjs/go/main/App";
export const load = (async () => {
try {
const config = await GetConfig();
return { data: config, error: null };
} catch (e) {
console.error("Failed to load config:", e);
return { data: null, error: 'Failed to load config' };
}
}) satisfies LayoutLoad;

View File

@@ -1,7 +1,9 @@
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { GetViewerData, GetStartupFile, ReadEML } from '$lib/wailsjs/go/main/App';
import { GetViewerData, GetStartupFile, ReadEML, ReadMSG } from '$lib/wailsjs/go/main/App';
import DOMPurify from 'dompurify';
import { settingsStore } from '$lib/stores/settings.svelte';
import type { internal } from '$lib/wailsjs/go/models';
export const load: PageLoad = async () => {
try {
@@ -18,7 +20,15 @@ export const load: PageLoad = async () => {
// Check if opened with a file
const startupFile = await GetStartupFile();
if (startupFile) {
const emlContent = await ReadEML(startupFile);
let emlContent: internal.EmailData;
if (startupFile.toLowerCase().endsWith(".msg")) {
const useExt = settingsStore.settings.useMsgConverter ?? true;
emlContent = await ReadMSG(startupFile, useExt);
} else {
emlContent = await ReadEML(startupFile);
}
if (emlContent) {
emlContent.body = DOMPurify.sanitize(emlContent.body || "");
return { email: emlContent };

View File

@@ -32,6 +32,7 @@
selectedLanguage: "it",
useBuiltinPreview: true,
useBuiltinPDFViewer: true,
useMsgConverter: true,
previewFileSupportedTypes: ["jpg", "jpeg", "png"],
};
@@ -59,6 +60,7 @@
useBuiltinPreview: !!s.useBuiltinPreview,
useBuiltinPDFViewer:
s.useBuiltinPDFViewer ?? defaults.useBuiltinPDFViewer ?? true,
useMsgConverter: s.useMsgConverter ?? defaults.useMsgConverter ?? true,
previewFileSupportedTypes:
s.previewFileSupportedTypes || defaults.previewFileSupportedTypes || [],
};
@@ -69,6 +71,7 @@
(a.selectedLanguage ?? "") === (b.selectedLanguage ?? "") &&
!!a.useBuiltinPreview === !!b.useBuiltinPreview &&
!!a.useBuiltinPDFViewer === !!b.useBuiltinPDFViewer &&
!!a.useMsgConverter === !!b.useMsgConverter &&
JSON.stringify(a.previewFileSupportedTypes?.sort()) ===
JSON.stringify(b.previewFileSupportedTypes?.sort())
);
@@ -363,6 +366,35 @@
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title>{m.settings_msg_converter_title()}</Card.Title>
<Card.Description
>{m.settings_msg_converter_description()}</Card.Description
>
</Card.Header>
<Card.Content class="space-y-4">
<div class="space-y-3">
<div
class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4"
>
<div>
<Label class="text-base">
{m.settings_msg_converter_label()}
</Label>
<p class="text-sm text-muted-foreground">
{m.settings_msg_converter_hint()}
</p>
</div>
<Switch
bind:checked={form.useMsgConverter}
class="cursor-pointer hover:cursor-pointer"
/>
</div>
</div>
</Card.Content>
</Card.Root>
{#if $dangerZoneEnabled}
<Card.Root class="border-destructive/50 bg-destructive/15">
<Card.Header class="space-y-1">