mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
359 lines
12 KiB
TypeScript
359 lines
12 KiB
TypeScript
import { Localized, useLocalization } from '@fluent/react';
|
|
import classNames from 'classnames';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
RpcMessage,
|
|
StartWifiProvisioningRequestT,
|
|
StatusData,
|
|
StopWifiProvisioningRequestT,
|
|
WifiProvisioningStatus,
|
|
WifiProvisioningStatusResponseT,
|
|
} from 'solarxr-protocol';
|
|
import { useOnboarding } from '@/hooks/onboarding';
|
|
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
|
import { ArrowLink } from '@/components/commons/ArrowLink';
|
|
import { Button } from '@/components/commons/Button';
|
|
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
|
import { ProgressBar } from '@/components/commons/ProgressBar';
|
|
import { TipBox, WarningBox } from '@/components/commons/TipBox';
|
|
import { Typography } from '@/components/commons/Typography';
|
|
import { TrackerCard } from '@/components/tracker/TrackerCard';
|
|
import { useIsRestCalibrationTrackers } from '@/hooks/imu-logic';
|
|
import './ConnectTracker.scss';
|
|
import { useAtomValue } from 'jotai';
|
|
import { connectedIMUTrackersAtom } from '@/store/app-store';
|
|
import { BaseModal } from '@/components/commons/BaseModal';
|
|
import { parseStatusToLocale, useStatusContext } from '@/hooks/status-system';
|
|
import { A } from '@/components/commons/A';
|
|
import { CONNECT_TRACKER } from '@/utils/tauri';
|
|
|
|
const statusLabelMap = {
|
|
[WifiProvisioningStatus.NONE]:
|
|
'onboarding-connect_tracker-connection_status-none',
|
|
[WifiProvisioningStatus.SERIAL_INIT]:
|
|
'onboarding-connect_tracker-connection_status-serial_init',
|
|
[WifiProvisioningStatus.OBTAINING_MAC_ADDRESS]:
|
|
'onboarding-connect_tracker-connection_status-obtaining_mac_address',
|
|
[WifiProvisioningStatus.PROVISIONING]:
|
|
'onboarding-connect_tracker-connection_status-provisioning',
|
|
[WifiProvisioningStatus.CONNECTING]:
|
|
'onboarding-connect_tracker-connection_status-connecting',
|
|
[WifiProvisioningStatus.LOOKING_FOR_SERVER]:
|
|
'onboarding-connect_tracker-connection_status-looking_for_server',
|
|
[WifiProvisioningStatus.DONE]:
|
|
'onboarding-connect_tracker-connection_status-done',
|
|
[WifiProvisioningStatus.CONNECTION_ERROR]:
|
|
'onboarding-connect_tracker-connection_status-connection_error',
|
|
[WifiProvisioningStatus.COULD_NOT_FIND_SERVER]:
|
|
'onboarding-connect_tracker-connection_status-could_not_find_server',
|
|
[WifiProvisioningStatus.NO_SERIAL_LOGS_ERROR]:
|
|
'onboarding-connect_tracker-connection_status-no_serial_log',
|
|
[WifiProvisioningStatus.NO_SERIAL_DEVICE_FOUND]:
|
|
'onboarding-connect_tracker-connection_status-no_serial_device_found',
|
|
};
|
|
|
|
const errorLabelMap = {
|
|
[WifiProvisioningStatus.NO_SERIAL_LOGS_ERROR]:
|
|
'onboarding-connect_serial-error-modal-no_serial_log',
|
|
[WifiProvisioningStatus.NO_SERIAL_DEVICE_FOUND]:
|
|
'onboarding-connect_serial-error-modal-no_serial_device_found',
|
|
};
|
|
|
|
const statusProgressMap = {
|
|
[WifiProvisioningStatus.NONE]: 0,
|
|
[WifiProvisioningStatus.SERIAL_INIT]: 0.2,
|
|
[WifiProvisioningStatus.NO_SERIAL_DEVICE_FOUND]: 0.2,
|
|
[WifiProvisioningStatus.OBTAINING_MAC_ADDRESS]: 0.3,
|
|
[WifiProvisioningStatus.NO_SERIAL_LOGS_ERROR]: 0.3,
|
|
[WifiProvisioningStatus.PROVISIONING]: 0.4,
|
|
[WifiProvisioningStatus.CONNECTING]: 0.6,
|
|
[WifiProvisioningStatus.LOOKING_FOR_SERVER]: 0.8,
|
|
[WifiProvisioningStatus.DONE]: 1,
|
|
[WifiProvisioningStatus.CONNECTION_ERROR]: 0.6,
|
|
[WifiProvisioningStatus.COULD_NOT_FIND_SERVER]: 0.8,
|
|
};
|
|
|
|
export function ConnectTrackersPage() {
|
|
const { l10n } = useLocalization();
|
|
const { statuses } = useStatusContext();
|
|
|
|
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
|
|
const { applyProgress, state } = useOnboarding();
|
|
const navigate = useNavigate();
|
|
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
|
const [provisioningStatus, setProvisioningStatus] =
|
|
useState<WifiProvisioningStatus>(WifiProvisioningStatus.NONE);
|
|
const [ignoreError, setIgnoreError] = useState(false);
|
|
|
|
applyProgress(0.4);
|
|
|
|
const bnoExists = useIsRestCalibrationTrackers(connectedIMUTrackers);
|
|
|
|
useEffect(() => {
|
|
if (!state.wifi) {
|
|
navigate('/onboarding/wifi-creds');
|
|
}
|
|
|
|
const req = new StartWifiProvisioningRequestT();
|
|
req.ssid = state.wifi?.ssid as string;
|
|
req.password = state.wifi?.password as string;
|
|
|
|
sendRPCPacket(RpcMessage.StartWifiProvisioningRequest, req);
|
|
return () => {
|
|
sendRPCPacket(
|
|
RpcMessage.StopWifiProvisioningRequest,
|
|
new StopWifiProvisioningRequestT()
|
|
);
|
|
};
|
|
}, []);
|
|
|
|
useRPCPacket(
|
|
RpcMessage.WifiProvisioningStatusResponse,
|
|
({ status }: WifiProvisioningStatusResponseT) => {
|
|
setProvisioningStatus(status);
|
|
}
|
|
);
|
|
|
|
const isError = useMemo(
|
|
() =>
|
|
[
|
|
WifiProvisioningStatus.CONNECTION_ERROR,
|
|
WifiProvisioningStatus.COULD_NOT_FIND_SERVER,
|
|
WifiProvisioningStatus.NO_SERIAL_LOGS_ERROR,
|
|
WifiProvisioningStatus.NO_SERIAL_DEVICE_FOUND,
|
|
].includes(provisioningStatus),
|
|
[provisioningStatus]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isError) {
|
|
setIgnoreError(false);
|
|
}
|
|
}, [isError]);
|
|
|
|
const progressBarClass = useMemo(() => {
|
|
if (isError) {
|
|
return 'bg-status-critical';
|
|
}
|
|
|
|
if (provisioningStatus === WifiProvisioningStatus.DONE) {
|
|
return 'bg-status-success';
|
|
}
|
|
}, [provisioningStatus]);
|
|
|
|
const slimeStatus = useMemo(() => {
|
|
if (isError) {
|
|
return SlimeState.SAD;
|
|
}
|
|
|
|
switch (provisioningStatus) {
|
|
case WifiProvisioningStatus.DONE:
|
|
return SlimeState.HAPPY;
|
|
case WifiProvisioningStatus.NONE:
|
|
return SlimeState.CURIOUS;
|
|
default:
|
|
return SlimeState.JUMPY;
|
|
}
|
|
}, [provisioningStatus, isError]);
|
|
|
|
const currentTip = useMemo(
|
|
() =>
|
|
connectedIMUTrackers.length > 0
|
|
? 'tips-find_tracker'
|
|
: 'tips-turn_on_tracker',
|
|
[connectedIMUTrackers.length]
|
|
);
|
|
|
|
const filteredStatuses = useMemo(() => {
|
|
return Object.entries(statuses).filter(
|
|
([, value]) => value.dataType == StatusData.StatusPublicNetwork
|
|
);
|
|
}, [statuses]);
|
|
|
|
return (
|
|
<>
|
|
<BaseModal
|
|
isOpen={
|
|
!ignoreError &&
|
|
[
|
|
WifiProvisioningStatus.NO_SERIAL_LOGS_ERROR,
|
|
WifiProvisioningStatus.NO_SERIAL_DEVICE_FOUND,
|
|
].includes(provisioningStatus)
|
|
}
|
|
appendClasses={'w-xl max-w-xl mobile:w-full'}
|
|
closeable
|
|
onRequestClose={() => {
|
|
setIgnoreError(true);
|
|
}}
|
|
>
|
|
<div className="flex flex-col items-center gap-2 ">
|
|
<Localized id={(errorLabelMap as any)[provisioningStatus]}>
|
|
<Typography variant="main-title" />
|
|
</Localized>
|
|
<Localized id={`${(errorLabelMap as any)[provisioningStatus]}-desc`}>
|
|
<Typography
|
|
variant="standard"
|
|
whitespace="whitespace-pre-wrap"
|
|
block
|
|
/>
|
|
</Localized>
|
|
<video
|
|
src={CONNECT_TRACKER}
|
|
loop
|
|
autoPlay
|
|
className="w-full aspect-video rounded-md mt-2"
|
|
/>
|
|
<div className="flex gap-3 pt-5 justify-end w-full">
|
|
<Button
|
|
variant="tertiary"
|
|
onClick={() => {
|
|
setIgnoreError(true);
|
|
}}
|
|
>
|
|
Close
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</BaseModal>
|
|
<div className="connect-tracker-layout h-full">
|
|
<div style={{ gridArea: 's' }} className="p-4">
|
|
<Typography variant="main-title">
|
|
{l10n.getString('onboarding-connect_tracker-title')}
|
|
</Typography>
|
|
<Typography>
|
|
{l10n.getString('onboarding-connect_tracker-description-p0-v1')}
|
|
</Typography>
|
|
<Typography>
|
|
{l10n.getString('onboarding-connect_tracker-description-p1-v1')}
|
|
</Typography>
|
|
<div className="flex flex-col gap-2 py-5">
|
|
<ArrowLink
|
|
to="/settings/serial"
|
|
state={{ SerialPort: 'Auto' }}
|
|
direction="right"
|
|
variant={state.alonePage ? 'boxed-2' : 'boxed'}
|
|
>
|
|
{l10n.getString('onboarding-connect_tracker-issue-serial')}
|
|
</ArrowLink>
|
|
</div>
|
|
<Localized
|
|
id={currentTip}
|
|
elems={{ em: <em className="italic" />, b: <b /> }}
|
|
>
|
|
<TipBox>Conditional tip</TipBox>
|
|
</Localized>
|
|
{filteredStatuses.map(([, status]) => (
|
|
<div className="pt-4">
|
|
<Localized
|
|
key={status.id}
|
|
id={`status_system-${StatusData[status.dataType]}`}
|
|
vars={parseStatusToLocale(status, connectedIMUTrackers, l10n)}
|
|
elems={{
|
|
PublicFixLink: (
|
|
<A href="https://docs.slimevr.dev/common-issues.html#network-profile-is-currently-set-to-public" />
|
|
),
|
|
}}
|
|
>
|
|
<WarningBox whitespace={false}>
|
|
{`Warning, you should fix ${StatusData[status.dataType]}`}
|
|
</WarningBox>
|
|
</Localized>
|
|
</div>
|
|
))}
|
|
<div
|
|
className={classNames(
|
|
'rounded-xl h-24 flex gap-2 p-3 lg:w-full mt-4 relative',
|
|
state.alonePage ? 'bg-background-60' : 'bg-background-70',
|
|
isError && 'border-2 border-status-critical'
|
|
)}
|
|
>
|
|
<div
|
|
className={classNames(
|
|
'flex flex-col justify-center fill-background-10 absolute',
|
|
'right-5 bottom-8'
|
|
)}
|
|
>
|
|
<LoaderIcon slimeState={slimeStatus} />
|
|
</div>
|
|
|
|
<div className="flex flex-col grow self-center">
|
|
<Typography bold>
|
|
{l10n.getString('onboarding-connect_tracker-usb')}
|
|
</Typography>
|
|
<div className="flex fill-background-10 gap-1">
|
|
<Typography>
|
|
{l10n.getString(statusLabelMap[provisioningStatus])}
|
|
</Typography>
|
|
</div>
|
|
<ProgressBar
|
|
progress={statusProgressMap[provisioningStatus]}
|
|
height={14}
|
|
animated={true}
|
|
colorClass={progressBarClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-row mt-4 gap-3">
|
|
<Button
|
|
variant="secondary"
|
|
state={{ alonePage: state.alonePage }}
|
|
to="/onboarding/wifi-creds"
|
|
>
|
|
{state.alonePage
|
|
? l10n.getString('onboarding-connect_tracker-back')
|
|
: l10n.getString('onboarding-previous_step')}
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
to={
|
|
state.alonePage
|
|
? '/'
|
|
: bnoExists
|
|
? '/onboarding/calibration-tutorial'
|
|
: '/onboarding/assign-tutorial'
|
|
}
|
|
className="ml-auto"
|
|
>
|
|
{l10n.getString('onboarding-connect_tracker-next')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div style={{ gridArea: 't' }} className="flex items-center px-5">
|
|
<Typography bold>
|
|
{l10n.getString('onboarding-connect_tracker-connected_trackers', {
|
|
amount: connectedIMUTrackers.length,
|
|
})}
|
|
</Typography>
|
|
</div>
|
|
<div style={{ gridArea: 'c' }} className="xs:overflow-y-auto">
|
|
<div className="grid lg:grid-cols-2 md:grid-cols-1 gap-2 pr-1 mx-5 py-4">
|
|
{Array.from({
|
|
...connectedIMUTrackers,
|
|
length: Math.max(connectedIMUTrackers.length, 1),
|
|
}).map((tracker, index) => (
|
|
<div key={index}>
|
|
{!tracker && (
|
|
<div
|
|
className={classNames(
|
|
'rounded-xl h-16 animate-pulse',
|
|
state.alonePage ? 'bg-background-80' : 'bg-background-70'
|
|
)}
|
|
/>
|
|
)}
|
|
{tracker && (
|
|
<TrackerCard
|
|
tracker={tracker.tracker}
|
|
device={tracker.device}
|
|
smol
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|