2025-07-15 12:36:53 -04:00
|
|
|
"use client";
|
|
|
|
|
|
2025-07-18 11:51:47 +02:00
|
|
|
import { useState, useEffect } from "react";
|
2025-07-15 12:36:53 -04:00
|
|
|
import { Button } from "./ui/button";
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogTrigger,
|
|
|
|
|
} from "./ui/dialog";
|
2025-07-18 06:22:49 +06:00
|
|
|
import { getPlatformSpecialKey } from "@/lib/utils";
|
2025-07-18 11:51:47 +02:00
|
|
|
import { Keyboard } from "lucide-react";
|
2025-07-18 07:09:05 +06:00
|
|
|
import {
|
|
|
|
|
useKeyboardShortcutsHelp,
|
|
|
|
|
KeyboardShortcut,
|
|
|
|
|
} from "@/hooks/use-keyboard-shortcuts-help";
|
2025-07-18 11:51:47 +02:00
|
|
|
import { useKeybindingsStore } from "@/stores/keybindings-store";
|
|
|
|
|
import { toast } from "sonner";
|
2025-07-15 12:36:53 -04:00
|
|
|
|
2025-07-18 06:22:49 +06:00
|
|
|
const modifier: {
|
|
|
|
|
[key: string]: string;
|
|
|
|
|
} = {
|
|
|
|
|
Shift: "Shift",
|
|
|
|
|
Alt: "Alt",
|
|
|
|
|
ArrowLeft: "←",
|
|
|
|
|
ArrowRight: "→",
|
|
|
|
|
ArrowUp: "↑",
|
|
|
|
|
ArrowDown: "↓",
|
|
|
|
|
Space: "Space",
|
2025-07-15 12:36:53 -04:00
|
|
|
};
|
|
|
|
|
|
2025-07-18 06:22:49 +06:00
|
|
|
function getKeyWithModifier(key: string) {
|
|
|
|
|
if (key === "Ctrl") return getPlatformSpecialKey();
|
|
|
|
|
return modifier[key] || key;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-20 16:12:38 +02:00
|
|
|
const ShortcutItem = ({
|
|
|
|
|
shortcut,
|
|
|
|
|
recordingKey,
|
|
|
|
|
onStartRecording,
|
|
|
|
|
}: {
|
2025-07-18 11:51:47 +02:00
|
|
|
shortcut: KeyboardShortcut;
|
|
|
|
|
recordingKey: string | null;
|
|
|
|
|
onStartRecording: (keyId: string, shortcut: KeyboardShortcut) => void;
|
|
|
|
|
}) => {
|
2025-07-16 16:05:30 +02:00
|
|
|
// Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J"
|
|
|
|
|
const displayKeys = shortcut.keys.filter((key: string) => {
|
|
|
|
|
if (
|
2025-07-18 06:22:49 +06:00
|
|
|
key.includes("Cmd") &&
|
|
|
|
|
shortcut.keys.includes(key.replace("Cmd", "Ctrl"))
|
|
|
|
|
)
|
2025-07-16 16:05:30 +02:00
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
{shortcut.icon && (
|
|
|
|
|
<div className="text-muted-foreground">{shortcut.icon}</div>
|
|
|
|
|
)}
|
|
|
|
|
<span className="text-sm">{shortcut.description}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
{displayKeys.map((key: string, index: number) => (
|
|
|
|
|
<div key={index} className="flex items-center gap-1">
|
|
|
|
|
<div className="flex items-center">
|
2025-07-18 11:51:47 +02:00
|
|
|
{key.split("+").map((keyPart: string, partIndex: number) => {
|
|
|
|
|
const keyId = `${shortcut.id}-${index}-${partIndex}`;
|
|
|
|
|
return (
|
2025-07-20 16:12:38 +02:00
|
|
|
<EditableShortcutKey
|
2025-07-18 11:51:47 +02:00
|
|
|
key={partIndex}
|
|
|
|
|
keyId={keyId}
|
|
|
|
|
originalKey={key}
|
|
|
|
|
shortcut={shortcut}
|
|
|
|
|
isRecording={recordingKey === keyId}
|
|
|
|
|
onStartRecording={() => onStartRecording(keyId, shortcut)}
|
|
|
|
|
>
|
|
|
|
|
{getKeyWithModifier(keyPart)}
|
|
|
|
|
</EditableShortcutKey>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-07-16 16:05:30 +02:00
|
|
|
</div>
|
|
|
|
|
{index < displayKeys.length - 1 && (
|
|
|
|
|
<span className="text-xs text-muted-foreground">or</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-07-15 12:36:53 -04:00
|
|
|
</div>
|
2025-07-16 16:05:30 +02:00
|
|
|
);
|
|
|
|
|
};
|
2025-07-15 12:36:53 -04:00
|
|
|
|
2025-07-20 16:12:38 +02:00
|
|
|
const EditableShortcutKey = ({
|
|
|
|
|
children,
|
2025-07-18 11:51:47 +02:00
|
|
|
keyId,
|
2025-07-20 16:12:38 +02:00
|
|
|
originalKey,
|
2025-07-18 11:51:47 +02:00
|
|
|
shortcut,
|
|
|
|
|
isRecording,
|
2025-07-20 16:12:38 +02:00
|
|
|
onStartRecording,
|
|
|
|
|
}: {
|
2025-07-18 11:51:47 +02:00
|
|
|
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 ${
|
2025-07-20 16:12:38 +02:00
|
|
|
isRecording
|
|
|
|
|
? "border-primary bg-primary/10"
|
2025-07-18 11:51:47 +02:00
|
|
|
: "border-white/10 bg-black/20"
|
|
|
|
|
}`}
|
|
|
|
|
onClick={handleClick}
|
2025-07-20 16:12:38 +02:00
|
|
|
title={
|
|
|
|
|
isRecording ? "Press any key combination..." : "Click to edit shortcut"
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</kbd>
|
2025-07-18 11:51:47 +02:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-15 12:36:53 -04:00
|
|
|
export const KeyboardShortcutsHelp = () => {
|
|
|
|
|
const [open, setOpen] = useState(false);
|
2025-07-18 11:51:47 +02:00
|
|
|
const [recordingKey, setRecordingKey] = useState<string | null>(null);
|
2025-07-20 16:12:38 +02:00
|
|
|
const [recordingShortcut, setRecordingShortcut] =
|
|
|
|
|
useState<KeyboardShortcut | null>(null);
|
2025-07-18 11:51:47 +02:00
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
updateKeybinding,
|
|
|
|
|
removeKeybinding,
|
|
|
|
|
getKeybindingString,
|
|
|
|
|
validateKeybinding,
|
|
|
|
|
getKeybindingsForAction,
|
|
|
|
|
} = useKeybindingsStore();
|
2025-07-15 12:36:53 -04:00
|
|
|
|
2025-07-18 07:09:05 +06:00
|
|
|
// Get shortcuts from centralized hook
|
|
|
|
|
const { shortcuts } = useKeyboardShortcutsHelp();
|
2025-07-16 14:37:08 +02:00
|
|
|
|
|
|
|
|
const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
|
2025-07-15 12:36:53 -04:00
|
|
|
|
2025-07-18 11:51:47 +02:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!recordingKey || !recordingShortcut) return;
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
const keyString = getKeybindingString(e);
|
|
|
|
|
if (keyString) {
|
|
|
|
|
// Auto-save the new keybinding
|
2025-07-20 16:12:38 +02:00
|
|
|
const conflict = validateKeybinding(
|
|
|
|
|
keyString,
|
|
|
|
|
recordingShortcut.action
|
|
|
|
|
);
|
2025-07-18 11:51:47 +02:00
|
|
|
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);
|
2025-07-20 16:12:38 +02:00
|
|
|
|
2025-07-18 11:51:47 +02:00
|
|
|
setRecordingKey(null);
|
|
|
|
|
setRecordingShortcut(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
|
|
|
setRecordingKey(null);
|
|
|
|
|
setRecordingShortcut(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
|
|
|
document.addEventListener("click", handleClickOutside);
|
2025-07-20 16:12:38 +02:00
|
|
|
|
2025-07-18 11:51:47 +02:00
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("keydown", handleKeyDown);
|
|
|
|
|
document.removeEventListener("click", handleClickOutside);
|
|
|
|
|
};
|
2025-07-20 16:12:38 +02:00
|
|
|
}, [
|
|
|
|
|
recordingKey,
|
|
|
|
|
recordingShortcut,
|
|
|
|
|
getKeybindingString,
|
|
|
|
|
updateKeybinding,
|
|
|
|
|
removeKeybinding,
|
|
|
|
|
validateKeybinding,
|
|
|
|
|
getKeybindingsForAction,
|
|
|
|
|
]);
|
2025-07-18 11:51:47 +02:00
|
|
|
|
|
|
|
|
const handleStartRecording = (keyId: string, shortcut: KeyboardShortcut) => {
|
|
|
|
|
setRecordingKey(keyId);
|
|
|
|
|
setRecordingShortcut(shortcut);
|
|
|
|
|
};
|
2025-07-18 07:38:32 +06:00
|
|
|
|
2025-07-15 12:36:53 -04:00
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
|
|
|
<DialogTrigger asChild>
|
2025-07-15 12:58:04 -04:00
|
|
|
<Button variant="text" size="sm" className="gap-2">
|
2025-07-15 12:36:53 -04:00
|
|
|
<Keyboard className="w-4 h-4" />
|
|
|
|
|
Shortcuts
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogTrigger>
|
2025-07-18 07:38:32 +06:00
|
|
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
2025-07-15 12:36:53 -04:00
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
|
|
<Keyboard className="w-5 h-5" />
|
|
|
|
|
Keyboard Shortcuts
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
Speed up your video editing workflow with these keyboard shortcuts.
|
2025-07-18 11:51:47 +02:00
|
|
|
Click any shortcut key to edit it.
|
2025-07-15 12:36:53 -04:00
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
2025-07-16 14:37:08 +02:00
|
|
|
|
2025-07-15 12:36:53 -04:00
|
|
|
<div className="space-y-6">
|
2025-07-16 14:37:08 +02:00
|
|
|
{categories.map((category) => (
|
2025-07-16 15:52:25 +02:00
|
|
|
<div key={category} className="flex flex-col gap-1">
|
|
|
|
|
<h3 className="text-xs text-muted-foreground uppercase tracking-wide font-medium">
|
2025-07-15 12:36:53 -04:00
|
|
|
{category}
|
|
|
|
|
</h3>
|
2025-07-16 15:52:25 +02:00
|
|
|
<div className="space-y-0.5">
|
2025-07-15 12:36:53 -04:00
|
|
|
{shortcuts
|
2025-07-16 14:37:08 +02:00
|
|
|
.filter((shortcut) => shortcut.category === category)
|
2025-07-15 12:36:53 -04:00
|
|
|
.map((shortcut, index) => (
|
2025-07-20 16:12:38 +02:00
|
|
|
<ShortcutItem
|
|
|
|
|
key={index}
|
2025-07-18 11:51:47 +02:00
|
|
|
shortcut={shortcut}
|
|
|
|
|
recordingKey={recordingKey}
|
|
|
|
|
onStartRecording={handleStartRecording}
|
|
|
|
|
/>
|
2025-07-15 12:36:53 -04:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
2025-07-16 14:37:08 +02:00
|
|
|
};
|