Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions gui/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,29 @@ settings-stay_aligned-debug-label = Debugging
settings-stay_aligned-debug-description = Please include your settings when reporting problems about Stay Aligned.
settings-stay_aligned-debug-copy-label = Copy settings to clipboard

settings-keybinds = Keybind settings
settings-keybinds_ = ''
settings-keybinds-description = Change keybinds for various shortcuts
keybind_config-keybind_name = Keybind
keybind_config-keybind_value = Combination
keybind_config-keybind_delay = Delay before trigger (s)
settings-keybinds_full-reset = Full Reset
settings-keybinds_yaw-reset = Yaw Reset
settings-keybinds_mounting-reset = Mounting Reset
settings-keybinds_feet-mounting-reset = Feet Mounting Reset
settings-keybinds_pause-tracking = Pause Tracking
settings-keybinds_record-keybind = Click to record
settings-keybinds_now-recording = Recording…
settings-keybinds_reset-button = Reset
settings-keybinds_reset-all-button = Reset all
settings-keybinds-wayland-description = You appear to be using wayland, Please change your shortcuts in your system settings.
settings-keybinds-wayland-open-system-settings-button = Open system settings
settings-sidebar-keybinds = Keybinds
settings-keybinds-recorder-modal-title = Assign keybind for
settings-keybinds-recorder-modal-reset-button = Reset
settings-keybinds-recorder-modal-unbind-button = Unbind
settings-keybinds-recorder-modal-done-button = Done

## FK/Tracking settings
settings-general-fk_settings = Tracking settings

Expand Down
2 changes: 2 additions & 0 deletions gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { QuizSlimeSetQuestion } from './components/onboarding/pages/quiz/SlimeSe
import { QuizUsageQuestion } from './components/onboarding/pages/quiz/UsageQuestion';
import { QuizRuntimeQuestion } from './components/onboarding/pages/quiz/RuntimeQuestion';
import { QuizMocapPosQuestion } from './components/onboarding/pages/quiz/MocapPreferencesQuestions';
import { KeybindSettings } from './components/settings/pages/KeybindSettings';
import { ElectronContextC, provideElectron } from './hooks/electron';
import { AppLocalizationProvider } from './i18n/config';
import { openUrl } from './hooks/crossplatform';
Expand Down Expand Up @@ -145,6 +146,7 @@ function Layout() {
<Route path="interface" element={<InterfaceSettings />} />
<Route path="interface/home" element={<HomeScreenSettings />} />
<Route path="advanced" element={<AdvancedSettings />} />
<Route path="keybinds" element={<KeybindSettings />} />
</Route>
<Route
path="/onboarding"
Expand Down
119 changes: 119 additions & 0 deletions gui/src/components/commons/KeybindRecorder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
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;

export const KeybindRecorder = forwardRef<
HTMLInputElement,
{
keys: string[];
onKeysChange: (v: string[]) => void;
error?: string;
}
>(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 [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();
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);
setTimeout(() => {
setInvalidSlot(null);
}, 350);
return;
}

if (displayKeys.length < maxKeybindLength) {
const updatedKeys = [...displayKeys, key];
setLocalKeys(updatedKeys);
onKeysChange(updatedKeys);
if (updatedKeys.length == maxKeybindLength) {
inputRef.current?.blur();
}
}
};

const handleOnBlur = () => {
setIsRecording(false);
if (displayKeys.length < maxKeybindLength - 2 || error) {
onKeysChange(oldKeys);
setLocalKeys(oldKeys);
}
};

const handleOnFocus = () => {
clearErrors('keybinds');
const initialKeys: string[] = [];
setOldKeys(keys);
setLocalKeys(initialKeys);
onKeysChange(initialKeys);
setIsRecording(true);
};

return (
<div className="w-full justify-center items-center flex flex-col gap-2">
<div className="flex gap-2 p-2 items-center rounded-lg relative">
<input
autoFocus
ref={inputRef}
className="opacity-0 absolute cursor-pointer w-full"
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onKeyDown={handleKeyDown}
/>
<div className="flex flex-grow gap-2 justify-center h-full">
{Array.from({ length: maxKeybindLength }).map((_, i) => {
const key = displayKeys[i];
const isActive = isRecording && i === activeIndex;
const isInvalid = invalidSlot === i;
return (
<div key={i} className="flex flex-row">
<div
className={classNames(
'flex p-2 rounded-lg min-w-[50px] min-h-[50px] text-main-title justify-center items-center bg-background-80 mobile:text-sm',
{
'keyslot-invalid ring-2 ring-status-critical': isInvalid,
'keyslot-animate ring-2 ring-accent':
isActive && !isInvalid,
'ring-accent': !isInvalid && !isInvalid,
}
)}
>
{key ?? ''}
</div>
<div className="flex pl-2 text-main-title justify-center items-center mobile:text-sm">
{i < maxKeybindLength - 1 ? '+' : ''}
</div>
</div>
);
})}
</div>
</div>
{displayError && (
<div className="isInvalid keyslot-invalid">
<Typography color="text-status-critical">{`${errorText} ${error}`}</Typography>
</div>
)}
</div>
);
});
93 changes: 93 additions & 0 deletions gui/src/components/commons/KeybindRecorderModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { BaseModal } from './BaseModal';
import {
Controller,
Control,
useFormContext,
FieldValues,
FieldPath,
} 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<T extends FieldValues = FieldValues>({
id,
control,
name,
isVisisble,
onClose,
onUnbind,
onSubmit,
}: {
id?: string;
control: Control<T>;
name: FieldPath<T>;
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}
appendClasses="w-full max-w-xl"
>
<div className="flex flex-col gap-3 w-full justify-between h-full">
<Typography variant="section-title">
{l10n.getString('settings-keybinds-recorder-modal-title')}{' '}
{l10n.getString(keybindlocalization)}
</Typography>
<Controller
control={control}
name={name}
render={({ field }) => (
<KeybindRecorder
keys={field.value ?? []}
onKeysChange={field.onChange}
ref={field.ref}
error={errors.keybinds?.message as string}
/>
)}
/>
<div className="flex flex-row justify-between w-full">
<div className="flex flex-row justify-start gap-4">
<Button
id="settings-keybinds-recorder-modal-reset-button"
variant="tertiary"
onClick={() => {
resetField(name);
handleSubmit(onSubmit)();
}}
/>
<Button
id="settings-keybinds-recorder-modal-unbind-button"
variant="tertiary"
onClick={() => {
onUnbind();
handleSubmit(onSubmit)();
}}
/>
</div>
<div className="flex flex-row justify-end">
<Button
id="settings-keybinds-recorder-modal-done-button"
variant="primary"
onClick={handleSubmit(onSubmit)}
/>
</div>
</div>
</div>
</BaseModal>
);
}
65 changes: 65 additions & 0 deletions gui/src/components/commons/KeybindRow.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.keybind-row {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
height: auto;
align-items: center;
gap: 10px;
}

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