Compare commits

..

4 Commits

6 changed files with 375 additions and 273 deletions

35
CHANGELOG.md Normal file
View File

@@ -0,0 +1,35 @@
# Changelog EMLy
## 1.5.4 (2026-02-10)
1) Aggiunti i pulsanti "Download" al MailViewer, PDF e Image viewer, per scaricare il file invece di aprirlo direttamente.2
2) Refactor del sistema di bug report.
3) Rimosso temporaneamente il fetching dei dati macchina all'apertura della pagine delle impostazioni, per evitare problemi di performance.
4) Fixato un bug dove, nel Bug Reporting, non si disattivaa il pulsante di invio, se tutti i campi erano compilati.
5) Aggiunto il supprto all'allegare i file di localStorage e config.ini al Bug Report, per investigare meglio i problemi legati all'ambiente dell'utente.
## 1.5.3 (2026-02-10)
1) Sistemato un bug dove, al primo avvio, il tema chiaro era applicato insieme all'opzioni del tema scuro sul contenuto mail, causando un contrasto eccessivo.
## 1.5.2 (2026-02-10)
1) Supporto tema chiaro/scuro.
2) Internazionalizzazione completa (Italiano/Inglese).
3) Opzioni di accessibilità (riduzione animazioni, contrasto).
## 1.5.1 (2026-02-09)
1) Sistemato un bug del primo avvio, con mismatch della lingua.
2) Aggiunto il supporto all'installazione sotto AppData/Local
## 1.5.0 (2026-02-08)
1) Sistema di aggiornamento automatico self-hosted (ancora non attivo di default).
2) Sistema di bug report integrato.
## 1.4.1 (2026-02-06)
1) Export/Import impostazioni.
2) Aggiornamento configurazione installer.

View File

@@ -5,7 +5,7 @@
# Existing Features
- [ ] Add seperated "Updater" binary, that will start on User login (via Scheduled Task), with a silent install mode.
- [ ] Attach localStorage, config file to the "Bug Reporter" ZIP file, to investigate the issue with the user enviroment.
- [x] Attach localStorage, config file to the "Bug Reporter" ZIP file, to investigate the issue with the user enviroment.
- [ ] Auto-send the "Bug Reporter" ZIP file to the support team, to investigate the issue with the user enviroment.
# Bugs

View File

@@ -38,6 +38,10 @@ type BugReportInput struct {
Description string `json:"description"`
// ScreenshotData is the base64-encoded PNG screenshot (captured before dialog opens)
ScreenshotData string `json:"screenshotData"`
// LocalStorageData is the JSON-encoded localStorage data
LocalStorageData string `json:"localStorageData"`
// ConfigData is the JSON-encoded config.ini data
ConfigData string `json:"configData"`
}
// SubmitBugReportResult contains the result of submitting a bug report.
@@ -120,10 +124,12 @@ func (a *App) CreateBugReportFolder() (*BugReportResult, error) {
// - User-provided description (report.txt)
// - Screenshot (captured before dialog opens)
// - Currently loaded mail file (if any)
// - localStorage data (localStorage.json)
// - Config.ini data (config.json)
// - System information (hostname, OS version, hardware ID)
//
// Parameters:
// - input: User-provided bug report details including pre-captured screenshot
// - input: User-provided bug report details including pre-captured screenshot, localStorage, and config data
//
// Returns:
// - *SubmitBugReportResult: Paths to the zip file and folder
@@ -168,6 +174,22 @@ func (a *App) SubmitBugReport(input BugReportInput) (*SubmitBugReportResult, err
}
}
// Save localStorage data if provided
if input.LocalStorageData != "" {
localStoragePath := filepath.Join(bugReportFolder, "localStorage.json")
if err := os.WriteFile(localStoragePath, []byte(input.LocalStorageData), 0644); err != nil {
Log("Failed to save localStorage data:", err)
}
}
// Save config data if provided
if input.ConfigData != "" {
configPath := filepath.Join(bugReportFolder, "config.json")
if err := os.WriteFile(configPath, []byte(input.ConfigData), 0644); err != nil {
Log("Failed to save config data:", err)
}
}
// Create the report.txt file with user's description
reportContent := fmt.Sprintf(`EMLy Bug Report
================

View File

@@ -0,0 +1,309 @@
<script lang="ts">
import { bugReportDialogOpen } from "$lib/stores/app";
import * as m from "$lib/paraglide/messages.js";
import * as Dialog from "$lib/components/ui/dialog/index.js";
import { Button, buttonVariants } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { Textarea } from "$lib/components/ui/textarea/index.js";
import { CheckCircle, Copy, FolderOpen, Camera, Loader2 } from "@lucide/svelte";
import { toast } from "svelte-sonner";
import { TakeScreenshot, SubmitBugReport, OpenFolderInExplorer, GetConfig } from "$lib/wailsjs/go/main/App";
import { browser } from "$app/environment";
// Bug report form state
let userName = $state("");
let userEmail = $state("");
let bugDescription = $state("");
// Bug report screenshot state
let screenshotData = $state("");
let isCapturing = $state(false);
// Bug report system data
let localStorageData = $state("");
let configData = $state("");
// Bug report UI state
let isSubmitting = $state(false);
let isSuccess = $state(false);
let resultZipPath = $state("");
let canSubmit: boolean = $derived(
bugDescription.trim().length > 0 && userName.trim().length > 0 && userEmail.trim().length > 0 && !isSubmitting && !isCapturing
);
// Bug report dialog effects
$effect(() => {
if ($bugReportDialogOpen) {
// Capture screenshot immediately when dialog opens
captureScreenshot();
// Capture localStorage data
captureLocalStorage();
// Capture config.ini data
captureConfig();
} else {
// Reset form when dialog closes
resetBugReportForm();
}
});
async function captureScreenshot() {
isCapturing = true;
try {
const result = await TakeScreenshot();
screenshotData = result.data;
console.log("Screenshot captured:", result.width, "x", result.height);
} catch (err) {
console.error("Failed to capture screenshot:", err);
} finally {
isCapturing = false;
}
}
function captureLocalStorage() {
if (!browser) return;
try {
const data: Record<string, string> = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
data[key] = localStorage.getItem(key) || "";
}
}
localStorageData = JSON.stringify(data, null, 2);
console.log("localStorage data captured");
} catch (err) {
console.error("Failed to capture localStorage:", err);
localStorageData = "Error capturing localStorage";
}
}
async function captureConfig() {
try {
const config = await GetConfig();
configData = JSON.stringify(config, null, 2);
console.log("Config data captured");
} catch (err) {
console.error("Failed to capture config:", err);
configData = "Error capturing config";
}
}
function resetBugReportForm() {
userName = "";
userEmail = "";
bugDescription = "";
screenshotData = "";
localStorageData = "";
configData = "";
isCapturing = false;
isSubmitting = false;
isSuccess = false;
resultZipPath = "";
}
async function handleBugReportSubmit(event: Event) {
event.preventDefault();
if (!bugDescription.trim()) {
toast.error("Please provide a bug description.");
return;
}
isSubmitting = true;
try {
const result = await SubmitBugReport({
name: userName,
email: userEmail,
description: bugDescription,
screenshotData: screenshotData,
localStorageData: localStorageData,
configData: configData
});
resultZipPath = result.zipPath;
isSuccess = true;
console.log("Bug report created:", result.zipPath);
} catch (err) {
console.error("Failed to create bug report:", err);
toast.error(m.bugreport_error());
} finally {
isSubmitting = false;
}
}
async function copyBugReportPath() {
try {
await navigator.clipboard.writeText(resultZipPath);
toast.success(m.bugreport_copied());
} catch (err) {
console.error("Failed to copy path:", err);
}
}
async function openBugReportFolder() {
try {
const folderPath = resultZipPath.replace(/\.zip$/, "");
await OpenFolderInExplorer(folderPath);
} catch (err) {
console.error("Failed to open folder:", err);
}
}
function closeBugReportDialog() {
$bugReportDialogOpen = false;
}
</script>
<Dialog.Root bind:open={$bugReportDialogOpen}>
<Dialog.Content class="sm:max-w-125 w-full max-h-[80vh] overflow-y-auto custom-scrollbar">
{#if isSuccess}
<!-- Success State -->
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<CheckCircle class="h-5 w-5 text-green-500" />
{m.bugreport_success_title()}
</Dialog.Title>
<Dialog.Description>
{m.bugreport_success_message()}
</Dialog.Description>
</Dialog.Header>
<div class="grid gap-4 py-4">
<div class="bg-muted rounded-md p-3">
<code class="text-xs break-all select-all">{resultZipPath}</code>
</div>
<div class="flex gap-2">
<Button variant="outline" class="flex-1" onclick={copyBugReportPath}>
<Copy class="h-4 w-4 mr-2" />
{m.bugreport_copy_path()}
</Button>
<Button variant="outline" class="flex-1" onclick={openBugReportFolder}>
<FolderOpen class="h-4 w-4 mr-2" />
{m.bugreport_open_folder()}
</Button>
</div>
</div>
<Dialog.Footer>
<Button onclick={closeBugReportDialog}>
{m.bugreport_close()}
</Button>
</Dialog.Footer>
{:else}
<!-- Form State -->
<form onsubmit={handleBugReportSubmit}>
<Dialog.Header>
<Dialog.Title>{m.bugreport_title()}</Dialog.Title>
<Dialog.Description>
{m.bugreport_description()}
</Dialog.Description>
</Dialog.Header>
<div class="grid gap-4 py-4">
<div class="grid gap-2">
<Label for="bug-name">{m.bugreport_name_label()}</Label>
<Input
id="bug-name"
placeholder={m.bugreport_name_placeholder()}
bind:value={userName}
disabled={isSubmitting}
/>
</div>
<div class="grid gap-2">
<Label for="bug-email">{m.bugreport_email_label()}</Label>
<Input
id="bug-email"
type="email"
placeholder={m.bugreport_email_placeholder()}
bind:value={userEmail}
disabled={isSubmitting}
/>
</div>
<div class="grid gap-2">
<Label for="bug-description">{m.bugreport_text_label()}</Label>
<Textarea
id="bug-description"
placeholder={m.bugreport_text_placeholder()}
bind:value={bugDescription}
disabled={isSubmitting}
class="min-h-30"
/>
</div>
<!-- Screenshot Preview -->
<div class="grid gap-2">
<Label class="flex items-center gap-2">
<Camera class="h-4 w-4" />
{m.bugreport_screenshot_label()}
</Label>
{#if isCapturing}
<div class="flex items-center gap-2 text-muted-foreground text-sm">
<Loader2 class="h-4 w-4 animate-spin" />
Capturing...
</div>
{:else if screenshotData}
<div class="border rounded-md overflow-hidden">
<img
src="data:image/png;base64,{screenshotData}"
alt="Screenshot preview"
class="w-full h-32 object-cover object-top opacity-80 hover:opacity-100 transition-opacity cursor-pointer"
/>
</div>
{:else}
<div class="text-muted-foreground text-sm">
No screenshot available
</div>
{/if}
</div>
<p class="text-muted-foreground text-sm">
{m.bugreport_info()}
</p>
</div>
<Dialog.Footer>
<button type="button" class={buttonVariants({ variant: "outline" })} disabled={isSubmitting} onclick={closeBugReportDialog}>
{m.bugreport_cancel()}
</button>
<Button type="submit" disabled={!canSubmit}>
{#if isSubmitting}
<Loader2 class="h-4 w-4 mr-2 animate-spin" />
{m.bugreport_submitting()}
{:else}
{m.bugreport_submit()}
{/if}
</Button>
</Dialog.Footer>
</form>
{/if}
</Dialog.Content>
</Dialog.Root>
<style>
:global(.custom-scrollbar::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
:global(.custom-scrollbar::-webkit-scrollbar-track) {
background: transparent;
}
:global(.custom-scrollbar::-webkit-scrollbar-thumb) {
background: var(--border);
border-radius: 6px;
}
:global(.custom-scrollbar::-webkit-scrollbar-thumb:hover) {
background: var(--muted-foreground);
}
:global(.custom-scrollbar::-webkit-scrollbar-corner) {
background: transparent;
}
</style>

View File

@@ -17,22 +17,14 @@
House,
Settings,
Bug,
Loader2,
Copy,
FolderOpen,
CheckCircle,
Camera,
Heart,
Info,
Music
} from "@lucide/svelte";
import { Separator } from "$lib/components/ui/separator/index.js";
import { toast } from "svelte-sonner";
import { Button, buttonVariants } from "$lib/components/ui/button/index.js";
import * as Dialog from "$lib/components/ui/dialog/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import { Label } from "$lib/components/ui/label/index.js";
import { Textarea } from "$lib/components/ui/textarea/index.js";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import BugReportDialog from "$lib/components/BugReportDialog.svelte";
import {
WindowMinimise,
@@ -44,7 +36,7 @@
EventsOff,
} from "$lib/wailsjs/runtime/runtime";
import { RefreshCcwDot } from "@lucide/svelte";
import { IsDebuggerRunning, QuitApp, TakeScreenshot, SubmitBugReport, OpenFolderInExplorer } from "$lib/wailsjs/go/main/App";
import { IsDebuggerRunning, QuitApp } from "$lib/wailsjs/go/main/App";
import { settingsStore } from "$lib/stores/settings.svelte.js";
let versionInfo: utils.Config | null = $state(null);
@@ -52,20 +44,6 @@
let isDebugerOn: boolean = $state(false);
let isDebbugerProtectionOn: boolean = $state(true);
// Bug report form state
let userName = $state("");
let userEmail = $state("");
let bugDescription = $state("");
// Bug report screenshot state
let screenshotData = $state("");
let isCapturing = $state(false);
// Bug report UI state
let isSubmitting = $state(false);
let isSuccess = $state(false);
let resultZipPath = $state("");
async function syncMaxState() {
isMaximized = await WindowIsMaximised();
}
@@ -155,17 +133,6 @@
applyTheme(stored === "light" ? "light" : "dark");
});
// Bug report dialog effects
$effect(() => {
if ($bugReportDialogOpen) {
// Capture screenshot immediately when dialog opens
captureScreenshot();
} else {
// Reset form when dialog closes
resetBugReportForm();
}
});
// Listen for automatic update notifications
$effect(() => {
if (!browser) return;
@@ -186,81 +153,6 @@
};
});
async function captureScreenshot() {
isCapturing = true;
try {
const result = await TakeScreenshot();
screenshotData = result.data;
console.log("Screenshot captured:", result.width, "x", result.height);
} catch (err) {
console.error("Failed to capture screenshot:", err);
} finally {
isCapturing = false;
}
}
function resetBugReportForm() {
userName = "";
userEmail = "";
bugDescription = "";
screenshotData = "";
isCapturing = false;
isSubmitting = false;
isSuccess = false;
resultZipPath = "";
}
async function handleBugReportSubmit(event: Event) {
event.preventDefault();
if (!bugDescription.trim()) {
toast.error("Please provide a bug description.");
return;
}
isSubmitting = true;
try {
const result = await SubmitBugReport({
name: userName,
email: userEmail,
description: bugDescription,
screenshotData: screenshotData
});
resultZipPath = result.zipPath;
isSuccess = true;
console.log("Bug report created:", result.zipPath);
} catch (err) {
console.error("Failed to create bug report:", err);
toast.error(m.bugreport_error());
} finally {
isSubmitting = false;
}
}
async function copyBugReportPath() {
try {
await navigator.clipboard.writeText(resultZipPath);
toast.success(m.bugreport_copied());
} catch (err) {
console.error("Failed to copy path:", err);
}
}
async function openBugReportFolder() {
try {
const folderPath = resultZipPath.replace(/\.zip$/, "");
await OpenFolderInExplorer(folderPath);
} catch (err) {
console.error("Failed to open folder:", err);
}
}
function closeBugReportDialog() {
$bugReportDialogOpen = false;
}
syncMaxState();
</script>
@@ -435,134 +327,7 @@
{/each}
</div>
<!-- Bug Report Dialog -->
<Dialog.Root bind:open={$bugReportDialogOpen}>
<Dialog.Content class="sm:max-w-[500px] w-full max-h-[80vh] overflow-y-auto custom-scrollbar">
{#if isSuccess}
<!-- Success State -->
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<CheckCircle class="h-5 w-5 text-green-500" />
{m.bugreport_success_title()}
</Dialog.Title>
<Dialog.Description>
{m.bugreport_success_message()}
</Dialog.Description>
</Dialog.Header>
<div class="grid gap-4 py-4">
<div class="bg-muted rounded-md p-3">
<code class="text-xs break-all select-all">{resultZipPath}</code>
</div>
<div class="flex gap-2">
<Button variant="outline" class="flex-1" onclick={copyBugReportPath}>
<Copy class="h-4 w-4 mr-2" />
{m.bugreport_copy_path()}
</Button>
<Button variant="outline" class="flex-1" onclick={openBugReportFolder}>
<FolderOpen class="h-4 w-4 mr-2" />
{m.bugreport_open_folder()}
</Button>
</div>
</div>
<Dialog.Footer>
<Button onclick={closeBugReportDialog}>
{m.bugreport_close()}
</Button>
</Dialog.Footer>
{:else}
<!-- Form State -->
<form onsubmit={handleBugReportSubmit}>
<Dialog.Header>
<Dialog.Title>{m.bugreport_title()}</Dialog.Title>
<Dialog.Description>
{m.bugreport_description()}
</Dialog.Description>
</Dialog.Header>
<div class="grid gap-4 py-4">
<div class="grid gap-2">
<Label for="bug-name">{m.bugreport_name_label()}</Label>
<Input
id="bug-name"
placeholder={m.bugreport_name_placeholder()}
bind:value={userName}
disabled={isSubmitting}
/>
</div>
<div class="grid gap-2">
<Label for="bug-email">{m.bugreport_email_label()}</Label>
<Input
id="bug-email"
type="email"
placeholder={m.bugreport_email_placeholder()}
bind:value={userEmail}
disabled={isSubmitting}
/>
</div>
<div class="grid gap-2">
<Label for="bug-description">{m.bugreport_text_label()}</Label>
<Textarea
id="bug-description"
placeholder={m.bugreport_text_placeholder()}
bind:value={bugDescription}
disabled={isSubmitting}
class="min-h-[120px]"
/>
</div>
<!-- Screenshot Preview -->
<div class="grid gap-2">
<Label class="flex items-center gap-2">
<Camera class="h-4 w-4" />
{m.bugreport_screenshot_label()}
</Label>
{#if isCapturing}
<div class="flex items-center gap-2 text-muted-foreground text-sm">
<Loader2 class="h-4 w-4 animate-spin" />
Capturing...
</div>
{:else if screenshotData}
<div class="border rounded-md overflow-hidden">
<img
src="data:image/png;base64,{screenshotData}"
alt="Screenshot preview"
class="w-full h-32 object-cover object-top opacity-80 hover:opacity-100 transition-opacity cursor-pointer"
/>
</div>
{:else}
<div class="text-muted-foreground text-sm">
No screenshot available
</div>
{/if}
</div>
<p class="text-muted-foreground text-sm">
{m.bugreport_info()}
</p>
</div>
<Dialog.Footer>
<button type="button" class={buttonVariants({ variant: "outline" })} disabled={isSubmitting} onclick={closeBugReportDialog}>
{m.bugreport_cancel()}
</button>
<Button type="submit" disabled={isSubmitting || isCapturing}>
{#if isSubmitting}
<Loader2 class="h-4 w-4 mr-2 animate-spin" />
{m.bugreport_submitting()}
{:else}
{m.bugreport_submit()}
{/if}
</Button>
</Dialog.Footer>
</form>
{/if}
</Dialog.Content>
</Dialog.Root>
<BugReportDialog />
</div>
<style>
@@ -808,26 +573,4 @@
transform: rotate(360deg);
}
}
:global(.custom-scrollbar::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
:global(.custom-scrollbar::-webkit-scrollbar-track) {
background: transparent;
}
:global(.custom-scrollbar::-webkit-scrollbar-thumb) {
background: var(--border);
border-radius: 6px;
}
:global(.custom-scrollbar::-webkit-scrollbar-thumb:hover) {
background: var(--muted-foreground);
}
:global(.custom-scrollbar::-webkit-scrollbar-corner) {
background: transparent;
}
</style>

View File

@@ -1,25 +1,18 @@
import type { PageLoad } from './$types';
import { GetMachineData, GetConfig } from "$lib/wailsjs/go/main/App";
import { GetConfig } from "$lib/wailsjs/go/main/App";
import { browser } from '$app/environment';
import { dangerZoneEnabled } from "$lib/stores/app";
import { get } from "svelte/store";
export const load = (async () => {
if (!browser) return { machineData: null, config: null };
if (!browser) return { config: null };
try {
const [machineData, configRoot] = await Promise.all([
get(dangerZoneEnabled) ? GetMachineData() : Promise.resolve(null),
GetConfig()
]);
const configRoot = await GetConfig();
return {
machineData,
config: configRoot.EMLy
};
} catch (e) {
console.error("Failed to load settings data", e);
return {
machineData: null,
config: null
};
}