mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
style: match editing experience to match capcut
This commit is contained in:
@@ -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 (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
value={displayKey}
|
|
||||||
readOnly
|
|
||||||
placeholder={isRecording ? "Press keys..." : "Click to record"}
|
|
||||||
className="font-mono text-sm"
|
|
||||||
onClick={handleStartRecording}
|
|
||||||
/>
|
|
||||||
{isRecording ? (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={!recordedKey}
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={handleCancel}>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Button size="sm" variant="outline" onClick={handleStartRecording}>
|
|
||||||
Record
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const KeybindingEditor = ({
|
|
||||||
shortcuts,
|
|
||||||
onClose,
|
|
||||||
}: KeybindingEditorProps) => {
|
|
||||||
const {
|
|
||||||
keybindings,
|
|
||||||
updateKeybinding,
|
|
||||||
removeKeybinding,
|
|
||||||
resetToDefaults,
|
|
||||||
isCustomized,
|
|
||||||
validateKeybinding,
|
|
||||||
getKeybindingsForAction,
|
|
||||||
exportKeybindings,
|
|
||||||
importKeybindings,
|
|
||||||
} = useKeybindingsStore();
|
|
||||||
|
|
||||||
const [editingShortcut, setEditingShortcut] = useState<string | null>(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<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
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 (
|
|
||||||
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Settings className="w-5 h-5" />
|
|
||||||
<h2 className="text-xl font-semibold">
|
|
||||||
Customize Keyboard Shortcuts
|
|
||||||
</h2>
|
|
||||||
{isCustomized && (
|
|
||||||
<Badge variant="secondary" className="ml-2">
|
|
||||||
Modified
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" size="sm" onClick={handleExportKeybindings}>
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".json"
|
|
||||||
onChange={handleImportKeybindings}
|
|
||||||
className="hidden"
|
|
||||||
id="import-keybindings"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
document.getElementById("import-keybindings")?.click()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
|
||||||
Import
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowResetDialog(true)}
|
|
||||||
disabled={!isCustomized}
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
|
||||||
Reset to Defaults
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={onClose}>
|
|
||||||
<X className="w-4 h-4 mr-2" />
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center">
|
|
||||||
<Input
|
|
||||||
placeholder="Search shortcuts..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Tabs value={selectedCategory} onValueChange={setSelectedCategory}>
|
|
||||||
<TabsList>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<TabsTrigger key={category} value={category}>
|
|
||||||
{category === "all" ? "All" : category}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{filteredShortcuts.map((shortcut) => (
|
|
||||||
<Card key={shortcut.id}>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium">{shortcut.description}</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{shortcut.category}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{editingShortcut === shortcut.id ? (
|
|
||||||
<KeyRecorder
|
|
||||||
value={newKeyBinding}
|
|
||||||
onValueChange={setNewKeyBinding}
|
|
||||||
onCancel={handleCancelEdit}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{shortcut.keys.map((key, index) => (
|
|
||||||
<Badge
|
|
||||||
key={index}
|
|
||||||
variant="secondary"
|
|
||||||
className="font-mono text-xs"
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{shortcut.keys.length === 0 && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-muted-foreground"
|
|
||||||
>
|
|
||||||
No binding
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{editingShortcut === shortcut.id ? (
|
|
||||||
<>
|
|
||||||
<Button size="sm" onClick={handleSaveShortcut}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCancelEdit}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleEditShortcut(shortcut)}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleRemoveShortcut(shortcut)}
|
|
||||||
disabled={shortcut.keys.length === 0}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AlertDialog open={showResetDialog} onOpenChange={setShowResetDialog}>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
|
||||||
<AlertTriangle className="w-5 h-5 text-amber-500" />
|
|
||||||
Reset Keyboard Shortcuts?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will reset all keyboard shortcuts to their default values.
|
|
||||||
Any custom keybindings will be lost.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={handleResetToDefaults}>
|
|
||||||
Reset to Defaults
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
|
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -12,13 +12,13 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "./ui/dialog";
|
} from "./ui/dialog";
|
||||||
import { getPlatformSpecialKey } from "@/lib/utils";
|
import { getPlatformSpecialKey } from "@/lib/utils";
|
||||||
import { Badge } from "./ui/badge";
|
import { Keyboard } from "lucide-react";
|
||||||
import { Keyboard, Settings } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
useKeyboardShortcutsHelp,
|
useKeyboardShortcutsHelp,
|
||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@/hooks/use-keyboard-shortcuts-help";
|
} from "@/hooks/use-keyboard-shortcuts-help";
|
||||||
import { KeybindingEditor } from "./keybinding-editor";
|
import { useKeybindingsStore } from "@/stores/keybindings-store";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
const modifier: {
|
const modifier: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
@@ -37,7 +37,15 @@ function getKeyWithModifier(key: string) {
|
|||||||
return modifier[key] || key;
|
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"
|
// Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J"
|
||||||
const displayKeys = shortcut.keys.filter((key: string) => {
|
const displayKeys = shortcut.keys.filter((key: string) => {
|
||||||
if (
|
if (
|
||||||
@@ -61,11 +69,21 @@ const ShortcutItem = ({ shortcut }: { shortcut: KeyboardShortcut }) => {
|
|||||||
{displayKeys.map((key: string, index: number) => (
|
{displayKeys.map((key: string, index: number) => (
|
||||||
<div key={index} className="flex items-center gap-1">
|
<div key={index} className="flex items-center gap-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{key.split("+").map((keyPart: string, partIndex: number) => (
|
{key.split("+").map((keyPart: string, partIndex: number) => {
|
||||||
<ShortcutKey key={partIndex}>
|
const keyId = `${shortcut.id}-${index}-${partIndex}`;
|
||||||
{getKeyWithModifier(keyPart)}
|
return (
|
||||||
</ShortcutKey>
|
<EditableShortcutKey
|
||||||
))}
|
key={partIndex}
|
||||||
|
keyId={keyId}
|
||||||
|
originalKey={key}
|
||||||
|
shortcut={shortcut}
|
||||||
|
isRecording={recordingKey === keyId}
|
||||||
|
onStartRecording={() => onStartRecording(keyId, shortcut)}
|
||||||
|
>
|
||||||
|
{getKeyWithModifier(keyPart)}
|
||||||
|
</EditableShortcutKey>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{index < displayKeys.length - 1 && (
|
{index < displayKeys.length - 1 && (
|
||||||
<span className="text-xs text-muted-foreground">or</span>
|
<span className="text-xs text-muted-foreground">or</span>
|
||||||
@@ -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 (
|
||||||
|
<kbd
|
||||||
|
className={`inline-flex font-sans text-xs rounded px-2 min-w-[1.5rem] min-h-[1.5rem] leading-none items-center justify-center shadow-sm border mr-1 cursor-pointer hover:bg-opacity-80 ${
|
||||||
|
isRecording
|
||||||
|
? "border-primary bg-primary/10"
|
||||||
|
: "border-white/10 bg-black/20"
|
||||||
|
}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
title={isRecording ? "Press any key combination..." : "Click to edit shortcut"}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</kbd>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const KeyboardShortcutsHelp = () => {
|
export const KeyboardShortcutsHelp = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [showEditor, setShowEditor] = useState(false);
|
const [recordingKey, setRecordingKey] = useState<string | null>(null);
|
||||||
|
const [recordingShortcut, setRecordingShortcut] = useState<KeyboardShortcut | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
updateKeybinding,
|
||||||
|
removeKeybinding,
|
||||||
|
getKeybindingString,
|
||||||
|
validateKeybinding,
|
||||||
|
getKeybindingsForAction,
|
||||||
|
} = useKeybindingsStore();
|
||||||
|
|
||||||
// Get shortcuts from centralized hook
|
// Get shortcuts from centralized hook
|
||||||
const { shortcuts } = useKeyboardShortcutsHelp();
|
const { shortcuts } = useKeyboardShortcutsHelp();
|
||||||
|
|
||||||
const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
|
const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
|
||||||
|
|
||||||
if (showEditor) {
|
useEffect(() => {
|
||||||
return (
|
if (!recordingKey || !recordingShortcut) return;
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
<Button variant="text" size="sm" className="gap-2">
|
e.preventDefault();
|
||||||
<Keyboard className="w-4 h-4" />
|
e.stopPropagation();
|
||||||
Shortcuts
|
|
||||||
</Button>
|
const keyString = getKeybindingString(e);
|
||||||
</DialogTrigger>
|
if (keyString) {
|
||||||
<DialogContent className="max-w-6xl max-h-[90vh] overflow-y-auto">
|
// Auto-save the new keybinding
|
||||||
<KeybindingEditor
|
const conflict = validateKeybinding(keyString, recordingShortcut.action);
|
||||||
shortcuts={shortcuts}
|
if (conflict) {
|
||||||
onClose={() => setShowEditor(false)}
|
toast.error(
|
||||||
/>
|
`Key "${keyString}" is already bound to "${conflict.existingAction}"`
|
||||||
</DialogContent>
|
);
|
||||||
</Dialog>
|
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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -121,21 +216,10 @@ export const KeyboardShortcutsHelp = () => {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Speed up your video editing workflow with these keyboard shortcuts.
|
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.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex justify-end mb-4">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowEditor(true)}
|
|
||||||
>
|
|
||||||
<Settings className="w-4 h-4 mr-2" />
|
|
||||||
Customize
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<div key={category} className="flex flex-col gap-1">
|
<div key={category} className="flex flex-col gap-1">
|
||||||
@@ -146,7 +230,12 @@ export const KeyboardShortcutsHelp = () => {
|
|||||||
{shortcuts
|
{shortcuts
|
||||||
.filter((shortcut) => shortcut.category === category)
|
.filter((shortcut) => shortcut.category === category)
|
||||||
.map((shortcut, index) => (
|
.map((shortcut, index) => (
|
||||||
<ShortcutItem key={index} shortcut={shortcut} />
|
<ShortcutItem
|
||||||
|
key={index}
|
||||||
|
shortcut={shortcut}
|
||||||
|
recordingKey={recordingKey}
|
||||||
|
onStartRecording={handleStartRecording}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,17 +245,3 @@ export const KeyboardShortcutsHelp = () => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function ShortcutKey({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<kbd
|
|
||||||
className="inline-flex font-sans text-xs rounded px-2 min-w-[1.5rem] min-h-[1.5rem] leading-none items-center justify-center shadow-sm border mr-1"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.2)",
|
|
||||||
borderColor: "rgba(255, 255, 255, 0.1)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</kbd>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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]);
|
}, [keybindings]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user