This commit is contained in:
Lyz Coote
2026-02-02 18:41:13 +01:00
commit d6a5cb8a67
161 changed files with 8630 additions and 0 deletions

View File

@@ -0,0 +1,444 @@
<script lang="ts">
import { browser } from "$app/environment";
import { page, navigating } from "$app/state";
import { beforeNavigate, goto } from "$app/navigation";
import { locales, localizeHref } from "$lib/paraglide/runtime";
import { unsavedChanges, sidebarOpen } from "$lib/stores/app";
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 {
PanelRightClose,
PanelRightOpen,
House,
Settings,
} from "@lucide/svelte";
import { Separator } from "$lib/components/ui/separator/index.js";
import { toast } from "svelte-sonner";
import {
WindowMinimise,
WindowMaximise,
WindowUnmaximise,
WindowIsMaximised,
Quit,
} from "$lib/wailsjs/runtime/runtime";
let versionInfo: utils.Config | null = $state(null);
let isMaximized = $state(false);
async function syncMaxState() {
isMaximized = await WindowIsMaximised();
}
beforeNavigate(({ cancel }) => {
if ($unsavedChanges) {
toast.warning(m.unsaved_changes_warning());
cancel();
}
});
async function toggleMaximize() {
if (isMaximized) {
WindowUnmaximise();
} else {
WindowMaximise();
}
isMaximized = !isMaximized;
}
function minimize() {
WindowMinimise();
}
function closeWindow() {
Quit();
}
function onTitlebarDblClick() {
toggleMaximize();
}
onMount(async () => {
versionInfo = await GetConfig();
});
let { children } = $props();
const THEME_KEY = "emly_theme";
let theme = $state<"dark" | "light">("dark");
function applyTheme(next: "dark" | "light") {
theme = next;
if (!browser) return;
document.documentElement.classList.toggle("dark", next === "dark");
try {
localStorage.setItem(THEME_KEY, next);
} catch {
// ignore
}
}
$effect(() => {
if (!browser) return;
let stored: string | null = null;
try {
stored = localStorage.getItem(THEME_KEY);
} catch {
stored = null;
}
applyTheme(stored === "light" ? "light" : "dark");
});
syncMaxState();
</script>
<div class="app">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="titlebar"
ondblclick={onTitlebarDblClick}
style="--wails-draggable:drag"
>
<div class="title">
<bold>EMLy</bold>
<div class="version-wrapper">
<version
>v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy
.GUIReleaseChannel}</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
>
</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
>
</div>
</div>
{/if}
</div>
</div>
<div class="controls">
<button class="btn" onclick={minimize}>─</button>
<button class="btn" onclick={toggleMaximize}>
{#if isMaximized}
{:else}
{/if}
</button>
<button class="btn close" onclick={closeWindow}>✕</button>
</div>
</div>
<div class="content">
<Sidebar.Provider>
{#if $sidebarOpen}
<AppSidebar />
{/if}
<main>
<!-- <Sidebar.Trigger /> -->
<Toaster />
{#await navigating?.complete}
<div class="loading-overlay">
<div class="spinner"></div>
<span style="opacity: 0.5; font-size: 13px">Loading...</span>
</div>
{:then}
{@render children()}
{/await}
</main>
</Sidebar.Provider>
</div>
<div class="footerbar">
{#if !$sidebarOpen}
<PanelRightClose
size="17"
onclick={() => {
$sidebarOpen = !$sidebarOpen;
}}
style="cursor: pointer;"
/>
{:else}
<PanelRightOpen
size="17"
onclick={() => {
$sidebarOpen = !$sidebarOpen;
}}
style="cursor: pointer;"
/>
{/if}
<Separator orientation="vertical" />
<House
size="16"
onclick={() => {
if(page.url.pathname !== "/") goto("/");
}}
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
<Settings
size="16"
onclick={() => {
if (page.url.pathname !== "/settings" && page.url.pathname !== "/settings/") goto("/settings");
}}
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
</div>
<div style="display:none">
{#each locales as locale}
<a href={localizeHref(page.url.pathname, { locale })}>
{locale}
</a>
{/each}
</div>
</div>
<style>
:global(body) {
margin: 0;
background: oklch(0 0 0);
color: #eaeaea;
font-family: system-ui, sans-serif;
}
.app {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.titlebar {
height: 32px;
background: oklch(0 0 0);
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 12px;
-webkit-app-region: drag;
user-select: none;
flex: 0 0 32px;
z-index: 50;
position: relative;
}
.footerbar {
height: 32px;
background: oklch(0 0 0);
display: flex;
align-items: center;
justify-content: flex-start;
gap: 12px;
padding: 0 12px;
user-select: none;
flex: 0 0 32px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.title {
font-size: 13px;
opacity: 0.9;
color: gray;
}
.title bold {
font-weight: 600;
color: white;
opacity: 0.7;
}
.title version {
color: rgb(228, 221, 221);
opacity: 0.4;
}
.version-wrapper {
position: relative;
display: inline-block;
cursor: default;
}
.version-tooltip {
visibility: hidden;
opacity: 0;
position: absolute;
top: 100%;
left: 0;
background-color: #111;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
padding: 8px 12px;
z-index: 1000;
margin-top: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
transition: all 0.2s ease-in-out;
transform: translateY(-5px);
pointer-events: none;
-webkit-app-region: no-drag;
}
.version-wrapper:hover .version-tooltip {
visibility: visible;
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.tooltip-item {
display: grid;
grid-template-columns: 40px auto auto;
gap: 8px;
font-size: 11px;
line-height: 1.6;
white-space: nowrap;
align-items: center;
}
.tooltip-item .label {
color: #9ca3af;
}
.tooltip-item .value {
color: #f3f4f6;
font-family: monospace;
}
.tooltip-item .channel {
color: #6b7280;
font-size: 10px;
}
.controls {
display: flex;
height: 100%;
opacity: 0.5;
}
.btn {
width: 46px;
height: 100%;
border: none;
background: transparent;
color: white;
font-size: 14px;
cursor: pointer;
-webkit-app-region: no-drag;
}
.btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background: rgba(255, 255, 255, 0.02);
}
.close:hover {
background: #e81123;
}
.content {
flex: 1 1 auto;
min-height: 0;
display: flex;
background: oklch(0 0 0);
overflow: hidden;
position: relative;
}
main {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
overflow: auto;
position: relative;
}
/* Override Shadcn Sidebar defaults to fit in content area */
:global(.content .group\/sidebar-wrapper) {
min-height: 0 !important;
height: 100% !important;
}
/* Target the fixed container of the sidebar */
:global(.content [data-slot="sidebar-container"]) {
position: absolute !important;
height: 100% !important;
/* Ensure it doesn't take viewport height */
max-height: 100% !important;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
::-webkit-scrollbar-corner {
background: transparent;
}
.loading-overlay {
position: absolute;
inset: 0;
z-index: 50;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
justify-content: center;
background: oklch(0 0 0);
}
.spinner {
width: 32px;
height: 32px;
border: 2px solid rgba(255, 255, 255, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import MailViewer from "$lib/components/dashboard/MailViewer.svelte";
import { mailState } from "$lib/stores/mail-state.svelte";
let { data } = $props();
$effect(() => {
if (data.email) {
mailState.setParams(data.email);
}
});
</script>
<div class="page">
<section class="center" aria-label="Overview">
<MailViewer />
</section>
</div>
<style>
.page {
height: 100%;
min-height: 0;
display: flex;
gap: 12px;
padding: 12px;
box-sizing: border-box;
overflow: hidden;
}
.center {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
::-webkit-scrollbar-corner {
background: transparent;
}
</style>

View File

@@ -0,0 +1,32 @@
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { GetImageViewerData, GetStartupFile, ReadEML } from '$lib/wailsjs/go/main/App';
import DOMPurify from 'dompurify';
export const load: PageLoad = async () => {
try {
// Check if we are in viewer mode
const viewerData = await GetImageViewerData();
if (viewerData && viewerData.data) {
throw redirect(302, "/image-viewer");
}
// Check if opened with a file
const startupFile = await GetStartupFile();
if (startupFile) {
const emlContent = await ReadEML(startupFile);
if (emlContent) {
emlContent.body = DOMPurify.sanitize(emlContent.body || "");
return { email: emlContent };
}
}
} catch (e) {
// If it's a redirect, re-throw it so SvelteKit handles it
if ((e as any)?.status === 302 || (e as any)?.status === 307 || (e as any)?.status === 303 || (e as any)?.location) {
throw e;
}
console.error("Error in load function:", e);
}
return { email: null };
};

View File

@@ -0,0 +1,478 @@
<script lang="ts">
import { browser } from "$app/environment";
import { goto } from "$app/navigation";
import { Button } from "$lib/components/ui/button";
import * as Card from "$lib/components/ui/card";
import { Label } from "$lib/components/ui/label";
import { Separator } from "$lib/components/ui/separator";
import { Switch } from "$lib/components/ui/switch";
import { ChevronLeft, Command, Option, Flame } from "@lucide/svelte";
import type { EMLy_GUI_Settings } from "$lib/types";
import { toast } from "svelte-sonner";
import { It, Us } from "svelte-flags";
import * as RadioGroup from "$lib/components/ui/radio-group/index.js";
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { Checkbox } from "$lib/components/ui/checkbox/index.js";
import {
dismissUnsavedChangesToast,
showUnsavedChangesToast,
} from "$lib/utils/unsaved-changes-toast";
import { dangerZoneEnabled, unsavedChanges } from "$lib/stores/app";
import { LogDebug } from "$lib/wailsjs/runtime/runtime";
import { settingsStore } from "$lib/stores/settings.svelte";
import { GetMachineData } from "$lib/wailsjs/go/main/App";
import * as m from "$lib/paraglide/messages";
import { setLocale } from "$lib/paraglide/runtime";
import { mailState } from "$lib/stores/mail-state.svelte.js";
let { data } = $props();
let machineData = $derived(data.machineData);
let config = $derived(data.config);
const defaults: EMLy_GUI_Settings = {
selectedLanguage: "it",
useBuiltinPreview: true,
previewFileSupportedTypes: ["jpg", "jpeg", "png"],
};
async function setLanguage(lang: EMLy_GUI_Settings["selectedLanguage"] | null) {
if (!browser) return;
try {
await setLocale(lang || "en", {reload: false});
toast.success(m.settings_toast_language_changed());
} catch {
toast.error(m.settings_toast_language_change_failed());
}
}
// Clone store state for form editing
let form = $state<EMLy_GUI_Settings>({ ...settingsStore.settings });
let lastSaved = $state<EMLy_GUI_Settings>({
...settingsStore.settings,
});
let dangerWarningOpen = $state(false);
function normalizeSettings(
s: EMLy_GUI_Settings,
): EMLy_GUI_Settings {
return {
selectedLanguage: s.selectedLanguage || defaults.selectedLanguage || "en",
useBuiltinPreview: !!s.useBuiltinPreview,
previewFileSupportedTypes:
s.previewFileSupportedTypes || defaults.previewFileSupportedTypes || [],
};
}
function isSameSettings(
a: EMLy_GUI_Settings,
b: EMLy_GUI_Settings,
) {
return (
(a.selectedLanguage ?? "") === (b.selectedLanguage ?? "") &&
!!a.useBuiltinPreview === !!b.useBuiltinPreview &&
JSON.stringify(a.previewFileSupportedTypes?.sort()) ===
JSON.stringify(b.previewFileSupportedTypes?.sort())
);
}
function resetToLastSaved() {
form = { ...lastSaved };
toast.info(m.settings_toast_reverted());
}
async function saveToStorage() {
if (!browser) return;
const settings = normalizeSettings(form);
const languageChanged =
settings.selectedLanguage !== lastSaved.selectedLanguage;
try {
settingsStore.update(settings);
} catch {
toast.error(m.settings_toast_save_failed());
return;
}
lastSaved = settings;
form = settings;
if (languageChanged) {
await setLanguage(settings.selectedLanguage);
location.reload();
} else {
toast.success(m.settings_toast_saved());
}
}
async function resetToDefaults() {
form = normalizeSettings(defaults);
lastSaved = normalizeSettings(defaults);
// Save to storage
if (browser) {
try {
settingsStore.reset();
settingsStore.update(form); // Ensure local form state is persisted
sessionStorage.removeItem("debugWindowInSettings");
dangerZoneEnabled.set(false);
LogDebug("Reset danger zone setting to false.");
} catch {
toast.error(m.settings_toast_reset_failed());
return;
}
}
await setLanguage(form.selectedLanguage);
mailState.clear();
toast.info(m.settings_toast_reset_success());
location.reload();
}
$effect(() => {
if (!browser) return;
const dirty = !isSameSettings(normalizeSettings(form), lastSaved);
unsavedChanges.set(dirty);
if (dirty) {
showUnsavedChangesToast({
onSave: saveToStorage,
onReset: resetToLastSaved,
});
} else {
dismissUnsavedChangesToast();
}
});
$effect(() => {
// Sync initial state from store when hydrated
// Ensure we don't update if the values are already practically identical to avoid loops
if (
settingsStore.hasHydrated &&
isSameSettings(lastSaved, defaults) &&
!isSameSettings(lastSaved, settingsStore.settings)
) {
form = { ...settingsStore.settings };
lastSaved = { ...settingsStore.settings };
}
});
let previousDangerZoneEnabled = $dangerZoneEnabled;
$effect(() => {
(async () => {
if ($dangerZoneEnabled && !previousDangerZoneEnabled) {
if (!data.machineData || data.machineData === undefined) {
data.machineData = await GetMachineData();
}
dangerWarningOpen = true;
toast.info("Here be dragons!", { icon: Flame });
}
previousDangerZoneEnabled = $dangerZoneEnabled;
})();
});
</script>
<div class="min-h-[calc(100vh-1rem)] from-background to-muted/30">
<div
class="mx-auto flex max-w-3xl flex-col gap-4 px-4 py-6 sm:px-6 sm:py-10 opacity-80"
>
<header class="flex items-start justify-between gap-3">
<div class="min-w-0">
<h1
class="text-balance text-2xl font-semibold tracking-tight sm:text-3xl"
>
{m.settings_title()}
</h1>
<p class="mt-2 text-sm text-muted-foreground">
{m.settings_description()}
</p>
</div>
<Button
class="cursor-pointer hover:cursor-pointer"
variant="ghost"
onclick={() => goto("/")}><ChevronLeft class="size-4" /> {m.settings_back()}</Button
>
</header>
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title>{m.settings_language_title()}</Card.Title>
<Card.Description
>{m.settings_language_description()}</Card.Description
>
</Card.Header>
<Card.Content>
<RadioGroup.Root
bind:value={form.selectedLanguage}
class="flex flex-col gap-3"
>
<div class="flex items-center space-x-2">
<RadioGroup.Item
value="en"
id="en"
class="cursor-pointer hover:cursor-pointer"
/>
<Label
for="en"
class="flex items-center gap-2 cursor-pointer hover:cursor-pointer"
>
<Us class="size-4 rounded-sm shadow-sm" />
{m.settings_language_english()}
</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item
value="it"
id="it"
class="cursor-pointer hover:cursor-pointer"
/>
<Label
for="it"
class="flex items-center gap-2 cursor-pointer hover:cursor-pointer"
>
<It class="size-4 rounded-sm shadow-sm" />
{m.settings_language_italian()}
</Label>
</div>
</RadioGroup.Root>
<div class="text-xs text-muted-foreground mt-4">
<strong>Info:</strong> {m.settings_language_info()}
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title>{m.settings_preview_files_title()}</Card.Title>
<Card.Description
>{m.settings_preview_files_description()}</Card.Description
>
</Card.Header>
<Card.Content class="space-y-4">
<div class="space-y-4">
<Label>{m.settings_preview_images_label()}</Label>
<div class="flex flex-col gap-3">
<div class="flex items-center space-x-2">
<Checkbox
id="preview-jpg"
checked={form.previewFileSupportedTypes?.includes("jpg")}
onCheckedChange={(checked) => {
const types = new Set(form.previewFileSupportedTypes || []);
if (checked) types.add("jpg");
else types.delete("jpg");
form.previewFileSupportedTypes = Array.from(
types,
).sort() as any[];
}}
/>
<Label
for="preview-jpg"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
JPG (.jpg)
</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="preview-jpeg"
checked={form.previewFileSupportedTypes?.includes("jpeg")}
onCheckedChange={(checked) => {
const types = new Set(form.previewFileSupportedTypes || []);
if (checked) types.add("jpeg");
else types.delete("jpeg");
form.previewFileSupportedTypes = Array.from(
types,
).sort() as any[];
}}
/>
<Label
for="preview-jpeg"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
JPEG (.jpeg)
</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="preview-png"
checked={form.previewFileSupportedTypes?.includes("png")}
onCheckedChange={(checked) => {
const types = new Set(form.previewFileSupportedTypes || []);
if (checked) types.add("png");
else types.delete("png");
form.previewFileSupportedTypes = Array.from(
types,
).sort() as any[];
}}
/>
<Label
for="preview-png"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
PNG (.png)
</Label>
</div>
</div>
<p class="text-xs text-muted-foreground mt-2">
{m.settings_preview_images_hint()}
</p>
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title>{m.settings_preview_page_title()}</Card.Title>
<Card.Description
>{m.settings_preview_page_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>
<div class="font-medium">{m.settings_preview_builtin_label()}</div>
<div class="text-sm text-muted-foreground">
{m.settings_preview_builtin_hint()}
</div>
</div>
<Switch
bind:checked={form.useBuiltinPreview}
class="cursor-pointer hover:cursor-pointer"
/>
</div>
<p class="text-xs text-muted-foreground mt-2">
{m.settings_preview_builtin_info()}
</p>
</div>
<Separator />
</Card.Content>
</Card.Root>
{#if $dangerZoneEnabled}
<Card.Root class="border-destructive/50 bg-destructive/15">
<Card.Header class="space-y-1">
<Card.Title class="text-destructive">{m.settings_danger_zone_title()}</Card.Title>
<Card.Description
>{m.settings_danger_zone_description()}</Card.Description
>
</Card.Header>
<Card.Content class="space-y-3">
<div
class="flex items-center justify-between gap-4 rounded-lg border border-destructive/30 bg-card p-4"
>
<div class="space-y-1">
<Label class="text-sm">{m.settings_danger_devtools_label()}</Label>
<div class="text-sm text-muted-foreground">
{m.settings_danger_devtools_hint()}
</div>
</div>
</div>
<Separator />
<div
class="flex items-center justify-between gap-4 rounded-lg border border-destructive/30 bg-card p-4"
>
<div class="space-y-1">
<Label class="text-sm">{m.settings_danger_reset_label()}</Label>
<div class="text-sm text-muted-foreground">
{m.settings_danger_reset_hint()}
</div>
</div>
<AlertDialog.Root>
<AlertDialog.Trigger
class={`${buttonVariants({ variant: "destructive" })} cursor-pointer hover:cursor-pointer`}
>
{m.settings_danger_reset_button()}
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title
style="color: var(--destructive); opacity: 0.7;"
>
<u>{m.settings_danger_reset_dialog_title()}</u>
</AlertDialog.Title>
<AlertDialog.Description>
{m.settings_danger_reset_dialog_description_part1()}
<br/>
{m.settings_danger_reset_dialog_description_part2()}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel
class="cursor-pointer hover:cursor-pointer"
>{m.settings_danger_reset_dialog_cancel()}</AlertDialog.Cancel
>
<AlertDialog.Action
onclick={() => {
resetToDefaults();
goto("/");
}}
class="cursor-pointer hover:cursor-pointer"
>{m.settings_danger_reset_dialog_continue()}</AlertDialog.Action
>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
</div>
<div class="text-xs text-muted-foreground">
<strong>{m.settings_danger_warning() }</strong>
</div>
<Separator />
<div class="text-xs text-muted-foreground">
OS: {machineData?.Version} ({machineData?.OS})
<br />
Hostname: {machineData?.Hostname}
<br />
ID: {machineData?.HWID}
<br />
CPU: {machineData?.CPU.processors[0].model.trim()} ({machineData
?.CPU.total_hardware_threads} cores)
<br />
RAM: {Math.round(
(machineData?.RAM.total_physical_bytes ?? 0) /
(1024 * 1024 * 1024),
)} GB
<br />
GPU: {machineData?.GPU.cards?.find(
(c) => !(c.pci?.product?.name ?? "").includes("Virtual"),
)?.pci?.product?.name ?? "N/A"}
<br />
<br />
GUI: {config
? `${config.GUISemver} (${config.GUIReleaseChannel})`
: "N/A"}
<br />
SDK: {config
? `${config.SDKDecoderSemver} (${config.SDKDecoderReleaseChannel})`
: "N/A"}
</div>
</Card.Content>
</Card.Root>
{/if}
<AlertDialog.Root bind:open={dangerWarningOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>{m.settings_danger_alert_title()}</AlertDialog.Title>
<AlertDialog.Description>
{m.settings_danger_alert_description()}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Action onclick={() => (dangerWarningOpen = false)}
>{m.settings_danger_alert_understood()}</AlertDialog.Action
>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
</div>
</div>

View File

@@ -0,0 +1,26 @@
import type { PageLoad } from './$types';
import { GetMachineData, GetConfig } from "$lib/wailsjs/go/main/App";
import { browser } from '$app/environment';
import { dangerZoneEnabled } from "$lib/stores/app";
import { get } from "svelte/store";
export const load = (async () => {
if (!browser) return { machineData: null, config: null };
try {
const [machineData, configRoot] = await Promise.all([
get(dangerZoneEnabled) ? GetMachineData() : Promise.resolve(null),
GetConfig()
]);
return {
machineData,
config: configRoot.EMLy
};
} catch (e) {
console.error("Failed to load settings data", e);
return {
machineData: null,
config: null
};
}
}) satisfies PageLoad;