430 lines
13 KiB
Go
430 lines
13 KiB
Go
// Package main provides viewer window functionality for EMLy.
|
|
// This file contains methods for opening attachments in viewer windows
|
|
// or with external applications.
|
|
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// =============================================================================
|
|
// Viewer Data Types
|
|
// =============================================================================
|
|
|
|
// ImageViewerData contains the data needed to display an image in the viewer window.
|
|
type ImageViewerData struct {
|
|
// Data is the base64-encoded image data
|
|
Data string `json:"data"`
|
|
// Filename is the original filename of the image
|
|
Filename string `json:"filename"`
|
|
}
|
|
|
|
// PDFViewerData contains the data needed to display a PDF in the viewer window.
|
|
type PDFViewerData struct {
|
|
// Data is the base64-encoded PDF data
|
|
Data string `json:"data"`
|
|
// Filename is the original filename of the PDF
|
|
Filename string `json:"filename"`
|
|
}
|
|
|
|
// ViewerData is a union type that contains either image or PDF viewer data.
|
|
// Used by the viewer page to determine which type of content to display.
|
|
type ViewerData struct {
|
|
// ImageData is set when viewing an image (mutually exclusive with PDFData)
|
|
ImageData *ImageViewerData `json:"imageData,omitempty"`
|
|
// PDFData is set when viewing a PDF (mutually exclusive with ImageData)
|
|
PDFData *PDFViewerData `json:"pdfData,omitempty"`
|
|
}
|
|
|
|
// =============================================================================
|
|
// Built-in Viewer Window Methods
|
|
// =============================================================================
|
|
|
|
// OpenEMLWindow opens an EML attachment in a new EMLy window.
|
|
// The EML data is saved to a temp file and a new EMLy instance is launched.
|
|
//
|
|
// This method tracks open EML files to prevent duplicate windows for the same file.
|
|
// The tracking is released when the viewer window is closed.
|
|
//
|
|
// Parameters:
|
|
// - base64Data: Base64-encoded EML file content
|
|
// - filename: The original filename of the EML attachment
|
|
//
|
|
// Returns:
|
|
// - error: Error if the file is already open or if launching fails
|
|
func (a *App) OpenEMLWindow(base64Data string, filename string) error {
|
|
// Check if this EML is already open
|
|
a.openEMLsMux.Lock()
|
|
if a.openEMLs[filename] {
|
|
a.openEMLsMux.Unlock()
|
|
return fmt.Errorf("eml '%s' is already open", filename)
|
|
}
|
|
a.openEMLs[filename] = true
|
|
a.openEMLsMux.Unlock()
|
|
|
|
// Decode base64 data
|
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
|
if err != nil {
|
|
a.openEMLsMux.Lock()
|
|
delete(a.openEMLs, filename)
|
|
a.openEMLsMux.Unlock()
|
|
return fmt.Errorf("failed to decode base64: %w", err)
|
|
}
|
|
|
|
// Save to temp file with timestamp to avoid conflicts
|
|
tempDir := os.TempDir()
|
|
timestamp := time.Now().Format("20060102_150405")
|
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s_%s_%s", "emly_attachment", timestamp, filename))
|
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
|
a.openEMLsMux.Lock()
|
|
delete(a.openEMLs, filename)
|
|
a.openEMLsMux.Unlock()
|
|
return fmt.Errorf("failed to write temp file: %w", err)
|
|
}
|
|
|
|
// Launch new EMLy instance with the file path
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
a.openEMLsMux.Lock()
|
|
delete(a.openEMLs, filename)
|
|
a.openEMLsMux.Unlock()
|
|
return fmt.Errorf("failed to get executable path: %w", err)
|
|
}
|
|
|
|
cmd := exec.Command(exe, tempFile)
|
|
if err := cmd.Start(); err != nil {
|
|
a.openEMLsMux.Lock()
|
|
delete(a.openEMLs, filename)
|
|
a.openEMLsMux.Unlock()
|
|
return fmt.Errorf("failed to start viewer: %w", err)
|
|
}
|
|
|
|
// Monitor process in background to release lock when closed
|
|
go func() {
|
|
cmd.Wait()
|
|
a.openEMLsMux.Lock()
|
|
delete(a.openEMLs, filename)
|
|
a.openEMLsMux.Unlock()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// OpenImageWindow opens an image attachment in a new EMLy viewer window.
|
|
// The image data is saved to a temp file and a new EMLy instance is launched
|
|
// with the --view-image flag.
|
|
//
|
|
// This method tracks open images to prevent duplicate windows for the same file.
|
|
//
|
|
// Parameters:
|
|
// - base64Data: Base64-encoded image data
|
|
// - filename: The original filename of the image
|
|
//
|
|
// Returns:
|
|
// - error: Error if the image is already open or if launching fails
|
|
func (a *App) OpenImageWindow(base64Data string, filename string) error {
|
|
// Check if this image is already open
|
|
a.openImagesMux.Lock()
|
|
if a.openImages[filename] {
|
|
a.openImagesMux.Unlock()
|
|
return fmt.Errorf("image '%s' is already open", filename)
|
|
}
|
|
a.openImages[filename] = true
|
|
a.openImagesMux.Unlock()
|
|
|
|
// Decode base64 data
|
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
|
if err != nil {
|
|
a.openImagesMux.Lock()
|
|
delete(a.openImages, filename)
|
|
a.openImagesMux.Unlock()
|
|
return fmt.Errorf("failed to decode base64: %w", err)
|
|
}
|
|
|
|
// Save to temp file
|
|
tempDir := os.TempDir()
|
|
timestamp := time.Now().Format("20060102_150405")
|
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s_%s", timestamp, filename))
|
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
|
a.openImagesMux.Lock()
|
|
delete(a.openImages, filename)
|
|
a.openImagesMux.Unlock()
|
|
return fmt.Errorf("failed to write temp file: %w", err)
|
|
}
|
|
|
|
// Launch new EMLy instance in image viewer mode
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
a.openImagesMux.Lock()
|
|
delete(a.openImages, filename)
|
|
a.openImagesMux.Unlock()
|
|
return fmt.Errorf("failed to get executable path: %w", err)
|
|
}
|
|
|
|
cmd := exec.Command(exe, "--view-image="+tempFile)
|
|
if err := cmd.Start(); err != nil {
|
|
a.openImagesMux.Lock()
|
|
delete(a.openImages, filename)
|
|
a.openImagesMux.Unlock()
|
|
return fmt.Errorf("failed to start viewer: %w", err)
|
|
}
|
|
|
|
// Monitor process in background to release lock when closed
|
|
go func() {
|
|
cmd.Wait()
|
|
a.openImagesMux.Lock()
|
|
delete(a.openImages, filename)
|
|
a.openImagesMux.Unlock()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// OpenPDFWindow opens a PDF attachment in a new EMLy viewer window.
|
|
// The PDF data is saved to a temp file and a new EMLy instance is launched
|
|
// with the --view-pdf flag.
|
|
//
|
|
// This method tracks open PDFs to prevent duplicate windows for the same file.
|
|
//
|
|
// Parameters:
|
|
// - base64Data: Base64-encoded PDF data
|
|
// - filename: The original filename of the PDF
|
|
//
|
|
// Returns:
|
|
// - error: Error if the PDF is already open or if launching fails
|
|
func (a *App) OpenPDFWindow(base64Data string, filename string) error {
|
|
// Check if this PDF is already open
|
|
a.openPDFsMux.Lock()
|
|
if a.openPDFs[filename] {
|
|
a.openPDFsMux.Unlock()
|
|
return fmt.Errorf("pdf '%s' is already open", filename)
|
|
}
|
|
a.openPDFs[filename] = true
|
|
a.openPDFsMux.Unlock()
|
|
|
|
// Decode base64 data
|
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
|
if err != nil {
|
|
a.openPDFsMux.Lock()
|
|
delete(a.openPDFs, filename)
|
|
a.openPDFsMux.Unlock()
|
|
return fmt.Errorf("failed to decode base64: %w", err)
|
|
}
|
|
|
|
// Save to temp file
|
|
tempDir := os.TempDir()
|
|
tempFile := filepath.Join(tempDir, filename)
|
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
|
a.openPDFsMux.Lock()
|
|
delete(a.openPDFs, filename)
|
|
a.openPDFsMux.Unlock()
|
|
return fmt.Errorf("failed to write temp file: %w", err)
|
|
}
|
|
|
|
// Launch new EMLy instance in PDF viewer mode
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
a.openPDFsMux.Lock()
|
|
delete(a.openPDFs, filename)
|
|
a.openPDFsMux.Unlock()
|
|
return fmt.Errorf("failed to get executable path: %w", err)
|
|
}
|
|
|
|
cmd := exec.Command(exe, "--view-pdf="+tempFile)
|
|
if err := cmd.Start(); err != nil {
|
|
a.openPDFsMux.Lock()
|
|
delete(a.openPDFs, filename)
|
|
a.openPDFsMux.Unlock()
|
|
return fmt.Errorf("failed to start viewer: %w", err)
|
|
}
|
|
|
|
// Monitor process in background to release lock when closed
|
|
go func() {
|
|
cmd.Wait()
|
|
a.openPDFsMux.Lock()
|
|
delete(a.openPDFs, filename)
|
|
a.openPDFsMux.Unlock()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// External Application Methods
|
|
// =============================================================================
|
|
|
|
// OpenPDF saves a PDF to temp and opens it with the system's default PDF application.
|
|
// This is used when the user prefers external viewers over the built-in viewer.
|
|
//
|
|
// Parameters:
|
|
// - base64Data: Base64-encoded PDF data
|
|
// - filename: The original filename of the PDF
|
|
//
|
|
// Returns:
|
|
// - error: Error if saving or launching fails
|
|
func (a *App) OpenPDF(base64Data string, filename string) error {
|
|
if base64Data == "" {
|
|
return fmt.Errorf("no data provided")
|
|
}
|
|
|
|
// Decode base64 data
|
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode base64: %w", err)
|
|
}
|
|
|
|
// Save to temp file with timestamp for uniqueness
|
|
tempDir := os.TempDir()
|
|
timestamp := time.Now().Format("20060102_150405")
|
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s_%s", timestamp, filename))
|
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write temp file: %w", err)
|
|
}
|
|
|
|
// Open with Windows default application
|
|
cmd := exec.Command("cmd", "/c", "start", "", tempFile)
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// OpenImage saves an image to temp and opens it with the system's default image viewer.
|
|
// This is used when the user prefers external viewers over the built-in viewer.
|
|
//
|
|
// Parameters:
|
|
// - base64Data: Base64-encoded image data
|
|
// - filename: The original filename of the image
|
|
//
|
|
// Returns:
|
|
// - error: Error if saving or launching fails
|
|
func (a *App) OpenImage(base64Data string, filename string) error {
|
|
if base64Data == "" {
|
|
return fmt.Errorf("no data provided")
|
|
}
|
|
|
|
// Decode base64 data
|
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode base64: %w", err)
|
|
}
|
|
|
|
// Save to temp file with timestamp for uniqueness
|
|
tempDir := os.TempDir()
|
|
timestamp := time.Now().Format("20060102_150405")
|
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s_%s", timestamp, filename))
|
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write temp file: %w", err)
|
|
}
|
|
|
|
// Open with Windows default application
|
|
cmd := exec.Command("cmd", "/c", "start", "", tempFile)
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Viewer Mode Detection
|
|
// =============================================================================
|
|
|
|
// GetImageViewerData checks CLI arguments and returns image data if running in image viewer mode.
|
|
// This is called by the viewer page on startup to get the image to display.
|
|
//
|
|
// Returns:
|
|
// - *ImageViewerData: Image data if in viewer mode, nil otherwise
|
|
// - error: Error if reading the image file fails
|
|
func (a *App) GetImageViewerData() (*ImageViewerData, error) {
|
|
for _, arg := range os.Args {
|
|
if strings.HasPrefix(arg, "--view-image=") {
|
|
filePath := strings.TrimPrefix(arg, "--view-image=")
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read image file: %w", err)
|
|
}
|
|
// Return as base64 for consistent frontend handling
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
return &ImageViewerData{
|
|
Data: encoded,
|
|
Filename: filepath.Base(filePath),
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// GetPDFViewerData checks CLI arguments and returns PDF data if running in PDF viewer mode.
|
|
// This is called by the viewer page on startup to get the PDF to display.
|
|
//
|
|
// Returns:
|
|
// - *PDFViewerData: PDF data if in viewer mode, nil otherwise
|
|
// - error: Error if reading the PDF file fails
|
|
func (a *App) GetPDFViewerData() (*PDFViewerData, error) {
|
|
for _, arg := range os.Args {
|
|
if strings.HasPrefix(arg, "--view-pdf=") {
|
|
filePath := strings.TrimPrefix(arg, "--view-pdf=")
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read PDF file: %w", err)
|
|
}
|
|
// Return as base64 for consistent frontend handling
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
return &PDFViewerData{
|
|
Data: encoded,
|
|
Filename: filepath.Base(filePath),
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// GetViewerData checks CLI arguments and returns viewer data for any viewer mode.
|
|
// This is a unified method that detects both image and PDF viewer modes.
|
|
//
|
|
// Returns:
|
|
// - *ViewerData: Contains either ImageData or PDFData depending on mode
|
|
// - error: Error if reading the file fails
|
|
func (a *App) GetViewerData() (*ViewerData, error) {
|
|
for _, arg := range os.Args {
|
|
// Check for image viewer mode
|
|
if strings.HasPrefix(arg, "--view-image=") {
|
|
filePath := strings.TrimPrefix(arg, "--view-image=")
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read image file: %w", err)
|
|
}
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
return &ViewerData{
|
|
ImageData: &ImageViewerData{
|
|
Data: encoded,
|
|
Filename: filepath.Base(filePath),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Check for PDF viewer mode
|
|
if strings.HasPrefix(arg, "--view-pdf=") {
|
|
filePath := strings.TrimPrefix(arg, "--view-pdf=")
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read PDF file: %w", err)
|
|
}
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
return &ViewerData{
|
|
PDFData: &PDFViewerData{
|
|
Data: encoded,
|
|
Filename: filepath.Base(filePath),
|
|
},
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|