feat: Implement bug reporting feature with screenshot capture

- Added a new utility for capturing screenshots on Windows.
- Introduced a bug report dialog in the frontend with fields for name, email, and bug description.
- Integrated screenshot capture functionality into the bug report dialog.
- Added localization for bug report messages in English and Italian.
- Updated package dependencies to include html2canvas for potential future use.
- Created a new UI dialog component structure for better organization and reusability.
- Enhanced the app layout to accommodate the new bug report feature.
This commit is contained in:
Flavio Fois
2026-02-05 21:38:51 +01:00
parent d9e848d3f4
commit 6a44eba7ca
22 changed files with 1176 additions and 39 deletions

View File

@@ -3,7 +3,7 @@
import { page, navigating } from "$app/state";
import { beforeNavigate, goto } from "$app/navigation";
import { locales, localizeHref } from "$lib/paraglide/runtime";
import { unsavedChanges, sidebarOpen } from "$lib/stores/app";
import { unsavedChanges, sidebarOpen, bugReportDialogOpen } from "$lib/stores/app";
import "../layout.css";
import { onMount } from "svelte";
import * as m from "$lib/paraglide/messages.js";
@@ -17,10 +17,20 @@
PanelRightOpen,
House,
Settings,
Bug,
Loader2,
Copy,
FolderOpen,
CheckCircle,
Camera,
} from "@lucide/svelte";
import { Separator } from "$lib/components/ui/separator/index.js";
import { toast } from "svelte-sonner";
import { buttonVariants } from "$lib/components/ui/button/index.js";
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 {
WindowMinimise,
@@ -30,7 +40,7 @@
Quit,
} from "$lib/wailsjs/runtime/runtime";
import { RefreshCcwDot } from "@lucide/svelte";
import { IsDebuggerRunning, QuitApp } from "$lib/wailsjs/go/main/App";
import { IsDebuggerRunning, QuitApp, TakeScreenshot, SubmitBugReport, OpenFolderInExplorer } from "$lib/wailsjs/go/main/App";
import { settingsStore } from "$lib/stores/settings.svelte.js";
let versionInfo: utils.Config | null = $state(null);
@@ -38,6 +48,20 @@
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();
}
@@ -126,6 +150,92 @@
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();
}
});
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>
@@ -246,6 +356,16 @@
class="hover:opacity-100 transition-opacity"
/>
<Separator orientation="vertical" />
<Bug
size="16"
onclick={() => {
$bugReportDialogOpen = !$bugReportDialogOpen;
}}
style="cursor: pointer; opacity: 0.7;"
class="hover:opacity-100 transition-opacity"
/>
<a
data-sveltekit-reload
href="/"
@@ -256,6 +376,7 @@
>
<RefreshCcwDot />
</a>
</div>
<div style="display:none">
@@ -265,6 +386,135 @@
</a>
{/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>
</div>
<style>
@@ -498,4 +748,26 @@
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: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
:global(.custom-scrollbar::-webkit-scrollbar-thumb:hover) {
background: rgba(255, 255, 255, 0.2);
}
:global(.custom-scrollbar::-webkit-scrollbar-corner) {
background: transparent;
}
</style>