feat: implement platform-aware keyboard shortcuts and add IPC handler for platform detection
This commit is contained in:
+10
-7
@@ -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");
|
||||
|
||||
@@ -56,5 +56,8 @@ electron.contextBridge.exposeInMainWorld("electronAPI", {
|
||||
},
|
||||
clearCurrentVideoPath: () => {
|
||||
return electron.ipcRenderer.invoke("clear-current-video-path");
|
||||
},
|
||||
getPlatform: () => {
|
||||
return electron.ipcRenderer.invoke("get-platform");
|
||||
}
|
||||
});
|
||||
|
||||
Vendored
+1
@@ -39,6 +39,7 @@ interface Window {
|
||||
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>
|
||||
getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>
|
||||
clearCurrentVideoPath: () => Promise<{ success: boolean }>
|
||||
getPlatform: () => Promise<string>
|
||||
hudOverlayHide: () => void;
|
||||
hudOverlayClose: () => void;
|
||||
}
|
||||
|
||||
@@ -207,4 +207,8 @@ export function registerIpcHandlers(
|
||||
currentVideoPath = null;
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
ipcMain.handle('get-platform', () => {
|
||||
return process.platform;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,4 +60,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
clearCurrentVideoPath: () => {
|
||||
return ipcRenderer.invoke('clear-current-video-path')
|
||||
},
|
||||
getPlatform: () => {
|
||||
return ipcRenderer.invoke('get-platform')
|
||||
},
|
||||
})
|
||||
Generated
+14
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<div className="relative group">
|
||||
<HelpCircle className="w-4 h-4 text-slate-500 hover:text-[#34B27B] transition-colors cursor-help" />
|
||||
@@ -22,15 +43,15 @@ export function KeyboardShortcutsHelp() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-400">Delete Selected</span>
|
||||
<kbd className="px-1 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-mono">{formatShortcut(['mod', 'D'])}</kbd>
|
||||
<kbd className="px-1 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-mono">{shortcuts.delete}</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-400">Pan Timeline</span>
|
||||
<kbd className="px-1 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-mono">{formatShortcut(['shift', 'mod', 'Scroll'])}</kbd>
|
||||
<kbd className="px-1 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-mono">{shortcuts.pan}</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-400">Zoom Timeline</span>
|
||||
<kbd className="px-1 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-mono">{formatShortcut(['mod', 'Scroll'])}</kbd>
|
||||
<kbd className="px-1 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-mono">{shortcuts.zoom}</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -434,6 +434,18 @@ export default function TimelineEditor({
|
||||
const [range, setRange] = useState<Range>(() => createInitialRange(totalMs));
|
||||
const [keyframes, setKeyframes] = useState<{ id: string; time: number }[]>([]);
|
||||
const [selectedKeyframeId, setSelectedKeyframeId] = useState<string | null>(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({
|
||||
<div className="flex-1" />
|
||||
<div className="flex items-center gap-4 text-[10px] text-slate-500 font-medium">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<kbd className="px-1.5 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-sans">⇧ + ⌘ + Scroll</kbd>
|
||||
<kbd className="px-1.5 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-sans">{shortcuts.pan}</kbd>
|
||||
<span>Pan</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<kbd className="px-1.5 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-sans">⌘ + Scroll</kbd>
|
||||
<kbd className="px-1.5 py-0.5 bg-white/5 border border-white/10 rounded text-[#34B27B] font-sans">{shortcuts.zoom}</kbd>
|
||||
<span>Zoom</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+35
-10
@@ -1,34 +1,59 @@
|
||||
let cachedPlatform: string | null = null;
|
||||
|
||||
/**
|
||||
* Gets the current platform from Electron
|
||||
*/
|
||||
const getPlatform = async (): Promise<string> => {
|
||||
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<boolean> => {
|
||||
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<string> => {
|
||||
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<string> => {
|
||||
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<string> => {
|
||||
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(' + ');
|
||||
|
||||
Reference in New Issue
Block a user