From dbc78cb8670bd13a1c84d6dc76011afc7d111520 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sun, 16 Nov 2025 22:12:22 -0700 Subject: [PATCH] fix wallpaper access in build --- dist-electron/main.js | 587 +++++++----------- dist-electron/preload.mjs | 49 +- electron-builder.json5 | 6 + electron/ipc/handlers.ts | 13 + electron/preload.ts | 4 + src/components/video-editor/SettingsPanel.tsx | 65 +- src/components/video-editor/VideoPlayback.tsx | 50 +- src/lib/assetPath.ts | 25 + src/lib/exporter/frameRenderer.ts | 15 +- src/vite-env.d.ts | 1 + 10 files changed, 383 insertions(+), 432 deletions(-) create mode 100644 src/lib/assetPath.ts diff --git a/dist-electron/main.js b/dist-electron/main.js index c12676b..0634f7d 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,475 +1,342 @@ -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({ +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({ width: 250, height: 80, minWidth: 250, maxWidth: 250, minHeight: 80, maxHeight: 80, - frame: false, - transparent: true, - resizable: false, - alwaysOnTop: true, - skipTaskbar: true, - hasShadow: false, + frame: !1, + transparent: !0, + resizable: !1, + alwaysOnTop: !0, + skipTaskbar: !0, + hasShadow: !1, webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), - nodeIntegration: false, - contextIsolation: true, - backgroundThrottling: false + preload: t.join(P, "preload.mjs"), + nodeIntegration: !1, + contextIsolation: !0, + backgroundThrottling: !1 } }); - 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; + 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; } -function createEditorWindow() { - const win = new BrowserWindow({ +function H() { + const e = new E({ width: 1200, height: 800, minWidth: 800, minHeight: 600, - frame: true, - transparent: false, - resizable: true, - alwaysOnTop: false, - skipTaskbar: false, + frame: !0, + transparent: !1, + resizable: !0, + alwaysOnTop: !1, + skipTaskbar: !1, title: "", webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), - nodeIntegration: false, - contextIsolation: true, - webSecurity: false + preload: t.join(P, "preload.mjs"), + nodeIntegration: !1, + contextIsolation: !0, + webSecurity: !1 } }); - 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; + 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; } -function createSourceSelectorWindow() { - const { width, height } = screen.getPrimaryDisplay().workAreaSize; - const win = new BrowserWindow({ +function z() { + const { width: e, height: n } = O.getPrimaryDisplay().workAreaSize, i = new E({ 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, - transparent: true, + x: Math.round((e - 620) / 2), + y: Math.round((n - 420) / 2), + frame: !1, + resizable: !1, + alwaysOnTop: !0, + transparent: !0, backgroundColor: "#00000000", webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), - nodeIntegration: false, - contextIsolation: true + preload: t.join(P, "preload.mjs"), + nodeIntegration: !1, + contextIsolation: !0 } }); - 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; + return y ? i.loadURL(y + "?windowType=source-selector") : i.loadFile(t.join(x, "index.html"), { + query: { windowType: "source-selector" } + }), i; } -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 }; +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 }; } } -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 - }; +function B() { + if (!u) + return { success: !1, message: "Not currently tracking" }; + u = !1; + const e = performance.now() - m; return { - success: true, + success: !0, message: "Mouse tracking stopped", - data: session + data: { + startTime: m, + events: f, + duration: e + } }; } -function setupMouseEventListeners() { - uIOhook.on("mousemove", (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime; - const event = { +function $() { + w.on("mousemove", (e) => { + if (u) { + const i = { type: "move", - timestamp, + timestamp: performance.now() - m, x: e.x, y: e.y }; - mouseEventData.push(event); + f.push(i); } - }); - uIOhook.on("mousedown", (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime; - const event = { + }), w.on("mousedown", (e) => { + if (u) { + const i = { type: "down", - timestamp, + timestamp: performance.now() - m, x: e.x, y: e.y, button: e.button, clicks: e.clicks }; - mouseEventData.push(event); + f.push(i); } - }); - uIOhook.on("mouseup", (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime; - const event = { + }), w.on("mouseup", (e) => { + if (u) { + const i = { type: "up", - timestamp, + timestamp: performance.now() - m, x: e.x, y: e.y, button: e.button }; - mouseEventData.push(event); + f.push(i); } - }); - uIOhook.on("click", (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime; - const event = { + }), w.on("click", (e) => { + if (u) { + const i = { type: "click", - timestamp, + timestamp: performance.now() - m, x: e.x, y: e.y, button: e.button, clicks: e.clicks }; - mouseEventData.push(event); + f.push(i); } }); } -function getTrackingData() { - return [...mouseEventData]; +function G() { + return [...f]; } -function cleanupMouseTracking() { - if (isHookStarted) { +function I() { + if (b) try { - uIOhook.stop(); - isHookStarted = false; - isMouseTrackingActive = false; - mouseEventData = []; - } catch (error) { - console.error("Error cleaning up mouse tracking:", error); + w.stop(), b = !1, u = !1, f = []; + } catch (e) { + console.error("Error cleaning up mouse tracking:", e); } - } } -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(); +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(); 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) => { + 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) => { try { - const videoPath = path.join(RECORDINGS_DIR, fileName); - await fs.writeFile(videoPath, Buffer.from(videoData)); - return { - success: true, - path: videoPath, + const r = t.join(h, s); + return await p.writeFile(r, Buffer.from(a)), { + success: !0, + path: r, message: "Video stored successfully" }; - } catch (error) { - console.error("Failed to store video:", error); - return { - success: false, + } catch (r) { + return console.error("Failed to store video:", r), { + success: !1, message: "Failed to store video", - error: String(error) + error: String(r) }; } - }); - ipcMain.handle("store-mouse-tracking-data", async (_, fileName) => { + }), c.handle("store-mouse-tracking-data", async (o, a) => { 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, + 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 (error) { - console.error("Failed to store mouse tracking data:", error); - return { - success: false, + } catch (s) { + return console.error("Failed to store mouse tracking data:", s), { + success: !1, message: "Failed to store mouse tracking data", - error: String(error) + error: String(s) }; } - }); - ipcMain.handle("get-recorded-video-path", async () => { + }), c.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) }; + 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) }; } - }); - ipcMain.handle("set-recording-state", (_, recording) => { - const source = selectedSource || { name: "Screen" }; - if (onRecordingStateChange) { - onRecordingStateChange(recording, source.name); - } - }); - ipcMain.handle("open-external-url", async (_, url) => { + }), c.handle("set-recording-state", (o, a) => { + T && T(a, (_ || { name: "Screen" }).name); + }), c.handle("open-external-url", async (o, a) => { try { - await shell.openExternal(url); - return { success: true }; - } catch (error) { - console.error("Failed to open URL:", error); - return { success: false, error: String(error) }; + return await V.openExternal(a), { success: !0 }; + } catch (s) { + return console.error("Failed to open URL:", s), { success: !1, error: String(s) }; } - }); - ipcMain.handle("save-exported-video", async (_, videoData, fileName) => { + }), c.handle("get-asset-base-path", () => { try { - const downloadsPath = app.getPath("downloads"); - const videoPath = path.join(downloadsPath, fileName); - await fs.writeFile(videoPath, Buffer.from(videoData)); - return { - success: true, - path: videoPath, + 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 (error) { - console.error("Failed to save exported video:", error); - return { - success: false, + } catch (r) { + return console.error("Failed to save exported video:", r), { + success: !1, message: "Failed to save exported video", - error: String(error) + error: String(r) }; } }); } -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); -async function cleanupOldRecordings() { +const K = t.dirname(S(import.meta.url)), h = t.join(d.getPath("userData"), "recordings"); +async function Q() { 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}`); - } + 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}`)); } - } catch (error) { - console.error("Failed to cleanup old recordings:", error); + } catch (e) { + console.error("Failed to cleanup old recordings:", e); } } -async function ensureRecordingsDir() { +async function X() { 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); + await p.mkdir(h, { recursive: !0 }), console.log("Recordings directory ready:", h); + } catch (e) { + console.error("Failed to create recordings directory:", e); } } -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(); +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(); } -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 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(); } -function updateTrayMenu() { - if (!tray) return; - const menuTemplate = [ +function M() { + if (!g) return; + const e = [ { label: "Stop Recording", click: () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send("stop-recording-from-tray"); - } + l && !l.isDestroyed() && l.webContents.send("stop-recording-from-tray"); } } - ]; - const contextMenu = Menu.buildFromTemplate(menuTemplate); - tray.setContextMenu(contextMenu); - tray.setToolTip(`Recording: ${selectedSourceName}`); + ], n = A.buildFromTemplate(e); + g.setContextMenu(n), g.setToolTip(`Recording: ${D}`); } -function createEditorWindowWrapper() { - if (mainWindow) { - mainWindow.close(); - mainWindow = null; - } - mainWindow = createEditorWindow(); +function ee() { + l && (l.close(), l = null), l = H(); } -function createSourceSelectorWindowWrapper() { - sourceSelectorWindow = createSourceSelectorWindow(); - sourceSelectorWindow.on("closed", () => { - sourceSelectorWindow = null; - }); - return sourceSelectorWindow; +function te() { + return k = z(), k.on("closed", () => { + k = null; + }), k; } -app.on("window-all-closed", () => { - if (process.platform !== "darwin") { - cleanupMouseTracking(); - app.quit(); - mainWindow = null; - } +d.on("window-all-closed", () => { + process.platform !== "darwin" && (I(), d.quit(), l = null); }); -app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } +d.on("activate", () => { + E.getAllWindows().length === 0 && F(); }); -app.on("before-quit", async (event) => { - event.preventDefault(); - cleanupMouseTracking(); - await cleanupOldRecordings(); - app.exit(0); +d.on("before-quit", async (e) => { + e.preventDefault(), I(), await Q(), d.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(); - } +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()); } - ); - createWindow(); + ), F(); }); export { - MAIN_DIST, - RECORDINGS_DIR, - RENDERER_DIST, - VITE_DEV_SERVER_URL + ie as MAIN_DIST, + h as RECORDINGS_DIR, + j as RENDERER_DIST, + Y as VITE_DEV_SERVER_URL }; diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index ded2edb..92d0779 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -1,48 +1 @@ -"use strict"; -const electron = require("electron"); -electron.contextBridge.exposeInMainWorld("electronAPI", { - 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); - } -}); +"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)}); diff --git a/electron-builder.json5 b/electron-builder.json5 index e347212..e6bf047 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -11,6 +11,12 @@ "dist", "dist-electron" ], + "extraResources": [ + { + "from": "public/wallpapers", + "to": "assets/wallpapers" + } + ], "mac": { "target": [ "dmg" diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 337d0b1..81e8af5 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -145,6 +145,19 @@ export function registerIpcHandlers( } }) + // Return base path for assets so renderer can resolve file:// paths in production + 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: ArrayBuffer, fileName: string) => { try { const downloadsPath = app.getPath('downloads') diff --git a/electron/preload.ts b/electron/preload.ts index 854c7e3..2b94f79 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,6 +1,10 @@ import { contextBridge, ipcRenderer } from 'electron' contextBridge.exposeInMainWorld('electronAPI', { + getAssetBasePath: async () => { + // ask main process for the correct base path (production vs dev) + return await ipcRenderer.invoke('get-asset-base-path') + }, getSources: async (opts: Electron.SourcesOptions) => { return await ipcRenderer.invoke('get-sources', opts) }, diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index ce908d4..371cde8 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -1,4 +1,6 @@ import { cn } from "@/lib/utils"; +import { useEffect } from "react"; +import { getAssetPath } from "@/lib/assetPath"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; @@ -10,7 +12,7 @@ import type { ZoomDepth, CropRegion } from "./types"; import { CropControl } from "./CropControl"; const WALLPAPER_COUNT = 12; -const WALLPAPER_PATHS = Array.from({ length: WALLPAPER_COUNT }, (_, i) => `/wallpapers/wallpaper${i + 1}.jpg`); +const WALLPAPER_RELATIVE = Array.from({ length: WALLPAPER_COUNT }, (_, i) => `wallpapers/wallpaper${i + 1}.jpg`); const GRADIENTS = [ "linear-gradient( 111.6deg, rgba(114,167,232,1) 9.4%, rgba(253,129,82,1) 43.9%, rgba(253,129,82,1) 54.8%, rgba(249,202,86,1) 86.3% )", "linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%)", @@ -54,6 +56,20 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [ ]; export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onZoomDepthChange, selectedZoomId, onZoomDelete, showShadow, onShadowChange, showBlur, onBlurChange, cropRegion, onCropChange, videoElement, onExport }: SettingsPanelProps) { + const [wallpaperPaths, setWallpaperPaths] = useState([]); + + useEffect(() => { + let mounted = true + ;(async () => { + try { + const resolved = await Promise.all(WALLPAPER_RELATIVE.map(p => getAssetPath(p))) + if (mounted) setWallpaperPaths(resolved) + } catch (err) { + if (mounted) setWallpaperPaths(WALLPAPER_RELATIVE.map(p => `/${p}`)) + } + })() + return () => { mounted = false } + }, []) const [hsva, setHsva] = useState({ h: 0, s: 0, v: 68, a: 1 }); const [gradient, setGradient] = useState(GRADIENTS[0]); const [showCropDropdown, setShowCropDropdown] = useState(false); @@ -191,21 +207,38 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
- {WALLPAPER_PATHS.map((path, idx) => ( -
onWallpaperChange(path)} - role="button" - /> - ))} + {(wallpaperPaths.length > 0 ? wallpaperPaths : WALLPAPER_RELATIVE.map(p => `/${p}`)).map((path, idx) => { + const isSelected = (() => { + if (!selected) return false; + // exact match + if (selected === path) return true; + // file:// vs absolute path mismatch: compare by filename suffix + 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 { + // ignore + } + return false; + })(); + + return ( +
onWallpaperChange(path)} + role="button" + /> + ) + })}
diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index e8208a7..ad7d253 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -1,5 +1,6 @@ import type React from "react"; import { useEffect, useRef, useImperativeHandle, forwardRef, useState, useMemo, useCallback } from "react"; +import { getAssetPath } from "@/lib/assetPath"; import * as PIXI from 'pixi.js'; import { ZOOM_DEPTH_SCALES, type ZoomRegion, type ZoomFocus, type ZoomDepth } from "./types"; import { DEFAULT_FOCUS, SMOOTHING_FACTOR, MIN_DELTA } from "./videoPlayback/constants"; @@ -677,10 +678,51 @@ const VideoPlayback = forwardRef(({ setVideoReady(true); }; - const isImageUrl = wallpaper?.startsWith('/wallpapers/') || wallpaper?.startsWith('http'); - const backgroundStyle = isImageUrl - ? { backgroundImage: `url(${wallpaper || '/wallpapers/wallpaper1.jpg'})` } - : { background: wallpaper || '/wallpapers/wallpaper1.jpg' }; + const [resolvedWallpaper, setResolvedWallpaper] = useState(null); + + useEffect(() => { + let mounted = true + ;(async () => { + try { + if (!wallpaper) { + const def = await getAssetPath('wallpapers/wallpaper1.jpg') + if (mounted) setResolvedWallpaper(def) + return + } + + // If it's a solid color or CSS gradient, use it directly + if (wallpaper.startsWith('#') || wallpaper.startsWith('linear-gradient') || wallpaper.startsWith('radial-gradient')) { + if (mounted) setResolvedWallpaper(wallpaper) + return + } + + // If it's an absolute web/http or file path, use as-is + if (wallpaper.startsWith('http') || wallpaper.startsWith('file://') || wallpaper.startsWith('/')) { + // If it's an absolute server path (starts with '/'), resolve via getAssetPath as well + if (wallpaper.startsWith('/')) { + const rel = wallpaper.replace(/^\//, '') + const p = await getAssetPath(rel) + if (mounted) setResolvedWallpaper(p) + return + } + if (mounted) setResolvedWallpaper(wallpaper) + return + } + + // Otherwise assume it's a relative path like 'wallpapers/wallpaper1.jpg' + const p = await getAssetPath(wallpaper.replace(/^\//, '')) + if (mounted) setResolvedWallpaper(p) + } catch (err) { + if (mounted) setResolvedWallpaper(wallpaper || '/wallpapers/wallpaper1.jpg') + } + })() + return () => { mounted = false } + }, [wallpaper]) + + const isImageUrl = Boolean(resolvedWallpaper && (resolvedWallpaper.startsWith('file://') || resolvedWallpaper.startsWith('http') || resolvedWallpaper.startsWith('/'))) + const backgroundStyle = isImageUrl + ? { backgroundImage: `url(${resolvedWallpaper || ''})` } + : { background: resolvedWallpaper || '' }; return (
diff --git a/src/lib/assetPath.ts b/src/lib/assetPath.ts new file mode 100644 index 0000000..ccc58bf --- /dev/null +++ b/src/lib/assetPath.ts @@ -0,0 +1,25 @@ +export async function getAssetPath(relativePath: string): Promise { + try { + if (typeof window !== 'undefined') { + // If running in a dev server (http/https), prefer the web-served path + if (window.location && window.location.protocol && window.location.protocol.startsWith('http')) { + return `/${relativePath.replace(/^\//, '')}` + } + + if ((window as any).electronAPI && typeof (window as any).electronAPI.getAssetBasePath === 'function') { + const base = await (window as any).electronAPI.getAssetBasePath() + if (base) { + const normalized = base.replace(/\\/g, '/') + return `file://${normalized}/${relativePath}` + } + } + } + } catch (err) { + // ignore and use fallback + } + + // Fallback for web/dev server: public/wallpapers are served at '/wallpapers/...' + return `/${relativePath.replace(/^\//, '')}` +} + +export default getAssetPath; diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 5440f4e..a4b7b37 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -124,14 +124,21 @@ export class FrameRenderer { try { // Render background based on type - if (wallpaper.startsWith('/') || wallpaper.startsWith('http')) { + if (wallpaper.startsWith('file://') || wallpaper.startsWith('data:') || wallpaper.startsWith('/') || wallpaper.startsWith('http')) { // Image background const img = new Image(); // Don't set crossOrigin for same-origin images to avoid CORS taint // Only set it for cross-origin URLs - const imageUrl = wallpaper.startsWith('http') ? wallpaper : window.location.origin + wallpaper; - if (wallpaper.startsWith('http') && !imageUrl.startsWith(window.location.origin)) { - img.crossOrigin = 'anonymous'; + let imageUrl: string; + if (wallpaper.startsWith('http')) { + imageUrl = wallpaper; + if (!imageUrl.startsWith(window.location.origin)) { + img.crossOrigin = 'anonymous'; + } + } else if (wallpaper.startsWith('file://') || wallpaper.startsWith('data:')) { + imageUrl = wallpaper; + } else { + imageUrl = window.location.origin + wallpaper; } await new Promise((resolve, reject) => { diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index aea0b03..c5ec8ec 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -37,6 +37,7 @@ interface Window { message?: string error?: string }> + getAssetBasePath: () => Promise setRecordingState: (recording: boolean) => Promise onStopRecordingFromTray: (callback: () => void) => () => void openExternalUrl: (url: string) => Promise<{ success: boolean; error?: string }>