feat: Implement Easter egg for music inspiration in credits page

- 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 2d06eeabf4
22 changed files with 575 additions and 282 deletions

View File

@@ -1,8 +1,8 @@
[EMLy] [EMLy]
SDK_DECODER_SEMVER = 1.3.2 SDK_DECODER_SEMVER = 1.3.2
SDK_DECODER_RELEASE_CHANNEL = beta SDK_DECODER_RELEASE_CHANNEL = stable
GUI_SEMVER = 1.4.0 GUI_SEMVER = 1.5.0
GUI_RELEASE_CHANNEL = stable GUI_RELEASE_CHANNEL = beta
LANGUAGE = it LANGUAGE = it
UPDATE_CHECK_ENABLED = false UPDATE_CHECK_ENABLED = false
UPDATE_PATH = UPDATE_PATH =

View File

@@ -50,7 +50,9 @@
"settings_danger_reset_dialog_continue": "Continue", "settings_danger_reset_dialog_continue": "Continue",
"settings_danger_warning": "Warning: This action is irreversible. Please ensure you have backed up any important data before proceeding.", "settings_danger_warning": "Warning: This action is irreversible. Please ensure you have backed up any important data before proceeding.",
"settings_danger_alert_title": "Advanced options enabled", "settings_danger_alert_title": "Advanced options enabled",
"settings_danger_alert_description": "You're about to access EMLy's advanced options. Modifying such options may cause instability, including crashes, freezes, or security software alerts. For support or troubleshooting, contact @lyzcoote on Discord.", "settings_danger_alert_description_part1": "You're about to access EMLy's advanced options.",
"settings_danger_alert_description_part2": "Modifying such options may cause instability, including crashes, freezes, or security software alerts.",
"settings_danger_alert_description_part3": "For support or troubleshooting, contact your TL/RDS.",
"settings_danger_alert_understood": "Understood", "settings_danger_alert_understood": "Understood",
"settings_toast_reverted": "Reverted to last saved settings.", "settings_toast_reverted": "Reverted to last saved settings.",
"settings_toast_save_failed": "Failed to save settings.", "settings_toast_save_failed": "Failed to save settings.",
@@ -98,6 +100,12 @@
"bugreport_email_placeholder": "your.email@example.com", "bugreport_email_placeholder": "your.email@example.com",
"bugreport_text_label": "Bug Description", "bugreport_text_label": "Bug Description",
"bugreport_text_placeholder": "Describe the bug in detail...", "bugreport_text_placeholder": "Describe the bug in detail...",
"settings_appearance_title": "Appearance",
"settings_appearance_description": "Customize the application theme.",
"settings_theme_label": "Theme",
"settings_theme_hint": "Choose between light and dark mode.",
"settings_theme_light": "Light",
"settings_theme_dark": "Dark",
"bugreport_info": "Your message, email file (if loaded), screenshot, and system information will be included in the report.", "bugreport_info": "Your message, email file (if loaded), screenshot, and system information will be included in the report.",
"bugreport_screenshot_label": "Attached Screenshot:", "bugreport_screenshot_label": "Attached Screenshot:",
"bugreport_cancel": "Cancel", "bugreport_cancel": "Cancel",
@@ -160,5 +168,35 @@
"credits_lib_dompurify": "XSS sanitizer for HTML content", "credits_lib_dompurify": "XSS sanitizer for HTML content",
"credits_license_title": "License & Source", "credits_license_title": "License & Source",
"credits_license_text": "EMLy is proprietary software developed by 3gIT. All rights reserved. The application uses various open source libraries, each governed by their respective licenses.", "credits_license_text": "EMLy is proprietary software developed by 3gIT. All rights reserved. The application uses various open source libraries, each governed by their respective licenses.",
"credits_copyright": "All rights reserved." "credits_copyright": "All rights reserved.",
"settings_updates_title": "Updates",
"settings_updates_description": "Check for and install application updates from your network share.",
"settings_updates_current_version": "Current Version",
"settings_updates_available": "Update Available",
"settings_updates_check_failed": "Check failed",
"settings_updates_no_updates": "No updates found",
"settings_updates_check_label": "Check for Updates",
"settings_updates_last_checked": "Last checked: {time}",
"settings_updates_click_check": "Click to check for available updates",
"settings_updates_checking": "Checking...",
"settings_updates_check_now": "Check Now",
"settings_updates_version_available": "Version {version} Available",
"settings_updates_downloading": "Downloading... {progress}%",
"settings_updates_click_download": "Click to download the update",
"settings_updates_download_button": "Download",
"settings_updates_ready_title": "Update Ready to Install",
"settings_updates_ready_ref": "Version {version} has been downloaded and verified",
"settings_updates_install_button": "Install Now",
"settings_updates_info_message": "Updates are checked from your configured network share path.",
"settings_updates_current_path": "Current path:",
"settings_updates_no_path": "No update path configured",
"settings_toast_update_available": "Update available: {version}",
"settings_toast_latest_version": "You're on the latest version",
"settings_toast_check_failed": "Failed to check for updates",
"settings_toast_download_success": "Update downloaded successfully",
"settings_toast_download_failed": "Failed to download update",
"settings_toast_install_failed": "Failed to launch installer",
"settings_danger_update_checker_label": "Enable Update Checker",
"settings_danger_update_checker_hint": "Check for application updates from network share",
"settings_danger_update_checker_info": "Info: 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."
} }

View File

@@ -50,7 +50,9 @@
"settings_danger_reset_dialog_continue": "Continua", "settings_danger_reset_dialog_continue": "Continua",
"settings_danger_warning": "Attenzione: Questa azione è irreversibile. Assicurati di aver effettuato il backup di tutti i dati importanti prima di procedere.", "settings_danger_warning": "Attenzione: Questa azione è irreversibile. Assicurati di aver effettuato il backup di tutti i dati importanti prima di procedere.",
"settings_danger_alert_title": "Opzioni avanzate abilitate", "settings_danger_alert_title": "Opzioni avanzate abilitate",
"settings_danger_alert_description": "Stai per accedere alle opzioni avanzate di EMLy. Modificare tali opzioni può causare instabilità, inclusi crash, blocchi o avvisi del software di sicurezza. Per supporto o risoluzione dei problemi, contatta @lyzcoote su Discord.", "settings_danger_alert_description_part1": "Stai per accedere alle opzioni avanzate di EMLy.",
"settings_danger_alert_description_part2": "Modificare tali opzioni può causare instabilità, inclusi crash, blocchi o avvisi del software di sicurezza.",
"settings_danger_alert_description_part3": "Per supporto o risoluzione dei problemi, contatta il proprio TL/RDS.",
"settings_danger_alert_understood": "Capito", "settings_danger_alert_understood": "Capito",
"settings_toast_reverted": "Ripristinato alle ultime impostazioni salvate.", "settings_toast_reverted": "Ripristinato alle ultime impostazioni salvate.",
"settings_toast_save_failed": "Impossibile salvare le impostazioni.", "settings_toast_save_failed": "Impossibile salvare le impostazioni.",
@@ -98,6 +100,12 @@
"bugreport_email_placeholder": "tua.email@esempio.com", "bugreport_email_placeholder": "tua.email@esempio.com",
"bugreport_text_label": "Descrizione del Bug", "bugreport_text_label": "Descrizione del Bug",
"bugreport_text_placeholder": "Descrivi il bug in dettaglio...", "bugreport_text_placeholder": "Descrivi il bug in dettaglio...",
"settings_appearance_title": "Aspetto",
"settings_appearance_description": "Personalizza il tema dell'applicazione.",
"settings_theme_label": "Tema",
"settings_theme_hint": "Scegli tra modalità chiara e scura.",
"settings_theme_light": "Chiaro",
"settings_theme_dark": "Scuro",
"bugreport_info": "Il tuo messaggio, il file email (se caricato), lo screenshot e le informazioni di sistema saranno inclusi nella segnalazione.", "bugreport_info": "Il tuo messaggio, il file email (se caricato), lo screenshot e le informazioni di sistema saranno inclusi nella segnalazione.",
"bugreport_screenshot_label": "Screenshot Allegato:", "bugreport_screenshot_label": "Screenshot Allegato:",
"bugreport_cancel": "Annulla", "bugreport_cancel": "Annulla",
@@ -160,5 +168,35 @@
"credits_lib_dompurify": "Sanitizzatore XSS per contenuti HTML", "credits_lib_dompurify": "Sanitizzatore XSS per contenuti HTML",
"credits_license_title": "Licenza e Sorgente", "credits_license_title": "Licenza e Sorgente",
"credits_license_text": "EMLy è un software proprietario sviluppato da 3gIT. Tutti i diritti riservati. L'applicazione utilizza varie librerie open source, ciascuna governata dalle rispettive licenze.", "credits_license_text": "EMLy è un software proprietario sviluppato da 3gIT. Tutti i diritti riservati. L'applicazione utilizza varie librerie open source, ciascuna governata dalle rispettive licenze.",
"credits_copyright": "Tutti i diritti riservati." "credits_copyright": "Tutti i diritti riservati.",
"settings_updates_title": "Aggiornamenti",
"settings_updates_description": "Controlla e installa gli aggiornamenti dell'applicazione dalla condivisione di rete.",
"settings_updates_current_version": "Versione corrente",
"settings_updates_available": "Aggiornamento disponibile",
"settings_updates_check_failed": "Controllo fallito",
"settings_updates_no_updates": "Nessun aggiornamento trovato",
"settings_updates_check_label": "Controlla aggiornamenti",
"settings_updates_last_checked": "Ultimo controllo: {time}",
"settings_updates_click_check": "Clicca per cercare aggiornamenti disponibili",
"settings_updates_checking": "Controllo in corso...",
"settings_updates_check_now": "Controlla ora",
"settings_updates_version_available": "Versione {version} disponibile",
"settings_updates_downloading": "Download in corso... {progress}%",
"settings_updates_click_download": "Clicca per scaricare l'aggiornamento",
"settings_updates_download_button": "Scarica",
"settings_updates_ready_title": "Aggiornamento pronto per l'installazione",
"settings_updates_ready_ref": "La versione {version} è stata scaricata e verificata",
"settings_updates_install_button": "Installa ora",
"settings_updates_info_message": "Gli aggiornamenti vengono controllati dal percorso di rete configurato.",
"settings_updates_current_path": "Percorso attuale:",
"settings_updates_no_path": "Nessun percorso di aggiornamento configurato",
"settings_toast_update_available": "Aggiornamento disponibile: {version}",
"settings_toast_latest_version": "Sei sull'ultima versione",
"settings_toast_check_failed": "Impossibile controllare gli aggiornamenti",
"settings_toast_download_success": "Aggiornamento scaricato con successo",
"settings_toast_download_failed": "Impossibile scaricare l'aggiornamento",
"settings_toast_install_failed": "Impossibile avviare l'installazione",
"settings_danger_update_checker_label": "Abilita controllo aggiornamenti",
"settings_danger_update_checker_hint": "Controlla aggiornamenti applicazione dalla condivisione di rete",
"settings_danger_update_checker_info": "Info: Quando abilitato, l'app controllerà gli aggiornamenti dal percorso di rete configurato. Disabilitalo se gestisci gli aggiornamenti manualmente o non hai accesso alla rete."
} }

View File

@@ -36,6 +36,7 @@
"vite-plugin-devtools-json": "^1.0.0" "vite-plugin-devtools-json": "^1.0.0"
}, },
"dependencies": { "dependencies": {
"@rollup/rollup-win32-arm64-msvc": "^4.57.1",
"@types/html2canvas": "^1.0.0", "@types/html2canvas": "^1.0.0",
"dompurify": "^3.3.1", "dompurify": "^3.3.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",

View File

@@ -14,11 +14,11 @@
try { try {
const key = "emly_theme"; const key = "emly_theme";
const stored = localStorage.getItem(key); const stored = localStorage.getItem(key);
const theme = stored === "light" || stored === "dark" ? stored : "dark"; const theme = stored === "light" || stored === "dark" ? stored : "light";
document.documentElement.classList.toggle("dark", theme === "dark"); document.documentElement.classList.toggle("dark", theme === "dark");
} catch { } catch {
// If storage is blocked, default to dark. // If storage is blocked, default to light.
document.documentElement.classList.add("dark"); document.documentElement.classList.remove("dark");
} }
})(); })();
</script> </script>
@@ -67,8 +67,21 @@
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div id="app-loading"> <div id="app-loading">
<div class="loader-spinner"></div> <div class="loader-spinner"></div>
<div>Loading, please wait...</div> <div id="loading-text">Loading...</div>
</div> </div>
<script>
(() => {
try {
const settings = JSON.parse(localStorage.getItem("emly_gui_settings") || "{}");
const lang = settings.selectedLanguage || "en";
const text = lang === "it" ? "Caricamento..." : "Loading...";
const el = document.getElementById("loading-text");
if (el) el.textContent = text;
} catch (e) {
console.error("Failed to localize loading text", e);
}
})();
</script>
<div style="display: contents;">%sveltekit.body%</div> <div style="display: contents;">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@@ -403,8 +403,8 @@
} }
.panel { .panel {
background: rgba(255, 255, 255, 0.04); background: var(--card);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid var(--border);
border-radius: 14px; border-radius: 14px;
overflow: hidden; overflow: hidden;
} }
@@ -423,20 +423,20 @@
height: 34px; height: 34px;
padding: 0 12px; padding: 0 12px;
border-radius: 10px; border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.06); background: var(--muted);
color: inherit; color: var(--muted-foreground);
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.5);
} }
.btn:hover { .btn:hover {
background: rgba(255, 255, 255, 0.09); background: var(--accent);
color: var(--accent-foreground);
} }
.events { .events {
@@ -454,16 +454,16 @@
} }
.email-header-content { .email-header-content {
background: rgba(255, 255, 255, 0.05); background: var(--card);
padding: 16px; padding: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08); border-bottom: 1px solid var(--border);
} }
.email-subject { .email-subject {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
line-height: 1.25; line-height: 1.25;
color: inherit; color: var(--foreground);
min-width: 0; min-width: 0;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
@@ -496,21 +496,21 @@
.email-meta-grid .label { .email-meta-grid .label {
text-align: right; text-align: right;
color: rgba(255, 255, 255, 0.5); color: var(--muted-foreground);
margin-right: 8px; margin-right: 8px;
font-weight: 500; font-weight: 500;
} }
.email-meta-grid .value { .email-meta-grid .value {
color: rgba(255, 255, 255, 0.9); color: var(--foreground);
word-break: break-all; word-break: break-all;
font-weight: 500; font-weight: 500;
} }
.email-attachments { .email-attachments {
padding: 10px 16px; padding: 10px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08); border-bottom: 1px solid var(--border);
background: rgba(255, 255, 255, 0.03); background: var(--muted);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
@@ -522,7 +522,7 @@
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.5); color: var(--muted-foreground);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -538,9 +538,9 @@
height: 28px; height: 28px;
padding: 0 10px; padding: 0 10px;
border-radius: 6px; border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid var(--border);
background: transparent; background: transparent;
color: rgba(255, 255, 255, 0.8); color: var(--foreground);
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
@@ -548,8 +548,8 @@
} }
.att-btn:hover { .att-btn:hover {
background: rgba(255, 255, 255, 0.05); background: var(--accent);
color: #fff; color: var(--accent-foreground);
} }
.att-btn.image { .att-btn.image {
@@ -628,10 +628,10 @@
justify-content: center; justify-content: center;
height: 36px; height: 36px;
padding: 0 16px; padding: 0 16px;
background: rgba(255, 255, 255, 0.1); background: var(--muted);
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
color: white; color: var(--foreground);
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
@@ -639,8 +639,8 @@
} }
.browse-btn:hover { .browse-btn:hover {
background: rgba(255, 255, 255, 0.15); background: var(--accent);
border-color: rgba(255, 255, 255, 0.25); border-color: var(--accent-foreground);
} }
.browse-btn:disabled, .browse-btn:disabled,
@@ -660,12 +660,12 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1); background: var(--border);
border-radius: 6px; border-radius: 6px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2); background: var(--muted-foreground);
} }
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {
@@ -674,7 +674,7 @@
.att-empty { .att-empty {
font-size: 11px; font-size: 11px;
color: rgba(255, 255, 255, 0.4); color: var(--muted-foreground);
font-style: italic; font-style: italic;
} }

View File

@@ -56,8 +56,8 @@
<Sidebar.Root style="opacity: 0.8;"> <Sidebar.Root style="opacity: 0.8;">
<Sidebar.Header> <Sidebar.Header>
<div <div
class="sidebar-title items-center justify-center p-3 border-b border-white/10" class="sidebar-title items-center justify-center p-3 border-b border-border flex"
style="padding: 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); display: flex; justify-content: center;" style="padding: 12px; display: flex; justify-content: center;"
> >
<img src="/appicon.png" alt="Logo" width="64" height="64" /> <img src="/appicon.png" alt="Logo" width="64" height="64" />
<span <span

View File

@@ -5,7 +5,7 @@
let { onSave, onReset } = $props(); let { onSave, onReset } = $props();
</script> </script>
<div class="flex items-center gap-4 rounded-lg border bg-background px-4 py-3 shadow-lg w-full max-w-md"> <div class="flex items-center gap-4 rounded-lg border bg-card px-4 py-3 shadow-lg w-full max-w-md">
<span class="text-sm text-muted-foreground flex-1"> <span class="text-sm text-muted-foreground flex-1">
{m.settings_unsaved_toast_message()} {m.settings_unsaved_toast_message()}
</span> </span>
@@ -19,9 +19,3 @@
</Button> </Button>
</div> </div>
</div> </div>
<style>
.bg-background {
background-color: oklch(0.205 0 0);
}
</style>

View File

@@ -1,6 +1,8 @@
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import type { EMLy_GUI_Settings } from "$lib/types"; import type { EMLy_GUI_Settings } from "$lib/types";
import { getFromLocalStorage, saveToLocalStorage } from "$lib/utils/localStorageHelper"; import { getFromLocalStorage, saveToLocalStorage } from "$lib/utils/localStorageHelper";
import { applyTheme, getStoredTheme } from "$lib/utils/theme";
import { setLocale } from "$lib/paraglide/runtime";
const STORAGE_KEY = "emly_gui_settings"; const STORAGE_KEY = "emly_gui_settings";
@@ -11,7 +13,9 @@ const defaults: EMLy_GUI_Settings = {
previewFileSupportedTypes: ["jpg", "jpeg", "png"], previewFileSupportedTypes: ["jpg", "jpeg", "png"],
enableAttachedDebuggerProtection: true, enableAttachedDebuggerProtection: true,
useDarkEmailViewer: true, useDarkEmailViewer: true,
enableUpdateChecker: true, enableUpdateChecker: false,
musicInspirationEnabled: false,
theme: "dark",
}; };
class SettingsStore { class SettingsStore {
@@ -33,6 +37,37 @@ class SettingsStore {
console.error("Failed to load settings", e); console.error("Failed to load settings", e);
} }
} }
// Migration: Check for legacy musicInspirationEnabled key
const legacyMusic = getFromLocalStorage("musicInspirationEnabled");
if (legacyMusic !== null) {
this.settings.musicInspirationEnabled = legacyMusic === "true";
localStorage.removeItem("musicInspirationEnabled");
this.save(); // Save immediately to persist the migration
}
// Sync theme from localStorage key used in app.html
const storedTheme = getStoredTheme();
if (!this.settings.theme) {
this.settings.theme = storedTheme;
} else if (this.settings.theme !== storedTheme) {
// If there's a mismatch, prioritize the theme from emly_theme key
this.settings.theme = storedTheme;
}
// Apply the theme
applyTheme(this.settings.theme);
// Apply the language
if (this.settings.selectedLanguage) {
setLocale(this.settings.selectedLanguage);
}
// Save defaults/merged settings to storage if they didn't exist or were updated during load
if (!stored) {
this.save();
}
this.hasHydrated = true; this.hasHydrated = true;
} }
@@ -43,11 +78,20 @@ class SettingsStore {
update(newSettings: Partial<EMLy_GUI_Settings>) { update(newSettings: Partial<EMLy_GUI_Settings>) {
this.settings = { ...this.settings, ...newSettings }; this.settings = { ...this.settings, ...newSettings };
// Apply theme if it changed
if (newSettings.theme && this.settings.theme) {
applyTheme(this.settings.theme);
}
this.save(); this.save();
} }
reset() { reset() {
this.settings = { ...defaults }; this.settings = { ...defaults };
if (this.settings.theme) {
applyTheme(this.settings.theme);
}
this.save(); this.save();
} }
} }

View File

@@ -10,6 +10,8 @@ interface EMLy_GUI_Settings {
enableAttachedDebuggerProtection?: boolean; enableAttachedDebuggerProtection?: boolean;
useDarkEmailViewer?: boolean; useDarkEmailViewer?: boolean;
enableUpdateChecker?: boolean; enableUpdateChecker?: boolean;
musicInspirationEnabled?: boolean;
theme?: "light" | "dark";
} }
type SupportedLanguages = "en" | "it"; type SupportedLanguages = "en" | "it";

View File

@@ -0,0 +1,45 @@
import { browser } from "$app/environment";
const THEME_KEY = "emly_theme";
export type Theme = "light" | "dark";
/**
* Applies the theme to the document element and saves it to localStorage
*/
export function applyTheme(theme: Theme) {
if (!browser) return;
const isDark = theme === "dark";
document.documentElement.classList.toggle("dark", isDark);
try {
localStorage.setItem(THEME_KEY, theme);
} catch (e) {
console.error("Failed to save theme to localStorage:", e);
}
}
/**
* Gets the current theme from localStorage or returns the default
*/
export function getStoredTheme(): Theme {
if (!browser) return "light";
try {
const stored = localStorage.getItem(THEME_KEY);
return stored === "light" || stored === "dark" ? stored : "light";
} catch {
return "light";
}
}
/**
* Toggles between light and dark theme
*/
export function toggleTheme(): Theme {
const current = getStoredTheme();
const newTheme: Theme = current === "dark" ? "light" : "dark";
applyTheme(newTheme);
return newTheme;
}

View File

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

View File

@@ -1,15 +1,48 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto, preloadData } from "$app/navigation";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import * as Card from "$lib/components/ui/card"; import * as Card from "$lib/components/ui/card";
import { Separator } from "$lib/components/ui/separator"; 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 * as m from "$lib/paraglide/messages";
import { OpenURLInBrowser } from "$lib/wailsjs/go/main/App"; 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 { data } = $props();
let config = $derived(data.config); 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 // Open external URL in default browser
async function openUrl(url: string) { async function openUrl(url: string) {
try { try {
@@ -67,7 +100,7 @@
]; ];
</script> </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 <div
class="mx-auto flex max-w-3xl flex-col gap-4 px-4 py-6 sm:px-6 sm:py-10 opacity-80" 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.Header>
<Card.Content class="space-y-4"> <Card.Content class="space-y-4">
{#each team as member} {#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 <img
src={gravatarUrls[member.email]} src={gravatarUrls[member.email]}
alt={member.name} 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="font-medium">{member.username} ({member.name})</div>
<div class="text-sm text-primary/80">{member.role}</div> <div class="text-sm text-primary/80">{member.role}</div>
<div class="text-sm text-muted-foreground mt-1">{member.description}</div> <div class="text-sm text-muted-foreground mt-1">{member.description}</div>
<a <a
href="mailto:{member.email}" 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" /> <Mail class="size-3" />
{member.email} {member.email}

View File

@@ -6,69 +6,11 @@
import { ChevronLeft, Music, ExternalLink } from "@lucide/svelte"; import { ChevronLeft, Music, ExternalLink } from "@lucide/svelte";
import * as m from "$lib/paraglide/messages"; import * as m from "$lib/paraglide/messages";
import { OpenURLInBrowser } from "$lib/wailsjs/go/main/App"; import { OpenURLInBrowser } from "$lib/wailsjs/go/main/App";
import type { SpotifyTrack } from "./+page";
let { data } = $props(); let { data } = $props();
let config = $derived(data.config); let config = $derived(data.config);
let tracks: SpotifyTrack[] = $derived(data.tracks ?? []);
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"
}
];
// Open external URL in default browser // Open external URL in default browser
async function openUrl(url: string) { async function openUrl(url: string) {
@@ -80,7 +22,7 @@
} }
</script> </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 <div
class="mx-auto flex max-w-4xl flex-col gap-4 px-4 py-6 sm:px-6 sm:py-10 opacity-80" 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.Header>
<Card.Content> <Card.Content>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-2"> <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="group relative">
<div class="overflow-hidden rounded-lg bg-muted" style="height: 352px;"> <div class="overflow-hidden rounded-lg bg-muted">
<iframe {#if track.embedHtml}
src={track.embedUrl} {@html track.embedHtml}
width="100%" {:else}
height="352" <iframe
frameborder="0" src={track.embedUrl}
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" width="100%"
loading="lazy" height="352"
title={`${track.artist} - ${track.name}`} frameborder="0"
class="rounded-lg" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
></iframe> loading="lazy"
</div> title={`${track.artist} - ${track.name}`}
<div class="mt-2 flex items-start justify-between gap-2"> class="rounded-lg"
<div class="min-w-0 flex-1"> ></iframe>
<p class="truncate text-sm font-medium">{track.name}</p> {/if}
<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> </div>
</div> </div>
{/each} {/each}
</div> </div>
@@ -184,10 +114,6 @@
and and
<span class="text-red-500"></span> <span class="text-red-500"></span>
</p> </p>
<p class="mt-1">
GUI: {config ? `v${config.GUISemver}` : "N/A"} •
SDK: {config ? `v${config.SDKDecoderSemver}` : "N/A"}
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,18 +2,98 @@ import type { PageLoad } from './$types';
import { GetConfig } from "$lib/wailsjs/go/main/App"; import { GetConfig } from "$lib/wailsjs/go/main/App";
import { browser } from '$app/environment'; import { browser } from '$app/environment';
export const load = (async () => { export interface SpotifyTrack {
if (!browser) return { config: null }; 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 { try {
const configRoot = await GetConfig(); const [configRoot, ...tracks] = await Promise.all([
GetConfig(),
...inspirationTracks.map(t => fetchEmbedHtml(t, fetch))
]);
return { return {
config: configRoot.EMLy config: configRoot.EMLy,
tracks
}; };
} catch (e) { } catch (e) {
console.error("Failed to load config for inspiration", e); console.error("Failed to load data for inspiration", e);
return { return {
config: null config: null,
tracks: inspirationTracks
}; };
} }
}) satisfies PageLoad; }) satisfies PageLoad;

View File

@@ -6,7 +6,7 @@
import { Label } from "$lib/components/ui/label"; import { Label } from "$lib/components/ui/label";
import { Separator } from "$lib/components/ui/separator"; import { Separator } from "$lib/components/ui/separator";
import { Switch } from "$lib/components/ui/switch"; 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 type { EMLy_GUI_Settings } from "$lib/types";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { It, Us } from "svelte-flags"; import { It, Us } from "svelte-flags";
@@ -40,7 +40,8 @@
previewFileSupportedTypes: ["jpg", "jpeg", "png"], previewFileSupportedTypes: ["jpg", "jpeg", "png"],
enableAttachedDebuggerProtection: true, enableAttachedDebuggerProtection: true,
useDarkEmailViewer: true, useDarkEmailViewer: true,
enableUpdateChecker: true, enableUpdateChecker: false,
theme: "dark",
}; };
async function setLanguage( async function setLanguage(
@@ -73,8 +74,10 @@
s.enableAttachedDebuggerProtection ?? defaults.enableAttachedDebuggerProtection ?? true, s.enableAttachedDebuggerProtection ?? defaults.enableAttachedDebuggerProtection ?? true,
useDarkEmailViewer: useDarkEmailViewer:
s.useDarkEmailViewer ?? defaults.useDarkEmailViewer ?? true, s.useDarkEmailViewer ?? defaults.useDarkEmailViewer ?? true,
enableUpdateChecker: enableUpdateChecker: runningInDevMode
s.enableUpdateChecker ?? defaults.enableUpdateChecker ?? true, ? false
: (s.enableUpdateChecker ?? defaults.enableUpdateChecker ?? true),
theme: s.theme || defaults.theme || "light",
}; };
} }
@@ -86,6 +89,7 @@
!!a.enableAttachedDebuggerProtection === !!b.enableAttachedDebuggerProtection && !!a.enableAttachedDebuggerProtection === !!b.enableAttachedDebuggerProtection &&
!!a.useDarkEmailViewer === !!b.useDarkEmailViewer && !!a.useDarkEmailViewer === !!b.useDarkEmailViewer &&
!!a.enableUpdateChecker === !!b.enableUpdateChecker && !!a.enableUpdateChecker === !!b.enableUpdateChecker &&
(a.theme ?? "light") === (b.theme ?? "light") &&
JSON.stringify(a.previewFileSupportedTypes?.sort()) === JSON.stringify(a.previewFileSupportedTypes?.sort()) ===
JSON.stringify(b.previewFileSupportedTypes?.sort()) JSON.stringify(b.previewFileSupportedTypes?.sort())
); );
@@ -278,25 +282,25 @@
updateStatus = status; updateStatus = status;
if (status.updateAvailable) { if (status.updateAvailable) {
toast.success(`Update available: ${status.availableVersion}`); toast.success(m.settings_toast_update_available({ version: status.availableVersion }));
} else if (!status.errorMessage) { } else if (!status.errorMessage) {
toast.info("You're on the latest version"); toast.info(m.settings_toast_latest_version());
} else { } else {
toast.error(status.errorMessage); toast.error(status.errorMessage);
} }
} catch (err) { } catch (err) {
console.error("Failed to check for updates:", 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() { async function downloadUpdate() {
try { try {
await DownloadUpdate(); await DownloadUpdate();
toast.success("Update downloaded successfully"); toast.success(m.settings_toast_download_success());
} catch (err) { } catch (err) {
console.error("Failed to download update:", 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 // App will quit, so no toast needed
} catch (err) { } catch (err) {
console.error("Failed to install update:", 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> </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 <div
class="mx-auto flex max-w-3xl flex-col gap-4 px-4 py-6 sm:px-6 sm:py-10 opacity-80" 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.Content>
</Card.Root> </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.Root>
<Card.Header class="space-y-1"> <Card.Header class="space-y-1">
<Card.Title>{m.settings_export_import_title()}</Card.Title> <Card.Title>{m.settings_export_import_title()}</Card.Title>
@@ -592,14 +642,14 @@
{#if form.enableUpdateChecker} {#if form.enableUpdateChecker}
<Card.Root> <Card.Root>
<Card.Header class="space-y-1"> <Card.Header class="space-y-1">
<Card.Title>Updates</Card.Title> <Card.Title>{m.settings_updates_title()}</Card.Title>
<Card.Description>Check for and install application updates from your network share</Card.Description> <Card.Description>{m.settings_updates_description()}</Card.Description>
</Card.Header> </Card.Header>
<Card.Content class="space-y-4"> <Card.Content class="space-y-4">
<!-- Current Version --> <!-- Current Version -->
<div class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4"> <div class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4">
<div> <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"> <div class="text-sm text-muted-foreground">
{updateStatus.currentVersion} ({config?.GUIReleaseChannel || "stable"}) {updateStatus.currentVersion} ({config?.GUIReleaseChannel || "stable"})
</div> </div>
@@ -607,17 +657,17 @@
{#if updateStatus.updateAvailable} {#if updateStatus.updateAvailable}
<div class="flex items-center gap-2 text-sm font-medium text-green-600 dark:text-green-400"> <div class="flex items-center gap-2 text-sm font-medium text-green-600 dark:text-green-400">
<AlertCircle class="size-4" /> <AlertCircle class="size-4" />
Update Available {m.settings_updates_available()}
</div> </div>
{:else if updateStatus.errorMessage && updateStatus.lastCheckTime} {:else if updateStatus.errorMessage && updateStatus.lastCheckTime}
<div class="flex items-center gap-2 text-sm text-destructive"> <div class="flex items-center gap-2 text-sm text-destructive">
<AlertCircle class="size-4" /> <AlertCircle class="size-4" />
Check failed {m.settings_updates_check_failed()}
</div> </div>
{:else if updateStatus.lastCheckTime} {:else if updateStatus.lastCheckTime}
<div class="flex items-center gap-2 text-sm text-muted-foreground"> <div class="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle2 class="size-4" /> <CheckCircle2 class="size-4" />
No updates found {m.settings_updates_no_updates()}
</div> </div>
{/if} {/if}
</div> </div>
@@ -627,12 +677,12 @@
<!-- Check for Updates --> <!-- Check for Updates -->
<div class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4"> <div class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4">
<div> <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"> <div class="text-sm text-muted-foreground">
{#if updateStatus.lastCheckTime} {#if updateStatus.lastCheckTime}
Last checked: {updateStatus.lastCheckTime} {m.settings_updates_last_checked({ time: updateStatus.lastCheckTime })}
{:else} {:else}
Click to check for available updates {m.settings_updates_click_check()}
{/if} {/if}
</div> </div>
</div> </div>
@@ -643,7 +693,7 @@
disabled={updateStatus.checking || updateStatus.downloading} disabled={updateStatus.checking || updateStatus.downloading}
> >
<RefreshCw class="size-4 mr-2 {updateStatus.checking ? 'animate-spin' : ''}" /> <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> </Button>
</div> </div>
@@ -652,12 +702,12 @@
<Separator /> <Separator />
<div class="flex items-center justify-between gap-4 rounded-lg border border-blue-500/30 bg-blue-500/10 p-4"> <div class="flex items-center justify-between gap-4 rounded-lg border border-blue-500/30 bg-blue-500/10 p-4">
<div> <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"> <div class="text-sm text-muted-foreground">
{#if updateStatus.downloading} {#if updateStatus.downloading}
Downloading... {updateStatus.downloadProgress}% {m.settings_updates_downloading({ progress: updateStatus.downloadProgress })}
{:else} {:else}
Click to download the update {m.settings_updates_click_download()}
{/if} {/if}
</div> </div>
{#if updateStatus.releaseNotes} {#if updateStatus.releaseNotes}
@@ -673,7 +723,7 @@
disabled={updateStatus.downloading} disabled={updateStatus.downloading}
> >
<Download class="size-4 mr-2" /> <Download class="size-4 mr-2" />
{updateStatus.downloading ? `${updateStatus.downloadProgress}%` : "Download"} {updateStatus.downloading ? `${updateStatus.downloadProgress}%` : m.settings_updates_download_button()}
</Button> </Button>
</div> </div>
{/if} {/if}
@@ -683,9 +733,9 @@
<Separator /> <Separator />
<div class="flex items-center justify-between gap-4 rounded-lg border border-green-500/30 bg-green-500/10 p-4"> <div class="flex items-center justify-between gap-4 rounded-lg border border-green-500/30 bg-green-500/10 p-4">
<div> <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"> <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>
</div> </div>
<Button <Button
@@ -694,7 +744,7 @@
onclick={installUpdate} onclick={installUpdate}
> >
<CheckCircle2 class="size-4 mr-2" /> <CheckCircle2 class="size-4 mr-2" />
Install Now {m.settings_updates_install_button()}
</Button> </Button>
</div> </div>
{/if} {/if}
@@ -713,11 +763,11 @@
<!-- Info about update path --> <!-- Info about update path -->
<div class="text-xs text-muted-foreground"> <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} {#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} {: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} {/if}
</div> </div>
</Card.Content> </Card.Content>
@@ -843,18 +893,19 @@
class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4 border-destructive/30" class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4 border-destructive/30"
> >
<div class="space-y-1"> <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"> <div class="text-sm text-muted-foreground">
Check for application updates from network share {m.settings_danger_update_checker_hint()}
</div> </div>
</div> </div>
<Switch <Switch
bind:checked={form.enableUpdateChecker} bind:checked={form.enableUpdateChecker}
class="cursor-pointer hover:cursor-pointer" class="cursor-pointer hover:cursor-pointer"
disabled={runningInDevMode}
/> />
</div> </div>
<div class="text-xs text-muted-foreground"> <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> </div>
<Separator /> <Separator />
@@ -871,6 +922,7 @@
</Card.Root> </Card.Root>
{/if} {/if}
{#if !runningInDevMode}
<AlertDialog.Root bind:open={dangerWarningOpen}> <AlertDialog.Root bind:open={dangerWarningOpen}>
<AlertDialog.Content> <AlertDialog.Content>
<AlertDialog.Header> <AlertDialog.Header>
@@ -878,7 +930,11 @@
>{m.settings_danger_alert_title()}</AlertDialog.Title >{m.settings_danger_alert_title()}</AlertDialog.Title
> >
<AlertDialog.Description> <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.Description>
</AlertDialog.Header> </AlertDialog.Header>
<AlertDialog.Footer> <AlertDialog.Footer>
@@ -888,5 +944,6 @@
</AlertDialog.Footer> </AlertDialog.Footer>
</AlertDialog.Content> </AlertDialog.Content>
</AlertDialog.Root> </AlertDialog.Root>
{/if}
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
#define ApplicationName 'EMLy' #define ApplicationName 'EMLy'
#define ApplicationVersion GetVersionNumbersString('EMLy.exe') #define ApplicationVersion GetVersionNumbersString('EMLy.exe')
#define ApplicationVersion '1.4.1' #define ApplicationVersion '1.5.0'
[Setup] [Setup]
AppName={#ApplicationName} AppName={#ApplicationName}
@@ -20,6 +20,8 @@ PrivilegesRequiredOverridesAllowed=dialog
SetupIconFile=..\build\windows\icon.ico SetupIconFile=..\build\windows\icon.ico
UninstallDisplayIcon={app}\{#ApplicationName}.exe UninstallDisplayIcon={app}\{#ApplicationName}.exe
AppVerName={#ApplicationName} {#ApplicationVersion} AppVerName={#ApplicationName} {#ApplicationVersion}
WizardStyle=modern dynamic includetitlebar
[Files] [Files]
; Source path relative to this .iss file (assuming it is in the "installer" folder and build is in "../build") ; Source path relative to this .iss file (assuming it is in the "installer" folder and build is in "../build")