WIP: add Electron platform configuration to Capacitor

- Add electron platform section to capacitor.config.json
- Configure deep linking with timesafari:// scheme
- Set up build options for macOS, Windows, and Linux
- Configure output directory and file inclusion
- Add platform-specific build targets (DMG, NSIS, AppImage)
- Support both x64 and arm64 architectures for macOS
- Set appropriate app categories for each platform

This enables building TimeSafari as a native desktop application
using Capacitor's Electron platform while maintaining existing
mobile and web functionality.
This commit is contained in:
Matthew Raymer
2025-06-25 12:50:46 +00:00
parent ca014a52de
commit ea0f49d5c3
29 changed files with 420 additions and 1987 deletions

View File

@@ -1,174 +0,0 @@
const { app, BrowserWindow } = require("electron");
const path = require("path");
const fs = require("fs");
const logger = require("../utils/logger");
// Check if running in dev mode
const isDev = process.argv.includes("--inspect");
function createWindow() {
// Add before createWindow function
const preloadPath = path.join(__dirname, "preload.js");
logger.log("Checking preload path:", preloadPath);
logger.log("Preload exists:", fs.existsSync(preloadPath));
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
allowRunningInsecureContent: false,
preload: path.join(__dirname, "preload.js"),
},
});
// Always open DevTools for now
mainWindow.webContents.openDevTools();
// Intercept requests to fix asset paths
mainWindow.webContents.session.webRequest.onBeforeRequest(
{
urls: [
"file://*/*/assets/*",
"file://*/assets/*",
"file:///assets/*", // Catch absolute paths
"<all_urls>", // Catch all URLs as a fallback
],
},
(details, callback) => {
let url = details.url;
// Handle paths that don't start with file://
if (!url.startsWith("file://") && url.includes("/assets/")) {
url = `file://${path.join(__dirname, "www", url)}`;
}
// Handle absolute paths starting with /assets/
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
const baseDir = url.includes("dist-electron")
? url.substring(
0,
url.indexOf("/dist-electron") + "/dist-electron".length,
)
: `file://${__dirname}`;
const assetPath = url.split("/assets/")[1];
const newUrl = `${baseDir}/www/assets/${assetPath}`;
callback({ redirectURL: newUrl });
return;
}
callback({}); // No redirect for other URLs
},
);
if (isDev) {
// Debug info
logger.log("Debug Info:");
logger.log("Running in dev mode:", isDev);
logger.log("App is packaged:", app.isPackaged);
logger.log("Process resource path:", process.resourcesPath);
logger.log("App path:", app.getAppPath());
logger.log("__dirname:", __dirname);
logger.log("process.cwd():", process.cwd());
}
const indexPath = path.join(__dirname, "www", "index.html");
if (isDev) {
logger.log("Loading index from:", indexPath);
logger.log("www path:", path.join(__dirname, "www"));
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
}
if (!fs.existsSync(indexPath)) {
logger.error(`Index file not found at: ${indexPath}`);
throw new Error("Index file not found");
}
// Add CSP headers to allow API connections
mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
"Content-Security-Policy": [
"default-src 'self';" +
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" +
"img-src 'self' data: https: blob:;" +
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
"style-src 'self' 'unsafe-inline';" +
"font-src 'self' data:;",
],
},
});
},
);
// Load the index.html
mainWindow
.loadFile(indexPath)
.then(() => {
logger.log("Successfully loaded index.html");
if (isDev) {
mainWindow.webContents.openDevTools();
logger.log("DevTools opened - running in dev mode");
}
})
.catch((err) => {
logger.error("Failed to load index.html:", err);
logger.error("Attempted path:", indexPath);
});
// Listen for console messages from the renderer
mainWindow.webContents.on("console-message", (_event, level, message) => {
logger.log("Renderer Console:", message);
});
// Add right after creating the BrowserWindow
mainWindow.webContents.on(
"did-fail-load",
(event, errorCode, errorDescription) => {
logger.error("Page failed to load:", errorCode, errorDescription);
},
);
mainWindow.webContents.on("preload-error", (event, preloadPath, error) => {
logger.error("Preload script error:", preloadPath, error);
});
mainWindow.webContents.on(
"console-message",
(event, level, message, line, sourceId) => {
logger.log("Renderer Console:", line, sourceId, message);
},
);
// Enable remote debugging when in dev mode
if (isDev) {
mainWindow.webContents.openDevTools();
}
}
// Handle app ready
app.whenReady().then(createWindow);
// Handle all windows closed
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// Handle any errors
process.on("uncaughtException", (error) => {
logger.error("Uncaught Exception:", error);
});

View File

@@ -1,221 +0,0 @@
import { app, BrowserWindow } from "electron";
import path from "path";
import fs from "fs";
// Simple logger implementation
const logger = {
// eslint-disable-next-line no-console
log: (...args: unknown[]) => console.log(...args),
// eslint-disable-next-line no-console
error: (...args: unknown[]) => console.error(...args),
// eslint-disable-next-line no-console
info: (...args: unknown[]) => console.info(...args),
// eslint-disable-next-line no-console
warn: (...args: unknown[]) => console.warn(...args),
// eslint-disable-next-line no-console
debug: (...args: unknown[]) => console.debug(...args),
};
// Check if running in dev mode
const isDev = process.argv.includes("--inspect");
async function createWindow(): Promise<void> {
// Add before createWindow function
const preloadPath = app.isPackaged
? path.join(app.getAppPath(), "dist-electron", "preload.js")
: path.join(__dirname, "preload.js");
logger.log("Checking preload path:", preloadPath);
logger.log("Preload exists:", fs.existsSync(preloadPath));
// Log environment and paths
logger.log("process.cwd():", process.cwd());
logger.log("__dirname:", __dirname);
logger.log("app.getAppPath():", app.getAppPath());
logger.log("app.isPackaged:", app.isPackaged);
// List files in __dirname and __dirname/www
try {
logger.log("Files in __dirname:", fs.readdirSync(__dirname));
const wwwDir = path.join(__dirname, "www");
if (fs.existsSync(wwwDir)) {
logger.log("Files in www:", fs.readdirSync(wwwDir));
} else {
logger.log("www directory does not exist in __dirname");
}
} catch (e) {
logger.error("Error reading directories:", e);
}
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
allowRunningInsecureContent: false,
preload: preloadPath,
},
});
// Always open DevTools for now
mainWindow.webContents.openDevTools();
// Intercept requests to robustly fix asset paths for Electron
mainWindow.webContents.session.webRequest.onBeforeRequest(
{ urls: ["*://*/*"] },
(details, callback) => {
const url = details.url;
logger.log('[main.ts] Intercepted request:', url);
// Match both file:///assets/... and /assets/...
const assetMatch = url.match(/(?:file:\/\/\/|file:\/\/|https?:\/\/[^\/]+)?\/assets\/(.+)/);
if (assetMatch) {
const assetRelPath = assetMatch[1];
const assetAbsPath = path.join(app.getAppPath(), "dist-electron", "www", "assets", assetRelPath);
logger.log('[main.ts] Asset request detected:', {
originalUrl: url,
assetRelPath,
assetAbsPath,
exists: fs.existsSync(assetAbsPath)
});
if (fs.existsSync(assetAbsPath)) {
const newUrl = `file://${assetAbsPath}`;
logger.log('[main.ts] Redirecting to:', newUrl);
callback({ redirectURL: newUrl });
return;
} else {
logger.error('[main.ts] Asset file not found:', assetAbsPath);
}
}
callback({});
}
);
if (isDev) {
// Debug info
logger.log("Debug Info:");
logger.log("Running in dev mode:", isDev);
logger.log("App is packaged:", app.isPackaged);
logger.log("Process resource path:", process.resourcesPath);
logger.log("App path:", app.getAppPath());
logger.log("__dirname:", __dirname);
logger.log("process.cwd():", process.cwd());
}
let indexPath: string;
if (app.isPackaged) {
indexPath = path.join(
app.getAppPath(),
"dist-electron",
"www",
"index.html",
);
logger.log("[main.ts] Using packaged indexPath:", indexPath);
} else {
indexPath = path.resolve(
process.cwd(),
"dist-electron",
"www",
"index.html",
);
logger.log("[main.ts] Using dev indexPath:", indexPath);
if (!fs.existsSync(indexPath)) {
logger.error("[main.ts] Dev index.html not found:", indexPath);
throw new Error("Index file not found");
}
}
if (isDev) {
logger.log("Loading index from:", indexPath);
logger.log("www path:", path.join(__dirname, "www"));
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
}
// Add CSP headers to allow API connections
mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
"Content-Security-Policy": [
"default-src 'self';" +
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" +
"img-src 'self' data: https: blob:;" +
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
"style-src 'self' 'unsafe-inline';" +
"font-src 'self' data:;",
],
},
});
},
);
// Load the index.html with the correct base URL for assets
const baseURL = `file://${path.dirname(indexPath)}/`;
logger.log('[main.ts] Loading with base URL:', baseURL);
try {
await mainWindow.loadURL(`file://${indexPath}`);
logger.log("Successfully loaded index.html");
if (isDev) {
mainWindow.webContents.openDevTools();
logger.log("DevTools opened - running in dev mode");
}
} catch (err) {
logger.error("Failed to load index.html:", err);
logger.error("Attempted path:", indexPath);
}
// Listen for console messages from the renderer
mainWindow.webContents.on("console-message", (_event, _level, message) => {
logger.log("Renderer Console:", message);
});
// Add right after creating the BrowserWindow
mainWindow.webContents.on(
"did-fail-load",
(_event, errorCode, errorDescription) => {
logger.error("Page failed to load:", errorCode, errorDescription);
},
);
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
logger.error("Preload script error:", preloadPath, error);
});
mainWindow.webContents.on(
"console-message",
(_event, _level, message, line, sourceId) => {
logger.log("Renderer Console:", line, sourceId, message);
},
);
// Enable remote debugging when in dev mode
if (isDev) {
mainWindow.webContents.openDevTools();
}
}
// Handle app ready
app.whenReady().then(createWindow);
// Handle all windows closed
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// Handle any errors
process.on("uncaughtException", (error) => {
logger.error("Uncaught Exception:", error);
});

View File

@@ -1,91 +0,0 @@
const { contextBridge, ipcRenderer } = require("electron");
const logger = {
log: (message, ...args) => {
// Always log in development, log with context in production
if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */
console.log(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
}
},
warn: (message, ...args) => {
// Always log warnings
/* eslint-disable no-console */
console.warn(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
},
error: (message, ...args) => {
// Always log errors
/* eslint-disable no-console */
console.error(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
},
info: (message, ...args) => {
// Always log info in development, log with context in production
if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */
console.info(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
}
},
};
// Use a more direct path resolution approach
const getPath = (pathType) => {
switch (pathType) {
case "userData":
return (
process.env.APPDATA ||
(process.platform === "darwin"
? `${process.env.HOME}/Library/Application Support`
: `${process.env.HOME}/.local/share`)
);
case "home":
return process.env.HOME;
case "appPath":
return process.resourcesPath;
default:
return "";
}
};
logger.info("Preload script starting...");
// Force electron platform in the renderer process
window.process = { env: { VITE_PLATFORM: "electron" } };
try {
contextBridge.exposeInMainWorld("electronAPI", {
// Path utilities
getPath,
// IPC functions
send: (channel, data) => {
const validChannels = ["toMain"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
const validChannels = ["fromMain"];
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
// Environment info
env: {
isElectron: true,
isDev: process.env.NODE_ENV === "development",
platform: "electron", // Explicitly set platform
},
// Path utilities
getBasePath: () => {
return process.env.NODE_ENV === "development" ? "/" : "./";
},
});
logger.info("Preload script completed successfully");
} catch (error) {
logger.error("Error in preload script:", error);
}

View File

@@ -1,17 +0,0 @@
import './assets/styles/tailwind.css';
import { initializeApp } from "./main.common";
import { logger } from "./utils/logger";
const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
logger.info("[Electron] Initializing app");
logger.info("[Electron] Platform:", { platform });
logger.info("[Electron] PWA enabled:", { pwa_enabled });
if (pwa_enabled) {
logger.warn("[Electron] PWA is enabled, but not supported in electron");
}
const app = initializeApp();
app.mount("#app");

View File

@@ -6,7 +6,7 @@ const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
// Only import service worker for web builds
if (platform !== "electron" && pwa_enabled) {
if (pwa_enabled) {
import("./registerServiceWorker"); // Web PWA support
}

View File

@@ -2,18 +2,10 @@
import { register } from "register-service-worker";
// Check if we're in an Electron environment
const isElectron =
process.env.VITE_PLATFORM === "electron" ||
process.env.VITE_DISABLE_PWA === "true" ||
window.navigator.userAgent.toLowerCase().includes("electron");
// Only register service worker if:
// 1. Not in Electron
// 2. PWA is explicitly enabled
// 3. In production mode
// 1. PWA is explicitly enabled
// 2. In production mode
if (
!isElectron &&
process.env.VITE_PWA_ENABLED === "true" &&
process.env.NODE_ENV === "production"
) {
@@ -45,11 +37,9 @@ if (
} else {
console.log(
`Service worker registration skipped - ${
isElectron
? "running in Electron"
: process.env.VITE_PWA_ENABLED !== "true"
? "PWA not enabled"
: "not in production mode"
process.env.VITE_PWA_ENABLED !== "true"
? "PWA not enabled"
: "not in production mode"
}`,
);
}

View File

@@ -277,14 +277,9 @@ const routes: Array<RouteRecordRaw> = [
},
];
const isElectron = window.location.protocol === "file:";
const initialPath = isElectron
? window.location.pathname.split("/dist-electron/www/")[1] || "/"
: window.location.pathname;
const initialPath = window.location.pathname;
const history = isElectron
? createMemoryHistory() // Memory history for Electron
: createWebHistory("/"); // Add base path for web apps
const history = createWebHistory("/"); // Add base path for web apps
/** @type {*} */
const router = createRouter({

View File

@@ -1,7 +1,6 @@
import { PlatformService } from "./PlatformService";
import { WebPlatformService } from "./platforms/WebPlatformService";
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
/**
* Factory class for creating platform-specific service implementations.
@@ -10,7 +9,6 @@ import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
* The factory determines which platform implementation to use based on the VITE_PLATFORM
* environment variable. Supported platforms are:
* - capacitor: Mobile platform using Capacitor
* - electron: Desktop platform using Electron
* - web: Default web platform (fallback)
*
* @example
@@ -39,9 +37,6 @@ export class PlatformServiceFactory {
case "capacitor":
PlatformServiceFactory.instance = new CapacitorPlatformService();
break;
case "electron":
PlatformServiceFactory.instance = new ElectronPlatformService();
break;
case "web":
default:
PlatformServiceFactory.instance = new WebPlatformService();

View File

@@ -244,13 +244,15 @@ export class CapacitorPlatformService implements PlatformService {
* @returns Platform capabilities object
*/
getCapabilities(): PlatformCapabilities {
const platform = Capacitor.getPlatform();
return {
hasFileSystem: true,
hasCamera: true,
isMobile: true,
isIOS: Capacitor.getPlatform() === "ios",
hasFileDownload: false,
needsFileHandlingInstructions: true,
isMobile: true, // Capacitor is always mobile
isIOS: platform === "ios",
hasFileDownload: false, // Mobile platforms need sharing
needsFileHandlingInstructions: true, // Mobile needs instructions
isNativeApp: true,
};
}

View File

@@ -1,358 +0,0 @@
import {
ImageResult,
PlatformService,
PlatformCapabilities,
} from "../PlatformService";
import { logger } from "../../utils/logger";
import { QueryExecResult, SqlValue } from "@/interfaces/database";
import {
SQLiteConnection,
SQLiteDBConnection,
CapacitorSQLite,
Changes,
} from "@capacitor-community/sqlite";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
interface Migration {
name: string;
sql: string;
}
/**
* Platform service implementation for Electron (desktop) platform.
* Provides native desktop functionality through Electron and Capacitor plugins for:
* - File system operations (TODO)
* - Camera integration (TODO)
* - SQLite database operations
* - System-level features (TODO)
*/
export class ElectronPlatformService implements PlatformService {
private sqlite: SQLiteConnection;
private db: SQLiteDBConnection | null = null;
private dbName = "timesafari.db";
private initialized = false;
constructor() {
this.sqlite = new SQLiteConnection(CapacitorSQLite);
}
private async initializeDatabase(): Promise<void> {
if (this.initialized) {
return;
}
try {
// Create/Open database
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
false,
);
await this.db.open();
// Set journal mode to WAL for better performance
await this.db.execute("PRAGMA journal_mode=WAL;");
// Run migrations
await this.runMigrations();
this.initialized = true;
logger.log(
"[ElectronPlatformService] SQLite database initialized successfully",
);
} catch (error) {
logger.error(
"[ElectronPlatformService] Error initializing SQLite database:",
error,
);
throw new Error(
"[ElectronPlatformService] Failed to initialize database",
);
}
}
private async runMigrations(): Promise<void> {
if (!this.db) {
throw new Error("Database not initialized");
}
// Create migrations table if it doesn't exist
await this.db.execute(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
// Get list of executed migrations
const result = await this.db.query("SELECT name FROM migrations;");
const executedMigrations = new Set(
result.values?.map((row) => row[0]) || [],
);
// Run pending migrations in order
const migrations: Migration[] = [
{
name: "001_initial",
sql: `
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dateCreated TEXT NOT NULL,
derivationPath TEXT,
did TEXT NOT NULL,
identityEncrBase64 TEXT,
mnemonicEncrBase64 TEXT,
passkeyCredIdHex TEXT,
publicKeyHex TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
CREATE TABLE IF NOT EXISTS secret (
id INTEGER PRIMARY KEY AUTOINCREMENT,
secretBase64 TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
accountDid TEXT,
activeDid TEXT,
apiServer TEXT,
filterFeedByNearby BOOLEAN,
filterFeedByVisible BOOLEAN,
finishedOnboarding BOOLEAN,
firstName TEXT,
hideRegisterPromptOnNewContact BOOLEAN,
isRegistered BOOLEAN,
lastName TEXT,
lastAckedOfferToUserJwtId TEXT,
lastAckedOfferToUserProjectsJwtId TEXT,
lastNotifiedClaimId TEXT,
lastViewedClaimId TEXT,
notifyingNewActivityTime TEXT,
notifyingReminderMessage TEXT,
notifyingReminderTime TEXT,
partnerApiServer TEXT,
passkeyExpirationMinutes INTEGER,
profileImageUrl TEXT,
searchBoxes TEXT,
showContactGivesInline BOOLEAN,
showGeneralAdvanced BOOLEAN,
showShortcutBvc BOOLEAN,
vapid TEXT,
warnIfProdServer BOOLEAN,
warnIfTestServer BOOLEAN,
webPushServer TEXT
);
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
did TEXT NOT NULL,
name TEXT,
contactMethods TEXT,
nextPubKeyHashB64 TEXT,
notes TEXT,
profileImageUrl TEXT,
publicKeyBase64 TEXT,
seesMe BOOLEAN,
registered BOOLEAN
);
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
CREATE TABLE IF NOT EXISTS logs (
date TEXT PRIMARY KEY,
message TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS temp (
id TEXT PRIMARY KEY,
blobB64 TEXT
);
`,
},
];
for (const migration of migrations) {
if (!executedMigrations.has(migration.name)) {
await this.db.execute(migration.sql);
await this.db.run("INSERT INTO migrations (name) VALUES (?)", [
migration.name,
]);
logger.log(`Migration ${migration.name} executed successfully`);
}
}
}
/**
* Gets the capabilities of the Electron platform
* @returns Platform capabilities object
*/
getCapabilities(): PlatformCapabilities {
return {
hasFileSystem: false, // Not implemented yet
hasCamera: false, // Not implemented yet
isMobile: false,
isIOS: false,
hasFileDownload: false, // Not implemented yet
needsFileHandlingInstructions: false,
isNativeApp: true, // Electron is a native app
};
}
/**
* Reads a file from the filesystem.
* @param _path - Path to the file to read
* @returns Promise that should resolve to file contents
* @throws Error with "Not implemented" message
* @todo Implement file reading using Electron's file system API
*/
async readFile(_path: string): Promise<string> {
throw new Error("Not implemented");
}
/**
* Writes content to a file.
* @param _path - Path where to write the file
* @param _content - Content to write to the file
* @throws Error with "Not implemented" message
* @todo Implement file writing using Electron's file system API
*/
async writeFile(_path: string, _content: string): Promise<void> {
throw new Error("Not implemented");
}
/**
* Writes content to a file and opens the system share dialog.
* @param _fileName - Name of the file to create
* @param _content - Content to write to the file
* @throws Error with "Not implemented" message
* @todo Implement using Electron's dialog and file system APIs
*/
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
throw new Error("Not implemented");
}
/**
* Deletes a file from the filesystem.
* @param _path - Path to the file to delete
* @throws Error with "Not implemented" message
* @todo Implement file deletion using Electron's file system API
*/
async deleteFile(_path: string): Promise<void> {
throw new Error("Not implemented");
}
/**
* Lists files in the specified directory.
* @param _directory - Path to the directory to list
* @returns Promise that should resolve to array of filenames
* @throws Error with "Not implemented" message
* @todo Implement directory listing using Electron's file system API
*/
async listFiles(_directory: string): Promise<string[]> {
throw new Error("Not implemented");
}
/**
* Should open system camera to take a picture.
* @returns Promise that should resolve to captured image data
* @throws Error with "Not implemented" message
* @todo Implement camera access using Electron's media APIs
*/
async takePicture(): Promise<ImageResult> {
logger.error("takePicture not implemented in Electron platform");
throw new Error("Not implemented");
}
/**
* Should open system file picker for selecting an image.
* @returns Promise that should resolve to selected image data
* @throws Error with "Not implemented" message
* @todo Implement file picker using Electron's dialog API
*/
async pickImage(): Promise<ImageResult> {
logger.error("pickImage not implemented in Electron platform");
throw new Error("Not implemented");
}
/**
* Should handle deep link URLs for the desktop application.
* @param _url - The deep link URL to handle
* @throws Error with "Not implemented" message
* @todo Implement deep link handling using Electron's protocol handler
*/
async handleDeepLink(_url: string): Promise<void> {
logger.error("handleDeepLink not implemented in Electron platform");
throw new Error("Not implemented");
}
/**
* @see PlatformService.dbQuery
*/
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
await this.initializeDatabase();
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const result = await this.db.query(sql, params || []);
const values = result.values || [];
return {
columns: [], // SQLite plugin doesn't provide column names in query result
values: values as SqlValue[][],
};
} catch (error) {
logger.error("Error executing query:", error);
throw new Error(
`Database query failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* @see PlatformService.dbExec
*/
async dbExec(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }> {
await this.initializeDatabase();
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const result = await this.db.run(sql, params || []);
const changes = result.changes as Changes;
return {
changes: changes?.changes || 0,
lastId: changes?.lastId,
};
} catch (error) {
logger.error("Error executing statement:", error);
throw new Error(
`Database execution failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Rotates the camera between front and back cameras.
* @returns Promise that resolves when the camera is rotated
* @throws Error indicating camera rotation is not implemented in Electron
*/
async rotateCamera(): Promise<void> {
throw new Error("Camera rotation not implemented in Electron platform");
}
}