From f34bd191831d93f118a50aedecf222b8e18abfa6 Mon Sep 17 00:00:00 2001 From: Alessandro Spisso <5341363+ilGianfri@users.noreply.github.com> Date: Thu, 4 Dec 2025 23:53:25 +0100 Subject: [PATCH] feat: implement platform-aware keyboard shortcuts and add IPC handler for platform detection --- dist-electron/main.js | 17 ++++--- dist-electron/preload.mjs | 3 ++ electron/electron-env.d.ts | 1 + electron/ipc/handlers.ts | 4 ++ electron/preload.ts | 3 ++ pnpm-lock.yaml | 14 ++++++ .../video-editor/KeyboardShortcutsHelp.tsx | 27 +++++++++-- .../video-editor/timeline/TimelineEditor.tsx | 16 ++++++- src/utils/platformUtils.ts | 45 ++++++++++++++----- 9 files changed, 108 insertions(+), 22 deletions(-) diff --git a/dist-electron/main.js b/dist-electron/main.js index 84b48e9..74485ec 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -2,8 +2,8 @@ import { ipcMain, screen, BrowserWindow, desktopCapturer, shell, app, dialog, na 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 __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; @@ -35,7 +35,7 @@ function createHudOverlayWindow() { skipTaskbar: true, hasShadow: false, webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), + preload: path.join(__dirname$2, "preload.mjs"), nodeIntegration: false, contextIsolation: true, backgroundThrottling: false @@ -74,7 +74,7 @@ function createEditorWindow() { title: "OpenScreen", backgroundColor: "#000000", webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), + preload: path.join(__dirname$2, "preload.mjs"), nodeIntegration: false, contextIsolation: true, webSecurity: false, @@ -109,7 +109,7 @@ function createSourceSelectorWindow() { transparent: true, backgroundColor: "#00000000", webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), + preload: path.join(__dirname$2, "preload.mjs"), nodeIntegration: false, contextIsolation: true } @@ -292,8 +292,11 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g currentVideoPath = null; return { success: true }; }); + ipcMain.handle("get-platform", () => { + return process.platform; + }); } -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const __dirname$1 = path.dirname(fileURLToPath(import.meta.url)); const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); async function ensureRecordingsDir() { try { @@ -304,7 +307,7 @@ async function ensureRecordingsDir() { console.error("Failed to create recordings directory:", error); } } -process.env.APP_ROOT = path.join(__dirname, ".."); +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"); diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 42f5816..cb59604 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -56,5 +56,8 @@ electron.contextBridge.exposeInMainWorld("electronAPI", { }, 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 0cf0b66..f005e96 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -207,4 +207,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 453c613..bab0829 100644 --- a/src/components/video-editor/KeyboardShortcutsHelp.tsx +++ b/src/components/video-editor/KeyboardShortcutsHelp.tsx @@ -1,7 +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 (
@@ -22,15 +43,15 @@ export function KeyboardShortcutsHelp() {
Delete Selected - {formatShortcut(['mod', 'D'])} + {shortcuts.delete}
Pan Timeline - {formatShortcut(['shift', 'mod', 'Scroll'])} + {shortcuts.pan}
Zoom Timeline - {formatShortcut(['mod', 'Scroll'])} + {shortcuts.zoom}
diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index d383b79..23fc92f 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -434,6 +434,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(() => { @@ -724,11 +736,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 index 4c63b81..6d47997 100644 --- a/src/utils/platformUtils.ts +++ b/src/utils/platformUtils.ts @@ -1,34 +1,59 @@ +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 = (): boolean => { - if (typeof navigator === 'undefined') return false; - return /Mac|iPhone|iPad|iPod/.test(navigator.platform); +export const isMac = async (): Promise => { + const platform = await getPlatform(); + return platform === 'darwin'; }; /** * Gets the modifier key symbol based on the platform */ -export const getModifierKey = (): string => { - return isMac() ? '⌘' : 'Ctrl'; +export const getModifierKey = async (): Promise => { + return (await isMac()) ? '⌘' : 'Ctrl'; }; /** * Gets the shift key symbol based on the platform */ -export const getShiftKey = (): string => { - return isMac() ? '⇧' : 'Shift'; +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 = (keys: string[]): string => { +export const formatShortcut = async (keys: string[]): Promise => { + const isMacPlatform = await isMac(); return keys .map(key => { - if (key.toLowerCase() === 'mod') return getModifierKey(); - if (key.toLowerCase() === 'shift') return getShiftKey(); + if (key.toLowerCase() === 'mod') return isMacPlatform ? '⌘' : 'Ctrl'; + if (key.toLowerCase() === 'shift') return isMacPlatform ? '⇧' : 'Shift'; return key; }) .join(' + ');