Files
OpenCut/apps/web/src/hooks/use-keybindings.ts
T

183 lines
4.9 KiB
TypeScript

import { useEffect } from "react";
import { invokeAction } from "@/lib/actions";
import { useKeybindingsStore } from "@/stores/keybindings-store";
import { useTimelineStore } from "@/stores/timeline-store";
import { isTypableDOMElement } from "@/utils/browser";
/**
* a composable that hooks to the caller component's
* lifecycle and hooks to the keyboard events to fire
* the appropriate actions based on keybindings
*/
export function useKeybindingsListener() {
const {
keybindings,
getKeybindingString,
overlayDepth,
isLoadingProject,
isRecording,
} = useKeybindingsStore();
const clipboard = useTimelineStore((state) => state.clipboard);
useEffect(() => {
const eventOptions: AddEventListenerOptions = { capture: true };
// #region agent log
fetch("http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Debug-Session-Id": "3997d9",
},
body: JSON.stringify({
sessionId: "3997d9",
runId: "initial",
hypothesisId: "H1",
location: "use-keybindings.ts:effect",
message: "Keybindings listener mounted",
data: {
overlayDepth,
isLoadingProject,
isRecording,
keybindingCount: Object.keys(keybindings).length,
},
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
const handleKeyDown = (ev: KeyboardEvent) => {
const normalizedKey = (ev.key ?? "").toLowerCase();
const shouldLogKey =
ev.code === "Space" ||
ev.code.startsWith("Key") ||
["escape", "delete", "backspace", "enter"].includes(normalizedKey);
if (overlayDepth > 0 || isLoadingProject || isRecording) {
if (shouldLogKey) {
// #region agent log
fetch(
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Debug-Session-Id": "3997d9",
},
body: JSON.stringify({
sessionId: "3997d9",
runId: "initial",
hypothesisId: "H2",
location: "use-keybindings.ts:blocked",
message: "Shortcut blocked by runtime gate",
data: {
key: ev.key,
code: ev.code,
overlayDepth,
isLoadingProject,
isRecording,
targetTag:
ev.target instanceof HTMLElement ? ev.target.tagName : null,
},
timestamp: Date.now(),
}),
},
).catch(() => {});
// #endregion
}
return;
}
const binding = getKeybindingString(ev);
const activeElement = document.activeElement;
const isTextInput =
activeElement instanceof HTMLElement &&
isTypableDOMElement({ element: activeElement });
const boundAction = binding ? keybindings[binding] : undefined;
if (shouldLogKey || binding || boundAction) {
// #region agent log
fetch(
"http://127.0.0.1:7245/ingest/669b22f8-172b-4e65-aa3f-1c702ede83f7",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Debug-Session-Id": "3997d9",
},
body: JSON.stringify({
sessionId: "3997d9",
runId: "initial",
hypothesisId: !binding ? "H3" : isTextInput ? "H5" : "H4",
location: "use-keybindings.ts:keydown",
message: "Shortcut keydown observed",
data: {
key: ev.key,
code: ev.code,
binding,
boundAction: boundAction ?? null,
isTextInput,
keybindingCount: Object.keys(keybindings).length,
activeTag:
activeElement instanceof HTMLElement
? activeElement.tagName
: null,
targetTag:
ev.target instanceof HTMLElement ? ev.target.tagName : null,
},
timestamp: Date.now(),
}),
},
).catch(() => {});
// #endregion
}
if (normalizedKey === "escape" && isTextInput) {
activeElement.blur();
return;
}
if (!binding) return;
if (!boundAction) return;
if (isTextInput) return;
if (boundAction === "paste-copied") {
if (!clipboard?.items.length) return;
ev.preventDefault();
invokeAction("paste-copied", undefined, "keypress");
return;
}
ev.preventDefault();
switch (boundAction) {
case "seek-forward":
invokeAction("seek-forward", { seconds: 1 }, "keypress");
break;
case "seek-backward":
invokeAction("seek-backward", { seconds: 1 }, "keypress");
break;
case "jump-forward":
invokeAction("jump-forward", { seconds: 5 }, "keypress");
break;
case "jump-backward":
invokeAction("jump-backward", { seconds: 5 }, "keypress");
break;
default:
invokeAction(boundAction, undefined, "keypress");
}
};
document.addEventListener("keydown", handleKeyDown, eventOptions);
return () => {
document.removeEventListener("keydown", handleKeyDown, eventOptions);
};
}, [
keybindings,
getKeybindingString,
overlayDepth,
isLoadingProject,
isRecording,
clipboard,
]);
}