This commit is contained in:
HannahPadd
2026-03-30 17:48:50 +02:00
parent cde42aa071
commit 9d26032460
10 changed files with 218 additions and 166 deletions

View File

@@ -1,6 +1,6 @@
import { useLocalization } from '@fluent/react';
import { useState, forwardRef, useRef } from 'react';
import { Typography } from './Typography';
import { RecordIcon } from './icon/RecordIcon';
const excludedKeys = [' ', 'SPACE', 'META'];
const maxKeybindLength = 4;
@@ -44,21 +44,17 @@ export const KeybindRecorder = forwardRef<
setShowError(false);
const updatedKeys = [...displayKeys, key];
setLocalKeys(updatedKeys);
onKeysChange([...keys, key]);
if (updatedKeys.length === maxKeybindLength) {
handleOnBlur();
onKeysChange(updatedKeys);
if (updatedKeys.length == maxKeybindLength) {
inputRef.current?.blur();
}
}
};
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 === maxKeybindLength) {
if (displayKeys.length < maxKeybindLength - 2) {
onKeysChange(oldKeys);
setLocalKeys(oldKeys);
}
@@ -73,22 +69,23 @@ export const KeybindRecorder = forwardRef<
};
return (
<div className="relative w-full">
{!showError ? (
<div className="absolute bottom-">
<Typography>{errorText}</Typography>
<div className="relative w-full justify-center align-center">
{showError ? (
<div className="absolute bottom isInvalid keyslot-invalid text-red-600">
<Typography color="red-600">{errorText}</Typography>
</div>
) : (
''
)}
<input
ref={inputRef}
className="opacity-0 absolute inset-0 cursor-pointer"
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onKeyDown={handleKeyDown}
/>
<div className="flex gap-2 min-h-[42px] items-center">
<div className="flex gap-2 my-5 min-h-[60px] w-full items-center bg-background-70 rounded-md">
<input
autoFocus
ref={inputRef}
className="opacity-0 absolute inset-0 cursor-pointer"
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onKeyDown={handleKeyDown}
/>
<div className="flex flex-grow gap-2 justify-center">
{Array.from({ length: maxKeybindLength }).map((_, i) => {
const key = displayKeys[i];
@@ -99,8 +96,8 @@ export const KeybindRecorder = forwardRef<
<div
key={i}
className={`
rounded-md min-w-[50px] min-h-[50px] text-lg flex items-center justify-center hover:ring-2 hover:ring-accent
${key ? 'bg-background-90' : 'bg-background-80'}
rounded-md m-2 min-w-[50px] min-h-[50px] text-lg flex items-center justify-center hover:ring-2 hover:ring-accent
${key ? 'bg-background-90 p-2' : 'bg-background-80'}
${
isInvalid
? 'keyslot-invalid ring-2 ring-red-600'
@@ -119,14 +116,6 @@ export const KeybindRecorder = forwardRef<
);
})}
</div>
{/*
<div className="w-40 flex-shrink-0 text-accent-background-10 text-right text-sm font-medium">
{displayKeys.length < maxKeybindLength && isRecording
? l10n.getString('settings-keybinds_now-recording')
: l10n.getString('settings-keybinds_record-keybind')}
</div>
*/}
</div>
</div>
);

View File

@@ -2,67 +2,73 @@ import { BaseModal } from './BaseModal';
import {
Controller,
Control,
UseFormResetField
UseFormResetField,
} from 'react-hook-form';
import { KeybindRecorder } from './KeybindRecorder';
import { Typography } from './Typography';
import { Button } from './Button';
import './KeybindRow.scss';
import { useLocalization } from '@fluent/react';
export function KeybindRecorderModal({
id,
control,
resetField,
name,
delay,
resetField,
isVisisble,
onClose,
onUnbind,
}: {
id?: string;
control: Control<any>;
resetField: UseFormResetField<any>;
name: string;
delay: string;
resetField: UseFormResetField<any>;
isVisisble: boolean;
onClose: () => void;
onUnbind: () => void;
}) {
const { l10n } = useLocalization();
return (
<BaseModal
isOpen={isVisisble}
appendClasses="w-full max-w-xl h-full max-h-48"
appendClasses="w-full max-w-xl h-full max-h-60"
>
<div className="flex-col gap-4 w-full">
<div className="flex flex-col w-full">
<div className="flex flex-col gap-3 w-full">
<Typography variant="section-title">
Create keybind for {l10n.getString(`settings-keybinds_${id}`)}
Assign keybind for {l10n.getString(`settings-keybinds_${id}`)}
</Typography>
<Controller
control={control}
name={name}
render={({ field }) => (
<KeybindRecorder
keys={field.value ?? []}
keys={field.value ?? []}
onKeysChange={field.onChange}
ref={field.ref}
/>
)}
/>
<div className="flex flex-row justify-end gap-4">
<Button
id="settings-keybinds_reset-button"
variant="primary"
onClick={() => {
resetField(name);
resetField(delay);
}}
/>
<Button id="" variant="primary" onClick={onClose}>
Done
</Button>
<div className="flex flex-row justify-between w-full">
<div className="flex flex-row justify-start gap-4">
<Button
id="settings-keybinds_reset-button"
variant="primary"
onClick={() => {
resetField(name)
}}
/>
<Button variant="primary" onClick={onUnbind}>
unbind
</Button>
</div>
<div className="flex flex-row justify-end">
<Button id="" variant="primary" onClick={onClose}>
Done
</Button>
</div>
</div>
</div>
</div>

View File

@@ -55,14 +55,6 @@ export function KeybindRow({
delay={delay}
isVisisble={true}
/>
<NumberSelector
control={control}
name={delay}
valueLabelFormat={(value) => secondsFormat.format(value)}
min={0}
max={10}
step={0.2}
/>
<div className="max-w-[45px]">
<Button
id="settings-keybinds_reset-button"

View File

@@ -1,15 +1,23 @@
import { Typography } from './Typography';
import './KeybindRow.scss';
import { ReactNode } from 'react';
import { ReactNode, useEffect, useState } from 'react';
import { Control, UseFormGetValues, useWatch } from 'react-hook-form';
import { NumberSelector } from './NumberSelector';
import { useLocaleConfig } from '@/i18n/config';
const createKeybindDisplay = (keybind: string[]): ReactNode | null => {
console.log(keybind.length);
if (keybind.length <= 1) {
return (
<div className="flex min-h-[50px] text-lg items-center justifiy-center">
Click to edit keybind
</div>
);
}
return keybind.map((key, i) => {
return (
<div className="flex flex-row">
<div
key={i}
className="flex rounded-xl min-w-[50px] min-h-[50px] text-lg justify-center items-center bg-background-70"
>
<div key={i} className="flex flex-row">
<div className="flex p-2 rounded-xl min-w-[50px] min-h-[50px] text-lg justify-center items-center bg-background-70">
{key ?? ''}
</div>
<div className="flex justify-center items-center text-lg gap-2 pl-3">
@@ -22,24 +30,60 @@ const createKeybindDisplay = (keybind: string[]): ReactNode | null => {
export function NewKeybindsRow({
id,
keybind,
delay,
control,
index,
getValue,
openKeybindRecorderModal,
}: {
id?: string;
keybind?: string[];
delay?: number;
control: Control<any>;
index: number;
getValue: UseFormGetValues<any>;
openKeybindRecorderModal: (index: number) => void;
}) {
const [keybindDisplay, setKeybindDisplay] = useState<ReactNode>(null);
const [binding, setBinding] = useState<string[]>();
const { currentLocales } = useLocaleConfig();
const secondsFormat = new Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'second',
unitDisplay: 'narrow',
maximumFractionDigits: 2,
});
const handleOpenModal = () => {
openKeybindRecorderModal(index);
};
useEffect(() => {
setBinding(getValue(`keybinds.${index}.binding`));
});
useEffect(() => {
if (binding != null) setKeybindDisplay(createKeybindDisplay(binding));
}, [binding]);
return (
<div className="keybind-row bg-background-60 rounded-xl hover:ring-2 hover:ring-accent">
<label className="text-sm font-medium text-background-10 p-2">
<Typography id={`settings-keybinds_${id}`} />
</label>
<div className="flex gap-2 min-h-[42px] items-center px-3 py-2 rounded-lg bg-background-80">
<div
className="flex gap-2 min-h-[42px] items-center px-3 py-2 rounded-lg bg-background-80 hover:ring-2 hover:ring-purple-500"
onClick={handleOpenModal}
>
<div className="flex flex-grow gap-2 justify-center">
{keybind != null ? createKeybindDisplay(keybind) : ''}
{keybindDisplay}
</div>
</div>
<div>{delay}</div>
<NumberSelector
control={control}
name={`keybinds.${index}.delay`}
valueLabelFormat={(value) => secondsFormat.format(value)}
min={0}
max={10}
step={0.2}
/>
</div>
);
}

View File

@@ -18,87 +18,102 @@ import {
KeybindT,
RpcMessage,
} from 'solarxr-protocol';
import { useForm } from 'react-hook-form';
import { useFieldArray, useForm } from 'react-hook-form';
export type KeybindForm = {
id: number;
name: string;
binding: string[];
delay: number;
keybinds: {
id: number;
name: string;
binding: string[];
delay: number;
}[];
};
export function NewKeybindSettings() {
const { l10n } = useLocalization();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const [isOpen, setIsOpen] = useState<boolean>(false);
const [keybinds, setKeybinds] = useState<KeybindT[]>();
const [selectedKeybind, setSelectedKeybind] = useState<KeybindT | null>(null);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
const [defaultKeybindsState, setDefaultKeybindsState] = useState<KeybindForm>(
{
keybinds: [],
}
);
const { control, resetField, reset, handleSubmit, watch } =
useForm<KeybindForm>({});
const { control, resetField, handleSubmit, reset, setValue, getValues } =
useForm<KeybindForm>({
defaultValues: defaultKeybindsState,
});
useEffect(() => {
const subscription = watch(() => handleSubmit(onSubmit)());
return () => subscription.unsubscribe();
}, []);
const onSubmit = (value: KeybindForm) => {
const changeKeybindRequest = new ChangeKeybindRequestT();
const keybind = new KeybindT();
keybind.keybindId = value.id;
keybind.keybindNameId = value.name;
keybind.keybindValue = value.binding.join('+');
keybind.keybindDelay = value.delay;
changeKeybindRequest.keybind = keybind;
console.log(`Onsubmit: ${keybind.keybindValue}`);
sendRPCPacket(RpcMessage.ChangeKeybindRequest, changeKeybindRequest);
};
useRPCPacket(RpcMessage.KeybindResponse, ({ keybind }: KeybindResponseT) => {
if (!keybind) return;
setKeybinds(keybind);
const { fields } = useFieldArray({
control,
name: 'keybinds',
});
const handleOnClick = () => {
console.log('pressed');
};
const handleOpenRecorderModal = (index: number) => {
if (keybinds == null) return;
console.log('Handle open recorder modal');
const kb = keybinds[index];
const onSubmit = (value: KeybindForm) => {
value.keybinds.forEach((kb) => {
const changeKeybindRequest = new ChangeKeybindRequestT();
setSelectedKeybind(kb);
setIsOpen(true);
const keybind = new KeybindT();
keybind.keybindId = kb.id;
keybind.keybindNameId = kb.name;
keybind.keybindValue = kb.binding.join('+');
keybind.keybindDelay = kb.delay;
reset({
id: kb.keybindId,
name: typeof kb.keybindNameId === 'string' ? kb.keybindNameId : '',
binding:
typeof kb.keybindValue === 'string' ? kb.keybindValue.split('+') : [],
delay: kb.keybindDelay,
changeKeybindRequest.keybind = keybind;
sendRPCPacket(RpcMessage.ChangeKeybindRequest, changeKeybindRequest);
});
};
useRPCPacket(
RpcMessage.KeybindResponse,
({ keybind, defaultKeybinds }: KeybindResponseT) => {
if (!keybind) return;
const mappedDefaults = defaultKeybinds.map((kb) => ({
id: kb.keybindId,
name: typeof kb.keybindNameId === 'string' ? kb.keybindNameId : '',
binding:
typeof kb.keybindValue === 'string' ? kb.keybindValue.split('+') : [],
delay: kb.keybindDelay,
}));
setDefaultKeybindsState({ keybinds: mappedDefaults });
const mapped = keybind.map((kb) => ({
id: kb.keybindId,
name: typeof kb.keybindNameId === 'string' ? kb.keybindNameId : '',
binding:
typeof kb.keybindValue === 'string' ? kb.keybindValue.split('+') : [],
delay: kb.keybindDelay,
}));
reset({ keybinds: mappedDefaults });
mapped.forEach((keybind, index) => {
setValue(`keybinds.${index}.binding`, keybind.binding);
setValue(`keybinds.${index}.delay`, keybind.delay);
});
}
);
const handleOpenRecorderModal = (index: number) => {
console.log('Handle open recorder modal', index);
setSelectedIndex(index);
setIsOpen(true);
};
const createKeybindRows = (): ReactNode => {
if (keybinds == null) return '';
return keybinds.map((key, i) => {
return fields.map((field, index) => {
return (
<div className="keybind-row" onClick={() => handleOpenRecorderModal(i)}>
<div className="keybind-row">
<NewKeybindsRow
key={i}
id={typeof key.keybindNameId === 'string' ? key.keybindNameId : ''}
keybind={
(typeof key.keybindValue === 'string'
? key.keybindValue
: ''
).split('+') || ''
}
delay={key.keybindDelay}
id={typeof field.name === 'string' ? field.name : ''}
control={control}
index={index}
getValue={getValues}
openKeybindRecorderModal={handleOpenRecorderModal}
/>
</div>
);
@@ -107,7 +122,7 @@ export function NewKeybindSettings() {
useEffect(() => {
sendRPCPacket(RpcMessage.KeybindRequest, new KeybindRequestT());
}, [isOpen]);
}, []);
return (
<SettingsPageLayout>
@@ -122,33 +137,40 @@ export function NewKeybindSettings() {
))}
</div>
<div className="keybind-settings">
<Typography id="keybind_config-keybind_name" />
<Typography id="keybind_config-keybind_value" />
<Typography id="keybind_config-keybind_delay" />
<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()}
<Button
id="settings-keybinds_reset-all-button"
className="justify-self-start"
onClick={handleOnClick}
onClick={() => reset(defaultKeybindsState)}
variant="primary"
/>
</div>
{selectedKeybind && (
{selectedIndex != null && (
<KeybindRecorderModal
id={
typeof selectedKeybind.keybindNameId === 'string'
? selectedKeybind.keybindNameId
: ''
}
id={fields[selectedIndex].name}
control={control}
resetField={resetField}
name="binding"
delay={selectedKeybind.keybindDelay.toString()}
name={`keybinds.${selectedIndex}.binding`}
isVisisble={isOpen}
onClose={() => {
console.log('onclose');
setIsOpen(false);
setSelectedKeybind(null);
setSelectedIndex(null);
handleSubmit(onSubmit)()
}}
onUnbind={() => {
setValue(`keybinds.${selectedIndex}.binding`, []);
}}
/>
)}

View File

@@ -41,13 +41,6 @@ class KeybindingsConfig {
"CTRL+ALT+SHIFT+O",
0f
)
keybinds[KeybindId.TEST] =
KeybindData(
KeybindId.TEST,
"test",
"A+B+C+D",
2f
)
}
}

View File

@@ -1,21 +1,19 @@
package dev.slimevr.keybind
import dev.slimevr.VRServer
import dev.slimevr.config.KeybindingsConfig
import solarxr_protocol.rpc.KeybindT
import java.util.concurrent.CopyOnWriteArrayList
class KeybindHandler(val vrServer: VRServer) {
private val listeners: MutableList<KeybindListener> = CopyOnWriteArrayList()
var keybinds: MutableList<KeybindT> = mutableListOf()
var defaultKeybinds: MutableList<KeybindT> = mutableListOf()
init {
createKeybinds()
}
fun sendKeybinds(KeybindName: String) {
this.listeners.forEach { it.sendKeybind() }
}
fun addListener(listener: KeybindListener) {
this.listeners.add(listener)
}
@@ -26,6 +24,7 @@ class KeybindHandler(val vrServer: VRServer) {
private fun createKeybinds() {
keybinds.clear()
defaultKeybinds.clear()
vrServer.configManager.vrConfig.keybindings.keybinds.forEach { (i, keybind) ->
keybinds.add(
KeybindT().apply {
@@ -36,9 +35,20 @@ class KeybindHandler(val vrServer: VRServer) {
},
)
}
val binds = KeybindingsConfig().keybinds
binds.forEach { (i, keybind) ->
defaultKeybinds.add(
KeybindT().apply {
keybindId = keybind.id
keybindNameId = keybind.name
keybindValue = keybind.binding
keybindDelay = keybind.delay
},
)
}
}
// TODO: Maybe recreating all the keybinds isn't the best idea?
fun updateKeybinds() {
createKeybinds()
}

View File

@@ -35,6 +35,7 @@ class RPCKeybindHandler(
fbb,
KeybindResponseT().apply {
keybind = api.server.keybindHandler.keybinds.toTypedArray()
defaultKeybinds = api.server.keybindHandler.defaultKeybinds.toTypedArray()
}
)

View File

@@ -85,11 +85,6 @@ keybindings:
name: "pause-tracking"
binding: "CTRL+ALT+SHIFT+O"
delay: 0.0
"5":
id: 5
name: "test"
binding: "A+B+C+D"
delay: 2.0
skeleton:
toggles: {}
values: {}