Adds update checker with user preference

Introduces an update checker feature that respects the user's preference, allowing them to enable or disable automatic update checks.

The setting is persisted in the config file and synced to the backend.

Also introduces a page dedicated to listing music that inspired the project, and makes some minor UI improvements
This commit is contained in:
Flavio Fois
2026-02-08 22:09:32 +01:00
parent 0cfe1b65f3
commit 5b62790248
9 changed files with 346 additions and 17 deletions

View File

@@ -98,3 +98,33 @@ func (a *App) ImportSettings() (string, error) {
return string(data), nil
}
// SetUpdateCheckerEnabled updates the UPDATE_CHECK_ENABLED setting in config.ini
// based on the user's preference from the GUI settings.
//
// Parameters:
// - enabled: true to enable update checking, false to disable
//
// Returns:
// - error: Error if loading or saving config fails
func (a *App) SetUpdateCheckerEnabled(enabled bool) error {
// Load current config
config := a.GetConfig()
if config == nil {
return fmt.Errorf("failed to load config")
}
// Update the setting
if enabled {
config.EMLy.UpdateCheckEnabled = "true"
} else {
config.EMLy.UpdateCheckEnabled = "false"
}
// Save config back to disk
if err := a.SaveConfig(config); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}

View File

@@ -1,9 +1,9 @@
[EMLy]
SDK_DECODER_SEMVER="1.3.2"
SDK_DECODER_RELEASE_CHANNEL="beta"
GUI_SEMVER="1.4.0"
GUI_RELEASE_CHANNEL="stable"
LANGUAGE="it"
UPDATE_CHECK_ENABLED="true"
UPDATE_PATH=""
UPDATE_AUTO_CHECK="true"
SDK_DECODER_SEMVER = 1.3.2
SDK_DECODER_RELEASE_CHANNEL = beta
GUI_SEMVER = 1.4.0
GUI_RELEASE_CHANNEL = stable
LANGUAGE = it
UPDATE_CHECK_ENABLED = false
UPDATE_PATH =
UPDATE_AUTO_CHECK = true

View File

@@ -3,7 +3,7 @@
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import { dangerZoneEnabled } from "$lib/stores/app";
import * as m from "$lib/paraglide/messages.js";
import { Mail, Heart } from "@lucide/svelte/icons";
import { Mail, Heart, Info } from "@lucide/svelte/icons";
const CLICK_WINDOW_MS = 4000;
const REQUIRED_CLICKS = 10;
@@ -46,7 +46,7 @@
{
title: m.sidebar_credits(),
url: "/credits",
icon: Heart,
icon: Info,
disabled: false,
id: 3,
},

View File

@@ -11,6 +11,7 @@ const defaults: EMLy_GUI_Settings = {
previewFileSupportedTypes: ["jpg", "jpeg", "png"],
enableAttachedDebuggerProtection: true,
useDarkEmailViewer: true,
enableUpdateChecker: true,
};
class SettingsStore {

View File

@@ -9,6 +9,25 @@ interface EMLy_GUI_Settings {
previewFileSupportedTypes?: SupportedFileTypePreview[];
enableAttachedDebuggerProtection?: boolean;
useDarkEmailViewer?: boolean;
enableUpdateChecker?: boolean;
}
type SupportedLanguages = "en" | "it";
// Plugin System Types
interface PluginFormatSupport {
extensions: string[];
mime_types?: string[];
priority: number;
}
interface PluginInfo {
name: string;
version: string;
author: string;
description: string;
capabilities: string[];
status: "unloaded" | "loading" | "active" | "error" | "disabled";
enabled: boolean;
last_error?: string;
supported_formats?: PluginFormatSupport[];
}

View File

@@ -24,6 +24,8 @@
CheckCircle,
Camera,
Heart,
Info,
Music
} from "@lucide/svelte";
import { Separator } from "$lib/components/ui/separator/index.js";
import { toast } from "svelte-sonner";
@@ -380,7 +382,7 @@
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
<Heart
<Info
size="16"
onclick={() => {
if (page.url.pathname !== "/credits" && page.url.pathname !== "/credits/")
@@ -389,12 +391,11 @@
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
<Separator orientation="vertical" />
<Bug
<Music
size="16"
onclick={() => {
$bugReportDialogOpen = !$bugReportDialogOpen;
if (page.url.pathname !== "/inspiration" && page.url.pathname !== "/inspiration/")
goto("/inspiration");
}}
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
@@ -410,6 +411,19 @@
>
<RefreshCcwDot />
</a>
<!-- svelte-ignore a11y_invalid_attribute -->
<a
href="#"
class={`${buttonVariants({ variant: "destructive" })} cursor-pointer hover:cursor-pointer`}
style="text-decoration: none; height: 24px; font-size: 12px; padding: 0 8px;"
aria-label={m.settings_danger_reload_button()}
title={m.settings_danger_reload_button() + " app"}
onclick={() => {
$bugReportDialogOpen = !$bugReportDialogOpen;
}}
>
<Bug />
</a>
</div>

View File

@@ -0,0 +1,199 @@
<script lang="ts">
import { goto } 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, Music, ExternalLink } from "@lucide/svelte";
import * as m from "$lib/paraglide/messages";
import { OpenURLInBrowser } from "$lib/wailsjs/go/main/App";
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"
}
];
// Open external URL in default browser
async function openUrl(url: string) {
try {
await OpenURLInBrowser(url);
} catch (e) {
console.error("Failed to open URL:", e);
}
}
</script>
<div class="min-h-[calc(100vh-1rem)] 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"
>
<header class="flex items-start justify-between gap-3">
<div class="min-w-0">
<h1
class="text-balance text-2xl font-semibold tracking-tight sm:text-3xl"
>
Musical Inspiration
</h1>
<p class="mt-2 text-sm text-muted-foreground">
This project was mainly coded to the following tracks
</p>
</div>
<Button
class="cursor-pointer hover:cursor-pointer"
variant="ghost"
onclick={() => goto("/")}
>
<ChevronLeft class="size-4" /> Back
</Button>
</header>
<Separator class="my-2" />
<!-- Spotify Embeds -->
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Music class="size-5" />
FOISX's Soundtrack
</Card.Title>
<Card.Description>
The albums and tracks that fueled the development of EMLy
</Card.Description>
</Card.Header>
<Card.Content>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-2">
{#each inspirationTracks as track}
<div class="group relative">
<div class="overflow-hidden rounded-lg bg-muted" style="height: 352px;">
<iframe
src={track.embedUrl}
width="100%"
height="352"
frameborder="0"
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
title={`${track.artist} - ${track.name}`}
class="rounded-lg"
></iframe>
</div>
<div class="mt-2 flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">{track.name}</p>
<p class="truncate text-xs text-muted-foreground">
{track.artist}
</p>
</div>
<Button
variant="ghost"
size="icon"
class="size-8 shrink-0 opacity-70 hover:opacity-100"
onclick={() => openUrl(track.spotifyUrl)}
title="Open in Spotify"
>
<ExternalLink class="size-4" />
</Button>
</div>
</div>
{/each}
</div>
</Card.Content>
</Card.Root>
<!-- Fun fact -->
<Card.Root class="border-primary/20 bg-primary/5">
<Card.Content class="">
<div class="flex items-start gap-3">
<Music class="size-5 text-primary mt-0.5 shrink-0" />
<div class="space-y-1">
<p class="text-sm font-medium">The Soundtrack</p>
<p class="text-sm text-muted-foreground">
These are just a small sample of what helped inspire the project.
Although they represent a wide variety of emotions, themes and genres, some exploring deep meanings
of betrayal, personal struggles, and introspection, they provided solace and strength to the main developer
during challenging times.
<br/>
Music has a unique way of transforming pain into creative energy..
</p>
</div>
</div>
</Card.Content>
</Card.Root>
<!-- Footer note -->
<div class="text-center text-xs text-muted-foreground">
<p>
Made with
<Music class="inline-block size-3 mx-1" />
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>
<style>
iframe {
border-radius: 0.5rem;
}
</style>

View File

@@ -0,0 +1,19 @@
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 };
try {
const configRoot = await GetConfig();
return {
config: configRoot.EMLy
};
} catch (e) {
console.error("Failed to load config for inspiration", e);
return {
config: null
};
}
}) satisfies PageLoad;

View File

@@ -25,7 +25,7 @@
import { setLocale } from "$lib/paraglide/runtime";
import { mailState } from "$lib/stores/mail-state.svelte.js";
import { dev } from '$app/environment';
import { ExportSettings, ImportSettings, CheckForUpdates, DownloadUpdate, InstallUpdate, GetUpdateStatus } from "$lib/wailsjs/go/main/App";
import { ExportSettings, ImportSettings, CheckForUpdates, DownloadUpdate, InstallUpdate, GetUpdateStatus, SetUpdateCheckerEnabled } from "$lib/wailsjs/go/main/App";
import { EventsOn, EventsOff } from "$lib/wailsjs/runtime/runtime";
let { data } = $props();
@@ -40,6 +40,7 @@
previewFileSupportedTypes: ["jpg", "jpeg", "png"],
enableAttachedDebuggerProtection: true,
useDarkEmailViewer: true,
enableUpdateChecker: true,
};
async function setLanguage(
@@ -72,6 +73,8 @@
s.enableAttachedDebuggerProtection ?? defaults.enableAttachedDebuggerProtection ?? true,
useDarkEmailViewer:
s.useDarkEmailViewer ?? defaults.useDarkEmailViewer ?? true,
enableUpdateChecker:
s.enableUpdateChecker ?? defaults.enableUpdateChecker ?? true,
};
}
@@ -82,6 +85,7 @@
!!a.useBuiltinPDFViewer === !!b.useBuiltinPDFViewer &&
!!a.enableAttachedDebuggerProtection === !!b.enableAttachedDebuggerProtection &&
!!a.useDarkEmailViewer === !!b.useDarkEmailViewer &&
!!a.enableUpdateChecker === !!b.enableUpdateChecker &&
JSON.stringify(a.previewFileSupportedTypes?.sort()) ===
JSON.stringify(b.previewFileSupportedTypes?.sort())
);
@@ -180,6 +184,23 @@
})();
});
// Sync update checker setting to backend config.ini
let previousUpdateCheckerEnabled = form.enableUpdateChecker;
$effect(() => {
(async () => {
if (!browser) return;
if (form.enableUpdateChecker !== previousUpdateCheckerEnabled) {
try {
await SetUpdateCheckerEnabled(form.enableUpdateChecker ?? true);
LogDebug(`Update checker ${form.enableUpdateChecker ? 'enabled' : 'disabled'}`);
} catch (err) {
console.error('Failed to sync update checker setting:', err);
}
previousUpdateCheckerEnabled = form.enableUpdateChecker;
}
})();
});
async function exportSettings() {
try {
const settingsJSON = JSON.stringify(form, null, 2);
@@ -568,6 +589,7 @@
</Card.Root>
<!-- Update Section -->
{#if form.enableUpdateChecker}
<Card.Root>
<Card.Header class="space-y-1">
<Card.Title>Updates</Card.Title>
@@ -587,10 +609,15 @@
<AlertCircle class="size-4" />
Update 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
</div>
{:else if updateStatus.lastCheckTime}
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle2 class="size-4" />
Up to date
No updates found
</div>
{/if}
</div>
@@ -695,6 +722,7 @@
</div>
</Card.Content>
</Card.Root>
{/if}
{#if $dangerZoneEnabled || dev}
<Card.Root class="border-destructive/50 bg-destructive/15">
@@ -811,6 +839,25 @@
</div>
<Separator />
<div
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>
<div class="text-sm text-muted-foreground">
Check for application updates from network share
</div>
</div>
<Switch
bind:checked={form.enableUpdateChecker}
class="cursor-pointer hover:cursor-pointer"
/>
</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.
</div>
<Separator />
<div class="text-xs text-muted-foreground">
GUI: {config
? `${config.GUISemver} (${config.GUIReleaseChannel})`