mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Large update to keybindrecorder
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useState, forwardRef } from 'react';
|
||||
import { useState, forwardRef, useRef } from 'react';
|
||||
import { Typography } from './Typography';
|
||||
|
||||
const excludedKeys = ['CONTROL', ' ', 'SPACE', 'ALT', 'META'];
|
||||
const keyOffset = 2;
|
||||
const maxKeybindLength = 4;
|
||||
|
||||
export const KeybindRecorder = forwardRef<
|
||||
HTMLInputElement,
|
||||
@@ -8,55 +13,104 @@ export const KeybindRecorder = forwardRef<
|
||||
onKeysChange: (v: string[]) => void;
|
||||
}
|
||||
>(function KeybindRecorder({ keys, onKeysChange }, ref) {
|
||||
const [localKeys, setLocalKeys] = useState<string[]>(keys);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [oldKeys, setOldKeys] = useState<string[]>([]);
|
||||
const [invalidSlot, setInvalidSlot] = useState<number | null>(null);
|
||||
const [showError, setShowError] = useState<boolean>(false);
|
||||
const [errorText, setErrorText] = useState<string>('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
const displayKeys = isRecording ? localKeys : keys;
|
||||
const activeIndex = isRecording ? displayKeys.length : -1;
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
const key = e.key.toUpperCase();
|
||||
const errorMsg = excludedKeys.includes(key)
|
||||
? `Cannot use ${key}!`
|
||||
: displayKeys.includes(key)
|
||||
? `${key} is a Duplicate Key!`
|
||||
: null;
|
||||
if (errorMsg) {
|
||||
setErrorText(errorMsg);
|
||||
setInvalidSlot(activeIndex);
|
||||
setShowError(true);
|
||||
setTimeout(() => {
|
||||
setInvalidSlot(null);
|
||||
}, 350);
|
||||
return;
|
||||
}
|
||||
|
||||
if (displayKeys.length < maxKeybindLength) {
|
||||
setShowError(false);
|
||||
const updatedKeys = [...displayKeys, key];
|
||||
setLocalKeys(updatedKeys);
|
||||
onKeysChange([...keys, key]);
|
||||
if (updatedKeys.length === maxKeybindLength) {
|
||||
handleOnBlur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnBlur = () => {
|
||||
console.log(`onblur keys length ${keys.length}`);
|
||||
if (inputRef != null && typeof inputRef !== 'function') {
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
setIsRecording(false);
|
||||
setShowError(false);
|
||||
if (displayKeys.length == keyOffset) {
|
||||
onKeysChange(oldKeys);
|
||||
setLocalKeys(oldKeys);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnFocus = () => {
|
||||
const initialKeys = ['CTRL', 'ALT'];
|
||||
setOldKeys(keys);
|
||||
setLocalKeys(initialKeys);
|
||||
onKeysChange(initialKeys);
|
||||
setIsRecording(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<style>
|
||||
{`
|
||||
@keyframes keyslot {
|
||||
0%, 100% { transform: scale(1); opacity: 0.6; }
|
||||
50% { transform: scale(1.08); opacity: 1; }
|
||||
}
|
||||
|
||||
.keyslot-animate {
|
||||
animation: keyslot 1s ease-in-out infinite;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
{!showError ? (
|
||||
<div className="absolute bottom-">
|
||||
<Typography>{errorText}</Typography>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
ref={inputRef}
|
||||
className="opacity-0 absolute inset-0 cursor-pointer"
|
||||
onFocus={() => {
|
||||
setOldKeys(keys);
|
||||
onKeysChange(['CTRL', 'ALT']);
|
||||
setIsRecording(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsRecording(false);
|
||||
if (keys.length < 4) onKeysChange(oldKeys);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
const key = e.key.toUpperCase();
|
||||
if (!keys.includes(key) && keys.length < 4) {
|
||||
onKeysChange([...keys, key]);
|
||||
}
|
||||
}}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<div className="flex gap-2 min-h-[42px] items-center px-3 py-2 rounded-lg bg-background-80">
|
||||
<div className="flex flex-grow gap-2 min-w-[180px]">
|
||||
{Array.from({ length: 4 }).map((_, i) => {
|
||||
const key = keys[i];
|
||||
const isActive = isRecording && i === keys.length;
|
||||
{Array.from({ length: maxKeybindLength }).map((_, i) => {
|
||||
const key = displayKeys[i];
|
||||
const isActive = isRecording && i === activeIndex;
|
||||
const isInvalid = invalidSlot === i;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`
|
||||
px-2 py-1 rounded-md text-sm min-w-[32px] text-center
|
||||
${key ? 'bg-accent-background-50' : 'bg-accent-background-50/30'}
|
||||
${isActive ? 'keyslot-animate ring-2 ring-accent' : ''}
|
||||
${
|
||||
isInvalid
|
||||
? 'keyslot-invalid ring-2 ring-red-600'
|
||||
: isActive
|
||||
? 'keyslot-animate ring-2 ring-accent'
|
||||
: 'ring-accent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{key ?? ''}
|
||||
@@ -65,7 +119,7 @@ export const KeybindRecorder = forwardRef<
|
||||
})}
|
||||
</div>
|
||||
<div className="w-40 flex-shrink-0 text-accent-background-10 text-right text-sm font-medium">
|
||||
{keys.length < 4 && isRecording
|
||||
{displayKeys.length < maxKeybindLength && isRecording
|
||||
? l10n.getString('settings-keybinds_now-recording')
|
||||
: l10n.getString('settings-keybinds_record-keybind')}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,40 @@
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.number-selector::after {
|
||||
gap: 0;
|
||||
.number-selector::after {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@keyframes keyslot {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% { transform: translate(1px, 1px) rotate(0deg); }
|
||||
10% { transform: translate(-1px, -2px) rotate(-1deg); }
|
||||
20% { transform: translate(-3px, 0px) rotate(1deg); }
|
||||
30% { transform: translate(3px, 2px) rotate(0deg); }
|
||||
40% { transform: translate(1px, -1px) rotate(1deg); }
|
||||
50% { transform: translate(-1px, 2px) rotate(-1deg); }
|
||||
60% { transform: translate(-3px, 1px) rotate(0deg); }
|
||||
70% { transform: translate(3px, 1px) rotate(-1deg); }
|
||||
80% { transform: translate(-1px, -1px) rotate(1deg); }
|
||||
90% { transform: translate(1px, 2px) rotate(0deg); }
|
||||
100% { transform: translate(1px, -2px) rotate(-1deg); }
|
||||
}
|
||||
|
||||
.keyslot-animate {
|
||||
animation: keyslot 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.keyslot-invalid {
|
||||
animation: shake 0.35s ease;
|
||||
}
|
||||
|
||||
@@ -57,60 +57,62 @@ export function NumberSelector({
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<Typography bold>{label}</Typography>
|
||||
<div className="flex gap-5 bg-background-60 p-2 rounded-lg">
|
||||
<div className="flex gap-1">
|
||||
{doubleStep !== undefined && (
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
{label?.length != 0 ? <Typography bold>{label}</Typography> : <></>}
|
||||
<div className="flex gap-5 bg-background-60 p-2 rounded-lg">
|
||||
<div className="flex gap-1">
|
||||
{doubleStep !== undefined && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(doubleStepFn(value, false))}
|
||||
disabled={doubleStepFn(value, false) < min || disabled}
|
||||
>
|
||||
{showButtonWithNumber
|
||||
? decimalFormat.format(-doubleStep)
|
||||
: '--'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(doubleStepFn(value, false))}
|
||||
disabled={doubleStepFn(value, false) < min || disabled}
|
||||
onClick={() => onChange(stepFn(value, false))}
|
||||
disabled={stepFn(value, false) < min || disabled}
|
||||
>
|
||||
{showButtonWithNumber
|
||||
? decimalFormat.format(-doubleStep)
|
||||
: '--'}
|
||||
-
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(stepFn(value, false))}
|
||||
disabled={stepFn(value, false) < min || disabled}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center text-center items-center w-10 text-standard">
|
||||
{valueLabelFormat ? valueLabelFormat(value) : value}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(stepFn(value, true))}
|
||||
disabled={stepFn(value, true) > max || disabled}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
{doubleStep !== undefined && (
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center text-center items-center w-10 text-standard">
|
||||
{valueLabelFormat ? valueLabelFormat(value) : value}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(doubleStepFn(value, true))}
|
||||
disabled={doubleStepFn(value, true) > max || disabled}
|
||||
onClick={() => onChange(stepFn(value, true))}
|
||||
disabled={stepFn(value, true) > max || disabled}
|
||||
>
|
||||
{showButtonWithNumber
|
||||
? decimalFormat.format(doubleStep)
|
||||
: '++'}
|
||||
+
|
||||
</Button>
|
||||
)}
|
||||
{doubleStep !== undefined && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(doubleStepFn(value, true))}
|
||||
disabled={doubleStepFn(value, true) > max || disabled}
|
||||
>
|
||||
{showButtonWithNumber
|
||||
? decimalFormat.format(doubleStep)
|
||||
: '++'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
grid-template-columns: subgrid;
|
||||
grid-template-rows: subgrid;
|
||||
|
||||
place-items: center;
|
||||
align-items: left;
|
||||
gap: 20px;
|
||||
|
||||
}
|
||||
|
||||
Submodule solarxr-protocol updated: bb6f13be59...522d685732
Reference in New Issue
Block a user