Large update to keybindrecorder

This commit is contained in:
HannahPadd
2026-03-26 17:45:56 +01:00
parent 9c8cd8517e
commit 934dd3dee2
5 changed files with 171 additions and 80 deletions

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>
)}
);
}}
/>
);
}

View File

@@ -17,6 +17,7 @@
grid-template-columns: subgrid;
grid-template-rows: subgrid;
place-items: center;
align-items: left;
gap: 20px;
}