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:
@@ -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
|
||||
}
|
||||
|
||||
16
config.ini
16
config.ini
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ const defaults: EMLy_GUI_Settings = {
|
||||
previewFileSupportedTypes: ["jpg", "jpeg", "png"],
|
||||
enableAttachedDebuggerProtection: true,
|
||||
useDarkEmailViewer: true,
|
||||
enableUpdateChecker: true,
|
||||
};
|
||||
|
||||
class SettingsStore {
|
||||
|
||||
19
frontend/src/lib/types.d.ts
vendored
19
frontend/src/lib/types.d.ts
vendored
@@ -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[];
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
199
frontend/src/routes/(app)/inspiration/+page.svelte
Normal file
199
frontend/src/routes/(app)/inspiration/+page.svelte
Normal 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>
|
||||
19
frontend/src/routes/(app)/inspiration/+page.ts
Normal file
19
frontend/src/routes/(app)/inspiration/+page.ts
Normal 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;
|
||||
@@ -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})`
|
||||
|
||||
Reference in New Issue
Block a user