mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-05 18:01:56 +02:00
Update keybinds form to reject duplicate submissions.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user