Files
SlimeVR-Server/gui/src/components/onboarding/pages/ConnectTracker.tsx

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>
</>
);
}