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

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

View File

@@ -50,7 +50,9 @@
"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_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_toast_reverted": "Reverted to last saved settings.",
"settings_toast_save_failed": "Failed to save settings.",
@@ -98,6 +100,12 @@
"bugreport_email_placeholder": "your.email@example.com",
"bugreport_text_label": "Bug Description",
"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_screenshot_label": "Attached Screenshot:",
"bugreport_cancel": "Cancel",
@@ -160,5 +168,35 @@
"credits_lib_dompurify": "XSS sanitizer for HTML content",
"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_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_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_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_toast_reverted": "Ripristinato alle ultime impostazioni salvate.",
"settings_toast_save_failed": "Impossibile salvare le impostazioni.",
@@ -98,6 +100,12 @@
"bugreport_email_placeholder": "tua.email@esempio.com",
"bugreport_text_label": "Descrizione del Bug",
"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_screenshot_label": "Screenshot Allegato:",
"bugreport_cancel": "Annulla",
@@ -160,5 +168,35 @@
"credits_lib_dompurify": "Sanitizzatore XSS per contenuti HTML",
"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_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"
},
"dependencies": {
"@rollup/rollup-win32-arm64-msvc": "^4.57.1",
"@types/html2canvas": "^1.0.0",
"dompurify": "^3.3.1",
"html2canvas": "^1.4.1",

View File

@@ -14,11 +14,11 @@
try {
const key = "emly_theme";
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");
} catch {
// If storage is blocked, default to dark.
document.documentElement.classList.add("dark");
// If storage is blocked, default to light.
document.documentElement.classList.remove("dark");
}
})();
</script>
@@ -67,8 +67,21 @@
<body data-sveltekit-preload-data="hover">
<div id="app-loading">
<div class="loader-spinner"></div>
<div>Loading, please wait...</div>
<div id="loading-text">Loading...</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>
</body>
</html>

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
let { onSave, onReset } = $props();
</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">
{m.settings_unsaved_toast_message()}
</span>
@@ -19,9 +19,3 @@
</Button>
</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 type { EMLy_GUI_Settings } from "$lib/types";
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";
@@ -11,7 +13,9 @@ const defaults: EMLy_GUI_Settings = {
previewFileSupportedTypes: ["jpg", "jpeg", "png"],
enableAttachedDebuggerProtection: true,
useDarkEmailViewer: true,
enableUpdateChecker: true,
enableUpdateChecker: false,
musicInspirationEnabled: false,
theme: "dark",
};
class SettingsStore {
@@ -33,6 +37,37 @@ class SettingsStore {
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;
}
@@ -43,11 +78,20 @@ class SettingsStore {
update(newSettings: Partial<EMLy_GUI_Settings>) {
this.settings = { ...this.settings, ...newSettings };
// Apply theme if it changed
if (newSettings.theme && this.settings.theme) {
applyTheme(this.settings.theme);
}
this.save();
}
reset() {
this.settings = { ...defaults };
if (this.settings.theme) {
applyTheme(this.settings.theme);
}
this.save();
}
}

View File

@@ -10,6 +10,8 @@ interface EMLy_GUI_Settings {
enableAttachedDebuggerProtection?: boolean;
useDarkEmailViewer?: boolean;
enableUpdateChecker?: boolean;
musicInspirationEnabled?: boolean;
theme?: "light" | "dark";
}
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 { 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,6 +391,7 @@
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
{#if settingsStore.settings.musicInspirationEnabled}
<Music
size="16"
onclick={() => {
@@ -400,6 +401,7 @@
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,9 +61,12 @@
</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;">
<div class="overflow-hidden rounded-lg bg-muted">
{#if track.embedHtml}
{@html track.embedHtml}
{:else}
<iframe
src={track.embedUrl}
width="100%"
@@ -132,24 +77,9 @@
title={`${track.artist} - ${track.name}`}
class="rounded-lg"
></iframe>
{/if}
</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>
</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 {

View File

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