mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Improve GUI consistency/UX (#313)
This commit is contained in:
@@ -34,8 +34,12 @@ import { ManualProportionsPage } from './components/onboarding/pages/body-propor
|
||||
import { TrackerSettingsPage } from './components/tracker/TrackerSettings';
|
||||
import { DonePage } from './components/onboarding/pages/Done';
|
||||
import { OSCSettings } from './components/settings/pages/OSCSettings';
|
||||
import { useConfig } from './hooks/config';
|
||||
|
||||
function Layout() {
|
||||
const { loading } = useConfig();
|
||||
if (loading) return (<></>);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Routes>
|
||||
@@ -154,7 +158,7 @@ function App() {
|
||||
<>
|
||||
<TopBar></TopBar>
|
||||
<div className="flex w-full h-full justify-center items-center p-2">
|
||||
{websocketAPI.isFistConnection
|
||||
{websocketAPI.isFirstConnection
|
||||
? 'Connecting to the server'
|
||||
: 'Connection lost to the server. Trying to reconnect...'}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,8 @@ export function CheckBox({
|
||||
control,
|
||||
outlined,
|
||||
name,
|
||||
// input props
|
||||
disabled,
|
||||
...props
|
||||
}: {
|
||||
label: string | ReactChild;
|
||||
@@ -15,7 +17,7 @@ export function CheckBox({
|
||||
name: string;
|
||||
variant?: 'checkbox' | 'toggle';
|
||||
outlined?: boolean;
|
||||
}) {
|
||||
} & React.HTMLProps<HTMLInputElement>) {
|
||||
const classes = useMemo(() => {
|
||||
const vriantsMap = {
|
||||
checkbox: {
|
||||
@@ -51,7 +53,10 @@ export function CheckBox({
|
||||
<label
|
||||
className={classNames(
|
||||
'w-full py-3 flex gap-2 items-center text-standard-bold',
|
||||
{ 'px-3': outlined }
|
||||
{
|
||||
'px-3': outlined,
|
||||
'cursor-pointer': !disabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -8,13 +8,16 @@ export function Radio({
|
||||
label,
|
||||
value,
|
||||
desciption,
|
||||
// input props
|
||||
disabled,
|
||||
...props
|
||||
}: {
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
label: string;
|
||||
value: string | number;
|
||||
desciption?: string;
|
||||
}) {
|
||||
} & React.HTMLProps<HTMLInputElement>) {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -26,6 +29,7 @@ export function Radio({
|
||||
{
|
||||
'border-accent-background-30': value == checked,
|
||||
'border-transparent': value != checked,
|
||||
'cursor-pointer': !disabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
@@ -37,6 +41,7 @@ export function Radio({
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
checked={value == checked}
|
||||
{...props}
|
||||
></input>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography bold>{label}</Typography>
|
||||
|
||||
@@ -9,17 +9,9 @@ import { TrackersTable } from '../tracker/TrackersTable';
|
||||
|
||||
export function Home() {
|
||||
const { config } = useConfig();
|
||||
const { useAssignedTrackers, useUnassignedTrackers } = useTrackers();
|
||||
const { trackers } = useTrackers();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const assignedTrackers = useAssignedTrackers();
|
||||
const unasignedTrackers = useUnassignedTrackers();
|
||||
|
||||
const trackers = useMemo(
|
||||
() => [...assignedTrackers, ...unasignedTrackers],
|
||||
[assignedTrackers, unasignedTrackers]
|
||||
);
|
||||
|
||||
const sendToSettings = (tracker: TrackerDataT) => {
|
||||
navigate(
|
||||
`/tracker/${tracker.trackerId?.trackerNum}/${tracker.trackerId?.deviceId?.id}`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { ResetRequestT, ResetType, RpcMessage } from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from '../../hooks/websocket-api';
|
||||
import { BigButton } from '../commons/BigButton';
|
||||
@@ -9,9 +9,8 @@ import {
|
||||
} from '../commons/icon/ResetIcon';
|
||||
|
||||
export function ResetButton({ type }: { type: ResetType }) {
|
||||
const timerid = useRef<NodeJS.Timer | null>(null);
|
||||
const [reseting, setReseting] = useState(false);
|
||||
const [timer, setTimer] = useState(0);
|
||||
const [resetting, setResetting] = useState(false);
|
||||
const [timer, setDisplayTimer] = useState(0);
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const getText = () => {
|
||||
@@ -34,46 +33,33 @@ export function ResetButton({ type }: { type: ResetType }) {
|
||||
return <ResetIcon width={20} />;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
const TIMER_DURATION = 3;
|
||||
const resetStart = () => {
|
||||
setResetting(true);
|
||||
setDisplayTimer(TIMER_DURATION);
|
||||
if (type !== ResetType.Quick) {
|
||||
for (let i = 1; i < TIMER_DURATION; i++) {
|
||||
setTimeout(() => setDisplayTimer(TIMER_DURATION - i), i * 1000);
|
||||
}
|
||||
setTimeout(resetEnd, TIMER_DURATION * 1000);
|
||||
} else {
|
||||
resetEnd();
|
||||
}
|
||||
};
|
||||
|
||||
const resetEnd = () => {
|
||||
const req = new ResetRequestT();
|
||||
req.resetType = type;
|
||||
setReseting(true);
|
||||
setTimer(0);
|
||||
if (type !== ResetType.Quick) {
|
||||
if (timerid.current) clearInterval(timerid.current);
|
||||
timerid.current = setInterval(() => {
|
||||
setTimer((timer) => {
|
||||
const newTimer = timer + 1;
|
||||
if (newTimer >= 3) {
|
||||
// Stop the current interval
|
||||
if (timerid.current) clearInterval(timerid.current);
|
||||
|
||||
// Only actually reset on exactly 0 so it doesn't repeatedly reset if bugged
|
||||
if (newTimer === 3) sendRPCPacket(RpcMessage.ResetRequest, req);
|
||||
else
|
||||
console.warn(
|
||||
`Reset timer is still running after 3 seconds (newTimer = ${newTimer})`
|
||||
);
|
||||
|
||||
// Reset the state
|
||||
// Don't reset the timer in-case the interval keeps running
|
||||
setReseting(false);
|
||||
}
|
||||
return newTimer;
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
sendRPCPacket(RpcMessage.ResetRequest, req);
|
||||
setReseting(false);
|
||||
}
|
||||
sendRPCPacket(RpcMessage.ResetRequest, req);
|
||||
setResetting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<BigButton
|
||||
text={!reseting || timer >= 3 ? getText() : `${3 - timer}`}
|
||||
text={!resetting ? getText() : String(timer)}
|
||||
icon={getIcon()}
|
||||
onClick={reset}
|
||||
disabled={reseting}
|
||||
onClick={resetStart}
|
||||
disabled={resetting}
|
||||
></BigButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ export function GeneralSettings() {
|
||||
<>
|
||||
<Typography variant="main-title">Interface</Typography>
|
||||
<Typography bold>Developer Mode</Typography>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col pt-2">
|
||||
<Typography color="secondary">
|
||||
This mode can be useful if you need in-depth data or to interact
|
||||
</Typography>
|
||||
|
||||
@@ -132,7 +132,7 @@ export function TrackerCard({
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'rounded-lg',
|
||||
interactable && 'hover:bg-background-50',
|
||||
interactable && 'hover:bg-background-50 cursor-pointer',
|
||||
outlined && 'outline outline-2 outline-accent-background-40',
|
||||
bg
|
||||
)}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Typography } from '../commons/Typography';
|
||||
import { MountingSelectionMenu } from '../onboarding/pages/mounting/MountingSelectionMenu';
|
||||
import { SingleTrackerBodyAssignmentMenu } from './SingleTrackerBodyAssignmentMenu';
|
||||
import { TrackerCard } from './TrackerCard';
|
||||
import { bodypartToString } from '../utils/formatting';
|
||||
|
||||
const rotationToQuatMap = {
|
||||
FRONT: 180,
|
||||
@@ -29,10 +30,10 @@ const rotationToQuatMap = {
|
||||
};
|
||||
|
||||
const rotationsLabels = {
|
||||
[rotationToQuatMap.BACK]: 'Back',
|
||||
[rotationToQuatMap.FRONT]: 'Front',
|
||||
[rotationToQuatMap.LEFT]: 'Left',
|
||||
[rotationToQuatMap.RIGHT]: 'Right',
|
||||
[rotationToQuatMap.BACK]: 'BACK',
|
||||
[rotationToQuatMap.FRONT]: 'FRONT',
|
||||
[rotationToQuatMap.LEFT]: 'LEFT',
|
||||
[rotationToQuatMap.RIGHT]: 'RIGHT',
|
||||
};
|
||||
|
||||
export function TrackerSettingsPage() {
|
||||
@@ -203,7 +204,7 @@ export function TrackerSettingsPage() {
|
||||
<div className="flex gap-3 items-center">
|
||||
<FootIcon></FootIcon>
|
||||
<Typography>
|
||||
{BodyPart[tracker?.tracker.info?.bodyPart || BodyPart.NONE]}
|
||||
{bodypartToString(tracker?.tracker.info?.bodyPart || BodyPart.NONE)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex">
|
||||
|
||||
4
gui/src/components/utils/formatting.ts
Normal file
4
gui/src/components/utils/formatting.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { BodyPart } from "solarxr-protocol";
|
||||
|
||||
export const bodypartToString = (id: BodyPart) =>
|
||||
BodyPart[id].replace(/_/g, ' ');
|
||||
@@ -3,9 +3,10 @@ import {
|
||||
readTextFile,
|
||||
writeFile,
|
||||
createDir,
|
||||
renameFile,
|
||||
} from '@tauri-apps/api/fs';
|
||||
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import { createContext, useContext, useRef, useState } from 'react';
|
||||
|
||||
export interface WindowConfig {
|
||||
width: number;
|
||||
@@ -30,6 +31,7 @@ export interface ConfigContext {
|
||||
const initialConfig = { doneOnboarding: false };
|
||||
|
||||
export function useConfigProvider(): ConfigContext {
|
||||
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
const [currConfig, set] = useState<Config | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -42,11 +44,20 @@ export function useConfigProvider(): ConfigContext {
|
||||
: null;
|
||||
set(newConfig as Config);
|
||||
|
||||
await createDir('', { dir: BaseDirectory.App, recursive: true });
|
||||
await writeFile(
|
||||
{ contents: JSON.stringify(newConfig), path: 'config.json' },
|
||||
{ dir: BaseDirectory.App }
|
||||
);
|
||||
if (!debounceTimer.current) {
|
||||
debounceTimer.current = setTimeout(async () => {
|
||||
await createDir('', { dir: BaseDirectory.App, recursive: true });
|
||||
await writeFile(
|
||||
{ contents: JSON.stringify(newConfig), path: 'config.json.tmp' },
|
||||
{ dir: BaseDirectory.App }
|
||||
);
|
||||
await renameFile(
|
||||
'config.json.tmp', 'config.json',
|
||||
{ dir: BaseDirectory.App }
|
||||
);
|
||||
debounceTimer.current = null;
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BodyPart, TrackerDataT, TrackerStatus } from 'solarxr-protocol';
|
||||
import { bodypartToString } from '../components/utils/formatting';
|
||||
import { QuaternionFromQuatT } from '../maths/quaternion';
|
||||
import { useAppContext } from './app';
|
||||
|
||||
@@ -40,7 +41,7 @@ export function useTracker(tracker: TrackerDataT) {
|
||||
useName: () =>
|
||||
useMemo(() => {
|
||||
if (tracker.info?.customName) return tracker.info?.customName;
|
||||
if (tracker.info?.bodyPart) return BodyPart[tracker.info?.bodyPart];
|
||||
if (tracker.info?.bodyPart) return bodypartToString(tracker.info?.bodyPart);
|
||||
return tracker.info?.displayName || 'NONE';
|
||||
}, [tracker.info]),
|
||||
useRotation: () =>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useInterval } from './timeout';
|
||||
|
||||
export interface WebSocketApi {
|
||||
isConnected: boolean;
|
||||
isFistConnection: boolean;
|
||||
isFirstConnection: boolean;
|
||||
useRPCPacket: <T>(type: RpcMessage, callback: (packet: T) => void) => void;
|
||||
useDataFeedPacket: <T>(
|
||||
type: DataFeedMessage,
|
||||
@@ -46,7 +46,7 @@ export function useProvideWebsocketApi(): WebSocketApi {
|
||||
const rpclistenerRef = useRef<EventTarget>(new EventTarget());
|
||||
const pubsublistenerRef = useRef<EventTarget>(new EventTarget());
|
||||
const datafeedlistenerRef = useRef<EventTarget>(new EventTarget());
|
||||
const [isFistConnection, setFirstConnection] = useState(true);
|
||||
const [isFirstConnection, setFirstConnection] = useState(true);
|
||||
const [isConnected, setConnected] = useState(false);
|
||||
|
||||
useInterval(() => {
|
||||
@@ -184,7 +184,7 @@ export function useProvideWebsocketApi(): WebSocketApi {
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
isFistConnection,
|
||||
isFirstConnection,
|
||||
useDataFeedPacket: <T>(
|
||||
type: DataFeedMessage,
|
||||
callback: (packet: T) => void
|
||||
|
||||
Reference in New Issue
Block a user