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:
@@ -89,5 +89,25 @@
|
||||
"mail_pdf_already_open": "The PDF is already open in another window.",
|
||||
"settings_danger_debugger_protection_label": "Enable attached debugger protection",
|
||||
"settings_danger_debugger_protection_hint": "This will prevent the app from being debugged by an attached debugger.",
|
||||
"settings_danger_debugger_protection_info": "Info: This actions are currently not configurable and is always enabled for private builds."
|
||||
"settings_danger_debugger_protection_info": "Info: This actions are currently not configurable and is always enabled for private builds.",
|
||||
"bugreport_title": "Report a Bug",
|
||||
"bugreport_description": "Describe what you were doing when the bug occurred and what you expected to happen instead.",
|
||||
"bugreport_name_label": "Name",
|
||||
"bugreport_name_placeholder": "Your name",
|
||||
"bugreport_email_label": "Email",
|
||||
"bugreport_email_placeholder": "your.email@example.com",
|
||||
"bugreport_text_label": "Bug Description",
|
||||
"bugreport_text_placeholder": "Describe the bug in detail...",
|
||||
"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",
|
||||
"bugreport_submit": "Submit Report",
|
||||
"bugreport_submitting": "Creating report...",
|
||||
"bugreport_success_title": "Bug Report Created",
|
||||
"bugreport_success_message": "Your bug report has been saved to:",
|
||||
"bugreport_copy_path": "Copy Path",
|
||||
"bugreport_open_folder": "Open Folder",
|
||||
"bugreport_close": "Close",
|
||||
"bugreport_error": "Failed to create bug report.",
|
||||
"bugreport_copied": "Path copied to clipboard!"
|
||||
}
|
||||
|
||||
@@ -89,5 +89,25 @@
|
||||
"mail_pdf_already_open": "Il file PDF è già aperto in una finestra separata.",
|
||||
"settings_danger_debugger_protection_label": "Abilita protezione da debugger",
|
||||
"settings_danger_debugger_protection_hint": "Questo impedirà che il debug dell'app venga eseguito da un debugger collegato.",
|
||||
"settings_danger_debugger_protection_info": "Info: Questa azione non è attualmente configurabile ed è sempre abilitata per le build private."
|
||||
"settings_danger_debugger_protection_info": "Info: Questa azione non è attualmente configurabile ed è sempre abilitata per le build private.",
|
||||
"bugreport_title": "Segnala un Bug",
|
||||
"bugreport_description": "Descrivi cosa stavi facendo quando si è verificato il bug e cosa ti aspettavi che accadesse.",
|
||||
"bugreport_name_label": "Nome",
|
||||
"bugreport_name_placeholder": "Il tuo nome",
|
||||
"bugreport_email_label": "Email",
|
||||
"bugreport_email_placeholder": "tua.email@esempio.com",
|
||||
"bugreport_text_label": "Descrizione del Bug",
|
||||
"bugreport_text_placeholder": "Descrivi il bug in dettaglio...",
|
||||
"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",
|
||||
"bugreport_submit": "Invia Segnalazione",
|
||||
"bugreport_submitting": "Creazione segnalazione...",
|
||||
"bugreport_success_title": "Segnalazione Bug Creata",
|
||||
"bugreport_success_message": "La tua segnalazione bug è stata salvata in:",
|
||||
"bugreport_copy_path": "Copia Percorso",
|
||||
"bugreport_open_folder": "Apri Cartella",
|
||||
"bugreport_close": "Chiudi",
|
||||
"bugreport_error": "Impossibile creare la segnalazione bug.",
|
||||
"bugreport_copied": "Percorso copiato negli appunti!"
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
"vite-plugin-devtools-json": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/html2canvas": "^1.0.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"pdfjs-dist": "^5.4.624",
|
||||
"svelte-flags": "^3.0.1",
|
||||
"svelte-sonner": "^1.0.7"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { X, MailOpen, Image, FileText, File, ShieldCheck, Shield, Signature, FileCode, Loader2 } from "@lucide/svelte";
|
||||
import { ShowOpenFileDialog, ReadEML, OpenPDF, OpenImageWindow, OpenPDFWindow, OpenImage, ReadMSG, ReadPEC, OpenEMLWindow, ConvertToUTF8 } from "$lib/wailsjs/go/main/App";
|
||||
import { ShowOpenFileDialog, ReadEML, OpenPDF, OpenImageWindow, OpenPDFWindow, OpenImage, ReadMSG, ReadPEC, OpenEMLWindow, ConvertToUTF8, SetCurrentMailFilePath } from "$lib/wailsjs/go/main/App";
|
||||
import type { internal } from "$lib/wailsjs/go/models";
|
||||
import { sidebarOpen } from "$lib/stores/app";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
@@ -59,7 +59,7 @@
|
||||
}
|
||||
|
||||
if(dev) {
|
||||
console.log(mailState.currentEmail)
|
||||
console.debug("emailObj:", mailState.currentEmail)
|
||||
}
|
||||
console.info("Current email changed:", mailState.currentEmail?.subject);
|
||||
if(mailState.currentEmail !== null) {
|
||||
@@ -183,6 +183,8 @@
|
||||
} else {
|
||||
email = await ReadEML(result);
|
||||
}
|
||||
// Track the current mail file path for bug reports
|
||||
await SetCurrentMailFilePath(result);
|
||||
mailState.setParams(email);
|
||||
sidebarOpen.set(false);
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||
45
frontend/src/lib/components/ui/dialog/dialog-content.svelte
Normal file
45
frontend/src/lib/components/ui/dialog/dialog-content.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import DialogPortal from "./dialog-portal.svelte";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
||||
children: Snippet;
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DialogPortal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring absolute end-4 top-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
20
frontend/src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
20
frontend/src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
frontend/src/lib/components/ui/dialog/dialog-header.svelte
Normal file
20
frontend/src/lib/components/ui/dialog/dialog-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
frontend/src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
20
frontend/src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Portal {...restProps} />
|
||||
17
frontend/src/lib/components/ui/dialog/dialog-title.svelte
Normal file
17
frontend/src/lib/components/ui/dialog/dialog-title.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("text-lg leading-none font-semibold", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
||||
7
frontend/src/lib/components/ui/dialog/dialog.svelte
Normal file
7
frontend/src/lib/components/ui/dialog/dialog.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Root bind:open {...restProps} />
|
||||
34
frontend/src/lib/components/ui/dialog/index.ts
Normal file
34
frontend/src/lib/components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Root from "./dialog.svelte";
|
||||
import Portal from "./dialog-portal.svelte";
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Close from "./dialog-close.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
||||
7
frontend/src/lib/components/ui/textarea/index.ts
Normal file
7
frontend/src/lib/components/ui/textarea/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./textarea.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Textarea,
|
||||
};
|
||||
23
frontend/src/lib/components/ui/textarea/textarea.svelte
Normal file
23
frontend/src/lib/components/ui/textarea/textarea.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "textarea",
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{...restProps}
|
||||
></textarea>
|
||||
@@ -5,6 +5,7 @@ const storedDebug = browser ? sessionStorage.getItem("debugWindowInSettings") ==
|
||||
export const dangerZoneEnabled = writable<boolean>(storedDebug);
|
||||
export const unsavedChanges = writable<boolean>(false);
|
||||
export const sidebarOpen = writable<boolean>(true);
|
||||
export const bugReportDialogOpen = writable<boolean>(false);
|
||||
|
||||
export type AppEvent = {
|
||||
id: string;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<section class="center" aria-label="Overview">
|
||||
<section class="center" aria-label="Overview" id="main-content-app">
|
||||
<MailViewer />
|
||||
</section>
|
||||
</div>
|
||||
@@ -36,26 +36,4 @@
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user