From 9c8cd8517e67f604ebbb5099f29cbbf3e86cb7ad Mon Sep 17 00:00:00 2001 From: HannahPadd Date: Wed, 25 Mar 2026 23:18:37 +0100 Subject: [PATCH] Update to form and ui. --- gui/package.json | 1 + gui/public/i18n/en/translation.ftl | 17 +- .../components/commons/KeybindRecorder.tsx | 48 ++- gui/src/components/commons/KeybindRow.scss | 16 + gui/src/components/commons/KeybindRow.tsx | 80 ++-- .../components/onboarding/UdevRulesModal.tsx | 21 +- .../settings/pages/KeybindSettings.scss | 22 ++ .../settings/pages/KeybindSettings.tsx | 342 +++++++++--------- .../pages/components/StayAlignedPoseModal.tsx | 4 +- gui/src/hooks/app.ts | 25 +- package.json | 2 +- server/core/build.gradle.kts | 2 +- .../src/main/java/dev/slimevr/Keybinding.kt | 2 +- .../dev/slimevr/protocol/rpc/RPCHandler.kt | 3 + .../rpc/installinfo/RPCInstallInfoHandler.kt | 5 +- .../rpc/keybinds/RPCKeybindHandler.kt | 2 + .../protocol/rpc/openuri/RPCOpenUriHandler.kt | 21 ++ server/desktop/build.gradle.kts | 3 +- 18 files changed, 354 insertions(+), 262 deletions(-) create mode 100644 gui/src/components/commons/KeybindRow.scss create mode 100644 gui/src/components/settings/pages/KeybindSettings.scss create mode 100644 server/core/src/main/java/dev/slimevr/protocol/rpc/openuri/RPCOpenUriHandler.kt diff --git a/gui/package.json b/gui/package.json index 1657a509c..58a1e4d6e 100644 --- a/gui/package.json +++ b/gui/package.json @@ -17,6 +17,7 @@ }, "scripts": { "start": "vite --force", + "dev:clean": "rm -rf node_modules/.vite && pnpm run gui", "gui": "electron-vite dev --config electron.vite.config.ts --watch", "build": "electron-vite build --config electron.vite.config.ts", "package": "electron-builder", diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index a16b62035..156507c32 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -605,9 +605,22 @@ settings-stay_aligned-debug-copy-label = Copy settings to clipboard settings-keybinds = Keybind settings 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-sidebar_keybinds_record-keybind = Click to record -settings-sidebar_keybinds_now-recording = Recording… + ## FK/Tracking settings settings-general-fk_settings = Tracking settings diff --git a/gui/src/components/commons/KeybindRecorder.tsx b/gui/src/components/commons/KeybindRecorder.tsx index c047f224d..dd5ef1b58 100644 --- a/gui/src/components/commons/KeybindRecorder.tsx +++ b/gui/src/components/commons/KeybindRecorder.tsx @@ -14,12 +14,24 @@ export const KeybindRecorder = forwardRef< return (
+ { setOldKeys(keys); - onKeysChange([]); + onKeysChange(['CTRL', 'ALT']); setIsRecording(true); }} onBlur={() => { @@ -33,21 +45,29 @@ export const KeybindRecorder = forwardRef< } }} /> -
-
- {keys.map((key, i) => ( -
- {key} -
- ))} +
+ {Array.from({ length: 4 }).map((_, i) => { + const key = keys[i]; + const isActive = isRecording && i === keys.length; + return ( +
+ {key ?? ''} +
+ ); + })}
- -
- {keys.length < 4 && isRecording ? l10n.getString('settings-sidebar_keybinds_now-recording') : l10n.getString('settings-sidebar_keybinds_record-keybind') } +
+ {keys.length < 4 && isRecording + ? l10n.getString('settings-keybinds_now-recording') + : l10n.getString('settings-keybinds_record-keybind')}
diff --git a/gui/src/components/commons/KeybindRow.scss b/gui/src/components/commons/KeybindRow.scss new file mode 100644 index 000000000..2f81f72dd --- /dev/null +++ b/gui/src/components/commons/KeybindRow.scss @@ -0,0 +1,16 @@ +:root { + --row-h: 80px; +} + +.keybind-row { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + height: var(--row-h); + align-items: center; + gap: 20px; +} + + .number-selector::after { + gap: 0; + } diff --git a/gui/src/components/commons/KeybindRow.tsx b/gui/src/components/commons/KeybindRow.tsx index 5a094a457..345cab8e0 100644 --- a/gui/src/components/commons/KeybindRow.tsx +++ b/gui/src/components/commons/KeybindRow.tsx @@ -3,19 +3,22 @@ import { Button } from './Button'; import { NumberSelector } from './NumberSelector'; import { KeybindRecorder } from './KeybindRecorder'; import { useLocaleConfig } from '@/i18n/config'; +import { Typography } from './Typography'; +import './KeybindRow.scss'; export function KeybindRow({ - label, + id, control, resetField, - bindingName, - delayName, + name, + delay, }: { - label: string; + id?: string; + label?: string; control: Control; resetField: UseFormResetField; - bindingName: string; - delayName: string; + name: string; + delay: string; }) { const { currentLocales } = useLocaleConfig(); const secondsFormat = new Intl.NumberFormat(currentLocales, { @@ -24,50 +27,41 @@ export function KeybindRow({ unitDisplay: 'narrow', maximumFractionDigits: 2, }); + return ( - - - - - - ( - - )} - /> - - +
+ + ( + + )} + /> secondsFormat.format(value)} min={0} max={10} step={0.2} /> - - - -
- -
- - +
+
+
); } diff --git a/gui/src/components/onboarding/UdevRulesModal.tsx b/gui/src/components/onboarding/UdevRulesModal.tsx index b89af1e73..634acb4d7 100644 --- a/gui/src/components/onboarding/UdevRulesModal.tsx +++ b/gui/src/components/onboarding/UdevRulesModal.tsx @@ -8,17 +8,17 @@ import { useWebsocketAPI } from '@/hooks/websocket-api'; import { RpcMessage, InstalledInfoResponseT } from 'solarxr-protocol'; import { useConfig } from '@/hooks/config'; import { useLocalization } from '@fluent/react'; +import { useAppContext } from '@/hooks/app'; export function UdevRulesModal() { const { config, setConfig } = useConfig(); - const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); const electron = useElectron(); const [udevContent, setUdevContent] = useState(''); - const [isUdevInstalledResponse, setIsUdevInstalledResponse] = useState(true); const [showUdevWarning, setShowUdevWarning] = useState(false); const [dontShowThisSession, setDontShowThisSession] = useState(false); const [dontShowAgain, setDontShowAgain] = useState(false); const { l10n } = useLocalization(); + const { installInfo } = useAppContext(); const handleUdevContent = async () => { if (electron.isElectron) { @@ -38,28 +38,15 @@ export function UdevRulesModal() { if (!config) throw 'Invalid state!'; if (electron.isElectron) { const isLinux = electron.data().os.type === 'linux'; - const udevMissing = !isUdevInstalledResponse; + const udevMissing = !installInfo?.isUdevInstalled; const notHiddenGlobally = !config.dontShowUdevModal; const notHiddenThisSession = !dontShowThisSession; const shouldShow = isLinux && udevMissing && notHiddenGlobally && notHiddenThisSession; setShowUdevWarning(shouldShow); } - }, [config, isUdevInstalledResponse, dontShowThisSession]); + }, [config, dontShowThisSession]); - useEffect(() => { - sendRPCPacket( - RpcMessage.InstalledInfoRequest, - new InstalledInfoResponseT() - ); - }, []); - - useRPCPacket( - RpcMessage.InstalledInfoResponse, - ({ isUdevInstalled }: InstalledInfoResponseT) => { - setIsUdevInstalledResponse(isUdevInstalled); - } - ); const handleModalClose = () => { if (!config) throw 'Invalid State!'; diff --git a/gui/src/components/settings/pages/KeybindSettings.scss b/gui/src/components/settings/pages/KeybindSettings.scss new file mode 100644 index 000000000..442e48500 --- /dev/null +++ b/gui/src/components/settings/pages/KeybindSettings.scss @@ -0,0 +1,22 @@ +:root { + --keybindrow-w: 1fr; + --top-row-h: 10px; + --keybindrow-h: 10px; + --buttonrow-h: 10px; +} + +.keybind-settings { + display: grid; + + grid-template: + 'n v d e' auto + 'k k k k' auto + 'b . . .' auto + / 200px 1fr 120px auto; + + grid-template-columns: subgrid; + grid-template-rows: subgrid; + + place-items: center; + +} diff --git a/gui/src/components/settings/pages/KeybindSettings.tsx b/gui/src/components/settings/pages/KeybindSettings.tsx index 6bc33d7a4..6a514018e 100644 --- a/gui/src/components/settings/pages/KeybindSettings.tsx +++ b/gui/src/components/settings/pages/KeybindSettings.tsx @@ -4,8 +4,9 @@ import { SettingsPagePaneLayout, } from '@/components/settings/SettingsPageLayout'; import { Typography } from '@/components/commons/Typography'; -import { Localized, useLocalization } from '@fluent/react'; -import { useForm } from 'react-hook-form'; +import { useLocalization } from '@fluent/react'; +import { DefaultValues, useForm } from 'react-hook-form'; +import './KeybindSettings.scss'; import { ReactNode, useEffect } from 'react'; import { KeybindRow } from '@/components/commons/KeybindRow'; import { Button } from '@/components/commons/Button'; @@ -17,7 +18,30 @@ import { KeybindT, KeybindName, ChangeKeybindRequestT, + OpenUriRequestT, } from 'solarxr-protocol'; +import { useAppContext } from '@/hooks/app'; + +function Table({ children }: { children: ReactNode }) { + return ( + + + + + + + + + {children} +
+ + + + + +
+ ); +} export type KeybindsForm = { names: { @@ -67,39 +91,17 @@ const defaultValues: KeybindsForm = { }, }; -export function useKeybindsForm() { - const { - register, - reset, - handleSubmit, - formState, - control, - getValues, - resetField, - watch, - } = useForm({ - defaultValues, - }); - - return { - control, - register, - reset, - handleSubmit, - formState, - getValues, - resetField, - watch, - }; -} - export function KeybindSettings() { const { l10n } = useLocalization(); - const { control, reset, handleSubmit, watch, getValues, resetField } = - useKeybindsForm(); + const { control, reset, handleSubmit, watch, resetField, getValues } = + useForm({ + defaultValues, + }); const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + const { installInfo } = useAppContext(); const onSubmit = (values: KeybindsForm) => { + console.log('Onsubmit in KeybindSettings'); const keybinds = new ChangeKeybindRequestT(); const fullResetKeybind = new KeybindT(); @@ -134,7 +136,6 @@ export function KeybindSettings() { feetResetKeybind.keybindDelay = values.delays.pauseTrackingDelay; keybinds.keybind.push(feetResetKeybind); - console.log(`Delay ${Number(fullResetKeybind.keybindDelay)}`); sendRPCPacket(RpcMessage.ChangeKeybindRequest, keybinds); }; @@ -150,162 +151,151 @@ export function KeybindSettings() { useRPCPacket(RpcMessage.KeybindResponse, ({ keybind }: KeybindResponseT) => { if (!keybind) return; - const keybindValues: KeybindsForm = { - names: { - fullResetName: KeybindName.FULL_RESET, - yawResetName: KeybindName.YAW_RESET, - mountingResetName: KeybindName.MOUNTING_RESET, - pauseTrackingName: KeybindName.PAUSE_TRACKING, - feetResetName: KeybindName.FEET_MOUNTING_RESET, + reset( + { + names: { + fullResetName: KeybindName.FULL_RESET, + yawResetName: KeybindName.YAW_RESET, + mountingResetName: KeybindName.MOUNTING_RESET, + pauseTrackingName: KeybindName.PAUSE_TRACKING, + feetResetName: KeybindName.FEET_MOUNTING_RESET, + }, + bindings: { + fullResetBinding: + (typeof keybind[KeybindName.FULL_RESET].keybindValue === 'string' + ? keybind[KeybindName.FULL_RESET].keybindValue + : '' + ).split('+') || defaultValues.bindings.fullResetBinding, + yawResetBinding: + (typeof keybind[KeybindName.YAW_RESET].keybindValue === 'string' + ? keybind[KeybindName.YAW_RESET].keybindValue + : '' + ).split('+') || defaultValues.bindings.yawResetBinding, + mountingResetBinding: + (typeof keybind[KeybindName.MOUNTING_RESET].keybindValue === + 'string' + ? keybind[KeybindName.MOUNTING_RESET].keybindValue + : '' + ).split('+') || defaultValues.bindings.mountingResetBinding, + pauseTrackingBinding: + (typeof keybind[KeybindName.PAUSE_TRACKING].keybindValue === + 'string' + ? keybind[KeybindName.PAUSE_TRACKING].keybindValue + : '' + ).split('+') || defaultValues.bindings.pauseTrackingBinding, + feetResetBinding: + (typeof keybind[KeybindName.FEET_MOUNTING_RESET].keybindValue === + 'string' + ? keybind[KeybindName.FEET_MOUNTING_RESET].keybindValue + : '' + ).split('+') || defaultValues.bindings.feetResetBinding, + }, + delays: { + fullResetDelay: + keybind[KeybindName.FULL_RESET].keybindDelay || + defaultValues.delays.fullResetDelay, + yawResetDelay: + keybind[KeybindName.YAW_RESET].keybindDelay || + defaultValues.delays.yawResetDelay, + mountingResetDelay: + keybind[KeybindName.MOUNTING_RESET].keybindDelay || + defaultValues.delays.mountingResetDelay, + pauseTrackingDelay: + keybind[KeybindName.PAUSE_TRACKING].keybindDelay || + defaultValues.delays.pauseTrackingDelay, + feetResetDelay: + keybind[KeybindName.FEET_MOUNTING_RESET].keybindDelay || + defaultValues.delays.feetResetDelay, + }, }, - bindings: { - fullResetBinding: - (typeof keybind[KeybindName.FULL_RESET].keybindValue === 'string' - ? keybind[KeybindName.FULL_RESET].keybindValue - : '' - ).split('+') || defaultValues.bindings.fullResetBinding, - - yawResetBinding: - (typeof keybind[KeybindName.YAW_RESET].keybindValue === 'string' - ? keybind[KeybindName.YAW_RESET].keybindValue - : '' - ).split('+') || defaultValues.bindings.yawResetBinding, - - mountingResetBinding: - (typeof keybind[KeybindName.MOUNTING_RESET].keybindValue === 'string' - ? keybind[KeybindName.MOUNTING_RESET].keybindValue - : '' - ).split('+') || defaultValues.bindings.mountingResetBinding, - - pauseTrackingBinding: - (typeof keybind[KeybindName.PAUSE_TRACKING].keybindValue === 'string' - ? keybind[KeybindName.PAUSE_TRACKING].keybindValue - : '' - ).split('+') || defaultValues.bindings.pauseTrackingBinding, - - feetResetBinding: - (typeof keybind[KeybindName.FEET_MOUNTING_RESET].keybindValue === - 'string' - ? keybind[KeybindName.FEET_MOUNTING_RESET].keybindValue - : '' - ).split('+') || defaultValues.bindings.feetResetBinding, - }, - delays: { - fullResetDelay: - keybind[KeybindName.FULL_RESET].keybindDelay || - defaultValues.delays.fullResetDelay, - yawResetDelay: - keybind[KeybindName.YAW_RESET].keybindDelay || - defaultValues.delays.yawResetDelay, - mountingResetDelay: - keybind[KeybindName.MOUNTING_RESET].keybindDelay || - defaultValues.delays.mountingResetDelay, - pauseTrackingDelay: - keybind[KeybindName.PAUSE_TRACKING].keybindDelay || - defaultValues.delays.mountingResetDelay, - feetResetDelay: - keybind[KeybindName.FEET_MOUNTING_RESET].keybindDelay || - defaultValues.delays.feetResetDelay, - }, - }; - - reset({ ...getValues(), ...keybindValues }); + { + keepDefaultValues: true, + } + ); }); - const handleResetButton = () => { - reset(defaultValues); + const handleOpenSystemSettingsButton = () => { + sendRPCPacket(RpcMessage.OpenUriRequest, new OpenUriRequestT()); }; - function Table({ children }: { children: ReactNode }) { - return ( - - - - - - - - - {children} -
- - - - Keybind - - - - - Combination - - - - - Delay before trigger (S) -
- ); - } + const handleResetAllButton = () => { + reset(defaultValues); + }; return (
} id="keybinds"> - <> - -
- {l10n - .getString('settings-keybinds-description') - .split('\n') - .map((line, i) => ( - {line} - ))} + +
+ {l10n + .getString('settings-keybinds-description') + .split('\n') + .map((line, i) => ( + {line} + ))} +
+ {!installInfo?.isWayland ? ( +
+ +
+
- - - - - - -
-
- - + ) : ( +
+ + + +
+ + + + + +
+ )} diff --git a/gui/src/components/settings/pages/components/StayAlignedPoseModal.tsx b/gui/src/components/settings/pages/components/StayAlignedPoseModal.tsx index 45bc3b9d2..1d40d442c 100644 --- a/gui/src/components/settings/pages/components/StayAlignedPoseModal.tsx +++ b/gui/src/components/settings/pages/components/StayAlignedPoseModal.tsx @@ -39,8 +39,8 @@ function StaAlignedPoseModal({ {l10n.getString(title)}
- {descriptionKeys.map((descriptionKey) => ( - {l10n.getString(descriptionKey)} + {descriptionKeys.map((descriptionKey, i) => ( + {l10n.getString(descriptionKey)} ))}
diff --git a/gui/src/hooks/app.ts b/gui/src/hooks/app.ts index 0623db19b..3418c0f2d 100644 --- a/gui/src/hooks/app.ts +++ b/gui/src/hooks/app.ts @@ -5,6 +5,7 @@ import { ResetResponseT, RpcMessage, StartDataFeedT, + InstalledInfoResponseT } from 'solarxr-protocol'; import { handleResetSounds } from '@/sounds/sounds'; import { useConfig } from './config'; @@ -18,10 +19,11 @@ import { DEFAULT_LOCALE, LangContext } from '@/i18n/config'; export interface AppContext { currentFirmwareRelease: FirmwareRelease | null; + installInfo: InstalledInfoResponseT | null; } export function useProvideAppContext(): AppContext { - const { useRPCPacket, sendDataFeedPacket, useDataFeedPacket, isConnected } = + const { useRPCPacket, sendRPCPacket, sendDataFeedPacket, useDataFeedPacket, isConnected } = useWebsocketAPI(); const { changeLocales } = useContext(LangContext); const { config } = useConfig(); @@ -34,6 +36,8 @@ export function useProvideAppContext(): AppContext { const [currentFirmwareRelease, setCurrentFirmwareRelease] = useState(null); + const [installInfo, setInstallInfo] = useState(null) + useEffect(() => { if (isConnected) { const startDataFeed = new StartDataFeedT(); @@ -50,6 +54,24 @@ export function useProvideAppContext(): AppContext { } }); + useEffect(() => { + sendRPCPacket( + RpcMessage.InstalledInfoRequest, + new InstalledInfoResponseT() + ); + }, []); + + useRPCPacket( + RpcMessage.InstalledInfoResponse, + ({ isUdevInstalled, isWayland }: InstalledInfoResponseT) => { + + setInstallInfo(new InstalledInfoResponseT( + isUdevInstalled, + isWayland + )); + } + ); + useEffect(() => { updateSentryContext(devices); }, [devices]); @@ -85,6 +107,7 @@ export function useProvideAppContext(): AppContext { return { currentFirmwareRelease, + installInfo }; } diff --git a/package.json b/package.json index 51a7f9726..65d08cee4 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "gui" ], "scripts": { - "gui": "pnpm run update-solarxr && cd gui && pnpm run gui", + "gui": "pnpm run update-solarxr && cd gui && pnpm run dev:clean", "lint:fix": "cd gui && pnpm lint:fix", "skipbundler": "cd gui && pnpm run skipbundler", "build": "cd gui && pnpm build", diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts index 648380ebe..e4ee4b15d 100644 --- a/server/core/build.gradle.kts +++ b/server/core/build.gradle.kts @@ -79,7 +79,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation("com.mayakapps.kache:kache:2.1.1") - implementation("com.github.HannahPadd:DbusGlobalShortcutsWayland:5010e75bd4") + implementation("com.github.HannahPadd:DbusGlobalShortcutsWayland:v0.1.0") api("com.github.loucass003:EspflashKotlin:v0.11.0") diff --git a/server/core/src/main/java/dev/slimevr/Keybinding.kt b/server/core/src/main/java/dev/slimevr/Keybinding.kt index b86d626aa..a01245ed7 100644 --- a/server/core/src/main/java/dev/slimevr/Keybinding.kt +++ b/server/core/src/main/java/dev/slimevr/Keybinding.kt @@ -3,7 +3,7 @@ package dev.slimevr import com.melloware.jintellitype.HotkeyListener import com.melloware.jintellitype.JIntellitype import dev.hannah.portals.PortalManager -import dev.hannah.portals.Shortcut +import dev.hannah.portals.globalShortcuts.Shortcut import dev.hannah.portals.globalShortcuts.ShortcutTuple import dev.slimevr.config.KeybindingsConfig import dev.slimevr.tracking.trackers.TrackerUtils diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt index bc7878978..17a833654 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt @@ -12,6 +12,7 @@ import dev.slimevr.protocol.rpc.firmware.RPCFirmwareUpdateHandler import dev.slimevr.protocol.rpc.games.vrchat.RPCVRChatHandler import dev.slimevr.protocol.rpc.installinfo.RPCInstallInfoHandler import dev.slimevr.protocol.rpc.keybinds.RPCKeybindHandler +import dev.slimevr.protocol.rpc.openuri.RPCOpenUriHandler import dev.slimevr.protocol.rpc.reset.RPCResetHandler import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler import dev.slimevr.protocol.rpc.serial.RPCSerialHandler @@ -54,6 +55,8 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler