/** * @fileoverview Electron Platform Service Implementation * @author Matthew Raymer * * Provides platform-specific functionality for Electron desktop applications. * This service extends CapacitorPlatformService to leverage Capacitor's APIs * while overriding desktop-specific behaviors. * * Key Features: * - Desktop-specific capabilities configuration * - Native IPC-based file operations with proper security * - Direct saving to user's Downloads folder via main process * - Native desktop integration support * * Architecture: * - Extends CapacitorPlatformService for API compatibility * - Overrides methods for desktop-specific implementations * - Maintains cross-platform service interface * * @since 1.0.0 */ import { CapacitorPlatformService } from "./CapacitorPlatformService"; import { logger } from "../../utils/logger"; /** * Electron-specific platform service implementation. * * This service handles the unique requirements of the Electron platform: * - Desktop-specific capabilities and UI patterns * - File system operations using Capacitor's Filesystem API * - Native desktop integration features * - Proper error handling with web fallbacks * * @extends CapacitorPlatformService * @example * ```typescript * const electronService = new ElectronPlatformService(); * await electronService.writeAndShareFile('backup.json', jsonData); * ``` */ export class ElectronPlatformService extends CapacitorPlatformService { /** * Gets the capabilities of the Electron platform. * Overrides the mobile-focused capabilities from CapacitorPlatformService * to provide desktop-specific feature flags. * * @returns Platform capabilities object specific to Electron */ getCapabilities() { return { hasFileSystem: true, hasCamera: false, // Desktop typically doesn't have integrated cameras for our use case isMobile: false, // Electron is desktop, not mobile isIOS: false, hasFileDownload: true, // Desktop supports direct file downloads needsFileHandlingInstructions: false, // Desktop users are familiar with file handling isNativeApp: true, // Electron is a native app }; } /** * Handles file export for Electron platform using native IPC. * * This method provides a secure, native file export mechanism that: * 1. Uses Electron's IPC (Inter-Process Communication) for secure file operations * 2. Writes files directly to the user's Downloads folder via the main process * 3. Provides exact file path feedback and proper error handling * 4. Falls back to web-style downloads if IPC is unavailable * * @param fileName - The name of the file to save (with date stamp) * @param content - The content to write to the file * @returns Promise that resolves when the file is successfully saved * @throws {Error} If both native IPC and fallback mechanisms fail * * @example * ```typescript * await electronService.writeAndShareFile('TimeSafari-backup-contacts-2025-07-06.json', jsonData); * ``` * * @note This implementation follows Electron's security best practices by: * - Using contextBridge to expose safe IPC methods * - Handling file operations in the main process with full filesystem access * - Providing exact file paths for better user experience * - Maintaining secure separation between renderer and main processes */ async writeAndShareFile(fileName: string, content: string): Promise { logger.info( `[ElectronPlatformService] Using native IPC for reliable file export: ${fileName}`, ); try { // Check if we're running in Electron with the API available if (typeof window !== "undefined" && window.electronAPI) { // Use the native Electron IPC API for file exports const result = await window.electronAPI.exportData(fileName, content); if (result.success) { logger.info( `[ElectronPlatformService] File exported successfully to: ${result.path}`, ); logger.info( `[ElectronPlatformService] File saved to Downloads folder: ${fileName}`, ); } else { logger.error( `[ElectronPlatformService] Native export failed: ${result.error}`, ); throw new Error(`Native file export failed: ${result.error}`); } } else { // Fallback to web-style download if Electron API is not available logger.warn( "[ElectronPlatformService] Electron API not available, falling back to web download", ); const blob = new Blob([content], { type: "application/json" }); const url = URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = url; downloadLink.download = fileName; downloadLink.style.display = "none"; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); setTimeout(() => URL.revokeObjectURL(url), 1000); logger.info( `[ElectronPlatformService] Fallback download initiated: ${fileName}`, ); } } catch (error) { logger.error("[ElectronPlatformService] File export failed:", error); throw new Error(`Failed to export file: ${error}`); } } /** * Checks if running on Electron platform. * * @returns true, as this is the Electron implementation */ isElectron(): boolean { return true; } /** * Saves content directly to the device's Downloads folder (Electron platform). * Uses Electron's IPC to save files directly to the Downloads directory. * * @param fileName - The filename of the file to save * @param content - The content to write to the file * @returns Promise that resolves when the file is saved */ async saveToDevice(fileName: string, content: string): Promise { try { // Ensure content is valid JSON try { JSON.parse(content); } catch { throw new Error('Content must be valid JSON'); } // Generate unique filename const uniqueFileName = this.generateUniqueFileNameElectron(fileName); logger.info( `[ElectronPlatformService] Using native IPC for direct file save`, uniqueFileName, ); // Check if we're running in Electron with the API available if (typeof window !== "undefined" && window.electronAPI) { // Use the native Electron IPC API for file exports const result = await window.electronAPI.exportData(uniqueFileName, content); if (result.success) { logger.info( `[ElectronPlatformService] File saved successfully to`, result.path, ); logger.info( `[ElectronPlatformService] File saved to Downloads folder`, uniqueFileName, ); } else { logger.error( `[ElectronPlatformService] Native save failed`, result.error, ); throw new Error(`Native file save failed: ${result.error}`); } } else { // Fallback to web-style download if Electron API is not available logger.warn( "[ElectronPlatformService] Electron API not available, falling back to web download", ); const blob = new Blob([content], { type: "application/json" }); const url = URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = url; downloadLink.download = uniqueFileName; downloadLink.style.display = "none"; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); setTimeout(() => URL.revokeObjectURL(url), 1000); logger.info( `[ElectronPlatformService] Fallback download initiated`, uniqueFileName, ); } } catch (error) { logger.error("[ElectronPlatformService] File save failed", error); throw new Error(`Failed to save file: ${error}`); } } /** * Opens the system file picker to let the user choose where to save a file (Electron platform). * Uses Electron's IPC to show the native save dialog. * * @param fileName - The suggested filename for the file * @param content - The content to write to the file * @returns Promise that resolves when the file is saved */ async saveAs(fileName: string, content: string): Promise { try { // Ensure content is valid JSON try { JSON.parse(content); } catch { throw new Error('Content must be valid JSON'); } // Generate unique filename const uniqueFileName = this.generateUniqueFileNameElectron(fileName); logger.info( `[ElectronPlatformService] Using native IPC for save as dialog`, uniqueFileName, ); // Check if we're running in Electron with the API available if (typeof window !== "undefined" && window.electronAPI) { // Use the native Electron IPC API for file exports (same as saveToDevice for now) // TODO: Implement native save dialog when available const result = await window.electronAPI.exportData(uniqueFileName, content); if (result.success) { logger.info( `[ElectronPlatformService] File saved successfully to`, result.path, ); logger.info( `[ElectronPlatformService] File saved via save as`, uniqueFileName, ); } else { logger.error( `[ElectronPlatformService] Native save as failed`, result.error, ); throw new Error(`Native file save as failed: ${result.error}`); } } else { // Fallback to web-style download if Electron API is not available logger.warn( "[ElectronPlatformService] Electron API not available, falling back to web download", ); const blob = new Blob([content], { type: "application/json" }); const url = URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = url; downloadLink.download = uniqueFileName; downloadLink.style.display = "none"; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); setTimeout(() => URL.revokeObjectURL(url), 1000); logger.info( `[ElectronPlatformService] Fallback download initiated`, uniqueFileName, ); } } catch (error) { logger.error("[ElectronPlatformService] File save as failed", error); throw new Error(`Failed to save file as: ${error}`); } } /** * Generates unique filename with timestamp, hashed device ID, and counter */ private generateUniqueFileNameElectron(baseName: string, counter = 0): string { const now = new Date(); const timestamp = now.toISOString() .replace(/[:.]/g, '-') .replace('T', '_') .replace('Z', ''); const deviceIdHash = this.getHashedDeviceIdentifierElectron(); const counterSuffix = counter > 0 ? `_${counter}` : ''; const maxBaseLength = 45; const truncatedBase = baseName.length > maxBaseLength ? baseName.substring(0, maxBaseLength) : baseName; const nameWithoutExt = truncatedBase.replace(/\.json$/i, ''); const extension = '.json'; const devicePart = `_${deviceIdHash}`; const timestampPart = `_${timestamp}${counterSuffix}`; const totalLength = nameWithoutExt.length + devicePart.length + timestampPart.length + extension.length; if (totalLength > 200) { const availableLength = 200 - devicePart.length - timestampPart.length - extension.length; const finalBase = nameWithoutExt.substring(0, Math.max(10, availableLength)); return `${finalBase}${devicePart}${timestampPart}${extension}`; } return `${nameWithoutExt}${devicePart}${timestampPart}${extension}`; } /** * Gets hashed device identifier */ private getHashedDeviceIdentifierElectron(): string { try { const deviceInfo = this.getDeviceInfoElectron(); return this.hashStringElectron(deviceInfo); } catch (error) { return 'electron'; } } /** * Gets device info string */ private getDeviceInfoElectron(): string { try { // Use machine-specific information const os = require('os'); const hostname = os.hostname() || 'unknown'; const platform = os.platform() || 'unknown'; const arch = os.arch() || 'unknown'; // Create device info string return `${platform}_${hostname}_${arch}`; } catch (error) { return 'electron'; } } /** * Simple hash function for device ID */ private hashStringElectron(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(16).padStart(4, '0').substring(0, 4); } /** * Checks if running on Capacitor platform. * * @returns false, as this is Electron, not pure Capacitor */ isCapacitor(): boolean { return false; } /** * Checks if running on web platform. * * @returns false, as this is not web */ isWeb(): boolean { return false; } // --- PWA/Web-only methods (no-op for Electron) --- public registerServiceWorker(): void {} }