Files
openscreen/dist-electron/main.js
T
2025-11-09 17:36:32 -07:00

455 lines
13 KiB
JavaScript

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: false,
transparent: true,
resizable: false,
alwaysOnTop: true,
skipTaskbar: true,
hasShadow: false,
webPreferences: {
preload: path.join(__dirname$1, "preload.mjs"),
nodeIntegration: false,
contextIsolation: true,
backgroundThrottling: false
}
});
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 createEditorWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
frame: true,
transparent: false,
resizable: true,
alwaysOnTop: false,
skipTaskbar: false,
webPreferences: {
preload: path.join(__dirname$1, "preload.mjs"),
nodeIntegration: false,
contextIsolation: true,
webSecurity: false
}
});
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 createSourceSelectorWindow() {
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
const win = new BrowserWindow({
width: 620,
height: 420,
minHeight: 350,
maxHeight: 500,
x: Math.round((width - 620) / 2),
y: Math.round((height - 420) / 2),
frame: false,
resizable: false,
alwaysOnTop: true,
backgroundColor: "#ffffff",
webPreferences: {
preload: path.join(__dirname$1, "preload.mjs"),
nodeIntegration: false,
contextIsolation: true
}
});
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 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 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: true,
message: "Mouse tracking stopped",
data: session
};
}
function setupMouseEventListeners() {
uIOhook.on("mousemove", (e) => {
if (isMouseTrackingActive) {
const timestamp = performance.now() - recordingStartTime;
const event = {
type: "move",
timestamp,
x: e.x,
y: e.y
};
mouseEventData.push(event);
}
});
uIOhook.on("mousedown", (e) => {
if (isMouseTrackingActive) {
const timestamp = performance.now() - recordingStartTime;
const event = {
type: "down",
timestamp,
x: e.x,
y: e.y,
button: e.button,
clicks: e.clicks
};
mouseEventData.push(event);
}
});
uIOhook.on("mouseup", (e) => {
if (isMouseTrackingActive) {
const timestamp = performance.now() - recordingStartTime;
const event = {
type: "up",
timestamp,
x: e.x,
y: e.y,
button: e.button
};
mouseEventData.push(event);
}
});
uIOhook.on("click", (e) => {
if (isMouseTrackingActive) {
const timestamp = performance.now() - recordingStartTime;
const event = {
type: "click",
timestamp,
x: e.x,
y: e.y,
button: e.button,
clicks: e.clicks
};
mouseEventData.push(event);
}
});
}
function getTrackingData() {
return [...mouseEventData];
}
function cleanupMouseTracking() {
if (isHookStarted) {
try {
uIOhook.stop();
isHookStarted = false;
isMouseTrackingActive = false;
mouseEventData = [];
} catch (error) {
console.error("Error cleaning up mouse tracking:", error);
}
}
}
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;
}
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 videoPath = path.join(RECORDINGS_DIR, fileName);
await fs.writeFile(videoPath, Buffer.from(videoData));
return {
success: true,
path: videoPath,
message: "Video stored successfully"
};
} catch (error) {
console.error("Failed to store video:", error);
return {
success: false,
message: "Failed to store video",
error: String(error)
};
}
});
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)
};
}
});
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) };
}
});
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings");
async function cleanupOldRecordings() {
try {
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);
}
}
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);
}
}
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 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: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("stop-recording-from-tray");
}
}
}
];
const contextMenu = Menu.buildFromTemplate(menuTemplate);
tray.setContextMenu(contextMenu);
tray.setToolTip(`Recording: ${selectedSourceName}`);
}
function createEditorWindowWrapper() {
if (mainWindow) {
mainWindow.close();
mainWindow = null;
}
mainWindow = createEditorWindow();
}
function createSourceSelectorWindowWrapper() {
sourceSelectorWindow = createSourceSelectorWindow();
sourceSelectorWindow.on("closed", () => {
sourceSelectorWindow = null;
});
return sourceSelectorWindow;
}
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
cleanupMouseTracking();
app.quit();
mainWindow = null;
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
app.on("before-quit", async (event) => {
event.preventDefault();
cleanupMouseTracking();
await cleanupOldRecordings();
app.exit(0);
});
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();
}
}
);
createWindow();
});
export {
MAIN_DIST,
RECORDINGS_DIR,
RENDERER_DIST,
VITE_DEV_SERVER_URL
};