diff --git a/src/desktop/notify.js b/src/desktop/notify.js new file mode 100644 index 0000000..968d68c --- /dev/null +++ b/src/desktop/notify.js @@ -0,0 +1,33 @@ +const listeners = new Map(); + +let sendMessage = () => {}; + +export function setMessageSender(sender) { + sendMessage = sender; +} + +export function sendIPC(channel, data) { + sendMessage(channel, data); +} + +export function onIPCData(channel, callback) { + if (!listeners.has(channel)) { + listeners.set(channel, new Set()); + } + listeners.get(channel).add(callback); +} + +export function removeAllListeners(channel) { + listeners.delete(channel); +} + +export function dispatchIPC(channel, data) { + const channelListeners = listeners.get(channel); + if (!channelListeners) { + return; + } + + for (const callback of channelListeners) { + callback(data); + } +} diff --git a/src/desktop/rpc.js b/src/desktop/rpc.js new file mode 100644 index 0000000..335ff9f --- /dev/null +++ b/src/desktop/rpc.js @@ -0,0 +1,152 @@ +import log4js from "log4js"; +import { BrowserView } from "electrobun/bun"; +import { startWaiting, stopWaiting } from "../spinner.js"; +import { setMessageSender, sendIPC } from "./notify.js"; + +const logger = log4js.getLogger("RPC"); +let mainWindow = null; + +export function createAppRpc() { + return BrowserView.defineRPC({ + maxRequestTime: 30000, + handlers: { + requests: { + getData: async () => { + startWaiting("Getting data...", logger); + try { + const socketClient = global.socketClient; + + if (!socketClient) { + pushState({ + authenticated: false, + connected: false, + loading: false, + host: {}, + printers: [], + documentPrinters: [], + }); + await stopWaiting(); + return { ok: true }; + } + + pushState({ + authenticated: socketClient.authenticated, + connected: socketClient.connected, + loading: socketClient.loading, + host: socketClient.host || {}, + printers: socketClient.printerManager.printers || [], + documentPrinters: + socketClient.documentPrinterManager.documentPrinters || [], + }); + await stopWaiting(); + return { ok: true }; + } catch (error) { + logger.error("Error getting printer data:", error); + pushState({ + authenticated: false, + connected: false, + loading: false, + host: {}, + printers: [], + documentPrinters: [], + }); + await stopWaiting(); + return { ok: false }; + } + }, + authenticateOTP: async ({ otp }) => { + startWaiting("Authenticating with OTP...", logger); + try { + const socketClient = global.socketClient; + if (socketClient) { + await socketClient.authenticateWithOtp(otp); + } else { + logger.error("Socket client not available for OTP authentication"); + } + await stopWaiting(); + return { ok: true }; + } catch (error) { + await stopWaiting(); + logger.error("Error during OTP authentication:", error); + return { ok: false }; + } + }, + minimizeWindow: async () => { + if (mainWindow && !mainWindow.isDestroyed?.()) { + mainWindow.minimize(); + } + return { ok: true }; + }, + maximizeWindow: async () => { + if (mainWindow && !mainWindow.isDestroyed?.()) { + if (mainWindow.isMaximized?.()) { + mainWindow.unmaximize(); + } else { + mainWindow.maximize(); + } + } + return { ok: true }; + }, + closeWindow: async () => { + if (mainWindow && !mainWindow.isDestroyed?.()) { + mainWindow.close(); + } + return { ok: true }; + }, + }, + messages: {}, + }, + }); +} + +function pushState({ + authenticated, + connected, + loading, + host, + printers, + documentPrinters, +}) { + sendIPC("setAuthenticated", authenticated); + sendIPC("setConnected", connected); + sendIPC("setLoading", loading); + sendIPC("setHost", host); + sendIPC("setPrinters", printers); + sendIPC("setDocumentPrinters", documentPrinters); +} + +export function registerMainWindow(window) { + mainWindow = window; + global.mainWindow = window; +} + +export async function setupIPC(window = mainWindow) { + if (window) { + registerMainWindow(window); + } + + if (!mainWindow) { + logger.warn("No main window available, skipping RPC setup"); + return; + } + + setMessageSender((channel, data) => { + try { + const send = mainWindow?.webview?.rpc?.send; + + if (send) { + send[channel](data); + logger.info(`Message sent to main window on channel: ${channel}`, data); + return; + } + + logger.warn( + `No RPC sender available for channel: ${channel}. Is the window ready?`, + ); + } catch (error) { + logger.error(`Error sending message on channel ${channel}:`, error); + } + }); + + logger.info("RPC handlers ready"); +} diff --git a/src/desktop/window.js b/src/desktop/window.js new file mode 100644 index 0000000..6bff630 --- /dev/null +++ b/src/desktop/window.js @@ -0,0 +1,59 @@ +import log4js from "log4js"; +import { BrowserWindow, Updater } from "electrobun/bun"; +import { loadConfig } from "../config.js"; +import { startWaiting, stopWaiting } from "../spinner.js"; +import { createAppRpc, registerMainWindow } from "./rpc.js"; + +const config = loadConfig(); +const logger = log4js.getLogger("Desktop"); +logger.level = config.logLevel; + +const DEV_SERVER_PORT = 5287; +const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`; + +export async function getMainViewUrl() { + const channel = await Updater.localInfo.channel(); + if (channel === "dev" || process.env.NODE_ENV === "development") { + try { + await fetch(DEV_SERVER_URL, { method: "HEAD" }); + logger.info(`Using Vite dev server at ${DEV_SERVER_URL}`); + return DEV_SERVER_URL; + } catch { + logger.warn( + "Vite dev server not running. Start it with `bun run dev:renderer`.", + ); + } + } + + return "views://mainview/index.html"; +} + +export async function createDesktopWindow() { + const url = await getMainViewUrl(); + const rpc = createAppRpc(); + + startWaiting("Creating desktop window...", logger); + + const mainWindow = new BrowserWindow({ + title: "Farm Control Server", + url, + rpc, + titleBarStyle: "hiddenInset", + trafficLightOffset: { x: 14, y: 12 }, + frame: { + width: 900, + height: 600, + x: 100, + y: 100, + }, + }); + + registerMainWindow(mainWindow); + + return new Promise((resolve) => { + mainWindow.webview.on("dom-ready", async () => { + await stopWaiting(); + resolve(mainWindow); + }); + }); +} diff --git a/src/electron/ipc.js b/src/electron/ipc.js deleted file mode 100644 index 750f5bf..0000000 --- a/src/electron/ipc.js +++ /dev/null @@ -1,122 +0,0 @@ -import log4js from "log4js"; -import { notPrompting } from "../utils.js"; -import { startWaiting, stopWaiting } from "../spinner.js"; - -const logger = log4js.getLogger("IPC"); -let mainWindow = null; - -export async function setupIPC() { - // Only import Electron if we're in an Electron environment - let ipcMain; - try { - const electron = await import("electron"); - ipcMain = electron.ipcMain; - mainWindow = global.mainWindow; - } catch (error) { - logger.warn("Electron not available, skipping IPC setup"); - return; - } - - // Only proceed if we have ipcMain - if (!ipcMain) { - logger.warn("ipcMain not available, skipping IPC setup"); - return; - } - - ipcMain.on("getData", async () => { - startWaiting("Getting data...", logger); - try { - // Get the global socket client instance - const socketClient = global.socketClient; - - if (!socketClient) { - sendIPC("setAuthenticated", false); - sendIPC("setConnected", false); - sendIPC("setLoading", false); - sendIPC("setHost", {}); - sendIPC("setPrinters", []); - sendIPC("setDocumentPrinters", []); - await stopWaiting(); - return; - } - - // Send individual data pieces to match renderer expectations - sendIPC("setAuthenticated", socketClient.authenticated); - sendIPC("setConnected", socketClient.connected); - sendIPC("setLoading", socketClient.loading); - sendIPC("setHost", socketClient.host || {}); - sendIPC("setPrinters", socketClient.printerManager.printers || []); - sendIPC( - "setDocumentPrinters", - socketClient.documentPrinterManager.documentPrinters || [], - ); - await stopWaiting(); - } catch (error) { - logger.error("Error getting printer data:", error); - sendIPC("setAuthenticated", false); - sendIPC("setConnected", false); - sendIPC("setLoading", false); - sendIPC("setHost", {}); - sendIPC("setPrinters", []); - await stopWaiting(); - } - }); - - // Window management IPC handlers - ipcMain.on("window-minimize", (event) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.minimize(); - } - }); - - ipcMain.on("window-maximize", (event) => { - if (mainWindow && !mainWindow.isDestroyed()) { - if (mainWindow.isMaximized()) { - mainWindow.unmaximize(); - } else { - mainWindow.maximize(); - } - } - }); - - ipcMain.on("window-close", (event) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.close(); - } - }); - - // OTP authentication handler - ipcMain.on("authenticateOTP", async (event, otp) => { - startWaiting("Authenticating with OTP...", logger); - try { - const socketClient = global.socketClient; - if (socketClient) { - notPrompting(); - await socketClient.authenticateWithOtp(otp); - } else { - logger.error("Socket client not available for OTP authentication"); - } - await stopWaiting(); - } catch (error) { - await stopWaiting(); - logger.error("Error during OTP authentication:", error); - } - }); - - logger.info("IPC handlers setup complete"); -} - -export function sendIPC(channel, data) { - try { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send(channel, data); - logger.info(`Message sent to main window on channel: ${channel}`, data); - } else { - logger.warn( - `No main window available, cannot send message on channel: ${channel}` - ); - } - } catch (error) { - logger.error(`Error sending message on channel ${channel}:`, error); - } -} diff --git a/src/electron/preload.js b/src/electron/preload.js deleted file mode 100644 index b07cf95..0000000 --- a/src/electron/preload.js +++ /dev/null @@ -1,21 +0,0 @@ -import { contextBridge, ipcRenderer } from "electron"; - -// Expose protected methods that allow the renderer process to use -// the ipcRenderer without exposing the entire object -contextBridge.exposeInMainWorld("electronAPI", { - onIPCData: (channel, callback) => { - ipcRenderer.on(channel, (event, data) => callback(data)); - }, - // Send messages to main process - sendIPC: (channel, data) => { - console.log("SEND IPC", channel); - ipcRenderer.send(channel, data); - }, - // Window management - minimize: () => ipcRenderer.send("window-minimize"), - maximize: () => ipcRenderer.send("window-maximize"), - close: () => ipcRenderer.send("window-close"), - removeAllListeners: (channel) => { - ipcRenderer.removeAllListeners(channel); - }, -}); diff --git a/src/electron/window.js b/src/electron/window.js deleted file mode 100644 index aeaf48a..0000000 --- a/src/electron/window.js +++ /dev/null @@ -1,107 +0,0 @@ -/* eslint-disable no-undef */ -// Remove CommonJS requires and use ES module imports -import path from "path"; -import { loadConfig } from "../config.js"; -import log4js from "log4js"; -import { fileURLToPath } from "url"; -import { startWaiting, stopWaiting } from "../spinner.js"; -// Load configuration -const config = loadConfig(); - -const logger = log4js.getLogger("Electron"); -logger.level = config.logLevel; - -export async function createElectronWindow() { - // Only import Electron if we're in an Electron environment - let app, BrowserWindow; - try { - const electron = (await import("electron")).default; - app = electron.app; - BrowserWindow = electron.BrowserWindow; - logger.trace("Imported electron"); - } catch (error) { - logger.warn( - "Electron not available, skipping window creation. Error:", - error, - ); - return; - } - - // Only proceed if we have app - if (!app) { - logger.warn("Electron app not available, skipping window creation"); - return; - } - - // __dirname workaround for ES modules - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - - // Determine if we are running from the build directory or src - const isRunningFromBuild = - __dirname.includes(path.sep + "build") || __dirname.includes("app.asar"); - - return new Promise((resolve) => { - function createWindow() { - logger.debug("Creating browser window..."); - - // Resolve paths correctly based on environment - const preloadPath = - process.env.NODE_ENV === "development" - ? path.join(__dirname, "preload.js") - : isRunningFromBuild - ? path.join(__dirname, "preload.js") - : path.join(__dirname, "..", "build", "electron", "preload.js"); - - const win = new BrowserWindow({ - width: 900, - height: 600, - resizable: true, - frame: false, - titleBarStyle: "hiddenInset", - trafficLightPosition: { x: 14, y: 12 }, - webPreferences: { - nodeIntegration: true, - contextIsolation: true, - preload: preloadPath, - }, - }); - - // Make the window globally accessible for IPC - global.mainWindow = win; - - logger.info("Preload Script", preloadPath); - if (process.env.NODE_ENV === "development") { - startWaiting("Loading development url...", logger); - win.loadURL("http://localhost:5287"); // Vite dev server - } else { - // In production, the built files will be in the build/electron directory - const htmlPath = isRunningFromBuild - ? path.join(__dirname, "index.html") - : path.join(__dirname, "..", "build", "electron", "index.html"); - - startWaiting("Loading production file...", logger); - logger.info("Loading production file:", htmlPath); - win.loadFile(htmlPath); - } - - // Resolve the promise when the window is ready - win.webContents.on("did-finish-load", async () => { - await stopWaiting(); - resolve(win); - }); - } - - app.whenReady().then(() => { - createWindow(); - - app.on("activate", function () { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); - }); - }); - - app.on("window-all-closed", function () { - if (process.platform !== "darwin") app.quit(); - }); - }); -}