diff --git a/dist-electron/main.js b/dist-electron/main.js index fe1bfeb..d7413f4 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,11 +1,16 @@ -import { ipcMain as i, screen as b, BrowserWindow as R, desktopCapturer as V, shell as O, app as d, dialog as S, nativeImage as W, Tray as k, Menu as L } from "electron"; -import { fileURLToPath as E } from "node:url"; -import o from "node:path"; -import P from "node:fs/promises"; -const _ = o.dirname(E(import.meta.url)), U = o.join(_, ".."), m = process.env.VITE_DEV_SERVER_URL, T = o.join(U, "dist"); -let f = null; -i.on("hud-overlay-hide", () => { - f && !f.isDestroyed() && f.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$2 = path.dirname(fileURLToPath(import.meta.url)); +const APP_ROOT = path.join(__dirname$2, ".."); +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 C() { const r = b.getPrimaryDisplay(), { workArea: n } = r, c = 500, w = 100, y = Math.floor(n.x + (n.width - c) / 2), h = Math.floor(n.y + n.height - w - 5), e = new R({ @@ -24,10 +29,10 @@ function C() { skipTaskbar: !0, hasShadow: !1, webPreferences: { - preload: o.join(_, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0, - backgroundThrottling: !1 + preload: path.join(__dirname$2, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + backgroundThrottling: false } }); return e.webContents.on("did-finish-load", () => { @@ -53,11 +58,11 @@ function M() { title: "OpenScreen", backgroundColor: "#000000", webPreferences: { - preload: o.join(_, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0, - webSecurity: !1, - backgroundThrottling: !1 + preload: path.join(__dirname$2, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + webSecurity: false, + backgroundThrottling: false } }); return r.maximize(), r.webContents.on("did-finish-load", () => { @@ -80,9 +85,9 @@ function A() { transparent: !0, backgroundColor: "#00000000", webPreferences: { - preload: o.join(_, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0 + preload: path.join(__dirname$2, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true } }); return m ? c.loadURL(m + "?windowType=source-selector") : c.loadFile(o.join(T, "index.html"), { @@ -199,23 +204,42 @@ function H(r, n, c, w, y) { }; } }); - let h = null; - i.handle("set-current-video-path", (e, s) => (h = s, { success: !0 })), i.handle("get-current-video-path", () => h ? { success: !0, path: h } : { success: !1 }), i.handle("clear-current-video-path", () => (h = null, { success: !0 })); + 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 z = o.dirname(E(import.meta.url)), p = o.join(d.getPath("userData"), "recordings"); -async function N() { +const __dirname$1 = path.dirname(fileURLToPath(import.meta.url)); +const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); +async function ensureRecordingsDir() { try { await P.mkdir(p, { recursive: !0 }), console.log("RECORDINGS_DIR:", p), console.log("User Data Path:", d.getPath("userData")); } catch (r) { console.error("Failed to create recordings directory:", r); } } -process.env.APP_ROOT = o.join(z, ".."); -const B = process.env.VITE_DEV_SERVER_URL, Y = o.join(process.env.APP_ROOT, "dist-electron"), D = o.join(process.env.APP_ROOT, "dist"); -process.env.VITE_PUBLIC = B ? o.join(process.env.APP_ROOT, "public") : D; -let l = null, g = null, u = null, x = ""; -function I() { - l = C(); +process.env.APP_ROOT = path.join(__dirname$1, ".."); +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 q() { const r = o.join(process.env.VITE_PUBLIC || D, "rec-button.png"); diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 5b8cb40..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")}); +"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/electron/electron-env.d.ts b/electron/electron-env.d.ts index 9328bbb..dba3f16 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -39,6 +39,7 @@ interface Window { setCurrentVideoPath: (path: string) => Promise<{ success: boolean }> getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }> clearCurrentVideoPath: () => Promise<{ success: boolean }> + getPlatform: () => Promise hudOverlayHide: () => void; hudOverlayClose: () => void; } diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index f83d3a8..34c9886 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -208,4 +208,8 @@ export function registerIpcHandlers( currentVideoPath = null; return { success: true }; }); + + ipcMain.handle('get-platform', () => { + return process.platform; + }); } diff --git a/electron/preload.ts b/electron/preload.ts index e8644e4..02fcc97 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -60,4 +60,7 @@ contextBridge.exposeInMainWorld('electronAPI', { clearCurrentVideoPath: () => { return ipcRenderer.invoke('clear-current-video-path') }, + getPlatform: () => { + return ipcRenderer.invoke('get-platform') + }, }) \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4382595..3e3aba0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: react-icons: specifier: ^5.5.0 version: 5.5.0(react@18.3.1) + react-resizable-panels: + specifier: ^3.0.6 + version: 3.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3266,6 +3269,12 @@ packages: '@types/react': optional: true + react-resizable-panels@3.0.6: + resolution: {integrity: sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -7302,6 +7311,11 @@ snapshots: optionalDependencies: '@types/react': 18.3.27 + react-resizable-panels@3.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): dependencies: get-nonce: 1.0.1 diff --git a/src/components/video-editor/KeyboardShortcutsHelp.tsx b/src/components/video-editor/KeyboardShortcutsHelp.tsx index 369146a..3edbd04 100644 --- a/src/components/video-editor/KeyboardShortcutsHelp.tsx +++ b/src/components/video-editor/KeyboardShortcutsHelp.tsx @@ -1,6 +1,28 @@ import { HelpCircle } from "lucide-react"; +import { useState, useEffect } from "react"; +import { formatShortcut } from "@/utils/platformUtils"; export function KeyboardShortcutsHelp() { + const [shortcuts, setShortcuts] = useState({ + delete: 'Ctrl + D', + pan: 'Shift + Ctrl + Scroll', + zoom: 'Ctrl + Scroll' + }); + + useEffect(() => { + Promise.all([ + formatShortcut(['mod', 'D']), + formatShortcut(['shift', 'mod', 'Scroll']), + formatShortcut(['mod', 'Scroll']) + ]).then(([deleteKey, panKey, zoomKey]) => { + setShortcuts({ + delete: deleteKey, + pan: panKey, + zoom: zoomKey + }); + }); + }, []); + return (
@@ -25,15 +47,15 @@ export function KeyboardShortcutsHelp() {
Delete Selected - ⌘ + D + {shortcuts.delete}
Pan Timeline - ⇧ + ⌘ + Scroll + {shortcuts.pan}
Zoom Timeline - ⌘ + Scroll + {shortcuts.zoom}
Pause/Play diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index 1e1c6b6..1cc56d7 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -18,6 +18,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { type AspectRatio, getAspectRatioLabel } from "@/utils/aspectRatioUtils"; +import { formatShortcut } from "@/utils/platformUtils"; const ZOOM_ROW_ID = "row-zoom"; const TRIM_ROW_ID = "row-trim"; @@ -520,6 +521,18 @@ export default function TimelineEditor({ const [range, setRange] = useState(() => createInitialRange(totalMs)); const [keyframes, setKeyframes] = useState<{ id: string; time: number }[]>([]); const [selectedKeyframeId, setSelectedKeyframeId] = useState(null); + const [shortcuts, setShortcuts] = useState({ + pan: 'Shift + Ctrl + Scroll', + zoom: 'Ctrl + Scroll' + }); + + useEffect(() => { + formatShortcut(['shift', 'mod', 'Scroll']).then(pan => { + formatShortcut(['mod', 'Scroll']).then(zoom => { + setShortcuts({ pan, zoom }); + }); + }); + }, []); // Add keyframe at current playhead position const addKeyframe = useCallback(() => { @@ -898,11 +911,11 @@ export default function TimelineEditor({
- ⇧ + ⌘ + Scroll + {shortcuts.pan} Pan - ⌘ + Scroll + {shortcuts.zoom} Zoom
diff --git a/src/utils/platformUtils.ts b/src/utils/platformUtils.ts new file mode 100644 index 0000000..6d47997 --- /dev/null +++ b/src/utils/platformUtils.ts @@ -0,0 +1,60 @@ +let cachedPlatform: string | null = null; + +/** + * Gets the current platform from Electron + */ +const getPlatform = async (): Promise => { + if (cachedPlatform) return cachedPlatform; + + try { + const platform = await window.electronAPI.getPlatform(); + cachedPlatform = platform; + return platform; + } catch (error) { + console.warn('Failed to get platform from Electron, falling back to navigator:', error); + // Fallback for development/testing + let fallbackPlatform = 'win32'; + if (typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform)) { + fallbackPlatform = 'darwin'; + } + cachedPlatform = fallbackPlatform; + return fallbackPlatform; + } +}; + +/** + * Detects if the current platform is macOS + */ +export const isMac = async (): Promise => { + const platform = await getPlatform(); + return platform === 'darwin'; +}; + +/** + * Gets the modifier key symbol based on the platform + */ +export const getModifierKey = async (): Promise => { + return (await isMac()) ? '⌘' : 'Ctrl'; +}; + +/** + * Gets the shift key symbol based on the platform + */ +export const getShiftKey = async (): Promise => { + return (await isMac()) ? '⇧' : 'Shift'; +}; + +/** + * Formats a keyboard shortcut for display based on the platform + * @param keys Array of key combinations (e.g., ['mod', 'D'] or ['shift', 'mod', 'Scroll']) + */ +export const formatShortcut = async (keys: string[]): Promise => { + const isMacPlatform = await isMac(); + return keys + .map(key => { + if (key.toLowerCase() === 'mod') return isMacPlatform ? '⌘' : 'Ctrl'; + if (key.toLowerCase() === 'shift') return isMacPlatform ? '⇧' : 'Shift'; + return key; + }) + .join(' + '); +}; \ No newline at end of file