window consistency across mac and win

This commit is contained in:
Siddharth
2025-11-20 12:25:46 -07:00
parent 2e2ce5e151
commit 6081747b7d
10 changed files with 637 additions and 276 deletions
+423 -247
View File
@@ -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
};
+60 -1
View File
@@ -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");
}
});
+3
View File
@@ -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<void>
maximizeWindow: () => Promise<void>
closeWindow: () => Promise<void>
}
}
+26
View File
@@ -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()
}
})
}
+9
View File
@@ -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')
},
})
+12 -2
View File
@@ -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,
@@ -159,6 +159,9 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
<Crop className="w-4 h-4" />
Crop Video
</Button>
<p className="text-[10px] text-slate-400/60 text-center mt-2">
If the preview looks weirdly positioned at any time, try force reloading
</p>
</div>
{showCropDropdown && cropRegion && onCropChange && (
+27 -12
View File
@@ -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 (
<div className="flex h-screen bg-background bg-black p-8 gap-8">
<Toaster position="top-center" />
<ExportDialog
isOpen={showExportDialog}
onClose={() => setShowExportDialog(false)}
progress={exportProgress}
isExporting={isExporting}
error={exportError}
onCancel={handleCancelExport}
/>
<div className="flex flex-col flex-[7] min-w-0 gap-6">
<div className="flex flex-col gap-3 flex-1">
<div className="flex flex-col h-screen bg-background bg-black">
{/* Drag region for window - more padding on macOS for traffic lights */}
<div
className={`h-8 flex-shrink-0 bg-black/50 backdrop-blur-sm flex items-center justify-between ${isMac ? 'pl-20 pr-4' : 'px-4'}`}
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
<div className="flex-1" />
<WindowControls />
</div>
<div className="flex flex-1 p-4 gap-4 overflow-hidden">
<div style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
<Toaster position="top-center" />
</div>
<ExportDialog
isOpen={showExportDialog}
onClose={() => setShowExportDialog(false)}
progress={exportProgress}
isExporting={isExporting}
error={exportError}
onCancel={handleCancelExport}
/>
<div className="flex flex-col flex-[7] min-w-0 gap-4">
<div className="flex flex-col gap-2 flex-1">
{videoPath && (
<>
<div className="flex justify-center w-full">
@@ -339,6 +353,7 @@ export default function VideoEditor() {
videoElement={videoPlaybackRef.current?.video || null}
onExport={handleExport}
/>
</div>
</div>
);
}
+17 -14
View File
@@ -728,21 +728,24 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
: 'none',
}}
/>
<div
ref={overlayRef}
className="absolute inset-0 select-none"
style={{ pointerEvents: 'none' }}
onPointerDown={handleOverlayPointerDown}
onPointerMove={handleOverlayPointerMove}
onPointerUp={handleOverlayPointerUp}
onPointerLeave={handleOverlayPointerLeave}
>
{/* Only render overlay after PIXI and video are fully initialized */}
{pixiReady && videoReady && (
<div
ref={focusIndicatorRef}
className="absolute rounded-md border border-sky-400/80 bg-sky-400/20 shadow-[0_0_0_1px_rgba(56,189,248,0.35)]"
style={{ display: 'none', pointerEvents: 'none' }}
/>
</div>
ref={overlayRef}
className="absolute inset-0 select-none"
style={{ pointerEvents: 'none' }}
onPointerDown={handleOverlayPointerDown}
onPointerMove={handleOverlayPointerMove}
onPointerUp={handleOverlayPointerUp}
onPointerLeave={handleOverlayPointerLeave}
>
<div
ref={focusIndicatorRef}
className="absolute rounded-md border border-sky-400/80 bg-sky-400/20 shadow-[0_0_0_1px_rgba(56,189,248,0.35)]"
style={{ display: 'none', pointerEvents: 'none' }}
/>
</div>
)}
<video
ref={videoRef}
src={videoPath}
@@ -0,0 +1,57 @@
export function WindowControls() {
// Only show custom controls on Windows
const isWindows = navigator.userAgent.includes('Windows');
if (!isWindows) {
return null;
}
const handleMinimize = () => {
window.electronAPI?.minimizeWindow?.();
};
const handleMaximize = () => {
window.electronAPI?.maximizeWindow?.();
};
const handleClose = () => {
window.electronAPI?.closeWindow?.();
};
return (
<div className="flex items-center" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
{/* Minimize - Horizontal Line */}
<button
onClick={handleMinimize}
className="w-12 h-8 flex items-center justify-center hover:bg-white/10 transition-colors group"
aria-label="Minimize"
>
<svg width="12" height="1" viewBox="0 0 12 1" className="text-gray-400 group-hover:text-white transition-colors">
<rect width="12" height="1" fill="currentColor" />
</svg>
</button>
{/* Maximize - Square */}
<button
onClick={handleMaximize}
className="w-12 h-8 flex items-center justify-center hover:bg-white/10 transition-colors group"
aria-label="Maximize"
>
<svg width="10" height="10" viewBox="0 0 10 10" className="text-gray-400 group-hover:text-white transition-colors">
<rect x="0" y="0" width="10" height="10" fill="none" stroke="currentColor" strokeWidth="1" />
</svg>
</button>
{/* Close - X */}
<button
onClick={handleClose}
className="w-12 h-8 flex items-center justify-center hover:bg-[#e81123] transition-colors group"
aria-label="Close"
>
<svg width="10" height="10" viewBox="0 0 10 10" className="text-gray-400 group-hover:text-white transition-colors">
<path d="M 0 0 L 10 10 M 10 0 L 0 10" stroke="currentColor" strokeWidth="1" />
</svg>
</button>
</div>
);
}