Update keybinds form to reject duplicate submissions.

This commit is contained in:
HannahPadd
2026-04-01 11:20:12 +02:00
parent 13f2abb7ab
commit bb0423457f
4 changed files with 122 additions and 258 deletions

View File

@@ -1,6 +1,7 @@
import { useState, forwardRef, useRef } from 'react';
import { Typography } from './Typography';
import classNames from 'classnames';
import { useFormContext } from 'react-hook-form';
const excludedKeys = [' ', 'SPACE', 'META'];
const maxKeybindLength = 4;
@@ -10,17 +11,20 @@ export const KeybindRecorder = forwardRef<
{
keys: string[];
onKeysChange: (v: string[]) => void;
error?: string;
}
>(function KeybindRecorder({ keys, onKeysChange }) {
>(function KeybindRecorder({ keys, onKeysChange, error }) {
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 displayKeys = isRecording ? localKeys : keys;
const activeIndex = isRecording ? displayKeys.length : -1;
const displayError = errorText || error;
const { clearErrors } = useFormContext();
const handleKeyDown = (e: React.KeyboardEvent) => {
e.preventDefault();
@@ -33,7 +37,6 @@ export const KeybindRecorder = forwardRef<
if (errorMsg) {
setErrorText(errorMsg);
setInvalidSlot(activeIndex);
setShowError(true);
setTimeout(() => {
setInvalidSlot(null);
}, 350);
@@ -41,7 +44,6 @@ export const KeybindRecorder = forwardRef<
}
if (displayKeys.length < maxKeybindLength) {
setShowError(false);
const updatedKeys = [...displayKeys, key];
setLocalKeys(updatedKeys);
onKeysChange(updatedKeys);
@@ -53,14 +55,14 @@ export const KeybindRecorder = forwardRef<
const handleOnBlur = () => {
setIsRecording(false);
setShowError(false);
if (displayKeys.length < maxKeybindLength - 2) {
if (displayKeys.length < maxKeybindLength - 2 || error) {
onKeysChange(oldKeys);
setLocalKeys(oldKeys);
}
};
const handleOnFocus = () => {
clearErrors('keybinds');
const initialKeys: string[] = [];
setOldKeys(keys);
setLocalKeys(initialKeys);
@@ -107,9 +109,9 @@ export const KeybindRecorder = forwardRef<
})}
</div>
</div>
{showError && (
{displayError && (
<div className="isInvalid keyslot-invalid">
<Typography color="text-status-critical">{errorText}</Typography>
<Typography color="text-status-critical">{`${errorText} ${error}`}</Typography>
</div>
)}
</div>

View File

@@ -1,5 +1,5 @@
import { BaseModal } from './BaseModal';
import { Controller, Control, UseFormResetField } from 'react-hook-form';
import { Controller, Control, useFormContext } from 'react-hook-form';
import { KeybindRecorder } from './KeybindRecorder';
import { Typography } from './Typography';
import { Button } from './Button';
@@ -10,26 +10,31 @@ export function KeybindRecorderModal({
id,
control,
name,
resetField,
isVisisble,
onClose,
onUnbind,
onSubmit,
}: {
id?: string;
control: Control<any>;
name: string;
resetField: UseFormResetField<any>;
isVisisble: boolean;
onClose: () => void;
onUnbind: () => void;
onSubmit: () => void;
}) {
const { l10n } = useLocalization();
const keybindlocalization = 'settings-keybinds_' + id;
const {
formState: { errors },
resetField,
handleSubmit,
} = useFormContext();
return (
<BaseModal
isOpen={isVisisble}
onRequestClose={() => onClose()}
onRequestClose={onClose}
appendClasses="w-full max-w-xl"
>
<div className="flex flex-col gap-3 w-full justify-between h-full">
@@ -45,6 +50,7 @@ export function KeybindRecorderModal({
keys={field.value ?? []}
onKeysChange={field.onChange}
ref={field.ref}
error={errors.keybinds?.message as string}
/>
)}
/>
@@ -55,7 +61,7 @@ export function KeybindRecorderModal({
variant="tertiary"
onClick={() => {
resetField(name);
onClose();
handleSubmit(onSubmit)();
}}
/>
<Button
@@ -63,7 +69,7 @@ export function KeybindRecorderModal({
variant="tertiary"
onClick={() => {
onUnbind();
onClose();
handleSubmit(onSubmit)();
}}
/>
</div>
@@ -71,7 +77,7 @@ export function KeybindRecorderModal({
<Button
id="settings-keybinds-recorder-modal-done-button"
variant="primary"
onClick={onClose}
onClick={handleSubmit(onSubmit)}
/>
</div>
</div>

View File

@@ -19,7 +19,7 @@ import {
OpenUriRequestT,
RpcMessage,
} from 'solarxr-protocol';
import { useFieldArray, useForm } from 'react-hook-form';
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
import { useAppContext } from '@/hooks/app';
import { useElectron } from '@/hooks/electron';
@@ -45,17 +45,33 @@ export function KeybindSettings() {
const currentIndex = useRef<number | null>(null);
const { installInfo } = useAppContext();
const { control, resetField, handleSubmit, reset, setValue, getValues } =
useForm<KeybindForm>({
defaultValues: defaultKeybindsState,
});
const methods = useForm<KeybindForm>({
defaultValues: defaultKeybindsState,
});
const {
control,
handleSubmit,
reset,
setValue,
getValues,
setError,
clearErrors,
resetField,
} = methods;
const { fields } = useFieldArray({
control,
name: 'keybinds',
});
const onSubmit = (value: KeybindForm) => {
const onSubmit = () => {
const value = getValues();
if (checkDuplicates(value)) {
return;
}
clearErrors('keybinds');
value.keybinds.forEach((kb) => {
const changeKeybindRequest = new ChangeKeybindRequestT();
@@ -68,9 +84,28 @@ export function KeybindSettings() {
changeKeybindRequest.keybind = keybind;
sendRPCPacket(RpcMessage.ChangeKeybindRequest, changeKeybindRequest);
setIsOpen(false);
});
};
const checkDuplicates = (value: KeybindForm) => {
const normalized = value.keybinds
.filter((kb) => kb.binding.length > 0)
.map((kb) => JSON.stringify([...kb.binding].sort()));
const unique = new Set(normalized);
if (unique.size !== normalized.length) {
setError('keybinds', {
type: 'manual',
message: 'Duplicate keybind combinations are not allowed',
});
return true;
}
return false;
};
const handleOpenSystemSettingsButton = () => {
sendRPCPacket(RpcMessage.OpenUriRequest, new OpenUriRequestT());
};
@@ -89,6 +124,7 @@ export function KeybindSettings() {
}));
setDefaultKeybindsState({ keybinds: mappedDefaults });
reset({ keybinds: mappedDefaults });
const mapped = keybind.map((kb) => ({
id: kb.keybindId,
@@ -97,7 +133,6 @@ export function KeybindSettings() {
typeof kb.keybindValue === 'string' ? kb.keybindValue.split('+') : [],
delay: kb.keybindDelay,
}));
reset({ keybinds: mappedDefaults });
mapped.forEach((keybind, index) => {
setValue(`keybinds.${index}.binding`, keybind.binding);
@@ -113,6 +148,13 @@ export function KeybindSettings() {
}
};
const onClose = () => {
if (currentIndex.current != null) {
resetField(`keybinds.${currentIndex.current}.binding`);
}
setIsOpen(false);
};
const createKeybindRows = (): ReactNode => {
return fields.map((field, index) => {
return (
@@ -146,7 +188,7 @@ export function KeybindSettings() {
<Typography key={i}>{line}</Typography>
))}
</div>
{installInfo?.isWayland ? (
{!installInfo?.isWayland ? (
<div className="flex flex-col gap-4">
<Typography id="settings-keybinds-wayland-description" />
<div>
@@ -160,57 +202,58 @@ export function KeybindSettings() {
</div>
) : (
electron.isElectron &&
electron.data().os.type === 'windows' && (
electron.data().os.type !== 'windows' && (
<>
<div className="keybind-settings">
<Typography
id="keybind_config-keybind_name"
variant="section-title"
/>
<Typography
id="keybind_config-keybind_value"
variant="section-title"
/>
<Typography
id="keybind_config-keybind_delay"
variant="section-title"
/>
{createKeybindRows()}
</div>
<div className="flex justify-end">
<Button
id="settings-keybinds_reset-all-button"
onClick={() => {
reset(defaultKeybindsState);
handleSubmit(onSubmit)();
<FormProvider {...methods}>
<div className="keybind-settings">
<Typography
id="keybind_config-keybind_name"
variant="section-title"
/>
<Typography
id="keybind_config-keybind_value"
variant="section-title"
/>
<Typography
id="keybind_config-keybind_delay"
variant="section-title"
/>
{createKeybindRows()}
</div>
<div className="flex justify-end">
<Button
id="settings-keybinds_reset-all-button"
onClick={() => {
reset(defaultKeybindsState);
handleSubmit(onSubmit)();
}}
variant="primary"
/>
</div>
<KeybindRecorderModal
id={
currentIndex.current != null
? fields[currentIndex.current].name
: ''
}
control={control}
name={
currentIndex.current != null
? `keybinds.${currentIndex.current}.binding`
: ''
}
isVisisble={isOpen}
onClose={onClose}
onUnbind={() => {
if (currentIndex.current != null)
setValue(
`keybinds.${currentIndex.current}.binding`,
[]
);
}}
variant="primary"
onSubmit={onSubmit}
/>
</div>
<KeybindRecorderModal
id={
currentIndex.current != null
? fields[currentIndex.current].name
: ''
}
control={control}
resetField={resetField}
name={
currentIndex.current != null
? `keybinds.${currentIndex.current}.binding`
: ''
}
isVisisble={isOpen}
onClose={() => {
setIsOpen(false);
handleSubmit(onSubmit)();
}}
onUnbind={() => {
if (currentIndex.current != null)
setValue(`keybinds.${currentIndex.current}.binding`, []);
}}
/>
</FormProvider>
</>
)
)}

View File

@@ -1,187 +0,0 @@
---
server:
trackerPort: 6969
useMagnetometerOnAllTrackers: false
filters:
type: "prediction"
amount: 0.2
driftCompensation:
enabled: false
prediction: false
amount: 0.8
maxResets: 6
oscRouter:
enabled: false
portIn: 9002
portOut: 9000
address: "127.0.0.1"
vrcOSC:
enabled: false
portIn: 9001
portOut: 9000
address: "127.0.0.1"
trackers:
right_foot: true
left_foot: true
waist: true
oscqueryEnabled: true
vmc:
enabled: false
portIn: 39540
portOut: 39539
address: "127.0.0.1"
anchorHip: true
vrmJson: null
mirrorTracking: false
autoBone:
cursorIncrement: 2
minDataDistance: 1
maxDataDistance: 1
numEpochs: 50
printEveryNumEpochs: 25
initialAdjustRate: 10.0
adjustRateDecay: 1.0
slideErrorFactor: 1.0
offsetSlideErrorFactor: 0.0
footHeightOffsetErrorFactor: 0.0
bodyProportionErrorFactor: 0.05
heightErrorFactor: 0.0
positionErrorFactor: 0.0
positionOffsetErrorFactor: 0.0
calcInitError: false
randomizeFrameOrder: true
scaleEachStep: true
sampleCount: 1500
sampleRateMs: 20
saveRecordings: false
useSkeletonHeight: false
randSeed: 4
useFrameFiltering: false
maxFinalError: 0.03
keybindings:
keybinds:
"0":
id: 0
name: "full-reset"
binding: "CTRL+ALT+SHIFT+Y"
delay: 0.0
"1":
id: 1
name: "yaw-reset"
binding: "CTRL+ALT+SHIFT+U"
delay: 0.0
"2":
id: 2
name: "mounting-reset"
binding: "CTRL+ALT+SHIFT+I"
delay: 0.0
"4":
id: 4
name: "feet-mounting-reset"
binding: "CTRL+ALT+SHIFT+P"
delay: 0.0
"3":
id: 3
name: "pause-tracking"
binding: "CTRL+ALT+SHIFT+O"
delay: 0.0
skeleton:
toggles: {}
values: {}
offsets:
hipLength: 0.04
upperLegLength: 0.42
footLength: 0.05
handDistanceY: 0.035
headShift: 0.1
lowerArmLength: 0.26
shouldersWidth: 0.35
waistLength: 0.2
handDistanceZ: 0.13
chestOffset: 0.0
chestLength: 0.16
lowerLegLength: 0.5
neckLength: 0.1
upperArmLength: 0.26
hipsWidth: 0.26
footShift: -0.05
elbowOffset: 0.0
hipOffset: 0.0
shouldersDistance: 0.08
upperChestLength: 0.16
skeletonOffset: 0.0
hmdHeight: 0.0
floorHeight: 0.0
legTweaks:
correctionStrength: 0.3
alwaysUseFloorclip: false
tapDetection:
yawResetDelay: 0.2
fullResetDelay: 1.0
mountingResetDelay: 1.0
yawResetEnabled: true
fullResetEnabled: true
mountingResetEnabled: true
setupMode: false
yawResetTaps: 2
fullResetTaps: 3
mountingResetTaps: 3
numberTrackersOverThreshold: 1
resetsConfig:
resetMountingFeet: false
mode: "BACK"
yawResetSmoothTime: 0.0
saveMountingReset: false
resetHmdPitch: false
lastMountingMethod: "AUTOMATIC"
yawResetDelay: 0.0
fullResetDelay: 3.0
mountingResetDelay: 3.0
stayAlignedConfig:
enabled: false
standingRelaxedPose:
enabled: false
upperLegAngleInDeg: 0.0
lowerLegAngleInDeg: 0.0
footAngleInDeg: 0.0
sittingRelaxedPose:
enabled: false
upperLegAngleInDeg: 0.0
lowerLegAngleInDeg: 0.0
footAngleInDeg: 0.0
flatRelaxedPose:
enabled: false
upperLegAngleInDeg: 0.0
lowerLegAngleInDeg: 0.0
footAngleInDeg: 0.0
setupComplete: false
hidConfig:
trackersOverHID: false
trackers: {}
bridges:
steamvr:
trackers:
right_knee: false
chest: false
right_foot: false
right_hand: false
left_foot: false
left_hand: false
waist: false
right_elbow: false
left_knee: false
hmd: false
left_elbow: false
automaticSharedTrackersToggling: true
steamvr_feeder:
trackers: {}
automaticSharedTrackersToggling: true
knownDevices: []
overlay:
mirrored: false
visible: false
trackingChecklist:
ignoredStepsIds: []
vrcConfig:
mutedWarnings: []
modelVersion: "15"