feat: Added light mode plus various bug fixes

- Added click handler for Easter egg that enables music inspiration feature.
- Updated credits page to include new icons and handle click events.
- Enhanced inspiration page to fetch and display Spotify track embed HTML.
- Refactored inspiration loading logic to include track data.
- Introduced theme selection in settings with light and dark modes.
- Updated settings page to reflect new theme options and improve toast messages.
- Refined layout styles across various pages for consistent theming.
- Bumped application version to 1.5.0 in installer script.
This commit is contained in:
Flavio Fois
2026-02-09 21:38:17 +01:00
parent 5b62790248
commit 51679b61eb
22 changed files with 575 additions and 282 deletions

View File

@@ -3,8 +3,7 @@
import { page, navigating } from "$app/state";
import { beforeNavigate, goto } from "$app/navigation";
import { locales, localizeHref } from "$lib/paraglide/runtime";
import { unsavedChanges, sidebarOpen, bugReportDialogOpen } from "$lib/stores/app";
import "../layout.css";
import { unsavedChanges, sidebarOpen, bugReportDialogOpen, dangerZoneEnabled } from "$lib/stores/app";
import { onMount } from "svelte";
import * as m from "$lib/paraglide/messages.js";
import type { utils } from "$lib/wailsjs/go/models";
@@ -100,6 +99,7 @@
}
onMount(async () => {
if(dev) dangerZoneEnabled.set(true);
if (browser && isDebbugerProtectionOn) {
detectDebugging();
setInterval(detectDebugging, 1000);
@@ -391,15 +391,17 @@
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
<Music
size="16"
onclick={() => {
if (page.url.pathname !== "/inspiration" && page.url.pathname !== "/inspiration/")
goto("/inspiration");
}}
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
{#if settingsStore.settings.musicInspirationEnabled}
<Music
size="16"
onclick={() => {
if (page.url.pathname !== "/inspiration" && page.url.pathname !== "/inspiration/")
goto("/inspiration");
}}
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
{/if}
<a
data-sveltekit-reload
@@ -568,8 +570,6 @@
<style>
:global(body) {
margin: 0;
background: oklch(0 0 0);
color: #eaeaea;
font-family: system-ui, sans-serif;
}
@@ -578,11 +578,13 @@
flex-direction: column;
height: 100vh;
overflow: hidden;
background: var(--background);
color: var(--foreground);
}
.titlebar {
height: 32px;
background: oklch(0 0 0);
background: var(--background);
display: flex;
align-items: center;
justify-content: space-between;
@@ -592,11 +594,12 @@
flex: 0 0 32px;
z-index: 50;
position: relative;
border-bottom: 1px solid var(--border);
}
.footerbar {
height: 32px;
background: oklch(0 0 0);
background: var(--background);
display: flex;
align-items: center;
justify-content: flex-start;
@@ -604,28 +607,28 @@
padding: 0 12px;
user-select: none;
flex: 0 0 32px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1px solid var(--border);
}
.title {
font-size: 13px;
opacity: 0.9;
color: gray;
color: var(--muted-foreground);
}
.title bold {
font-weight: 600;
color: white;
color: var(--foreground);
opacity: 0.7;
}
.title version {
color: rgb(228, 221, 221);
opacity: 0.4;
color: var(--muted-foreground);
opacity: 0.6;
}
.title version debug {
color: #e11d48;
color: var(--destructive);
opacity: 1;
font-weight: 600;
}
@@ -642,8 +645,9 @@
position: absolute;
top: 100%;
left: 0;
background-color: #111;
border: 1px solid rgba(255, 255, 255, 0.1);
background-color: var(--popover);
color: var(--popover-foreground);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
z-index: 1000;
@@ -673,16 +677,16 @@
}
.tooltip-item .label {
color: #9ca3af;
color: var(--muted-foreground);
}
.tooltip-item .value {
color: #f3f4f6;
color: var(--foreground);
font-family: monospace;
}
.tooltip-item .channel {
color: #6b7280;
color: var(--muted-foreground);
font-size: 10px;
}
@@ -697,20 +701,20 @@
height: 100%;
border: none;
background: transparent;
color: white;
color: var(--foreground);
font-size: 14px;
cursor: pointer;
-webkit-app-region: no-drag;
}
.btn:hover {
background: rgba(255, 255, 255, 0.1);
background: var(--accent);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background: rgba(255, 255, 255, 0.02);
background: var(--muted);
}
.close:hover {
@@ -721,7 +725,7 @@
flex: 1 1 auto;
min-height: 0;
display: flex;
background: oklch(0 0 0);
background: var(--background);
overflow: hidden;
position: relative;
}
@@ -758,12 +762,12 @@
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
background: var(--border);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
background: var(--muted-foreground);
}
::-webkit-scrollbar-corner {
@@ -779,14 +783,14 @@
gap: 10px;
align-items: center;
justify-content: center;
background: oklch(0 0 0);
background: var(--background);
}
.spinner {
width: 32px;
height: 32px;
border: 2px solid rgba(255, 255, 255, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@@ -807,12 +811,12 @@
}
:global(.custom-scrollbar::-webkit-scrollbar-thumb) {
background: rgba(255, 255, 255, 0.1);
background: var(--border);
border-radius: 6px;
}
:global(.custom-scrollbar::-webkit-scrollbar-thumb:hover) {
background: rgba(255, 255, 255, 0.2);
background: var(--muted-foreground);
}
:global(.custom-scrollbar::-webkit-scrollbar-corner) {

View File

@@ -1,15 +1,48 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { goto, preloadData } from "$app/navigation";
import { Button } from "$lib/components/ui/button";
import * as Card from "$lib/components/ui/card";
import { Separator } from "$lib/components/ui/separator";
import { ChevronLeft, Heart, Code, Package, Globe, Github, Mail, BadgeInfo } from "@lucide/svelte";
import { ChevronLeft, Heart, Code, Package, Globe, Github, Mail, BadgeInfo, Music, PartyPopper } from "@lucide/svelte";
import * as m from "$lib/paraglide/messages";
import { OpenURLInBrowser } from "$lib/wailsjs/go/main/App";
import { dangerZoneEnabled } from "$lib/stores/app";
import { settingsStore } from "$lib/stores/settings.svelte";
import { toast } from "svelte-sonner";
let { data } = $props();
let config = $derived(data.config);
// Easter Egg State
const REQUIRED_CLICKS = 10;
const CLICK_WINDOW_MS = 4000;
let recentClicks: number[] = [];
function handleEasterEggClick(_event: MouseEvent) {
console.log("clicked")
// Only proceed if danger zone is already enabled
if (!$dangerZoneEnabled) return;
// If already enabled, do nothing to avoid spam
if (settingsStore.settings.musicInspirationEnabled) return;
const now = Date.now();
// Clean old clicks
recentClicks = recentClicks.filter(t => now - t < CLICK_WINDOW_MS);
recentClicks.push(now);
if (recentClicks.length >= REQUIRED_CLICKS) {
recentClicks = [];
try {
settingsStore.update({ musicInspirationEnabled: true });
preloadData("/inspiration");
} catch (e) {
console.error("Failed to enable music inspiration:", e);
}
}
}
// Open external URL in default browser
async function openUrl(url: string) {
try {
@@ -67,7 +100,7 @@
];
</script>
<div class="min-h-[calc(100vh-1rem)] from-background to-muted/30">
<div class="min-h-[calc(100vh-1rem)] bg-gradient-to-b 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"
>
@@ -132,19 +165,29 @@
</Card.Header>
<Card.Content class="space-y-4">
{#each team as member}
<div class="flex items-start gap-4 rounded-lg border bg-card p-4">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex items-start gap-4 rounded-lg border bg-card p-4 relative overflow-hidden"
onclick={member.username === "FOISX" ? handleEasterEggClick : undefined}
>
<!-- Selectable trigger area overlay for cleaner interaction -->
{#if member.username === "FOISX" && $dangerZoneEnabled && !settingsStore.settings.musicInspirationEnabled}
<div class="absolute inset-0 cursor-pointer z-10 opacity-0 bg-transparent"></div>
{/if}
<img
src={gravatarUrls[member.email]}
alt={member.name}
class="h-14 w-14 rounded-full border-2 border-primary/20"
class="h-14 w-14 rounded-full border-2 border-primary/20 z-0 select-none"
/>
<div class="flex-1">
<div class="flex-1 z-0">
<div class="font-medium">{member.username} ({member.name})</div>
<div class="text-sm text-primary/80">{member.role}</div>
<div class="text-sm text-muted-foreground mt-1">{member.description}</div>
<a
href="mailto:{member.email}"
class="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-primary mt-2 transition-colors"
class="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-primary mt-2 transition-colors relative z-20"
>
<Mail class="size-3" />
{member.email}

View File

@@ -6,69 +6,11 @@
import { ChevronLeft, Music, ExternalLink } from "@lucide/svelte";
import * as m from "$lib/paraglide/messages";
import { OpenURLInBrowser } from "$lib/wailsjs/go/main/App";
import type { SpotifyTrack } from "./+page";
let { data } = $props();
let config = $derived(data.config);
interface SpotifyTrack {
name: string;
artist: string;
albumArt?: string;
spotifyUrl: string;
embedUrl: string;
}
// Music that inspired this project
const inspirationTracks: SpotifyTrack[] = [
{
name: "Strays",
artist: "Ivycomb, Stephanafro",
spotifyUrl: "https://open.spotify.com/track/1aXATIo34e5ZZvFcavePpy",
embedUrl: "https://open.spotify.com/embed/track/1aXATIo34e5ZZvFcavePpy?utm_source=generator"
},
{
name: "Headlock",
artist: "Imogen Heap",
spotifyUrl: "https://open.spotify.com/track/63Pi2NAx5yCgeLhCTOrEou",
embedUrl: "https://open.spotify.com/embed/track/63Pi2NAx5yCgeLhCTOrEou?utm_source=generator"
},
{
name: "I Still Create",
artist: "YonKaGor",
spotifyUrl: "https://open.spotify.com/track/0IqTgwWU2syiSYbdBEromt",
embedUrl: "https://open.spotify.com/embed/track/0IqTgwWU2syiSYbdBEromt?utm_source=generator"
},
{
name: "Raised by Aliens",
artist: "ivy comb, Stephanafro",
spotifyUrl: "https://open.spotify.com/track/5ezyCaoc5XiVdkpRYWeyG5",
embedUrl: "https://open.spotify.com/embed/track/5ezyCaoc5XiVdkpRYWeyG5?utm_source=generator"
},
{
name: "VENOMOUS",
artist: "passengerprincess",
spotifyUrl: "https://open.spotify.com/track/4rPKifkzrhIYAsl1njwmjd",
embedUrl: "https://open.spotify.com/embed/track/4rPKifkzrhIYAsl1njwmjd?utm_source=generator"
},
{
name: "PREY",
artist: "passengerprincess",
spotifyUrl: "https://open.spotify.com/track/510m8qwFCHgzi4zsQnjLUX",
embedUrl: "https://open.spotify.com/embed/track/510m8qwFCHgzi4zsQnjLUX?utm_source=generator"
},
{
name: "Dracula",
artist: "Tame Impala",
spotifyUrl: "https://open.spotify.com/track/1NXbNEAcPvY5G1xvfN57aA",
embedUrl: "https://open.spotify.com/embed/track/1NXbNEAcPvY5G1xvfN57aA?utm_source=generator"
},
{
name: "Electric love",
artist: "When Snakes Sing",
spotifyUrl: "https://open.spotify.com/track/1nDkT2Cn13qDnFegF93UHi",
embedUrl: "https://open.spotify.com/embed/track/1nDkT2Cn13qDnFegF93UHi?utm_source=generator"
}
];
let tracks: SpotifyTrack[] = $derived(data.tracks ?? []);
// Open external URL in default browser
async function openUrl(url: string) {
@@ -80,7 +22,7 @@
}
</script>
<div class="min-h-[calc(100vh-1rem)] from-background to-muted/30">
<div class="min-h-[calc(100vh-1rem)] bg-gradient-to-b from-background to-muted/30">
<div
class="mx-auto flex max-w-4xl flex-col gap-4 px-4 py-6 sm:px-6 sm:py-10 opacity-80"
>
@@ -119,37 +61,25 @@
</Card.Header>
<Card.Content>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-2">
{#each inspirationTracks as track}
{#each tracks as track}
<div class="group relative">
<div class="overflow-hidden rounded-lg bg-muted" style="height: 352px;">
<iframe
src={track.embedUrl}
width="100%"
height="352"
frameborder="0"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
title={`${track.artist} - ${track.name}`}
class="rounded-lg"
></iframe>
</div>
<div class="mt-2 flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">{track.name}</p>
<p class="truncate text-xs text-muted-foreground">
{track.artist}
</p>
</div>
<Button
variant="ghost"
size="icon"
class="size-8 shrink-0 opacity-70 hover:opacity-100"
onclick={() => openUrl(track.spotifyUrl)}
title="Open in Spotify"
>
<ExternalLink class="size-4" />
</Button>
<div class="overflow-hidden rounded-lg bg-muted">
{#if track.embedHtml}
{@html track.embedHtml}
{:else}
<iframe
src={track.embedUrl}
width="100%"
height="352"
frameborder="0"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
title={`${track.artist} - ${track.name}`}
class="rounded-lg"
></iframe>
{/if}
</div>
</div>
{/each}
</div>
@@ -184,10 +114,6 @@
and
<span class="text-red-500"></span>
</p>
<p class="mt-1">
GUI: {config ? `v${config.GUISemver}` : "N/A"} •
SDK: {config ? `v${config.SDKDecoderSemver}` : "N/A"}
</p>
</div>
</div>
</div>

View File

@@ -2,18 +2,98 @@ import type { PageLoad } from './$types';
import { GetConfig } from "$lib/wailsjs/go/main/App";
import { browser } from '$app/environment';
export const load = (async () => {
if (!browser) return { config: null };
export interface SpotifyTrack {
name: string;
artist: string;
spotifyUrl: string;
embedUrl: string;
embedHtml?: string;
}
// Music that inspired this project
const inspirationTracks: SpotifyTrack[] = [
{
name: "Strays",
artist: "Ivycomb, Stephanafro",
spotifyUrl: "https://open.spotify.com/track/1aXATIo34e5ZZvFcavePpy",
embedUrl: "https://open.spotify.com/embed/track/1aXATIo34e5ZZvFcavePpy?utm_source=generator"
},
{
name: "Headlock",
artist: "Imogen Heap",
spotifyUrl: "https://open.spotify.com/track/63Pi2NAx5yCgeLhCTOrEou",
embedUrl: "https://open.spotify.com/embed/track/63Pi2NAx5yCgeLhCTOrEou?utm_source=generator"
},
{
name: "I Still Create",
artist: "YonKaGor",
spotifyUrl: "https://open.spotify.com/track/0IqTgwWU2syiSYbdBEromt",
embedUrl: "https://open.spotify.com/embed/track/0IqTgwWU2syiSYbdBEromt?utm_source=generator"
},
{
name: "Raised by Aliens",
artist: "ivy comb, Stephanafro",
spotifyUrl: "https://open.spotify.com/track/5ezyCaoc5XiVdkpRYWeyG5",
embedUrl: "https://open.spotify.com/embed/track/5ezyCaoc5XiVdkpRYWeyG5?utm_source=generator"
},
{
name: "VENOMOUS",
artist: "passengerprincess",
spotifyUrl: "https://open.spotify.com/track/4rPKifkzrhIYAsl1njwmjd",
embedUrl: "https://open.spotify.com/embed/track/4rPKifkzrhIYAsl1njwmjd?utm_source=generator"
},
{
name: "PREY",
artist: "passengerprincess",
spotifyUrl: "https://open.spotify.com/track/510m8qwFCHgzi4zsQnjLUX",
embedUrl: "https://open.spotify.com/embed/track/510m8qwFCHgzi4zsQnjLUX?utm_source=generator"
},
{
name: "Dracula",
artist: "Tame Impala",
spotifyUrl: "https://open.spotify.com/track/1NXbNEAcPvY5G1xvfN57aA",
embedUrl: "https://open.spotify.com/embed/track/1NXbNEAcPvY5G1xvfN57aA?utm_source=generator"
},
{
name: "Electric love",
artist: "When Snakes Sing",
spotifyUrl: "https://open.spotify.com/track/1nDkT2Cn13qDnFegF93UHi",
embedUrl: "https://open.spotify.com/embed/track/1nDkT2Cn13qDnFegF93UHi?utm_source=generator"
}
];
async function fetchEmbedHtml(track: SpotifyTrack, fetch: typeof globalThis.fetch): Promise<SpotifyTrack> {
try {
const oEmbedUrl = `https://open.spotify.com/oembed?url=${encodeURIComponent(track.spotifyUrl)}`;
const res = await fetch(oEmbedUrl);
if (res.ok) {
const data = await res.json();
return { ...track, embedHtml: data.html };
}
} catch (e) {
console.error(`Failed to fetch oEmbed for ${track.name}:`, e);
}
return track;
}
export const load = (async ({fetch}) => {
if (!browser) return { config: null, tracks: inspirationTracks };
try {
const configRoot = await GetConfig();
const [configRoot, ...tracks] = await Promise.all([
GetConfig(),
...inspirationTracks.map(t => fetchEmbedHtml(t, fetch))
]);
return {
config: configRoot.EMLy
config: configRoot.EMLy,
tracks
};
} catch (e) {
console.error("Failed to load config for inspiration", e);
console.error("Failed to load data for inspiration", e);
return {
config: null
config: null,
tracks: inspirationTracks
};
}
}) satisfies PageLoad;

View File

@@ -6,7 +6,7 @@
import { Label } from "$lib/components/ui/label";
import { Separator } from "$lib/components/ui/separator";
import { Switch } from "$lib/components/ui/switch";
import { ChevronLeft, Flame, Download, Upload, RefreshCw, CheckCircle2, AlertCircle } from "@lucide/svelte";
import { ChevronLeft, Flame, Download, Upload, RefreshCw, CheckCircle2, AlertCircle, Sun, Moon } from "@lucide/svelte";
import type { EMLy_GUI_Settings } from "$lib/types";
import { toast } from "svelte-sonner";
import { It, Us } from "svelte-flags";
@@ -40,7 +40,8 @@
previewFileSupportedTypes: ["jpg", "jpeg", "png"],
enableAttachedDebuggerProtection: true,
useDarkEmailViewer: true,
enableUpdateChecker: true,
enableUpdateChecker: false,
theme: "dark",
};
async function setLanguage(
@@ -73,8 +74,10 @@
s.enableAttachedDebuggerProtection ?? defaults.enableAttachedDebuggerProtection ?? true,
useDarkEmailViewer:
s.useDarkEmailViewer ?? defaults.useDarkEmailViewer ?? true,
enableUpdateChecker:
s.enableUpdateChecker ?? defaults.enableUpdateChecker ?? true,
enableUpdateChecker: runningInDevMode
? false
: (s.enableUpdateChecker ?? defaults.enableUpdateChecker ?? true),
theme: s.theme || defaults.theme || "light",
};
}
@@ -86,6 +89,7 @@
!!a.enableAttachedDebuggerProtection === !!b.enableAttachedDebuggerProtection &&
!!a.useDarkEmailViewer === !!b.useDarkEmailViewer &&
!!a.enableUpdateChecker === !!b.enableUpdateChecker &&
(a.theme ?? "light") === (b.theme ?? "light") &&
JSON.stringify(a.previewFileSupportedTypes?.sort()) ===
JSON.stringify(b.previewFileSupportedTypes?.sort())
);
@@ -278,25 +282,25 @@
updateStatus = status;
if (status.updateAvailable) {
toast.success(`Update available: ${status.availableVersion}`);
toast.success(m.settings_toast_update_available({ version: status.availableVersion }));
} else if (!status.errorMessage) {
toast.info("You're on the latest version");
toast.info(m.settings_toast_latest_version());
} else {
toast.error(status.errorMessage);
}
} catch (err) {
console.error("Failed to check for updates:", err);
toast.error("Failed to check for updates");
toast.error(m.settings_toast_check_failed());
}
}
async function downloadUpdate() {
try {
await DownloadUpdate();
toast.success("Update downloaded successfully");
toast.success(m.settings_toast_download_success());
} catch (err) {
console.error("Failed to download update:", err);
toast.error("Failed to download update");
toast.error(m.settings_toast_download_failed());
}
}
@@ -306,7 +310,7 @@
// App will quit, so no toast needed
} catch (err) {
console.error("Failed to install update:", err);
toast.error("Failed to launch installer");
toast.error(m.settings_toast_install_failed());
}
}
@@ -324,7 +328,7 @@
});
</script>
<div class="min-h-[calc(100vh-1rem)] from-background to-muted/30">
<div class="min-h-[calc(100vh-1rem)] bg-gradient-to-b 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"
>
@@ -393,6 +397,52 @@
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title>{m.settings_appearance_title()}</Card.Title>
<Card.Description>{m.settings_appearance_description()}</Card.Description>
</Card.Header>
<Card.Content>
<RadioGroup.Root
bind:value={form.theme}
class="flex flex-col gap-3"
>
<div class="flex items-center space-x-2">
<RadioGroup.Item
value="light"
id="theme-light"
class="cursor-pointer hover:cursor-pointer"
/>
<Label
for="theme-light"
class="flex items-center gap-2 cursor-pointer hover:cursor-pointer"
>
<Sun class="size-4" />
{m.settings_theme_light()}
</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item
value="dark"
id="theme-dark"
class="cursor-pointer hover:cursor-pointer"
/>
<Label
for="theme-dark"
class="flex items-center gap-2 cursor-pointer hover:cursor-pointer"
>
<Moon class="size-4" />
{m.settings_theme_dark()}
</Label>
</div>
</RadioGroup.Root>
<div class="text-xs text-muted-foreground mt-4">
<strong>Info:</strong>
{m.settings_theme_hint()}
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title>{m.settings_export_import_title()}</Card.Title>
@@ -592,14 +642,14 @@
{#if form.enableUpdateChecker}
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title>Updates</Card.Title>
<Card.Description>Check for and install application updates from your network share</Card.Description>
<Card.Title>{m.settings_updates_title()}</Card.Title>
<Card.Description>{m.settings_updates_description()}</Card.Description>
</Card.Header>
<Card.Content class="space-y-4">
<!-- Current Version -->
<div class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4">
<div>
<div class="font-medium">Current Version</div>
<div class="font-medium">{m.settings_updates_current_version()}</div>
<div class="text-sm text-muted-foreground">
{updateStatus.currentVersion} ({config?.GUIReleaseChannel || "stable"})
</div>
@@ -607,17 +657,17 @@
{#if updateStatus.updateAvailable}
<div class="flex items-center gap-2 text-sm font-medium text-green-600 dark:text-green-400">
<AlertCircle class="size-4" />
Update Available
{m.settings_updates_available()}
</div>
{:else if updateStatus.errorMessage && updateStatus.lastCheckTime}
<div class="flex items-center gap-2 text-sm text-destructive">
<AlertCircle class="size-4" />
Check failed
{m.settings_updates_check_failed()}
</div>
{:else if updateStatus.lastCheckTime}
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle2 class="size-4" />
No updates found
{m.settings_updates_no_updates()}
</div>
{/if}
</div>
@@ -627,12 +677,12 @@
<!-- Check for Updates -->
<div class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4">
<div>
<div class="font-medium">Check for Updates</div>
<div class="font-medium">{m.settings_updates_check_label()}</div>
<div class="text-sm text-muted-foreground">
{#if updateStatus.lastCheckTime}
Last checked: {updateStatus.lastCheckTime}
{m.settings_updates_last_checked({ time: updateStatus.lastCheckTime })}
{:else}
Click to check for available updates
{m.settings_updates_click_check()}
{/if}
</div>
</div>
@@ -643,7 +693,7 @@
disabled={updateStatus.checking || updateStatus.downloading}
>
<RefreshCw class="size-4 mr-2 {updateStatus.checking ? 'animate-spin' : ''}" />
{updateStatus.checking ? "Checking..." : "Check Now"}
{updateStatus.checking ? m.settings_updates_checking() : m.settings_updates_check_now()}
</Button>
</div>
@@ -652,12 +702,12 @@
<Separator />
<div class="flex items-center justify-between gap-4 rounded-lg border border-blue-500/30 bg-blue-500/10 p-4">
<div>
<div class="font-medium">Version {updateStatus.availableVersion} Available</div>
<div class="font-medium">{m.settings_updates_version_available({ version: updateStatus.availableVersion })}</div>
<div class="text-sm text-muted-foreground">
{#if updateStatus.downloading}
Downloading... {updateStatus.downloadProgress}%
{m.settings_updates_downloading({ progress: updateStatus.downloadProgress })}
{:else}
Click to download the update
{m.settings_updates_click_download()}
{/if}
</div>
{#if updateStatus.releaseNotes}
@@ -673,7 +723,7 @@
disabled={updateStatus.downloading}
>
<Download class="size-4 mr-2" />
{updateStatus.downloading ? `${updateStatus.downloadProgress}%` : "Download"}
{updateStatus.downloading ? `${updateStatus.downloadProgress}%` : m.settings_updates_download_button()}
</Button>
</div>
{/if}
@@ -683,9 +733,9 @@
<Separator />
<div class="flex items-center justify-between gap-4 rounded-lg border border-green-500/30 bg-green-500/10 p-4">
<div>
<div class="font-medium">Update Ready to Install</div>
<div class="font-medium">{m.settings_updates_ready_title()}</div>
<div class="text-sm text-muted-foreground">
Version {updateStatus.availableVersion} has been downloaded and verified
{m.settings_updates_ready_ref({ version: updateStatus.availableVersion })}
</div>
</div>
<Button
@@ -694,7 +744,7 @@
onclick={installUpdate}
>
<CheckCircle2 class="size-4 mr-2" />
Install Now
{m.settings_updates_install_button()}
</Button>
</div>
{/if}
@@ -713,11 +763,11 @@
<!-- Info about update path -->
<div class="text-xs text-muted-foreground">
<strong>Info:</strong> Updates are checked from your configured network share path.
<strong>Info:</strong> {m.settings_updates_info_message()}
{#if (config as any)?.UpdatePath}
Current path: <code class="text-xs bg-muted px-1 py-0.5 rounded">{(config as any).UpdatePath}</code>
{m.settings_updates_current_path()} <code class="text-xs bg-muted px-1 py-0.5 rounded">{(config as any).UpdatePath}</code>
{:else}
<span class="text-amber-600 dark:text-amber-400">No update path configured</span>
<span class="text-amber-600 dark:text-amber-400">{m.settings_updates_no_path()}</span>
{/if}
</div>
</Card.Content>
@@ -843,18 +893,19 @@
class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4 border-destructive/30"
>
<div class="space-y-1">
<Label class="text-sm">Enable Update Checker</Label>
<Label class="text-sm">{m.settings_danger_update_checker_label()}</Label>
<div class="text-sm text-muted-foreground">
Check for application updates from network share
{m.settings_danger_update_checker_hint()}
</div>
</div>
<Switch
bind:checked={form.enableUpdateChecker}
class="cursor-pointer hover:cursor-pointer"
disabled={runningInDevMode}
/>
</div>
<div class="text-xs text-muted-foreground">
<strong>Info:</strong> When enabled, the app will check for updates from your configured network share. Disable this if you manage updates manually or don't have network access.
{m.settings_danger_update_checker_info()}
</div>
<Separator />
@@ -871,6 +922,7 @@
</Card.Root>
{/if}
{#if !runningInDevMode}
<AlertDialog.Root bind:open={dangerWarningOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
@@ -878,7 +930,11 @@
>{m.settings_danger_alert_title()}</AlertDialog.Title
>
<AlertDialog.Description>
{m.settings_danger_alert_description()}
{m.settings_danger_alert_description_part1()}
<br />
{m.settings_danger_alert_description_part2()}
<br />
{m.settings_danger_alert_description_part3()}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
@@ -888,5 +944,6 @@
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
{/if}
</div>
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { setupConsoleLogger } from '$lib/utils/logger-hook';
import "./layout.css";
let { children } = $props();

View File

@@ -72,7 +72,8 @@
<style>
:global(body) {
margin: 0;
background: #000;
background: var(--background);
color: var(--foreground);
overflow: hidden;
}
@@ -81,11 +82,13 @@
flex-direction: column;
height: 100vh;
overflow: hidden;
background: var(--background);
color: var(--foreground);
}
.titlebar {
height: 32px;
background: #000;
background: var(--background);
display: flex;
align-items: center;
justify-content: space-between;
@@ -94,13 +97,13 @@
user-select: none;
flex: 0 0 32px;
z-index: 50;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
border-bottom: 1px solid var(--border);
}
.title {
font-size: 13px;
font-weight: 500;
color: rgba(255, 255, 255, 0.7);
color: var(--muted-foreground);
}
.controls {
@@ -114,7 +117,7 @@
height: 100%;
border: none;
background: transparent;
color: white;
color: var(--foreground);
font-size: 14px;
cursor: pointer;
-webkit-app-region: no-drag;
@@ -124,16 +127,18 @@
}
.btn:hover {
background: rgba(255, 255, 255, 0.1);
background: var(--accent);
}
.close:hover {
background: #e81123;
color: white;
}
.content {
flex: 1;
overflow: hidden;
position: relative;
background: var(--background);
}
</style>

View File

@@ -177,7 +177,7 @@
<style>
:global(body) {
margin: 0;
background: #000;
background: var(--background);
}
.page-container {
@@ -185,15 +185,15 @@
display: flex;
flex-direction: column;
overflow: hidden;
background: #000;
color: white;
background: var(--background);
color: var(--foreground);
font-family: system-ui, -apple-system, sans-serif;
}
.toolbar {
height: 50px;
background: rgba(255, 255, 255, 0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: var(--card);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
@@ -221,7 +221,7 @@
.separator {
width: 1px;
height: 18px;
background: rgba(255, 255, 255, 0.15);
background: var(--border);
margin: 0 4px;
}
@@ -233,21 +233,21 @@
height: 32px;
padding: 0;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.85);
border: 1px solid var(--border);
background: var(--muted);
color: var(--foreground);
cursor: pointer;
transition: all 0.2s;
}
.btn:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
background: var(--accent);
color: var(--accent-foreground);
}
.image-area {
flex: 1;
background: rgba(0, 0, 0, 0.5);
background: var(--muted);
position: relative;
overflow: hidden;
display: flex;
@@ -275,16 +275,16 @@
}
.loading {
color: rgba(255, 255, 255, 0.5);
color: var(--muted-foreground);
font-size: 14px;
}
.error-message {
color: #f87171;
background: rgba(248, 113, 113, 0.1);
color: var(--destructive);
background: var(--destructive-foreground);
padding: 12px 16px;
border-radius: 8px;
border: 1px solid rgba(248, 113, 113, 0.2);
border: 1px solid var(--destructive);
font-size: 14px;
}
</style>

View File

@@ -79,7 +79,8 @@
<style>
:global(body) {
margin: 0;
background: #000;
background: var(--background);
color: var(--foreground);
overflow: hidden;
}
@@ -88,11 +89,13 @@
flex-direction: column;
height: 100vh;
overflow: hidden;
background: var(--background);
color: var(--foreground);
}
.titlebar {
height: 32px;
background: #000;
background: var(--background);
display: flex;
align-items: center;
justify-content: space-between;
@@ -101,13 +104,14 @@
user-select: none;
flex: 0 0 32px;
z-index: 50;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
border-bottom: 1px solid var(--border);
}
.title {
font-size: 13px;
opacity: 0.9;
color: white;
color: var(--muted-foreground);
font-weight: 500;
}
.controls {
@@ -121,24 +125,25 @@
height: 100%;
border: none;
background: transparent;
color: white;
color: var(--foreground);
font-size: 14px;
cursor: pointer;
-webkit-app-region: no-drag;
}
.btn:hover {
background: rgba(255, 255, 255, 0.1);
background: var(--accent);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background: rgba(255, 255, 255, 0.02);
background: var(--muted);
}
.close:hover {
background: #e81123;
color: white;
}
.content {
@@ -147,6 +152,6 @@
display: flex;
flex-direction: column;
overflow: hidden;
background: #111;
background: var(--background);
}
</style>

View File

@@ -233,9 +233,10 @@
display: flex;
flex-direction: column;
height: 100%;
background: #1e1e1e;
background: var(--background);
position: relative;
user-select: none;
color: var(--foreground);
}
.loading-overlay {
@@ -247,15 +248,15 @@
gap: 10px;
align-items: center;
justify-content: center;
background: #111;
color: white;
background: var(--background);
color: var(--foreground);
}
.spinner {
width: 32px;
height: 32px;
border: 2px solid rgba(255, 255, 255, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@@ -273,14 +274,14 @@
display: flex;
align-items: center;
justify-content: center;
background: #111;
color: #ef4444;
background: var(--background);
color: var(--destructive);
}
.toolbar {
height: 50px;
background: #000;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: var(--card);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
@@ -308,7 +309,7 @@
.separator {
width: 1px;
height: 18px;
background: rgba(255, 255, 255, 0.15);
background: var(--border);
margin: 0 4px;
}
@@ -320,22 +321,16 @@
height: 32px;
padding: 0;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.85);
border: 1px solid var(--border);
background: var(--muted);
color: var(--foreground);
cursor: pointer;
transition: all 0.2s;
}
.btn:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
.separator {
width: 1px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
background: var(--accent);
color: var(--accent-foreground);
}
.canvas-container {
@@ -345,7 +340,7 @@
justify-content: center;
align-items: flex-start; /* scroll from top */
padding: 20px;
background: #333; /* Dark background for contrast */
background: var(--muted);
}
canvas {
@@ -363,12 +358,12 @@
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
background: var(--border);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
background: var(--muted-foreground);
}
::-webkit-scrollbar-corner {