From 391938049b24562a750a9c99e67ca5955816ccbf Mon Sep 17 00:00:00 2001
From: Alessandro Spisso <5341363+ilGianfri@users.noreply.github.com>
Date: Mon, 1 Dec 2025 00:19:34 +0100
Subject: [PATCH 1/2] Add platform-aware keyboard shortcut formatting
Introduces a new utility (platformUtils.ts) to format keyboard shortcuts based on the user's platform (macOS or others). Updates KeyboardShortcutsHelp and TimelineEditor to use the new formatShortcut function for displaying shortcuts, ensuring correct symbols are shown for modifier keys.
---
.../video-editor/KeyboardShortcutsHelp.tsx | 7 ++--
.../video-editor/timeline/TimelineEditor.tsx | 1 +
src/utils/platformUtils.ts | 35 +++++++++++++++++++
3 files changed, 40 insertions(+), 3 deletions(-)
create mode 100644 src/utils/platformUtils.ts
diff --git a/src/components/video-editor/KeyboardShortcutsHelp.tsx b/src/components/video-editor/KeyboardShortcutsHelp.tsx
index b4cd2e4..453c613 100644
--- a/src/components/video-editor/KeyboardShortcutsHelp.tsx
+++ b/src/components/video-editor/KeyboardShortcutsHelp.tsx
@@ -1,4 +1,5 @@
import { HelpCircle } from "lucide-react";
+import { formatShortcut } from "@/utils/platformUtils";
export function KeyboardShortcutsHelp() {
return (
@@ -21,15 +22,15 @@ export function KeyboardShortcutsHelp() {
Delete Selected
- ⌘ + D
+ {formatShortcut(['mod', 'D'])}
Pan Timeline
- ⇧ + ⌘ + Scroll
+ {formatShortcut(['shift', 'mod', 'Scroll'])}
Zoom Timeline
- ⌘ + Scroll
+ {formatShortcut(['mod', 'Scroll'])}
diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx
index 907081d..d383b79 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";
diff --git a/src/utils/platformUtils.ts b/src/utils/platformUtils.ts
new file mode 100644
index 0000000..4c63b81
--- /dev/null
+++ b/src/utils/platformUtils.ts
@@ -0,0 +1,35 @@
+/**
+ * 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);
+};
+
+/**
+ * Gets the modifier key symbol based on the platform
+ */
+export const getModifierKey = (): string => {
+ return isMac() ? '⌘' : 'Ctrl';
+};
+
+/**
+ * Gets the shift key symbol based on the platform
+ */
+export const getShiftKey = (): string => {
+ return 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 => {
+ return keys
+ .map(key => {
+ if (key.toLowerCase() === 'mod') return getModifierKey();
+ if (key.toLowerCase() === 'shift') return getShiftKey();
+ return key;
+ })
+ .join(' + ');
+};
\ No newline at end of file
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 2/2] 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(' + ');