diff --git a/dist-electron/main.js b/dist-electron/main.js index 0634f7d..ed37626 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,342 +1,518 @@ -import { BrowserWindow as E, screen as O, ipcMain as c, desktopCapturer as W, shell as V, app as d, nativeImage as L, Tray as U, Menu as A } from "electron"; -import { fileURLToPath as S } from "node:url"; -import t from "node:path"; -import p from "node:fs/promises"; -import { uIOhook as w } from "uiohook-napi"; -const P = t.dirname(S(import.meta.url)), C = t.join(P, ".."), y = process.env.VITE_DEV_SERVER_URL, x = t.join(C, "dist"); -function N() { - const e = new E({ +import { BrowserWindow, screen, ipcMain, desktopCapturer, shell, app, nativeImage, Tray, Menu } from "electron"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import fs from "node:fs/promises"; +import { uIOhook } from "uiohook-napi"; +const __dirname$1 = path.dirname(fileURLToPath(import.meta.url)); +const APP_ROOT = path.join(__dirname$1, ".."); +const VITE_DEV_SERVER_URL$1 = process.env["VITE_DEV_SERVER_URL"]; +const RENDERER_DIST$1 = path.join(APP_ROOT, "dist"); +function createHudOverlayWindow() { + const win = new BrowserWindow({ width: 250, height: 80, minWidth: 250, maxWidth: 250, minHeight: 80, maxHeight: 80, - frame: !1, - transparent: !0, - resizable: !1, - alwaysOnTop: !0, - skipTaskbar: !0, - hasShadow: !1, + frame: false, + transparent: true, + resizable: false, + alwaysOnTop: true, + skipTaskbar: true, + hasShadow: false, webPreferences: { - preload: t.join(P, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0, - backgroundThrottling: !1 + preload: path.join(__dirname$1, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + backgroundThrottling: false } }); - return e.webContents.on("did-finish-load", () => { - e == null || e.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }), y ? e.loadURL(y + "?windowType=hud-overlay") : e.loadFile(t.join(x, "index.html"), { - query: { windowType: "hud-overlay" } - }), e; + win.webContents.on("did-finish-load", () => { + win == null ? void 0 : win.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); + }); + if (VITE_DEV_SERVER_URL$1) { + win.loadURL(VITE_DEV_SERVER_URL$1 + "?windowType=hud-overlay"); + } else { + win.loadFile(path.join(RENDERER_DIST$1, "index.html"), { + query: { windowType: "hud-overlay" } + }); + } + return win; } -function H() { - const e = new E({ +function createEditorWindow() { + const isMac = process.platform === "darwin"; + const win = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, - frame: !0, - transparent: !1, - resizable: !0, - alwaysOnTop: !1, - skipTaskbar: !1, - title: "", + // On macOS, use hiddenInset for native controls; on Windows, frameless + ...isMac ? { + titleBarStyle: "hiddenInset", + trafficLightPosition: { x: 12, y: 12 } + } : { + frame: false, + icon: void 0 + // No app icon on Windows + }, + transparent: false, + resizable: true, + alwaysOnTop: false, + skipTaskbar: false, + title: "OpenScreen", + backgroundColor: "#000000", webPreferences: { - preload: t.join(P, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0, - webSecurity: !1 + preload: path.join(__dirname$1, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + webSecurity: false } }); - return e.maximize(), e.webContents.on("did-finish-load", () => { - e == null || e.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }), y ? e.loadURL(y + "?windowType=editor") : e.loadFile(t.join(x, "index.html"), { - query: { windowType: "editor" } - }), e; + win.maximize(); + win.webContents.on("did-finish-load", () => { + win == null ? void 0 : win.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); + }); + if (VITE_DEV_SERVER_URL$1) { + win.loadURL(VITE_DEV_SERVER_URL$1 + "?windowType=editor"); + } else { + win.loadFile(path.join(RENDERER_DIST$1, "index.html"), { + query: { windowType: "editor" } + }); + } + return win; } -function z() { - const { width: e, height: n } = O.getPrimaryDisplay().workAreaSize, i = new E({ +function createSourceSelectorWindow() { + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + const win = new BrowserWindow({ width: 620, height: 420, minHeight: 350, maxHeight: 500, - x: Math.round((e - 620) / 2), - y: Math.round((n - 420) / 2), - frame: !1, - resizable: !1, - alwaysOnTop: !0, - transparent: !0, + x: Math.round((width - 620) / 2), + y: Math.round((height - 420) / 2), + frame: false, + resizable: false, + alwaysOnTop: true, + transparent: true, backgroundColor: "#00000000", webPreferences: { - preload: t.join(P, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0 + preload: path.join(__dirname$1, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true } }); - return y ? i.loadURL(y + "?windowType=source-selector") : i.loadFile(t.join(x, "index.html"), { - query: { windowType: "source-selector" } - }), i; + if (VITE_DEV_SERVER_URL$1) { + win.loadURL(VITE_DEV_SERVER_URL$1 + "?windowType=source-selector"); + } else { + win.loadFile(path.join(RENDERER_DIST$1, "index.html"), { + query: { windowType: "source-selector" } + }); + } + return win; } -let u = !1, b = !1, m = 0, f = []; -function q() { - if (u) - return { success: !1, message: "Already tracking" }; - if (u = !0, m = performance.now(), f = [], b) - return { success: !0, message: "Mouse tracking resumed", startTime: m }; - $(); - try { - return w.start(), b = !0, { success: !0, message: "Mouse tracking started", startTime: m }; - } catch (e) { - return console.error("Failed to start mouse tracking:", e), u = !1, { success: !1, message: "Failed to start hook", error: e }; +let isMouseTrackingActive = false; +let isHookStarted = false; +let recordingStartTime = 0; +let mouseEventData = []; +function startMouseTracking() { + if (isMouseTrackingActive) { + return { success: false, message: "Already tracking" }; + } + isMouseTrackingActive = true; + recordingStartTime = performance.now(); + mouseEventData = []; + if (!isHookStarted) { + setupMouseEventListeners(); + try { + uIOhook.start(); + isHookStarted = true; + return { success: true, message: "Mouse tracking started", startTime: recordingStartTime }; + } catch (error) { + console.error("Failed to start mouse tracking:", error); + isMouseTrackingActive = false; + return { success: false, message: "Failed to start hook", error }; + } + } else { + return { success: true, message: "Mouse tracking resumed", startTime: recordingStartTime }; } } -function B() { - if (!u) - return { success: !1, message: "Not currently tracking" }; - u = !1; - const e = performance.now() - m; +function stopMouseTracking() { + if (!isMouseTrackingActive) { + return { success: false, message: "Not currently tracking" }; + } + isMouseTrackingActive = false; + const duration = performance.now() - recordingStartTime; + const session = { + startTime: recordingStartTime, + events: mouseEventData, + duration + }; return { - success: !0, + success: true, message: "Mouse tracking stopped", - data: { - startTime: m, - events: f, - duration: e - } + data: session }; } -function $() { - w.on("mousemove", (e) => { - if (u) { - const i = { +function setupMouseEventListeners() { + uIOhook.on("mousemove", (e) => { + if (isMouseTrackingActive) { + const timestamp = performance.now() - recordingStartTime; + const event = { type: "move", - timestamp: performance.now() - m, + timestamp, x: e.x, y: e.y }; - f.push(i); + mouseEventData.push(event); } - }), w.on("mousedown", (e) => { - if (u) { - const i = { + }); + uIOhook.on("mousedown", (e) => { + if (isMouseTrackingActive) { + const timestamp = performance.now() - recordingStartTime; + const event = { type: "down", - timestamp: performance.now() - m, + timestamp, x: e.x, y: e.y, button: e.button, clicks: e.clicks }; - f.push(i); + mouseEventData.push(event); } - }), w.on("mouseup", (e) => { - if (u) { - const i = { + }); + uIOhook.on("mouseup", (e) => { + if (isMouseTrackingActive) { + const timestamp = performance.now() - recordingStartTime; + const event = { type: "up", - timestamp: performance.now() - m, + timestamp, x: e.x, y: e.y, button: e.button }; - f.push(i); + mouseEventData.push(event); } - }), w.on("click", (e) => { - if (u) { - const i = { + }); + uIOhook.on("click", (e) => { + if (isMouseTrackingActive) { + const timestamp = performance.now() - recordingStartTime; + const event = { type: "click", - timestamp: performance.now() - m, + timestamp, x: e.x, y: e.y, button: e.button, clicks: e.clicks }; - f.push(i); + mouseEventData.push(event); } }); } -function G() { - return [...f]; +function getTrackingData() { + return [...mouseEventData]; } -function I() { - if (b) +function cleanupMouseTracking() { + if (isHookStarted) { try { - w.stop(), b = !1, u = !1, f = []; - } catch (e) { - console.error("Error cleaning up mouse tracking:", e); + uIOhook.stop(); + isHookStarted = false; + isMouseTrackingActive = false; + mouseEventData = []; + } catch (error) { + console.error("Error cleaning up mouse tracking:", error); } + } } -let _ = null; -function J(e, n, i, v, T) { - c.handle("get-sources", async (o, a) => (await W.getSources(a)).map((r) => ({ - id: r.id, - name: r.name, - display_id: r.display_id, - thumbnail: r.thumbnail ? r.thumbnail.toDataURL() : null, - appIcon: r.appIcon ? r.appIcon.toDataURL() : null - }))), c.handle("select-source", (o, a) => { - _ = a; - const s = v(); - return s && s.close(), _; - }), c.handle("get-selected-source", () => _), c.handle("open-source-selector", () => { - const o = v(); - if (o) { - o.focus(); +let selectedSource = null; +function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, getMainWindow, getSourceSelectorWindow, onRecordingStateChange) { + ipcMain.handle("get-sources", async (_, opts) => { + const sources = await desktopCapturer.getSources(opts); + return sources.map((source) => ({ + id: source.id, + name: source.name, + display_id: source.display_id, + thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, + appIcon: source.appIcon ? source.appIcon.toDataURL() : null + })); + }); + ipcMain.handle("select-source", (_, source) => { + selectedSource = source; + const sourceSelectorWin = getSourceSelectorWindow(); + if (sourceSelectorWin) { + sourceSelectorWin.close(); + } + return selectedSource; + }); + ipcMain.handle("get-selected-source", () => { + return selectedSource; + }); + ipcMain.handle("open-source-selector", () => { + const sourceSelectorWin = getSourceSelectorWindow(); + if (sourceSelectorWin) { + sourceSelectorWin.focus(); return; } - n(); - }), c.handle("switch-to-editor", () => { - const o = i(); - o && o.close(), e(); - }), c.handle("start-mouse-tracking", () => q()), c.handle("stop-mouse-tracking", () => B()), c.handle("store-recorded-video", async (o, a, s) => { + createSourceSelectorWindow2(); + }); + ipcMain.handle("switch-to-editor", () => { + const mainWin = getMainWindow(); + if (mainWin) { + mainWin.close(); + } + createEditorWindow2(); + }); + ipcMain.handle("start-mouse-tracking", () => { + return startMouseTracking(); + }); + ipcMain.handle("stop-mouse-tracking", () => { + return stopMouseTracking(); + }); + ipcMain.handle("store-recorded-video", async (_, videoData, fileName) => { try { - const r = t.join(h, s); - return await p.writeFile(r, Buffer.from(a)), { - success: !0, - path: r, + const videoPath = path.join(RECORDINGS_DIR, fileName); + await fs.writeFile(videoPath, Buffer.from(videoData)); + return { + success: true, + path: videoPath, message: "Video stored successfully" }; - } catch (r) { - return console.error("Failed to store video:", r), { - success: !1, + } catch (error) { + console.error("Failed to store video:", error); + return { + success: false, message: "Failed to store video", - error: String(r) - }; - } - }), c.handle("store-mouse-tracking-data", async (o, a) => { - try { - const s = G(); - if (s.length === 0) - return { success: !1, message: "No tracking data to save" }; - const r = t.join(h, a); - return await p.writeFile(r, JSON.stringify(s, null, 2), "utf-8"), { - success: !0, - path: r, - eventCount: s.length, - message: "Mouse tracking data stored successfully" - }; - } catch (s) { - return console.error("Failed to store mouse tracking data:", s), { - success: !1, - message: "Failed to store mouse tracking data", - error: String(s) - }; - } - }), c.handle("get-recorded-video-path", async () => { - try { - const a = (await p.readdir(h)).filter((R) => R.endsWith(".webm")); - if (a.length === 0) - return { success: !1, message: "No recorded video found" }; - const s = a.sort().reverse()[0]; - return { success: !0, path: t.join(h, s) }; - } catch (o) { - return console.error("Failed to get video path:", o), { success: !1, message: "Failed to get video path", error: String(o) }; - } - }), c.handle("set-recording-state", (o, a) => { - T && T(a, (_ || { name: "Screen" }).name); - }), c.handle("open-external-url", async (o, a) => { - try { - return await V.openExternal(a), { success: !0 }; - } catch (s) { - return console.error("Failed to open URL:", s), { success: !1, error: String(s) }; - } - }), c.handle("get-asset-base-path", () => { - try { - return d.isPackaged ? t.join(process.resourcesPath, "assets") : t.join(d.getAppPath(), "public", "assets"); - } catch (o) { - return console.error("Failed to resolve asset base path:", o), null; - } - }), c.handle("save-exported-video", async (o, a, s) => { - try { - const r = d.getPath("downloads"), R = t.join(r, s); - return await p.writeFile(R, Buffer.from(a)), { - success: !0, - path: R, - message: "Video exported successfully" - }; - } catch (r) { - return console.error("Failed to save exported video:", r), { - success: !1, - message: "Failed to save exported video", - error: String(r) + error: String(error) }; } }); -} -const K = t.dirname(S(import.meta.url)), h = t.join(d.getPath("userData"), "recordings"); -async function Q() { - try { - const e = await p.readdir(h), n = Date.now(), i = 1 * 24 * 60 * 60 * 1e3; - for (const v of e) { - const T = t.join(h, v), o = await p.stat(T); - n - o.mtimeMs > i && (await p.unlink(T), console.log(`Deleted old recording: ${v}`)); + ipcMain.handle("store-mouse-tracking-data", async (_, fileName) => { + try { + const data = getTrackingData(); + if (data.length === 0) { + return { success: false, message: "No tracking data to save" }; + } + const trackingPath = path.join(RECORDINGS_DIR, fileName); + await fs.writeFile(trackingPath, JSON.stringify(data, null, 2), "utf-8"); + return { + success: true, + path: trackingPath, + eventCount: data.length, + message: "Mouse tracking data stored successfully" + }; + } catch (error) { + console.error("Failed to store mouse tracking data:", error); + return { + success: false, + message: "Failed to store mouse tracking data", + error: String(error) + }; } - } catch (e) { - console.error("Failed to cleanup old recordings:", e); - } + }); + ipcMain.handle("get-recorded-video-path", async () => { + try { + const files = await fs.readdir(RECORDINGS_DIR); + const videoFiles = files.filter((file) => file.endsWith(".webm")); + if (videoFiles.length === 0) { + return { success: false, message: "No recorded video found" }; + } + const latestVideo = videoFiles.sort().reverse()[0]; + const videoPath = path.join(RECORDINGS_DIR, latestVideo); + return { success: true, path: videoPath }; + } catch (error) { + console.error("Failed to get video path:", error); + return { success: false, message: "Failed to get video path", error: String(error) }; + } + }); + ipcMain.handle("set-recording-state", (_, recording) => { + const source = selectedSource || { name: "Screen" }; + if (onRecordingStateChange) { + onRecordingStateChange(recording, source.name); + } + }); + ipcMain.handle("open-external-url", async (_, url) => { + try { + await shell.openExternal(url); + return { success: true }; + } catch (error) { + console.error("Failed to open URL:", error); + return { success: false, error: String(error) }; + } + }); + ipcMain.handle("get-asset-base-path", () => { + try { + if (app.isPackaged) { + return path.join(process.resourcesPath, "assets"); + } + return path.join(app.getAppPath(), "public", "assets"); + } catch (err) { + console.error("Failed to resolve asset base path:", err); + return null; + } + }); + ipcMain.handle("save-exported-video", async (_, videoData, fileName) => { + try { + const downloadsPath = app.getPath("downloads"); + const videoPath = path.join(downloadsPath, fileName); + await fs.writeFile(videoPath, Buffer.from(videoData)); + return { + success: true, + path: videoPath, + message: "Video exported successfully" + }; + } catch (error) { + console.error("Failed to save exported video:", error); + return { + success: false, + message: "Failed to save exported video", + error: String(error) + }; + } + }); + ipcMain.handle("minimize-window", () => { + const mainWin = getMainWindow(); + if (mainWin) { + mainWin.minimize(); + } + }); + ipcMain.handle("maximize-window", () => { + const mainWin = getMainWindow(); + if (mainWin) { + if (mainWin.isMaximized()) { + mainWin.unmaximize(); + } else { + mainWin.maximize(); + } + } + }); + ipcMain.handle("close-window", () => { + const mainWin = getMainWindow(); + if (mainWin) { + mainWin.close(); + } + }); } -async function X() { +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); +async function cleanupOldRecordings() { try { - await p.mkdir(h, { recursive: !0 }), console.log("Recordings directory ready:", h); - } catch (e) { - console.error("Failed to create recordings directory:", e); + const files = await fs.readdir(RECORDINGS_DIR); + const now = Date.now(); + const maxAge = 1 * 24 * 60 * 60 * 1e3; + for (const file of files) { + const filePath = path.join(RECORDINGS_DIR, file); + const stats = await fs.stat(filePath); + if (now - stats.mtimeMs > maxAge) { + await fs.unlink(filePath); + console.log(`Deleted old recording: ${file}`); + } + } + } catch (error) { + console.error("Failed to cleanup old recordings:", error); } } -process.env.APP_ROOT = t.join(K, ".."); -const Y = process.env.VITE_DEV_SERVER_URL, ie = t.join(process.env.APP_ROOT, "dist-electron"), j = t.join(process.env.APP_ROOT, "dist"); -process.env.VITE_PUBLIC = Y ? t.join(process.env.APP_ROOT, "public") : j; -let l = null, k = null, g = null, D = ""; -function F() { - l = N(); +async function ensureRecordingsDir() { + try { + await fs.mkdir(RECORDINGS_DIR, { recursive: true }); + console.log("Recordings directory ready:", RECORDINGS_DIR); + } catch (error) { + console.error("Failed to create recordings directory:", error); + } } -function Z() { - const e = t.join(process.env.VITE_PUBLIC || j, "rec-button.png"); - let n = L.createFromPath(e); - n = n.resize({ width: 24, height: 24, quality: "best" }), g = new U(n), M(); +process.env.APP_ROOT = path.join(__dirname, ".."); +const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; +const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); +const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); +process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, "public") : RENDERER_DIST; +let mainWindow = null; +let sourceSelectorWindow = null; +let tray = null; +let selectedSourceName = ""; +function createWindow() { + mainWindow = createHudOverlayWindow(); } -function M() { - if (!g) return; - const e = [ +function createTray() { + const iconPath = path.join(process.env.VITE_PUBLIC || RENDERER_DIST, "rec-button.png"); + let icon = nativeImage.createFromPath(iconPath); + icon = icon.resize({ width: 24, height: 24, quality: "best" }); + tray = new Tray(icon); + updateTrayMenu(); +} +function updateTrayMenu() { + if (!tray) return; + const menuTemplate = [ { label: "Stop Recording", click: () => { - l && !l.isDestroyed() && l.webContents.send("stop-recording-from-tray"); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("stop-recording-from-tray"); + } } } - ], n = A.buildFromTemplate(e); - g.setContextMenu(n), g.setToolTip(`Recording: ${D}`); + ]; + const contextMenu = Menu.buildFromTemplate(menuTemplate); + tray.setContextMenu(contextMenu); + tray.setToolTip(`Recording: ${selectedSourceName}`); } -function ee() { - l && (l.close(), l = null), l = H(); +function createEditorWindowWrapper() { + if (mainWindow) { + mainWindow.close(); + mainWindow = null; + } + mainWindow = createEditorWindow(); } -function te() { - return k = z(), k.on("closed", () => { - k = null; - }), k; +function createSourceSelectorWindowWrapper() { + sourceSelectorWindow = createSourceSelectorWindow(); + sourceSelectorWindow.on("closed", () => { + sourceSelectorWindow = null; + }); + return sourceSelectorWindow; } -d.on("window-all-closed", () => { - process.platform !== "darwin" && (I(), d.quit(), l = null); +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + cleanupMouseTracking(); + app.quit(); + mainWindow = null; + } }); -d.on("activate", () => { - E.getAllWindows().length === 0 && F(); +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } }); -d.on("before-quit", async (e) => { - e.preventDefault(), I(), await Q(), d.exit(0); +app.on("before-quit", async (event) => { + event.preventDefault(); + cleanupMouseTracking(); + await cleanupOldRecordings(); + app.exit(0); }); -d.whenReady().then(async () => { - await X(), J( - ee, - te, - () => l, - () => k, - (e, n) => { - D = n, e ? (g || Z(), M(), l && l.minimize()) : (g && (g.destroy(), g = null), l && l.restore()); +app.whenReady().then(async () => { + await ensureRecordingsDir(); + registerIpcHandlers( + createEditorWindowWrapper, + createSourceSelectorWindowWrapper, + () => mainWindow, + () => sourceSelectorWindow, + (recording, sourceName) => { + selectedSourceName = sourceName; + if (recording) { + if (!tray) createTray(); + updateTrayMenu(); + if (mainWindow) mainWindow.minimize(); + } else { + if (tray) { + tray.destroy(); + tray = null; + } + if (mainWindow) mainWindow.restore(); + } } - ), F(); + ); + createWindow(); }); export { - ie as MAIN_DIST, - h as RECORDINGS_DIR, - j as RENDERER_DIST, - Y as VITE_DEV_SERVER_URL + MAIN_DIST, + RECORDINGS_DIR, + RENDERER_DIST, + VITE_DEV_SERVER_URL }; diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 92d0779..51a8efa 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -1 +1,60 @@ -"use strict";const e=require("electron");e.contextBridge.exposeInMainWorld("electronAPI",{getAssetBasePath:async()=>await e.ipcRenderer.invoke("get-asset-base-path"),getSources:async r=>await e.ipcRenderer.invoke("get-sources",r),switchToEditor:()=>e.ipcRenderer.invoke("switch-to-editor"),openSourceSelector:()=>e.ipcRenderer.invoke("open-source-selector"),selectSource:r=>e.ipcRenderer.invoke("select-source",r),getSelectedSource:()=>e.ipcRenderer.invoke("get-selected-source"),startMouseTracking:()=>e.ipcRenderer.invoke("start-mouse-tracking"),stopMouseTracking:()=>e.ipcRenderer.invoke("stop-mouse-tracking"),storeRecordedVideo:(r,t)=>e.ipcRenderer.invoke("store-recorded-video",r,t),storeMouseTrackingData:r=>e.ipcRenderer.invoke("store-mouse-tracking-data",r),getRecordedVideoPath:()=>e.ipcRenderer.invoke("get-recorded-video-path"),setRecordingState:r=>e.ipcRenderer.invoke("set-recording-state",r),onStopRecordingFromTray:r=>{const t=()=>r();return e.ipcRenderer.on("stop-recording-from-tray",t),()=>e.ipcRenderer.removeListener("stop-recording-from-tray",t)},openExternalUrl:r=>e.ipcRenderer.invoke("open-external-url",r),saveExportedVideo:(r,t)=>e.ipcRenderer.invoke("save-exported-video",r,t)}); +"use strict"; +const electron = require("electron"); +electron.contextBridge.exposeInMainWorld("electronAPI", { + getAssetBasePath: async () => { + return await electron.ipcRenderer.invoke("get-asset-base-path"); + }, + getSources: async (opts) => { + return await electron.ipcRenderer.invoke("get-sources", opts); + }, + switchToEditor: () => { + return electron.ipcRenderer.invoke("switch-to-editor"); + }, + openSourceSelector: () => { + return electron.ipcRenderer.invoke("open-source-selector"); + }, + selectSource: (source) => { + return electron.ipcRenderer.invoke("select-source", source); + }, + getSelectedSource: () => { + return electron.ipcRenderer.invoke("get-selected-source"); + }, + startMouseTracking: () => { + return electron.ipcRenderer.invoke("start-mouse-tracking"); + }, + stopMouseTracking: () => { + return electron.ipcRenderer.invoke("stop-mouse-tracking"); + }, + storeRecordedVideo: (videoData, fileName) => { + return electron.ipcRenderer.invoke("store-recorded-video", videoData, fileName); + }, + storeMouseTrackingData: (fileName) => { + return electron.ipcRenderer.invoke("store-mouse-tracking-data", fileName); + }, + getRecordedVideoPath: () => { + return electron.ipcRenderer.invoke("get-recorded-video-path"); + }, + setRecordingState: (recording) => { + return electron.ipcRenderer.invoke("set-recording-state", recording); + }, + onStopRecordingFromTray: (callback) => { + const listener = () => callback(); + electron.ipcRenderer.on("stop-recording-from-tray", listener); + return () => electron.ipcRenderer.removeListener("stop-recording-from-tray", listener); + }, + openExternalUrl: (url) => { + return electron.ipcRenderer.invoke("open-external-url", url); + }, + saveExportedVideo: (videoData, fileName) => { + return electron.ipcRenderer.invoke("save-exported-video", videoData, fileName); + }, + minimizeWindow: () => { + return electron.ipcRenderer.invoke("minimize-window"); + }, + maximizeWindow: () => { + return electron.ipcRenderer.invoke("maximize-window"); + }, + closeWindow: () => { + return electron.ipcRenderer.invoke("close-window"); + } +}); diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 0853fc1..6ac2284 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -38,6 +38,9 @@ interface Window { onStopRecordingFromTray: (callback: () => void) => () => void openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }> saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => Promise<{ success: boolean; path?: string; message?: string }> + minimizeWindow: () => Promise + maximizeWindow: () => Promise + closeWindow: () => Promise } } diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 81e8af5..e7a7956 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -178,4 +178,30 @@ export function registerIpcHandlers( } } }) + + // Window control handlers for frameless window + ipcMain.handle('minimize-window', () => { + const mainWin = getMainWindow() + if (mainWin) { + mainWin.minimize() + } + }) + + ipcMain.handle('maximize-window', () => { + const mainWin = getMainWindow() + if (mainWin) { + if (mainWin.isMaximized()) { + mainWin.unmaximize() + } else { + mainWin.maximize() + } + } + }) + + ipcMain.handle('close-window', () => { + const mainWin = getMainWindow() + if (mainWin) { + mainWin.close() + } + }) } diff --git a/electron/preload.ts b/electron/preload.ts index 2b94f79..b8d29a7 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -49,4 +49,13 @@ contextBridge.exposeInMainWorld('electronAPI', { saveExportedVideo: (videoData: ArrayBuffer, fileName: string) => { return ipcRenderer.invoke('save-exported-video', videoData, fileName) }, + minimizeWindow: () => { + return ipcRenderer.invoke('minimize-window') + }, + maximizeWindow: () => { + return ipcRenderer.invoke('maximize-window') + }, + closeWindow: () => { + return ipcRenderer.invoke('close-window') + }, }) \ No newline at end of file diff --git a/electron/windows.ts b/electron/windows.ts index cf63669..03720f4 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -47,17 +47,27 @@ export function createHudOverlayWindow(): BrowserWindow { } export function createEditorWindow(): BrowserWindow { + const isMac = process.platform === 'darwin' + const win = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, - frame: true, + // On macOS, use hiddenInset for native controls; on Windows, frameless + ...(isMac ? { + titleBarStyle: 'hiddenInset', + trafficLightPosition: { x: 12, y: 12 }, + } : { + frame: false, + icon: undefined, // No app icon on Windows + }), transparent: false, resizable: true, alwaysOnTop: false, skipTaskbar: false, - title: '', + title: 'OpenScreen', + backgroundColor: '#000000', webPreferences: { preload: path.join(__dirname, 'preload.mjs'), nodeIntegration: false, diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index d70220b..53e1dd0 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -159,6 +159,9 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, Crop Video +

+ If the preview looks weirdly positioned at any time, try force reloading +

{showCropDropdown && cropRegion && onCropChange && ( diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 62d671f..3c7e3e8 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -9,6 +9,7 @@ import PlaybackControls from "./PlaybackControls"; import TimelineEditor from "./timeline/TimelineEditor"; import { SettingsPanel } from "./SettingsPanel"; import { ExportDialog } from "./ExportDialog"; +import { WindowControls } from "./WindowControls"; import type { Span } from "dnd-timeline"; import { DEFAULT_ZOOM_DEPTH, @@ -267,19 +268,32 @@ export default function VideoEditor() { ); } + const isMac = navigator.userAgent.includes('Mac'); + return ( -
- - setShowExportDialog(false)} - progress={exportProgress} - isExporting={isExporting} - error={exportError} - onCancel={handleCancelExport} - /> -
-
+
+ {/* Drag region for window - more padding on macOS for traffic lights */} +
+
+ +
+
+
+ +
+ setShowExportDialog(false)} + progress={exportProgress} + isExporting={isExporting} + error={exportError} + onCancel={handleCancelExport} + /> +
+
{videoPath && ( <>
@@ -339,6 +353,7 @@ export default function VideoEditor() { videoElement={videoPlaybackRef.current?.video || null} onExport={handleExport} /> +
); } \ No newline at end of file diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 106c6c6..4396086 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -728,21 +728,24 @@ const VideoPlayback = forwardRef(({ : 'none', }} /> -
+ {/* Only render overlay after PIXI and video are fully initialized */} + {pixiReady && videoReady && (
-
+ ref={overlayRef} + className="absolute inset-0 select-none" + style={{ pointerEvents: 'none' }} + onPointerDown={handleOverlayPointerDown} + onPointerMove={handleOverlayPointerMove} + onPointerUp={handleOverlayPointerUp} + onPointerLeave={handleOverlayPointerLeave} + > +
+
+ )}