Compare commits
11 Commits
fc98f0ed74
...
webview-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a343769e5 | ||
|
|
33cb171fb1 | ||
|
|
549eed065a | ||
|
|
547018a39f | ||
|
|
18c256ebf9 | ||
|
|
3eb95cca7f | ||
|
|
6f373dd9ab | ||
|
|
eac7a12cd4 | ||
|
|
86e33d6189 | ||
|
|
402a90cf4b | ||
|
|
b68c173d2a |
35
CHANGELOG.md
Normal file
35
CHANGELOG.md
Normal 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) 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.
|
||||||
12
TODO.md
Normal file
12
TODO.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# New Features
|
||||||
|
- [ ] Add an option to select the folder to save Attachments to, instead of always saving to the Downloads folder.
|
||||||
|
- [ ] Create a sorta of "Bug Reporter" loader, to load the .ZIP file with the Bug Report data, and replicate the same enviroment as the user, to investigate the issue. (EML file, settings)
|
||||||
|
- [x] Add a "Download" button to the MailViewer, PDF and Image viewer, to download the file instead of just opening it.
|
||||||
|
|
||||||
|
# Existing Features
|
||||||
|
- [ ] Add seperated "Updater" binary, that will start on User login (via Scheduled Task), with a silent install mode.
|
||||||
|
- [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
|
||||||
|
- [ ] Missing i18n for Toast notifications (to investigate)
|
||||||
@@ -38,6 +38,10 @@ type BugReportInput struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
// ScreenshotData is the base64-encoded PNG screenshot (captured before dialog opens)
|
// ScreenshotData is the base64-encoded PNG screenshot (captured before dialog opens)
|
||||||
ScreenshotData string `json:"screenshotData"`
|
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.
|
// SubmitBugReportResult contains the result of submitting a bug report.
|
||||||
@@ -120,10 +124,12 @@ func (a *App) CreateBugReportFolder() (*BugReportResult, error) {
|
|||||||
// - User-provided description (report.txt)
|
// - User-provided description (report.txt)
|
||||||
// - Screenshot (captured before dialog opens)
|
// - Screenshot (captured before dialog opens)
|
||||||
// - Currently loaded mail file (if any)
|
// - Currently loaded mail file (if any)
|
||||||
|
// - localStorage data (localStorage.json)
|
||||||
|
// - Config.ini data (config.json)
|
||||||
// - System information (hostname, OS version, hardware ID)
|
// - System information (hostname, OS version, hardware ID)
|
||||||
//
|
//
|
||||||
// Parameters:
|
// 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:
|
// Returns:
|
||||||
// - *SubmitBugReportResult: Paths to the zip file and folder
|
// - *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
|
// Create the report.txt file with user's description
|
||||||
reportContent := fmt.Sprintf(`EMLy Bug Report
|
reportContent := fmt.Sprintf(`EMLy Bug Report
|
||||||
================
|
================
|
||||||
|
|||||||
49
app_mail.go
49
app_mail.go
@@ -3,7 +3,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"emly/backend/utils/mail"
|
internal "emly/backend/utils/mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -86,3 +86,50 @@ func (a *App) ReadMSGOSS(filePath string) (*internal.EmailData, error) {
|
|||||||
func (a *App) ShowOpenFileDialog() (string, error) {
|
func (a *App) ShowOpenFileDialog() (string, error) {
|
||||||
return internal.ShowFileDialog(a.ctx)
|
return internal.ShowFileDialog(a.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) ShowOpenFolderDialog() (string, error) {
|
||||||
|
return internal.ShowFolderDialog(a.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAttachment saves an attachment to the configured download folder.
|
||||||
|
// Uses EXPORT_ATTACHMENT_FOLDER from config.ini if set,
|
||||||
|
// otherwise falls back to WEBVIEW2_DOWNLOAD_PATH, then to default Downloads folder.
|
||||||
|
// After saving, opens Windows Explorer to show the saved file.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filename: The name to save the file as
|
||||||
|
// - base64Data: The base64-encoded attachment data
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The full path where the file was saved
|
||||||
|
// - error: Any file system errors
|
||||||
|
func (a *App) SaveAttachment(filename string, base64Data string) (string, error) {
|
||||||
|
// Try to get configured export folder first
|
||||||
|
folderPath := a.GetExportAttachmentFolder()
|
||||||
|
|
||||||
|
// If not set, try to get WEBVIEW2_DOWNLOAD_PATH from config
|
||||||
|
if folderPath == "" {
|
||||||
|
config := a.GetConfig()
|
||||||
|
if config != nil && config.EMLy.WebView2DownloadPath != "" {
|
||||||
|
folderPath = config.EMLy.WebView2DownloadPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
savedPath, err := internal.SaveAttachmentToFolder(filename, base64Data, folderPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenExplorerForPath opens Windows Explorer to show the specified file or folder.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - path: The full path to open in Explorer
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Any execution errors
|
||||||
|
func (a *App) OpenExplorerForPath(path string) error {
|
||||||
|
return internal.OpenFileExplorer(path)
|
||||||
|
}
|
||||||
|
|||||||
@@ -128,3 +128,41 @@ func (a *App) SetUpdateCheckerEnabled(enabled bool) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExportAttachmentFolder updates the EXPORT_ATTACHMENT_FOLDER setting in config.ini
|
||||||
|
// based on the user's preference from the GUI settings.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - folderPath: The path to the folder where attachments should be exported
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if loading or saving config fails
|
||||||
|
func (a *App) SetExportAttachmentFolder(folderPath string) error {
|
||||||
|
// Load current config
|
||||||
|
config := a.GetConfig()
|
||||||
|
if config == nil {
|
||||||
|
return fmt.Errorf("failed to load config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the setting
|
||||||
|
config.EMLy.ExportAttachmentFolder = folderPath
|
||||||
|
|
||||||
|
// Save config back to disk
|
||||||
|
if err := a.SaveConfig(config); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExportAttachmentFolder returns the EXPORT_ATTACHMENT_FOLDER setting from config.ini
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The path to the export folder, or empty string if not set
|
||||||
|
func (a *App) GetExportAttachmentFolder() string {
|
||||||
|
config := a.GetConfig()
|
||||||
|
if config == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return config.EMLy.ExportAttachmentFolder
|
||||||
|
}
|
||||||
|
|||||||
232
app_update.go
232
app_update.go
@@ -410,6 +410,238 @@ func shellExecuteAsAdmin(exePath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// launchDetachedInstaller launches the installer as a completely detached process
|
||||||
|
// using CreateProcess with DETACHED_PROCESS and CREATE_NEW_PROCESS_GROUP flags.
|
||||||
|
// This allows the installer to continue running and close EMLy without errors.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - exePath: Full path to the installer executable
|
||||||
|
// - args: Array of command-line arguments to pass to the installer
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if process creation fails
|
||||||
|
func launchDetachedInstaller(exePath string, args []string) error {
|
||||||
|
// Build command line: executable path + arguments
|
||||||
|
cmdLine := fmt.Sprintf(`"%s"`, exePath)
|
||||||
|
if len(args) > 0 {
|
||||||
|
cmdLine += " " + strings.Join(args, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Launching detached installer: %s", cmdLine)
|
||||||
|
|
||||||
|
// Convert to UTF16 for Windows API
|
||||||
|
cmdLinePtr := syscall.StringToUTF16Ptr(cmdLine)
|
||||||
|
|
||||||
|
// Setup process startup info
|
||||||
|
var si syscall.StartupInfo
|
||||||
|
var pi syscall.ProcessInformation
|
||||||
|
|
||||||
|
si.Cb = uint32(unsafe.Sizeof(si))
|
||||||
|
si.Flags = syscall.STARTF_USESHOWWINDOW
|
||||||
|
si.ShowWindow = syscall.SW_HIDE // Hide installer window (silent mode)
|
||||||
|
|
||||||
|
// Process creation flags:
|
||||||
|
// CREATE_NEW_PROCESS_GROUP: Creates process in new process group
|
||||||
|
// DETACHED_PROCESS: Process has no console, completely detached from parent
|
||||||
|
const (
|
||||||
|
CREATE_NEW_PROCESS_GROUP = 0x00000200
|
||||||
|
DETACHED_PROCESS = 0x00000008
|
||||||
|
)
|
||||||
|
flags := uint32(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
|
||||||
|
|
||||||
|
// Create the detached process
|
||||||
|
err := syscall.CreateProcess(
|
||||||
|
nil, // Application name (nil = use command line)
|
||||||
|
cmdLinePtr, // Command line
|
||||||
|
nil, // Process security attributes
|
||||||
|
nil, // Thread security attributes
|
||||||
|
false, // Inherit handles
|
||||||
|
flags, // Creation flags
|
||||||
|
nil, // Environment (nil = inherit)
|
||||||
|
nil, // Current directory (nil = inherit)
|
||||||
|
&si, // Startup info
|
||||||
|
&pi, // Process information (output)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("CreateProcess failed: %v", err)
|
||||||
|
return fmt.Errorf("failed to create detached process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close process and thread handles immediately
|
||||||
|
// We don't need to wait for the process - it's fully detached
|
||||||
|
syscall.CloseHandle(pi.Process)
|
||||||
|
syscall.CloseHandle(pi.Thread)
|
||||||
|
|
||||||
|
log.Printf("Detached installer process launched successfully (PID: %d)", pi.ProcessId)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallUpdateSilent downloads the update (if needed) and launches the installer
|
||||||
|
// in completely silent mode with a detached process. The installer will run with
|
||||||
|
// these arguments: /VERYSILENT /ALLUSERS /SUPPRESSMSGBOXES /NORESTART /FORCEUPGRADE
|
||||||
|
//
|
||||||
|
// This method automatically quits EMLy after launching the installer, allowing the
|
||||||
|
// installer to close the application and complete the upgrade without user interaction.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if download or launch fails
|
||||||
|
func (a *App) InstallUpdateSilent() error {
|
||||||
|
log.Println("Starting silent update installation...")
|
||||||
|
|
||||||
|
// If installer not ready, attempt to download first
|
||||||
|
if !updateStatus.Ready || updateStatus.InstallerPath == "" {
|
||||||
|
log.Println("Installer not ready, downloading update first...")
|
||||||
|
|
||||||
|
_, err := a.DownloadUpdate()
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("Failed to download update: %v", err)
|
||||||
|
log.Println(errMsg)
|
||||||
|
updateStatus.ErrorMessage = errMsg
|
||||||
|
return fmt.Errorf("download failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait briefly for download to complete
|
||||||
|
log.Println("Download initiated, waiting for completion...")
|
||||||
|
for i := 0; i < 60; i++ { // Wait up to 60 seconds
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
if updateStatus.Ready {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if updateStatus.ErrorMessage != "" {
|
||||||
|
return fmt.Errorf("download error: %s", updateStatus.ErrorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updateStatus.Ready {
|
||||||
|
return fmt.Errorf("download timeout - update not ready after 60 seconds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installerPath := updateStatus.InstallerPath
|
||||||
|
|
||||||
|
// Verify installer exists
|
||||||
|
if _, err := os.Stat(installerPath); os.IsNotExist(err) {
|
||||||
|
updateStatus.ErrorMessage = "Installer file not found"
|
||||||
|
updateStatus.Ready = false
|
||||||
|
log.Printf("Installer not found: %s", installerPath)
|
||||||
|
return fmt.Errorf("installer not found: %s", installerPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Installer ready at: %s", installerPath)
|
||||||
|
|
||||||
|
// Prepare silent installation arguments
|
||||||
|
args := []string{
|
||||||
|
"/VERYSILENT", // No UI, completely silent
|
||||||
|
"/ALLUSERS", // Install for all users (requires admin)
|
||||||
|
"/SUPPRESSMSGBOXES", // Suppress all message boxes
|
||||||
|
"/NORESTART", // Don't restart system
|
||||||
|
"/FORCEUPGRADE", // Skip upgrade confirmation dialog
|
||||||
|
`/LOG="C:\install.log"`, // Create installation log
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Launching installer with args: %v", args)
|
||||||
|
|
||||||
|
// Launch detached installer
|
||||||
|
if err := launchDetachedInstaller(installerPath, args); err != nil {
|
||||||
|
errMsg := fmt.Sprintf("Failed to launch installer: %v", err)
|
||||||
|
log.Println(errMsg)
|
||||||
|
updateStatus.ErrorMessage = errMsg
|
||||||
|
return fmt.Errorf("failed to launch installer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Detached installer launched successfully, quitting EMLy...")
|
||||||
|
|
||||||
|
// Quit application to allow installer to replace files
|
||||||
|
time.Sleep(500 * time.Millisecond) // Brief delay to ensure installer starts
|
||||||
|
runtime.Quit(a.ctx)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallUpdateSilentFromPath downloads an installer from a custom SMB/network path
|
||||||
|
// and launches it in silent mode with a detached process. Use this when you know the
|
||||||
|
// exact installer path (e.g., \\server\updates\EMLy_Installer.exe) without needing
|
||||||
|
// to check the version.json manifest.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - smbPath: Full UNC path or local path to the installer (e.g., \\server\share\EMLy.exe)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if download or launch fails
|
||||||
|
func (a *App) InstallUpdateSilentFromPath(smbPath string) error {
|
||||||
|
log.Printf("Starting silent installation from custom path: %s", smbPath)
|
||||||
|
|
||||||
|
// Verify source installer exists and is accessible
|
||||||
|
if _, err := os.Stat(smbPath); os.IsNotExist(err) {
|
||||||
|
errMsg := fmt.Sprintf("Installer not found at: %s", smbPath)
|
||||||
|
log.Println(errMsg)
|
||||||
|
return fmt.Errorf("%s", errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temporary directory for installer
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
installerFilename := filepath.Base(smbPath)
|
||||||
|
tempInstallerPath := filepath.Join(tempDir, installerFilename)
|
||||||
|
|
||||||
|
log.Printf("Copying installer to temp location: %s", tempInstallerPath)
|
||||||
|
|
||||||
|
// Copy installer from SMB path to local temp
|
||||||
|
sourceFile, err := os.Open(smbPath)
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("Failed to open source installer: %v", err)
|
||||||
|
log.Println(errMsg)
|
||||||
|
return fmt.Errorf("failed to open installer: %w", err)
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(tempInstallerPath)
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("Failed to create temp installer: %v", err)
|
||||||
|
log.Println(errMsg)
|
||||||
|
return fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
// Copy file
|
||||||
|
bytesWritten, err := io.Copy(destFile, sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Sprintf("Failed to copy installer: %v", err)
|
||||||
|
log.Println(errMsg)
|
||||||
|
return fmt.Errorf("failed to copy installer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Installer copied successfully (%d bytes)", bytesWritten)
|
||||||
|
|
||||||
|
// Prepare silent installation arguments
|
||||||
|
args := []string{
|
||||||
|
"/VERYSILENT", // No UI, completely silent
|
||||||
|
"/ALLUSERS", // Install for all users (requires admin)
|
||||||
|
"/SUPPRESSMSGBOXES", // Suppress all message boxes
|
||||||
|
"/NORESTART", // Don't restart system
|
||||||
|
"/FORCEUPGRADE", // Skip upgrade confirmation dialog
|
||||||
|
`/LOG="C:\install.log"`, // Create installation log
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Launching installer with args: %v", args)
|
||||||
|
|
||||||
|
// Launch detached installer
|
||||||
|
if err := launchDetachedInstaller(tempInstallerPath, args); err != nil {
|
||||||
|
errMsg := fmt.Sprintf("Failed to launch installer: %v", err)
|
||||||
|
log.Println(errMsg)
|
||||||
|
return fmt.Errorf("failed to launch installer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Detached installer launched successfully, quitting EMLy...")
|
||||||
|
|
||||||
|
// Quit application to allow installer to replace files
|
||||||
|
time.Sleep(500 * time.Millisecond) // Brief delay to ensure installer starts
|
||||||
|
runtime.Quit(a.ctx)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Status Methods
|
// Status Methods
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ type EMLyConfig struct {
|
|||||||
UpdateCheckEnabled string `ini:"UPDATE_CHECK_ENABLED"`
|
UpdateCheckEnabled string `ini:"UPDATE_CHECK_ENABLED"`
|
||||||
UpdatePath string `ini:"UPDATE_PATH"`
|
UpdatePath string `ini:"UPDATE_PATH"`
|
||||||
UpdateAutoCheck string `ini:"UPDATE_AUTO_CHECK"`
|
UpdateAutoCheck string `ini:"UPDATE_AUTO_CHECK"`
|
||||||
|
WebView2UserDataPath string `ini:"WEBVIEW2_USERDATA_PATH"`
|
||||||
|
WebView2DownloadPath string `ini:"WEBVIEW2_DOWNLOAD_PATH"`
|
||||||
|
ExportAttachmentFolder string `ini:"EXPORT_ATTACHMENT_FOLDER"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig reads the config.ini file at the given path and returns a Config struct
|
// LoadConfig reads the config.ini file at the given path and returns a Config struct
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
@@ -16,6 +23,14 @@ var EmailDialogOptions = runtime.OpenDialogOptions{
|
|||||||
ShowHiddenFiles: false,
|
ShowHiddenFiles: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var FolderDialogOptions = runtime.OpenDialogOptions{
|
||||||
|
Title: "Select Folder",
|
||||||
|
Filters: []runtime.FileFilter{
|
||||||
|
{DisplayName: "Folders", Pattern: "*"},
|
||||||
|
},
|
||||||
|
ShowHiddenFiles: false,
|
||||||
|
}
|
||||||
|
|
||||||
func ShowFileDialog(ctx context.Context) (string, error) {
|
func ShowFileDialog(ctx context.Context) (string, error) {
|
||||||
filePath, err := runtime.OpenFileDialog(ctx, EmailDialogOptions)
|
filePath, err := runtime.OpenFileDialog(ctx, EmailDialogOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -23,3 +38,86 @@ func ShowFileDialog(ctx context.Context) (string, error) {
|
|||||||
}
|
}
|
||||||
return filePath, nil
|
return filePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ShowFolderDialog(ctx context.Context) (string, error) {
|
||||||
|
folderPath, err := runtime.OpenDirectoryDialog(ctx, FolderDialogOptions)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return folderPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAttachmentToFolder saves a base64-encoded attachment to the specified folder.
|
||||||
|
// If folderPath is empty, uses the user's Downloads folder as default.
|
||||||
|
// Expands environment variables in the format %%VAR%% or %VAR%.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filename: The name to save the file as
|
||||||
|
// - base64Data: The base64-encoded file content
|
||||||
|
// - folderPath: Optional custom folder path (uses Downloads if empty)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The full path where the file was saved
|
||||||
|
// - error: Any file system or decoding errors
|
||||||
|
func SaveAttachmentToFolder(filename string, base64Data string, folderPath string) (string, error) {
|
||||||
|
// Decode base64 data
|
||||||
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode attachment data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use configured folder or default to Downloads
|
||||||
|
targetFolder := folderPath
|
||||||
|
if targetFolder == "" {
|
||||||
|
targetFolder = filepath.Join(os.Getenv("USERPROFILE"), "Downloads")
|
||||||
|
} else {
|
||||||
|
// Expand environment variables (%%VAR%% or %VAR% format)
|
||||||
|
re := regexp.MustCompile(`%%([^%]+)%%|%([^%]+)%`)
|
||||||
|
targetFolder = re.ReplaceAllStringFunc(targetFolder, func(match string) string {
|
||||||
|
varName := strings.Trim(match, "%")
|
||||||
|
return os.Getenv(varName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the target folder exists
|
||||||
|
if err := os.MkdirAll(targetFolder, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create target folder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create full path
|
||||||
|
fullPath := filepath.Join(targetFolder, filename)
|
||||||
|
|
||||||
|
// Save the file
|
||||||
|
if err := os.WriteFile(fullPath, data, 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to save attachment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenFileExplorer opens Windows Explorer and selects the specified file.
|
||||||
|
// Uses the /select parameter to highlight the file in Explorer.
|
||||||
|
// If the path is a directory, opens the directory without selecting anything.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filePath: The full path to the file or directory to open in Explorer
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Any execution errors
|
||||||
|
func OpenFileExplorer(filePath string) error {
|
||||||
|
// Check if path is a directory or file
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
// Open directory
|
||||||
|
cmd := exec.Command("explorer.exe", filePath)
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open and select file
|
||||||
|
cmd := exec.Command("explorer.exe", "/select,", filePath)
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
[EMLy]
|
[EMLy]
|
||||||
SDK_DECODER_SEMVER = 1.3.2
|
SDK_DECODER_SEMVER = 1.3.2
|
||||||
SDK_DECODER_RELEASE_CHANNEL = stable
|
SDK_DECODER_RELEASE_CHANNEL = stable
|
||||||
GUI_SEMVER = 1.5.3
|
GUI_SEMVER = 1.5.4
|
||||||
GUI_RELEASE_CHANNEL = beta
|
GUI_RELEASE_CHANNEL = beta
|
||||||
LANGUAGE = it
|
LANGUAGE = it
|
||||||
UPDATE_CHECK_ENABLED = false
|
UPDATE_CHECK_ENABLED = false
|
||||||
UPDATE_PATH =
|
UPDATE_PATH =
|
||||||
UPDATE_AUTO_CHECK = true
|
UPDATE_AUTO_CHECK = true
|
||||||
|
WEBVIEW2_USERDATA_PATH =
|
||||||
|
WEBVIEW2_DOWNLOAD_PATH = %%USERPROFILE%%\Documents\EMLy_Attachments
|
||||||
|
EXPORT_ATTACHMENT_FOLDER =
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
"settings_preview_page_description": "Modify settings related to the preview page",
|
"settings_preview_page_description": "Modify settings related to the preview page",
|
||||||
"settings_preview_builtin_label": "Use built-in preview for images",
|
"settings_preview_builtin_label": "Use built-in preview for images",
|
||||||
"settings_preview_builtin_hint": "Uses EMLy's built-in image previewer for supported image file types.",
|
"settings_preview_builtin_hint": "Uses EMLy's built-in image previewer for supported image file types.",
|
||||||
"settings_preview_builtin_info": "Info: If disabled, image files will be treated as downloads instead of being previewed within the app.",
|
"settings_preview_builtin_info": "Info: If disabled, image files will be opened by the computer's default app instead of being previewed within the app.",
|
||||||
"settings_preview_pdf_builtin_label": "Use built-in viewer for PDFs",
|
"settings_preview_pdf_builtin_label": "Use built-in viewer for PDFs",
|
||||||
"settings_preview_pdf_builtin_hint": "Uses EMLy's built-in viewer for PDF files.",
|
"settings_preview_pdf_builtin_hint": "Uses EMLy's built-in viewer for PDF files.",
|
||||||
"settings_preview_pdf_builtin_info": "Info: If disabled, PDF files will be treated as downloads instead of being previewed within the app.",
|
"settings_preview_pdf_builtin_info": "Info: If disabled, PDF files will be opened by the computer's default app instead of being previewed within the app.",
|
||||||
"settings_msg_converter_title": "MSG Handling",
|
"settings_msg_converter_title": "MSG Handling",
|
||||||
"settings_msg_converter_description": "Configure how MSG files are processed.",
|
"settings_msg_converter_description": "Configure how MSG files are processed.",
|
||||||
"settings_msg_converter_label": "Use MSG to EML converter",
|
"settings_msg_converter_label": "Use MSG to EML converter",
|
||||||
@@ -69,6 +69,8 @@
|
|||||||
"mail_open_btn_title": "Open another file",
|
"mail_open_btn_title": "Open another file",
|
||||||
"mail_close_btn_label": "Close",
|
"mail_close_btn_label": "Close",
|
||||||
"mail_close_btn_title": "Close",
|
"mail_close_btn_title": "Close",
|
||||||
|
"mail_download_btn_label": "Download",
|
||||||
|
"mail_download_btn_title": "Download",
|
||||||
"mail_from": "From:",
|
"mail_from": "From:",
|
||||||
"mail_to": "To:",
|
"mail_to": "To:",
|
||||||
"mail_cc": "Cc:",
|
"mail_cc": "Cc:",
|
||||||
@@ -79,8 +81,9 @@
|
|||||||
"mail_error_image": "Failed to open image file.",
|
"mail_error_image": "Failed to open image file.",
|
||||||
"settings_toast_language_changed": "Language changed successfully!",
|
"settings_toast_language_changed": "Language changed successfully!",
|
||||||
"settings_toast_language_change_failed": "Failed to change language.",
|
"settings_toast_language_change_failed": "Failed to change language.",
|
||||||
"mail_open_btn_text": "Open EML/MSG File",
|
"mail_open_btn_text": "Open File",
|
||||||
"mail_close_btn_text": "Close",
|
"mail_close_btn_text": "Close",
|
||||||
|
"mail_download_btn_text": "Download",
|
||||||
"settings_danger_reset_dialog_description_part1": "This action cannot be undone.",
|
"settings_danger_reset_dialog_description_part1": "This action cannot be undone.",
|
||||||
"settings_danger_reset_dialog_description_part2": "This will permanently delete your current settings and return the app to its default state.",
|
"settings_danger_reset_dialog_description_part2": "This will permanently delete your current settings and return the app to its default state.",
|
||||||
"mail_error_opening": "Failed to open EML file.",
|
"mail_error_opening": "Failed to open EML file.",
|
||||||
@@ -215,5 +218,11 @@
|
|||||||
"pdf_error_no_data_desc": "No PDF data provided. Please open this window from the main EMLy application.",
|
"pdf_error_no_data_desc": "No PDF data provided. Please open this window from the main EMLy application.",
|
||||||
"pdf_error_timeout": "Timeout loading PDF. The worker might have failed to initialize.",
|
"pdf_error_timeout": "Timeout loading PDF. The worker might have failed to initialize.",
|
||||||
"pdf_error_parsing": "Error parsing PDF: ",
|
"pdf_error_parsing": "Error parsing PDF: ",
|
||||||
"pdf_error_rendering": "Error rendering page: "
|
"pdf_error_rendering": "Error rendering page: ",
|
||||||
|
"settings_custom_download_label": "Custom Attachment Download",
|
||||||
|
"settings_custom_download_hint": "Save attachments to a custom folder and open Explorer automatically",
|
||||||
|
"settings_custom_download_info": "Info: When enabled, attachments will be saved to the folder configured below (or WEBVIEW2_DOWNLOAD_PATH if not set) and Windows Explorer will open to show the file. When disabled, uses browser's default download behavior.",
|
||||||
|
"settings_export_folder_label": "Select a folder to save exported attachments",
|
||||||
|
"settings_export_folder_hint": "Choose a default location for saving attachments that you export from emails (instead of the Downloads folder)",
|
||||||
|
"settings_select_folder_button": "Select folder"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
"settings_preview_page_description": "Modifica le impostazioni relative alla pagina di anteprima",
|
"settings_preview_page_description": "Modifica le impostazioni relative alla pagina di anteprima",
|
||||||
"settings_preview_builtin_label": "Usa anteprima integrata per le immagini",
|
"settings_preview_builtin_label": "Usa anteprima integrata per le immagini",
|
||||||
"settings_preview_builtin_hint": "Usa il visualizzatore di immagini integrato di EMLy per i tipi di file immagini supportati.",
|
"settings_preview_builtin_hint": "Usa il visualizzatore di immagini integrato di EMLy per i tipi di file immagini supportati.",
|
||||||
"settings_preview_builtin_info": "Info: Se disabilitato, i file immagine verranno trattati come download anziché essere visualizzati all'interno dell'app.",
|
"settings_preview_builtin_info": "Info: Se disabilitato, i file immagine verranno aperti tramite l'app di default attuale anziché essere visualizzati all'interno dell'app.",
|
||||||
"settings_preview_pdf_builtin_label": "Usa visualizzatore integrato per PDF",
|
"settings_preview_pdf_builtin_label": "Usa visualizzatore integrato per PDF",
|
||||||
"settings_preview_pdf_builtin_hint": "Usa il visualizzatore integrato di EMLy per i file PDF.",
|
"settings_preview_pdf_builtin_hint": "Usa il visualizzatore integrato di EMLy per i file PDF.",
|
||||||
"settings_preview_pdf_builtin_info": "Info: Se disabilitato, i file PDF verranno trattati come download invece di essere visualizzati nell'app.",
|
"settings_preview_pdf_builtin_info": "Info: Se disabilitato, i file PDF verranno aperti tramite l'app di default attuale invece di essere visualizzati nell'app.",
|
||||||
"settings_msg_converter_title": "Gestione MSG",
|
"settings_msg_converter_title": "Gestione MSG",
|
||||||
"settings_msg_converter_description": "Configura come vengono elaborati i file MSG.",
|
"settings_msg_converter_description": "Configura come vengono elaborati i file MSG.",
|
||||||
"settings_msg_converter_label": "Usa convertitore MSG in EML",
|
"settings_msg_converter_label": "Usa convertitore MSG in EML",
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"mail_error_image": "Impossibile aprire il file immagine.",
|
"mail_error_image": "Impossibile aprire il file immagine.",
|
||||||
"settings_toast_language_changed": "Lingua cambiata con successo!",
|
"settings_toast_language_changed": "Lingua cambiata con successo!",
|
||||||
"settings_toast_language_change_failed": "Impossibile cambiare lingua.",
|
"settings_toast_language_change_failed": "Impossibile cambiare lingua.",
|
||||||
"mail_open_btn_text": "Apri file EML/MSG",
|
"mail_open_btn_text": "Apri file",
|
||||||
"mail_close_btn_text": "Chiudi",
|
"mail_close_btn_text": "Chiudi",
|
||||||
"settings_danger_reset_dialog_description_part1": "Questa azione non può essere annullata.",
|
"settings_danger_reset_dialog_description_part1": "Questa azione non può essere annullata.",
|
||||||
"settings_danger_reset_dialog_description_part2": "Questo eliminerà permanentemente le tue impostazioni attuali e riporterà l'app allo stato predefinito.",
|
"settings_danger_reset_dialog_description_part2": "Questo eliminerà permanentemente le tue impostazioni attuali e riporterà l'app allo stato predefinito.",
|
||||||
@@ -215,5 +215,15 @@
|
|||||||
"pdf_error_no_data_desc": "Nessun dato PDF fornito. Apri questa finestra dall'applicazione principale EMLy.",
|
"pdf_error_no_data_desc": "Nessun dato PDF fornito. Apri questa finestra dall'applicazione principale EMLy.",
|
||||||
"pdf_error_timeout": "Timeout caricamento PDF. Il worker potrebbe non essersi inizializzato correttamente.",
|
"pdf_error_timeout": "Timeout caricamento PDF. Il worker potrebbe non essersi inizializzato correttamente.",
|
||||||
"pdf_error_parsing": "Errore nel parsing del PDF: ",
|
"pdf_error_parsing": "Errore nel parsing del PDF: ",
|
||||||
"pdf_error_rendering": "Errore nel rendering della pagina: "
|
"pdf_error_rendering": "Errore nel rendering della pagina: ",
|
||||||
|
"mail_download_btn_label": "Scarica",
|
||||||
|
"mail_download_btn_title": "Scarica",
|
||||||
|
"mail_download_btn_text": "Scarica",
|
||||||
|
"settings_custom_download_label": "Download Personalizzato Allegati",
|
||||||
|
"settings_custom_download_hint": "Salva gli allegati in una cartella personalizzata e apri automaticamente Esplora Risorse",
|
||||||
|
"settings_custom_download_info": "Info: Quando abilitato, gli allegati verranno salvati nella cartella configurata di seguito (o WEBVIEW2_DOWNLOAD_PATH se non impostata) e Windows Explorer si aprirà per mostrare il file. Quando disabilitato, usa il comportamento di download predefinito del browser.",
|
||||||
|
"settings_export_folder_label": "Seleziona una cartella per salvare gli allegati esportati",
|
||||||
|
"settings_export_folder_hint": "Scegli una posizione predefinita per salvare gli allegati che esporti dalle email (invece della cartella Download)",
|
||||||
|
"settings_select_folder_button": "Seleziona cartella"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
309
frontend/src/lib/components/BugReportDialog.svelte
Normal file
309
frontend/src/lib/components/BugReportDialog.svelte
Normal 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>
|
||||||
@@ -9,11 +9,13 @@
|
|||||||
Signature,
|
Signature,
|
||||||
FileCode,
|
FileCode,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Download,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { sidebarOpen } from '$lib/stores/app';
|
import { sidebarOpen } from '$lib/stores/app';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { EventsOn, WindowShow, WindowUnminimise } from '$lib/wailsjs/runtime/runtime';
|
import { EventsOn, WindowShow, WindowUnminimise } from '$lib/wailsjs/runtime/runtime';
|
||||||
|
import { SaveAttachment, OpenExplorerForPath } from '$lib/wailsjs/go/main/App';
|
||||||
import { mailState } from '$lib/stores/mail-state.svelte';
|
import { mailState } from '$lib/stores/mail-state.svelte';
|
||||||
import * as m from '$lib/paraglide/messages';
|
import * as m from '$lib/paraglide/messages';
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
isEmailFile,
|
isEmailFile,
|
||||||
} from '$lib/utils/mail';
|
} from '$lib/utils/mail';
|
||||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||||
|
import { Separator } from "$lib/components/ui/separator";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// State
|
// State
|
||||||
@@ -59,6 +62,43 @@
|
|||||||
mailState.clear();
|
mailState.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onDownloadAttachments() {
|
||||||
|
if (!mailState.currentEmail || !mailState.currentEmail.attachments) return;
|
||||||
|
|
||||||
|
// Check if custom download behavior is enabled
|
||||||
|
const useCustomDownload = settingsStore.settings.useCustomAttachmentDownload ?? false;
|
||||||
|
|
||||||
|
if (useCustomDownload) {
|
||||||
|
// Use backend SaveAttachment (saves to configured folder and opens Explorer)
|
||||||
|
try {
|
||||||
|
let lastSavedPath = '';
|
||||||
|
for (const att of mailState.currentEmail.attachments) {
|
||||||
|
const base64 = arrayBufferToBase64(att.data);
|
||||||
|
lastSavedPath = await SaveAttachment(att.filename, base64);
|
||||||
|
toast.success(`Saved: ${att.filename}`);
|
||||||
|
}
|
||||||
|
// Open Explorer to show the folder where files were saved
|
||||||
|
if (lastSavedPath) {
|
||||||
|
await OpenExplorerForPath(lastSavedPath);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Failed to save attachments: ${err}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use browser default download (downloads to browser's default folder)
|
||||||
|
mailState.currentEmail.attachments.forEach((att) => {
|
||||||
|
const base64 = arrayBufferToBase64(att.data);
|
||||||
|
const dataUrl = createDataUrl(att.contentType, base64);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.download = att.filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onOpenMail() {
|
async function onOpenMail() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
loadingText = m.layout_loading_text();
|
loadingText = m.layout_loading_text();
|
||||||
@@ -224,6 +264,16 @@
|
|||||||
{mailState.currentEmail.subject || m.mail_subject_no_subject()}
|
{mailState.currentEmail.subject || m.mail_subject_no_subject()}
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
onclick={onDownloadAttachments}
|
||||||
|
aria-label={m.mail_download_btn_label()}
|
||||||
|
title={m.mail_download_btn_title()}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Download size="15" />
|
||||||
|
{m.mail_download_btn_text()}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
onclick={onOpenMail}
|
onclick={onOpenMail}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const defaults: EMLy_GUI_Settings = {
|
|||||||
reduceMotion: false,
|
reduceMotion: false,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
increaseWindowButtonsContrast: false,
|
increaseWindowButtonsContrast: false,
|
||||||
|
exportAttachmentFolder: "",
|
||||||
|
useCustomAttachmentDownload: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsStore {
|
class SettingsStore {
|
||||||
|
|||||||
12
frontend/src/lib/types.d.ts
vendored
12
frontend/src/lib/types.d.ts
vendored
@@ -5,15 +5,17 @@ type SupportedFileTypePreview = "jpg" | "jpeg" | "png";
|
|||||||
interface EMLy_GUI_Settings {
|
interface EMLy_GUI_Settings {
|
||||||
selectedLanguage: SupportedLanguages = "en" | "it";
|
selectedLanguage: SupportedLanguages = "en" | "it";
|
||||||
useBuiltinPreview: boolean;
|
useBuiltinPreview: boolean;
|
||||||
useBuiltinPDFViewer?: boolean;
|
useBuiltinPDFViewer: boolean;
|
||||||
previewFileSupportedTypes?: SupportedFileTypePreview[];
|
previewFileSupportedTypes: SupportedFileTypePreview[];
|
||||||
enableAttachedDebuggerProtection?: boolean;
|
enableAttachedDebuggerProtection: boolean;
|
||||||
useDarkEmailViewer?: boolean;
|
useDarkEmailViewer?: boolean;
|
||||||
enableUpdateChecker?: boolean;
|
enableUpdateChecker?: boolean;
|
||||||
musicInspirationEnabled?: boolean;
|
musicInspirationEnabled?: boolean;
|
||||||
reduceMotion?: boolean;
|
reduceMotion?: boolean;
|
||||||
theme?: "light" | "dark";
|
theme: "light" | "dark";
|
||||||
increaseWindowButtonsContrast?: boolean;
|
increaseWindowButtonsContrast: boolean;
|
||||||
|
exportAttachmentFolder?: string;
|
||||||
|
useCustomAttachmentDownload?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedLanguages = "en" | "it";
|
type SupportedLanguages = "en" | "it";
|
||||||
|
|||||||
@@ -17,22 +17,14 @@
|
|||||||
House,
|
House,
|
||||||
Settings,
|
Settings,
|
||||||
Bug,
|
Bug,
|
||||||
Loader2,
|
|
||||||
Copy,
|
|
||||||
FolderOpen,
|
|
||||||
CheckCircle,
|
|
||||||
Camera,
|
|
||||||
Heart,
|
Heart,
|
||||||
Info,
|
Info,
|
||||||
Music
|
Music
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { Button, buttonVariants } from "$lib/components/ui/button/index.js";
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
import BugReportDialog from "$lib/components/BugReportDialog.svelte";
|
||||||
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 {
|
import {
|
||||||
WindowMinimise,
|
WindowMinimise,
|
||||||
@@ -44,7 +36,7 @@
|
|||||||
EventsOff,
|
EventsOff,
|
||||||
} from "$lib/wailsjs/runtime/runtime";
|
} from "$lib/wailsjs/runtime/runtime";
|
||||||
import { RefreshCcwDot } from "@lucide/svelte";
|
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";
|
import { settingsStore } from "$lib/stores/settings.svelte.js";
|
||||||
|
|
||||||
let versionInfo: utils.Config | null = $state(null);
|
let versionInfo: utils.Config | null = $state(null);
|
||||||
@@ -52,20 +44,6 @@
|
|||||||
let isDebugerOn: boolean = $state(false);
|
let isDebugerOn: boolean = $state(false);
|
||||||
let isDebbugerProtectionOn: boolean = $state(true);
|
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() {
|
async function syncMaxState() {
|
||||||
isMaximized = await WindowIsMaximised();
|
isMaximized = await WindowIsMaximised();
|
||||||
}
|
}
|
||||||
@@ -155,17 +133,6 @@
|
|||||||
applyTheme(stored === "light" ? "light" : "dark");
|
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
|
// Listen for automatic update notifications
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!browser) return;
|
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();
|
syncMaxState();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -435,134 +327,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bug Report Dialog -->
|
<BugReportDialog />
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -808,26 +573,4 @@
|
|||||||
transform: rotate(360deg);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -59,6 +59,16 @@ const inspirationTracks: SpotifyTrack[] = [
|
|||||||
artist: "When Snakes Sing",
|
artist: "When Snakes Sing",
|
||||||
spotifyUrl: "https://open.spotify.com/track/1nDkT2Cn13qDnFegF93UHi",
|
spotifyUrl: "https://open.spotify.com/track/1nDkT2Cn13qDnFegF93UHi",
|
||||||
embedUrl: "https://open.spotify.com/embed/track/1nDkT2Cn13qDnFegF93UHi?utm_source=generator"
|
embedUrl: "https://open.spotify.com/embed/track/1nDkT2Cn13qDnFegF93UHi?utm_source=generator"
|
||||||
|
}, {
|
||||||
|
name: "Keep It Tucked",
|
||||||
|
artist: "ThxSoMch",
|
||||||
|
spotifyUrl: "https://open.spotify.com/track/1EdQCb51lC8usq47IMhADP",
|
||||||
|
embedUrl: "https://open.spotify.com/embed/track/1EdQCb51lC8usq47IMhADP?utm_source=generator"
|
||||||
|
}, {
|
||||||
|
name: "Deadly Valentine",
|
||||||
|
artist: "Charlotte Gainsbourg",
|
||||||
|
spotifyUrl: "https://open.spotify.com/track/0pfTlQJBOV4LUmF8qqrVy5",
|
||||||
|
embedUrl: "https://open.spotify.com/embed/track/0pfTlQJBOV4LUmF8qqrVy5?utm_source=generator"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -71,7 +81,7 @@ async function fetchEmbedHtml(track: SpotifyTrack, fetch: typeof globalThis.fetc
|
|||||||
return { ...track, embedHtml: data.html };
|
return { ...track, embedHtml: data.html };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to fetch oEmbed for ${track.name}:`, e);
|
console.error(`Failed to fetch oEmbed for ${track.spotifyUrl}:`, e);
|
||||||
}
|
}
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { Label } from "$lib/components/ui/label";
|
import { Label } from "$lib/components/ui/label";
|
||||||
import { Separator } from "$lib/components/ui/separator";
|
import { Separator } from "$lib/components/ui/separator";
|
||||||
import { Switch } from "$lib/components/ui/switch";
|
import { Switch } from "$lib/components/ui/switch";
|
||||||
import { ChevronLeft, Flame, Download, Upload, RefreshCw, CheckCircle2, AlertCircle, Sun, Moon } from "@lucide/svelte";
|
import { ChevronLeft, Flame, Download, Upload, RefreshCw, CheckCircle2, AlertCircle, Sun, Moon, FolderArchive } from "@lucide/svelte";
|
||||||
import type { EMLy_GUI_Settings } from "$lib/types";
|
import type { EMLy_GUI_Settings } from "$lib/types";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
import { It, Us } from "svelte-flags";
|
import { It, Us } from "svelte-flags";
|
||||||
@@ -25,8 +25,9 @@
|
|||||||
import { setLocale } from "$lib/paraglide/runtime";
|
import { setLocale } from "$lib/paraglide/runtime";
|
||||||
import { mailState } from "$lib/stores/mail-state.svelte.js";
|
import { mailState } from "$lib/stores/mail-state.svelte.js";
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { ExportSettings, ImportSettings, CheckForUpdates, DownloadUpdate, InstallUpdate, GetUpdateStatus, SetUpdateCheckerEnabled } from "$lib/wailsjs/go/main/App";
|
import { ExportSettings, ImportSettings, CheckForUpdates, DownloadUpdate, InstallUpdate, SetUpdateCheckerEnabled, ShowOpenFolderDialog, GetExportAttachmentFolder, SetExportAttachmentFolder } from "$lib/wailsjs/go/main/App";
|
||||||
import { EventsOn, EventsOff } from "$lib/wailsjs/runtime/runtime";
|
import { EventsOn, EventsOff } from "$lib/wailsjs/runtime/runtime";
|
||||||
|
import Input from "$lib/components/ui/input/input.svelte";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let config = $derived(data.config);
|
let config = $derived(data.config);
|
||||||
@@ -44,6 +45,8 @@
|
|||||||
reduceMotion: false,
|
reduceMotion: false,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
increaseWindowButtonsContrast: false,
|
increaseWindowButtonsContrast: false,
|
||||||
|
exportAttachmentFolder: "",
|
||||||
|
useCustomAttachmentDownload: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function setLanguage(
|
async function setLanguage(
|
||||||
@@ -82,6 +85,8 @@
|
|||||||
reduceMotion: s.reduceMotion ?? defaults.reduceMotion ?? false,
|
reduceMotion: s.reduceMotion ?? defaults.reduceMotion ?? false,
|
||||||
theme: s.theme || defaults.theme || "light",
|
theme: s.theme || defaults.theme || "light",
|
||||||
increaseWindowButtonsContrast: s.increaseWindowButtonsContrast ?? defaults.increaseWindowButtonsContrast ?? false,
|
increaseWindowButtonsContrast: s.increaseWindowButtonsContrast ?? defaults.increaseWindowButtonsContrast ?? false,
|
||||||
|
exportAttachmentFolder: s.exportAttachmentFolder || defaults.exportAttachmentFolder || "",
|
||||||
|
useCustomAttachmentDownload: s.useCustomAttachmentDownload ?? defaults.useCustomAttachmentDownload ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +99,8 @@
|
|||||||
!!a.useDarkEmailViewer === !!b.useDarkEmailViewer &&
|
!!a.useDarkEmailViewer === !!b.useDarkEmailViewer &&
|
||||||
!!a.enableUpdateChecker === !!b.enableUpdateChecker &&
|
!!a.enableUpdateChecker === !!b.enableUpdateChecker &&
|
||||||
!!a.reduceMotion === !!b.reduceMotion &&
|
!!a.reduceMotion === !!b.reduceMotion &&
|
||||||
|
!!a.exportAttachmentFolder === !!b.exportAttachmentFolder &&
|
||||||
|
!!a.useCustomAttachmentDownload === !!b.useCustomAttachmentDownload &&
|
||||||
(a.theme ?? "light") === (b.theme ?? "light") &&
|
(a.theme ?? "light") === (b.theme ?? "light") &&
|
||||||
!!a.increaseWindowButtonsContrast === !!b.increaseWindowButtonsContrast &&
|
!!a.increaseWindowButtonsContrast === !!b.increaseWindowButtonsContrast &&
|
||||||
JSON.stringify(a.previewFileSupportedTypes?.sort()) ===
|
JSON.stringify(a.previewFileSupportedTypes?.sort()) ===
|
||||||
@@ -142,6 +149,7 @@
|
|||||||
sessionStorage.removeItem("debugWindowInSettings");
|
sessionStorage.removeItem("debugWindowInSettings");
|
||||||
dangerZoneEnabled.set(false);
|
dangerZoneEnabled.set(false);
|
||||||
LogDebug("Reset danger zone setting to false.");
|
LogDebug("Reset danger zone setting to false.");
|
||||||
|
await SetExportAttachmentFolder("");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(m.settings_toast_reset_failed());
|
toast.error(m.settings_toast_reset_failed());
|
||||||
return;
|
return;
|
||||||
@@ -195,10 +203,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Sync update checker setting to backend config.ini
|
// Sync update checker setting to backend config.ini
|
||||||
let previousUpdateCheckerEnabled = form.enableUpdateChecker;
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
let previousUpdateCheckerEnabled = form.enableUpdateChecker;
|
||||||
if (form.enableUpdateChecker !== previousUpdateCheckerEnabled) {
|
if (form.enableUpdateChecker !== previousUpdateCheckerEnabled) {
|
||||||
try {
|
try {
|
||||||
await SetUpdateCheckerEnabled(form.enableUpdateChecker ?? true);
|
await SetUpdateCheckerEnabled(form.enableUpdateChecker ?? true);
|
||||||
@@ -221,6 +229,52 @@
|
|||||||
previousTheme = form.theme;
|
previousTheme = form.theme;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load export attachment folder from config.ini on startup
|
||||||
|
$effect(() => {
|
||||||
|
if (!browser) return;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const configFolder = await GetExportAttachmentFolder();
|
||||||
|
if (configFolder && configFolder.trim() !== "") {
|
||||||
|
form.exportAttachmentFolder = configFolder;
|
||||||
|
// Also update lastSaved to avoid triggering unsaved changes
|
||||||
|
lastSaved = { ...lastSaved, exportAttachmentFolder: configFolder };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load export folder from config:", err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openFolderDialog(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const result = await ShowOpenFolderDialog();
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to open folder dialog:", err);
|
||||||
|
toast.error("Failed to open folder dialog.");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectExportFolder() {
|
||||||
|
const folder = await openFolderDialog();
|
||||||
|
if (folder) {
|
||||||
|
// Save to form state
|
||||||
|
form.exportAttachmentFolder = folder;
|
||||||
|
// Save to config.ini
|
||||||
|
try {
|
||||||
|
await SetExportAttachmentFolder(folder);
|
||||||
|
toast.success("Export folder updated!");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save export folder:", err);
|
||||||
|
toast.error("Failed to save export folder to config.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function exportSettings() {
|
async function exportSettings() {
|
||||||
try {
|
try {
|
||||||
const settingsJSON = JSON.stringify(form, null, 2);
|
const settingsJSON = JSON.stringify(form, null, 2);
|
||||||
@@ -344,7 +398,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="min-h-[calc(100vh-1rem)] bg-gradient-to-b from-background to-muted/30">
|
<div class="min-h-[calc(100vh-1rem)] bg-linear-to-b from-background to-muted/30">
|
||||||
<div
|
<div
|
||||||
class="mx-auto flex max-w-3xl flex-col gap-4 px-4 py-6 sm:px-6 sm:py-10 opacity-80"
|
class="mx-auto flex max-w-3xl flex-col gap-4 px-4 py-6 sm:px-6 sm:py-10 opacity-80"
|
||||||
>
|
>
|
||||||
@@ -692,6 +746,61 @@
|
|||||||
{m.settings_preview_pdf_builtin_info()}
|
{m.settings_preview_pdf_builtin_info()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">{m.settings_custom_download_label()}</div>
|
||||||
|
<div class="text-sm text-muted-foreground">
|
||||||
|
{m.settings_custom_download_hint()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="use-custom-attachment-download"
|
||||||
|
bind:checked={form.useCustomAttachmentDownload}
|
||||||
|
class="cursor-pointer hover:cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground mt-2">
|
||||||
|
{m.settings_custom_download_info()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if form.useCustomAttachmentDownload}
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="rounded-lg border bg-card p-4 space-y-3">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">
|
||||||
|
{m.settings_export_folder_label()}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-muted-foreground">
|
||||||
|
{m.settings_export_folder_hint()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="%USERPROFILE%\Documents\EMLy_Attachments"
|
||||||
|
class="flex-1"
|
||||||
|
readonly
|
||||||
|
bind:value={form.exportAttachmentFolder}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="cursor-pointer hover:cursor-pointer"
|
||||||
|
onclick={selectExportFolder}
|
||||||
|
>
|
||||||
|
<FolderArchive class="size-4 mr-2" />
|
||||||
|
{m.settings_select_folder_button()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
import type { PageLoad } from './$types';
|
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 { browser } from '$app/environment';
|
||||||
import { dangerZoneEnabled } from "$lib/stores/app";
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
if (!browser) return { machineData: null, config: null };
|
if (!browser) return { config: null };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [machineData, configRoot] = await Promise.all([
|
const configRoot = await GetConfig();
|
||||||
get(dangerZoneEnabled) ? GetMachineData() : Promise.resolve(null),
|
|
||||||
GetConfig()
|
|
||||||
]);
|
|
||||||
return {
|
return {
|
||||||
machineData,
|
|
||||||
config: configRoot.EMLy
|
config: configRoot.EMLy
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load settings data", e);
|
console.error("Failed to load settings data", e);
|
||||||
return {
|
return {
|
||||||
machineData: null,
|
|
||||||
config: null
|
config: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
AlignHorizontalSpaceAround,
|
AlignHorizontalSpaceAround,
|
||||||
|
Download
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import { sidebarOpen } from "$lib/stores/app";
|
import { sidebarOpen } from "$lib/stores/app";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
@@ -84,6 +85,17 @@
|
|||||||
fitToScreen();
|
fitToScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadImage() {
|
||||||
|
if (!imageData || !filename) return;
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = `data:image/png;base64,${imageData}`;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
function handleWheel(e: WheelEvent) {
|
function handleWheel(e: WheelEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const delta = -e.deltaY * 0.001;
|
const delta = -e.deltaY * 0.001;
|
||||||
@@ -116,6 +128,10 @@
|
|||||||
<h1 class="title" title={filename}>{filename || "Image Viewer"}</h1>
|
<h1 class="title" title={filename}>{filename || "Image Viewer"}</h1>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
<button class="btn" onclick={() => downloadImage()} title="Download">
|
||||||
|
<Download size="16" />
|
||||||
|
</button>
|
||||||
|
<div class="separator"></div>
|
||||||
<button class="btn" onclick={() => zoom(0.1)} title="Zoom In">
|
<button class="btn" onclick={() => zoom(0.1)} title="Zoom In">
|
||||||
<ZoomIn size="16" />
|
<ZoomIn size="16" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount, untrack } from "svelte";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
import {
|
import {
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
AlignHorizontalSpaceAround,
|
AlignHorizontalSpaceAround,
|
||||||
|
Download
|
||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import { sidebarOpen } from "$lib/stores/app";
|
import { sidebarOpen } from "$lib/stores/app";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
@@ -106,7 +107,13 @@
|
|||||||
if (!pdfDoc || !canvasRef) return;
|
if (!pdfDoc || !canvasRef) return;
|
||||||
|
|
||||||
if (renderTask) {
|
if (renderTask) {
|
||||||
await renderTask.promise.catch(() => {}); // Cancel previous render if any (though we wait usually)
|
// Cancel previous render if any and await its cleanup
|
||||||
|
renderTask.cancel();
|
||||||
|
try {
|
||||||
|
await renderTask.promise;
|
||||||
|
} catch (e) {
|
||||||
|
// Expected cancellation error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -130,7 +137,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Cast to any to avoid type mismatch with PDF.js definitions
|
// Cast to any to avoid type mismatch with PDF.js definitions
|
||||||
await page.render(renderContext as any).promise;
|
const task = page.render(renderContext as any);
|
||||||
|
renderTask = task;
|
||||||
|
await task.promise;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.name !== "RenderingCancelledException") {
|
if (e.name !== "RenderingCancelledException") {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -155,11 +164,15 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// Re-render when scale or rotation changes
|
// Re-render when scale or rotation changes
|
||||||
// Access them here to ensure dependency tracking since renderPage is async
|
// Access them here to ensure dependency tracking since renderPage is untracked
|
||||||
const _deps = [scale, rotation];
|
// We also track pageNum to ensure we re-render if it changes via other means,
|
||||||
|
// although navigation functions usually call renderPage manually.
|
||||||
|
const _deps = [scale, rotation, pageNum];
|
||||||
|
|
||||||
if (pdfDoc) {
|
if (pdfDoc) {
|
||||||
renderPage(pageNum);
|
// Untrack renderPage because it reads and writes to renderTask,
|
||||||
|
// which would otherwise cause an infinite loop.
|
||||||
|
untrack(() => renderPage(pageNum));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,6 +195,24 @@
|
|||||||
pageNum--;
|
pageNum--;
|
||||||
renderPage(pageNum);
|
renderPage(pageNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadPDF() {
|
||||||
|
if (!pdfData) return;
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const blob = new Blob([pdfData], { type: "application/pdf" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename || "document.pdf";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error("Failed to download PDF: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="viewer-container">
|
<div class="viewer-container">
|
||||||
@@ -202,6 +233,10 @@
|
|||||||
<h1 class="title" title={filename}>{filename || m.pdf_viewer_title()}</h1>
|
<h1 class="title" title={filename}>{filename || m.pdf_viewer_title()}</h1>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
<button class="btn" onclick={() => downloadPDF()} title={m.mail_download_btn_title()}>
|
||||||
|
<Download size="16" />
|
||||||
|
</button>
|
||||||
|
<div class="separator"></div>
|
||||||
<button class="btn" onclick={() => zoom(0.1)} title={m.pdf_zoom_in()}>
|
<button class="btn" onclick={() => zoom(0.1)} title={m.pdf_zoom_in()}>
|
||||||
<ZoomIn size="16" />
|
<ZoomIn size="16" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#define ApplicationName 'EMLy'
|
#define ApplicationName 'EMLy'
|
||||||
#define ApplicationVersion GetVersionNumbersString('EMLy.exe')
|
#define ApplicationVersion GetVersionNumbersString('EMLy.exe')
|
||||||
#define ApplicationVersion '1.5.3_beta'
|
#define ApplicationVersion '1.5.4_beta'
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
@@ -80,6 +80,20 @@ var
|
|||||||
PreviousVersion: String;
|
PreviousVersion: String;
|
||||||
IsUpgrade: Boolean;
|
IsUpgrade: Boolean;
|
||||||
|
|
||||||
|
// Check if a command line parameter exists
|
||||||
|
function CmdLineParamExists(const Param: string): Boolean;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
for I := 1 to ParamCount do
|
||||||
|
if CompareText(ParamStr(I), Param) = 0 then
|
||||||
|
begin
|
||||||
|
Result := True;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
// Check if a previous version is installed
|
// Check if a previous version is installed
|
||||||
function GetPreviousVersion(): String;
|
function GetPreviousVersion(): String;
|
||||||
var
|
var
|
||||||
@@ -115,13 +129,17 @@ begin
|
|||||||
|
|
||||||
if IsUpgrade then
|
if IsUpgrade then
|
||||||
begin
|
begin
|
||||||
// Show upgrade message
|
// Check for /FORCEUPGRADE parameter to skip confirmation
|
||||||
Message := FmtMessage(CustomMessage('UpgradeDetected'), [PreviousVersion]) + #13#10#13#10 +
|
if not CmdLineParamExists('/FORCEUPGRADE') then
|
||||||
CustomMessage('UpgradeMessage');
|
|
||||||
|
|
||||||
if MsgBox(Message, mbInformation, MB_YESNO) = IDNO then
|
|
||||||
begin
|
begin
|
||||||
Result := False;
|
// Show upgrade message
|
||||||
|
Message := FmtMessage(CustomMessage('UpgradeDetected'), [PreviousVersion]) + #13#10#13#10 +
|
||||||
|
CustomMessage('UpgradeMessage');
|
||||||
|
|
||||||
|
if MsgBox(Message, mbInformation, MB_YESNO) = IDNO then
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
56
main.go
56
main.go
@@ -4,11 +4,14 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2"
|
"github.com/wailsapp/wails/v2"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,6 +36,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer CloseLogger()
|
defer CloseLogger()
|
||||||
|
|
||||||
|
// Load config.ini to get WebView2 paths
|
||||||
|
configPath := filepath.Join(filepath.Dir(os.Args[0]), "config.ini")
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
configPath = "config.ini" // fallback to current directory
|
||||||
|
}
|
||||||
|
|
||||||
// Check for custom args
|
// Check for custom args
|
||||||
args := os.Args
|
args := os.Args
|
||||||
uniqueId := "emly-app-lock"
|
uniqueId := "emly-app-lock"
|
||||||
@@ -74,6 +83,49 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create application with options
|
// Create application with options
|
||||||
|
// Configure WebView2 DataPath (user data folder)
|
||||||
|
userDataPath := filepath.Join(os.Getenv("APPDATA"), "EMLy") // default
|
||||||
|
downloadPath := filepath.Join(os.Getenv("USERPROFILE"), "Downloads") // default
|
||||||
|
|
||||||
|
// Helper function to expand Windows-style environment variables
|
||||||
|
expandEnvVars := func(path string) string {
|
||||||
|
// Match %%VAR%% or %VAR% patterns and replace with actual values
|
||||||
|
re := regexp.MustCompile(`%%([^%]+)%%|%([^%]+)%`)
|
||||||
|
return re.ReplaceAllStringFunc(path, func(match string) string {
|
||||||
|
varName := strings.Trim(match, "%")
|
||||||
|
return os.Getenv(varName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load paths from config.ini if available
|
||||||
|
if cfg, err := os.ReadFile(configPath); err == nil {
|
||||||
|
// Simple INI parsing for these specific values
|
||||||
|
lines := strings.Split(string(cfg), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "WEBVIEW2_USERDATA_PATH") {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
path := strings.TrimSpace(parts[1])
|
||||||
|
if path != "" {
|
||||||
|
userDataPath = expandEnvVars(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(line, "WEBVIEW2_DOWNLOAD_PATH") {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
path := strings.TrimSpace(parts[1])
|
||||||
|
if path != "" {
|
||||||
|
downloadPath = expandEnvVars(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("WebView2 UserDataPath: %s", userDataPath)
|
||||||
|
log.Printf("WebView2 DownloadPath: %s", downloadPath)
|
||||||
|
|
||||||
err := wails.Run(&options.App{
|
err := wails.Run(&options.App{
|
||||||
Title: windowTitle,
|
Title: windowTitle,
|
||||||
Width: windowWidth,
|
Width: windowWidth,
|
||||||
@@ -94,6 +146,10 @@ func main() {
|
|||||||
MinWidth: 964,
|
MinWidth: 964,
|
||||||
MinHeight: 690,
|
MinHeight: 690,
|
||||||
Frameless: frameless,
|
Frameless: frameless,
|
||||||
|
Windows: &windows.Options{
|
||||||
|
WebviewUserDataPath: userDataPath,
|
||||||
|
WebviewBrowserPath: "", // Empty = use system Edge WebView2
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user