diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/actions/components/shortcuts-dialog.tsx | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz | |
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/actions/components/shortcuts-dialog.tsx')
| -rw-r--r-- | apps/web/src/actions/components/shortcuts-dialog.tsx | 229 |
1 files changed, 229 insertions, 0 deletions
diff --git a/apps/web/src/actions/components/shortcuts-dialog.tsx b/apps/web/src/actions/components/shortcuts-dialog.tsx new file mode 100644 index 00000000..cd9c0242 --- /dev/null +++ b/apps/web/src/actions/components/shortcuts-dialog.tsx @@ -0,0 +1,229 @@ +"use client";
+
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import {
+ type KeyboardShortcut,
+ useKeyboardShortcutsHelp,
+} from "@/actions/use-keyboard-shortcuts-help";
+import { useKeybindingsStore } from "@/actions/keybindings-store";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+
+export function ShortcutsDialog({
+ isOpen,
+ onOpenChange,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+}) {
+ const [recordingShortcut, setRecordingShortcut] =
+ useState<KeyboardShortcut | null>(null);
+
+ const {
+ updateKeybinding,
+ removeKeybinding,
+ getKeybindingString,
+ validateKeybinding,
+ getKeybindingsForAction,
+ setIsRecording,
+ resetToDefaults,
+ isRecording,
+ } = useKeybindingsStore();
+
+ const { shortcuts } = useKeyboardShortcutsHelp();
+
+ const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
+
+ useEffect(() => {
+ if (!isRecording || !recordingShortcut) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const keyString = getKeybindingString(e);
+ if (keyString) {
+ const conflict = validateKeybinding(
+ keyString,
+ recordingShortcut.action,
+ );
+ if (conflict) {
+ toast.error(
+ `Key "${keyString}" is already bound to "${conflict.existingAction}"`,
+ );
+ setRecordingShortcut(null);
+ return;
+ }
+
+ const oldKeys = getKeybindingsForAction(recordingShortcut.action);
+ for (const key of oldKeys) {
+ removeKeybinding(key);
+ }
+
+ updateKeybinding(keyString, recordingShortcut.action);
+
+ setIsRecording(false);
+ setRecordingShortcut(null);
+ }
+ };
+
+ const handleClickOutside = () => {
+ setRecordingShortcut(null);
+ setIsRecording(false);
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+ document.addEventListener("click", handleClickOutside);
+
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ document.removeEventListener("click", handleClickOutside);
+ };
+ }, [
+ recordingShortcut,
+ getKeybindingString,
+ updateKeybinding,
+ removeKeybinding,
+ validateKeybinding,
+ getKeybindingsForAction,
+ setIsRecording,
+ isRecording,
+ ]);
+
+ const handleStartRecording = (shortcut: KeyboardShortcut) => {
+ setRecordingShortcut(shortcut);
+ setIsRecording(true);
+ };
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
+ <DialogContent className="flex max-h-[80vh] max-w-2xl flex-col p-0">
+ <DialogHeader>
+ <DialogTitle>Keyboard shortcuts</DialogTitle>
+ </DialogHeader>
+
+ <DialogBody className="scrollbar-thin grow overflow-y-auto">
+ <div className="flex flex-col gap-6">
+ {categories.map((category) => (
+ <div key={category} className="flex flex-col gap-1">
+ <h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
+ {category}
+ </h3>
+ <div className="flex flex-col gap-1">
+ {shortcuts
+ .filter((shortcut) => shortcut.category === category)
+ .map((shortcut) => (
+ <ShortcutItem
+ key={shortcut.action}
+ shortcut={shortcut}
+ isRecording={
+ shortcut.action === recordingShortcut?.action
+ }
+ onStartRecording={() => handleStartRecording(shortcut)}
+ />
+ ))}
+ </div>
+ </div>
+ ))}
+ </div>
+ </DialogBody>
+ <DialogFooter>
+ <Button variant="destructive" onClick={resetToDefaults}>
+ Reset to default
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
+
+function ShortcutItem({
+ shortcut,
+ isRecording,
+ onStartRecording,
+}: {
+ shortcut: KeyboardShortcut;
+ isRecording: boolean;
+ onStartRecording: (params: { shortcut: KeyboardShortcut }) => void;
+}) {
+ const displayKeys = shortcut.keys.filter((key: string) => {
+ if (
+ key.includes("Cmd") &&
+ shortcut.keys.includes(key.replace("Cmd", "Ctrl"))
+ )
+ 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-2">
+ {displayKeys.map((key: string, index: number) => (
+ <div key={key} className="flex items-center gap-2">
+ <div className="flex items-center gap-1">
+ {key.split("+").map((keyPart: string, partIndex: number) => {
+ const keyId = `${shortcut.id}-${index}-${partIndex}`;
+ return (
+ <EditableShortcutKey
+ key={keyId}
+ isRecording={isRecording}
+ onStartRecording={() => onStartRecording({ shortcut })}
+ >
+ {keyPart}
+ </EditableShortcutKey>
+ );
+ })}
+ </div>
+ {index < displayKeys.length - 1 && (
+ <span className="text-muted-foreground text-xs">or</span>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+}
+
+function EditableShortcutKey({
+ children,
+ isRecording,
+ onStartRecording,
+}: {
+ children: React.ReactNode;
+ isRecording: boolean;
+ onStartRecording: () => void;
+}) {
+ const handleClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onStartRecording();
+ };
+
+ return (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleClick}
+ title={
+ isRecording ? "Press any key combination..." : "Click to edit shortcut"
+ }
+ >
+ {children}
+ </Button>
+ );
+}
|
