diff options
Diffstat (limited to 'apps/web/src/lib/actions')
| -rw-r--r-- | apps/web/src/lib/actions/definitions.ts | 162 | ||||
| -rw-r--r-- | apps/web/src/lib/actions/index.ts | 3 | ||||
| -rw-r--r-- | apps/web/src/lib/actions/registry.ts | 61 | ||||
| -rw-r--r-- | apps/web/src/lib/actions/types.ts | 42 |
4 files changed, 268 insertions, 0 deletions
diff --git a/apps/web/src/lib/actions/definitions.ts b/apps/web/src/lib/actions/definitions.ts new file mode 100644 index 00000000..83144a25 --- /dev/null +++ b/apps/web/src/lib/actions/definitions.ts @@ -0,0 +1,162 @@ +import type { ShortcutKey } from "@/types/keybinding"; + +export type TActionCategory = + | "playback" + | "navigation" + | "editing" + | "selection" + | "history" + | "timeline" + | "controls"; + +export interface TActionDefinition { + description: string; + category: TActionCategory; + defaultShortcuts?: ShortcutKey[]; + args?: Record<string, unknown>; +} + +export const ACTIONS = { + "toggle-play": { + description: "Play/Pause", + category: "playback", + defaultShortcuts: ["space", "k"], + }, + "stop-playback": { + description: "Stop playback", + category: "playback", + }, + "seek-forward": { + description: "Seek forward 1 second", + category: "playback", + defaultShortcuts: ["l"], + args: { seconds: "number" }, + }, + "seek-backward": { + description: "Seek backward 1 second", + category: "playback", + defaultShortcuts: ["j"], + args: { seconds: "number" }, + }, + "frame-step-forward": { + description: "Frame step forward", + category: "navigation", + defaultShortcuts: ["right"], + }, + "frame-step-backward": { + description: "Frame step backward", + category: "navigation", + defaultShortcuts: ["left"], + }, + "jump-forward": { + description: "Jump forward 5 seconds", + category: "navigation", + defaultShortcuts: ["shift+right"], + args: { seconds: "number" }, + }, + "jump-backward": { + description: "Jump backward 5 seconds", + category: "navigation", + defaultShortcuts: ["shift+left"], + args: { seconds: "number" }, + }, + "goto-start": { + description: "Go to timeline start", + category: "navigation", + defaultShortcuts: ["home", "enter"], + }, + "goto-end": { + description: "Go to timeline end", + category: "navigation", + defaultShortcuts: ["end"], + }, + split: { + description: "Split elements at playhead", + category: "editing", + defaultShortcuts: ["s"], + }, + "split-left": { + description: "Split and remove left", + category: "editing", + defaultShortcuts: ["q"], + }, + "split-right": { + description: "Split and remove right", + category: "editing", + defaultShortcuts: ["w"], + }, + "delete-selected": { + description: "Delete selected elements", + category: "editing", + defaultShortcuts: ["backspace", "delete"], + }, + "copy-selected": { + description: "Copy selected elements", + category: "editing", + defaultShortcuts: ["ctrl+c"], + }, + "paste-copied": { + description: "Paste elements at playhead", + category: "editing", + defaultShortcuts: ["ctrl+v"], + }, + "toggle-snapping": { + description: "Toggle snapping", + category: "editing", + defaultShortcuts: ["n"], + }, + "select-all": { + description: "Select all elements", + category: "selection", + defaultShortcuts: ["ctrl+a"], + }, + "duplicate-selected": { + description: "Duplicate selected element", + category: "selection", + defaultShortcuts: ["ctrl+d"], + }, + "toggle-elements-muted-selected": { + description: "Mute/unmute selected elements", + category: "selection", + }, + "toggle-elements-visibility-selected": { + description: "Show/hide selected elements", + category: "selection", + }, + "toggle-bookmark": { + description: "Toggle bookmark at playhead", + category: "timeline", + }, + undo: { + description: "Undo", + category: "history", + defaultShortcuts: ["ctrl+z"], + }, + redo: { + description: "Redo", + category: "history", + defaultShortcuts: ["ctrl+shift+z", "ctrl+y"], + }, +} as const satisfies Record<string, TActionDefinition>; + +export type TAction = keyof typeof ACTIONS; + +export function getActionDefinition(action: TAction): TActionDefinition { + return ACTIONS[action]; +} + +export function getDefaultShortcuts(): Record<ShortcutKey, TAction> { + const shortcuts: Record<string, TAction> = {}; + + for (const [action, def] of Object.entries(ACTIONS) as Array< + [TAction, TActionDefinition] + >) { + if (def.defaultShortcuts) { + for (const shortcut of def.defaultShortcuts) { + shortcuts[shortcut] = action; + } + } + } + + return shortcuts as Record<ShortcutKey, TAction>; +} diff --git a/apps/web/src/lib/actions/index.ts b/apps/web/src/lib/actions/index.ts new file mode 100644 index 00000000..801a846a --- /dev/null +++ b/apps/web/src/lib/actions/index.ts @@ -0,0 +1,3 @@ +export * from "./definitions"; +export * from "./types"; +export * from "./registry"; diff --git a/apps/web/src/lib/actions/registry.ts b/apps/web/src/lib/actions/registry.ts new file mode 100644 index 00000000..b9c84529 --- /dev/null +++ b/apps/web/src/lib/actions/registry.ts @@ -0,0 +1,61 @@ +import type { + TAction, + TActionFunc, + TActionWithArgs, + TActionWithOptionalArgs, + TActionArgsMap, + TArgOfAction, + TInvocationTrigger, +} from "./types"; + +type ActionHandler = (arg: unknown, trigger?: TInvocationTrigger) => void; +const boundActions: Partial<Record<TAction, ActionHandler[]>> = {}; + +export function bindAction<A extends TAction>( + action: A, + handler: TActionFunc<A>, +) { + const handlers = boundActions[action]; + const typedHandler = handler as ActionHandler; + if (handlers) { + handlers.push(typedHandler); + } else { + boundActions[action] = [typedHandler]; + } +} + +export function unbindAction<A extends TAction>( + action: A, + handler: TActionFunc<A>, +) { + const handlers = boundActions[action]; + if (!handlers) return; + + const typedHandler = handler as ActionHandler; + boundActions[action] = handlers.filter((h) => h !== typedHandler); + + if (boundActions[action]?.length === 0) { + delete boundActions[action]; + } +} + +type InvokeActionFunc = { + ( + action: TActionWithOptionalArgs, + args?: undefined, + trigger?: TInvocationTrigger, + ): void; + <A extends TActionWithArgs>( + action: A, + args: TActionArgsMap[A], + trigger?: TInvocationTrigger, + ): void; +}; + +export const invokeAction: InvokeActionFunc = <A extends TAction>( + action: A, + args?: TArgOfAction<A>, + trigger?: TInvocationTrigger, +) => { + boundActions[action]?.forEach((handler) => handler(args, trigger)); +}; diff --git a/apps/web/src/lib/actions/types.ts b/apps/web/src/lib/actions/types.ts new file mode 100644 index 00000000..508bf10e --- /dev/null +++ b/apps/web/src/lib/actions/types.ts @@ -0,0 +1,42 @@ +import type { MutableRefObject } from "react"; +import type { TAction } from "./definitions"; + +export type { TAction }; + +export type TActionArgsMap = { + "seek-forward": { seconds: number } | undefined; + "seek-backward": { seconds: number } | undefined; + "jump-forward": { seconds: number } | undefined; + "jump-backward": { seconds: number } | undefined; +}; + +type TKeysWithValueUndefined<T> = { + [K in keyof T]: undefined extends T[K] ? K : never; +}[keyof T]; + +export type TActionWithArgs = keyof TActionArgsMap; + +export type TActionWithOptionalArgs = + | TActionWithNoArgs + | TKeysWithValueUndefined<TActionArgsMap>; + +export type TActionWithNoArgs = Exclude<TAction, TActionWithArgs>; + +export type TArgOfAction<A extends TAction> = A extends TActionWithArgs + ? TActionArgsMap[A] + : undefined; + +export type TActionFunc<A extends TAction> = A extends TActionWithArgs + ? (arg: TArgOfAction<A>, trigger?: TInvocationTrigger) => void + : (_?: undefined, trigger?: TInvocationTrigger) => void; + +export type TInvocationTrigger = "keypress" | "mouseclick"; + +export type TBoundActionList = { + [A in TAction]?: Array<TActionFunc<A>>; +}; + +export type TActionHandlerOptions = + | MutableRefObject<boolean> + | boolean + | undefined; |
