Files
EMLy/app_viewer.go
2026-02-05 22:41:02 +01:00

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
}