♻️(frontend) centralize shortcuts in a catalog
Centralize shortcuts into a single source of truth, making them easier to discover and manage, and laying the groundwork for future override support and the ability to revert to default definitions if needed. Shortcuts are now retrieved by identifier, while leaving each component responsible for declaring when a shortcut should be enabled and which handler should be called;
This commit is contained in:
@@ -18,6 +18,7 @@ import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
|||||||
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
|
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
|
||||||
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
|
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
|
||||||
import { ToggleSource, CaptureOptionsBySource } from '@livekit/components-core'
|
import { ToggleSource, CaptureOptionsBySource } from '@livekit/components-core'
|
||||||
|
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
|
||||||
|
|
||||||
type ToggleDeviceStyleProps = {
|
type ToggleDeviceStyleProps = {
|
||||||
variant?: NonNullable<ButtonRecipeProps>['variant']
|
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||||
@@ -88,12 +89,14 @@ export const ToggleDevice = <T extends ToggleSource>({
|
|||||||
const deviceShortcut = useDeviceShortcut(kind)
|
const deviceShortcut = useDeviceShortcut(kind)
|
||||||
|
|
||||||
useRegisterKeyboardShortcut({
|
useRegisterKeyboardShortcut({
|
||||||
shortcut: deviceShortcut,
|
id: deviceShortcut?.id,
|
||||||
handler: async () => await toggle(),
|
handler: async () => await toggle(),
|
||||||
isDisabled: cannotUseDevice,
|
isDisabled: cannotUseDevice,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const pushToTalkShortcut = getShortcutDescriptorById('push-to-talk')
|
||||||
useLongPress({
|
useLongPress({
|
||||||
keyCode: kind === 'audioinput' ? 'KeyV' : undefined,
|
keyCode: kind === 'audioinput' ? pushToTalkShortcut?.code : undefined,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onKeyUp,
|
onKeyUp,
|
||||||
isDisabled: cannotUseDevice,
|
isDisabled: cannotUseDevice,
|
||||||
@@ -103,7 +106,9 @@ export const ToggleDevice = <T extends ToggleSource>({
|
|||||||
const label = t(enabled ? 'disable' : 'enable', {
|
const label = t(enabled ? 'disable' : 'enable', {
|
||||||
keyPrefix: `selectDevice.${kind}`,
|
keyPrefix: `selectDevice.${kind}`,
|
||||||
})
|
})
|
||||||
return deviceShortcut ? appendShortcutLabel(label, deviceShortcut) : label
|
return deviceShortcut?.shortcut
|
||||||
|
? appendShortcutLabel(label, deviceShortcut.shortcut)
|
||||||
|
: label
|
||||||
}, [enabled, kind, deviceShortcut, t])
|
}, [enabled, kind, deviceShortcut, t])
|
||||||
|
|
||||||
const Icon =
|
const Icon =
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Shortcut } from '@/features/shortcuts/types'
|
import {
|
||||||
|
getShortcutDescriptorById,
|
||||||
|
ShortcutDescriptor,
|
||||||
|
} from '@/features/shortcuts/catalog'
|
||||||
|
|
||||||
export const useDeviceShortcut = (kind: MediaDeviceKind) => {
|
export const useDeviceShortcut = (kind: MediaDeviceKind) => {
|
||||||
return useMemo<Shortcut | undefined>(() => {
|
return useMemo<ShortcutDescriptor | undefined>(() => {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'audioinput':
|
case 'audioinput':
|
||||||
return {
|
return getShortcutDescriptorById('toggle-microphone')
|
||||||
key: 'd',
|
|
||||||
ctrlKey: true,
|
|
||||||
}
|
|
||||||
case 'videoinput':
|
case 'videoinput':
|
||||||
return {
|
return getShortcutDescriptorById('toggle-camera')
|
||||||
key: 'e',
|
|
||||||
ctrlKey: true,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function DesktopControlBar({
|
|||||||
const desktopControlBarEl = useRef<HTMLDivElement>(null)
|
const desktopControlBarEl = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useRegisterKeyboardShortcut({
|
useRegisterKeyboardShortcut({
|
||||||
shortcut: { key: 'F2' },
|
id: 'focus-toolbar',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
const root = desktopControlBarEl.current
|
const root = desktopControlBarEl.current
|
||||||
if (!root) return
|
if (!root) return
|
||||||
|
|||||||
47
src/frontend/src/features/shortcuts/catalog.ts
Normal file
47
src/frontend/src/features/shortcuts/catalog.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Shortcut } from './types'
|
||||||
|
|
||||||
|
// Central list of current keyboard shortcuts.
|
||||||
|
// Keep a single source of truth for display and, later, customization
|
||||||
|
export type ShortcutCategory = 'navigation' | 'media' | 'interaction'
|
||||||
|
|
||||||
|
export type ShortcutId =
|
||||||
|
| 'focus-toolbar'
|
||||||
|
| 'toggle-microphone'
|
||||||
|
| 'toggle-camera'
|
||||||
|
| 'push-to-talk'
|
||||||
|
|
||||||
|
export const getShortcutDescriptorById = (id: ShortcutId) =>
|
||||||
|
shortcutCatalog.find((item) => item.id === id)
|
||||||
|
|
||||||
|
export type ShortcutDescriptor = {
|
||||||
|
id: ShortcutId
|
||||||
|
category: ShortcutCategory
|
||||||
|
shortcut?: Shortcut
|
||||||
|
kind?: 'press' | 'longPress'
|
||||||
|
code?: string // used when kind === 'longPress' (KeyboardEvent.code)
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shortcutCatalog: ShortcutDescriptor[] = [
|
||||||
|
{
|
||||||
|
id: 'focus-toolbar',
|
||||||
|
category: 'navigation',
|
||||||
|
shortcut: { key: 'F2' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'toggle-microphone',
|
||||||
|
category: 'media',
|
||||||
|
shortcut: { key: 'd', ctrlKey: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'toggle-camera',
|
||||||
|
category: 'media',
|
||||||
|
shortcut: { key: 'e', ctrlKey: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'push-to-talk',
|
||||||
|
category: 'media',
|
||||||
|
kind: 'longPress',
|
||||||
|
code: 'KeyV',
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { keyboardShortcutsStore } from '@/stores/keyboardShortcuts'
|
import { keyboardShortcutsStore } from '@/stores/keyboardShortcuts'
|
||||||
import { formatShortcutKey } from '@/features/shortcuts/utils'
|
import { formatShortcutKey } from '@/features/shortcuts/utils'
|
||||||
import { Shortcut } from '@/features/shortcuts/types'
|
import { ShortcutId, getShortcutDescriptorById } from './catalog'
|
||||||
|
|
||||||
export type useRegisterKeyboardShortcutProps = {
|
export type useRegisterKeyboardShortcutProps = {
|
||||||
shortcut?: Shortcut
|
id?: ShortcutId
|
||||||
handler: () => Promise<void | boolean | undefined> | void
|
handler: () => Promise<void | boolean | undefined> | void
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRegisterKeyboardShortcut = ({
|
export const useRegisterKeyboardShortcut = ({
|
||||||
shortcut,
|
id,
|
||||||
handler,
|
handler,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
}: useRegisterKeyboardShortcutProps) => {
|
}: useRegisterKeyboardShortcutProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shortcut) return
|
if (!id) return
|
||||||
const formattedKey = formatShortcutKey(shortcut)
|
const descriptor = getShortcutDescriptorById(id)
|
||||||
|
if (!descriptor?.shortcut) return
|
||||||
|
const formattedKey = formatShortcutKey(descriptor.shortcut)
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
keyboardShortcutsStore.shortcuts.delete(formattedKey)
|
keyboardShortcutsStore.shortcuts.delete(formattedKey)
|
||||||
} else {
|
} else {
|
||||||
keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
|
keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
|
||||||
}
|
}
|
||||||
}, [handler, shortcut, isDisabled])
|
}, [handler, id, isDisabled])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user