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:
368
app.go
368
app.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
@@ -24,14 +25,15 @@ import (
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
StartupFilePath string
|
||||
openImagesMux sync.Mutex
|
||||
openImages map[string]bool
|
||||
openPDFsMux sync.Mutex
|
||||
openPDFs map[string]bool
|
||||
openEMLsMux sync.Mutex
|
||||
openEMLs map[string]bool
|
||||
ctx context.Context
|
||||
StartupFilePath string
|
||||
CurrentMailFilePath string // Tracks the currently loaded mail file (from startup or file dialog)
|
||||
openImagesMux sync.Mutex
|
||||
openImages map[string]bool
|
||||
openPDFsMux sync.Mutex
|
||||
openPDFs map[string]bool
|
||||
openEMLsMux sync.Mutex
|
||||
openEMLs map[string]bool
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
@@ -48,6 +50,11 @@ func NewApp() *App {
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
|
||||
// Set CurrentMailFilePath to StartupFilePath if a file was opened via command line
|
||||
if a.StartupFilePath != "" {
|
||||
a.CurrentMailFilePath = a.StartupFilePath
|
||||
}
|
||||
|
||||
isViewer := false
|
||||
for _, arg := range os.Args {
|
||||
if strings.Contains(arg, "--view-image") || strings.Contains(arg, "--view-pdf") {
|
||||
@@ -102,6 +109,16 @@ func (a *App) GetStartupFile() string {
|
||||
return a.StartupFilePath
|
||||
}
|
||||
|
||||
// SetCurrentMailFilePath sets the path of the currently loaded mail file
|
||||
func (a *App) SetCurrentMailFilePath(filePath string) {
|
||||
a.CurrentMailFilePath = filePath
|
||||
}
|
||||
|
||||
// GetCurrentMailFilePath returns the path of the currently loaded mail file
|
||||
func (a *App) GetCurrentMailFilePath() string {
|
||||
return a.CurrentMailFilePath
|
||||
}
|
||||
|
||||
// ReadEML reads a .eml file and returns the email data
|
||||
func (a *App) ReadEML(filePath string) (*internal.EmailData, error) {
|
||||
return internal.ReadEmlFile(filePath)
|
||||
@@ -534,3 +551,338 @@ func (a *App) ConvertToUTF8(s string) string {
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
// ScreenshotResult contains the screenshot data and metadata
|
||||
type ScreenshotResult struct {
|
||||
Data string `json:"data"` // Base64-encoded PNG data
|
||||
Width int `json:"width"` // Image width in pixels
|
||||
Height int `json:"height"` // Image height in pixels
|
||||
Filename string `json:"filename"` // Suggested filename
|
||||
}
|
||||
|
||||
// TakeScreenshot captures the current Wails application window and returns it as base64 PNG
|
||||
func (a *App) TakeScreenshot() (*ScreenshotResult, error) {
|
||||
// Get the window title to find our window
|
||||
windowTitle := "EMLy - EML Viewer for 3gIT"
|
||||
|
||||
// Check if we're in viewer mode
|
||||
for _, arg := range os.Args {
|
||||
if strings.Contains(arg, "--view-image") {
|
||||
windowTitle = "EMLy Image Viewer"
|
||||
break
|
||||
}
|
||||
if strings.Contains(arg, "--view-pdf") {
|
||||
windowTitle = "EMLy PDF Viewer"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
img, err := utils.CaptureWindowByTitle(windowTitle)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to capture window: %w", err)
|
||||
}
|
||||
|
||||
base64Data, err := utils.ScreenshotToBase64PNG(img)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode screenshot: %w", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
|
||||
return &ScreenshotResult{
|
||||
Data: base64Data,
|
||||
Width: bounds.Dx(),
|
||||
Height: bounds.Dy(),
|
||||
Filename: fmt.Sprintf("emly_screenshot_%s.png", timestamp),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SaveScreenshot captures and saves the screenshot to a file, returning the file path
|
||||
func (a *App) SaveScreenshot() (string, error) {
|
||||
result, err := a.TakeScreenshot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Decode base64 data
|
||||
data, err := base64.StdEncoding.DecodeString(result.Data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode screenshot data: %w", err)
|
||||
}
|
||||
|
||||
// Save to temp directory
|
||||
tempDir := os.TempDir()
|
||||
filePath := filepath.Join(tempDir, result.Filename)
|
||||
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to save screenshot: %w", err)
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// SaveScreenshotAs opens a save dialog and saves the screenshot to the selected location
|
||||
func (a *App) SaveScreenshotAs() (string, error) {
|
||||
result, err := a.TakeScreenshot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Open save dialog
|
||||
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
DefaultFilename: result.Filename,
|
||||
Title: "Save Screenshot",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "PNG Images (*.png)",
|
||||
Pattern: "*.png",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open save dialog: %w", err)
|
||||
}
|
||||
|
||||
if savePath == "" {
|
||||
return "", nil // User cancelled
|
||||
}
|
||||
|
||||
// Decode base64 data
|
||||
data, err := base64.StdEncoding.DecodeString(result.Data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode screenshot data: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(savePath, data, 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to save screenshot: %w", err)
|
||||
}
|
||||
|
||||
return savePath, nil
|
||||
}
|
||||
|
||||
// BugReportResult contains paths to the bug report files
|
||||
type BugReportResult struct {
|
||||
FolderPath string `json:"folderPath"` // Path to the bug report folder
|
||||
ScreenshotPath string `json:"screenshotPath"` // Path to the screenshot file
|
||||
MailFilePath string `json:"mailFilePath"` // Path to the copied mail file (empty if no mail)
|
||||
}
|
||||
|
||||
// CreateBugReportFolder creates a folder in temp with screenshot and optionally the current mail file
|
||||
func (a *App) CreateBugReportFolder() (*BugReportResult, error) {
|
||||
// Create timestamp for unique folder name
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
folderName := fmt.Sprintf("emly_bugreport_%s", timestamp)
|
||||
|
||||
// Create folder in temp directory
|
||||
tempDir := os.TempDir()
|
||||
bugReportFolder := filepath.Join(tempDir, folderName)
|
||||
|
||||
if err := os.MkdirAll(bugReportFolder, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create bug report folder: %w", err)
|
||||
}
|
||||
|
||||
result := &BugReportResult{
|
||||
FolderPath: bugReportFolder,
|
||||
}
|
||||
|
||||
// Take and save screenshot
|
||||
screenshotResult, err := a.TakeScreenshot()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to take screenshot: %w", err)
|
||||
}
|
||||
|
||||
screenshotData, err := base64.StdEncoding.DecodeString(screenshotResult.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode screenshot: %w", err)
|
||||
}
|
||||
|
||||
screenshotPath := filepath.Join(bugReportFolder, screenshotResult.Filename)
|
||||
if err := os.WriteFile(screenshotPath, screenshotData, 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to save screenshot: %w", err)
|
||||
}
|
||||
result.ScreenshotPath = screenshotPath
|
||||
|
||||
// Check if there's a mail file loaded and copy it
|
||||
if a.CurrentMailFilePath != "" {
|
||||
// Read the original mail file
|
||||
mailData, err := os.ReadFile(a.CurrentMailFilePath)
|
||||
if err != nil {
|
||||
// Log but don't fail - screenshot is still valid
|
||||
Log("Failed to read mail file for bug report:", err)
|
||||
} else {
|
||||
// Get the original filename
|
||||
mailFilename := filepath.Base(a.CurrentMailFilePath)
|
||||
mailFilePath := filepath.Join(bugReportFolder, mailFilename)
|
||||
|
||||
if err := os.WriteFile(mailFilePath, mailData, 0644); err != nil {
|
||||
Log("Failed to copy mail file for bug report:", err)
|
||||
} else {
|
||||
result.MailFilePath = mailFilePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// BugReportInput contains the user-provided bug report details
|
||||
type BugReportInput struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Description string `json:"description"`
|
||||
ScreenshotData string `json:"screenshotData"` // Base64-encoded PNG screenshot
|
||||
}
|
||||
|
||||
// SubmitBugReportResult contains the result of submitting a bug report
|
||||
type SubmitBugReportResult struct {
|
||||
ZipPath string `json:"zipPath"` // Path to the created zip file
|
||||
FolderPath string `json:"folderPath"` // Path to the bug report folder
|
||||
}
|
||||
|
||||
// SubmitBugReport creates a complete bug report with user input, saves it, and zips the folder
|
||||
func (a *App) SubmitBugReport(input BugReportInput) (*SubmitBugReportResult, error) {
|
||||
// Create timestamp for unique folder name
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
folderName := fmt.Sprintf("emly_bugreport_%s", timestamp)
|
||||
|
||||
// Create folder in temp directory
|
||||
tempDir := os.TempDir()
|
||||
bugReportFolder := filepath.Join(tempDir, folderName)
|
||||
|
||||
if err := os.MkdirAll(bugReportFolder, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create bug report folder: %w", err)
|
||||
}
|
||||
|
||||
// Save the pre-captured screenshot
|
||||
if input.ScreenshotData != "" {
|
||||
screenshotData, err := base64.StdEncoding.DecodeString(input.ScreenshotData)
|
||||
if err != nil {
|
||||
Log("Failed to decode screenshot:", err)
|
||||
} else {
|
||||
screenshotPath := filepath.Join(bugReportFolder, fmt.Sprintf("emly_screenshot_%s.png", timestamp))
|
||||
if err := os.WriteFile(screenshotPath, screenshotData, 0644); err != nil {
|
||||
Log("Failed to save screenshot:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the mail file if one is loaded
|
||||
if a.CurrentMailFilePath != "" {
|
||||
mailData, err := os.ReadFile(a.CurrentMailFilePath)
|
||||
if err != nil {
|
||||
Log("Failed to read mail file for bug report:", err)
|
||||
} else {
|
||||
mailFilename := filepath.Base(a.CurrentMailFilePath)
|
||||
mailFilePath := filepath.Join(bugReportFolder, mailFilename)
|
||||
if err := os.WriteFile(mailFilePath, mailData, 0644); err != nil {
|
||||
Log("Failed to copy mail file for bug report:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the report.txt file with user's description
|
||||
reportContent := fmt.Sprintf(`EMLy Bug Report
|
||||
================
|
||||
|
||||
Name: %s
|
||||
Email: %s
|
||||
|
||||
Description:
|
||||
%s
|
||||
|
||||
Generated: %s
|
||||
`, input.Name, input.Email, input.Description, time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
reportPath := filepath.Join(bugReportFolder, "report.txt")
|
||||
if err := os.WriteFile(reportPath, []byte(reportContent), 0644); err != nil {
|
||||
return nil, fmt.Errorf("failed to save report file: %w", err)
|
||||
}
|
||||
|
||||
// Get machine info and save it
|
||||
machineInfo, err := utils.GetMachineInfo()
|
||||
if err == nil && machineInfo != nil {
|
||||
sysInfoContent := fmt.Sprintf(`System Information
|
||||
==================
|
||||
|
||||
Hostname: %s
|
||||
OS: %s
|
||||
Version: %s
|
||||
Hardware ID: %s
|
||||
External IP: %s
|
||||
`, machineInfo.Hostname, machineInfo.OS, machineInfo.Version, machineInfo.HWID, machineInfo.ExternalIP)
|
||||
|
||||
sysInfoPath := filepath.Join(bugReportFolder, "system_info.txt")
|
||||
if err := os.WriteFile(sysInfoPath, []byte(sysInfoContent), 0644); err != nil {
|
||||
Log("Failed to save system info:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Zip the folder
|
||||
zipPath := bugReportFolder + ".zip"
|
||||
if err := zipFolder(bugReportFolder, zipPath); err != nil {
|
||||
return nil, fmt.Errorf("failed to create zip file: %w", err)
|
||||
}
|
||||
|
||||
return &SubmitBugReportResult{
|
||||
ZipPath: zipPath,
|
||||
FolderPath: bugReportFolder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// zipFolder creates a zip archive of the given folder
|
||||
func zipFolder(sourceFolder, destZip string) error {
|
||||
zipFile, err := os.Create(destZip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Walk through the folder and add all files to the zip
|
||||
return filepath.Walk(sourceFolder, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip the root folder itself
|
||||
if path == sourceFolder {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get relative path for the zip entry
|
||||
relPath, err := filepath.Rel(sourceFolder, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip directories (they'll be created implicitly)
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create the file in the zip
|
||||
writer, err := zipWriter.Create(relPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
fileContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write to zip
|
||||
_, err = writer.Write(fileContent)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// OpenFolderInExplorer opens the specified folder in Windows Explorer
|
||||
func (a *App) OpenFolderInExplorer(folderPath string) error {
|
||||
cmd := exec.Command("explorer", folderPath)
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user