diff --git a/apps/web/src/components/keybinding-editor.tsx b/apps/web/src/components/keybinding-editor.tsx
deleted file mode 100644
index bc98c5fd..00000000
--- a/apps/web/src/components/keybinding-editor.tsx
+++ /dev/null
@@ -1,425 +0,0 @@
-"use client";
-
-import { useState, useEffect } from "react";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Badge } from "@/components/ui/badge";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog";
-import { useKeybindingsStore } from "@/stores/keybindings-store";
-import { Action } from "@/constants/actions";
-import { KeyboardShortcut } from "@/hooks/use-keyboard-shortcuts-help";
-import {
- Settings,
- RotateCcw,
- Download,
- Upload,
- X,
- Check,
- AlertTriangle,
-} from "lucide-react";
-import { toast } from "sonner";
-
-interface KeybindingEditorProps {
- shortcuts: KeyboardShortcut[];
- onClose: () => void;
-}
-
-interface KeyRecorderProps {
- value: string;
- onValueChange: (value: string) => void;
- onCancel: () => void;
-}
-
-const KeyRecorder = ({ value, onValueChange, onCancel }: KeyRecorderProps) => {
- const [isRecording, setIsRecording] = useState(false);
- const [recordedKey, setRecordedKey] = useState("");
- const { getKeybindingString } = useKeybindingsStore();
-
- useEffect(() => {
- if (!isRecording) return;
-
- const handleKeyDown = (e: KeyboardEvent) => {
- e.preventDefault();
-
- const keyString = getKeybindingString(e);
- if (keyString) {
- setRecordedKey(keyString);
- }
- };
-
- document.addEventListener("keydown", handleKeyDown);
- return () => document.removeEventListener("keydown", handleKeyDown);
- }, [isRecording, getKeybindingString]);
-
- const handleStartRecording = () => {
- setIsRecording(true);
- setRecordedKey("");
- };
-
- const handleConfirm = () => {
- onValueChange(recordedKey);
- setIsRecording(false);
- setRecordedKey("");
- };
-
- const handleCancel = () => {
- setIsRecording(false);
- setRecordedKey("");
- onCancel();
- };
-
- const displayKey = recordedKey || value;
-
- return (
-
-
- {isRecording ? (
-
-
-
-
-
-
-
-
- ) : (
-
- Record
-
- )}
-
- );
-};
-
-export const KeybindingEditor = ({
- shortcuts,
- onClose,
-}: KeybindingEditorProps) => {
- const {
- keybindings,
- updateKeybinding,
- removeKeybinding,
- resetToDefaults,
- isCustomized,
- validateKeybinding,
- getKeybindingsForAction,
- exportKeybindings,
- importKeybindings,
- } = useKeybindingsStore();
-
- const [editingShortcut, setEditingShortcut] = useState(null);
- const [newKeyBinding, setNewKeyBinding] = useState("");
- const [showResetDialog, setShowResetDialog] = useState(false);
- const [searchTerm, setSearchTerm] = useState("");
- const [selectedCategory, setSelectedCategory] = useState("all");
-
- const categories = [
- "all",
- ...Array.from(new Set(shortcuts.map((s) => s.category))),
- ];
-
- const filteredShortcuts = shortcuts.filter((shortcut) => {
- const matchesSearch =
- shortcut.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
- shortcut.keys.some((key) =>
- key.toLowerCase().includes(searchTerm.toLowerCase())
- );
- const matchesCategory =
- selectedCategory === "all" || shortcut.category === selectedCategory;
- return matchesSearch && matchesCategory;
- });
-
- const handleEditShortcut = (shortcut: KeyboardShortcut) => {
- setEditingShortcut(shortcut.id);
- setNewKeyBinding(shortcut.keys[0] || "");
- };
-
- const handleSaveShortcut = () => {
- if (!editingShortcut || !newKeyBinding) return;
-
- const shortcut = shortcuts.find((s) => s.id === editingShortcut);
- if (!shortcut) return;
-
- // Validate the new keybinding
- const conflict = validateKeybinding(newKeyBinding, shortcut.action);
- if (conflict) {
- toast.error(
- `Key "${newKeyBinding}" is already bound to "${conflict.existingAction}"`
- );
- return;
- }
-
- // Remove old keybindings for this action
- const oldKeys = getKeybindingsForAction(shortcut.action);
- oldKeys.forEach((key) => removeKeybinding(key));
-
- // Add new keybinding
- updateKeybinding(newKeyBinding, shortcut.action);
-
- setEditingShortcut(null);
- setNewKeyBinding("");
- toast.success("Keybinding updated successfully");
- };
-
- const handleCancelEdit = () => {
- setEditingShortcut(null);
- setNewKeyBinding("");
- };
-
- const handleRemoveShortcut = (shortcut: KeyboardShortcut) => {
- const keys = getKeybindingsForAction(shortcut.action);
- keys.forEach((key) => removeKeybinding(key));
- toast.success("Keybinding removed");
- };
-
- const handleResetToDefaults = () => {
- resetToDefaults();
- setShowResetDialog(false);
- toast.success("Keybindings reset to defaults");
- };
-
- const handleExportKeybindings = () => {
- const config = exportKeybindings();
- const blob = new Blob([JSON.stringify(config, null, 2)], {
- type: "application/json",
- });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = "opencut-keybindings.json";
- a.click();
- URL.revokeObjectURL(url);
- toast.success("Keybindings exported");
- };
-
- const handleImportKeybindings = (
- event: React.ChangeEvent
- ) => {
- const file = event.target.files?.[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.onload = (e) => {
- try {
- const config = JSON.parse(e.target?.result as string);
- // Validate config structure
- if (!config || typeof config !== "object") {
- throw new Error("Invalid configuration format");
- }
-
- // Validate each keybinding
- for (const [key, action] of Object.entries(config)) {
- if (typeof key !== "string" || typeof action !== "string") {
- throw new Error(`Invalid keybinding: ${key} -> ${action}`);
- }
- }
- importKeybindings(config);
- toast.success("Keybindings imported successfully");
- } catch (error) {
- toast.error(`Failed to import keybindings: ${error}`);
- }
- };
- reader.readAsText(file);
- };
-
- return (
-
-
-
-
-
- Customize Keyboard Shortcuts
-
- {isCustomized && (
-
- Modified
-
- )}
-
-
-
-
- Export
-
-
-
- document.getElementById("import-keybindings")?.click()
- }
- >
-
- Import
-
- setShowResetDialog(true)}
- disabled={!isCustomized}
- >
-
- Reset to Defaults
-
-
-
- Close
-
-
-
-
-
- setSearchTerm(e.target.value)}
- className="flex-1"
- />
-
-
- {categories.map((category) => (
-
- {category === "all" ? "All" : category}
-
- ))}
-
-
-
-
-
- {filteredShortcuts.map((shortcut) => (
-
-
-
-
-
-
-
{shortcut.description}
-
- {shortcut.category}
-
-
-
-
-
-
- {editingShortcut === shortcut.id ? (
-
- ) : (
-
- {shortcut.keys.map((key, index) => (
-
- {key}
-
- ))}
- {shortcut.keys.length === 0 && (
-
- No binding
-
- )}
-
- )}
-
-
- {editingShortcut === shortcut.id ? (
- <>
-
- Save
-
-
- Cancel
-
- >
- ) : (
- <>
- handleEditShortcut(shortcut)}
- >
- Edit
-
- handleRemoveShortcut(shortcut)}
- disabled={shortcut.keys.length === 0}
- >
- Remove
-
- >
- )}
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
- Reset Keyboard Shortcuts?
-
-
- This will reset all keyboard shortcuts to their default values.
- Any custom keybindings will be lost.
-
-
-
- Cancel
-
- Reset to Defaults
-
-
-
-
-
- );
-};
diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx
index 0481b1dd..91cfb01a 100644
--- a/apps/web/src/components/keyboard-shortcuts-help.tsx
+++ b/apps/web/src/components/keyboard-shortcuts-help.tsx
@@ -1,7 +1,7 @@
"use client";
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { Button } from "./ui/button";
import {
Dialog,
@@ -12,13 +12,13 @@ import {
DialogTrigger,
} from "./ui/dialog";
import { getPlatformSpecialKey } from "@/lib/utils";
-import { Badge } from "./ui/badge";
-import { Keyboard, Settings } from "lucide-react";
+import { Keyboard } from "lucide-react";
import {
useKeyboardShortcutsHelp,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts-help";
-import { KeybindingEditor } from "./keybinding-editor";
+import { useKeybindingsStore } from "@/stores/keybindings-store";
+import { toast } from "sonner";
const modifier: {
[key: string]: string;
@@ -37,7 +37,15 @@ function getKeyWithModifier(key: string) {
return modifier[key] || key;
}
-const ShortcutItem = ({ shortcut }: { shortcut: KeyboardShortcut }) => {
+const ShortcutItem = ({
+ shortcut,
+ recordingKey,
+ onStartRecording
+}: {
+ shortcut: KeyboardShortcut;
+ recordingKey: string | null;
+ onStartRecording: (keyId: string, shortcut: KeyboardShortcut) => void;
+}) => {
// Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J"
const displayKeys = shortcut.keys.filter((key: string) => {
if (
@@ -61,11 +69,21 @@ const ShortcutItem = ({ shortcut }: { shortcut: KeyboardShortcut }) => {
{displayKeys.map((key: string, index: number) => (
- {key.split("+").map((keyPart: string, partIndex: number) => (
-
- {getKeyWithModifier(keyPart)}
-
- ))}
+ {key.split("+").map((keyPart: string, partIndex: number) => {
+ const keyId = `${shortcut.id}-${index}-${partIndex}`;
+ return (
+ onStartRecording(keyId, shortcut)}
+ >
+ {getKeyWithModifier(keyPart)}
+
+ );
+ })}
{index < displayKeys.length - 1 && (
or
@@ -77,33 +95,110 @@ const ShortcutItem = ({ shortcut }: { shortcut: KeyboardShortcut }) => {
);
};
+const EditableShortcutKey = ({
+ children,
+ keyId,
+ originalKey,
+ shortcut,
+ isRecording,
+ onStartRecording
+}: {
+ children: React.ReactNode;
+ keyId: string;
+ originalKey: string;
+ shortcut: KeyboardShortcut;
+ isRecording: boolean;
+ onStartRecording: () => void;
+}) => {
+ const handleClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onStartRecording();
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
export const KeyboardShortcutsHelp = () => {
const [open, setOpen] = useState(false);
- const [showEditor, setShowEditor] = useState(false);
+ const [recordingKey, setRecordingKey] = useState
(null);
+ const [recordingShortcut, setRecordingShortcut] = useState(null);
+
+ const {
+ updateKeybinding,
+ removeKeybinding,
+ getKeybindingString,
+ validateKeybinding,
+ getKeybindingsForAction,
+ } = useKeybindingsStore();
// Get shortcuts from centralized hook
const { shortcuts } = useKeyboardShortcutsHelp();
const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
- if (showEditor) {
- return (
-
-
-
-
- Shortcuts
-
-
-
- setShowEditor(false)}
- />
-
-
- );
- }
+ useEffect(() => {
+ if (!recordingKey || !recordingShortcut) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const keyString = getKeybindingString(e);
+ if (keyString) {
+ // Auto-save the new keybinding
+ const conflict = validateKeybinding(keyString, recordingShortcut.action);
+ if (conflict) {
+ toast.error(
+ `Key "${keyString}" is already bound to "${conflict.existingAction}"`
+ );
+ setRecordingKey(null);
+ setRecordingShortcut(null);
+ return;
+ }
+
+ // Remove old keybindings for this action
+ const oldKeys = getKeybindingsForAction(recordingShortcut.action);
+ oldKeys.forEach((key) => removeKeybinding(key));
+
+ // Add new keybinding
+ updateKeybinding(keyString, recordingShortcut.action);
+
+ setRecordingKey(null);
+ setRecordingShortcut(null);
+ }
+ };
+
+ const handleClickOutside = (e: MouseEvent) => {
+ setRecordingKey(null);
+ setRecordingShortcut(null);
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+ document.addEventListener("click", handleClickOutside);
+
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ document.removeEventListener("click", handleClickOutside);
+ };
+ }, [recordingKey, recordingShortcut, getKeybindingString, updateKeybinding, removeKeybinding, validateKeybinding, getKeybindingsForAction]);
+
+ const handleStartRecording = (keyId: string, shortcut: KeyboardShortcut) => {
+ setRecordingKey(keyId);
+ setRecordingShortcut(shortcut);
+ };
return (
@@ -121,21 +216,10 @@ export const KeyboardShortcutsHelp = () => {
Speed up your video editing workflow with these keyboard shortcuts.
- Most shortcuts work when the timeline is focused.
+ Click any shortcut key to edit it.
-
- setShowEditor(true)}
- >
-
- Customize
-
-
-
{categories.map((category) => (
@@ -146,7 +230,12 @@ export const KeyboardShortcutsHelp = () => {
{shortcuts
.filter((shortcut) => shortcut.category === category)
.map((shortcut, index) => (
-
+
))}
@@ -156,17 +245,3 @@ export const KeyboardShortcutsHelp = () => {
);
};
-
-function ShortcutKey({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- );
-}
diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
index bd8beffc..c7de8466 100644
--- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
+++ b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts
@@ -114,7 +114,13 @@ export const useKeyboardShortcutsHelp = () => {
}
});
- return result;
+ // Sort shortcuts by category first, then by description to ensure consistent ordering
+ return result.sort((a, b) => {
+ if (a.category !== b.category) {
+ return a.category.localeCompare(b.category);
+ }
+ return a.description.localeCompare(b.description);
+ });
}, [keybindings]);
return {