mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Mobile gui basics (#713)
* Mobile implementation without settings and widgets * Working Settings and widgets + Few tweaks for responsive * Make linter happy * Update gui/src/components/Navbar.tsx Co-authored-by: Uriel <urielfontan2002@gmail.com> * Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx Co-authored-by: Uriel <urielfontan2002@gmail.com> * Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx Co-authored-by: Uriel <urielfontan2002@gmail.com> * Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx Co-authored-by: Uriel <urielfontan2002@gmail.com> * Update gui/src/components/onboarding/pages/CalibrationTutorial.tsx Co-authored-by: Uriel <urielfontan2002@gmail.com> * Update gui/src/components/settings/pages/Serial.tsx Co-authored-by: Uriel <urielfontan2002@gmail.com> * Apply review changes * Revert removing full height in body * Fix onboarding manual body proportions on mobile * make connect trackers page work on mobile * Apply suggestions from code review Co-authored-by: DevMiner <devminer@devminer.xyz> * Applie required changes * rollback server ip * Remove placeholder string * Remove unused isMobile * Remove unused isMobile * error on unused vars * make it warn but make the lint script error on warnings --------- Co-authored-by: Uriel <urielfontan2002@gmail.com> Co-authored-by: DevMiner <devminer@devminer.xyz>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"react-dom": "^18.0.0",
|
||||
"react-hook-form": "^7.29.0",
|
||||
"react-modal": "3.15.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"semver": "^7.5.0",
|
||||
"solarxr-protocol": "file:../solarxr-protocol",
|
||||
@@ -39,7 +40,7 @@
|
||||
"dev": "tauri dev",
|
||||
"skipbundler": "tauri build -b none",
|
||||
"tauri": "tauri",
|
||||
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx,json}\" && prettier --check \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"",
|
||||
"lint": "eslint --max-warnings=0 \"src/**/*.{js,jsx,ts,tsx,json}\" && prettier --check \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"",
|
||||
"lint:fix": "eslint --fix \"src/**/*.{js,jsx,ts,tsx,json}\"",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,md,json}\"",
|
||||
"preview-vite": "vite preview",
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
"title": "SlimeVR",
|
||||
"width": 1289,
|
||||
"height": 709,
|
||||
"minWidth": 880,
|
||||
"minHeight": 740,
|
||||
"minWidth": 393,
|
||||
"minHeight": 851,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false,
|
||||
|
||||
120
gui/src/App.tsx
120
gui/src/App.tsx
@@ -49,6 +49,9 @@ import { CalibrationTutorialPage } from './components/onboarding/pages/Calibrati
|
||||
import { AssignmentTutorialPage } from './components/onboarding/pages/assignment-preparation/AssignmentTutorial';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import semver from 'semver';
|
||||
import { tauri } from '../src-tauri/tauri.conf.json';
|
||||
import { useBreakpoint } from './hooks/breakpoint';
|
||||
import { VRModePage } from './components/vr-mode/VRModePage';
|
||||
|
||||
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
|
||||
export const VersionContext = createContext('');
|
||||
@@ -56,8 +59,10 @@ export const DOCS_SITE = 'https://docs.slimevr.dev/';
|
||||
|
||||
function Layout() {
|
||||
const { loading } = useConfig();
|
||||
|
||||
if (loading) return <></>;
|
||||
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
return (
|
||||
<>
|
||||
<SerialDetectionModal></SerialDetectionModal>
|
||||
@@ -66,15 +71,23 @@ function Layout() {
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<MainLayoutRoute>
|
||||
<MainLayoutRoute isMobile={isMobile}>
|
||||
<Home />
|
||||
</MainLayoutRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/vr-mode"
|
||||
element={
|
||||
<MainLayoutRoute isMobile={isMobile}>
|
||||
<VRModePage />
|
||||
</MainLayoutRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tracker/:trackernum/:deviceid"
|
||||
element={
|
||||
<MainLayoutRoute background={false}>
|
||||
<MainLayoutRoute background={false} isMobile={isMobile}>
|
||||
<TrackerSettingsPage />
|
||||
</MainLayoutRoute>
|
||||
}
|
||||
@@ -135,7 +148,10 @@ function Layout() {
|
||||
);
|
||||
}
|
||||
|
||||
const MIN_SIZE = { width: 880, height: 740 };
|
||||
const MIN_SIZE = {
|
||||
width: tauri.windows[0].minWidth,
|
||||
height: tauri.windows[0].minHeight,
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const websocketAPI = useProvideWebsocketApi();
|
||||
@@ -160,17 +176,19 @@ export default function App() {
|
||||
fetchReleases().catch(() => console.error('failed to fetch releases'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
os.type()
|
||||
.then((type) => document.body.classList.add(type.toLowerCase()))
|
||||
.catch(console.error);
|
||||
|
||||
return () => {
|
||||
if (window.__TAURI_METADATA__) {
|
||||
useEffect(() => {
|
||||
os.type()
|
||||
.then((type) => document.body.classList.remove(type.toLowerCase()))
|
||||
.then((type) => document.body.classList.add(type.toLowerCase()))
|
||||
.catch(console.error);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return () => {
|
||||
os.type()
|
||||
.then((type) => document.body.classList.remove(type.toLowerCase()))
|
||||
.catch(console.error);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
// This doesn't seem to resize it live, but if you close it, it gets restored to min size
|
||||
useEffect(() => {
|
||||
@@ -195,45 +213,47 @@ export default function App() {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen(
|
||||
'server-status',
|
||||
(event: Event<[string, string]>) => {
|
||||
const [eventType, s] = event.payload;
|
||||
if ('stderr' === eventType) {
|
||||
// This strange invocation is what lets us lose the line information in the console
|
||||
// See more here: https://stackoverflow.com/a/48994308
|
||||
setTimeout(
|
||||
console.log.bind(
|
||||
console,
|
||||
`%c[SERVER] %c${s}`,
|
||||
'color:cyan',
|
||||
'color:red'
|
||||
)
|
||||
);
|
||||
} else if (eventType === 'stdout') {
|
||||
setTimeout(
|
||||
console.log.bind(
|
||||
console,
|
||||
`%c[SERVER] %c${s}`,
|
||||
'color:cyan',
|
||||
'color:green'
|
||||
)
|
||||
);
|
||||
} else if (eventType === 'error') {
|
||||
console.error('Error: %s', s);
|
||||
} else if (eventType === 'terminated') {
|
||||
console.error('Server Process Terminated: %s', s);
|
||||
} else if (eventType === 'other') {
|
||||
console.log('Other process event: %s', s);
|
||||
if (window.__TAURI_METADATA__) {
|
||||
useEffect(() => {
|
||||
const unlisten = listen(
|
||||
'server-status',
|
||||
(event: Event<[string, string]>) => {
|
||||
const [eventType, s] = event.payload;
|
||||
if ('stderr' === eventType) {
|
||||
// This strange invocation is what lets us lose the line information in the console
|
||||
// See more here: https://stackoverflow.com/a/48994308
|
||||
setTimeout(
|
||||
console.log.bind(
|
||||
console,
|
||||
`%c[SERVER] %c${s}`,
|
||||
'color:cyan',
|
||||
'color:red'
|
||||
)
|
||||
);
|
||||
} else if (eventType === 'stdout') {
|
||||
setTimeout(
|
||||
console.log.bind(
|
||||
console,
|
||||
`%c[SERVER] %c${s}`,
|
||||
'color:cyan',
|
||||
'color:green'
|
||||
)
|
||||
);
|
||||
} else if (eventType === 'error') {
|
||||
console.error('Error: %s', s);
|
||||
} else if (eventType === 'terminated') {
|
||||
console.error('Server Process Terminated: %s', s);
|
||||
} else if (eventType === 'other') {
|
||||
console.log('Other process event: %s', s);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
unlisten.then(() => {});
|
||||
};
|
||||
}, []);
|
||||
);
|
||||
return () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
unlisten.then(() => {});
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyboard(ev: KeyboardEvent) {
|
||||
|
||||
@@ -1,62 +1,38 @@
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import {
|
||||
LegTweaksTmpChangeT,
|
||||
LegTweaksTmpClearT,
|
||||
ResetType,
|
||||
RpcMessage,
|
||||
SettingsRequestT,
|
||||
SettingsResponseT,
|
||||
StatusData,
|
||||
} from 'solarxr-protocol';
|
||||
import { useConfig } from '../hooks/config';
|
||||
import { useLayout } from '../hooks/layout';
|
||||
import { BVHButton } from './BVHButton';
|
||||
import { ResetButton } from './home/ResetButton';
|
||||
import { useElemSize, useLayout } from '../hooks/layout';
|
||||
import { Navbar } from './Navbar';
|
||||
import { TopBar } from './TopBar';
|
||||
import { DeveloperModeWidget } from './widgets/DeveloperModeWidget';
|
||||
import { OverlayWidget } from './widgets/OverlayWidget';
|
||||
import { ClearDriftCompensationButton } from './ClearDriftCompensationButton';
|
||||
import { useWebsocketAPI } from '../hooks/websocket-api';
|
||||
import { useStatusContext, parseStatusToLocale } from '../hooks/status-system';
|
||||
import { Localized } from '@fluent/react';
|
||||
import { TipBox } from './commons/TipBox';
|
||||
import { useAppContext } from '../hooks/app';
|
||||
import { TrackingPauseButton } from './TrackingPauseButton';
|
||||
import { WidgetsComponent } from './WidgetsComponent';
|
||||
|
||||
export function MainLayoutRoute({
|
||||
children,
|
||||
background = true,
|
||||
widgets = true,
|
||||
isMobile = undefined,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
background?: boolean;
|
||||
isMobile?: boolean;
|
||||
widgets?: boolean;
|
||||
}) {
|
||||
const { height, ref: navRef } = useElemSize<HTMLDivElement>();
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
const { layoutWidth, ref: refw } = useLayout<HTMLDivElement>();
|
||||
const { config } = useConfig();
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const { trackers } = useAppContext();
|
||||
const [driftCompensationEnabled, setDriftCompensationEnabled] =
|
||||
useState(false);
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
const [ProportionsLastPageOpen, setProportionsLastPageOpen] = useState(true);
|
||||
const { statuses } = useStatusContext();
|
||||
const unprioritizedStatuses = useMemo(
|
||||
() => Object.values(statuses).filter((status) => !status.prioritized),
|
||||
[statuses]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
|
||||
}, []);
|
||||
|
||||
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
|
||||
if (settings.driftCompensation != null)
|
||||
setDriftCompensationEnabled(settings.driftCompensation.enabled);
|
||||
});
|
||||
|
||||
function usePageChanged(callback: () => void) {
|
||||
useEffect(() => {
|
||||
callback();
|
||||
@@ -87,69 +63,34 @@ export function MainLayoutRoute({
|
||||
return (
|
||||
<>
|
||||
<TopBar></TopBar>
|
||||
<div ref={ref} className="flex-grow" style={{ height: layoutHeight }}>
|
||||
<div className="flex h-full pb-3">
|
||||
<Navbar></Navbar>
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex-grow"
|
||||
style={{ height: layoutHeight - height }}
|
||||
>
|
||||
<div className="flex h-full xs:pb-3">
|
||||
{!isMobile && <Navbar></Navbar>}
|
||||
<div
|
||||
className="flex gap-2 pr-3 w-full"
|
||||
className="flex gap-2 xs:pr-3 w-full"
|
||||
ref={refw}
|
||||
style={{ minWidth: layoutWidth }}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col rounded-xl w-full overflow-hidden',
|
||||
'flex flex-col rounded-xl w-full overflow-hidden mobile:overflow-y-auto',
|
||||
background && 'bg-background-70'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{widgets && (
|
||||
{!isMobile && widgets && (
|
||||
<div className="flex flex-col px-2 min-w-[274px] w-[274px] gap-2 pt-2 rounded-xl overflow-y-auto bg-background-70">
|
||||
<div className="grid grid-cols-2 gap-2 w-full [&>*:nth-child(odd):last-of-type]:col-span-full">
|
||||
<ResetButton type={ResetType.Yaw} variant="big"></ResetButton>
|
||||
<ResetButton
|
||||
type={ResetType.Full}
|
||||
variant="big"
|
||||
></ResetButton>
|
||||
{config?.debug && (
|
||||
<ResetButton
|
||||
type={ResetType.Mounting}
|
||||
variant="big"
|
||||
></ResetButton>
|
||||
)}
|
||||
<BVHButton></BVHButton>
|
||||
<TrackingPauseButton></TrackingPauseButton>
|
||||
{driftCompensationEnabled && (
|
||||
<ClearDriftCompensationButton></ClearDriftCompensationButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<OverlayWidget></OverlayWidget>
|
||||
</div>
|
||||
<div className="w-full flex flex-col max-h-[33%] gap-3 overflow-y-auto mb-2">
|
||||
{unprioritizedStatuses.map((status) => (
|
||||
<Localized
|
||||
id={`status_system-${StatusData[status.dataType]}`}
|
||||
vars={parseStatusToLocale(status, trackers)}
|
||||
key={status.id}
|
||||
>
|
||||
<TipBox whitespace={false} hideIcon={true}>
|
||||
{`Warning, you should fix ${
|
||||
StatusData[status.dataType]
|
||||
}`}
|
||||
</TipBox>
|
||||
</Localized>
|
||||
))}
|
||||
</div>
|
||||
{config?.debug && (
|
||||
<div className="w-full">
|
||||
<DeveloperModeWidget></DeveloperModeWidget>
|
||||
</div>
|
||||
)}
|
||||
<WidgetsComponent></WidgetsComponent>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div ref={navRef}>{isMobile && <Navbar></Navbar>}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
|
||||
export function NavButton({
|
||||
to,
|
||||
@@ -31,7 +32,9 @@ export function NavButton({
|
||||
to={to}
|
||||
state={state}
|
||||
className={classnames(
|
||||
'flex flex-col justify-center gap-4 w-[85px] py-3 rounded-md group select-text',
|
||||
'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,
|
||||
@@ -60,41 +63,56 @@ export function NavButton({
|
||||
);
|
||||
}
|
||||
|
||||
export function Navbar() {
|
||||
export function MainLinks() {
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavButton to="/" icon={<CubeIcon></CubeIcon>}>
|
||||
{l10n.getString('navbar-home')}
|
||||
</NavButton>
|
||||
<NavButton
|
||||
to="/onboarding/trackers-assign"
|
||||
state={{ alonePage: true }}
|
||||
icon={<HumanIcon></HumanIcon>}
|
||||
>
|
||||
{l10n.getString('navbar-trackers_assign')}
|
||||
</NavButton>
|
||||
<NavButton
|
||||
to="/onboarding/mounting/choose"
|
||||
match="/onboarding/mounting/*"
|
||||
state={{ alonePage: true }}
|
||||
icon={<WrenchIcon></WrenchIcon>}
|
||||
>
|
||||
{l10n.getString('navbar-mounting')}
|
||||
</NavButton>
|
||||
<NavButton
|
||||
to="/onboarding/body-proportions/choose"
|
||||
match="/onboarding/body-proportions/*"
|
||||
state={{ alonePage: true }}
|
||||
icon={<RulerIcon></RulerIcon>}
|
||||
>
|
||||
{l10n.getString('navbar-body_proportions')}
|
||||
</NavButton>
|
||||
<NavButton to="/onboarding/home" icon={<SparkleIcon></SparkleIcon>}>
|
||||
{l10n.getString('navbar-onboarding')}
|
||||
</NavButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Navbar() {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
return isMobile ? (
|
||||
<div className="flex flex-row justify-around px-2 pt-2 bg-background-80 gap-2">
|
||||
<MainLinks></MainLinks>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col px-2 pt-2">
|
||||
<div className="flex flex-col flex-grow gap-2">
|
||||
<NavButton to="/" icon={<CubeIcon></CubeIcon>}>
|
||||
{l10n.getString('navbar-home')}
|
||||
</NavButton>
|
||||
<NavButton
|
||||
to="/onboarding/trackers-assign"
|
||||
state={{ alonePage: true }}
|
||||
icon={<HumanIcon></HumanIcon>}
|
||||
>
|
||||
{l10n.getString('navbar-trackers_assign')}
|
||||
</NavButton>
|
||||
<NavButton
|
||||
to="/onboarding/mounting/choose"
|
||||
match="/onboarding/mounting/*"
|
||||
state={{ alonePage: true }}
|
||||
icon={<WrenchIcon></WrenchIcon>}
|
||||
>
|
||||
{l10n.getString('navbar-mounting')}
|
||||
</NavButton>
|
||||
<NavButton
|
||||
to="/onboarding/body-proportions/choose"
|
||||
match="/onboarding/body-proportions/*"
|
||||
state={{ alonePage: true }}
|
||||
icon={<RulerIcon></RulerIcon>}
|
||||
>
|
||||
{l10n.getString('navbar-body_proportions')}
|
||||
</NavButton>
|
||||
<NavButton to="/onboarding/home" icon={<SparkleIcon></SparkleIcon>}>
|
||||
{l10n.getString('navbar-onboarding')}
|
||||
</NavButton>
|
||||
<MainLinks></MainLinks>
|
||||
</div>
|
||||
<NavButton
|
||||
to="/settings/trackers"
|
||||
|
||||
@@ -18,6 +18,26 @@ import { open } from '@tauri-apps/api/shell';
|
||||
import { GH_REPO, VersionContext, DOCS_SITE } from '../App';
|
||||
import classNames from 'classnames';
|
||||
import { QuestionIcon } from './commons/icon/QuestionIcon';
|
||||
import { useBreakpoint, useIsTauri } from '../hooks/breakpoint';
|
||||
import { GearIcon } from './commons/icon/GearIcon';
|
||||
|
||||
export function VersionTag() {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex justify-around flex-col text-standard-bold',
|
||||
'text-status-success bg-status-success bg-opacity-20 rounded-lg',
|
||||
'px-3 select-text cursor-pointer'
|
||||
)}
|
||||
onClick={() => {
|
||||
const url = `https://github.com/${GH_REPO}/releases`;
|
||||
open(url).catch(() => window.open(url, '_blank'));
|
||||
}}
|
||||
>
|
||||
{(__VERSION_TAG__ || __COMMIT_HASH__) + (__GIT_CLEAN__ ? '' : '-dirty')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
progress,
|
||||
@@ -25,6 +45,8 @@ export function TopBar({
|
||||
children?: ReactNode;
|
||||
progress?: number;
|
||||
}) {
|
||||
const isTauri = useIsTauri();
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const version = useContext(VersionContext);
|
||||
const [localIp, setLocalIp] = useState<string | null>(null);
|
||||
@@ -44,108 +66,147 @@ export function TopBar({
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-50">
|
||||
<div
|
||||
className="flex px-2 pb-1 mt-3 justify-around z-50"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="flex gap-2" data-tauri-drag-region>
|
||||
<NavLink
|
||||
to="/"
|
||||
className="flex justify-around flex-col select-all"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<SlimeVRIcon></SlimeVRIcon>
|
||||
</NavLink>
|
||||
<div className="flex justify-around flex-col" data-tauri-drag-region>
|
||||
<Typography>SlimeVR</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex justify-around flex-col text-standard-bold',
|
||||
'text-status-success bg-status-success bg-opacity-20 rounded-lg',
|
||||
'px-3 select-text cursor-pointer'
|
||||
)}
|
||||
onClick={() => {
|
||||
const url = `https://github.com/${GH_REPO}/releases`;
|
||||
open(url).catch(() => window.open(url, '_blank'));
|
||||
}}
|
||||
>
|
||||
{(__VERSION_TAG__ || __COMMIT_HASH__) +
|
||||
(__GIT_CLEAN__ ? '' : '-dirty')}
|
||||
</div>
|
||||
{doesMatchSettings && (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex justify-around flex-col text-standard-bold text-status-special',
|
||||
'bg-status-special bg-opacity-20 rounded-lg px-3 select-text'
|
||||
)}
|
||||
>
|
||||
{localIp || 'unknown local ip'}
|
||||
</div>
|
||||
)}
|
||||
{version && (
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
const url = document.body.classList.contains('windows_nt')
|
||||
? 'https://slimevr.dev/download'
|
||||
: `https://github.com/${GH_REPO}/releases/latest`;
|
||||
open(url).catch(() => window.open(url, '_blank'));
|
||||
}}
|
||||
>
|
||||
<DownloadIcon></DownloadIcon>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-grow items-center h-full justify-center z-50"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<>
|
||||
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-50">
|
||||
<div
|
||||
className="flex max-w-xl h-full items-center w-full"
|
||||
className="flex px-2 pb-1 mt-3 justify-around z-50"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{progress !== undefined && (
|
||||
<ProgressBar progress={progress} height={3} parts={3}></ProgressBar>
|
||||
<div className="flex gap-2 mobile:w-5" data-tauri-drag-region>
|
||||
<NavLink
|
||||
to="/"
|
||||
className="flex justify-around flex-col select-all"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<SlimeVRIcon></SlimeVRIcon>
|
||||
</NavLink>
|
||||
{(isTauri || !isMobile) && (
|
||||
<div
|
||||
className={classNames('flex justify-around flex-col')}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<Typography>SlimeVR</Typography>
|
||||
</div>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<>
|
||||
<VersionTag></VersionTag>
|
||||
{doesMatchSettings && (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex justify-around flex-col text-standard-bold text-status-special',
|
||||
'bg-status-special bg-opacity-20 rounded-lg px-3 select-text'
|
||||
)}
|
||||
>
|
||||
{localIp || 'unknown local ip'}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{version && (
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
const url = document.body.classList.contains('windows_nt')
|
||||
? 'https://slimevr.dev/download'
|
||||
: `https://github.com/${GH_REPO}/releases/latest`;
|
||||
open(url).catch(() => window.open(url, '_blank'));
|
||||
}}
|
||||
>
|
||||
<DownloadIcon></DownloadIcon>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-grow items-center h-full justify-center z-50"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{!isMobile && (
|
||||
<>
|
||||
<div
|
||||
className="flex max-w-xl h-full items-center w-full"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{progress !== undefined && (
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
height={3}
|
||||
parts={3}
|
||||
></ProgressBar>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isTauri && (
|
||||
<div className="flex flex-row gap-2">
|
||||
<div
|
||||
className="flex justify-around flex-col xs:hidden"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<Typography variant="section-title">SlimeVR</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex justify-end items-center px-2 gap-2 z-50"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<NavLink
|
||||
to="/settings/trackers"
|
||||
className="flex justify-around flex-col select-all fill-background-50"
|
||||
data-tauri-drag-region
|
||||
state={{ scrollTo: 'steamvr' }}
|
||||
>
|
||||
<GearIcon></GearIcon>
|
||||
</NavLink>
|
||||
|
||||
{!isMobile && (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex items-center justify-center stroke-window-icon',
|
||||
'hover:bg-background-60 rounded-full w-7 h-7 cursor-pointer'
|
||||
)}
|
||||
onClick={() =>
|
||||
open(DOCS_SITE).catch(() => window.open(DOCS_SITE, '_blank'))
|
||||
}
|
||||
>
|
||||
<QuestionIcon></QuestionIcon>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isTauri && (
|
||||
<>
|
||||
<div
|
||||
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
|
||||
onClick={() => appWindow.minimize()}
|
||||
>
|
||||
<MinimiseIcon></MinimiseIcon>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
|
||||
onClick={() => appWindow.toggleMaximize()}
|
||||
>
|
||||
<MaximiseIcon></MaximiseIcon>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
|
||||
onClick={() => appWindow.close()}
|
||||
>
|
||||
<CloseIcon></CloseIcon>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex justify-end items-center px-2 gap-2 z-50"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex items-center justify-center stroke-window-icon',
|
||||
'hover:bg-background-60 rounded-full w-7 h-7 cursor-pointer'
|
||||
)}
|
||||
onClick={() =>
|
||||
open(DOCS_SITE).catch(() => window.open(DOCS_SITE, '_blank'))
|
||||
}
|
||||
>
|
||||
<QuestionIcon></QuestionIcon>
|
||||
{isMobile && progress !== undefined && (
|
||||
<div className="flex gap-2 px-2 h-6 mb-2 justify-center flex-col border-b border-accent-background-30">
|
||||
<ProgressBar progress={progress} height={3} parts={3}></ProgressBar>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
|
||||
onClick={() => appWindow.minimize()}
|
||||
>
|
||||
<MinimiseIcon></MinimiseIcon>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
|
||||
onClick={() => appWindow.toggleMaximize()}
|
||||
>
|
||||
<MaximiseIcon></MaximiseIcon>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-center hover:bg-background-60 rounded-full w-7 h-7"
|
||||
onClick={() => appWindow.close()}
|
||||
>
|
||||
<CloseIcon></CloseIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
75
gui/src/components/WidgetsComponent.tsx
Normal file
75
gui/src/components/WidgetsComponent.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Localized } from '@fluent/react';
|
||||
import { BVHButton } from './BVHButton';
|
||||
import { ClearDriftCompensationButton } from './ClearDriftCompensationButton';
|
||||
import { TrackingPauseButton } from './TrackingPauseButton';
|
||||
import { ResetButton } from './home/ResetButton';
|
||||
import { OverlayWidget } from './widgets/OverlayWidget';
|
||||
import { TipBox } from './commons/TipBox';
|
||||
import { DeveloperModeWidget } from './widgets/DeveloperModeWidget';
|
||||
import { useConfig } from '../hooks/config';
|
||||
import {
|
||||
ResetType,
|
||||
RpcMessage,
|
||||
SettingsResponseT,
|
||||
StatusData,
|
||||
} from 'solarxr-protocol';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { parseStatusToLocale, useStatusContext } from '../hooks/status-system';
|
||||
import { useWebsocketAPI } from '../hooks/websocket-api';
|
||||
import { useAppContext } from '../hooks/app';
|
||||
|
||||
export function WidgetsComponent() {
|
||||
const { config } = useConfig();
|
||||
const { useRPCPacket } = useWebsocketAPI();
|
||||
const [driftCompensationEnabled, setDriftCompensationEnabled] =
|
||||
useState(false);
|
||||
const { trackers } = useAppContext();
|
||||
const { statuses } = useStatusContext();
|
||||
const unprioritizedStatuses = useMemo(
|
||||
() => Object.values(statuses).filter((status) => !status.prioritized),
|
||||
[statuses]
|
||||
);
|
||||
|
||||
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
|
||||
if (settings.driftCompensation != null)
|
||||
setDriftCompensationEnabled(settings.driftCompensation.enabled);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-2 w-full [&>*:nth-child(odd):last-of-type]:col-span-full">
|
||||
<ResetButton type={ResetType.Yaw} variant="big"></ResetButton>
|
||||
<ResetButton type={ResetType.Full} variant="big"></ResetButton>
|
||||
{config?.debug && (
|
||||
<ResetButton type={ResetType.Mounting} variant="big"></ResetButton>
|
||||
)}
|
||||
<BVHButton></BVHButton>
|
||||
<TrackingPauseButton></TrackingPauseButton>
|
||||
{driftCompensationEnabled && (
|
||||
<ClearDriftCompensationButton></ClearDriftCompensationButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<OverlayWidget></OverlayWidget>
|
||||
</div>
|
||||
<div className="w-full flex flex-col max-h-[33%] gap-3 overflow-y-auto mb-2">
|
||||
{unprioritizedStatuses.map((status) => (
|
||||
<Localized
|
||||
id={`status_system-${StatusData[status.dataType]}`}
|
||||
vars={parseStatusToLocale(status, trackers)}
|
||||
key={status.id}
|
||||
>
|
||||
<TipBox whitespace={false} hideIcon={true}>
|
||||
{`Warning, you should fix ${StatusData[status.dataType]}`}
|
||||
</TipBox>
|
||||
</Localized>
|
||||
))}
|
||||
</div>
|
||||
{config?.debug && (
|
||||
<div className="w-full">
|
||||
<DeveloperModeWidget></DeveloperModeWidget>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export function BaseModal({
|
||||
props.className ||
|
||||
classNames(
|
||||
'items-center focus:ring-transparent focus:ring-offset-transparent',
|
||||
'focus:outline-transparent outline-none bg-background-60 p-6 rounded-lg',
|
||||
'focus:outline-transparent outline-none bg-background-60 p-6 rounded-lg m-2',
|
||||
'text-background-10'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import { ReactNode, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { BodyPart } from 'solarxr-protocol';
|
||||
import { PersonFrontIcon } from './PersonFrontIcon';
|
||||
import { useBreakpoint } from '../../hooks/breakpoint';
|
||||
|
||||
export function BodyInteractions({
|
||||
leftControls,
|
||||
@@ -22,6 +23,8 @@ export function BodyInteractions({
|
||||
onSelectRole: (role: BodyPart) => void;
|
||||
highlightedRoles: BodyPart[];
|
||||
}) {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
|
||||
const personRef = useRef<HTMLDivElement | null>(null);
|
||||
const leftContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const rightContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -123,7 +126,7 @@ export function BodyInteractions({
|
||||
controlPosition.left < slot.left ? controlPosition.width : 0;
|
||||
|
||||
const constolLeft = controlPosition.left + offsetX;
|
||||
const LINE_BREAK_WIDTH = 40;
|
||||
const LINE_BREAK_WIDTH = isMobile ? 20 : 40;
|
||||
const leftOffsetX =
|
||||
LINE_BREAK_WIDTH * (controlPosition.left < slot.left ? -1 : 1);
|
||||
|
||||
@@ -160,7 +163,7 @@ export function BodyInteractions({
|
||||
ref={personRef}
|
||||
className={classNames(
|
||||
'relative w-full flex justify-center',
|
||||
variant === 'tracker-select' && 'mx-10'
|
||||
variant === 'tracker-select' && 'mobile:mx-0 xs:mx-10'
|
||||
)}
|
||||
>
|
||||
<PersonFrontIcon width={width}></PersonFrontIcon>
|
||||
|
||||
@@ -14,14 +14,16 @@ export function Dropdown({
|
||||
direction = 'up',
|
||||
variant = 'primary',
|
||||
alignment = 'right',
|
||||
display = 'fit',
|
||||
placeholder,
|
||||
control,
|
||||
name,
|
||||
items = [],
|
||||
}: {
|
||||
direction?: DropdownDirection;
|
||||
variant?: 'primary' | 'secondary';
|
||||
variant?: 'primary' | 'secondary' | 'tertiary';
|
||||
alignment?: 'right' | 'left';
|
||||
display?: 'fit' | 'block';
|
||||
placeholder: string;
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
@@ -37,10 +39,39 @@ export function Dropdown({
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('wheel', onWheelEvent, { passive: true });
|
||||
function onTouchEvent(event: TouchEvent) {
|
||||
// Check if we touch scroll outside of the dropdown
|
||||
if (
|
||||
isOpen &&
|
||||
!document
|
||||
.querySelector('div.dropdown-scroll')
|
||||
?.contains(event.target as HTMLDivElement)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
const isInDropdownScroll = document
|
||||
.querySelector('div.dropdown-scroll')
|
||||
?.contains(event.target as HTMLDivElement);
|
||||
const isInDropdown = document
|
||||
.querySelector('div.dropdown')
|
||||
?.contains(event.target as HTMLDivElement);
|
||||
if (isOpen && !isInDropdownScroll && !isInDropdown) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', onClick, false);
|
||||
document.addEventListener('touchmove', onTouchEvent, false);
|
||||
// TS doesn't let me specify { passive: true }, but I believe it will work anyways
|
||||
return () => document.removeEventListener('wheel', onWheelEvent);
|
||||
document.addEventListener('wheel', onWheelEvent, { passive: true });
|
||||
return () => {
|
||||
document.removeEventListener('wheel', onWheelEvent);
|
||||
document.removeEventListener('click', onClick);
|
||||
document.removeEventListener('touchmove', onTouchEvent);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
@@ -55,15 +86,23 @@ export function Dropdown({
|
||||
onClick={() => setOpen(false)}
|
||||
></div>
|
||||
)}
|
||||
<div className="relative w-fit">
|
||||
<div
|
||||
className={classNames(
|
||||
'relative',
|
||||
display === 'fit' && 'w-fit',
|
||||
display === 'block' && 'w-full'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'min-h-[35px] text-background-10 px-5 py-2.5 rounded-md focus:ring-4 text-center',
|
||||
'min-h-[35px] text-background-10 px-5 py-2.5 rounded-md focus:ring-4 text-center dropdown',
|
||||
'flex cursor-pointer',
|
||||
variant == 'primary' &&
|
||||
'bg-background-60 hover:bg-background-50',
|
||||
variant == 'secondary' &&
|
||||
'bg-background-70 hover:bg-background-60'
|
||||
'bg-background-70 hover:bg-background-60',
|
||||
variant == 'tertiary' &&
|
||||
'bg-accent-background-30 hover:bg-accent-background-20'
|
||||
)}
|
||||
onClick={() => setOpen((open) => !open)}
|
||||
onKeyDown={(ev) => a11yClick(ev) && setOpen((open) => !open)}
|
||||
@@ -101,21 +140,32 @@ export function Dropdown({
|
||||
className={classNames(
|
||||
'absolute z-10 rounded shadow min-w-max max-h-[50vh]',
|
||||
'overflow-y-auto dropdown-scroll',
|
||||
display === 'fit' && 'w-fit',
|
||||
display === 'block' && 'w-full',
|
||||
direction === 'up' && 'bottom-[45px]',
|
||||
direction === 'down' && 'top-[45px]',
|
||||
variant == 'primary' && 'bg-background-60',
|
||||
variant == 'secondary' && 'bg-background-70',
|
||||
variant == 'tertiary' && 'bg-accent-background-30',
|
||||
alignment === 'right' && 'right-0',
|
||||
alignment === 'left' && 'left-0'
|
||||
)}
|
||||
>
|
||||
<ul className="py-1 text-sm text-background-20 flex flex-col pr-2">
|
||||
<ul className="py-1 text-sm flex flex-col ">
|
||||
{items.map((item) => (
|
||||
<li
|
||||
className={classNames(
|
||||
'py-2 px-4 hover:text-background-10 min-w-max cursor-pointer',
|
||||
variant == 'primary' && 'hover:bg-background-50',
|
||||
variant == 'secondary' && 'hover:bg-background-60'
|
||||
'py-2 px-4 min-w-max cursor-pointer pr-2',
|
||||
variant == 'primary' &&
|
||||
'hover:bg-background-50 text-background-20 hover:text-background-10',
|
||||
variant == 'secondary' &&
|
||||
'hover:bg-background-60 text-background-20 hover:text-background-10',
|
||||
variant == 'tertiary' &&
|
||||
value !== item.value &&
|
||||
'bg-accent-background-30 hover:bg-accent-background-20',
|
||||
variant == 'tertiary' &&
|
||||
value === item.value &&
|
||||
'bg-accent-background-20'
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(item.value);
|
||||
|
||||
@@ -60,7 +60,7 @@ export function WarningBox({
|
||||
<div className="flex flex-col">
|
||||
<Typography
|
||||
color="text-background-60"
|
||||
whitespace={whitespace ? 'whitespace-pre' : undefined}
|
||||
whitespace={whitespace ? 'whitespace-pre-line' : undefined}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
|
||||
@@ -9,7 +9,12 @@ export function Typography({
|
||||
children,
|
||||
italic = false,
|
||||
}: {
|
||||
variant?: 'main-title' | 'section-title' | 'standard' | 'vr-accessible';
|
||||
variant?:
|
||||
| 'main-title'
|
||||
| 'section-title'
|
||||
| 'standard'
|
||||
| 'vr-accessible'
|
||||
| 'mobile-title';
|
||||
bold?: boolean;
|
||||
italic?: boolean;
|
||||
block?: boolean;
|
||||
@@ -26,6 +31,7 @@ export function Typography({
|
||||
const tags = {
|
||||
'main-title': 'h1',
|
||||
'section-title': 'h2',
|
||||
'mobile-title': 'h1',
|
||||
standard: 'p',
|
||||
'vr-accessible': 'p',
|
||||
};
|
||||
@@ -37,6 +43,8 @@ export function Typography({
|
||||
{
|
||||
className: classNames([
|
||||
'transition-colors',
|
||||
variant === 'mobile-title' &&
|
||||
'xs:text-main-title mobile:text-section-title',
|
||||
variant === 'main-title' && 'text-main-title',
|
||||
variant === 'section-title' && 'text-section-title',
|
||||
variant === 'standard' &&
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import { StatusData, TrackerDataT } from 'solarxr-protocol';
|
||||
import { useConfig } from '../../hooks/config';
|
||||
import { useTrackers } from '../../hooks/tracker';
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '../../hooks/status-system';
|
||||
import { useMemo } from 'react';
|
||||
import { WarningBox } from '../commons/TipBox';
|
||||
import { HeadsetIcon } from '../commons/icon/HeadsetIcon';
|
||||
|
||||
const DONT_REPEAT_STATUSES = [StatusData.StatusTrackerReset];
|
||||
|
||||
@@ -39,13 +40,20 @@ export function Home() {
|
||||
}, [statuses]);
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto flex flex-col gap-2">
|
||||
<div className="flex flex-col flex-wrap gap-3 px-4 pt-4 lg:flex-row">
|
||||
{filteredStatuses
|
||||
.filter(([, status]) => status.prioritized)
|
||||
.map(([, status]) => (
|
||||
<div className="md:w-1/2 w-full" key={status.id}>
|
||||
<div className="relative h-full">
|
||||
<NavLink
|
||||
to="/vr-mode"
|
||||
className="xs:hidden absolute z-50 h-12 w-12 rounded-full bg-accent-background-30 bottom-3 right-3 flex justify-center items-center fill-background-10"
|
||||
>
|
||||
<HeadsetIcon></HeadsetIcon>
|
||||
</NavLink>
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="px-2 pt-4 gap-3 w-full grid md:grid-cols-2 mobile:grid-cols-1">
|
||||
{filteredStatuses
|
||||
.filter(([, status]) => status.prioritized)
|
||||
.map(([, status]) => (
|
||||
<Localized
|
||||
key={status.id}
|
||||
id={`status_system-${StatusData[status.dataType]}`}
|
||||
vars={parseStatusToLocale(status, trackers)}
|
||||
>
|
||||
@@ -53,42 +61,44 @@ export function Home() {
|
||||
{`Warning, you should fix ${StatusData[status.dataType]}`}
|
||||
</WarningBox>
|
||||
</Localized>
|
||||
))}
|
||||
</div>
|
||||
<div className="overflow-y-auto flex flex-col gap-2">
|
||||
{trackers.length === 0 && (
|
||||
<div className="flex px-5 pt-5 justify-center">
|
||||
<Typography variant="standard">
|
||||
{l10n.getString('home-no_trackers')}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{trackers.length === 0 && (
|
||||
<div className="flex px-5 pt-5 justify-center">
|
||||
<Typography variant="standard">
|
||||
{l10n.getString('home-no_trackers')}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{!config?.debug && trackers.length > 0 && (
|
||||
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-3 px-4 my-4">
|
||||
{trackers.map(({ tracker, device }, index) => (
|
||||
<TrackerCard
|
||||
key={index}
|
||||
tracker={tracker}
|
||||
device={device}
|
||||
onClick={() => sendToSettings(tracker)}
|
||||
smol
|
||||
interactable
|
||||
warning={Object.values(statuses).some((status) =>
|
||||
trackerStatusRelated(tracker, status)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{!config?.debug && trackers.length > 0 && (
|
||||
<div className="grid sm:grid-cols-1 md:grid-cols-2 gap-3 px-2 my-2">
|
||||
{trackers.map(({ tracker, device }, index) => (
|
||||
<TrackerCard
|
||||
key={index}
|
||||
tracker={tracker}
|
||||
device={device}
|
||||
onClick={() => sendToSettings(tracker)}
|
||||
smol
|
||||
interactable
|
||||
warning={Object.values(statuses).some((status) =>
|
||||
trackerStatusRelated(tracker, status)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{config?.debug && trackers.length > 0 && (
|
||||
<div className="px-2 pt-5 overflow-y-scroll overflow-x-auto">
|
||||
<TrackersTable
|
||||
flatTrackers={trackers}
|
||||
clickedTracker={(tracker) => sendToSettings(tracker)}
|
||||
></TrackersTable>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{config?.debug && trackers.length > 0 && (
|
||||
<div className="px-2 pt-5 overflow-y-scroll overflow-x-auto">
|
||||
<TrackersTable
|
||||
flatTrackers={trackers}
|
||||
clickedTracker={(tracker) => sendToSettings(tracker)}
|
||||
></TrackersTable>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,12 +34,14 @@ export function BodyAssignment({
|
||||
rolesWithErrors = {},
|
||||
highlightedRoles = [],
|
||||
onlyAssigned = false,
|
||||
width,
|
||||
}: {
|
||||
advanced: boolean;
|
||||
onlyAssigned?: boolean;
|
||||
rolesWithErrors?: Partial<Record<BodyPart, BodyPartError>>;
|
||||
highlightedRoles?: BodyPart[];
|
||||
onRoleSelected: (role: BodyPart) => void;
|
||||
width?: number;
|
||||
}) {
|
||||
const { useAssignedTrackers } = useTrackers();
|
||||
|
||||
@@ -74,6 +76,7 @@ export function BodyAssignment({
|
||||
return (
|
||||
<>
|
||||
<BodyInteractions
|
||||
width={width}
|
||||
assignedRoles={assignedRoles}
|
||||
highlightedRoles={highlightedRoles}
|
||||
onSelectRole={onRoleSelected}
|
||||
|
||||
@@ -3,9 +3,11 @@ import { useLayout } from '../../hooks/layout';
|
||||
import { useOnboarding } from '../../hooks/onboarding';
|
||||
import { MainLayoutRoute } from '../MainLayout';
|
||||
import { TopBar } from '../TopBar';
|
||||
import { useBreakpoint } from '../../hooks/breakpoint';
|
||||
|
||||
export function OnboardingLayout({ children }: { children: ReactNode }) {
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { state } = useOnboarding();
|
||||
|
||||
return !state.alonePage ? (
|
||||
@@ -13,15 +15,15 @@ export function OnboardingLayout({ children }: { children: ReactNode }) {
|
||||
<TopBar progress={state.progress}></TopBar>
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex-grow pt-10 mx-4"
|
||||
className="flex-grow xs:pt-10 mobile:pt-2"
|
||||
style={{ height: layoutHeight }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<MainLayoutRoute widgets={false}>
|
||||
<div className="flex-grow pt-10 mx-4">{children}</div>
|
||||
<MainLayoutRoute widgets={false} isMobile={isMobile}>
|
||||
<div className="flex-grow xs:pt-10 mobile:pt-2">{children}</div>
|
||||
</MainLayoutRoute>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function SkipSetupButton({
|
||||
className={classNames(
|
||||
'text-background-40 hover:text-background-30',
|
||||
'stroke-background-40 hover:stroke-background-30',
|
||||
'absolute -top-10 right-0'
|
||||
'absolute xs:-top-10 xs:right-4 mobile:top-0 mobile:right-0'
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function SkipSetupWarningModal({
|
||||
overlayClassName={props.overlayClassName}
|
||||
>
|
||||
<div className="flex w-full h-full flex-col ">
|
||||
<div className="flex w-full flex-col flex-grow items-center gap-3">
|
||||
<div className="flex flex-col flex-grow items-center gap-3">
|
||||
<Localized id="onboarding-setup_warning" elems={{ b: <b></b> }}>
|
||||
<WarningBox>
|
||||
<b>Warning:</b> The setup is needed for good tracking, this is
|
||||
|
||||
@@ -40,7 +40,7 @@ export function StepContainer({
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'step-container transition-transform duration-500 w-full p-8 rounded-lg flex gap-4 h-full',
|
||||
'step-container transition-transform duration-500 relative w-full xs:p-8 mobile:p-2 rounded-lg flex gap-4 h-full',
|
||||
!active && 'opacity-40 pointer-events-none',
|
||||
variant === 'onboarding' && 'bg-background-70',
|
||||
variant === 'alone' && 'bg-background-60'
|
||||
@@ -51,7 +51,7 @@ export function StepContainer({
|
||||
}}
|
||||
>
|
||||
{type === 'numbered' && (
|
||||
<div className="flex flex-col">
|
||||
<div className="xs:flex xs:flex-col mobile:absolute mobile:-top-3 mobile:-right-4">
|
||||
<div className="bg-accent-background-40 rounded-full h-8 w-8 flex flex-col items-center justify-center">
|
||||
<Typography variant="section-title" bold>
|
||||
{step + 1}
|
||||
|
||||
@@ -77,11 +77,11 @@ export function CalibrationTutorialPage() {
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex w-full h-full justify-center px-20 gap-14">
|
||||
<div className="flex gap-8 self-center">
|
||||
<div className="flex w-full h-full justify-center xs:px-20 mobile:px-5 pb-5 gap-14">
|
||||
<div className="flex gap-4 self-center mobile:z-10">
|
||||
<div className="flex flex-col max-w-md gap-3">
|
||||
<div>
|
||||
<Typography variant="main-title">
|
||||
<Typography variant="mobile-title">
|
||||
{l10n.getString('onboarding-calibration_tutorial')}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
@@ -97,6 +97,11 @@ export function CalibrationTutorialPage() {
|
||||
</Typography>
|
||||
</Localized>
|
||||
<div>
|
||||
<div className="xs:hidden flex flex-row justify-center">
|
||||
<div className="stroke-none fill-background-10 ">
|
||||
<TaybolIcon width="220"></TaybolIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<LoaderIcon slimeState={slimeStatus}></LoaderIcon>
|
||||
</div>
|
||||
@@ -116,11 +121,11 @@ export function CalibrationTutorialPage() {
|
||||
<div className="flex justify-center">
|
||||
<Typography variant="section-title">{progressText}</Typography>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-3 mobile:flex-col">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to="/onboarding/wifi-creds"
|
||||
className="mr-auto"
|
||||
className="xs:mr-auto"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
@@ -132,7 +137,7 @@ export function CalibrationTutorialPage() {
|
||||
}}
|
||||
disabled={isCounting}
|
||||
hidden={CalibrationStatus.SUCCESS === calibrationStatus}
|
||||
className="ml-auto"
|
||||
className="xs:ml-auto"
|
||||
>
|
||||
{l10n.getString('onboarding-calibration_tutorial-calibrate')}
|
||||
</Button>
|
||||
@@ -140,7 +145,7 @@ export function CalibrationTutorialPage() {
|
||||
variant="primary"
|
||||
to="/onboarding/assign-tutorial"
|
||||
className={classNames(
|
||||
'ml-auto',
|
||||
'xs:ml-auto',
|
||||
CalibrationStatus.SUCCESS !== calibrationStatus && 'hidden'
|
||||
)}
|
||||
>
|
||||
@@ -149,10 +154,9 @@ export function CalibrationTutorialPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex self-center w-[32rem]">
|
||||
<div className="stroke-none fill-background-10">
|
||||
<div className="mobile:hidden flex self-center w-[32rem] mobile:absolute">
|
||||
<div className="stroke-none xs:fill-background-10 mobile:fill-background-50 mobile:blur-sm">
|
||||
<TaybolIcon width="500"></TaybolIcon>
|
||||
{/* <img src="/images/taybol.png"></img> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { TrackerCard } from '../../tracker/TrackerCard';
|
||||
import { SkipSetupWarningModal } from '../SkipSetupWarningModal';
|
||||
import { SkipSetupButton } from '../SkipSetupButton';
|
||||
import { useBnoExists } from '../../../hooks/imu-logic';
|
||||
import { useBreakpoint } from '../../../hooks/breakpoint';
|
||||
|
||||
const BOTTOM_HEIGHT = 80;
|
||||
|
||||
@@ -57,9 +58,12 @@ const statusProgressMap = {
|
||||
};
|
||||
|
||||
export function ConnectTrackersPage() {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
const { trackers, useConnectedTrackers } = useTrackers();
|
||||
const { layoutHeight, ref } = isMobile
|
||||
? { layoutHeight: 0, ref: undefined }
|
||||
: useLayout<HTMLDivElement>();
|
||||
const { useConnectedTrackers } = useTrackers();
|
||||
const { applyProgress, state, skipSetup } = useOnboarding();
|
||||
const navigate = useNavigate();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
@@ -128,13 +132,13 @@ export function ConnectTrackersPage() {
|
||||
}, [provisioningStatus]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center relative">
|
||||
<div className="flex flex-col h-full items-center relative overflow-y-auto px-4 pb-4">
|
||||
<SkipSetupButton
|
||||
visible={!state.alonePage}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex gap-10 w-full max-w-7xl ">
|
||||
<div className="flex gap-10 mobile:flex-col w-full xs:max-w-7xl">
|
||||
<div className="flex flex-col w-full max-w-sm">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-connect_tracker-title')}
|
||||
@@ -225,7 +229,7 @@ export function ConnectTrackersPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex flex-col xs:flex-grow">
|
||||
<Typography color="secondary" bold>
|
||||
{l10n.getString('onboarding-connect_tracker-connected_trackers', {
|
||||
amount: connectedTrackers.length,
|
||||
@@ -233,20 +237,20 @@ export function ConnectTrackersPage() {
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
className="flex-grow overflow-y-scroll"
|
||||
className="xs:flex-grow xs:overflow-y-scroll"
|
||||
ref={ref}
|
||||
style={{ height: layoutHeight - BOTTOM_HEIGHT }}
|
||||
style={isMobile ? { height: layoutHeight - BOTTOM_HEIGHT } : {}}
|
||||
>
|
||||
<div className="grid lg:grid-cols-2 md:grid-cols-1 gap-2 mx-3 pt-3">
|
||||
<div className="grid lg:grid-cols-2 md:grid-cols-1 gap-2 xs:mx-3 pt-3">
|
||||
{Array.from({
|
||||
...connectedTrackers,
|
||||
length: Math.max(trackers.length, 20),
|
||||
length: Math.max(connectedTrackers.length, isMobile ? 1 : 20),
|
||||
}).map((tracker, index) => (
|
||||
<div key={index}>
|
||||
{!tracker && (
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-xl h-16',
|
||||
'rounded-xl h-16 mobile:animate-pulse',
|
||||
state.alonePage
|
||||
? 'bg-background-80'
|
||||
: 'bg-background-70'
|
||||
|
||||
@@ -17,7 +17,7 @@ export function HomePage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative px-4">
|
||||
<SkipSetupButton
|
||||
visible={true}
|
||||
modalVisible={skipWarning}
|
||||
@@ -25,14 +25,14 @@ export function HomePage() {
|
||||
></SkipSetupButton>
|
||||
<div className="flex flex-col gap-5 items-center z-10 scale-150 mb-20">
|
||||
<SlimeVRIcon></SlimeVRIcon>
|
||||
<Typography variant="main-title">
|
||||
<Typography variant="mobile-title">
|
||||
{l10n.getString('onboarding-home')}
|
||||
</Typography>
|
||||
<Button variant="primary" to="/onboarding/wifi-creds">
|
||||
{l10n.getString('onboarding-home-start')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute right-0 bottom-4 z-50">
|
||||
<div className="absolute right-4 bottom-4 z-50">
|
||||
<LangSelector />
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -18,8 +18,10 @@ import { useTrackers } from '../../../hooks/tracker';
|
||||
import { BodyDisplay } from '../../commons/BodyDisplay';
|
||||
import { useWebsocketAPI } from '../../../hooks/websocket-api';
|
||||
import classNames from 'classnames';
|
||||
import { useBreakpoint } from '../../../hooks/breakpoint';
|
||||
|
||||
export function ResetTutorialPage() {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, skipSetup } = useOnboarding();
|
||||
const [skipWarning, setSkipWarning] = useState(false);
|
||||
@@ -119,80 +121,79 @@ export function ResetTutorialPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
|
||||
<SkipSetupButton
|
||||
visible={true}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex flex-col w-full h-full justify-center px-20">
|
||||
<div className="flex gap-8 self-center">
|
||||
<div className="flex flex-col gap-3 w-96 self-center">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-reset_tutorial')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-reset_tutorial-description')}
|
||||
</Typography>
|
||||
<div className="flex">
|
||||
<Button variant="secondary" to="/onboarding/mounting/choose">
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<div className="flex xs:flex-row mobile:flex-col w-full h-full xs:justify-center xs:px-20 mobile:px-4 gap-8 self-center">
|
||||
<div className="flex flex-col gap-3 xs:w-96 self-center">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-reset_tutorial')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-reset_tutorial-description')}
|
||||
</Typography>
|
||||
<div className="flex">
|
||||
<Button variant="secondary" to="/onboarding/mounting/choose">
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
hidden={curIndex + 1 >= order.length}
|
||||
variant="secondary"
|
||||
className="ml-auto"
|
||||
onClick={() => {
|
||||
setCurIndex(curIndex + 1);
|
||||
}}
|
||||
>
|
||||
{l10n.getString('onboarding-reset_tutorial-skip')}
|
||||
</Button>
|
||||
<Button
|
||||
hidden={curIndex + 1 >= order.length}
|
||||
variant="secondary"
|
||||
className="ml-auto"
|
||||
onClick={() => {
|
||||
setCurIndex(curIndex + 1);
|
||||
}}
|
||||
>
|
||||
{l10n.getString('onboarding-reset_tutorial-skip')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/body-proportions/choose"
|
||||
className={classNames(
|
||||
'ml-auto',
|
||||
order.length > curIndex + 1 && 'hidden'
|
||||
)}
|
||||
>
|
||||
{l10n.getString('onboarding-continue')}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/body-proportions/choose"
|
||||
className={classNames(
|
||||
'self-center w-72 md:hidden mt-10 ml-auto border-background-10',
|
||||
'border-l-4 pl-4',
|
||||
curIndex < order.length && 'visible',
|
||||
curIndex >= order.length && 'hidden'
|
||||
'ml-auto',
|
||||
order.length > curIndex + 1 && 'hidden'
|
||||
)}
|
||||
>
|
||||
<Typography whitespace="whitespace-pre-line" color="secondary">
|
||||
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
|
||||
taps: tapSettings[curIndex],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
{l10n.getString('onboarding-continue')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<BodyDisplay
|
||||
trackers={[order[curIndex]]}
|
||||
hideUnassigned={true}
|
||||
></BodyDisplay>
|
||||
<div
|
||||
className={classNames(
|
||||
'self-center w-72 md-max:hidden',
|
||||
curIndex >= order.length && 'hidden'
|
||||
)}
|
||||
>
|
||||
<Typography whitespace="whitespace-pre-line" color="secondary">
|
||||
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
|
||||
taps: tapSettings[curIndex],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'self-center xs:w-72 md:hidden xs:mt-10 mobile:mt-5 xs:ml-auto border-background-10',
|
||||
'border-l-4 pl-4',
|
||||
curIndex < order.length && 'visible',
|
||||
curIndex >= order.length && 'hidden'
|
||||
)}
|
||||
>
|
||||
<Typography whitespace="whitespace-pre-line" color="secondary">
|
||||
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
|
||||
taps: tapSettings[curIndex],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-center">
|
||||
<BodyDisplay
|
||||
width={isMobile ? 160 : undefined}
|
||||
trackers={[order[curIndex]]}
|
||||
hideUnassigned={true}
|
||||
></BodyDisplay>
|
||||
<div
|
||||
className={classNames(
|
||||
'self-center w-72 md-max:hidden',
|
||||
curIndex >= order.length && 'hidden'
|
||||
)}
|
||||
>
|
||||
<Typography whitespace="whitespace-pre-line" color="secondary">
|
||||
{l10n.getString(`onboarding-reset_tutorial-${curIndex}`, {
|
||||
taps: tapSettings[curIndex],
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,13 +28,13 @@ export function WifiCredsPage() {
|
||||
className="flex flex-col w-full h-full"
|
||||
onSubmit={handleSubmit(submitWifiCreds)}
|
||||
>
|
||||
<div className="flex flex-col w-full h-full justify-center items-center relative">
|
||||
<div className="flex flex-col w-full h-full xs:justify-center items-center relative ">
|
||||
<SkipSetupButton
|
||||
visible={true}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex gap-10">
|
||||
<div className="flex mobile:flex-col xs:gap-10 px-4">
|
||||
<div className="flex flex-col max-w-sm">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-wifi_creds')}
|
||||
|
||||
@@ -29,67 +29,69 @@ export function AssignmentTutorialPage() {
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex flex-col w-full h-full justify-center px-20 gap-3">
|
||||
<div className="mt-10 self-center">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-assignment_tutorial')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col self-center justify-center gap-5">
|
||||
<div className="flex gap-12 flex-row self-center justify-center">
|
||||
<div className="flex flex-col gap-5 w-1/4">
|
||||
<div>
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString(
|
||||
'onboarding-assignment_tutorial-first_step'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="stroke-background-10 fill-background-10">
|
||||
<StickerSlime width="65%"></StickerSlime>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10 w-1/4">
|
||||
<div>
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString(
|
||||
'onboarding-assignment_tutorial-second_step'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="fill-background-10 stroke-background-10">
|
||||
<TrackerArrow width="75%"></TrackerArrow>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString(
|
||||
'onboarding-assignment_tutorial-second_step-continuation'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="fill-background-10 stroke-background-10">
|
||||
<ExtensionArrow width="75%"></ExtensionArrow>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full h-full justify-center xs:px-20 gap-3 pb-2">
|
||||
<div className="overflow-y-auto px-4">
|
||||
<div className="mb-10 self-center">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-assignment_tutorial')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to={
|
||||
bnoExists
|
||||
? '/onboarding/calibration-tutorial'
|
||||
: '/onboarding/wifi-creds'
|
||||
}
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/trackers-assign"
|
||||
className="ml-auto"
|
||||
>
|
||||
{l10n.getString('onboarding-assignment_tutorial-done')}
|
||||
</Button>
|
||||
<div className="flex flex-col self-center justify-center gap-5 px-2">
|
||||
<div className="flex gap-12 xs:flex-row mobile:flex-col self-center justify-center">
|
||||
<div className="flex flex-col gap-5 xs:w-1/4">
|
||||
<div>
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString(
|
||||
'onboarding-assignment_tutorial-first_step'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="stroke-background-10 fill-background-10 flex justify-center">
|
||||
<StickerSlime width="65%"></StickerSlime>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10 xs:w-1/4">
|
||||
<div>
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString(
|
||||
'onboarding-assignment_tutorial-second_step'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="fill-background-10 stroke-background-10 flex justify-center">
|
||||
<TrackerArrow width="75%"></TrackerArrow>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString(
|
||||
'onboarding-assignment_tutorial-second_step-continuation'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="fill-background-10 stroke-background-10 flex justify-center">
|
||||
<ExtensionArrow width="75%"></ExtensionArrow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to={
|
||||
bnoExists
|
||||
? '/onboarding/calibration-tutorial'
|
||||
: '/onboarding/wifi-creds'
|
||||
}
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/trackers-assign"
|
||||
className="ml-auto"
|
||||
>
|
||||
{l10n.getString('onboarding-assignment_tutorial-done')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function AutomaticProportionsPage() {
|
||||
|
||||
return (
|
||||
<AutoboneContextC.Provider value={context}>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative px-4 pb-4">
|
||||
<SkipSetupButton
|
||||
visible={!state.alonePage}
|
||||
modalVisible={skipWarning}
|
||||
@@ -70,21 +70,19 @@ export function AutomaticProportionsPage() {
|
||||
></StepperSlider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full pb-4 flex flex-row">
|
||||
<div className="flex flex-grow gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={startCountdown}
|
||||
disabled={isCounting}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="opacity-0 h-0">
|
||||
{l10n.getString('reset-reset_all')}
|
||||
</div>
|
||||
{!isCounting ? l10n.getString('reset-reset_all') : timer}
|
||||
<div className="w-full pb-4 flex flex-grow flex-row mobile:justify-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={startCountdown}
|
||||
disabled={isCounting}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="opacity-0 h-0">
|
||||
{l10n.getString('reset-reset_all')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{!isCounting ? l10n.getString('reset-reset_all') : timer}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SkipSetupWarningModal
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../../../hooks/manual-proportions';
|
||||
import { useLocaleConfig } from '../../../../i18n/config';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
import { useBreakpoint } from '../../../../hooks/breakpoint';
|
||||
|
||||
function IncrementButton({
|
||||
children,
|
||||
@@ -20,10 +21,10 @@ function IncrementButton({
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'p-3 rounded-lg w-16 h-16 flex flex-col justify-center items-center bg-background-60 hover:bg-opacity-50'
|
||||
'p-3 rounded-lg xs:w-16 xs:h-16 mobile:w-10 flex flex-col justify-center items-center bg-background-60 hover:bg-opacity-50'
|
||||
)}
|
||||
>
|
||||
<Typography variant="main-title" bold>
|
||||
<Typography variant="mobile-title" bold>
|
||||
{children}
|
||||
</Typography>
|
||||
</div>
|
||||
@@ -39,6 +40,8 @@ export function BodyProportions({
|
||||
type: 'linear' | 'ratio';
|
||||
variant: 'onboarding' | 'alone';
|
||||
}) {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
|
||||
const [bodyParts, _ratioMode, currentSelection, dispatch, setRatioMode] =
|
||||
useManualProportions();
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
@@ -69,8 +72,8 @@ export function BodyProportions({
|
||||
<div className="relative w-full">
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col overflow-y-scroll overflow-x-hidden max-h-[450px] h-[54vh]',
|
||||
'w-full px-1 gap-3 gradient-mask-b-90'
|
||||
'flex flex-col xs:overflow-y-scroll xs:overflow-x-hidden xs:max-h-[450px] xs:h-[54vh]',
|
||||
'w-full px-1 gap-3 xs:gradient-mask-b-90'
|
||||
)}
|
||||
>
|
||||
<>
|
||||
@@ -79,7 +82,7 @@ export function BodyProportions({
|
||||
'index' in props && props.index !== undefined
|
||||
? props.bones[props.index].value
|
||||
: originalValue;
|
||||
const selected = currentSelection.label === label;
|
||||
const selected = isMobile || currentSelection.label === label;
|
||||
|
||||
const selectNew = () => {
|
||||
switch (type) {
|
||||
@@ -130,8 +133,9 @@ export function BodyProportions({
|
||||
>
|
||||
{!precise && (
|
||||
<IncrementButton
|
||||
onClick={() =>
|
||||
type === LabelType.GroupPart
|
||||
onClick={() => {
|
||||
selectNew();
|
||||
return type === LabelType.GroupPart
|
||||
? dispatch({
|
||||
type: ProportionChangeType.Ratio,
|
||||
value: -0.05,
|
||||
@@ -139,15 +143,16 @@ export function BodyProportions({
|
||||
: dispatch({
|
||||
type: ProportionChangeType.Linear,
|
||||
value: -5,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{configFormat.format(-5)}
|
||||
</IncrementButton>
|
||||
)}
|
||||
<IncrementButton
|
||||
onClick={() =>
|
||||
type === LabelType.GroupPart
|
||||
onClick={() => {
|
||||
selectNew();
|
||||
return type === LabelType.GroupPart
|
||||
? dispatch({
|
||||
type: ProportionChangeType.Ratio,
|
||||
value: -0.01,
|
||||
@@ -155,15 +160,16 @@ export function BodyProportions({
|
||||
: dispatch({
|
||||
type: ProportionChangeType.Linear,
|
||||
value: -1,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{configFormat.format(-1)}
|
||||
</IncrementButton>
|
||||
{precise && (
|
||||
<IncrementButton
|
||||
onClick={() =>
|
||||
type === LabelType.GroupPart
|
||||
onClick={() => {
|
||||
selectNew();
|
||||
return type === LabelType.GroupPart
|
||||
? dispatch({
|
||||
type: ProportionChangeType.Ratio,
|
||||
value: -0.005,
|
||||
@@ -171,8 +177,8 @@ export function BodyProportions({
|
||||
: dispatch({
|
||||
type: ProportionChangeType.Linear,
|
||||
value: -0.5,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{configFormat.format(-0.5)}
|
||||
</IncrementButton>
|
||||
@@ -185,14 +191,14 @@ export function BodyProportions({
|
||||
<div
|
||||
key={label}
|
||||
className={classNames(
|
||||
'p-3 rounded-lg h-16 flex w-full items-center justify-between px-6 transition-colors duration-300 bg-background-60',
|
||||
'p-3 rounded-lg xs:h-16 flex w-full items-center justify-between xs:px-6 mobile:px-3 transition-colors duration-300 bg-background-60',
|
||||
(selected && 'opacity-100') || 'opacity-50'
|
||||
)}
|
||||
>
|
||||
<Typography variant="section-title" bold>
|
||||
{l10n.getString(label)}
|
||||
</Typography>
|
||||
<Typography variant="main-title" bold>
|
||||
<Typography variant="mobile-title" bold>
|
||||
{type === LabelType.GroupPart
|
||||
? /* Make number rounding so it's based on .5 decimals */
|
||||
percentageFormat.format(Math.round(value * 200) / 200)
|
||||
@@ -213,8 +219,9 @@ export function BodyProportions({
|
||||
>
|
||||
{precise && (
|
||||
<IncrementButton
|
||||
onClick={() =>
|
||||
type === LabelType.GroupPart
|
||||
onClick={() => {
|
||||
selectNew();
|
||||
return type === LabelType.GroupPart
|
||||
? dispatch({
|
||||
type: ProportionChangeType.Ratio,
|
||||
value: 0.005,
|
||||
@@ -222,15 +229,16 @@ export function BodyProportions({
|
||||
: dispatch({
|
||||
type: ProportionChangeType.Linear,
|
||||
value: 0.5,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{configFormat.format(+0.5)}
|
||||
</IncrementButton>
|
||||
)}
|
||||
<IncrementButton
|
||||
onClick={() =>
|
||||
type === LabelType.GroupPart
|
||||
onClick={() => {
|
||||
selectNew();
|
||||
return type === LabelType.GroupPart
|
||||
? dispatch({
|
||||
type: ProportionChangeType.Ratio,
|
||||
value: 0.01,
|
||||
@@ -238,15 +246,16 @@ export function BodyProportions({
|
||||
: dispatch({
|
||||
type: ProportionChangeType.Linear,
|
||||
value: 1,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{configFormat.format(+1)}
|
||||
</IncrementButton>
|
||||
{!precise && (
|
||||
<IncrementButton
|
||||
onClick={() =>
|
||||
type === LabelType.GroupPart
|
||||
onClick={() => {
|
||||
selectNew();
|
||||
return type === LabelType.GroupPart
|
||||
? dispatch({
|
||||
type: ProportionChangeType.Ratio,
|
||||
value: 0.05,
|
||||
@@ -254,8 +263,8 @@ export function BodyProportions({
|
||||
: dispatch({
|
||||
type: ProportionChangeType.Linear,
|
||||
value: 5,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{configFormat.format(+5)}
|
||||
</IncrementButton>
|
||||
|
||||
@@ -11,11 +11,45 @@ import { useLocalization } from '@fluent/react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { SkipSetupWarningModal } from '../../SkipSetupWarningModal';
|
||||
import { SkipSetupButton } from '../../SkipSetupButton';
|
||||
import { useBreakpoint } from '../../../../hooks/breakpoint';
|
||||
|
||||
export function ButtonsControl() {
|
||||
const { l10n } = useLocalization();
|
||||
const { state } = useOnboarding();
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
|
||||
const resetAll = () => {
|
||||
sendRPCPacket(
|
||||
RpcMessage.SkeletonResetAllRequest,
|
||||
new SkeletonResetAllRequestT()
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
to="/onboarding/body-proportions/choose"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={resetAll}>
|
||||
{l10n.getString('reset-reset_all')}
|
||||
</Button>
|
||||
{!state.alonePage && (
|
||||
<Button variant="primary" className="ml-auto" to="/onboarding/done">
|
||||
{l10n.getString('onboarding-continue')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ManualProportionsPage() {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
const [skipWarning, setSkipWarning] = useState(false);
|
||||
|
||||
applyProgress(0.9);
|
||||
@@ -27,28 +61,21 @@ export function ManualProportionsPage() {
|
||||
});
|
||||
const { precise, ratio } = watch();
|
||||
|
||||
const resetAll = () => {
|
||||
sendRPCPacket(
|
||||
RpcMessage.SkeletonResetAllRequest,
|
||||
new SkeletonResetAllRequestT()
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('ratioMode', ratio.toString());
|
||||
}, [ratio]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center overflow-y-auto relative">
|
||||
<SkipSetupButton
|
||||
visible={!state.alonePage}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex flex-col w-full h-full max-w-5xl justify-center">
|
||||
<div className="flex flex-col w-full h-full xs:max-w-5xl xs:justify-center">
|
||||
<div className="flex gap-8 justify-center">
|
||||
<div className="flex flex-col w-full max-w-2xl gap-3 items-center">
|
||||
<div className="flex flex-col w-full xs:max-w-2xl gap-3 items-center">
|
||||
<div className="flex flex-col">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-manual_proportions-title')}
|
||||
@@ -67,7 +94,13 @@ export function ManualProportionsPage() {
|
||||
name="precise"
|
||||
variant="toggle"
|
||||
></CheckBox>
|
||||
{isMobile && (
|
||||
<div className="flex gap-3 justify-between">
|
||||
<ButtonsControl></ButtonsControl>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BodyProportions
|
||||
precise={precise}
|
||||
type={ratio ? 'ratio' : 'linear'}
|
||||
@@ -78,31 +111,11 @@ export function ManualProportionsPage() {
|
||||
<PersonFrontIcon width={200}></PersonFrontIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-5">
|
||||
<Button
|
||||
variant="secondary"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
to="/onboarding/body-proportions/choose"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={resetAll}>
|
||||
{l10n.getString('reset-reset_all')}
|
||||
</Button>
|
||||
{!state.alonePage && (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="ml-auto"
|
||||
to="/onboarding/done"
|
||||
>
|
||||
{l10n.getString('onboarding-continue')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full py-4 flex flex-row">
|
||||
<div className="flex flex-grow gap-3"></div>
|
||||
<div className="flex gap-3"></div>
|
||||
{!isMobile && (
|
||||
<div className="flex gap-3 mt-5 mx-4">
|
||||
<ButtonsControl></ButtonsControl>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SkipSetupWarningModal
|
||||
|
||||
@@ -15,8 +15,10 @@ import { useWebsocketAPI } from '../../../../hooks/websocket-api';
|
||||
import saveAs from 'file-saver';
|
||||
import { save } from '@tauri-apps/api/dialog';
|
||||
import { writeTextFile } from '@tauri-apps/api/fs';
|
||||
import { useIsTauri } from '../../../../hooks/breakpoint';
|
||||
|
||||
export function ProportionsChoose() {
|
||||
const isTauri = useIsTauri();
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const [skipWarning, setSkipWarning] = useState(false);
|
||||
@@ -29,22 +31,25 @@ export function ProportionsChoose() {
|
||||
const blob = new Blob([JSON.stringify(data)], {
|
||||
type: 'application/json',
|
||||
});
|
||||
save({
|
||||
filters: [
|
||||
{
|
||||
name: l10n.getString('onboarding-choose_proportions-file_type'),
|
||||
extensions: ['json'],
|
||||
},
|
||||
],
|
||||
defaultPath: 'body-proportions.json',
|
||||
})
|
||||
.then((path) =>
|
||||
path ? writeTextFile(path, JSON.stringify(data)) : undefined
|
||||
)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
saveAs(blob, 'body-proportions.json');
|
||||
});
|
||||
if (isTauri) {
|
||||
save({
|
||||
filters: [
|
||||
{
|
||||
name: l10n.getString('onboarding-choose_proportions-file_type'),
|
||||
extensions: ['json'],
|
||||
},
|
||||
],
|
||||
defaultPath: 'body-proportions.json',
|
||||
})
|
||||
.then((path) =>
|
||||
path ? writeTextFile(path, JSON.stringify(data)) : undefined
|
||||
)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
} else {
|
||||
saveAs(blob, 'body-proportions.json');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -52,17 +57,17 @@ export function ProportionsChoose() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center mobile:overflow-y-auto relative px-4 pb-4">
|
||||
<SkipSetupButton
|
||||
visible={!state.alonePage}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex flex-col gap-4 justify-center">
|
||||
<div className="w-[666px]">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-choose_proportions')}
|
||||
</Typography>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-choose_proportions')}
|
||||
</Typography>
|
||||
<div className="xs:w-10/12 xs:max-w-[666px]">
|
||||
<Typography
|
||||
variant="standard"
|
||||
color="secondary"
|
||||
@@ -71,111 +76,109 @@ export function ProportionsChoose() {
|
||||
{l10n.getString('onboarding-choose_proportions-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classNames('h-full w-[760px] min-w-[760px]')}>
|
||||
<div className="flex flex-row gap-4 [&>div]:grow">
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-manual_proportions'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-manual_proportions-subtitle'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-manual_proportions-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid xs:grid-cols-2 w-full xs:flex-row mobile:flex-col gap-4 [&>div]:grow'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row flex-grow',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-manual_proportions'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-manual_proportions-subtitle'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-manual_proportions-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
to="/onboarding/body-proportions/manual"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-automatic_proportions-manual')}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
to="/onboarding/body-proportions/manual"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-automatic_proportions-manual')}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row relative',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<img
|
||||
onMouseEnter={() => setAnimated(() => true)}
|
||||
onAnimationEnd={() => setAnimated(() => false)}
|
||||
src="/images/slimetower.png"
|
||||
className={classNames(
|
||||
'absolute w-1/3 -right-2 -top-32',
|
||||
animated && 'animate-[bounce_1s_1]'
|
||||
)}
|
||||
></img>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-auto_proportions'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-auto_proportions-subtitle'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-auto_proportions-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row relative',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<img
|
||||
onMouseEnter={() => setAnimated(() => true)}
|
||||
onAnimationEnd={() => setAnimated(() => false)}
|
||||
src="/images/slimetower.png"
|
||||
className={classNames(
|
||||
'absolute w-[100px] -right-2 -top-24',
|
||||
animated && 'animate-[bounce_1s_1]'
|
||||
)}
|
||||
></img>
|
||||
<div>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-auto_proportions'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-auto_proportions-subtitle'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-choose_proportions-auto_proportions-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/body-proportions/auto"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-manual_proportions-auto')}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/body-proportions/auto"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-manual_proportions-auto')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
{!state.alonePage && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="ml-4"
|
||||
to="/onboarding/reset-tutorial"
|
||||
>
|
||||
<Button variant="secondary" to="/onboarding/reset-tutorial">
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
className="ml-auto mr-4"
|
||||
className="ml-auto"
|
||||
onClick={() =>
|
||||
sendRPCPacket(
|
||||
RpcMessage.SkeletonConfigRequest,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useBreakpoint } from '../../../../../hooks/breakpoint';
|
||||
import { useTrackers } from '../../../../../hooks/tracker';
|
||||
import { BodyDisplay } from '../../../../commons/BodyDisplay';
|
||||
import { Button } from '../../../../commons/Button';
|
||||
@@ -12,6 +13,7 @@ export function PutTrackersOnStep({
|
||||
nextStep: () => void;
|
||||
variant: 'alone' | 'onboarding';
|
||||
}) {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
const { trackers } = useTrackers();
|
||||
|
||||
@@ -36,8 +38,20 @@ export function PutTrackersOnStep({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
|
||||
<BodyDisplay
|
||||
trackers={trackers}
|
||||
width={120}
|
||||
dotsSize={15}
|
||||
variant="dots"
|
||||
hideUnassigned={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
to="/onboarding/body-proportions/choose"
|
||||
@@ -53,15 +67,17 @@ export function PutTrackersOnStep({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
|
||||
<BodyDisplay
|
||||
trackers={trackers}
|
||||
width={150}
|
||||
dotsSize={15}
|
||||
variant="dots"
|
||||
hideUnassigned={true}
|
||||
/>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
|
||||
<BodyDisplay
|
||||
trackers={trackers}
|
||||
width={150}
|
||||
dotsSize={15}
|
||||
variant="dots"
|
||||
hideUnassigned={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Recording({ nextStep }: { nextStep: () => void }) {
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<ol className="list-decimal">
|
||||
<ol className="list-decimal mobile:px-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('onboarding-automatic_proportions-recording-steps')
|
||||
|
||||
@@ -22,7 +22,7 @@ export function RequirementsStep({
|
||||
'onboarding-automatic_proportions-requirements-title'
|
||||
)}
|
||||
</Typography>
|
||||
<ul className="list-disc">
|
||||
<ul className="list-disc mobile:px-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString(
|
||||
@@ -38,7 +38,7 @@ export function RequirementsStep({
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
onClick={prevStep}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function StartRecording({
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<ol className="list-decimal">
|
||||
<ol className="list-decimal mobile:px-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('onboarding-automatic_proportions-recording-steps')
|
||||
@@ -54,7 +54,7 @@ export function StartRecording({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
onClick={prevStep}
|
||||
|
||||
@@ -25,14 +25,14 @@ export function AutomaticMountingPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 h-full items-center w-full justify-center relative">
|
||||
<div className="flex flex-col gap-2 h-full items-center w-full xs:justify-center relative overflow-y-auto overflow-x-hidden px-4 pb-4">
|
||||
<SkipSetupButton
|
||||
visible={!state.alonePage}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex flex-col w-full h-full justify-center max-w-3xl gap-5">
|
||||
<div className="flex flex-col max-w-lg gap-3">
|
||||
<div className="flex flex-col w-full h-full xs:justify-center xs:max-w-3xl gap-5">
|
||||
<div className="flex flex-col xs:max-w-lg gap-3">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-automatic_mounting-title')}
|
||||
</Typography>
|
||||
@@ -40,7 +40,7 @@ export function AutomaticMountingPage() {
|
||||
{l10n.getString('onboarding-automatic_mounting-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="flex pb-4">
|
||||
<StepperSlider
|
||||
variant={state.alonePage ? 'alone' : 'onboarding'}
|
||||
steps={steps}
|
||||
|
||||
@@ -13,8 +13,10 @@ import { MountingSelectionMenu } from './MountingSelectionMenu';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { SkipSetupWarningModal } from '../../SkipSetupWarningModal';
|
||||
import { SkipSetupButton } from '../../SkipSetupButton';
|
||||
import { useBreakpoint } from '../../../../hooks/breakpoint';
|
||||
|
||||
export function ManualMountingPage() {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const { sendRPCPacket } = useWebsocketAPI();
|
||||
@@ -64,45 +66,44 @@ export function ManualMountingPage() {
|
||||
onClose={() => setSelectRole(BodyPart.NONE)}
|
||||
onDirectionSelected={onDirectionSelected}
|
||||
></MountingSelectionMenu>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
|
||||
<SkipSetupButton
|
||||
visible={!state.alonePage}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex flex-col w-full h-full justify-center items-center">
|
||||
<div className="flex md:gap-8">
|
||||
<div className="flex flex-col w-full max-w-md gap-3">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-manual_mounting')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-manual_mounting-description')}
|
||||
</Typography>
|
||||
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
|
||||
<div className="flex flex-row gap-3 mt-auto">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to="/onboarding/mounting/choose"
|
||||
state={state}
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
<div className="flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center">
|
||||
<div className="flex flex-col w-full xs:max-w-sm gap-3">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-manual_mounting')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-manual_mounting-description')}
|
||||
</Typography>
|
||||
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
|
||||
<div className="flex flex-row gap-3 mt-auto">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to="/onboarding/mounting/choose"
|
||||
state={state}
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
</Button>
|
||||
{!state.alonePage && (
|
||||
<Button variant="primary" to="/onboarding/reset-tutorial">
|
||||
{l10n.getString('onboarding-manual_mounting-next')}
|
||||
</Button>
|
||||
{!state.alonePage && (
|
||||
<Button variant="primary" to="/onboarding/reset-tutorial">
|
||||
{l10n.getString('onboarding-manual_mounting-next')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
|
||||
<BodyAssignment
|
||||
onlyAssigned={true}
|
||||
advanced={true}
|
||||
onRoleSelected={setSelectRole}
|
||||
></BodyAssignment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-center">
|
||||
<BodyAssignment
|
||||
width={isMobile ? 160 : undefined}
|
||||
onlyAssigned={true}
|
||||
advanced={true}
|
||||
onRoleSelected={setSelectRole}
|
||||
></BodyAssignment>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SkipSetupWarningModal
|
||||
|
||||
@@ -10,20 +10,21 @@ import { Button } from '../../../commons/Button';
|
||||
export function MountingChoose() {
|
||||
const { l10n } = useLocalization();
|
||||
const { applyProgress, skipSetup, state } = useOnboarding();
|
||||
const [animated, setAnimated] = useState(false);
|
||||
const [skipWarning, setSkipWarning] = useState(false);
|
||||
|
||||
applyProgress(0.65);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto px-4 pb-4">
|
||||
<SkipSetupButton
|
||||
visible={!state.alonePage}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex flex-col gap-4 justify-center">
|
||||
<div className="w-[666px]">
|
||||
<div className="xs:w-10/12 xs:max-w-[666px]">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-choose_mounting')}
|
||||
</Typography>
|
||||
@@ -35,99 +36,106 @@ export function MountingChoose() {
|
||||
{l10n.getString('onboarding-choose_mounting-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classNames('h-full w-[760px] min-w-[760px]')}>
|
||||
<div className="flex flex-row gap-4 [&>div]:grow">
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-manual_mounting'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-manual_mounting-subtitle'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-manual_mounting-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid xs:grid-cols-2 w-full xs:flex-row mobile:flex-col gap-4 [&>div]:grow'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-manual_mounting'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-manual_mounting-subtitle'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-manual_mounting-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
to="/onboarding/mounting/manual"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-automatic_mounting-manual_mounting'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
to="/onboarding/mounting/manual"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-automatic_mounting-manual_mounting'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row relative',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<img
|
||||
src="/images/boxslime.png"
|
||||
className="absolute w-1/3 -right-10 -top-16"
|
||||
></img>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-auto_mounting'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-auto_mounting-subtitle'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-auto_mounting-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg p-4 flex flex-row relative',
|
||||
!state.alonePage && 'bg-background-70',
|
||||
state.alonePage && 'bg-background-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<div>
|
||||
<img
|
||||
onMouseEnter={() => setAnimated(() => true)}
|
||||
onAnimationEnd={() => setAnimated(() => false)}
|
||||
src="/images/boxslime.png"
|
||||
className={classNames(
|
||||
'absolute w-[100px] -right-2 -top-10',
|
||||
animated && 'animate-[bounce_1s_1]'
|
||||
)}
|
||||
></img>
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-auto_mounting'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="vr-accessible" italic>
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-auto_mounting-subtitle'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-choose_mounting-auto_mounting-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/mounting/auto"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-manual_mounting-auto_mounting')}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/mounting/auto"
|
||||
className="self-start mt-auto"
|
||||
state={{ alonePage: state.alonePage }}
|
||||
>
|
||||
{l10n.getString('onboarding-manual_mounting-auto_mounting')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!state.alonePage && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="self-start ml-4"
|
||||
className="self-start"
|
||||
to="/onboarding/trackers-assign"
|
||||
>
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
|
||||
@@ -19,7 +19,7 @@ function MoutingOrientationCard({
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="h-32 bg-background-60 rounded-md flex justify-between p-4 hover:bg-background-50"
|
||||
className="xs:h-32 mobile:h-20 bg-background-60 rounded-md flex justify-between p-4 hover:bg-background-50"
|
||||
>
|
||||
<div className="flex flex-col justify-center">
|
||||
<Typography variant="main-title">{orientation}</Typography>
|
||||
@@ -56,7 +56,7 @@ export function MountingSelectionMenu({
|
||||
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full bg-black bg-opacity-90 z-20'
|
||||
)}
|
||||
className={classNames(
|
||||
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10'
|
||||
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10 px-2'
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full h-full flex-col ">
|
||||
@@ -64,11 +64,11 @@ export function MountingSelectionMenu({
|
||||
{l10n.getString('mounting_selection_menu')}
|
||||
</Typography>
|
||||
<div
|
||||
className="flex w-full flex-col flex-grow items-center gap-3 justify-center"
|
||||
className="flex w-full flex-col flex-grow items-center gap-3 xs:justify-center"
|
||||
ref={refTrackers}
|
||||
style={{ height: trackersHeight - optionsHeight }}
|
||||
>
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-6 w-full">
|
||||
<div className="grid xs:grid-cols-2 xs:grid-rows-2 mobile:grid-cols-1 gap-6 w-full">
|
||||
<MoutingOrientationCard
|
||||
orientation={l10n.getString('tracker-rotation-left')}
|
||||
onClick={() => onDirectionSelected(rotationToQuatMap.LEFT)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '../../../../commons/Button';
|
||||
import { Typography } from '../../../../commons/Typography';
|
||||
import { ResetButton } from '../../../../home/ResetButton';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useBreakpoint } from '../../../../../hooks/breakpoint';
|
||||
|
||||
export function MountingResetStep({
|
||||
nextStep,
|
||||
@@ -13,6 +14,7 @@ export function MountingResetStep({
|
||||
prevStep: () => void;
|
||||
variant: 'onboarding' | 'alone';
|
||||
}) {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
return (
|
||||
@@ -38,7 +40,17 @@ export function MountingResetStep({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
{isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<img
|
||||
src="/images/mounting-reset-pose.png"
|
||||
width={125}
|
||||
alt="mounting reset ski pose"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
onClick={prevStep}
|
||||
@@ -52,13 +64,15 @@ export function MountingResetStep({
|
||||
></ResetButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<img
|
||||
src="/images/mounting-reset-pose.png"
|
||||
width={125}
|
||||
alt="mounting reset ski pose"
|
||||
/>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<img
|
||||
src="/images/mounting-reset-pose.png"
|
||||
width={125}
|
||||
alt="mounting reset ski pose"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '../../../../commons/Button';
|
||||
import { Typography } from '../../../../commons/Typography';
|
||||
import { ResetButton } from '../../../../home/ResetButton';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useBreakpoint } from '../../../../../hooks/breakpoint';
|
||||
|
||||
export function PreparationStep({
|
||||
nextStep,
|
||||
@@ -13,11 +14,12 @@ export function PreparationStep({
|
||||
prevStep: () => void;
|
||||
variant: 'onboarding' | 'alone';
|
||||
}) {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex mobile:flex-col items-center w-full">
|
||||
<div className="flex flex-col flex-grow justify-between">
|
||||
<div className="flex flex-col gap-4 max-w-sm">
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString('onboarding-automatic_mounting-preparation-title')}
|
||||
@@ -35,9 +37,16 @@ export function PreparationStep({
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-grow items-center"></div>
|
||||
<div className="flex gap-3">
|
||||
{isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<img
|
||||
src="/images/reset-pose.png"
|
||||
width={100}
|
||||
alt="Reset position"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
onClick={prevStep}
|
||||
@@ -51,9 +60,11 @@ export function PreparationStep({
|
||||
></ResetButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<img src="/images/reset-pose.png" width={90} alt="Reset position" />
|
||||
</div>
|
||||
</>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<img src="/images/reset-pose.png" width={90} alt="Reset position" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useBreakpoint } from '../../../../../hooks/breakpoint';
|
||||
import { useTrackers } from '../../../../../hooks/tracker';
|
||||
import { BodyDisplay } from '../../../../commons/BodyDisplay';
|
||||
import { Button } from '../../../../commons/Button';
|
||||
@@ -12,12 +13,13 @@ export function PutTrackersOnStep({
|
||||
nextStep: () => void;
|
||||
variant: 'alone' | 'onboarding';
|
||||
}) {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { trackers } = useTrackers();
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex mobile:flex-col items-center w-full">
|
||||
<div className="flex flex-col flex-grow gap-2">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString(
|
||||
@@ -36,8 +38,20 @@ export function PutTrackersOnStep({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
|
||||
<BodyDisplay
|
||||
trackers={trackers}
|
||||
width={150}
|
||||
dotsSize={15}
|
||||
variant="dots"
|
||||
hideUnassigned={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
to="/onboarding/mounting/choose"
|
||||
@@ -53,15 +67,17 @@ export function PutTrackersOnStep({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
|
||||
<BodyDisplay
|
||||
trackers={trackers}
|
||||
width={150}
|
||||
dotsSize={15}
|
||||
variant="dots"
|
||||
hideUnassigned={true}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
|
||||
<BodyDisplay
|
||||
trackers={trackers}
|
||||
width={150}
|
||||
dotsSize={15}
|
||||
variant="dots"
|
||||
hideUnassigned={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -250,15 +250,17 @@ export function TrackersAssignPage() {
|
||||
onClose={() => closeChokerWarning(true)}
|
||||
accept={() => closeChokerWarning(false)}
|
||||
></NeckWarningModal>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center relative">
|
||||
<div className="relative mx-4 top-4">
|
||||
<SkipSetupButton
|
||||
visible={!state.alonePage}
|
||||
modalVisible={skipWarning}
|
||||
onClick={() => setSkipWarning(true)}
|
||||
></SkipSetupButton>
|
||||
<div className="flex flex-col w-full h-full justify-center items-center">
|
||||
<div className="flex md:gap-8">
|
||||
<div className="flex flex-col max-w-sm gap-3">
|
||||
</div>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
|
||||
<div className="flex flex-col w-full overflow-y-auto px-4 xs:items-center">
|
||||
<div className="flex mobile:flex-col md:gap-8 mobile:gap-4 mobile:pb-4">
|
||||
<div className="flex flex-col xs:max-w-sm gap-3">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-assign_trackers-title')}
|
||||
</Typography>
|
||||
@@ -287,7 +289,7 @@ export function TrackersAssignPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row mt-auto">
|
||||
<div className="flex flex-row">
|
||||
{!state.alonePage && (
|
||||
<>
|
||||
<Button
|
||||
@@ -310,8 +312,9 @@ export function TrackersAssignPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
|
||||
<div className="flex flex-col rounded-xl fill-background-50">
|
||||
<BodyAssignment
|
||||
width={150}
|
||||
onlyAssigned={false}
|
||||
highlightedRoles={firstError?.affectedRoles || []}
|
||||
rolesWithErrors={rolesWithErrors}
|
||||
@@ -321,10 +324,6 @@ export function TrackersAssignPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full pb-4 flex flex-row">
|
||||
<div className="flex flex-grow gap-3"></div>
|
||||
<div className="flex gap-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<SkipSetupWarningModal
|
||||
accept={skipSetup}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function TrackerSelectionMenu({
|
||||
>
|
||||
<div className="flex w-full h-full flex-col ">
|
||||
<div className="flex w-full flex-col flex-grow items-center gap-3">
|
||||
<Typography variant="main-title" bold>
|
||||
<Typography variant="mobile-title" bold>
|
||||
{l10n.getString('tracker_selection_menu-' + BodyPart[bodyPart])}
|
||||
</Typography>
|
||||
<div className="w-full max-w-sm">
|
||||
@@ -57,7 +57,7 @@ export function TrackerSelectionMenu({
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div
|
||||
className="w-full h-full min-w-[700px] overflow-y-auto p-2 pt-0 flex flex-col gap-6"
|
||||
className="w-full h-full xs:min-w-[700px] overflow-y-auto p-2 pt-0 flex flex-col gap-6"
|
||||
ref={refTrackers}
|
||||
style={{ height: trackersHeight - optionsHeight }}
|
||||
>
|
||||
@@ -67,7 +67,7 @@ export function TrackerSelectionMenu({
|
||||
<Typography>
|
||||
{l10n.getString('tracker_selection_menu-unassigned')}
|
||||
</Typography>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid xs:grid-cols-2 mobile:grid-cols-1 gap-3">
|
||||
{unassignedTrackers.map((fd, index) => (
|
||||
<TrackerCard
|
||||
key={index}
|
||||
@@ -88,7 +88,7 @@ export function TrackerSelectionMenu({
|
||||
<Typography>
|
||||
{l10n.getString('tracker_selection_menu-assigned')}
|
||||
</Typography>
|
||||
<div className=" grid grid-cols-2 gap-3">
|
||||
<div className=" grid xs:grid-cols-2 mobile:grid-cols-1 gap-3">
|
||||
{assignedTrackers.map((fd, index) => (
|
||||
<TrackerCard
|
||||
key={index}
|
||||
@@ -106,9 +106,6 @@ export function TrackerSelectionMenu({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute px-2 pr-4 bottom-0 h-10 w-full border-b-[1px] border-background-40">
|
||||
<div className="w-full h-full bg-gradient-to-b from-transparent to-black opacity-50"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,106 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useLayout } from '../../hooks/layout';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { useElemSize, useLayout } from '../../hooks/layout';
|
||||
import { Navbar } from '../Navbar';
|
||||
import { TopBar } from '../TopBar';
|
||||
import { SettingsSidebar } from './SettingsSidebar';
|
||||
import { useBreakpoint } from '../../hooks/breakpoint';
|
||||
import { Dropdown } from '../commons/Dropdown';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export function SettingSelectorMobile() {
|
||||
const { l10n } = useLocalization();
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const links: { label: string; value: { url: string; scrollTo?: string } }[] =
|
||||
[
|
||||
{
|
||||
label: l10n.getString('settings-sidebar-general'),
|
||||
value: { url: '/settings/trackers', scrollTo: 'steamvr' },
|
||||
},
|
||||
{
|
||||
label: l10n.getString('settings-sidebar-osc_router'),
|
||||
value: { url: '/settings/osc/router', scrollTo: 'router' },
|
||||
},
|
||||
{
|
||||
label: l10n.getString('settings-sidebar-osc_trackers'),
|
||||
value: { url: '/settings/osc/vrchat', scrollTo: 'vrchat' },
|
||||
},
|
||||
{
|
||||
label: 'VMC',
|
||||
value: { url: '/settings/osc/vmc', scrollTo: 'vmc' },
|
||||
},
|
||||
{
|
||||
label: l10n.getString('settings-sidebar-serial'),
|
||||
value: { url: '/settings/serial' },
|
||||
},
|
||||
];
|
||||
|
||||
const { control, watch, handleSubmit, setValue } = useForm<{
|
||||
link: string;
|
||||
}>({
|
||||
defaultValues: { link: links[0].value.url },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// This works because the component gets mounted/unmounted when switching beween desktop or mobile layout
|
||||
setValue('link', pathname, { shouldDirty: false, shouldTouch: false });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(onSubmit)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const onSubmit = ({ link }: { link: string }) => {
|
||||
const item = links.find(({ value: { url } }) => url === link);
|
||||
|
||||
if (!item) return;
|
||||
navigate(item.value.url, { state: { scrollTo: item.value.scrollTo } });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed top-12 z-50 px-4 w-full">
|
||||
<Dropdown
|
||||
control={control}
|
||||
display="block"
|
||||
items={links.map(({ label, value: { url: value } }) => ({
|
||||
label,
|
||||
value,
|
||||
}))}
|
||||
variant="tertiary"
|
||||
direction="down"
|
||||
// There is always an option selected placholder is not used
|
||||
placeholder=""
|
||||
name="link"
|
||||
></Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsLayoutRoute({ children }: { children: ReactNode }) {
|
||||
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
|
||||
|
||||
const { height, ref: navRef } = useElemSize<HTMLDivElement>();
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
return (
|
||||
<>
|
||||
<TopBar></TopBar>
|
||||
<div ref={ref} className="flex-grow" style={{ height: layoutHeight }}>
|
||||
<div className="flex h-full pb-3">
|
||||
<Navbar></Navbar>
|
||||
<div className="flex h-full xs:pb-3">
|
||||
{!isMobile && <Navbar></Navbar>}
|
||||
<div className="h-full w-full gap-2 flex">
|
||||
<SettingsSidebar></SettingsSidebar>
|
||||
<div className="w-full flex flex-col overflow-y-auto pr-1 mr-1">
|
||||
{children}
|
||||
{!isMobile && <SettingsSidebar></SettingsSidebar>}
|
||||
<div className="w-full flex flex-col">
|
||||
{isMobile && <SettingSelectorMobile></SettingSelectorMobile>}
|
||||
<div
|
||||
className="flex flex-col overflow-y-auto xs:pr-1 xs:mr-1 mobile:pt-7 pb-3"
|
||||
style={{ minHeight: layoutHeight - height }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div ref={navRef}>{isMobile && <Navbar></Navbar>}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export function SettingsPageLayout({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
children: ReactNode;
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const pageRef = useRef<HTMLDivElement | null>(null);
|
||||
const { state } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const typedState: { scrollTo: string } = state;
|
||||
if (!pageRef.current || !typedState || !typedState.scrollTo) {
|
||||
return;
|
||||
}
|
||||
const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`);
|
||||
if (elem) {
|
||||
elem.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<div ref={pageRef} className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsPagePaneLayout({
|
||||
children,
|
||||
className,
|
||||
icon,
|
||||
@@ -13,13 +42,13 @@ export function SettingsPageLayout({
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-background-70 rounded-lg p-8 flex gap-8 w-full',
|
||||
'mobile:scroll-mt-7 bg-background-70 rounded-lg px-4 py-8 flex xs:gap-4 w-full relative',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="w-10 h-10 bg-accent-background-40 flex justify-center items-center rounded-full fill-background-10">
|
||||
<div className="flex mobile:absolute mobile:right-4">
|
||||
<div className=" w-10 h-10 bg-accent-background-40 flex justify-center items-center rounded-full fill-background-10">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,13 +74,9 @@ export function SettingsSidebar() {
|
||||
<SettingsLink to="/settings/osc/router" scrollTo="router">
|
||||
{l10n.getString('settings-sidebar-osc_router')}
|
||||
</SettingsLink>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/osc/vrchat" scrollTo="vrchat">
|
||||
{l10n.getString('settings-sidebar-osc_trackers')}
|
||||
</SettingsLink>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<SettingsLink to="/settings/osc/vmc" scrollTo="vmc">
|
||||
VMC
|
||||
</SettingsLink>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
OSCRouterSettingsT,
|
||||
@@ -15,7 +14,10 @@ import { CheckBox } from '../../commons/Checkbox';
|
||||
import { RouterIcon } from '../../commons/icon/RouterIcon';
|
||||
import { Input } from '../../commons/Input';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
import { SettingsPageLayout } from '../SettingsPageLayout';
|
||||
import {
|
||||
SettingsPageLayout,
|
||||
SettingsPagePaneLayout,
|
||||
} from '../SettingsPageLayout';
|
||||
|
||||
interface OSCRouterSettingsForm {
|
||||
router: {
|
||||
@@ -42,8 +44,6 @@ const defaultValues = {
|
||||
export function OSCRouterSettings() {
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const { state } = useLocation();
|
||||
const pageRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
const { reset, control, watch, handleSubmit } =
|
||||
useForm<OSCRouterSettingsForm>({
|
||||
@@ -96,125 +96,115 @@ export function OSCRouterSettings() {
|
||||
reset(formData);
|
||||
});
|
||||
|
||||
// Handle scrolling to selected page
|
||||
useEffect(() => {
|
||||
const typedState: { scrollTo: string } = state as any;
|
||||
if (!pageRef.current || !typedState || !typedState.scrollTo) {
|
||||
return;
|
||||
}
|
||||
const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`);
|
||||
if (elem) {
|
||||
elem.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-2 w-full" ref={pageRef}>
|
||||
<SettingsPageLayout icon={<RouterIcon></RouterIcon>} id="router">
|
||||
<>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-osc-router')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-router-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-router-enable')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-router-enable-description')}
|
||||
<SettingsPageLayout>
|
||||
<form className="flex flex-col gap-2 w-full">
|
||||
<SettingsPagePaneLayout icon={<RouterIcon></RouterIcon>} id="router">
|
||||
<>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-osc-router')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="router.oscSettings.enabled"
|
||||
label={l10n.getString('settings-osc-router-enable-label')}
|
||||
/>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-router-network')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-router-network-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<Localized
|
||||
id="settings-osc-router-network-port_in"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-router-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-router-enable')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-router-enable-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
name="router.oscSettings.portIn"
|
||||
placeholder="9002"
|
||||
name="router.oscSettings.enabled"
|
||||
label={l10n.getString('settings-osc-router-enable-label')}
|
||||
/>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-router-network')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-router-network-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<Localized
|
||||
id="settings-osc-router-network-port_in"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
name="router.oscSettings.portIn"
|
||||
placeholder="9002"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="settings-osc-router-network-port_out"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
name="router.oscSettings.portOut"
|
||||
placeholder="9000"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-router-network-address')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'settings-osc-router-network-address-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<Input
|
||||
type="text"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern:
|
||||
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
|
||||
}}
|
||||
name="router.oscSettings.address"
|
||||
placeholder={l10n.getString(
|
||||
'settings-osc-router-network-address-placeholder'
|
||||
)}
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="settings-osc-router-network-port_out"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
name="router.oscSettings.portOut"
|
||||
placeholder="9000"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-router-network-address')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'settings-osc-router-network-address-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<Input
|
||||
type="text"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern:
|
||||
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
|
||||
}}
|
||||
name="router.oscSettings.address"
|
||||
placeholder={l10n.getString(
|
||||
'settings-osc-router-network-address-placeholder'
|
||||
)}
|
||||
label=""
|
||||
></Input>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPageLayout>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPagePaneLayout>
|
||||
</form>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Typography } from '../../commons/Typography';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { BaseModal } from '../../commons/BaseModal';
|
||||
import { WarningBox } from '../../commons/TipBox';
|
||||
import { useBreakpoint } from '../../../hooks/breakpoint';
|
||||
|
||||
export interface SerialForm {
|
||||
port: string;
|
||||
@@ -32,8 +33,8 @@ export function Serial() {
|
||||
layoutWidth,
|
||||
ref: consoleRef,
|
||||
} = useLayout<HTMLDivElement>();
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
const { state } = useLocation();
|
||||
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
@@ -203,7 +204,7 @@ export function Serial() {
|
||||
ref={consoleRef}
|
||||
className="overflow-x-auto overflow-y-auto"
|
||||
style={{
|
||||
height: layoutHeight - height - 30,
|
||||
height: layoutHeight - height - 30 - (isMobile ? 88 : 0),
|
||||
width: layoutWidth - 24,
|
||||
}}
|
||||
>
|
||||
@@ -217,7 +218,7 @@ export function Serial() {
|
||||
</div>
|
||||
<div className="" ref={toolbarRef}>
|
||||
<div className="border-t-2 pt-2 border-background-60 border-solid m-2 gap-2 flex flex-row">
|
||||
<div className="flex flex-grow gap-2">
|
||||
<div className="flex flex-grow gap-2 mobile:grid mobile:grid-cols-2 mobile:grid-rows-2">
|
||||
<Button variant="quaternary" onClick={reboot}>
|
||||
{l10n.getString('settings-serial-reboot')}
|
||||
</Button>
|
||||
@@ -230,19 +231,34 @@ export function Serial() {
|
||||
<Button variant="quaternary" onClick={getInfos}>
|
||||
{l10n.getString('settings-serial-get_infos')}
|
||||
</Button>
|
||||
{isMobile && (
|
||||
<Dropdown
|
||||
control={control}
|
||||
name="port"
|
||||
display="block"
|
||||
placeholder={l10n.getString(
|
||||
'settings-serial-serial_select'
|
||||
)}
|
||||
items={serialDevices.map((device) => ({
|
||||
label: device.name?.toString() || 'error',
|
||||
value: device.port?.toString() || 'error',
|
||||
}))}
|
||||
></Dropdown>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
{!isMobile && (
|
||||
<Dropdown
|
||||
control={control}
|
||||
name="port"
|
||||
display="fit"
|
||||
placeholder={l10n.getString('settings-serial-serial_select')}
|
||||
items={serialDevices.map((device) => ({
|
||||
label: device.name?.toString() || 'error',
|
||||
value: device.port?.toString() || 'error',
|
||||
}))}
|
||||
></Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
RpcMessage,
|
||||
@@ -17,7 +16,10 @@ import { VMCIcon } from '../../commons/icon/VMCIcon';
|
||||
import { Input } from '../../commons/Input';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
import { magic } from '../../utils/formatting';
|
||||
import { SettingsPageLayout } from '../SettingsPageLayout';
|
||||
import {
|
||||
SettingsPageLayout,
|
||||
SettingsPagePaneLayout,
|
||||
} from '../SettingsPageLayout';
|
||||
|
||||
interface VMCSettingsForm {
|
||||
vmc: {
|
||||
@@ -47,8 +49,6 @@ const defaultValues = {
|
||||
export function VMCSettings() {
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const { state } = useLocation();
|
||||
const pageRef = useRef<HTMLFormElement | null>(null);
|
||||
const [modelName, setModelName] = useState<string | null>(null);
|
||||
const [flashLoaded, setFlashLoaded] = useState(false);
|
||||
|
||||
@@ -119,170 +119,162 @@ export function VMCSettings() {
|
||||
reset(formData);
|
||||
});
|
||||
|
||||
// Handle scrolling to selected page
|
||||
useEffect(() => {
|
||||
const typedState: { scrollTo: string } = state as any;
|
||||
if (!pageRef.current || !typedState || !typedState.scrollTo) {
|
||||
return;
|
||||
}
|
||||
const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`);
|
||||
if (elem) {
|
||||
elem.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-2 w-full" ref={pageRef}>
|
||||
<SettingsPageLayout icon={<VMCIcon></VMCIcon>} id="vmc">
|
||||
<>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-osc-vmc')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vmc-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-enable')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-enable-description')}
|
||||
<SettingsPageLayout>
|
||||
<form className="flex flex-col gap-2 w-full">
|
||||
<SettingsPagePaneLayout icon={<VMCIcon></VMCIcon>} id="vmc">
|
||||
<>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-osc-vmc')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vmc.oscSettings.enabled"
|
||||
label={l10n.getString('settings-osc-vmc-enable-label')}
|
||||
/>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-network')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vmc-network-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<Localized
|
||||
id="settings-osc-vmc-network-port_in"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vmc-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-enable')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-enable-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vmc.oscSettings.portIn"
|
||||
rules={{ required: true }}
|
||||
placeholder="9002"
|
||||
name="vmc.oscSettings.enabled"
|
||||
label={l10n.getString('settings-osc-vmc-enable-label')}
|
||||
/>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-network')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vmc-network-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<Localized
|
||||
id="settings-osc-vmc-network-port_in"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
name="vmc.oscSettings.portIn"
|
||||
rules={{ required: true }}
|
||||
placeholder="9002"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="settings-osc-vmc-network-port_out"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
name="vmc.oscSettings.portOut"
|
||||
rules={{ required: true }}
|
||||
placeholder="9000"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-network-address')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-network-address-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<Input
|
||||
type="text"
|
||||
control={control}
|
||||
name="vmc.oscSettings.address"
|
||||
rules={{
|
||||
required: true,
|
||||
pattern:
|
||||
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
|
||||
}}
|
||||
placeholder={l10n.getString(
|
||||
'settings-osc-vmc-network-address-placeholder'
|
||||
)}
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="settings-osc-vmc-network-port_out"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-vrm')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-vrm-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color={flashLoaded ? 'primary' : 'secondary'}>
|
||||
{modelName === null
|
||||
? l10n.getString('settings-osc-vmc-vrm-model_unloaded')
|
||||
: l10n.getString('settings-osc-vmc-vrm-model_loaded', {
|
||||
name: modelName,
|
||||
titled: (!!modelName).toString(),
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<FileInput
|
||||
control={control}
|
||||
name="vmc.oscSettings.portOut"
|
||||
rules={{ required: true }}
|
||||
placeholder="9000"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-network-address')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-network-address-description')}
|
||||
name="vmc.vrmJson"
|
||||
rules={{
|
||||
required: false,
|
||||
}}
|
||||
value="help"
|
||||
label="settings-osc-vmc-vrm-file_select"
|
||||
accept="model/gltf-binary, model/gltf+json, model/vrml, .vrm, .glb, .gltf"
|
||||
></FileInput>
|
||||
{/* For some reason, linux (GNOME) is detecting the VRM file is a VRML */}
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-anchor_hip')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<Input
|
||||
type="text"
|
||||
control={control}
|
||||
name="vmc.oscSettings.address"
|
||||
rules={{
|
||||
required: true,
|
||||
pattern:
|
||||
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
|
||||
}}
|
||||
placeholder={l10n.getString(
|
||||
'settings-osc-vmc-network-address-placeholder'
|
||||
)}
|
||||
label=""
|
||||
></Input>
|
||||
</div>
|
||||
<Typography bold>{l10n.getString('settings-osc-vmc-vrm')}</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-vrm-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color={flashLoaded ? 'primary' : 'secondary'}>
|
||||
{modelName === null
|
||||
? l10n.getString('settings-osc-vmc-vrm-model_unloaded')
|
||||
: l10n.getString('settings-osc-vmc-vrm-model_loaded', {
|
||||
name: modelName,
|
||||
titled: (!!modelName).toString(),
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<FileInput
|
||||
control={control}
|
||||
name="vmc.vrmJson"
|
||||
rules={{
|
||||
required: false,
|
||||
}}
|
||||
value="help"
|
||||
label="settings-osc-vmc-vrm-file_select"
|
||||
accept="model/gltf-binary, model/gltf+json, model/vrml, .vrm, .glb, .gltf"
|
||||
></FileInput>
|
||||
{/* For some reason, linux (GNOME) is detecting the VRM file is a VRML */}
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vmc-anchor_hip')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-anchor_hip-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vmc.anchorHip"
|
||||
label={l10n.getString('settings-osc-vmc-anchor_hip-label')}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPageLayout>
|
||||
</form>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vmc-anchor_hip-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vmc.anchorHip"
|
||||
label={l10n.getString('settings-osc-vmc-anchor_hip-label')}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPagePaneLayout>
|
||||
</form>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
ChangeSettingsRequestT,
|
||||
OSCSettingsT,
|
||||
@@ -16,7 +15,10 @@ import { CheckBox } from '../../commons/Checkbox';
|
||||
import { VRCIcon } from '../../commons/icon/VRCIcon';
|
||||
import { Input } from '../../commons/Input';
|
||||
import { Typography } from '../../commons/Typography';
|
||||
import { SettingsPageLayout } from '../SettingsPageLayout';
|
||||
import {
|
||||
SettingsPageLayout,
|
||||
SettingsPagePaneLayout,
|
||||
} from '../SettingsPageLayout';
|
||||
|
||||
interface VRCOSCSettingsForm {
|
||||
vrchat: {
|
||||
@@ -61,8 +63,6 @@ const defaultValues = {
|
||||
export function VRCOSCSettings() {
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const { state } = useLocation();
|
||||
const pageRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
const { reset, control, watch, handleSubmit } = useForm<VRCOSCSettingsForm>({
|
||||
defaultValues: defaultValues,
|
||||
@@ -120,173 +120,165 @@ export function VRCOSCSettings() {
|
||||
reset(formData);
|
||||
});
|
||||
|
||||
// Handle scrolling to selected page
|
||||
useEffect(() => {
|
||||
const typedState: { scrollTo: string } = state as any;
|
||||
if (!pageRef.current || !typedState || !typedState.scrollTo) {
|
||||
return;
|
||||
}
|
||||
const elem = pageRef.current.querySelector(`#${typedState.scrollTo}`);
|
||||
if (elem) {
|
||||
elem.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-2 w-full" ref={pageRef}>
|
||||
<SettingsPageLayout icon={<VRCIcon></VRCIcon>} id="vrchat">
|
||||
<>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-osc-vrchat')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vrchat-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vrchat-enable')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vrchat-enable-description')}
|
||||
<SettingsPageLayout>
|
||||
<form className="flex flex-col gap-2 w-full">
|
||||
<SettingsPagePaneLayout icon={<VRCIcon></VRCIcon>} id="vrchat">
|
||||
<>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-osc-vrchat')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.oscSettings.enabled"
|
||||
label={l10n.getString('settings-osc-vrchat-enable-label')}
|
||||
/>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vrchat-network')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vrchat-network-description')}
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
<>
|
||||
{l10n
|
||||
.getString('settings-osc-vrchat-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vrchat-enable')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<Localized
|
||||
id="settings-osc-vrchat-network-port_in"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vrchat-enable-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.oscSettings.portIn"
|
||||
rules={{ required: true }}
|
||||
placeholder="9001"
|
||||
name="vrchat.oscSettings.enabled"
|
||||
label={l10n.getString('settings-osc-vrchat-enable-label')}
|
||||
/>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vrchat-network')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('settings-osc-vrchat-network-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<Localized
|
||||
id="settings-osc-vrchat-network-port_in"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
name="vrchat.oscSettings.portIn"
|
||||
rules={{ required: true }}
|
||||
placeholder="9001"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="settings-osc-vrchat-network-port_out"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
control={control}
|
||||
name="vrchat.oscSettings.portOut"
|
||||
rules={{ required: true }}
|
||||
placeholder="9000"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vrchat-network-address')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'settings-osc-vrchat-network-address-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<Input
|
||||
type="text"
|
||||
control={control}
|
||||
name="vrchat.oscSettings.address"
|
||||
rules={{
|
||||
required: true,
|
||||
pattern:
|
||||
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
|
||||
}}
|
||||
placeholder={l10n.getString(
|
||||
'settings-osc-vrchat-network-address-placeholder'
|
||||
)}
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="settings-osc-vrchat-network-port_out"
|
||||
attrs={{ placeholder: true, label: true }}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vrchat-network-trackers')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.oscSettings.portOut"
|
||||
rules={{ required: true }}
|
||||
placeholder="9000"
|
||||
label=""
|
||||
></Input>
|
||||
</Localized>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vrchat-network-address')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'settings-osc-vrchat-network-address-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid gap-3 pb-5">
|
||||
<Input
|
||||
type="text"
|
||||
control={control}
|
||||
name="vrchat.oscSettings.address"
|
||||
rules={{
|
||||
required: true,
|
||||
pattern:
|
||||
/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/i,
|
||||
}}
|
||||
placeholder={l10n.getString(
|
||||
'settings-osc-vrchat-network-address-placeholder'
|
||||
)}
|
||||
label=""
|
||||
></Input>
|
||||
</div>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-osc-vrchat-network-trackers')}
|
||||
</Typography>
|
||||
<div className="flex flex-col pb-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-description'
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 pb-5">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.trackers.chest"
|
||||
label={l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-chest'
|
||||
)}
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.trackers.waist"
|
||||
label={l10n.getString('settings-osc-vrchat-network-trackers-hip')}
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.trackers.knees"
|
||||
label={l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-knees'
|
||||
)}
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.trackers.feet"
|
||||
label={l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-feet'
|
||||
)}
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.trackers.elbows"
|
||||
label={l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-elbows'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPageLayout>
|
||||
</form>
|
||||
name="vrchat.trackers.chest"
|
||||
label={l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-chest'
|
||||
)}
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.trackers.waist"
|
||||
label={l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-hip'
|
||||
)}
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.trackers.knees"
|
||||
label={l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-knees'
|
||||
)}
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.trackers.feet"
|
||||
label={l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-feet'
|
||||
)}
|
||||
/>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="vrchat.trackers.elbows"
|
||||
label={l10n.getString(
|
||||
'settings-osc-vrchat-network-trackers-elbows'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPagePaneLayout>
|
||||
</form>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { BodyAssignment } from '../onboarding/BodyAssignment';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { NeckWarningModal } from '../onboarding/NeckWarningModal';
|
||||
import { useChokerWarning } from '../../hooks/choker-warning';
|
||||
import { useBreakpoint } from '../../hooks/breakpoint';
|
||||
|
||||
export function SingleTrackerBodyAssignmentMenu({
|
||||
isOpen,
|
||||
@@ -19,6 +20,7 @@ export function SingleTrackerBodyAssignmentMenu({
|
||||
onClose: () => void;
|
||||
onRoleSelected: (role: BodyPart) => void;
|
||||
}) {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { l10n } = useLocalization();
|
||||
const { control, watch } = useForm<{ advanced: boolean }>({
|
||||
defaultValues: { advanced: false },
|
||||
@@ -41,51 +43,50 @@ export function SingleTrackerBodyAssignmentMenu({
|
||||
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full justify-center bg-black bg-opacity-90 z-20'
|
||||
)}
|
||||
className={classNames(
|
||||
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10'
|
||||
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-12 z-10 overflow-y-auto'
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full h-full flex-col gap-10 px-3">
|
||||
<div className="flex flex-col w-full h-full justify-center items-center">
|
||||
<div className="flex gap-8">
|
||||
<div className="flex flex-col max-w-sm gap-3">
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString('body_assignment_menu')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('body_assignment_menu-description')}
|
||||
</Typography>
|
||||
<CheckBox
|
||||
control={control}
|
||||
label={l10n.getString(
|
||||
'body_assignment_menu-show_advanced_locations'
|
||||
)}
|
||||
name="advanced"
|
||||
variant="toggle"
|
||||
></CheckBox>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to="/onboarding/trackers-assign"
|
||||
state={{ alonePage: true }}
|
||||
>
|
||||
{l10n.getString('body_assignment_menu-manage_trackers')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex xs:flex-row h-full xs:gap-8 mobile:flex-col xs:justify-center items-center">
|
||||
<div className="flex flex-col xs:max-w-sm gap-3">
|
||||
<Typography variant="mobile-title" bold>
|
||||
{l10n.getString('body_assignment_menu')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('body_assignment_menu-description')}
|
||||
</Typography>
|
||||
<CheckBox
|
||||
control={control}
|
||||
label={l10n.getString(
|
||||
'body_assignment_menu-show_advanced_locations'
|
||||
)}
|
||||
name="advanced"
|
||||
variant="toggle"
|
||||
></CheckBox>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="secondary"
|
||||
to="/onboarding/trackers-assign"
|
||||
state={{ alonePage: true }}
|
||||
>
|
||||
{l10n.getString('body_assignment_menu-manage_trackers')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow gap-3 rounded-xl fill-background-50">
|
||||
<BodyAssignment
|
||||
onlyAssigned={false}
|
||||
advanced={advanced}
|
||||
onRoleSelected={tryOpenChokerWarning}
|
||||
></BodyAssignment>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => onRoleSelected(BodyPart.NONE)}
|
||||
>
|
||||
{l10n.getString('body_assignment_menu-unassign_tracker')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col xs:flex-grow gap-3 rounded-xl fill-background-50 py-2">
|
||||
<BodyAssignment
|
||||
width={isMobile ? 160 : undefined}
|
||||
onlyAssigned={false}
|
||||
advanced={advanced}
|
||||
onRoleSelected={tryOpenChokerWarning}
|
||||
></BodyAssignment>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => onRoleSelected(BodyPart.NONE)}
|
||||
>
|
||||
{l10n.getString('body_assignment_menu-unassign_tracker')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ function TrackerSmol({
|
||||
const trackerName = useName();
|
||||
|
||||
return (
|
||||
<div className="flex rounded-md py-3 px-5 w-full gap-4 h-16">
|
||||
<div className="flex rounded-md py-3 px-4 w-full gap-4 h-16">
|
||||
<div className="flex flex-col justify-center items-center fill-background-10">
|
||||
<BodyPartIcon bodyPart={tracker.info?.bodyPart}></BodyPartIcon>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function TrackerPartCard({
|
||||
(showCard && (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col gap-1 control w-32 hover:bg-background-50 px-2 py-1 rounded-md relative',
|
||||
'flex flex-col gap-1 control xs:w-32 hover:bg-background-50 px-2 py-1 rounded-md relative',
|
||||
direction === 'left' ? 'items-start' : 'items-end'
|
||||
)}
|
||||
id={BodyPart[role]}
|
||||
|
||||
@@ -165,7 +165,7 @@ export function TrackerSettingsPage() {
|
||||
onClose={() => setSelectRotation(false)}
|
||||
onDirectionSelected={onDirectionSelected}
|
||||
></MountingSelectionMenu>
|
||||
<div className="flex gap-2 md:h-full max-md:flex-wrap md:flex-row ">
|
||||
<div className="flex gap-2 md:h-full max-md:flex-wrap md:flex-row xs:flex-col mobile:flex-col">
|
||||
<div className="flex flex-col w-full md:max-w-xs gap-2">
|
||||
{tracker && (
|
||||
<TrackerCard
|
||||
|
||||
9
gui/src/components/vr-mode/VRModePage.tsx
Normal file
9
gui/src/components/vr-mode/VRModePage.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { WidgetsComponent } from '../WidgetsComponent';
|
||||
|
||||
export function VRModePage() {
|
||||
return (
|
||||
<div className="p-2 flex flex-col gap-2 h-full">
|
||||
<WidgetsComponent></WidgetsComponent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
gui/src/hooks/breakpoint.ts
Normal file
28
gui/src/hooks/breakpoint.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import resolveConfig from 'tailwindcss/resolveConfig';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import tailwindConfig from '../../tailwind.config';
|
||||
|
||||
const fullConfig = resolveConfig(tailwindConfig as any);
|
||||
const breakpoints = tailwindConfig.theme.screens;
|
||||
|
||||
type BreakpointKey = keyof typeof breakpoints;
|
||||
|
||||
export function useBreakpoint<K extends BreakpointKey>(breakpointKey: K) {
|
||||
// FIXME There is a flickering issue caused by this, because isMobile is not resolved fast enough
|
||||
// one solution would be to have this solved only once on the appProvider and reuse the value all the time
|
||||
const bool = useMediaQuery({
|
||||
query: fullConfig.theme.screens[breakpointKey].raw
|
||||
? fullConfig.theme.screens[breakpointKey].raw
|
||||
: `(min-width: ${fullConfig.theme.screens[breakpointKey]})`,
|
||||
});
|
||||
const capitalizedKey =
|
||||
breakpointKey.toString()[0].toUpperCase() + breakpointKey.toString().substring(1);
|
||||
type Key = `is${Capitalize<K>}`;
|
||||
return {
|
||||
[`is${capitalizedKey}`]: bool,
|
||||
} as Record<Key, boolean>;
|
||||
}
|
||||
|
||||
export function useIsTauri() {
|
||||
return !!window.__TAURI_METADATA__;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MutableRefObject, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { MutableRefObject, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
export function useLayout<T extends HTMLElement>() {
|
||||
const [layoutHeight, setLayoutHeigt] = useState(window.innerHeight);
|
||||
@@ -47,7 +47,7 @@ export function useElemSize<T extends HTMLElement>(
|
||||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current) {
|
||||
observer.current.observe(ref.current);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
body {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: 'poppins', sans-serif, 'Twemoji Chromium', emoji;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
user-select: none;
|
||||
background: theme('colors.background.20');
|
||||
|
||||
|
||||
2
gui/src/vite-env.d.ts
vendored
2
gui/src/vite-env.d.ts
vendored
@@ -4,3 +4,5 @@
|
||||
declare const __COMMIT_HASH__: string;
|
||||
declare const __VERSION_TAG__: string;
|
||||
declare const __GIT_CLEAN__: boolean;
|
||||
|
||||
declare module 'tailwind-gradient-mask-image';
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const plugin = require('tailwindcss/plugin');
|
||||
import plugin from 'tailwindcss/plugin'
|
||||
import forms from '@tailwindcss/forms'
|
||||
import gradient from 'tailwind-gradient-mask-image'
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const rem = (pt) => `${pt / 16}rem`;
|
||||
|
||||
const rem = (pt: number) => `${pt / 16}rem`;
|
||||
|
||||
const colors = {
|
||||
'blue-gray': {
|
||||
@@ -151,10 +155,11 @@ const colors = {
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
const config = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
screens: {
|
||||
mobile: { raw: 'not (min-width: 800px)' },
|
||||
xs: '800px',
|
||||
sm: '900px',
|
||||
md: '1100px',
|
||||
@@ -197,7 +202,7 @@ module.exports = {
|
||||
DEFAULT: rem(12),
|
||||
},
|
||||
fontWeight: {
|
||||
DEFAULT: 500,
|
||||
DEFAULT: '500',
|
||||
},
|
||||
color: {
|
||||
DEFAULT: 'rgb(var(--default-color), <alpha-value>)',
|
||||
@@ -227,10 +232,11 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('tailwind-gradient-mask-image'),
|
||||
forms,
|
||||
gradient,
|
||||
plugin(function ({ addUtilities }) {
|
||||
const textConfig = (fontSize, fontWeight) => ({
|
||||
|
||||
const textConfig = (fontSize: any, fontWeight: any) => ({
|
||||
fontSize,
|
||||
fontWeight,
|
||||
});
|
||||
@@ -245,4 +251,6 @@ module.exports = {
|
||||
});
|
||||
}),
|
||||
],
|
||||
};
|
||||
} satisfies Config
|
||||
|
||||
export default config;
|
||||
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": false,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
76
package-lock.json
generated
76
package-lock.json
generated
@@ -45,6 +45,7 @@
|
||||
"react-dom": "^18.0.0",
|
||||
"react-hook-form": "^7.29.0",
|
||||
"react-modal": "3.15.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"semver": "^7.5.0",
|
||||
"solarxr-protocol": "file:../solarxr-protocol",
|
||||
@@ -4688,6 +4689,11 @@
|
||||
"postcss": "^8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/css-mediaquery": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
|
||||
"integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q=="
|
||||
},
|
||||
"node_modules/css-prefers-color-scheme": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz",
|
||||
@@ -6308,6 +6314,11 @@
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/hyphenate-style-name": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
|
||||
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
|
||||
},
|
||||
"node_modules/identity-obj-proxy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
|
||||
@@ -7032,6 +7043,14 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/matchmediaquery": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz",
|
||||
"integrity": "sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==",
|
||||
"dependencies": {
|
||||
"css-mediaquery": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/memfs": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz",
|
||||
@@ -8609,6 +8628,23 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-responsive": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz",
|
||||
"integrity": "sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==",
|
||||
"dependencies": {
|
||||
"hyphenate-style-name": "^1.0.0",
|
||||
"matchmediaquery": "^0.3.0",
|
||||
"prop-types": "^15.6.1",
|
||||
"shallow-equal": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.7.0.tgz",
|
||||
@@ -8957,6 +8993,11 @@
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shallow-equal": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
|
||||
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -13030,6 +13071,11 @@
|
||||
"postcss-selector-parser": "^6.0.9"
|
||||
}
|
||||
},
|
||||
"css-mediaquery": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
|
||||
"integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q=="
|
||||
},
|
||||
"css-prefers-color-scheme": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz",
|
||||
@@ -14208,6 +14254,11 @@
|
||||
"integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
|
||||
"dev": true
|
||||
},
|
||||
"hyphenate-style-name": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
|
||||
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
|
||||
},
|
||||
"identity-obj-proxy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
|
||||
@@ -14716,6 +14767,14 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"matchmediaquery": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz",
|
||||
"integrity": "sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==",
|
||||
"requires": {
|
||||
"css-mediaquery": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"memfs": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz",
|
||||
@@ -15708,6 +15767,17 @@
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
|
||||
"integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ=="
|
||||
},
|
||||
"react-responsive": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz",
|
||||
"integrity": "sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==",
|
||||
"requires": {
|
||||
"hyphenate-style-name": "^1.0.0",
|
||||
"matchmediaquery": "^0.3.0",
|
||||
"prop-types": "^15.6.1",
|
||||
"shallow-equal": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.7.0.tgz",
|
||||
@@ -15938,6 +16008,11 @@
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"shallow-equal": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
|
||||
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -16030,6 +16105,7 @@
|
||||
"react-dom": "^18.0.0",
|
||||
"react-hook-form": "^7.29.0",
|
||||
"react-modal": "3.15.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"semver": "^7.5.0",
|
||||
"solarxr-protocol": "file:../solarxr-protocol",
|
||||
|
||||
Reference in New Issue
Block a user