mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
WIP
This commit is contained in:
@@ -80,7 +80,7 @@ function Layout() {
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<MainLayout isMobile={isMobile}>
|
||||
<MainLayout isMobile={isMobile} full>
|
||||
<Home />
|
||||
</MainLayout>
|
||||
}
|
||||
@@ -88,7 +88,7 @@ function Layout() {
|
||||
<Route
|
||||
path="/firmware-update"
|
||||
element={
|
||||
<MainLayout isMobile={isMobile} widgets={false}>
|
||||
<MainLayout isMobile={isMobile}>
|
||||
<FirmwareUpdate />
|
||||
</MainLayout>
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
's c' calc(100% - var(--topbar-h))
|
||||
/ var(--navbar-w) calc(100% - var(--navbar-w));
|
||||
|
||||
&:has(.widgets) {
|
||||
&.full {
|
||||
grid-template:
|
||||
't t t' var(--topbar-h)
|
||||
's c w' calc(100% - var(--topbar-h))
|
||||
/ var(--navbar-w) calc(100% - var(--navbar-w) - var(--widget-w)) var(--widget-w);
|
||||
's r r' var(--toolbar-h)
|
||||
's c l' calc(70% - var(--topbar-h) - var(--toolbar-h))
|
||||
's c p' calc(30%)
|
||||
/ var(--navbar-w) calc(75% - var(--navbar-w)) calc(25%);
|
||||
}
|
||||
|
||||
@screen mobile {
|
||||
|
||||
@@ -9,19 +9,21 @@ import {
|
||||
import { Navbar } from './Navbar';
|
||||
import { TopBar } from './TopBar';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { WidgetsComponent } from './WidgetsComponent';
|
||||
import './MainLayout.scss';
|
||||
import { Toolbar } from './Toolbar';
|
||||
import { WidgetsComponent } from './WidgetsComponent';
|
||||
import { SkeletonVisualizerWidget } from './widgets/SkeletonVisualizerWidget';
|
||||
|
||||
export function MainLayout({
|
||||
children,
|
||||
background = true,
|
||||
widgets = true,
|
||||
full = false,
|
||||
isMobile = undefined,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
background?: boolean;
|
||||
isMobile?: boolean;
|
||||
widgets?: boolean;
|
||||
full?: boolean;
|
||||
}) {
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
const [ProportionsLastPageOpen, setProportionsLastPageOpen] = useState(true);
|
||||
@@ -58,33 +60,50 @@ export function MainLayout({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="main-layout w-full h-screen">
|
||||
<div style={{ gridArea: 't' }}>
|
||||
<TopBar></TopBar>
|
||||
</div>
|
||||
<div style={{ gridArea: 's' }} className="overflow-y-auto">
|
||||
<Navbar></Navbar>
|
||||
</div>
|
||||
<div
|
||||
style={{ gridArea: 'c' }}
|
||||
className={classNames(
|
||||
'overflow-y-auto mr-2 my-2 mobile:m-0',
|
||||
'flex flex-col rounded-xl',
|
||||
background && 'bg-background-70'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{!isMobile && widgets && (
|
||||
<div
|
||||
style={{ gridArea: 'w' }}
|
||||
className="overflow-y-auto mr-2 my-2 rounded-xl bg-background-70 flex flex-col gap-2 p-2 widgets"
|
||||
>
|
||||
<WidgetsComponent></WidgetsComponent>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'main-layout w-full h-screen',
|
||||
!isMobile && full && 'full'
|
||||
)}
|
||||
>
|
||||
<div style={{ gridArea: 't' }}>
|
||||
<TopBar></TopBar>
|
||||
</div>
|
||||
<div style={{ gridArea: 's' }} className="overflow-y-auto">
|
||||
<Navbar></Navbar>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ gridArea: 'c' }}
|
||||
className={classNames(
|
||||
'overflow-y-auto mr-2 my-2 mobile:m-0',
|
||||
'flex flex-col rounded-md',
|
||||
background && 'bg-background-70'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{!isMobile && full && (
|
||||
<>
|
||||
<div style={{ gridArea: 'r' }}>
|
||||
<Toolbar></Toolbar>
|
||||
</div>
|
||||
<div
|
||||
style={{ gridArea: 'p' }}
|
||||
className="overflow-y-auto mr-2 mb-2 rounded-md bg-background-70 flex flex-col"
|
||||
>
|
||||
{/* <WidgetsComponent></WidgetsComponent> */}
|
||||
<SkeletonVisualizerWidget height={'100%'} maxHeight={'auto'} />
|
||||
</div>
|
||||
<div
|
||||
style={{ gridArea: 'l' }}
|
||||
className="overflow-y-auto mr-2 my-2 rounded-md bg-background-70 flex flex-col gap-2 p-2"
|
||||
>
|
||||
{/* <WidgetsComponent></WidgetsComponent> */}
|
||||
{/* <SkeletonVisualizerWidget height={'100%'} maxHeight={'auto'} /> */}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ import { useLocalization } from '@fluent/react';
|
||||
import classnames from 'classnames';
|
||||
import { ReactNode } from 'react';
|
||||
import { NavLink, useMatch } from 'react-router-dom';
|
||||
import { CubeIcon } from './commons/icon/CubeIcon';
|
||||
import { GearIcon } from './commons/icon/GearIcon';
|
||||
import { HumanIcon } from './commons/icon/HumanIcon';
|
||||
import { RulerIcon } from './commons/icon/RulerIcon';
|
||||
import { SparkleIcon } from './commons/icon/SparkleIcon';
|
||||
import { WrenchIcon } from './commons/icon/WrenchIcons';
|
||||
import { useBreakpoint } from '@/hooks/breakpoint';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { Tooltip } from './commons/Tooltip';
|
||||
import { HomeIcon } from './commons/icon/HomeIcon';
|
||||
import { SkiIcon } from './commons/icon/SkiIcon';
|
||||
|
||||
export function NavButton({
|
||||
to,
|
||||
@@ -24,43 +25,43 @@ export function NavButton({
|
||||
state?: any;
|
||||
icon: ReactNode;
|
||||
}) {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const doesMatch = useMatch({
|
||||
path: match || to,
|
||||
});
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
state={state}
|
||||
className={classnames(
|
||||
'flex flex-col justify-center xs:gap-4 mobile:gap-2',
|
||||
'xs:w-[85px] mobile:w-[80px] mobile:h-[80px]',
|
||||
'xs:py-3 mobile:py-4 rounded-md mobile:rounded-b-none group select-text',
|
||||
{
|
||||
'bg-accent-background-50 fill-accent-background-20': doesMatch,
|
||||
'hover:bg-background-70': !doesMatch,
|
||||
}
|
||||
)}
|
||||
<Tooltip
|
||||
disabled={isMobile}
|
||||
preferedDirection={'right'}
|
||||
content={children}
|
||||
variant="floating"
|
||||
>
|
||||
<div className="flex justify-around">
|
||||
<div
|
||||
className={classnames('scale-150', {
|
||||
'fill-accent-lighter': doesMatch,
|
||||
'fill-background-50': !doesMatch,
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classnames('text-center', {
|
||||
'text-accent-background-10': doesMatch,
|
||||
'text-background-10': !doesMatch,
|
||||
})}
|
||||
<NavLink
|
||||
to={to}
|
||||
state={state}
|
||||
className={classnames(
|
||||
'flex flex-col justify-center xs:gap-4 mobile:gap-2',
|
||||
'xs:w-[50px] xs:h-[50px] mobile:w-[50px] mobile:h-[50px]',
|
||||
'xs:py-3 mobile:py-4 rounded-md mobile:rounded-b-none group select-text',
|
||||
{
|
||||
'bg-accent-background-50 fill-accent-background-20': doesMatch,
|
||||
'hover:bg-background-70': !doesMatch,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</NavLink>
|
||||
<div className="flex justify-around">
|
||||
<div
|
||||
className={classnames('scale-150', {
|
||||
'fill-accent-lighter': doesMatch,
|
||||
'fill-background-50': !doesMatch,
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,7 +71,7 @@ export function MainLinks() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavButton to="/" icon={<CubeIcon></CubeIcon>}>
|
||||
<NavButton to="/" icon={<HomeIcon></HomeIcon>}>
|
||||
{l10n.getString('navbar-home')}
|
||||
</NavButton>
|
||||
<NavButton
|
||||
@@ -84,7 +85,7 @@ export function MainLinks() {
|
||||
to="/onboarding/mounting/choose"
|
||||
match="/onboarding/mounting/*"
|
||||
state={{ alonePage: true }}
|
||||
icon={<WrenchIcon></WrenchIcon>}
|
||||
icon={<SkiIcon></SkiIcon>}
|
||||
>
|
||||
{l10n.getString('navbar-mounting')}
|
||||
</NavButton>
|
||||
|
||||
13
gui/src/components/Toolbar.tsx
Normal file
13
gui/src/components/Toolbar.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ResetType } from 'solarxr-protocol';
|
||||
import { ResetButton } from './home/ResetButton';
|
||||
|
||||
export function Toolbar() {
|
||||
return (
|
||||
<div className="flex p-2 gap-2 bg-background-70 rounded-md mr-2 my-2">
|
||||
<ResetButton type={ResetType.Yaw} variant="small"></ResetButton>
|
||||
<ResetButton type={ResetType.Full} variant="small"></ResetButton>
|
||||
<ResetButton type={ResetType.Mounting} variant="small"></ResetButton>
|
||||
{/* <ClearMountingButton></ClearMountingButton> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,11 +13,15 @@ import { createPortal } from 'react-dom';
|
||||
import { Typography } from './Typography';
|
||||
import { CloseIcon } from './icon/CloseIcon';
|
||||
|
||||
type Direction = 'top' | 'left' | 'right' | 'bottom';
|
||||
interface TooltipProps {
|
||||
content: ReactNode;
|
||||
children: ReactElement;
|
||||
preferedDirection: 'top' | 'left' | 'right' | 'bottom';
|
||||
preferedDirection: Direction;
|
||||
blockedDirections?: Direction[];
|
||||
mode?: 'corner' | 'center';
|
||||
variant?: 'auto' | 'drawer' | 'floating';
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TooltipPos {
|
||||
@@ -76,6 +80,7 @@ const clamp = (v: number, min: number, max: number) =>
|
||||
|
||||
const getFloatingTooltipPosition = (
|
||||
preferedDirection: TooltipProps['preferedDirection'],
|
||||
blockedDirections: Direction[],
|
||||
mode: TooltipProps['mode'],
|
||||
childrenRect: DOMRect,
|
||||
tooltipRect: DOMRect
|
||||
@@ -134,9 +139,10 @@ const getFloatingTooltipPosition = (
|
||||
const pos = getPosition(preferedDirection);
|
||||
if (isNotInside({ ...pos, height: tooltipRect.height }, windowRect)) {
|
||||
const [firstPos] = ['left', 'top', 'right', 'bottom']
|
||||
.filter((dir) => !blockedDirections.includes(dir as Direction))
|
||||
.map((dir) => ({
|
||||
dir,
|
||||
area: getPosition(dir as TooltipProps['preferedDirection']),
|
||||
area: getPosition(dir as Direction),
|
||||
}))
|
||||
.toSorted(
|
||||
(a, b) =>
|
||||
@@ -225,12 +231,13 @@ const getFloatingTooltipPosition = (
|
||||
export function FloatingTooltip({
|
||||
childRef,
|
||||
preferedDirection,
|
||||
blockedDirections = [],
|
||||
mode,
|
||||
children,
|
||||
}: {
|
||||
childRef: MutableRefObject<HTMLDivElement | null>;
|
||||
children: ReactNode;
|
||||
} & Pick<TooltipProps, 'mode' | 'preferedDirection'>) {
|
||||
} & Pick<TooltipProps, 'mode' | 'preferedDirection' | 'blockedDirections'>) {
|
||||
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
const [tooltipStyle, setTooltipStyle] = useState<TooltipPos | undefined>();
|
||||
|
||||
@@ -244,6 +251,7 @@ export function FloatingTooltip({
|
||||
setTooltipStyle(
|
||||
getFloatingTooltipPosition(
|
||||
preferedDirection,
|
||||
blockedDirections,
|
||||
mode,
|
||||
childrenRect,
|
||||
tooltipRect
|
||||
@@ -372,15 +380,15 @@ export function DrawerTooltip({
|
||||
if (childRef.current && childRef.current.children[0]) {
|
||||
const elem = childRef.current.children[0] as HTMLElement;
|
||||
|
||||
elem.addEventListener('mousedown', touchStart); // for debug on desktop
|
||||
elem.addEventListener('mouseup', touchEnd); // for debug on desktop
|
||||
elem.addEventListener('click', touchEnd);
|
||||
// elem.addEventListener('mousedown', touchStart); // for debug on desktop
|
||||
// elem.addEventListener('mouseup', touchEnd); // for debug on desktop
|
||||
// elem.addEventListener('click', touchEnd);
|
||||
elem.addEventListener('touchstart', touchStart);
|
||||
elem.addEventListener('touchend', touchEnd);
|
||||
|
||||
return () => {
|
||||
elem.removeEventListener('mousedown', touchStart); // for debug on desktop
|
||||
elem.removeEventListener('mouseup', touchEnd); // for debug on desktop
|
||||
// elem.removeEventListener('mousedown', touchStart); // for debug on desktop
|
||||
// elem.removeEventListener('mouseup', touchEnd); // for debug on desktop
|
||||
elem.removeEventListener('touchstart', touchStart);
|
||||
elem.removeEventListener('touchend', touchEnd);
|
||||
clearTimeout(touchTimeout.current);
|
||||
@@ -435,30 +443,51 @@ export function Tooltip({
|
||||
content,
|
||||
children,
|
||||
preferedDirection,
|
||||
blockedDirections = [],
|
||||
mode = 'center',
|
||||
variant = 'auto',
|
||||
disabled = false,
|
||||
}: TooltipProps) {
|
||||
const childRef = useRef<HTMLDivElement | null>(null);
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
|
||||
let portal = null;
|
||||
if (variant === 'auto') {
|
||||
portal = isMobile ? (
|
||||
<DrawerTooltip childRef={childRef}>{content}</DrawerTooltip>
|
||||
) : (
|
||||
<FloatingTooltip
|
||||
preferedDirection={preferedDirection}
|
||||
blockedDirections={blockedDirections}
|
||||
mode={mode}
|
||||
childRef={childRef}
|
||||
>
|
||||
{content}
|
||||
</FloatingTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'drawer')
|
||||
portal = <DrawerTooltip childRef={childRef}>{content}</DrawerTooltip>;
|
||||
|
||||
if (variant === 'floating')
|
||||
portal = (
|
||||
<FloatingTooltip
|
||||
blockedDirections={blockedDirections}
|
||||
preferedDirection={preferedDirection}
|
||||
mode={mode}
|
||||
childRef={childRef}
|
||||
>
|
||||
{content}
|
||||
</FloatingTooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="contents" ref={childRef}>
|
||||
{children}
|
||||
</div>
|
||||
{createPortal(
|
||||
isMobile ? (
|
||||
<DrawerTooltip childRef={childRef}>{content}</DrawerTooltip>
|
||||
) : (
|
||||
<FloatingTooltip
|
||||
preferedDirection={preferedDirection}
|
||||
mode={mode}
|
||||
childRef={childRef}
|
||||
>
|
||||
{content}
|
||||
</FloatingTooltip>
|
||||
),
|
||||
document.body
|
||||
)}
|
||||
{!disabled && createPortal(portal, document.body)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
12
gui/src/components/commons/icon/HomeIcon.tsx
Normal file
12
gui/src/components/commons/icon/HomeIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function HomeIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="24px"
|
||||
>
|
||||
<path d="M160-120v-480l320-240 320 240v480H560v-280H400v280H160Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
12
gui/src/components/commons/icon/SkiIcon.tsx
Normal file
12
gui/src/components/commons/icon/SkiIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export function SkiIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="24px"
|
||||
>
|
||||
<path d="M740-40q-26 0-50.5-4T642-56L80-261l20-57 276 101 69-178-143-149q-27-28-21.5-66.5T320-669l139-80q17-10 34.5-11.5T528-755q17 6 29.5 19t18.5 31l13 43q13 43 42.5 76t70.5 50l21-64 57 18-45 138q-74-12-131-58t-84-114l-101 58 121 138-89 230 124 45 84-257q14 5 28 9t29 7l-85 262 31 11q18 6 37.5 9.5T740-100q26 0 49.5-5t45.5-15l45 45q-32 17-67 26t-73 9Zm-80-660q-33 0-56.5-23.5T580-780q0-33 23.5-56.5T660-860q33 0 56.5 23.5T740-780q0 33-23.5 56.5T660-700Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -34,8 +34,6 @@ export function OnboardingLayout({ children }: { children: ReactNode }) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<MainLayout widgets={false} isMobile={isMobile}>
|
||||
{children}
|
||||
</MainLayout>
|
||||
<MainLayout isMobile={isMobile}>{children}</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ declare module '@react-three/fiber' {
|
||||
}
|
||||
}
|
||||
|
||||
const GROUND_COLOR = '#4444aa';
|
||||
const GROUND_COLOR = '#2c2c6b';
|
||||
const FRUSTUM_SIZE = 10;
|
||||
const FACTOR = 2;
|
||||
// Not currently used but nice to have
|
||||
@@ -196,7 +196,7 @@ export function SkeletonVisualizerWidget({
|
||||
|
||||
if (!skeleton.current) return <></>;
|
||||
return (
|
||||
<div className="bg-background-60 flex flex-col p-3 rounded-lg gap-2">
|
||||
<div className="bg-background-70 flex flex-col rounded-lg gap-2 h-full">
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography color="primary" textAlign="text-center">
|
||||
@@ -205,8 +205,8 @@ export function SkeletonVisualizerWidget({
|
||||
}
|
||||
>
|
||||
<Canvas
|
||||
className={classNames('container mx-auto')}
|
||||
style={{ height, background: 'transparent', maxHeight }}
|
||||
className={classNames('container mx-auto h-full')}
|
||||
style={{ background: 'transparent' }}
|
||||
>
|
||||
<gridHelper args={[10, 50, GROUND_COLOR, GROUND_COLOR]} />
|
||||
<group position={[0, heightOffset, 0]} quaternion={yawReset}>
|
||||
|
||||
@@ -87,13 +87,15 @@ body {
|
||||
// overflow: hidden; -- NEVER EVER BRING THIS BACK <3
|
||||
background: theme('colors.background.20');
|
||||
|
||||
--navbar-w: 101px;
|
||||
--widget-w: 274px;
|
||||
--navbar-w: 66px;
|
||||
--topbar-h: 38px;
|
||||
--toolbar-h: 65px;
|
||||
--preview-w: 400px;
|
||||
--flightlist-w: 272px;
|
||||
|
||||
@screen mobile {
|
||||
--topbar-h: 44px;
|
||||
--navbar-h: 90px;
|
||||
--navbar-h: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ export class BoneKind extends Bone {
|
||||
case BodyPart.NECK:
|
||||
return new Color('silver');
|
||||
case BodyPart.UPPER_CHEST:
|
||||
return new Color('blue');
|
||||
return new Color('chartreuse');
|
||||
case BodyPart.CHEST:
|
||||
return new Color('purple');
|
||||
case BodyPart.WAIST:
|
||||
@@ -201,13 +201,13 @@ export class BoneKind extends Bone {
|
||||
return new Color('orange');
|
||||
case BodyPart.LEFT_UPPER_LEG:
|
||||
case BodyPart.RIGHT_UPPER_LEG:
|
||||
return new Color('blue');
|
||||
return new Color('chartreuse');
|
||||
case BodyPart.LEFT_LOWER_LEG:
|
||||
case BodyPart.RIGHT_LOWER_LEG:
|
||||
return new Color('teal');
|
||||
case BodyPart.LEFT_FOOT:
|
||||
case BodyPart.RIGHT_FOOT:
|
||||
return new Color('#00ffcc');
|
||||
return new Color('gold');
|
||||
case BodyPart.LEFT_LOWER_ARM:
|
||||
case BodyPart.RIGHT_LOWER_ARM:
|
||||
return new Color('red');
|
||||
|
||||
Reference in New Issue
Block a user