mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
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:
@@ -95,6 +95,7 @@
|
||||
freetype
|
||||
expat
|
||||
libayatana-appindicator
|
||||
libusb1
|
||||
])
|
||||
++ lib.optionals pkgs.stdenv.isDarwin [
|
||||
pkgs.darwin.apple_sdk.frameworks.Security
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
gui/public/images/autobone-poster.webp
Normal file
BIN
gui/public/images/autobone-poster.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
gui/public/videos/autobone.webm
Normal file
BIN
gui/public/videos/autobone.webm
Normal file
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user