Fixing small bugs (#924)

Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
Co-authored-by: ZRock35 <91239122+ZRock35@users.noreply.github.com>
This commit is contained in:
Uriel
2024-02-16 01:52:14 -03:00
committed by GitHub
parent 7d0d64418d
commit 908e220b23
24 changed files with 454 additions and 199 deletions

View File

@@ -95,6 +95,7 @@
freetype
expat
libayatana-appindicator
libusb1
])
++ lib.optionals pkgs.stdenv.isDarwin [
pkgs.darwin.apple_sdk.frameworks.Security

View File

@@ -19,6 +19,7 @@ tips-find_tracker = Not sure which tracker is which? Shake a tracker and it will
tips-do_not_move_heels = Ensure your heels do not move during recording!
tips-file_select = Drag & drop files to use, or <u>browse</u>.
tips-tap_setup = You can slowly tap 2 times your tracker to choose it instead of selecting it from the menu.
tips-turn_on_tracker = Using official SlimeVR trackers? Remember to <b><em>turn on your tracker</em></b> after connecting it to the PC!
## Body parts
body_part-NONE = Unassigned
@@ -652,6 +653,7 @@ onboarding-assign_trackers-assigned = { $assigned } of { $trackers ->
} assigned
onboarding-assign_trackers-advanced = Show advanced assign locations
onboarding-assign_trackers-next = I assigned all the trackers
onboarding-assign_trackers-mirror_view = Mirror view
## Tracker assignment warnings
# Note for devs, number is used for representing boolean states per bit.
@@ -761,8 +763,9 @@ onboarding-automatic_mounting-put_trackers_on-next = I have all my trackers on
## Tracker proportions method choose
onboarding-choose_proportions = What proportion calibration method to use?
# Multiline string
onboarding-choose_proportions-description = Body proportions are used to know the measurements of your body. They're required to calculate the trackers' positions.
onboarding-choose_proportions-description-v1 = Body proportions are used to know the measurements of your body. They're required to calculate the trackers' positions.
When proportions of your body don't match the ones saved, your tracking precision will be worse and you will notice things like skating or sliding, or your body not matching your avatar well.
<b>You only need to measure your body once!</b> Unless they are wrong or your body has changed, then you don't need to do them again.
onboarding-choose_proportions-auto_proportions = Automatic proportions
# Italized text
onboarding-choose_proportions-auto_proportions-subtitle = Recommended

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

View File

@@ -8,6 +8,8 @@ export function Preload() {
<link rel="preload" href="/images/reset-pose.webp" as="image" />
<link rel="preload" href="/images/slimes.webp" as="image" />
<link rel="preload" href="/videos/autobone.webm" as="video" />
<link
rel="preload"
href="/sounds/quick-reset-started-sound.mp3"

View File

@@ -12,6 +12,7 @@ export function BodyInteractions({
width = 228,
dotsSize = 15,
variant = 'tracker-select',
mirror,
onSelectRole,
}: {
leftControls?: ReactNode;
@@ -22,6 +23,7 @@ export function BodyInteractions({
assignedRoles: BodyPart[];
onSelectRole: (role: BodyPart) => void;
highlightedRoles: BodyPart[];
mirror: boolean;
}) {
const { isMobile } = useBreakpoint('mobile');
@@ -166,7 +168,7 @@ export function BodyInteractions({
variant === 'tracker-select' && 'mobile:mx-0 xs:mx-10'
)}
>
<PersonFrontIcon width={width}></PersonFrontIcon>
<PersonFrontIcon width={width} mirror={mirror}></PersonFrontIcon>
{slotsButtonsPos.map(
({ top, left, height, width, id, hidden, buttonOffset }) => (
<div

View File

@@ -98,10 +98,10 @@ export const InputInside = forwardRef<
></input>
{type === 'password' && (
<div
className="fill-background-10 absolute top-0 h-full flex flex-col justify-center right-0 p-4"
className="fill-background-10 absolute inset-y-0 right-0 pr-6 z-10 my-auto w-[16px] h-[16px]"
onClick={togglePassword}
>
<EyeIcon></EyeIcon>
<EyeIcon width={16} closed={forceText}></EyeIcon>
</div>
)}
{error?.message && (

View File

@@ -1,7 +1,36 @@
import { BodyPart } from 'solarxr-protocol';
export function PersonFrontIcon({ width }: { width?: number }) {
export const SIDES = [
{
shoulder: BodyPart.LEFT_SHOULDER,
upperArm: BodyPart.LEFT_UPPER_ARM,
lowerArm: BodyPart.LEFT_LOWER_ARM,
hand: BodyPart.LEFT_HAND,
upperLeg: BodyPart.LEFT_UPPER_LEG,
lowerLeg: BodyPart.LEFT_LOWER_LEG,
foot: BodyPart.LEFT_FOOT,
},
{
shoulder: BodyPart.RIGHT_SHOULDER,
upperArm: BodyPart.RIGHT_UPPER_ARM,
lowerArm: BodyPart.RIGHT_LOWER_ARM,
hand: BodyPart.RIGHT_HAND,
upperLeg: BodyPart.RIGHT_UPPER_LEG,
lowerLeg: BodyPart.RIGHT_LOWER_LEG,
foot: BodyPart.RIGHT_FOOT,
},
];
export function PersonFrontIcon({
width,
mirror = true,
}: {
width?: number;
mirror?: boolean;
}) {
const CIRCLE_RADIUS = 0.0001;
const left = +!mirror;
const right = +mirror;
return (
<svg
@@ -62,49 +91,49 @@ export function PersonFrontIcon({ width }: { width?: number }) {
cx="128"
cy="218"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.RIGHT_HAND]}
id={BodyPart[SIDES[right].hand]}
/>
<circle
className="body-part-circle"
cx="115"
cy="140"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.RIGHT_UPPER_ARM]}
id={BodyPart[SIDES[right].upperArm]}
/>
<circle
className="body-part-circle"
cx="105"
cy="105"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.RIGHT_SHOULDER]}
id={BodyPart[SIDES[right].shoulder]}
/>
<circle
className="body-part-circle"
cx="125"
cy="194"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.RIGHT_LOWER_ARM]}
id={BodyPart[SIDES[right].lowerArm]}
/>
<circle
className="body-part-circle"
cx="97.004"
cy="360"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.RIGHT_LOWER_LEG]}
id={BodyPart[SIDES[right].lowerLeg]}
/>
<circle
className="body-part-circle"
cx="97"
cy="250"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.RIGHT_UPPER_LEG]}
id={BodyPart[SIDES[right].upperLeg]}
/>
<circle
className="body-part-circle"
cx="97.004"
cy="380"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.RIGHT_FOOT]}
id={BodyPart[SIDES[right].foot]}
/>
<circle
@@ -112,7 +141,7 @@ export function PersonFrontIcon({ width }: { width?: number }) {
cx="36"
cy="218"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.LEFT_HAND]}
id={BodyPart[SIDES[left].hand]}
/>
<circle
@@ -120,28 +149,28 @@ export function PersonFrontIcon({ width }: { width?: number }) {
cx="50"
cy="140"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.LEFT_UPPER_ARM]}
id={BodyPart[SIDES[left].upperArm]}
/>
<circle
className="body-part-circle"
cx="58"
cy="105"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.LEFT_SHOULDER]}
id={BodyPart[SIDES[left].shoulder]}
/>
<circle
className="body-part-circle"
cx="39"
cy="194"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.LEFT_LOWER_ARM]}
id={BodyPart[SIDES[left].lowerArm]}
/>
<circle
className="body-part-circle"
cx="67.004"
cy="360"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.LEFT_LOWER_LEG]}
id={BodyPart[SIDES[left].lowerLeg]}
/>
<circle
@@ -149,14 +178,14 @@ export function PersonFrontIcon({ width }: { width?: number }) {
cx="67"
cy="250"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.LEFT_UPPER_LEG]}
id={BodyPart[SIDES[left].upperLeg]}
/>
<circle
className="body-part-circle"
cx="67.004"
cy="380"
r={CIRCLE_RADIUS}
id={BodyPart[BodyPart.LEFT_FOOT]}
id={BodyPart[SIDES[left].foot]}
/>
</svg>
);

View File

@@ -1,12 +1,36 @@
export function EyeIcon() {
return (
export function EyeIcon({
width = 14,
closed = false,
}: {
width?: number;
closed?: boolean;
}) {
return closed ? (
<svg
width="14"
height="10"
viewBox="0 0 14 10"
xmlns="http://www.w3.org/2000/svg"
width={width}
height={width}
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M9.09817 4.99914C9.09817 6.11133 8.15709 7.01294 6.9962 7.01294C5.83532 7.01294 4.89424 6.11133 4.89424 4.99914C4.89424 3.88693 5.83532 2.98533 6.9962 2.98533C8.15709 2.98532 9.09817 3.88694 9.09817 4.99914ZM7 0.806091C5.79804 0.811423 4.55217 1.10403 3.37279 1.66426C2.49711 2.09735 1.64372 2.70838 0.90293 3.46257C0.539093 3.84756 0.0750283 4.40501 0 4.99979C0.00886667 5.515 0.561517 6.15093 0.90293 6.53703C1.5976 7.2616 2.42877 7.85557 3.37279 8.33578C4.47262 8.86954 5.68997 9.17685 7 9.19395C8.2031 9.18853 9.44869 8.89255 10.6268 8.33578C11.5024 7.90269 12.3563 7.29122 13.0971 6.53703C13.4609 6.15204 13.925 5.59457 14 4.99979C13.9911 4.48458 13.4385 3.84863 13.0971 3.46254C12.4024 2.73797 11.5708 2.14446 10.6268 1.66423C9.52751 1.13088 8.30716 0.82568 7 0.806091ZM6.99911 1.84732C8.8205 1.84732 10.297 3.25891 10.297 5.00025C10.297 6.74157 8.8205 8.15316 6.99911 8.15316C5.17773 8.15316 3.70124 6.74156 3.70124 5.00025C3.70124 3.25891 5.17773 1.84732 6.99911 1.84732Z" />
<path d="M3.53 2.47a.75.75 0 0 0-1.06 1.06l18 18a.75.75 0 1 0 1.06-1.06l-18-18ZM22.676 12.553a11.249 11.249 0 0 1-2.631 4.31l-3.099-3.099a5.25 5.25 0 0 0-6.71-6.71L7.759 4.577a11.217 11.217 0 0 1 4.242-.827c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113Z" />
<path d="M15.75 12c0 .18-.013.357-.037.53l-4.244-4.243A3.75 3.75 0 0 1 15.75 12ZM12.53 15.713l-4.243-4.244a3.75 3.75 0 0 0 4.244 4.243Z" />
<path d="M6.75 12c0-.619.107-1.213.304-1.764l-3.1-3.1a11.25 11.25 0 0 0-2.63 4.31c-.12.362-.12.752 0 1.114 1.489 4.467 5.704 7.69 10.675 7.69 1.5 0 2.933-.294 4.242-.827l-2.477-2.477A5.25 5.25 0 0 1 6.75 12Z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={width}
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" />
<path
fillRule="evenodd"
d="M1.323 11.447C2.811 6.976 7.028 3.75 12.001 3.75c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113-1.487 4.471-5.705 7.697-10.677 7.697-4.97 0-9.186-3.223-10.675-7.69a1.762 1.762 0 0 1 0-1.113ZM17.25 12a5.25 5.25 0 1 1-10.5 0 5.25 5.25 0 0 1 10.5 0Z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@@ -5,3 +5,20 @@ export function PlayIcon({ width = 33 }: { width?: number }) {
</svg>
);
}
export function PlayCircleIcon({ width = 24 }: { width?: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
viewBox="0 0 24 24"
fill="inherit"
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm14.024-.983a1.125 1.125 0 0 1 0 1.966l-5.603 3.113A1.125 1.125 0 0 1 9 15.113V8.887c0-.857.921-1.4 1.671-.983l5.603 3.113Z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@@ -5,7 +5,16 @@ import { useTrackers } from '@/hooks/tracker';
import { BodyInteractions } from '@/components/commons/BodyInteractions';
import { TrackerPartCard } from '@/components/tracker/TrackerPartCard';
import { BodyPartError } from './pages/trackers-assign/TrackerAssignment';
import { SIDES } from '@/components/commons/PersonFrontIcon';
export const LOWER_BODY = new Set([
BodyPart.LEFT_FOOT,
BodyPart.RIGHT_FOOT,
BodyPart.LEFT_LOWER_LEG,
BodyPart.RIGHT_LOWER_LEG,
BodyPart.LEFT_UPPER_LEG,
BodyPart.RIGHT_UPPER_LEG,
]);
export const SPINE_PARTS = [
BodyPart.UPPER_CHEST,
BodyPart.CHEST,
@@ -37,6 +46,7 @@ export const ASSIGNMENT_RULES: Partial<
export function BodyAssignment({
advanced,
mirror,
onRoleSelected,
rolesWithErrors = {},
highlightedRoles = [],
@@ -44,6 +54,7 @@ export function BodyAssignment({
width,
}: {
advanced: boolean;
mirror: boolean;
onlyAssigned?: boolean;
rolesWithErrors?: Partial<Record<BodyPart, BodyPartError>>;
highlightedRoles?: BodyPart[];
@@ -80,10 +91,14 @@ export function BodyAssignment({
[assignedTrackers]
);
const left = +!mirror;
const right = +mirror;
return (
<>
<BodyInteractions
width={width}
mirror={mirror}
assignedRoles={assignedRoles}
highlightedRoles={highlightedRoles}
onSelectRole={onRoleSelected}
@@ -116,39 +131,39 @@ export function BodyAssignment({
{advanced && (
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.LEFT_SHOULDER]?.label}
td={trackerPartGrouped[BodyPart.LEFT_SHOULDER]}
role={BodyPart.LEFT_SHOULDER}
onClick={() => onRoleSelected(BodyPart.LEFT_SHOULDER)}
roleError={rolesWithErrors[SIDES[left].shoulder]?.label}
td={trackerPartGrouped[SIDES[left].shoulder]}
role={SIDES[left].shoulder}
onClick={() => onRoleSelected(SIDES[left].shoulder)}
direction="right"
/>
)}
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.LEFT_UPPER_ARM]?.label}
td={trackerPartGrouped[BodyPart.LEFT_UPPER_ARM]}
role={BodyPart.LEFT_UPPER_ARM}
onClick={() => onRoleSelected(BodyPart.LEFT_UPPER_ARM)}
roleError={rolesWithErrors[SIDES[left].upperArm]?.label}
td={trackerPartGrouped[SIDES[left].upperArm]}
role={SIDES[left].upperArm}
onClick={() => onRoleSelected(SIDES[left].upperArm)}
direction="right"
/>
</div>
<div className="flex flex-col gap-2">
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.LEFT_LOWER_ARM]?.label}
td={trackerPartGrouped[BodyPart.LEFT_LOWER_ARM]}
role={BodyPart.LEFT_LOWER_ARM}
onClick={() => onRoleSelected(BodyPart.LEFT_LOWER_ARM)}
roleError={rolesWithErrors[SIDES[left].lowerArm]?.label}
td={trackerPartGrouped[SIDES[left].lowerArm]}
role={SIDES[left].lowerArm}
onClick={() => onRoleSelected(SIDES[left].lowerArm)}
direction="right"
/>
{advanced && (
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.LEFT_HAND]?.label}
td={trackerPartGrouped[BodyPart.LEFT_HAND]}
role={BodyPart.LEFT_HAND}
onClick={() => onRoleSelected(BodyPart.LEFT_HAND)}
roleError={rolesWithErrors[SIDES[left].hand]?.label}
td={trackerPartGrouped[SIDES[left].hand]}
role={SIDES[left].hand}
onClick={() => onRoleSelected(SIDES[left].hand)}
direction="right"
/>
)}
@@ -156,27 +171,27 @@ export function BodyAssignment({
<div className="flex flex-col gap-2">
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.LEFT_UPPER_LEG]?.label}
td={trackerPartGrouped[BodyPart.LEFT_UPPER_LEG]}
role={BodyPart.LEFT_UPPER_LEG}
onClick={() => onRoleSelected(BodyPart.LEFT_UPPER_LEG)}
roleError={rolesWithErrors[SIDES[left].upperLeg]?.label}
td={trackerPartGrouped[SIDES[left].upperLeg]}
role={SIDES[left].upperLeg}
onClick={() => onRoleSelected(SIDES[left].upperLeg)}
direction="right"
/>
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.LEFT_LOWER_LEG]?.label}
td={trackerPartGrouped[BodyPart.LEFT_LOWER_LEG]}
role={BodyPart.LEFT_LOWER_LEG}
onClick={() => onRoleSelected(BodyPart.LEFT_LOWER_LEG)}
roleError={rolesWithErrors[SIDES[left].lowerLeg]?.label}
td={trackerPartGrouped[SIDES[left].lowerLeg]}
role={SIDES[left].lowerLeg}
onClick={() => onRoleSelected(SIDES[left].lowerLeg)}
direction="right"
/>
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.LEFT_FOOT]?.label}
td={trackerPartGrouped[BodyPart.LEFT_FOOT]}
role={BodyPart.LEFT_FOOT}
onClick={() => onRoleSelected(BodyPart.LEFT_FOOT)}
roleError={rolesWithErrors[SIDES[left].foot]?.label}
td={trackerPartGrouped[SIDES[left].foot]}
role={SIDES[left].foot}
onClick={() => onRoleSelected(SIDES[left].foot)}
direction="right"
/>
</div>
@@ -208,20 +223,20 @@ export function BodyAssignment({
{advanced && (
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.RIGHT_SHOULDER]?.label}
td={trackerPartGrouped[BodyPart.RIGHT_SHOULDER]}
role={BodyPart.RIGHT_SHOULDER}
onClick={() => onRoleSelected(BodyPart.RIGHT_SHOULDER)}
roleError={rolesWithErrors[SIDES[right].shoulder]?.label}
td={trackerPartGrouped[SIDES[right].shoulder]}
role={SIDES[right].shoulder}
onClick={() => onRoleSelected(SIDES[right].shoulder)}
direction="left"
/>
)}
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.RIGHT_UPPER_ARM]?.label}
td={trackerPartGrouped[BodyPart.RIGHT_UPPER_ARM]}
role={BodyPart.RIGHT_UPPER_ARM}
onClick={() => onRoleSelected(BodyPart.RIGHT_UPPER_ARM)}
roleError={rolesWithErrors[SIDES[right].upperArm]?.label}
td={trackerPartGrouped[SIDES[right].upperArm]}
role={SIDES[right].upperArm}
onClick={() => onRoleSelected(SIDES[right].upperArm)}
direction="left"
/>
</div>
@@ -248,19 +263,19 @@ export function BodyAssignment({
<div className="flex flex-col gap-2">
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.RIGHT_LOWER_ARM]?.label}
td={trackerPartGrouped[BodyPart.RIGHT_LOWER_ARM]}
role={BodyPart.RIGHT_LOWER_ARM}
onClick={() => onRoleSelected(BodyPart.RIGHT_LOWER_ARM)}
roleError={rolesWithErrors[SIDES[right].lowerArm]?.label}
td={trackerPartGrouped[SIDES[right].lowerArm]}
role={SIDES[right].lowerArm}
onClick={() => onRoleSelected(SIDES[right].lowerArm)}
direction="left"
/>
{advanced && (
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.RIGHT_HAND]?.label}
td={trackerPartGrouped[BodyPart.RIGHT_HAND]}
onClick={() => onRoleSelected(BodyPart.RIGHT_HAND)}
role={BodyPart.RIGHT_HAND}
roleError={rolesWithErrors[SIDES[right].hand]?.label}
td={trackerPartGrouped[SIDES[right].hand]}
onClick={() => onRoleSelected(SIDES[right].hand)}
role={SIDES[right].hand}
direction="left"
/>
)}
@@ -269,27 +284,27 @@ export function BodyAssignment({
<div className="flex flex-col gap-2">
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.RIGHT_UPPER_LEG]?.label}
td={trackerPartGrouped[BodyPart.RIGHT_UPPER_LEG]}
role={BodyPart.RIGHT_UPPER_LEG}
onClick={() => onRoleSelected(BodyPart.RIGHT_UPPER_LEG)}
roleError={rolesWithErrors[SIDES[right].upperLeg]?.label}
td={trackerPartGrouped[SIDES[right].upperLeg]}
role={SIDES[right].upperLeg}
onClick={() => onRoleSelected(SIDES[right].upperLeg)}
direction="left"
/>
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.RIGHT_LOWER_LEG]?.label}
td={trackerPartGrouped[BodyPart.RIGHT_LOWER_LEG]}
role={BodyPart.RIGHT_LOWER_LEG}
onClick={() => onRoleSelected(BodyPart.RIGHT_LOWER_LEG)}
roleError={rolesWithErrors[SIDES[right].lowerLeg]?.label}
td={trackerPartGrouped[SIDES[right].lowerLeg]}
role={SIDES[right].lowerLeg}
onClick={() => onRoleSelected(SIDES[right].lowerLeg)}
direction="left"
/>
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.RIGHT_FOOT]?.label}
td={trackerPartGrouped[BodyPart.RIGHT_FOOT]}
role={BodyPart.RIGHT_FOOT}
onClick={() => onRoleSelected(BodyPart.RIGHT_FOOT)}
roleError={rolesWithErrors[SIDES[right].foot]?.label}
td={trackerPartGrouped[SIDES[right].foot]}
role={SIDES[right].foot}
onClick={() => onRoleSelected(SIDES[right].foot)}
direction="left"
/>
</div>

View File

@@ -17,6 +17,7 @@ type StepComponentType = FC<{
prevStep: () => void;
resetSteps: () => void;
variant: 'alone' | 'onboarding';
active: boolean;
}>;
export type Step = {
type: 'numbered' | 'fullsize';
@@ -160,6 +161,7 @@ export function StepperSlider({
nextStep={nextStep}
prevStep={prevStep}
resetSteps={resetSteps}
active={index === step}
/>
</StepContainer>
))}

View File

@@ -1,4 +1,4 @@
import { useLocalization } from '@fluent/react';
import { Localized, useLocalization } from '@fluent/react';
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -126,6 +126,14 @@ export function ConnectTrackersPage() {
}
}, [provisioningStatus]);
const currentTip = useMemo(
() =>
connectedIMUTrackers.length > 0
? 'tips-find_tracker'
: 'tips-turn_on_tracker',
[connectedIMUTrackers.length]
);
return (
<div className="flex flex-col h-full items-center px-4 pb-4">
<div className="flex gap-10 mobile:flex-col w-full xs:max-w-7xl">
@@ -156,7 +164,12 @@ export function ConnectTrackersPage() {
{l10n.getString('onboarding-connect_tracker-issue-serial')}
</ArrowLink>
</div>
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
<Localized
id={currentTip}
elems={{ em: <em className="italic"></em>, b: <b></b> }}
>
<TipBox>Conditional tip</TipBox>
</Localized>
<div
className={classNames(

View File

@@ -77,7 +77,14 @@ export function WifiCredsPage() {
>
<Input
control={control}
rules={{ required: true }}
rules={{
validate: {
validPassword: (v: string | undefined) =>
v === undefined ||
v.length === 0 ||
new Blob([v]).size >= 8,
},
}}
name="password"
type="password"
label="Password"

View File

@@ -159,13 +159,18 @@ export function ProportionsChoose() {
{l10n.getString('onboarding-choose_proportions')}
</Typography>
<div className="xs:w-10/12 xs:max-w-[666px]">
<Typography
variant="standard"
color="secondary"
whitespace="whitespace-pre-line"
<Localized
id="onboarding-choose_proportions-description-v1"
elems={{ b: <b className="text-base underline"></b> }}
>
{l10n.getString('onboarding-choose_proportions-description')}
</Typography>
<Typography
variant="standard"
color="secondary"
whitespace="whitespace-pre-line"
>
How to measure your body!
</Typography>
</Localized>
</div>
<div
className={classNames(

View File

@@ -1,4 +1,4 @@
import { ReactNode, useEffect, useState } from 'react';
import { ReactNode, useEffect, useRef, useState } from 'react';
import { ProcessStatus, useAutobone } from '@/hooks/autobone';
import { ProgressBar } from '@/components/commons/ProgressBar';
import { TipBox } from '@/components/commons/TipBox';
@@ -6,13 +6,16 @@ import { Typography } from '@/components/commons/Typography';
import { useLocalization } from '@fluent/react';
import { P, match } from 'ts-pattern';
import { AutoboneErrorModal } from './AutoboneErrorModal';
import { PlayCircleIcon } from '@/components/commons/icon/PlayIcon';
export function Recording({
nextStep,
resetSteps,
active,
}: {
nextStep: () => void;
resetSteps: () => void;
active: boolean;
}) {
const { l10n } = useLocalization();
const { progress, hasCalibration, hasRecording, eta } = useAutobone();
@@ -35,8 +38,33 @@ export function Recording({
}
}, [progress, hasCalibration, hasRecording]);
const videoRef = useRef<HTMLVideoElement | null>(null);
const [paused, setPaused] = useState(true);
function toggleVideo() {
if (!videoRef.current) return;
if (videoRef.current.paused) {
videoRef.current.play();
} else {
videoRef.current.pause();
videoRef.current.fastSeek(0);
}
setPaused(videoRef.current.paused);
}
useEffect(() => {
if (!active && !paused) {
toggleVideo();
return;
}
if (active && paused) {
toggleVideo();
return;
}
}, [active]);
return (
<div className="flex flex-col items-center w-full justify-between">
<div className="flex flex-row flex-grow">
<AutoboneErrorModal
isOpen={modalOpen}
onClose={() => {
@@ -44,75 +72,99 @@ export function Recording({
resetSteps();
}}
></AutoboneErrorModal>
<div className="flex gap-1 flex-col justify-center items-center">
<div className="flex text-status-critical justify-center items-center gap-1">
<div className="w-2 h-2 rounded-lg bg-status-critical"></div>
<Typography color="text-status-critical">
{l10n.getString('onboarding-automatic_proportions-recording-title')}
<div className="flex flex-col items-center w-full justify-between">
<div className="flex gap-1 flex-col justify-center items-center">
<div className="flex text-status-critical justify-center items-center gap-1">
<div className="w-2 h-2 rounded-lg bg-status-critical"></div>
<Typography color="text-status-critical">
{l10n.getString(
'onboarding-automatic_proportions-recording-title'
)}
</Typography>
</div>
<Typography variant="section-title">
{l10n.getString(
'onboarding-automatic_proportions-recording-description-p0'
)}
</Typography>
<Typography color="secondary">
{l10n.getString(
'onboarding-automatic_proportions-recording-description-p1'
)}
</Typography>
</div>
<Typography variant="section-title">
{l10n.getString(
'onboarding-automatic_proportions-recording-description-p0'
)}
</Typography>
<Typography color="secondary">
{l10n.getString(
'onboarding-automatic_proportions-recording-description-p1'
)}
</Typography>
</div>
<ol className="list-decimal mobile:px-4">
<>
{l10n
.getString('onboarding-automatic_proportions-recording-steps')
.split('\n')
.map((line, i) => (
<li key={i}>
<Typography color="secondary">{line}</Typography>
</li>
))}
</>
</ol>
<div className="flex">
<TipBox>{l10n.getString('tips-do_not_move_heels')}</TipBox>
</div>
<div className="flex flex-col gap-2 items-center w-full max-w-[150px]">
<ProgressBar
progress={progress}
height={2}
colorClass={match([hasCalibration, hasRecording])
.returnType<string | undefined>()
.with(
P.union(
[ProcessStatus.REJECTED, P._],
[P._, ProcessStatus.REJECTED]
),
() => 'bg-status-critical'
)
.with(
[ProcessStatus.FULFILLED, ProcessStatus.FULFILLED],
() => 'bg-status-success'
)
.otherwise(() => undefined)}
></ProgressBar>
<Typography color="secondary">
{match([hasCalibration, hasRecording])
.returnType<ReactNode>()
.with([ProcessStatus.PENDING, ProcessStatus.FULFILLED], () =>
l10n.getString(
'onboarding-automatic_proportions-recording-processing'
<ol className="list-decimal mobile:px-4">
<>
{l10n
.getString('onboarding-automatic_proportions-recording-steps')
.split('\n')
.map((line, i) => (
<li key={i}>
<Typography color="secondary">{line}</Typography>
</li>
))}
</>
</ol>
<div className="flex">
<TipBox>{l10n.getString('tips-do_not_move_heels')}</TipBox>
</div>
<div className="flex flex-col gap-2 items-center w-full max-w-[150px]">
<ProgressBar
progress={progress}
height={2}
colorClass={match([hasCalibration, hasRecording])
.returnType<string | undefined>()
.with(
P.union(
[ProcessStatus.REJECTED, P._],
[P._, ProcessStatus.REJECTED]
),
() => 'bg-status-critical'
)
)
.with([ProcessStatus.PENDING, ProcessStatus.PENDING], () =>
l10n.getString(
'onboarding-automatic_proportions-recording-timer',
{ time: Math.round(eta) }
.with(
[ProcessStatus.FULFILLED, ProcessStatus.FULFILLED],
() => 'bg-status-success'
)
)
.otherwise(() => '')}
</Typography>
.otherwise(() => undefined)}
></ProgressBar>
<Typography color="secondary">
{match([hasCalibration, hasRecording])
.returnType<ReactNode>()
.with([ProcessStatus.PENDING, ProcessStatus.FULFILLED], () =>
l10n.getString(
'onboarding-automatic_proportions-recording-processing'
)
)
.with([ProcessStatus.PENDING, ProcessStatus.PENDING], () =>
l10n.getString(
'onboarding-automatic_proportions-recording-timer',
{ time: Math.round(eta) }
)
)
.otherwise(() => '')}
</Typography>
</div>
</div>
<button className="relative appearance-none h-fit" onClick={toggleVideo}>
<div
className="absolute w-[100px] h-[100px] top-0 bottom-0 left-0 right-0 m-auto fill-background-20"
hidden={!paused}
>
<PlayCircleIcon width={100}></PlayCircleIcon>
</div>
<video
preload=""
ref={videoRef}
src="/videos/autobone.webm"
className="min-w-[12rem] w-[12rem]"
muted
loop
playsInline
controls={false}
poster="/images/autobone-poster.webp"
></video>
</button>
</div>
);
}

View File

@@ -3,18 +3,39 @@ import { Button } from '@/components/commons/Button';
import { TipBox } from '@/components/commons/TipBox';
import { Typography } from '@/components/commons/Typography';
import { useLocalization } from '@fluent/react';
import { useEffect, useRef, useState } from 'react';
import { PlayCircleIcon } from '@/components/commons/icon/PlayIcon';
export function StartRecording({
nextStep,
prevStep,
variant,
active,
}: {
nextStep: () => void;
prevStep: () => void;
variant: 'onboarding' | 'alone';
active: boolean;
}) {
const { l10n } = useLocalization();
const { startRecording } = useAutobone();
const videoRef = useRef<HTMLVideoElement | null>(null);
const [paused, setPaused] = useState(true);
function toggleVideo() {
if (!videoRef.current) return;
if (videoRef.current.paused) {
videoRef.current.play();
} else {
videoRef.current.pause();
videoRef.current.fastSeek(0);
}
setPaused(videoRef.current.paused);
}
useEffect(() => {
if (!active && !paused) toggleVideo();
}, [active]);
const start = () => {
nextStep();
@@ -24,34 +45,59 @@ export function StartRecording({
return (
<>
<div className="flex flex-col flex-grow">
<div className="flex flex-grow flex-col gap-4">
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-automatic_proportions-start_recording-title'
)}
</Typography>
<div>
<Typography color="secondary">
<div className="flex flex-row flex-grow">
<div className="flex flex-grow flex-col gap-4">
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-automatic_proportions-start_recording-description'
'onboarding-automatic_proportions-start_recording-title'
)}
</Typography>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-automatic_proportions-start_recording-description'
)}
</Typography>
</div>
<ol className="list-decimal mobile:px-4">
<>
{l10n
.getString('onboarding-automatic_proportions-recording-steps')
.split('\n')
.map((line, i) => (
<li key={i}>
<Typography color="secondary">{line}</Typography>
</li>
))}
</>
</ol>
<div className="flex">
<TipBox>{l10n.getString('tips-do_not_move_heels')}</TipBox>
</div>
</div>
<ol className="list-decimal mobile:px-4">
<>
{l10n
.getString('onboarding-automatic_proportions-recording-steps')
.split('\n')
.map((line, i) => (
<li key={i}>
<Typography color="secondary">{line}</Typography>
</li>
))}
</>
</ol>
<div className="flex">
<TipBox>{l10n.getString('tips-do_not_move_heels')}</TipBox>
</div>
<button
className="relative appearance-none h-fit"
onClick={toggleVideo}
>
<div
className="absolute w-[100px] h-[100px] top-0 bottom-0 left-0 right-0 m-auto fill-background-20"
hidden={!paused}
>
<PlayCircleIcon width={100}></PlayCircleIcon>
</div>
<video
preload=""
ref={videoRef}
src="/videos/autobone.webm"
className="min-w-[12rem] w-[12rem]"
muted
loop
playsInline
controls={false}
poster="/images/autobone-poster.webp"
></video>
</button>
</div>
<div className="flex gap-3 mobile:justify-between">

View File

@@ -13,14 +13,14 @@ import { MountingSelectionMenu } from './MountingSelectionMenu';
import { useLocalization } from '@fluent/react';
import { useBreakpoint } from '@/hooks/breakpoint';
import { Quaternion } from 'three';
import { useConfig } from '@/hooks/config';
import { defaultConfig, useConfig } from '@/hooks/config';
export function ManualMountingPage() {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { applyProgress, state } = useOnboarding();
const { sendRPCPacket } = useWebsocketAPI();
const { setConfig } = useConfig();
const { setConfig, config } = useConfig();
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
@@ -102,6 +102,7 @@ export function ManualMountingPage() {
<div className="flex flex-row justify-center">
<BodyAssignment
width={isMobile ? 160 : undefined}
mirror={config?.mirrorView ?? defaultConfig.mirrorView}
onlyAssigned={true}
advanced={true}
onRoleSelected={setSelectRole}

View File

@@ -26,6 +26,7 @@ import { Typography } from '@/components/commons/Typography';
import {
ASSIGNMENT_RULES,
BodyAssignment,
LOWER_BODY,
} from '@/components/onboarding/BodyAssignment';
import { NeckWarningModal } from '@/components/onboarding/NeckWarningModal';
import { TrackerSelectionMenu } from './TrackerSelectionMenu';
@@ -53,15 +54,21 @@ export function TrackersAssignPage() {
const { applyProgress, state } = useOnboarding();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const { control, watch } = useForm<{ advanced: boolean }>({
defaultValues: { advanced: config?.advancedAssign ?? false },
const { control, watch } = useForm<{
advanced: boolean;
mirrorView: boolean;
}>({
defaultValues: {
advanced: config?.advancedAssign ?? false,
mirrorView: config?.mirrorView ?? true,
},
});
const { advanced } = watch();
const { advanced, mirrorView } = watch();
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
const assignedTrackers = useAssignedTrackers();
useEffect(() => {
setConfig({ advancedAssign: advanced });
}, [advanced]);
setConfig({ advancedAssign: advanced, mirrorView });
}, [advanced, mirrorView]);
const [tapDetectionSettings, setTapDetectionSettings] = useState<Omit<
TapDetectionSettingsT,
@@ -153,6 +160,14 @@ export function TrackersAssignPage() {
: trackerRoles.includes(part),
]);
// Special exception for waist/hip: https://github.com/SlimeVR/SlimeVR-Server/issues/612
if (
(assignedRole === BodyPart.HIP || assignedRole === BodyPart.WAIST) &&
!trackerRoles.some((t) => LOWER_BODY.has(t))
) {
return;
}
if (unassignedRoles.every(([, state]) => state)) return;
return {
@@ -277,12 +292,22 @@ export function TrackersAssignPage() {
</Typography>
</div>
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
<CheckBox
control={control}
label={l10n.getString('onboarding-assign_trackers-advanced')}
name="advanced"
variant="toggle"
></CheckBox>
<div>
<CheckBox
control={control}
label={l10n.getString('onboarding-assign_trackers-advanced')}
name="advanced"
variant="toggle"
></CheckBox>
<CheckBox
control={control}
label={l10n.getString(
'onboarding-assign_trackers-mirror_view'
)}
name="mirrorView"
variant="toggle"
></CheckBox>
</div>
{!!firstError && (
<div className="bg-status-warning text-background-60 px-3 py-2 text-justify rounded-md">
<div className="flex flex-col gap-1 whitespace-normal">
@@ -320,6 +345,7 @@ export function TrackersAssignPage() {
highlightedRoles={firstError?.affectedRoles || []}
rolesWithErrors={rolesWithErrors}
advanced={advanced}
mirror={mirrorView}
onRoleSelected={tryOpenChokerWarning}
></BodyAssignment>
</div>

View File

@@ -10,6 +10,7 @@ import { useLocalization } from '@fluent/react';
import { NeckWarningModal } from '@/components/onboarding/NeckWarningModal';
import { useChokerWarning } from '@/hooks/choker-warning';
import { useBreakpoint } from '@/hooks/breakpoint';
import { defaultConfig, useConfig } from '@/hooks/config';
export function SingleTrackerBodyAssignmentMenu({
isOpen,
@@ -22,6 +23,7 @@ export function SingleTrackerBodyAssignmentMenu({
}) {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { config } = useConfig();
const { control, watch } = useForm<{ advanced: boolean }>({
defaultValues: { advanced: false },
});
@@ -75,6 +77,7 @@ export function SingleTrackerBodyAssignmentMenu({
</div>
<div className="flex flex-col xs:flex-grow gap-3 rounded-xl fill-background-50 py-2">
<BodyAssignment
mirror={config?.mirrorView ?? defaultConfig.mirrorView}
width={isMobile ? 160 : undefined}
onlyAssigned={false}
advanced={advanced}

View File

@@ -88,6 +88,11 @@ export function ToggleableSkeletonVisualizerWidget(
const { l10n } = useLocalization();
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const state = localStorage.getItem('skeletonModelPreview');
if (state) setEnabled(state === 'true');
}, []);
return (
<>
{!enabled && (
@@ -96,7 +101,7 @@ export function ToggleableSkeletonVisualizerWidget(
className="w-full"
onClick={() => {
setEnabled(true);
localStorage.setItem('modelPreview', 'true');
localStorage.setItem('skeletonModelPreview', 'true');
}}
>
{l10n.getString('widget-skeleton_visualizer-preview')}
@@ -109,7 +114,7 @@ export function ToggleableSkeletonVisualizerWidget(
variant="secondary"
onClick={() => {
setEnabled(false);
localStorage.setItem('modelPreview', 'false');
localStorage.setItem('skeletonModelPreview', 'false');
}}
>
{l10n.getString('widget-skeleton_visualizer-hide')}

View File

@@ -28,6 +28,7 @@ export interface Config {
advancedAssign: boolean;
useTray: boolean | null;
doneManualMounting: boolean;
mirrorView: boolean;
}
export interface ConfigContext {
@@ -51,6 +52,7 @@ export const defaultConfig: Omit<Config, 'devSettings'> = {
advancedAssign: false,
useTray: null,
doneManualMounting: false,
mirrorView: true,
};
function fallbackToDefaults(loadedConfig: any): Config {

View File

@@ -5,7 +5,7 @@ import { useOnboarding } from './onboarding';
export interface WifiFormData {
ssid: string;
password: string;
password?: string;
}
export function useWifiForm() {
@@ -27,7 +27,7 @@ export function useWifiForm() {
}, []);
const submitWifiCreds = (value: WifiFormData) => {
setWifiCredentials(value.ssid, value.password);
setWifiCredentials(value.ssid, value.password ?? '');
navigate('/onboarding/connect-trackers', {
state: { alonePage: state.alonePage },
});

View File

@@ -52,8 +52,8 @@ enum class OperatingSystem(
}
fun resolveLogDirectory(identifier: String): Path? = when (currentPlatform) {
LINUX -> System.getenv("XDG_CONFIG_HOME")?.let { Path(it, identifier, "logs") }
?: System.getenv("HOME")?.let { Path(it, ".config", identifier, "logs") }
LINUX -> System.getenv("XDG_DATA_HOME")?.let { Path(it, identifier, "logs") }
?: System.getenv("HOME")?.let { Path(it, ".local", "share", identifier, "logs") }
WINDOWS -> System.getenv("AppData")?.let { Path(it, identifier, "logs") }
OSX -> System.getenv("HOME")?.let { Path(it, "Library", "Logs", identifier) }
UNKNOWN -> null