diff --git a/dist-electron/main.js b/dist-electron/main.js
index 8ae5b38..cf3ccf3 100644
--- a/dist-electron/main.js
+++ b/dist-electron/main.js
@@ -1,236 +1,354 @@
-import { BrowserWindow as v, screen as j, ipcMain as c, desktopCapturer as x, shell as D, app as l, nativeImage as F, Tray as W, Menu as O } from "electron";
-import { fileURLToPath as P } from "node:url";
-import t from "node:path";
-import p from "node:fs/promises";
-const _ = t.dirname(P(import.meta.url)), V = t.join(_, ".."), f = process.env.VITE_DEV_SERVER_URL, T = t.join(V, "dist");
-function L() {
- const e = new v({
+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";
+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(_, "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());
- }), f ? e.loadURL(f + "?windowType=hud-overlay") : e.loadFile(t.join(T, "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 k() {
- const e = new v({
+function createEditorWindow() {
+ const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 12, y: 12 },
- transparent: !1,
- resizable: !0,
- alwaysOnTop: !1,
- skipTaskbar: !1,
+ transparent: false,
+ resizable: true,
+ alwaysOnTop: false,
+ skipTaskbar: false,
title: "OpenScreen",
backgroundColor: "#000000",
webPreferences: {
- preload: t.join(_, "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());
- }), f ? e.loadURL(f + "?windowType=editor") : e.loadFile(t.join(T, "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 U() {
- const { width: e, height: s } = j.getPrimaryDisplay().workAreaSize, u = new v({
+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((s - 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(_, "preload.mjs"),
- nodeIntegration: !1,
- contextIsolation: !0
+ preload: path.join(__dirname$1, "preload.mjs"),
+ nodeIntegration: false,
+ contextIsolation: true
}
});
- return f ? u.loadURL(f + "?windowType=source-selector") : u.loadFile(t.join(T, "index.html"), {
- query: { windowType: "source-selector" }
- }), u;
+ 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 R = null;
-function C(e, s, u, m, w) {
- c.handle("get-sources", async (o, n) => (await x.getSources(n)).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, n) => {
- R = n;
- const i = m();
- return i && i.close(), R;
- }), c.handle("get-selected-source", () => R), c.handle("open-source-selector", () => {
- const o = m();
- 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;
}
- s();
- }), c.handle("switch-to-editor", () => {
- const o = u();
- o && o.close(), e();
- }), c.handle("store-recorded-video", async (o, n, i) => {
+ createSourceSelectorWindow2();
+ });
+ ipcMain.handle("switch-to-editor", () => {
+ const mainWin = getMainWindow();
+ if (mainWin) {
+ mainWin.close();
+ }
+ createEditorWindow2();
+ });
+ ipcMain.handle("store-recorded-video", async (_, videoData, fileName) => {
try {
- const r = t.join(h, i);
- return await p.writeFile(r, Buffer.from(n)), {
- success: !0,
- path: r,
+ const videoPath = path.join(RECORDINGS_DIR, fileName);
+ console.log("[STORE-VIDEO] Saving to:", videoPath);
+ console.log("[STORE-VIDEO] RECORDINGS_DIR:", RECORDINGS_DIR);
+ console.log("[STORE-VIDEO] Platform:", process.platform);
+ await fs.writeFile(videoPath, Buffer.from(videoData));
+ console.log("[STORE-VIDEO] Success! File size:", Buffer.from(videoData).length, "bytes");
+ 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("[STORE-VIDEO] Failed to store video:", error);
+ return {
+ success: false,
message: "Failed to store video",
- error: String(r)
+ error: String(error)
};
}
- }), c.handle("get-recorded-video-path", async () => {
+ });
+ ipcMain.handle("get-recorded-video-path", async () => {
try {
- const n = (await p.readdir(h)).filter((y) => y.endsWith(".webm"));
- if (n.length === 0)
- return { success: !1, message: "No recorded video found" };
- const i = n.sort().reverse()[0];
- return { success: !0, path: t.join(h, i) };
- } catch (o) {
- return console.error("Failed to get video path:", o), { success: !1, message: "Failed to get video path", error: String(o) };
+ console.log("[GET-VIDEO] RECORDINGS_DIR:", RECORDINGS_DIR);
+ console.log("[GET-VIDEO] Platform:", process.platform);
+ const files = await fs.readdir(RECORDINGS_DIR);
+ console.log("[GET-VIDEO] All files:", files);
+ const videoFiles = files.filter((file) => file.endsWith(".webm"));
+ console.log("[GET-VIDEO] Video files:", videoFiles);
+ if (videoFiles.length === 0) {
+ console.log("[GET-VIDEO] No video files found");
+ return { success: false, message: "No recorded video found" };
+ }
+ const latestVideo = videoFiles.sort().reverse()[0];
+ const videoPath = path.join(RECORDINGS_DIR, latestVideo);
+ console.log("[GET-VIDEO] Latest video path:", videoPath);
+ console.log("[GET-VIDEO] Path separators:", videoPath.includes("\\") ? "backslash" : "forward slash");
+ return { success: true, path: videoPath };
+ } catch (error) {
+ console.error("[GET-VIDEO] Failed to get video path:", error);
+ return { success: false, message: "Failed to get video path", error: String(error) };
}
- }), c.handle("set-recording-state", (o, n) => {
- w && w(n, (R || { name: "Screen" }).name);
- }), c.handle("open-external-url", async (o, n) => {
- try {
- return await D.openExternal(n), { success: !0 };
- } catch (i) {
- return console.error("Failed to open URL:", i), { success: !1, error: String(i) };
+ });
+ ipcMain.handle("set-recording-state", (_, recording) => {
+ const source = selectedSource || { name: "Screen" };
+ if (onRecordingStateChange) {
+ onRecordingStateChange(recording, source.name);
}
- }), c.handle("get-asset-base-path", () => {
+ });
+ ipcMain.handle("open-external-url", async (_, url) => {
try {
- return l.isPackaged ? t.join(process.resourcesPath, "assets") : t.join(l.getAppPath(), "public", "assets");
- } catch (o) {
- return console.error("Failed to resolve asset base path:", o), null;
+ await shell.openExternal(url);
+ return { success: true };
+ } catch (error) {
+ console.error("Failed to open URL:", error);
+ return { success: false, error: String(error) };
}
- }), c.handle("save-exported-video", async (o, n, i) => {
+ });
+ ipcMain.handle("get-asset-base-path", () => {
try {
- const r = l.getPath("downloads"), y = t.join(r, i);
- return await p.writeFile(y, Buffer.from(n)), {
- success: !0,
- path: y,
+ 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 (r) {
- return console.error("Failed to save exported video:", r), {
- success: !1,
+ } catch (error) {
+ console.error("Failed to save exported video:", error);
+ return {
+ success: false,
message: "Failed to save exported video",
- error: String(r)
+ error: String(error)
};
}
});
}
-const A = t.dirname(P(import.meta.url)), h = t.join(l.getPath("userData"), "recordings");
-async function M() {
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings");
+async function cleanupOldRecordings() {
try {
- const e = await p.readdir(h), s = Date.now(), u = 1 * 24 * 60 * 60 * 1e3;
- for (const m of e) {
- const w = t.join(h, m), o = await p.stat(w);
- s - o.mtimeMs > u && (await p.unlink(w), console.log(`Deleted old recording: ${m}`));
+ 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 (e) {
- console.error("Failed to cleanup old recordings:", e);
+ } catch (error) {
+ console.error("Failed to cleanup old recordings:", error);
}
}
-async function z() {
+async function ensureRecordingsDir() {
try {
- await p.mkdir(h, { recursive: !0 }), console.log("Recordings directory ready:", h);
- } catch (e) {
- console.error("Failed to create recordings directory:", e);
+ await fs.mkdir(RECORDINGS_DIR, { recursive: true });
+ console.log("=".repeat(60));
+ console.log("[STARTUP] Platform:", process.platform);
+ console.log("[STARTUP] RECORDINGS_DIR:", RECORDINGS_DIR);
+ console.log("[STARTUP] User Data Path:", app.getPath("userData"));
+ console.log("=".repeat(60));
+ } catch (error) {
+ console.error("[STARTUP] Failed to create recordings directory:", error);
}
}
-process.env.APP_ROOT = t.join(A, "..");
-const H = process.env.VITE_DEV_SERVER_URL, Q = t.join(process.env.APP_ROOT, "dist-electron"), b = t.join(process.env.APP_ROOT, "dist");
-process.env.VITE_PUBLIC = H ? t.join(process.env.APP_ROOT, "public") : b;
-let a = null, g = null, d = null, E = "";
-function S() {
- a = L();
+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 B() {
- const e = t.join(process.env.VITE_PUBLIC || b, "rec-button.png");
- let s = F.createFromPath(e);
- s = s.resize({ width: 24, height: 24, quality: "best" }), d = new W(s), I();
+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 I() {
- if (!d) return;
- const e = [
+function updateTrayMenu() {
+ if (!tray) return;
+ const menuTemplate = [
{
label: "Stop Recording",
click: () => {
- a && !a.isDestroyed() && a.webContents.send("stop-recording-from-tray");
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ mainWindow.webContents.send("stop-recording-from-tray");
+ }
}
}
- ], s = O.buildFromTemplate(e);
- d.setContextMenu(s), d.setToolTip(`Recording: ${E}`);
+ ];
+ const contextMenu = Menu.buildFromTemplate(menuTemplate);
+ tray.setContextMenu(contextMenu);
+ tray.setToolTip(`Recording: ${selectedSourceName}`);
}
-function N() {
- a && (a.close(), a = null), a = k();
+function createEditorWindowWrapper() {
+ if (mainWindow) {
+ mainWindow.close();
+ mainWindow = null;
+ }
+ mainWindow = createEditorWindow();
}
-function q() {
- return g = U(), g.on("closed", () => {
- g = null;
- }), g;
+function createSourceSelectorWindowWrapper() {
+ sourceSelectorWindow = createSourceSelectorWindow();
+ sourceSelectorWindow.on("closed", () => {
+ sourceSelectorWindow = null;
+ });
+ return sourceSelectorWindow;
}
-l.on("window-all-closed", () => {
+app.on("window-all-closed", () => {
});
-l.on("activate", () => {
- v.getAllWindows().length === 0 && S();
+app.on("activate", () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow();
+ }
});
-l.on("before-quit", async (e) => {
- e.preventDefault(), await M(), l.exit(0);
+app.on("before-quit", async (event) => {
+ event.preventDefault();
+ await cleanupOldRecordings();
+ app.exit(0);
});
-l.whenReady().then(async () => {
- await z(), C(
- N,
- q,
- () => a,
- () => g,
- (e, s) => {
- E = s, e ? (d || B(), I(), a && a.minimize()) : (d && (d.destroy(), d = null), a && a.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();
+ }
}
- ), S();
+ );
+ createWindow();
});
export {
- Q as MAIN_DIST,
- h as RECORDINGS_DIR,
- b as RENDERER_DIST,
- H 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 a08f981..7f7797b 100644
--- a/dist-electron/preload.mjs
+++ b/dist-electron/preload.mjs
@@ -1 +1,42 @@
-"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"),storeRecordedVideo:(r,t)=>e.ipcRenderer.invoke("store-recorded-video",r,t),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");
+ },
+ storeRecordedVideo: (videoData, fileName) => {
+ return electron.ipcRenderer.invoke("store-recorded-video", videoData, 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);
+ }
+});
diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx
index 29068c0..7e9a29c 100644
--- a/src/components/video-editor/SettingsPanel.tsx
+++ b/src/components/video-editor/SettingsPanel.tsx
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
import { useState } from "react";
import Colorful from '@uiw/react-color-colorful';
import { hsvaToHex } from '@uiw/color-convert';
-import { Trash2, Download, Crop, X, Bug, Upload } from "lucide-react";
+import { Trash2, Download, Crop, X, Bug, Upload, Coffee } from "lucide-react";
import { toast } from "sonner";
import type { ZoomDepth, CropRegion } from "./types";
import { CropControl } from "./CropControl";
@@ -401,16 +401,28 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,