From a2ca0799d4a504bcbb4c6971ce165f697f5419b0 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Tue, 20 Jan 2026 21:10:22 -0800 Subject: [PATCH] accordion & settings cleanup --- dist-electron/main.js | 508 ++++++++----- dist-electron/preload.mjs | 64 +- package-lock.json | 62 ++ package.json | 1 + src/components/ui/accordion.tsx | 55 ++ src/components/video-editor/ExportDialog.tsx | 14 + src/components/video-editor/SettingsPanel.tsx | 718 +++++++++--------- tailwind.config.cjs | 14 + 8 files changed, 879 insertions(+), 557 deletions(-) create mode 100644 src/components/ui/accordion.tsx diff --git a/dist-electron/main.js b/dist-electron/main.js index 31d9b1f..39a5ce5 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,288 +1,422 @@ -import { ipcMain as s, screen as F, BrowserWindow as R, desktopCapturer as L, shell as C, app as d, dialog as E, nativeImage as U, Tray as M, Menu as A } from "electron"; -import { fileURLToPath as j } from "node:url"; -import o from "node:path"; -import P from "node:fs/promises"; -const _ = o.dirname(j(import.meta.url)), z = o.join(_, ".."), w = process.env.VITE_DEV_SERVER_URL, S = o.join(z, "dist"); -let m = null; -s.on("hud-overlay-hide", () => { - m && !m.isDestroyed() && m.minimize(); +import { ipcMain, screen, BrowserWindow, desktopCapturer, shell, app, dialog, 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"); +let hudOverlayWindow = null; +ipcMain.on("hud-overlay-hide", () => { + if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) { + hudOverlayWindow.minimize(); + } }); -function H() { - const n = F.getPrimaryDisplay(), { workArea: t } = n, c = 500, u = 100, y = Math.floor(t.x + (t.width - c) / 2), p = Math.floor(t.y + t.height - u - 5), e = new R({ - width: c, - height: u, +function createHudOverlayWindow() { + const primaryDisplay = screen.getPrimaryDisplay(); + const { workArea } = primaryDisplay; + const windowWidth = 500; + const windowHeight = 100; + const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2); + const y = Math.floor(workArea.y + workArea.height - windowHeight - 5); + const win = new BrowserWindow({ + width: windowWidth, + height: windowHeight, minWidth: 500, maxWidth: 500, minHeight: 100, maxHeight: 100, - x: y, - y: p, - frame: !1, - transparent: !0, - resizable: !1, - alwaysOnTop: !0, - skipTaskbar: !0, - hasShadow: !1, + x, + y, + frame: false, + transparent: true, + resizable: false, + alwaysOnTop: true, + skipTaskbar: true, + hasShadow: false, webPreferences: { - preload: o.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()); - }), m = e, e.on("closed", () => { - m === e && (m = null); - }), w ? e.loadURL(w + "?windowType=hud-overlay") : e.loadFile(o.join(S, "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()); + }); + hudOverlayWindow = win; + win.on("closed", () => { + if (hudOverlayWindow === win) { + hudOverlayWindow = null; + } + }); + 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 q() { - const n = process.platform === "darwin", t = new R({ +function createEditorWindow() { + const isMac = process.platform === "darwin"; + const win = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, - ...n && { + ...isMac && { 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: o.join(_, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0, - webSecurity: !1, - backgroundThrottling: !1 + preload: path.join(__dirname$1, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + webSecurity: false, + backgroundThrottling: false } }); - return t.maximize(), t.webContents.on("did-finish-load", () => { - t == null || t.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }), w ? t.loadURL(w + "?windowType=editor") : t.loadFile(o.join(S, "index.html"), { - query: { windowType: "editor" } - }), t; + 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 B() { - const { width: n, height: t } = F.getPrimaryDisplay().workAreaSize, c = new R({ +function createSourceSelectorWindow() { + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + const win = new BrowserWindow({ width: 620, height: 420, minHeight: 350, maxHeight: 500, - x: Math.round((n - 620) / 2), - y: Math.round((t - 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: o.join(_, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0 + preload: path.join(__dirname$1, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true } }); - return w ? c.loadURL(w + "?windowType=source-selector") : c.loadFile(o.join(S, "index.html"), { - query: { windowType: "source-selector" } - }), c; + 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 T = null; -function N(n, t, c, u, y) { - s.handle("get-sources", async (e, a) => (await L.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 - }))), s.handle("select-source", (e, a) => { - T = a; - const l = u(); - return l && l.close(), T; - }), s.handle("get-selected-source", () => T), s.handle("open-source-selector", () => { - const e = u(); - if (e) { - e.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; } - t(); - }), s.handle("switch-to-editor", () => { - const e = c(); - e && e.close(), n(); - }), s.handle("store-recorded-video", async (e, a, l) => { + 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 = o.join(h, l); - return await P.writeFile(r, Buffer.from(a)), p = r, { - success: !0, - path: r, + const videoPath = path.join(RECORDINGS_DIR, fileName); + await fs.writeFile(videoPath, Buffer.from(videoData)); + currentVideoPath = videoPath; + 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) + error: String(error) }; } - }), s.handle("get-recorded-video-path", async () => { + }); + ipcMain.handle("get-recorded-video-path", async () => { try { - const a = (await P.readdir(h)).filter((I) => I.endsWith(".webm")); - if (a.length === 0) - return { success: !1, message: "No recorded video found" }; - const l = a.sort().reverse()[0]; - return { success: !0, path: o.join(h, l) }; - } catch (e) { - return console.error("Failed to get video path:", e), { success: !1, message: "Failed to get video path", error: String(e) }; + 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) }; } - }), s.handle("set-recording-state", (e, a) => { - y && y(a, (T || { name: "Screen" }).name); - }), s.handle("open-external-url", async (e, a) => { - try { - return await C.openExternal(a), { success: !0 }; - } catch (l) { - return console.error("Failed to open URL:", l), { success: !1, error: String(l) }; + }); + ipcMain.handle("set-recording-state", (_, recording) => { + const source = selectedSource || { name: "Screen" }; + if (onRecordingStateChange) { + onRecordingStateChange(recording, source.name); } - }), s.handle("get-asset-base-path", () => { + }); + ipcMain.handle("open-external-url", async (_, url) => { try { - return d.isPackaged ? o.join(process.resourcesPath, "assets") : o.join(d.getAppPath(), "public", "assets"); - } catch (e) { - return console.error("Failed to resolve asset base path:", e), null; + await shell.openExternal(url); + return { success: true }; + } catch (error) { + console.error("Failed to open URL:", error); + return { success: false, error: String(error) }; } - }), s.handle("save-exported-video", async (e, a, l) => { + }); + ipcMain.handle("get-asset-base-path", () => { try { - const r = l.toLowerCase().endsWith(".gif"), I = r ? [{ name: "GIF Image", extensions: ["gif"] }] : [{ name: "MP4 Video", extensions: ["mp4"] }], v = await E.showSaveDialog({ - title: r ? "Save Exported GIF" : "Save Exported Video", - defaultPath: o.join(d.getPath("downloads"), l), - filters: I, + 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 isGif = fileName.toLowerCase().endsWith(".gif"); + const filters = isGif ? [{ name: "GIF Image", extensions: ["gif"] }] : [{ name: "MP4 Video", extensions: ["mp4"] }]; + const result = await dialog.showSaveDialog({ + title: isGif ? "Save Exported GIF" : "Save Exported Video", + defaultPath: path.join(app.getPath("downloads"), fileName), + filters, properties: ["createDirectory", "showOverwriteConfirmation"] }); - return v.canceled || !v.filePath ? { - success: !1, - cancelled: !0, - message: "Export cancelled" - } : (await P.writeFile(v.filePath, Buffer.from(a)), { - success: !0, - path: v.filePath, + if (result.canceled || !result.filePath) { + return { + success: false, + cancelled: true, + message: "Export cancelled" + }; + } + await fs.writeFile(result.filePath, Buffer.from(videoData)); + return { + success: true, + path: result.filePath, 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) }; } - }), s.handle("open-video-file-picker", async () => { + }); + ipcMain.handle("open-video-file-picker", async () => { try { - const e = await E.showOpenDialog({ + const result = await dialog.showOpenDialog({ title: "Select Video File", - defaultPath: h, + defaultPath: RECORDINGS_DIR, filters: [ { name: "Video Files", extensions: ["webm", "mp4", "mov", "avi", "mkv"] }, { name: "All Files", extensions: ["*"] } ], properties: ["openFile"] }); - return e.canceled || e.filePaths.length === 0 ? { success: !1, cancelled: !0 } : { - success: !0, - path: e.filePaths[0] + if (result.canceled || result.filePaths.length === 0) { + return { success: false, cancelled: true }; + } + return { + success: true, + path: result.filePaths[0] }; - } catch (e) { - return console.error("Failed to open file picker:", e), { - success: !1, + } catch (error) { + console.error("Failed to open file picker:", error); + return { + success: false, message: "Failed to open file picker", - error: String(e) + error: String(error) }; } }); - let p = null; - s.handle("set-current-video-path", (e, a) => (p = a, { success: !0 })), s.handle("get-current-video-path", () => p ? { success: !0, path: p } : { success: !1 }), s.handle("clear-current-video-path", () => (p = null, { success: !0 })), s.handle("get-platform", () => process.platform); + let currentVideoPath = null; + ipcMain.handle("set-current-video-path", (_, path2) => { + currentVideoPath = path2; + return { success: true }; + }); + ipcMain.handle("get-current-video-path", () => { + return currentVideoPath ? { success: true, path: currentVideoPath } : { success: false }; + }); + ipcMain.handle("clear-current-video-path", () => { + currentVideoPath = null; + return { success: true }; + }); + ipcMain.handle("get-platform", () => { + return process.platform; + }); } -const G = o.dirname(j(import.meta.url)), h = o.join(d.getPath("userData"), "recordings"); -async function $() { +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); +async function ensureRecordingsDir() { try { - await P.mkdir(h, { recursive: !0 }), console.log("RECORDINGS_DIR:", h), console.log("User Data Path:", d.getPath("userData")); - } catch (n) { - console.error("Failed to create recordings directory:", n); + await fs.mkdir(RECORDINGS_DIR, { recursive: true }); + console.log("RECORDINGS_DIR:", RECORDINGS_DIR); + console.log("User Data Path:", app.getPath("userData")); + } catch (error) { + console.error("Failed to create recordings directory:", error); } } -process.env.APP_ROOT = o.join(G, ".."); -const Q = process.env.VITE_DEV_SERVER_URL, re = o.join(process.env.APP_ROOT, "dist-electron"), O = o.join(process.env.APP_ROOT, "dist"); -process.env.VITE_PUBLIC = Q ? o.join(process.env.APP_ROOT, "public") : O; -let i = null, g = null, f = null, V = ""; -const W = k("openscreen.png"), J = k("rec-button.png"); -function b() { - i = H(); +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 = ""; +const defaultTrayIcon = getTrayIcon("openscreen.png"); +const recordingTrayIcon = getTrayIcon("rec-button.png"); +function createWindow() { + mainWindow = createHudOverlayWindow(); } -function D() { - f = new M(W); +function createTray() { + tray = new Tray(defaultTrayIcon); } -function k(n) { - return U.createFromPath(o.join(process.env.VITE_PUBLIC || O, n)).resize({ +function getTrayIcon(filename) { + return nativeImage.createFromPath(path.join(process.env.VITE_PUBLIC || RENDERER_DIST, filename)).resize({ width: 24, height: 24, quality: "best" }); } -function x(n = !1) { - if (!f) return; - const t = n ? J : W, c = n ? `Recording: ${V}` : "OpenScreen", u = n ? [ +function updateTrayMenu(recording = false) { + if (!tray) return; + const trayIcon = recording ? recordingTrayIcon : defaultTrayIcon; + const trayToolTip = recording ? `Recording: ${selectedSourceName}` : "OpenScreen"; + const menuTemplate = recording ? [ { label: "Stop Recording", click: () => { - i && !i.isDestroyed() && i.webContents.send("stop-recording-from-tray"); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("stop-recording-from-tray"); + } } } ] : [ { label: "Open", click: () => { - i && !i.isDestroyed() ? i.isMinimized() && i.restore() : b(); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.isMinimized() && mainWindow.restore(); + } else { + createWindow(); + } } }, { label: "Quit", click: () => { - d.quit(); + app.quit(); } } ]; - f.setImage(t), f.setToolTip(c), f.setContextMenu(A.buildFromTemplate(u)); + tray.setImage(trayIcon); + tray.setToolTip(trayToolTip); + tray.setContextMenu(Menu.buildFromTemplate(menuTemplate)); } -function K() { - i && (i.close(), i = null), i = q(); +function createEditorWindowWrapper() { + if (mainWindow) { + mainWindow.close(); + mainWindow = null; + } + mainWindow = createEditorWindow(); } -function X() { - return g = B(), g.on("closed", () => { - g = null; - }), g; +function createSourceSelectorWindowWrapper() { + sourceSelectorWindow = createSourceSelectorWindow(); + sourceSelectorWindow.on("closed", () => { + sourceSelectorWindow = null; + }); + return sourceSelectorWindow; } -d.on("window-all-closed", () => { +app.on("window-all-closed", () => { }); -d.on("activate", () => { - R.getAllWindows().length === 0 && b(); +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } }); -d.whenReady().then(async () => { - const { ipcMain: n } = await import("electron"); - n.on("hud-overlay-close", () => { - d.quit(); - }), D(), x(), await $(), N( - K, - X, - () => i, - () => g, - (t, c) => { - V = c, f || D(), x(t), t || i && i.restore(); +app.whenReady().then(async () => { + const { ipcMain: ipcMain2 } = await import("electron"); + ipcMain2.on("hud-overlay-close", () => { + app.quit(); + }); + createTray(); + updateTrayMenu(); + await ensureRecordingsDir(); + registerIpcHandlers( + createEditorWindowWrapper, + createSourceSelectorWindowWrapper, + () => mainWindow, + () => sourceSelectorWindow, + (recording, sourceName) => { + selectedSourceName = sourceName; + if (!tray) createTray(); + updateTrayMenu(recording); + if (!recording) { + if (mainWindow) mainWindow.restore(); + } } - ), b(); + ); + createWindow(); }); export { - re as MAIN_DIST, - h as RECORDINGS_DIR, - O as RENDERER_DIST, - Q 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 ba75414..cb59604 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -1 +1,63 @@ -"use strict";const e=require("electron");e.contextBridge.exposeInMainWorld("electronAPI",{hudOverlayHide:()=>{e.ipcRenderer.send("hud-overlay-hide")},hudOverlayClose:()=>{e.ipcRenderer.send("hud-overlay-close")},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),openVideoFilePicker:()=>e.ipcRenderer.invoke("open-video-file-picker"),setCurrentVideoPath:r=>e.ipcRenderer.invoke("set-current-video-path",r),getCurrentVideoPath:()=>e.ipcRenderer.invoke("get-current-video-path"),clearCurrentVideoPath:()=>e.ipcRenderer.invoke("clear-current-video-path"),getPlatform:()=>e.ipcRenderer.invoke("get-platform")}); +"use strict"; +const electron = require("electron"); +electron.contextBridge.exposeInMainWorld("electronAPI", { + hudOverlayHide: () => { + electron.ipcRenderer.send("hud-overlay-hide"); + }, + hudOverlayClose: () => { + electron.ipcRenderer.send("hud-overlay-close"); + }, + 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); + }, + openVideoFilePicker: () => { + return electron.ipcRenderer.invoke("open-video-file-picker"); + }, + setCurrentVideoPath: (path) => { + return electron.ipcRenderer.invoke("set-current-video-path", path); + }, + getCurrentVideoPath: () => { + return electron.ipcRenderer.invoke("get-current-video-path"); + }, + clearCurrentVideoPath: () => { + return electron.ipcRenderer.invoke("clear-current-video-path"); + }, + getPlatform: () => { + return electron.ipcRenderer.invoke("get-platform"); + } +}); diff --git a/package-lock.json b/package-lock.json index 6266c00..3d3aca6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", @@ -2360,6 +2361,37 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -2383,6 +2415,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", diff --git a/package.json b/package.json index 60c0521..2b4c0a0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..7c406b2 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/video-editor/ExportDialog.tsx b/src/components/video-editor/ExportDialog.tsx index 16938fa..417b4d2 100644 --- a/src/components/video-editor/ExportDialog.tsx +++ b/src/components/video-editor/ExportDialog.tsx @@ -24,6 +24,20 @@ export function ExportDialog({ }: ExportDialogProps) { const [showSuccess, setShowSuccess] = useState(false); + // Reset showSuccess when a new export starts or dialog reopens + useEffect(() => { + if (isExporting) { + setShowSuccess(false); + } + }, [isExporting]); + + // Reset showSuccess when dialog opens fresh + useEffect(() => { + if (isOpen && !isExporting && !progress) { + setShowSuccess(false); + } + }, [isOpen, isExporting, progress]); + useEffect(() => { if (!isExporting && progress && progress.percentage >= 100 && !error) { setShowSuccess(true); diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index fdbe9b5..f53afd8 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -7,7 +7,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { useState } from "react"; import Block from '@uiw/react-color-block'; -import { Trash2, Download, Crop, X, Bug, Upload, Star, Film, Image } from "lucide-react"; +import { Trash2, Download, Crop, X, Bug, Upload, Star, Film, Image, Sparkles, Palette } from "lucide-react"; import { toast } from "sonner"; import type { ZoomDepth, CropRegion, AnnotationRegion, AnnotationType } from "./types"; import { CropControl } from "./CropControl"; @@ -16,6 +16,7 @@ import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel"; import { type AspectRatio } from "@/utils/aspectRatioUtils"; import type { ExportQuality, ExportFormat, GifFrameRate, GifSizePreset } from "@/lib/exporter"; import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; const WALLPAPER_COUNT = 18; const WALLPAPER_RELATIVE = Array.from({ length: WALLPAPER_COUNT }, (_, i) => `wallpapers/wallpaper${i + 1}.jpg`); @@ -253,157 +254,288 @@ export function SettingsPanel({ } return ( -
-
-
- Zoom Level -
- {zoomEnabled && selectedZoomDepth && ( - - {ZOOM_DEPTH_OPTIONS.find(o => o.depth === selectedZoomDepth)?.label} Active - - )} - +
+
+
+
+ Zoom Level +
+ {zoomEnabled && selectedZoomDepth && ( + + {ZOOM_DEPTH_OPTIONS.find(o => o.depth === selectedZoomDepth)?.label} + + )} + +
+
+ {ZOOM_DEPTH_OPTIONS.map((option) => { + const isActive = selectedZoomDepth === option.depth; + return ( + + ); + })} +
+ {!zoomEnabled && ( +

Select a zoom region to adjust

+ )} + {zoomEnabled && ( + + )}
-
- {ZOOM_DEPTH_OPTIONS.map((option) => { - const isActive = selectedZoomDepth === option.depth; - return ( - - ); - })} -
- {!zoomEnabled && ( -

Select a zoom region in the timeline to adjust depth.

- )} - {zoomEnabled && ( - - )} -
- {/* Trim Delete Section */} -
{trimEnabled && ( - +
+ +
)} + + + + +
+ + Video Effects +
+
+ +
+
+
Motion Blur
+ +
+
+
Blur BG
+ +
+
+ +
+
+
+
Shadow
+ {Math.round(shadowIntensity * 100)}% +
+ onShadowChange?.(values[0])} + min={0} + max={1} + step={0.01} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+
+
Roundness
+ {borderRadius}px +
+ onBorderRadiusChange?.(values[0])} + min={0} + max={16} + step={0.5} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+
+
Padding
+ {padding}% +
+ onPaddingChange?.(values[0])} + min={0} + max={100} + step={1} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3" + /> +
+
+ + +
+
+ + + +
+ + Background +
+
+ + + + Image + Color + Gradient + + +
+ + + + +
+ {customImages.map((imageUrl, idx) => { + const isSelected = selected === imageUrl; + return ( +
onWallpaperChange(imageUrl)} + role="button" + > + +
+ ); + })} + + {(wallpaperPaths.length > 0 ? wallpaperPaths : WALLPAPER_RELATIVE.map(p => `/${p}`)).map((path) => { + const isSelected = (() => { + if (!selected) return false; + if (selected === path) return true; + try { + const clean = (s: string) => s.replace(/^file:\/\//, '').replace(/^\//, '') + if (clean(selected).endsWith(clean(path))) return true; + if (clean(path).endsWith(clean(selected))) return true; + } catch {} + return false; + })(); + return ( +
onWallpaperChange(path)} + role="button" + /> + ) + })} +
+ + + +
+ { + setSelectedColor(color.hex); + onWallpaperChange(color.hex); + }} + style={{ + width: '100%', + borderRadius: '8px', + }} + /> +
+
+ + +
+ {GRADIENTS.map((g, idx) => ( +
{ setGradient(g); onWallpaperChange(g); }} + role="button" + /> + ))} +
+ +
+ + + +
-
-
- {/* Motion Blur Switch */} -
-
Motion Blur
- -
- {/* Blur Background Switch */} -
-
Blur
- -
-
-
- -
-
- {/* Drop Shadow Slider */} -
-
-
Shadow
- {Math.round(shadowIntensity * 100)}% -
- onShadowChange?.(values[0])} - min={0} - max={1} - step={0.01} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B]" - /> -
- {/* Corner Roundness Slider */} -
-
-
Roundness
- {borderRadius}px -
- onBorderRadiusChange?.(values[0])} - min={0} - max={16} - step={0.5} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B]" - /> -
- {/* Padding Slider */} -
-
-
Padding
- {padding}% -
- onPaddingChange?.(values[0])} - min={0} - max={100} - step={1} - className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B]" - /> -
-
-
- -
- -
- {showCropDropdown && cropRegion && onCropChange && ( <>
)} - - - Image - Color - Gradient - - -
- - {/* Upload Button */} - - - -
- {/* Custom Images */} - {customImages.map((imageUrl, idx) => { - const isSelected = selected === imageUrl; - return ( -
onWallpaperChange(imageUrl)} - role="button" - > - -
- ); - })} - - {/* Preset Wallpapers */} - {(wallpaperPaths.length > 0 ? wallpaperPaths : WALLPAPER_RELATIVE.map(p => `/${p}`)).map((path, idx) => { - const isSelected = (() => { - if (!selected) return false; - if (selected === path) return true; - try { - const clean = (s: string) => s.replace(/^file:\/\//, '').replace(/^\//, '') - if (clean(selected).endsWith(clean(path))) return true; - if (clean(path).endsWith(clean(selected))) return true; - } catch {} - return false; - })(); - return ( -
onWallpaperChange(path)} - role="button" - /> - ) - })} -
- - - -
- { - setSelectedColor(color.hex); - onWallpaperChange(color.hex); - }} - style={{ - width: '100%', - borderRadius: '12px', - }} - /> -
-
- - -
- {GRADIENTS.map((g, idx) => ( -
{ setGradient(g); onWallpaperChange(g); }} - role="button" - /> - ))} -
- +
+
+ +
- -
- {/* Format Selection */} -
-
Export Format
-
+ {exportFormat === 'mp4' && ( +
+
-
- - {/* MP4 Quality Options */} - {exportFormat === 'mp4' && ( - <> -
Export Quality
-
- - - -
- )} - {/* GIF Options */} {exportFormat === 'gif' && ( -
- {/* Frame Rate */} -
-
Frame Rate
-
+
+
+
{GIF_FRAME_RATES.map((rate) => ( ))}
-
- - {/* Size Preset */} -
-
Output Size
-
+
{Object.entries(GIF_SIZE_PRESETS).map(([key, _preset]) => ( ))}
-
- {gifOutputDimensions.width} × {gifOutputDimensions.height}px -
- - {/* Loop Toggle */} -
- Loop Animation - +
+ {gifOutputDimensions.width} × {gifOutputDimensions.height}px +
+ Loop + +
)} @@ -707,31 +686,32 @@ export function SettingsPanel({ type="button" size="lg" onClick={onExport} - className="w-full py-6 text-lg font-semibold flex items-center justify-center gap-3 bg-[#34B27B] text-white rounded-xl shadow-lg shadow-[#34B27B]/20 hover:bg-[#34B27B]/90 hover:scale-[1.02] active:scale-[0.98] transition-all duration-200" + className="w-full py-5 text-sm font-semibold flex items-center justify-center gap-2 bg-[#34B27B] text-white rounded-xl shadow-lg shadow-[#34B27B]/20 hover:bg-[#34B27B]/90 hover:scale-[1.02] active:scale-[0.98] transition-all duration-200" > - - Export {exportFormat === 'gif' ? 'GIF' : 'Video'} + + Export {exportFormat === 'gif' ? 'GIF' : 'Video'} -
+ +
diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 126e9e2..00eeabe 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -7,6 +7,20 @@ module.exports = { ], theme: { extend: { + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)',