fix: Added warning to loging with lookup provider for better UX feat: Added ROMM Client API Token in plugin settings
226 lines
No EOL
7.1 KiB
TypeScript
226 lines
No EOL
7.1 KiB
TypeScript
import { DependencyList, useEffect, useState } from "react";
|
|
import { GamepadButtonEvent } from "./gamepads";
|
|
import { dispatchFocusedEvent, GetFocusedTree } from "./spatialNavigation";
|
|
import { getCurrentFocusKey } from "@noriginmedia/norigin-spatial-navigation";
|
|
import { isTextInputFocused } from "./utils";
|
|
|
|
const shortcutMap = new Map<string, (() => Shortcut[])[]>();
|
|
const conflictSet = new Set<number>();
|
|
let hadEnterDown = false;
|
|
|
|
export enum GamePadButtonCode
|
|
{
|
|
A = 0,
|
|
B = 1,
|
|
X = 2,
|
|
Y = 3,
|
|
L1 = 4,
|
|
R1 = 5,
|
|
L2 = 6,
|
|
R2 = 7,
|
|
Select = 8,
|
|
Start = 9,
|
|
LJoy = 10,
|
|
RJoy = 11,
|
|
Up = 12,
|
|
Down = 13,
|
|
Left = 14,
|
|
Right = 15,
|
|
Steam = 16
|
|
}
|
|
|
|
export interface Shortcut
|
|
{
|
|
label?: string;
|
|
button: GamePadButtonCode;
|
|
heldTime?: number;
|
|
action?: (e: GamepadButtonEvent) => void;
|
|
side?: "left" | "right";
|
|
}
|
|
|
|
let isDirty = false;
|
|
const shortcutChangeDispatcher = setInterval(() =>
|
|
{
|
|
if (!isDirty) return;
|
|
window.dispatchEvent(new Event('shortcutsChanged'));
|
|
isDirty = false;
|
|
}, 100);
|
|
import.meta.hot?.dispose(() => clearInterval(shortcutChangeDispatcher));
|
|
|
|
function markDirtyThrottled ()
|
|
{
|
|
isDirty = true;
|
|
}
|
|
|
|
window.addEventListener('focuschanged', markDirtyThrottled);
|
|
import.meta.hot?.dispose(() => window.removeEventListener('focuschanged', markDirtyThrottled));
|
|
import.meta.hot?.dispose(() => shortcutMap.clear());
|
|
|
|
export function useShortcutContext ()
|
|
{
|
|
const [array, setArray] = useState<({ key: string; } & Shortcut)[] | undefined>();
|
|
|
|
useEffect(() =>
|
|
{
|
|
const handleShortcutRebuild = () =>
|
|
{
|
|
conflictSet.clear();
|
|
const focusKey = getCurrentFocusKey();
|
|
const newArray = GetFocusedTree(focusKey)
|
|
.filter(f => shortcutMap.has(f))
|
|
.flatMap(f => shortcutMap.get(f)!.map(s => ({ key: f, handler: s })))
|
|
.flatMap(kvp => kvp.handler().map(s => ({ key: kvp.key, ...s })))
|
|
.filter(s =>
|
|
{
|
|
const empty = !conflictSet.has(s.button);
|
|
conflictSet.add(s.button);
|
|
return empty;
|
|
});
|
|
if (!compareShortcutArrays(newArray, array))
|
|
{
|
|
setArray(newArray);
|
|
}
|
|
};
|
|
|
|
const shortcuts = new Map(array?.reverse().map(s => [s.button, s]) ?? []);
|
|
const holdChecks = new Map<GamePadButtonCode, NodeJS.Timeout>();
|
|
|
|
const handleGamepadButtonDown = (e: Event) =>
|
|
{
|
|
const event = e as GamepadButtonEvent;
|
|
if (event.button == GamePadButtonCode.B && document.fullscreenElement)
|
|
{
|
|
document.exitFullscreen();
|
|
return;
|
|
}
|
|
|
|
if (shortcuts.has(event.button))
|
|
{
|
|
const shortcut = shortcuts.get(event.button);
|
|
if (shortcut)
|
|
{
|
|
if (shortcut.heldTime && shortcut.heldTime > 0)
|
|
{
|
|
holdChecks.set(event.button, setTimeout(() =>
|
|
{
|
|
shortcut.action?.(event);
|
|
}, shortcut.heldTime));
|
|
} else
|
|
{
|
|
shortcut.action?.(event);
|
|
}
|
|
|
|
}
|
|
}
|
|
else if (event.button === GamePadButtonCode.A)
|
|
{
|
|
dispatchFocusedEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', charCode: 13, keyCode: 13, view: window, bubbles: true }));
|
|
hadEnterDown = true;
|
|
}
|
|
};
|
|
|
|
const handleKeyPress = (e: KeyboardEvent) =>
|
|
{
|
|
if (e.key === 'Escape')
|
|
{
|
|
shortcuts.get(GamePadButtonCode.B)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.B }));
|
|
} else
|
|
{
|
|
// We use backspace and space in typing
|
|
if (isTextInputFocused())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (e.key === 'Backspace')
|
|
{
|
|
shortcuts.get(GamePadButtonCode.X)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.X }));
|
|
} else if (e.key === ' ')
|
|
{
|
|
shortcuts.get(GamePadButtonCode.Y)?.action?.(new GamepadButtonEvent('gamepadbuttondown', { button: GamePadButtonCode.Y }));
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleGamepadButtonUp = (e: Event) =>
|
|
{
|
|
const event = e as GamepadButtonEvent;
|
|
if (hadEnterDown && event.button === GamePadButtonCode.A)
|
|
{
|
|
dispatchFocusedEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', charCode: 13, keyCode: 13, view: window, bubbles: true }));
|
|
}
|
|
|
|
if (shortcuts.has(event.button))
|
|
{
|
|
if (holdChecks.has(event.button))
|
|
{
|
|
clearInterval(holdChecks.get(event.button));
|
|
}
|
|
}
|
|
};
|
|
|
|
function compareShortcut (a: Shortcut, b: Shortcut)
|
|
{
|
|
return a.action === b.action && a.button === b.button && a.label === b.label;
|
|
}
|
|
|
|
function compareShortcutArrays (a: Shortcut[] | undefined, b: Shortcut[] | undefined)
|
|
{
|
|
if (a === b) return true;
|
|
if (a === undefined || b === undefined) return false;
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i++)
|
|
{
|
|
if (!compareShortcut(a[i], b[i]))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!array)
|
|
{
|
|
handleShortcutRebuild();
|
|
}
|
|
window.addEventListener('gamepadbuttondown', handleGamepadButtonDown);
|
|
window.addEventListener('keydown', handleKeyPress);
|
|
window.addEventListener('gamepadbuttonup', handleGamepadButtonUp);
|
|
window.addEventListener('shortcutsChanged', handleShortcutRebuild);
|
|
|
|
return () =>
|
|
{
|
|
window.removeEventListener('gamepadbuttondown', handleGamepadButtonDown);
|
|
window.removeEventListener('gamepadbuttonup', handleGamepadButtonUp);
|
|
window.removeEventListener('shortcutsChanged', handleShortcutRebuild);
|
|
window.removeEventListener('keydown', handleKeyPress);
|
|
holdChecks.forEach(c => clearInterval(c));
|
|
};
|
|
}, [array]);
|
|
|
|
return { shortcuts: array };
|
|
}
|
|
|
|
export function useShortcuts (focusKey: string, build: () => Shortcut[], deps?: DependencyList)
|
|
{
|
|
useEffect(() =>
|
|
{
|
|
const array = shortcutMap.get(focusKey) ?? [];
|
|
array.push(build);
|
|
shortcutMap.set(focusKey, array);
|
|
markDirtyThrottled();
|
|
|
|
return () =>
|
|
{
|
|
const array = shortcutMap.get(focusKey);
|
|
if (array)
|
|
{
|
|
const index = array.indexOf(build);
|
|
array?.splice(index, 1);
|
|
}
|
|
|
|
markDirtyThrottled();
|
|
};
|
|
}, [focusKey, ...deps ?? []]);
|
|
|
|
} |