518 lines
16 KiB
JavaScript
518 lines
16 KiB
JavaScript
import { ipcMain as i, screen as R, BrowserWindow as x, app as f, desktopCapturer as ee, shell as te, dialog as I, nativeImage as re, Tray as oe, Menu as V } from "electron";
|
|
import { fileURLToPath as B } from "node:url";
|
|
import a from "node:path";
|
|
import p from "node:fs/promises";
|
|
const N = a.dirname(B(import.meta.url)), se = a.join(N, ".."), T = process.env.VITE_DEV_SERVER_URL, W = a.join(se, "dist");
|
|
let O = null;
|
|
i.on("hud-overlay-hide", () => {
|
|
O && !O.isDestroyed() && O.minimize();
|
|
});
|
|
function ne() {
|
|
const o = R.getPrimaryDisplay(), { workArea: r } = o, c = 500, g = 100, y = Math.floor(r.x + (r.width - c) / 2), t = Math.floor(r.y + r.height - g - 5), e = new x({
|
|
width: c,
|
|
height: g,
|
|
minWidth: 500,
|
|
maxWidth: 500,
|
|
minHeight: 100,
|
|
maxHeight: 100,
|
|
x: y,
|
|
y: t,
|
|
frame: !1,
|
|
transparent: !0,
|
|
resizable: !1,
|
|
alwaysOnTop: !0,
|
|
skipTaskbar: !0,
|
|
hasShadow: !1,
|
|
webPreferences: {
|
|
preload: a.join(N, "preload.mjs"),
|
|
nodeIntegration: !1,
|
|
contextIsolation: !0,
|
|
backgroundThrottling: !1
|
|
}
|
|
});
|
|
return e.webContents.on("did-finish-load", () => {
|
|
e == null || e.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString());
|
|
}), O = e, e.on("closed", () => {
|
|
O === e && (O = null);
|
|
}), T ? e.loadURL(T + "?windowType=hud-overlay") : e.loadFile(a.join(W, "index.html"), {
|
|
query: { windowType: "hud-overlay" }
|
|
}), e;
|
|
}
|
|
function ae() {
|
|
const o = process.platform === "darwin", r = new x({
|
|
width: 1200,
|
|
height: 800,
|
|
minWidth: 800,
|
|
minHeight: 600,
|
|
...o && {
|
|
titleBarStyle: "hiddenInset",
|
|
trafficLightPosition: { x: 12, y: 12 }
|
|
},
|
|
transparent: !1,
|
|
resizable: !0,
|
|
alwaysOnTop: !1,
|
|
skipTaskbar: !1,
|
|
title: "OpenScreen",
|
|
backgroundColor: "#000000",
|
|
webPreferences: {
|
|
preload: a.join(N, "preload.mjs"),
|
|
nodeIntegration: !1,
|
|
contextIsolation: !0,
|
|
webSecurity: !1,
|
|
backgroundThrottling: !1
|
|
}
|
|
});
|
|
return r.maximize(), r.webContents.on("did-finish-load", () => {
|
|
r == null || r.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString());
|
|
}), T ? r.loadURL(T + "?windowType=editor") : r.loadFile(a.join(W, "index.html"), {
|
|
query: { windowType: "editor" }
|
|
}), r;
|
|
}
|
|
function ie() {
|
|
const { width: o, height: r } = R.getPrimaryDisplay().workAreaSize, c = new x({
|
|
width: 620,
|
|
height: 420,
|
|
minHeight: 350,
|
|
maxHeight: 500,
|
|
x: Math.round((o - 620) / 2),
|
|
y: Math.round((r - 420) / 2),
|
|
frame: !1,
|
|
resizable: !1,
|
|
alwaysOnTop: !0,
|
|
transparent: !0,
|
|
backgroundColor: "#00000000",
|
|
webPreferences: {
|
|
preload: a.join(N, "preload.mjs"),
|
|
nodeIntegration: !1,
|
|
contextIsolation: !0
|
|
}
|
|
});
|
|
return T ? c.loadURL(T + "?windowType=source-selector") : c.loadFile(a.join(W, "index.html"), {
|
|
query: { windowType: "source-selector" }
|
|
}), c;
|
|
}
|
|
const D = "openscreen", U = a.join(f.getPath("userData"), "shortcuts.json");
|
|
let v = null, w = null, m = null;
|
|
function z(o) {
|
|
return a.resolve(o);
|
|
}
|
|
function le(o) {
|
|
return !o || !m ? !1 : z(o) === z(m);
|
|
}
|
|
const ce = 1, ue = 100, de = 60 * 60 * 10;
|
|
let C = null, G = 0, F = [], _ = [];
|
|
function M(o, r, c) {
|
|
return Math.min(c, Math.max(r, o));
|
|
}
|
|
function J() {
|
|
C && (clearInterval(C), C = null);
|
|
}
|
|
function $() {
|
|
const o = R.getCursorScreenPoint(), r = Number(v == null ? void 0 : v.display_id), y = ((Number.isFinite(r) ? R.getAllDisplays().find((l) => l.id === r) ?? null : null) ?? R.getDisplayNearestPoint(o)).bounds, t = Math.max(1, y.width), e = Math.max(1, y.height), n = M((o.x - y.x) / t, 0, 1), s = M((o.y - y.y) / e, 0, 1);
|
|
F.push({
|
|
timeMs: Math.max(0, Date.now() - G),
|
|
cx: n,
|
|
cy: s
|
|
}), F.length > de && F.shift();
|
|
}
|
|
function pe(o, r, c, g, y) {
|
|
i.handle("get-sources", async (t, e) => (await ee.getSources(e)).map((s) => ({
|
|
id: s.id,
|
|
name: s.name,
|
|
display_id: s.display_id,
|
|
thumbnail: s.thumbnail ? s.thumbnail.toDataURL() : null,
|
|
appIcon: s.appIcon ? s.appIcon.toDataURL() : null
|
|
}))), i.handle("select-source", (t, e) => {
|
|
v = e;
|
|
const n = g();
|
|
return n && n.close(), v;
|
|
}), i.handle("get-selected-source", () => v), i.handle("open-source-selector", () => {
|
|
const t = g();
|
|
if (t) {
|
|
t.focus();
|
|
return;
|
|
}
|
|
r();
|
|
}), i.handle("switch-to-editor", () => {
|
|
const t = c();
|
|
t && t.close(), o();
|
|
}), i.handle("store-recorded-video", async (t, e, n) => {
|
|
try {
|
|
const s = a.join(S, n);
|
|
await p.writeFile(s, Buffer.from(e)), w = s, m = null;
|
|
const l = `${s}.cursor.json`;
|
|
return _.length > 0 && await p.writeFile(
|
|
l,
|
|
JSON.stringify({ version: ce, samples: _ }, null, 2),
|
|
"utf-8"
|
|
), _ = [], {
|
|
success: !0,
|
|
path: s,
|
|
message: "Video stored successfully"
|
|
};
|
|
} catch (s) {
|
|
return console.error("Failed to store video:", s), {
|
|
success: !1,
|
|
message: "Failed to store video",
|
|
error: String(s)
|
|
};
|
|
}
|
|
}), i.handle("get-recorded-video-path", async () => {
|
|
try {
|
|
const e = (await p.readdir(S)).filter((l) => l.endsWith(".webm"));
|
|
if (e.length === 0)
|
|
return { success: !1, message: "No recorded video found" };
|
|
const n = e.sort().reverse()[0];
|
|
return { success: !0, path: a.join(S, n) };
|
|
} catch (t) {
|
|
return console.error("Failed to get video path:", t), { success: !1, message: "Failed to get video path", error: String(t) };
|
|
}
|
|
}), i.handle("set-recording-state", (t, e) => {
|
|
e ? (J(), F = [], _ = [], G = Date.now(), $(), C = setInterval($, ue)) : (J(), _ = [...F], F = []), y && y(e, (v || { name: "Screen" }).name);
|
|
}), i.handle("get-cursor-telemetry", async (t, e) => {
|
|
const n = e ?? w;
|
|
if (!n)
|
|
return { success: !0, samples: [] };
|
|
const s = `${n}.cursor.json`;
|
|
try {
|
|
const l = await p.readFile(s, "utf-8"), d = JSON.parse(l);
|
|
return { success: !0, samples: (Array.isArray(d) ? d : Array.isArray(d == null ? void 0 : d.samples) ? d.samples : []).filter((b) => !!(b && typeof b == "object")).map((b) => {
|
|
const h = b;
|
|
return {
|
|
timeMs: typeof h.timeMs == "number" && Number.isFinite(h.timeMs) ? Math.max(0, h.timeMs) : 0,
|
|
cx: typeof h.cx == "number" && Number.isFinite(h.cx) ? M(h.cx, 0, 1) : 0.5,
|
|
cy: typeof h.cy == "number" && Number.isFinite(h.cy) ? M(h.cy, 0, 1) : 0.5
|
|
};
|
|
}).sort((b, h) => b.timeMs - h.timeMs) };
|
|
} catch (l) {
|
|
return l.code === "ENOENT" ? { success: !0, samples: [] } : (console.error("Failed to load cursor telemetry:", l), { success: !1, message: "Failed to load cursor telemetry", error: String(l), samples: [] });
|
|
}
|
|
}), i.handle("open-external-url", async (t, e) => {
|
|
try {
|
|
return await te.openExternal(e), { success: !0 };
|
|
} catch (n) {
|
|
return console.error("Failed to open URL:", n), { success: !1, error: String(n) };
|
|
}
|
|
}), i.handle("get-asset-base-path", () => {
|
|
try {
|
|
return f.isPackaged ? a.join(process.resourcesPath, "assets") : a.join(f.getAppPath(), "public", "assets");
|
|
} catch (t) {
|
|
return console.error("Failed to resolve asset base path:", t), null;
|
|
}
|
|
}), i.handle("save-exported-video", async (t, e, n) => {
|
|
try {
|
|
const s = n.toLowerCase().endsWith(".gif"), l = s ? [{ name: "GIF Image", extensions: ["gif"] }] : [{ name: "MP4 Video", extensions: ["mp4"] }], d = await I.showSaveDialog({
|
|
title: s ? "Save Exported GIF" : "Save Exported Video",
|
|
defaultPath: a.join(f.getPath("downloads"), n),
|
|
filters: l,
|
|
properties: ["createDirectory", "showOverwriteConfirmation"]
|
|
});
|
|
return d.canceled || !d.filePath ? {
|
|
success: !1,
|
|
canceled: !0,
|
|
message: "Export canceled"
|
|
} : (await p.writeFile(d.filePath, Buffer.from(e)), {
|
|
success: !0,
|
|
path: d.filePath,
|
|
message: "Video exported successfully"
|
|
});
|
|
} catch (s) {
|
|
return console.error("Failed to save exported video:", s), {
|
|
success: !1,
|
|
message: "Failed to save exported video",
|
|
error: String(s)
|
|
};
|
|
}
|
|
}), i.handle("open-video-file-picker", async () => {
|
|
try {
|
|
const t = await I.showOpenDialog({
|
|
title: "Select Video File",
|
|
defaultPath: S,
|
|
filters: [
|
|
{ name: "Video Files", extensions: ["webm", "mp4", "mov", "avi", "mkv"] },
|
|
{ name: "All Files", extensions: ["*"] }
|
|
],
|
|
properties: ["openFile"]
|
|
});
|
|
return t.canceled || t.filePaths.length === 0 ? { success: !1, canceled: !0 } : (m = null, {
|
|
success: !0,
|
|
path: t.filePaths[0]
|
|
});
|
|
} catch (t) {
|
|
return console.error("Failed to open file picker:", t), {
|
|
success: !1,
|
|
message: "Failed to open file picker",
|
|
error: String(t)
|
|
};
|
|
}
|
|
}), i.handle("save-project-file", async (t, e, n, s) => {
|
|
try {
|
|
const l = le(s) ? s : null;
|
|
if (l)
|
|
return await p.writeFile(l, JSON.stringify(e, null, 2), "utf-8"), m = l, {
|
|
success: !0,
|
|
path: l,
|
|
message: "Project saved successfully"
|
|
};
|
|
const d = (n || `project-${Date.now()}`).replace(/[^a-zA-Z0-9-_]/g, "_"), k = d.endsWith(`.${D}`) ? d : `${d}.${D}`, P = await I.showSaveDialog({
|
|
title: "Save OpenScreen Project",
|
|
defaultPath: a.join(S, k),
|
|
filters: [
|
|
{ name: "OpenScreen Project", extensions: [D] },
|
|
{ name: "JSON", extensions: ["json"] }
|
|
],
|
|
properties: ["createDirectory", "showOverwriteConfirmation"]
|
|
});
|
|
return P.canceled || !P.filePath ? {
|
|
success: !1,
|
|
canceled: !0,
|
|
message: "Save project canceled"
|
|
} : (await p.writeFile(P.filePath, JSON.stringify(e, null, 2), "utf-8"), m = P.filePath, {
|
|
success: !0,
|
|
path: P.filePath,
|
|
message: "Project saved successfully"
|
|
});
|
|
} catch (l) {
|
|
return console.error("Failed to save project file:", l), {
|
|
success: !1,
|
|
message: "Failed to save project file",
|
|
error: String(l)
|
|
};
|
|
}
|
|
}), i.handle("load-project-file", async () => {
|
|
try {
|
|
const t = await I.showOpenDialog({
|
|
title: "Open OpenScreen Project",
|
|
defaultPath: S,
|
|
filters: [
|
|
{ name: "OpenScreen Project", extensions: [D] },
|
|
{ name: "JSON", extensions: ["json"] },
|
|
{ name: "All Files", extensions: ["*"] }
|
|
],
|
|
properties: ["openFile"]
|
|
});
|
|
if (t.canceled || t.filePaths.length === 0)
|
|
return { success: !1, canceled: !0, message: "Open project canceled" };
|
|
const e = t.filePaths[0], n = await p.readFile(e, "utf-8"), s = JSON.parse(n);
|
|
return m = e, s && typeof s == "object" && typeof s.videoPath == "string" && (w = s.videoPath), {
|
|
success: !0,
|
|
path: e,
|
|
project: s
|
|
};
|
|
} catch (t) {
|
|
return console.error("Failed to load project file:", t), {
|
|
success: !1,
|
|
message: "Failed to load project file",
|
|
error: String(t)
|
|
};
|
|
}
|
|
}), i.handle("load-current-project-file", async () => {
|
|
try {
|
|
if (!m)
|
|
return { success: !1, message: "No active project" };
|
|
const t = await p.readFile(m, "utf-8"), e = JSON.parse(t);
|
|
return e && typeof e == "object" && typeof e.videoPath == "string" && (w = e.videoPath), {
|
|
success: !0,
|
|
path: m,
|
|
project: e
|
|
};
|
|
} catch (t) {
|
|
return console.error("Failed to load current project file:", t), {
|
|
success: !1,
|
|
message: "Failed to load current project file",
|
|
error: String(t)
|
|
};
|
|
}
|
|
}), i.handle("set-current-video-path", (t, e) => (w = e, m = null, { success: !0 })), i.handle("get-current-video-path", () => w ? { success: !0, path: w } : { success: !1 }), i.handle("clear-current-video-path", () => (w = null, { success: !0 })), i.handle("get-platform", () => process.platform), i.handle("get-shortcuts", async () => {
|
|
try {
|
|
const t = await p.readFile(U, "utf-8");
|
|
return JSON.parse(t);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}), i.handle("save-shortcuts", async (t, e) => {
|
|
try {
|
|
return await p.writeFile(U, JSON.stringify(e, null, 2), "utf-8"), { success: !0 };
|
|
} catch (n) {
|
|
return console.error("Failed to save shortcuts:", n), { success: !1, error: String(n) };
|
|
}
|
|
});
|
|
}
|
|
const fe = a.dirname(B(import.meta.url)), S = a.join(f.getPath("userData"), "recordings");
|
|
async function he() {
|
|
try {
|
|
await p.mkdir(S, { recursive: !0 }), console.log("RECORDINGS_DIR:", S), console.log("User Data Path:", f.getPath("userData"));
|
|
} catch (o) {
|
|
console.error("Failed to create recordings directory:", o);
|
|
}
|
|
}
|
|
process.env.APP_ROOT = a.join(fe, "..");
|
|
const me = process.env.VITE_DEV_SERVER_URL, Oe = a.join(process.env.APP_ROOT, "dist-electron"), X = a.join(process.env.APP_ROOT, "dist");
|
|
process.env.VITE_PUBLIC = me ? a.join(process.env.APP_ROOT, "public") : X;
|
|
let u = null, E = null, j = null, Z = "";
|
|
const Q = Y("openscreen.png"), ye = Y("rec-button.png");
|
|
function L() {
|
|
u = ne();
|
|
}
|
|
function ge(o) {
|
|
return o.webContents.getURL().includes("windowType=editor");
|
|
}
|
|
function A(o) {
|
|
let r = x.getFocusedWindow() ?? u;
|
|
if (!r || r.isDestroyed() || !ge(r)) {
|
|
if (K(), r = u, !r || r.isDestroyed()) return;
|
|
r.webContents.once("did-finish-load", () => {
|
|
!r || r.isDestroyed() || r.webContents.send(o);
|
|
});
|
|
return;
|
|
}
|
|
r.webContents.send(o);
|
|
}
|
|
function we() {
|
|
const o = process.platform === "darwin", r = [];
|
|
o && r.push({
|
|
label: f.name,
|
|
submenu: [
|
|
{ role: "about" },
|
|
{ type: "separator" },
|
|
{ role: "services" },
|
|
{ type: "separator" },
|
|
{ role: "hide" },
|
|
{ role: "hideOthers" },
|
|
{ role: "unhide" },
|
|
{ type: "separator" },
|
|
{ role: "quit" }
|
|
]
|
|
}), r.push(
|
|
{
|
|
label: "File",
|
|
submenu: [
|
|
{
|
|
label: "Load Project…",
|
|
accelerator: "CmdOrCtrl+O",
|
|
click: () => A("menu-load-project")
|
|
},
|
|
{
|
|
label: "Save Project…",
|
|
accelerator: "CmdOrCtrl+S",
|
|
click: () => A("menu-save-project")
|
|
},
|
|
{
|
|
label: "Save Project As…",
|
|
accelerator: "CmdOrCtrl+Shift+S",
|
|
click: () => A("menu-save-project-as")
|
|
},
|
|
...o ? [] : [{ type: "separator" }, { role: "quit" }]
|
|
]
|
|
},
|
|
{
|
|
label: "Edit",
|
|
submenu: [
|
|
{ role: "undo" },
|
|
{ role: "redo" },
|
|
{ type: "separator" },
|
|
{ role: "cut" },
|
|
{ role: "copy" },
|
|
{ role: "paste" },
|
|
{ role: "selectAll" }
|
|
]
|
|
},
|
|
{
|
|
label: "View",
|
|
submenu: [
|
|
{ role: "reload" },
|
|
{ role: "forceReload" },
|
|
{ role: "toggleDevTools" },
|
|
{ type: "separator" },
|
|
{ role: "resetZoom" },
|
|
{ role: "zoomIn" },
|
|
{ role: "zoomOut" },
|
|
{ type: "separator" },
|
|
{ role: "togglefullscreen" }
|
|
]
|
|
},
|
|
{
|
|
label: "Window",
|
|
submenu: o ? [
|
|
{ role: "minimize" },
|
|
{ role: "zoom" },
|
|
{ type: "separator" },
|
|
{ role: "front" }
|
|
] : [
|
|
{ role: "minimize" },
|
|
{ role: "close" }
|
|
]
|
|
}
|
|
);
|
|
const c = V.buildFromTemplate(r);
|
|
V.setApplicationMenu(c);
|
|
}
|
|
function H() {
|
|
j = new oe(Q);
|
|
}
|
|
function Y(o) {
|
|
return re.createFromPath(a.join(process.env.VITE_PUBLIC || X, o)).resize({
|
|
width: 24,
|
|
height: 24,
|
|
quality: "best"
|
|
});
|
|
}
|
|
function q(o = !1) {
|
|
if (!j) return;
|
|
const r = o ? ye : Q, c = o ? `Recording: ${Z}` : "OpenScreen", g = o ? [
|
|
{
|
|
label: "Stop Recording",
|
|
click: () => {
|
|
u && !u.isDestroyed() && u.webContents.send("stop-recording-from-tray");
|
|
}
|
|
}
|
|
] : [
|
|
{
|
|
label: "Open",
|
|
click: () => {
|
|
u && !u.isDestroyed() ? u.isMinimized() && u.restore() : L();
|
|
}
|
|
},
|
|
{
|
|
label: "Quit",
|
|
click: () => {
|
|
f.quit();
|
|
}
|
|
}
|
|
];
|
|
j.setImage(r), j.setToolTip(c), j.setContextMenu(V.buildFromTemplate(g));
|
|
}
|
|
function K() {
|
|
u && (u.close(), u = null), u = ae();
|
|
}
|
|
function Se() {
|
|
return E = ie(), E.on("closed", () => {
|
|
E = null;
|
|
}), E;
|
|
}
|
|
f.on("window-all-closed", () => {
|
|
});
|
|
f.on("activate", () => {
|
|
x.getAllWindows().length === 0 && L();
|
|
});
|
|
f.whenReady().then(async () => {
|
|
const { ipcMain: o } = await import("electron");
|
|
o.on("hud-overlay-close", () => {
|
|
f.quit();
|
|
}), H(), q(), we(), await he(), pe(
|
|
K,
|
|
Se,
|
|
() => u,
|
|
() => E,
|
|
(r, c) => {
|
|
Z = c, j || H(), q(r), r || u && u.restore();
|
|
}
|
|
), L();
|
|
});
|
|
export {
|
|
Oe as MAIN_DIST,
|
|
S as RECORDINGS_DIR,
|
|
X as RENDERER_DIST,
|
|
me as VITE_DEV_SERVER_URL
|
|
};
|