mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Compare commits
7 Commits
v19.0.0-rc
...
feat/new-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c52a2cdd0 | ||
|
|
c0038bf14c | ||
|
|
4aa9f85c1a | ||
|
|
4f0bddc98e | ||
|
|
be9373bd8a | ||
|
|
fadbfa23ab | ||
|
|
a355bc5d90 |
@@ -14,6 +14,7 @@
|
||||
"@react-three/fiber": "^8.17.10",
|
||||
"@sentry/react": "^9.9.0",
|
||||
"@sentry/vite-plugin": "^2.22.7",
|
||||
"@slimevr/update-manifest": "^0.0.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/react-query": "^5.48.0",
|
||||
"@tauri-apps/api": "^2.0.2",
|
||||
|
||||
@@ -1,6 +1,93 @@
|
||||
use std::process;
|
||||
|
||||
use cfg_aliases::cfg_aliases;
|
||||
|
||||
fn main() -> shadow_rs::SdResult<()> {
|
||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=.git/index");
|
||||
|
||||
let commit_hash = process::Command::new("git")
|
||||
.args(&["rev-parse", "--verify", "--short", "HEAD"])
|
||||
.output()
|
||||
.map(|output| {
|
||||
if output.status.success() {
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
} else {
|
||||
eprintln!("Warning: Failed to get git commit hash: {:?}", output);
|
||||
"unknown_commit".to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Warning: Could not run 'git rev-parse': {}", e);
|
||||
"no_git_commit".to_string()
|
||||
});
|
||||
|
||||
let version_tag = process::Command::new("git")
|
||||
.args(&[
|
||||
"--no-pager",
|
||||
"tag",
|
||||
"--sort",
|
||||
"-taggerdate",
|
||||
"--points-at",
|
||||
"HEAD",
|
||||
])
|
||||
.output()
|
||||
.map(|output| {
|
||||
if output.status.success() {
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.split('\n')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string()
|
||||
} else {
|
||||
eprintln!("Warning: Failed to get git tag: {:?}", output);
|
||||
"".to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Warning: Could not run 'git tag': {}", e);
|
||||
"".to_string()
|
||||
});
|
||||
|
||||
let git_status_output = process::Command::new("git")
|
||||
.args(&["status", "--porcelain"])
|
||||
.output()
|
||||
.map(|output| {
|
||||
if output.status.success() {
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
} else {
|
||||
eprintln!("Warning: Failed to get git status: {:?}", output);
|
||||
"error".to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Warning: Could not run 'git status': {}", e);
|
||||
"error".to_string()
|
||||
});
|
||||
|
||||
let git_clean = git_status_output.is_empty();
|
||||
|
||||
if !git_clean {
|
||||
println!(
|
||||
"cargo:warning=Git is dirty because of:\n'{}'",
|
||||
git_status_output
|
||||
);
|
||||
}
|
||||
|
||||
let mut version = if !version_tag.is_empty() {
|
||||
version_tag
|
||||
} else {
|
||||
commit_hash
|
||||
};
|
||||
|
||||
if !git_clean {
|
||||
version.push_str("-dirty");
|
||||
}
|
||||
|
||||
println!("cargo:warning=Version is {}", version);
|
||||
println!("cargo:rustc-env=SLIMEVR_SERVER_VERSION={}", version);
|
||||
|
||||
// Bypass for Nix script having libudev-zero and Tauri not liking it
|
||||
if let Some(path) = option_env!("SLIMEVR_RUST_LD_LIBRARY_PATH") {
|
||||
println!("cargo:rustc-env=LD_LIBRARY_PATH={path}");
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://github.com/SlimeVR/SlimeVR-Tracker-ESP/releases/download/*"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/SlimeVR/SlimeVR-Server/releases/download/v0.0.0-update-manifest/update-manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ fn update_window_state(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const VERSION: &str = env!("SLIMEVR_SERVER_VERSION");
|
||||
|
||||
#[tauri::command]
|
||||
fn logging(msg: String) {
|
||||
log::info!(target: "webview", "{}", msg)
|
||||
@@ -95,10 +97,12 @@ fn main() -> Result<()> {
|
||||
}));
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let tauri_context = tauri::generate_context!();
|
||||
|
||||
// Set up loggers and global handlers
|
||||
let _logger = setup_logger(&tauri_context);
|
||||
log::info!(target: "backend", "Starting SlimeVR GUI v{}", VERSION);
|
||||
|
||||
// Ensure child processes die when spawned on windows
|
||||
// and then check for WebView2's existence
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Outlet,
|
||||
Route,
|
||||
BrowserRouter as Router,
|
||||
Routes,
|
||||
} from 'react-router-dom';
|
||||
import { Home } from './components/home/Home';
|
||||
@@ -16,54 +16,54 @@ import {
|
||||
WebSocketApiContext,
|
||||
} from './hooks/websocket-api';
|
||||
|
||||
import { UpdateContextProvider } from '@/components/providers/UpdateContext.js';
|
||||
import { UpdateSettings } from '@/components/settings/pages/UpdateSettings.js';
|
||||
import { withSentryReactRouterV6Routing } from '@sentry/react';
|
||||
import { Event, listen } from '@tauri-apps/api/event';
|
||||
import * as os from '@tauri-apps/plugin-os';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { AppLayout } from './AppLayout';
|
||||
import { FirmwareToolSettings } from './components/firmware-tool/FirmwareTool';
|
||||
import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate';
|
||||
import { OnboardingContextProvider } from './components/onboarding/OnboardingContextProvicer';
|
||||
import { OnboardingLayout } from './components/onboarding/OnboardingLayout';
|
||||
import { AssignmentTutorialPage } from './components/onboarding/pages/assignment-preparation/AssignmentTutorial';
|
||||
import { AutomaticProportionsPage } from './components/onboarding/pages/body-proportions/AutomaticProportions';
|
||||
import { ManualProportionsPage } from './components/onboarding/pages/body-proportions/ManualProportions';
|
||||
import { ScaledProportionsPage } from './components/onboarding/pages/body-proportions/ScaledProportions';
|
||||
import { CalibrationTutorialPage } from './components/onboarding/pages/CalibrationTutorial';
|
||||
import { ConnectionLost } from './components/onboarding/pages/ConnectionLost';
|
||||
import { ConnectTrackersPage } from './components/onboarding/pages/ConnectTracker';
|
||||
import { DonePage } from './components/onboarding/pages/Done';
|
||||
import { EnterVRPage } from './components/onboarding/pages/EnterVR';
|
||||
import { HomePage } from './components/onboarding/pages/Home';
|
||||
import { AutomaticMountingPage } from './components/onboarding/pages/mounting/AutomaticMounting';
|
||||
import { ManualMountingPage } from './components/onboarding/pages/mounting/ManualMounting';
|
||||
import { MountingChoose } from './components/onboarding/pages/mounting/MountingChoose';
|
||||
import { ResetTutorialPage } from './components/onboarding/pages/ResetTutorial';
|
||||
import { StayAlignedSetup } from './components/onboarding/pages/stay-aligned/StayAlignedSetup';
|
||||
import { TrackersAssignPage } from './components/onboarding/pages/trackers-assign/TrackerAssignment';
|
||||
import { WifiCredsPage } from './components/onboarding/pages/WifiCreds';
|
||||
import { Preload } from './components/Preload';
|
||||
import { ConfigContextProvider } from './components/providers/ConfigContext';
|
||||
import { StatusProvider } from './components/providers/StatusSystemContext';
|
||||
import { SerialDetectionModal } from './components/SerialDetectionModal';
|
||||
import { AdvancedSettings } from './components/settings/pages/AdvancedSettings';
|
||||
import { InterfaceSettings } from './components/settings/pages/InterfaceSettings';
|
||||
import { OSCRouterSettings } from './components/settings/pages/OSCRouterSettings';
|
||||
import { VMCSettings } from './components/settings/pages/VMCSettings';
|
||||
import { VRCOSCSettings } from './components/settings/pages/VRCOSCSettings';
|
||||
import { TopBar } from './components/TopBar';
|
||||
import { TrackerSettingsPage } from './components/tracker/TrackerSettings';
|
||||
import { OSCRouterSettings } from './components/settings/pages/OSCRouterSettings';
|
||||
import * as os from '@tauri-apps/plugin-os';
|
||||
import { VMCSettings } from './components/settings/pages/VMCSettings';
|
||||
import { MountingChoose } from './components/onboarding/pages/mounting/MountingChoose';
|
||||
import { StatusProvider } from './components/providers/StatusSystemContext';
|
||||
import { VersionUpdateModal } from './components/VersionUpdateModal';
|
||||
import { CalibrationTutorialPage } from './components/onboarding/pages/CalibrationTutorial';
|
||||
import { AssignmentTutorialPage } from './components/onboarding/pages/assignment-preparation/AssignmentTutorial';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import semver from 'semver';
|
||||
import { useBreakpoint, useIsTauri } from './hooks/breakpoint';
|
||||
import { VRModePage } from './components/vr-mode/VRModePage';
|
||||
import { InterfaceSettings } from './components/settings/pages/InterfaceSettings';
|
||||
import { error, log } from './utils/logging';
|
||||
import { FirmwareToolSettings } from './components/firmware-tool/FirmwareTool';
|
||||
import { AppLayout } from './AppLayout';
|
||||
import { Preload } from './components/Preload';
|
||||
import { UnknownDeviceModal } from './components/UnknownDeviceModal';
|
||||
import { useDiscordPresence } from './hooks/discord-presence';
|
||||
import { withSentryReactRouterV6Routing } from '@sentry/react';
|
||||
import { ScaledProportionsPage } from './components/onboarding/pages/body-proportions/ScaledProportions';
|
||||
import { AdvancedSettings } from './components/settings/pages/AdvancedSettings';
|
||||
import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate';
|
||||
import { ConnectionLost } from './components/onboarding/pages/ConnectionLost';
|
||||
import { VersionUpdateModal } from './components/VersionUpdateModal';
|
||||
import { VRModePage } from './components/vr-mode/VRModePage';
|
||||
import { VRCWarningsPage } from './components/vrc/VRCWarningsPage';
|
||||
import { StayAlignedSetup } from './components/onboarding/pages/stay-aligned/StayAlignedSetup';
|
||||
import { useBreakpoint, useIsTauri } from './hooks/breakpoint';
|
||||
import { useDiscordPresence } from './hooks/discord-presence';
|
||||
import { error, log } from './utils/logging';
|
||||
|
||||
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
|
||||
export const VersionContext = createContext('');
|
||||
export const DOCS_SITE = 'https://docs.slimevr.dev';
|
||||
export const SLIMEVR_DISCORD = 'https://discord.gg/slimevr';
|
||||
|
||||
@@ -128,6 +128,7 @@ function Layout() {
|
||||
</SettingsLayout>
|
||||
}
|
||||
>
|
||||
<Route path="updates" element={<UpdateSettings />} />
|
||||
<Route path="firmware-tool" element={<FirmwareToolSettings />} />
|
||||
<Route path="trackers" element={<GeneralSettings />} />
|
||||
<Route path="serial" element={<Serial />} />
|
||||
@@ -186,7 +187,6 @@ function Layout() {
|
||||
|
||||
export default function App() {
|
||||
const websocketAPI = useProvideWebsocketApi();
|
||||
const [updateFound, setUpdateFound] = useState('');
|
||||
const isTauri = useIsTauri();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -205,25 +205,6 @@ export default function App() {
|
||||
return () => window.removeEventListener('keydown', onKeydown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchReleases() {
|
||||
const releases = await fetch(
|
||||
`https://api.github.com/repos/${GH_REPO}/releases`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((json: any[]) => json.filter((rl) => rl?.prerelease === false));
|
||||
|
||||
if (
|
||||
__VERSION_TAG__ &&
|
||||
typeof releases[0].tag_name === 'string' &&
|
||||
semver.gt(releases[0].tag_name, __VERSION_TAG__)
|
||||
) {
|
||||
setUpdateFound(releases[0].tag_name);
|
||||
}
|
||||
}
|
||||
fetchReleases().catch(() => error('failed to fetch releases'));
|
||||
}, []);
|
||||
|
||||
if (isTauri) {
|
||||
useEffect(() => {
|
||||
const type = os.type();
|
||||
@@ -293,7 +274,7 @@ export default function App() {
|
||||
<AppContextProvider>
|
||||
<OnboardingContextProvider>
|
||||
<StatusProvider>
|
||||
<VersionContext.Provider value={updateFound}>
|
||||
<UpdateContextProvider>
|
||||
<div className="h-full w-full text-standard bg-background-80 text-background-10">
|
||||
<Preload />
|
||||
{!websocketAPI.isConnected && (
|
||||
@@ -301,7 +282,7 @@ export default function App() {
|
||||
)}
|
||||
{websocketAPI.isConnected && <Layout></Layout>}
|
||||
</div>
|
||||
</VersionContext.Provider>
|
||||
</UpdateContextProvider>
|
||||
</StatusProvider>
|
||||
</OnboardingContextProvider>
|
||||
</AppContextProvider>
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
import { ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import { DOCS_SITE, GH_REPO } from '@/App';
|
||||
import { useBreakpoint, useIsTauri } from '@/hooks/breakpoint';
|
||||
import { STABLE_CHANNEL, useConfig } from '@/hooks/config';
|
||||
import { useUpdateContext } from '@/hooks/update.js';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { connectedIMUTrackersAtom } from '@/store/app-store';
|
||||
import { error } from '@/utils/logging';
|
||||
import { isTrayAvailable } from '@/utils/tauri';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen, TauriEvent } from '@tauri-apps/api/event';
|
||||
import {
|
||||
CloseRequestedEvent,
|
||||
getCurrentWindow,
|
||||
UserAttentionType,
|
||||
} from '@tauri-apps/api/window';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { NavLink, useMatch } from 'react-router-dom';
|
||||
import {
|
||||
RpcMessage,
|
||||
@@ -6,38 +24,21 @@ import {
|
||||
ServerInfosResponseT,
|
||||
TrackerStatus,
|
||||
} from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { useDoubleTap } from 'use-double-tap';
|
||||
import { CloseIcon } from './commons/icon/CloseIcon';
|
||||
import { DownloadIcon } from './commons/icon/DownloadIcon';
|
||||
import { GearIcon } from './commons/icon/GearIcon';
|
||||
import { MaximiseIcon } from './commons/icon/MaximiseIcon';
|
||||
import { MinimiseIcon } from './commons/icon/MinimiseIcon';
|
||||
import { QuestionIcon } from './commons/icon/QuestionIcon';
|
||||
import { SlimeVRIcon } from './commons/icon/SimevrIcon';
|
||||
import { ProgressBar } from './commons/ProgressBar';
|
||||
import { Typography } from './commons/Typography';
|
||||
import { DownloadIcon } from './commons/icon/DownloadIcon';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { DOCS_SITE, GH_REPO, VersionContext } from '@/App';
|
||||
import classNames from 'classnames';
|
||||
import { QuestionIcon } from './commons/icon/QuestionIcon';
|
||||
import { useBreakpoint, useIsTauri } from '@/hooks/breakpoint';
|
||||
import { GearIcon } from './commons/icon/GearIcon';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { TrackersStillOnModal } from './TrackersStillOnModal';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { listen, TauriEvent } from '@tauri-apps/api/event';
|
||||
import { TrayOrExitModal } from './TrayOrExitModal';
|
||||
import { error } from '@/utils/logging';
|
||||
import { useDoubleTap } from 'use-double-tap';
|
||||
import { isTrayAvailable } from '@/utils/tauri';
|
||||
import { ErrorConsentModal } from './ErrorConsentModal';
|
||||
import {
|
||||
CloseRequestedEvent,
|
||||
getCurrentWindow,
|
||||
UserAttentionType,
|
||||
} from '@tauri-apps/api/window';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { connectedIMUTrackersAtom } from '@/store/app-store';
|
||||
import { TrackersStillOnModal } from './TrackersStillOnModal';
|
||||
import { TrayOrExitModal } from './TrayOrExitModal';
|
||||
|
||||
export function VersionTag() {
|
||||
function VersionTag() {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -55,6 +56,34 @@ export function VersionTag() {
|
||||
);
|
||||
}
|
||||
|
||||
export function VersionIndicator() {
|
||||
const { config } = useConfig();
|
||||
|
||||
const channel = config?.updateChannel ?? STABLE_CHANNEL;
|
||||
const isStable = channel === STABLE_CHANNEL;
|
||||
|
||||
return (
|
||||
<div className="bg-opacity-10 bg-status-success rounded-lg overflow-hidden flex gap-1">
|
||||
<VersionTag />
|
||||
|
||||
<NavLink
|
||||
to="/settings/updates"
|
||||
className="flex justify-around flex-col py-1 pr-0.5 transition-all fill-status-success"
|
||||
data-tauri-drag-region
|
||||
state={{ scrollTo: 'channel' }}
|
||||
>
|
||||
<GearIcon />
|
||||
</NavLink>
|
||||
|
||||
{!isStable && (
|
||||
<div className="flex justify-around flex-col text-standard-bold text-status-success pl-0.5 pr-3 select-text">
|
||||
{channel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TopBar({
|
||||
progress,
|
||||
}: {
|
||||
@@ -66,7 +95,7 @@ export function TopBar({
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const connectedIMUTrackers = useAtomValue(connectedIMUTrackersAtom);
|
||||
const { config, setConfig, saveConfig } = useConfig();
|
||||
const version = useContext(VersionContext);
|
||||
const { isUpToDate } = useUpdateContext();
|
||||
const [localIp, setLocalIp] = useState<string | null>(null);
|
||||
const [showConnectedTrackersWarning, setConnectedTrackerWarning] =
|
||||
useState(false);
|
||||
@@ -181,7 +210,7 @@ export function TopBar({
|
||||
)}
|
||||
{(!(isMobile && !config?.decorations) || showVersionMobile) && (
|
||||
<>
|
||||
<VersionTag></VersionTag>
|
||||
<VersionIndicator />
|
||||
{doesMatchSettings && (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -196,7 +225,7 @@ export function TopBar({
|
||||
</>
|
||||
)}
|
||||
|
||||
{version && (
|
||||
{!isUpToDate && (
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { GH_REPO } from '@/App';
|
||||
import { useUpdateContext } from '@/hooks/update.js';
|
||||
import { error } from '@/utils/logging';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useContext, useState } from 'react';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useState } from 'react';
|
||||
import semver from 'semver';
|
||||
import { BaseModal } from './commons/BaseModal';
|
||||
import { Button } from './commons/Button';
|
||||
import { Typography } from './commons/Typography';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import semver from 'semver';
|
||||
import { GH_REPO, VersionContext } from '@/App';
|
||||
import { error } from '@/utils/logging';
|
||||
|
||||
export function VersionUpdateModal() {
|
||||
const { l10n } = useLocalization();
|
||||
const newVersion = useContext(VersionContext);
|
||||
const { latestVersionOnChannel: newVersion } = useUpdateContext();
|
||||
const [forceClose, setForceClose] = useState(false);
|
||||
const closeModal = () => {
|
||||
localStorage.setItem('lastVersionFound', newVersion);
|
||||
localStorage.setItem('lastVersionFound', newVersion ?? '');
|
||||
setForceClose(true);
|
||||
};
|
||||
let isVersionNew = false;
|
||||
try {
|
||||
// TODO(devminer): check over this, if this is still necessary
|
||||
if (newVersion) {
|
||||
isVersionNew = semver.gt(
|
||||
newVersion,
|
||||
@@ -28,6 +30,8 @@ export function VersionUpdateModal() {
|
||||
error('failed to parse new version');
|
||||
}
|
||||
|
||||
if (newVersion === null) return null;
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={!forceClose && !!newVersion && isVersionNew}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { a11yClick } from '@/utils/a11y';
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { Control, Controller, UseControllerProps } from 'react-hook-form';
|
||||
import { a11yClick } from '@/utils/a11y';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Control, Controller, UseControllerProps } from 'react-hook-form';
|
||||
import { ArrowDownIcon } from './icon/ArrowIcons';
|
||||
|
||||
interface DropdownProps {
|
||||
@@ -33,6 +33,7 @@ export interface DropdownItem {
|
||||
component?: ReactNode;
|
||||
value: string;
|
||||
fontName?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export type DropdownDirection = 'up' | 'down';
|
||||
@@ -99,20 +100,34 @@ export function DropdownItems({
|
||||
<li
|
||||
style={item.fontName ? { fontFamily: item.fontName } : {}}
|
||||
className={classNames(
|
||||
'py-2 px-4 min-w-max cursor-pointer first-of-type:*:pointer-events-none',
|
||||
variant == 'primary' &&
|
||||
'checked-hover:bg-background-50 text-background-20 ' +
|
||||
'checked-hover:text-background-10',
|
||||
variant == 'secondary' &&
|
||||
'checked-hover:bg-background-60 text-background-20 ' +
|
||||
'checked-hover:text-background-10',
|
||||
variant == 'tertiary' &&
|
||||
'bg-accent-background-30 checked-hover:bg-accent-background-20'
|
||||
'py-2 px-4 min-w-max first-of-type:*:pointer-events-none',
|
||||
{
|
||||
'cursor-pointer': !item.disabled,
|
||||
},
|
||||
item.disabled
|
||||
? {
|
||||
'bg-background-70':
|
||||
variant === 'primary',
|
||||
'bg-background-80':
|
||||
variant === 'secondary',
|
||||
'bg-background-40':
|
||||
variant === 'tertiary',
|
||||
}
|
||||
: {
|
||||
'checked-hover:bg-background-50 text-background-20 checked-hover:text-background-10':
|
||||
variant === 'primary',
|
||||
'checked-hover:bg-background-60 text-background-20 checked-hover:text-background-10':
|
||||
variant === 'secondary',
|
||||
'bg-accent-background-30 checked-hover:bg-accent-background-20':
|
||||
variant == 'tertiary',
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
if (item.disabled) return;
|
||||
onSelectItem(item);
|
||||
}}
|
||||
onKeyDown={(ev) => {
|
||||
if (item.disabled) return;
|
||||
if (!a11yClick(ev)) return;
|
||||
onSelectItem(item);
|
||||
}}
|
||||
|
||||
6
gui/src/components/commons/MarkdownLink.tsx
Normal file
6
gui/src/components/commons/MarkdownLink.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { A } from '@/components/commons/A.js';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
export const MarkdownLink = (props: ComponentProps<'a'>) => (
|
||||
<A href={props.href}>{props.children}</A>
|
||||
);
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useBreakpoint } from '@/hooks/breakpoint';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
MutableRefObject,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
ReactElement,
|
||||
useLayoutEffect,
|
||||
MutableRefObject,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Typography } from './Typography';
|
||||
@@ -419,6 +419,7 @@ export function DrawerTooltip({
|
||||
>
|
||||
<div className="h-12 rounded-t-lg relative flex justify-center items-center">
|
||||
<Typography variant="section-title" textAlign="text-center">
|
||||
{/* TODO(futura): translations */}
|
||||
Pro tip
|
||||
</Typography>
|
||||
<button
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { useFirmwareTool } from '@/hooks/firmware-tool';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { Control, useForm } from 'react-hook-form';
|
||||
import {
|
||||
CreateImuConfigDTO,
|
||||
Imudto,
|
||||
} from '@/firmware-tool-api/firmwareToolSchemas';
|
||||
import { CheckBox } from '@/components/commons/Checkbox';
|
||||
import { Dropdown } from '@/components/commons/Dropdown';
|
||||
import { TrashIcon } from '@/components/commons/icon/TrashIcon';
|
||||
import { Input } from '@/components/commons/Input';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
} from '@/components/commons/icon/ArrowIcons';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useElemSize } from '@/hooks/layout';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { TrashIcon } from '@/components/commons/icon/TrashIcon';
|
||||
import { useGetFirmwaresImus } from '@/firmware-tool-api/firmwareToolComponents';
|
||||
import { CheckBox } from '@/components/commons/Checkbox';
|
||||
import {
|
||||
CreateImuConfigDTO,
|
||||
Imudto,
|
||||
} from '@/firmware-tool-api/firmwareToolSchemas';
|
||||
import { useFirmwareTool } from '@/hooks/firmware-tool';
|
||||
import { useElemSize } from '@/hooks/layout';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Control, useForm } from 'react-hook-form';
|
||||
|
||||
function IMUCard({
|
||||
control,
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
import { Localized, ReactLocalization, useLocalization } from '@fluent/react';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { MarkdownLink } from '@/components/commons/MarkdownLink.js';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { DeviceCardControl } from '@/components/firmware-tool/DeviceCard';
|
||||
import { useAppContext } from '@/hooks/app';
|
||||
import {
|
||||
firmwareUpdateErrorStatus,
|
||||
getFlashingRequests,
|
||||
SelectedDevice,
|
||||
} from '@/hooks/firmware-tool';
|
||||
import { checkForUpdate } from '@/hooks/firmware-update';
|
||||
import { getTrackerName } from '@/hooks/tracker';
|
||||
import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { devicesAtom } from '@/store/app-store';
|
||||
import { Localized, ReactLocalization, useLocalization } from '@fluent/react';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Control, useForm } from 'react-hook-form';
|
||||
import Markdown from 'react-markdown';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import remark from 'remark-gfm';
|
||||
import {
|
||||
DeviceDataT,
|
||||
DeviceIdTableT,
|
||||
@@ -12,28 +33,7 @@ import {
|
||||
RpcMessage,
|
||||
TrackerStatus,
|
||||
} from 'solarxr-protocol';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import Markdown from 'react-markdown';
|
||||
import remark from 'remark-gfm';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { useAppContext } from '@/hooks/app';
|
||||
import { DeviceCardControl } from '@/components/firmware-tool/DeviceCard';
|
||||
import { Control, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import {
|
||||
firmwareUpdateErrorStatus,
|
||||
getFlashingRequests,
|
||||
SelectedDevice,
|
||||
} from '@/hooks/firmware-tool';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { object } from 'yup';
|
||||
import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
|
||||
import { A } from '@/components/commons/A';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { devicesAtom } from '@/store/app-store';
|
||||
import { checkForUpdate } from '@/hooks/firmware-update';
|
||||
|
||||
interface FirmwareUpdateForm {
|
||||
selectedDevices: { [key: string]: boolean };
|
||||
@@ -95,10 +95,6 @@ const StatusList = ({ status }: { status: Record<string, UpdateStatus> }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const MarkdownLink = (props: ComponentProps<'a'>) => (
|
||||
<A href={props.href}>{props.children}</A>
|
||||
);
|
||||
|
||||
export function FirmwareUpdate() {
|
||||
const navigate = useNavigate();
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
427
gui/src/components/providers/UpdateContext.tsx
Normal file
427
gui/src/components/providers/UpdateContext.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -1,9 +1,9 @@
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useVRCConfig } from '@/hooks/vrc-config';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { NavLink, useLocation, useMatch } from 'react-router-dom';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useVRCConfig } from '@/hooks/vrc-config';
|
||||
|
||||
export function SettingsLink({
|
||||
to,
|
||||
@@ -69,6 +69,9 @@ export function SettingsSidebar() {
|
||||
<SettingsLink to="/settings/trackers" scrollTo="gestureControl">
|
||||
{l10n.getString('settings-sidebar-gesture_control')}
|
||||
</SettingsLink>
|
||||
<SettingsLink to="/settings/updates" scrollTo="updates">
|
||||
{l10n.getString('settings-sidebar-updates')}
|
||||
</SettingsLink>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
294
gui/src/components/settings/pages/UpdateSettings.tsx
Normal file
294
gui/src/components/settings/pages/UpdateSettings.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import { Button } from '@/components/commons/Button.js';
|
||||
import { SteamIcon } from '@/components/commons/icon/SteamIcon';
|
||||
import { Input } from '@/components/commons/Input.js';
|
||||
import { MarkdownLink } from '@/components/commons/MarkdownLink.js';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { UpdateChannelOptions } from '@/components/settings/pages/components/UpdateChannelOptions.js';
|
||||
import { UpdateChannelVersionOptions } from '@/components/settings/pages/components/UpdateVersionOptions.js';
|
||||
import {
|
||||
SettingsPageLayout,
|
||||
SettingsPagePaneLayout,
|
||||
} from '@/components/settings/SettingsPageLayout';
|
||||
import { useBreakpoint } from '@/hooks/breakpoint.js';
|
||||
import { defaultConfig, useConfig } from '@/hooks/config';
|
||||
import { useUpdateContext } from '@/hooks/update.js';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { type ChannelName, type Version } from '@slimevr/update-manifest';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import Markdown from 'react-markdown';
|
||||
import remark from 'remark-gfm';
|
||||
|
||||
export type SettingsForm = {
|
||||
channel: ChannelName;
|
||||
autoUpdate: boolean;
|
||||
};
|
||||
|
||||
export function UpdateSettings() {
|
||||
const { l10n } = useLocalization();
|
||||
const { config, setConfig } = useConfig();
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
|
||||
const { manifest: updateManifest } = useUpdateContext();
|
||||
|
||||
const { reset, control, watch, handleSubmit, getValues, setValue } =
|
||||
useForm<SettingsForm>({
|
||||
defaultValues: {
|
||||
channel: config?.updateChannel ?? defaultConfig.updateChannel,
|
||||
autoUpdate:
|
||||
config?.notifyOnAvailableUpdates ??
|
||||
defaultConfig.notifyOnAvailableUpdates,
|
||||
},
|
||||
});
|
||||
|
||||
const { channel } = watch();
|
||||
|
||||
const onSubmit = (values: SettingsForm) => {
|
||||
setConfig({
|
||||
updateChannel: values.channel,
|
||||
notifyOnAvailableUpdates: values.autoUpdate,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(onSubmit)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
if (!updateManifest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(updateManifest);
|
||||
|
||||
return (
|
||||
<SettingsPageLayout>
|
||||
<form className="flex flex-col gap-2 w-full">
|
||||
<SettingsPagePaneLayout icon={<SteamIcon />} id="channels">
|
||||
<>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-general-update')}
|
||||
</Typography>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-general-update-subtitle')}
|
||||
</Typography>
|
||||
<div className="flex flex-col py-2">
|
||||
{l10n
|
||||
.getString('settings-general-update-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:gap-4 sm:gap-2 xs:gap-1 mobile:gap-4">
|
||||
<UpdateChannelOptions
|
||||
manifest={updateManifest}
|
||||
value={channel}
|
||||
variant={isMobile ? 'dropdown' : 'radio'}
|
||||
onSelect={(channel) => setValue('channel', channel)}
|
||||
asList={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</SettingsPagePaneLayout>
|
||||
|
||||
<SettingsPagePaneLayout icon={<SteamIcon />} id="change_system">
|
||||
<>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-general-change_system')}
|
||||
</Typography>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-general-change_system-subtitle')}
|
||||
</Typography>
|
||||
<div className="flex flex-col py-2">
|
||||
{l10n
|
||||
.getString('settings-general-change_system-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ChangeSystem />
|
||||
</>
|
||||
</SettingsPagePaneLayout>
|
||||
|
||||
<SettingsPagePaneLayout icon={<SteamIcon />} id="change_version">
|
||||
<>
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('settings-general-change_version')}
|
||||
</Typography>
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-general-change_version-subtitle')}
|
||||
</Typography>
|
||||
<div className="flex flex-col py-2">
|
||||
{l10n
|
||||
.getString('settings-general-change_version-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ChangeVersion />
|
||||
</>
|
||||
</SettingsPagePaneLayout>
|
||||
</form>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangeVersion() {
|
||||
const { isMobile } = useBreakpoint('mobile');
|
||||
const { config } = useConfig();
|
||||
const {
|
||||
manifest,
|
||||
channel: currentChannel,
|
||||
checkCompatibility,
|
||||
} = useUpdateContext();
|
||||
const [channel, setChannel] = useState<ChannelName>(currentChannel);
|
||||
const [version, setVersion] = useState<Version | null>(
|
||||
manifest && __VERSION_TAG__ in manifest.channels[channel].versions
|
||||
? (__VERSION_TAG__ as Version)
|
||||
: null
|
||||
);
|
||||
|
||||
const res = version && checkCompatibility(channel, version);
|
||||
|
||||
const install = useCallback(() => {}, []);
|
||||
|
||||
return (
|
||||
<div className="flex md-max:flex-col gap-2">
|
||||
<div className="md:w-1/4">
|
||||
<Typography>Channel</Typography>
|
||||
<div className="flex flex-col md:gap-4 sm:gap-2 xs:gap-1 mobile:gap-4 max-h-[384px] md:max-h-[512px] pr-1">
|
||||
{manifest && (
|
||||
<UpdateChannelOptions
|
||||
manifest={manifest}
|
||||
value={channel}
|
||||
variant={isMobile ? 'dropdown' : 'radio'}
|
||||
onSelect={(channel) => {
|
||||
setChannel(channel);
|
||||
setVersion(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config?.debug && (
|
||||
<div className="md:w-1/4">
|
||||
<Typography>Version</Typography>
|
||||
<div className="flex flex-col md:gap-4 sm:gap-2 xs:gap-1 mobile:gap-4 max-h-[384px] md:max-h-[512px] overflow-auto pr-1">
|
||||
{manifest && (
|
||||
<UpdateChannelVersionOptions
|
||||
manifest={manifest}
|
||||
channel={channel}
|
||||
value={version ?? ''}
|
||||
variant={isMobile ? 'dropdown' : 'radio'}
|
||||
onSelect={(version) =>
|
||||
setVersion(version === '' ? null : (version as Version))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="md:w-1/2 flex flex-col gap-2">
|
||||
{res?.version && (
|
||||
<>
|
||||
<Typography>Release Notes</Typography>
|
||||
<div className="bg-background-60 rounded-lg px-3 py-2 max-h-[512px] overflow-auto">
|
||||
<Markdown
|
||||
remarkPlugins={[remark]}
|
||||
components={{ a: MarkdownLink }}
|
||||
className={classNames(
|
||||
'w-full text-sm prose-xl prose text-background-10 prose-h1:text-background-10',
|
||||
'prose-h2:text-background-10 prose-h3:text-background-10 prose-a:text-background-20 prose-strong:text-background-10',
|
||||
'prose-code:text-background-20'
|
||||
)}
|
||||
>
|
||||
{res.version.release_notes}
|
||||
</Markdown>
|
||||
</div>
|
||||
<div className="inline ml-auto w-fit">
|
||||
{/* TODO(devminer): add translations */}
|
||||
<Button variant="primary" disabled={!res.isInstallable}>
|
||||
Install
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ChangeSYstemForm = {
|
||||
platform: string;
|
||||
architecture: string;
|
||||
};
|
||||
|
||||
function ChangeSystem() {
|
||||
const { platform, architecture, changeSystem } = useUpdateContext();
|
||||
|
||||
const { reset, control, watch, handleSubmit, getValues, setValue } =
|
||||
useForm<ChangeSYstemForm>({
|
||||
defaultValues: {
|
||||
platform,
|
||||
architecture,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: ChangeSYstemForm) => {
|
||||
console.log(values);
|
||||
changeSystem(values.platform, values.architecture);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(onSubmit)());
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="grid gap-2 items-center grid-rows-2 grid-cols-[fit-content(20%),1fr]">
|
||||
<Typography bold variant="vr-accessible">
|
||||
Platform
|
||||
</Typography>
|
||||
<Input control={control} name="platform" placeholder={platform} />
|
||||
<Typography bold variant="vr-accessible">
|
||||
Architecture
|
||||
</Typography>
|
||||
<Input
|
||||
control={control}
|
||||
name="architecture"
|
||||
placeholder={architecture}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
Reset to real values
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { useLocalization } from '@fluent/react';
|
||||
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
import { StayAlignedRelaxedPose } from 'solarxr-protocol';
|
||||
|
||||
function StaAlignedPoseModal({
|
||||
function StayAlignedPoseModal({
|
||||
open,
|
||||
title,
|
||||
descriptionKeys,
|
||||
@@ -80,7 +80,7 @@ export const StandingRelaxedPoseModal = ({
|
||||
}: {
|
||||
open: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||
}) => (
|
||||
<StaAlignedPoseModal
|
||||
<StayAlignedPoseModal
|
||||
open={open}
|
||||
title={'onboarding-stay_aligned-relaxed_poses-standing-title'}
|
||||
descriptionKeys={[
|
||||
@@ -94,7 +94,7 @@ export const StandingRelaxedPoseModal = ({
|
||||
width={260}
|
||||
alt="Reset position"
|
||||
/>
|
||||
</StaAlignedPoseModal>
|
||||
</StayAlignedPoseModal>
|
||||
);
|
||||
|
||||
export const SittingRelaxedPoseModal = ({
|
||||
@@ -102,7 +102,7 @@ export const SittingRelaxedPoseModal = ({
|
||||
}: {
|
||||
open: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||
}) => (
|
||||
<StaAlignedPoseModal
|
||||
<StayAlignedPoseModal
|
||||
open={open}
|
||||
title={'onboarding-stay_aligned-relaxed_poses-sitting-title'}
|
||||
descriptionKeys={[
|
||||
@@ -116,7 +116,7 @@ export const SittingRelaxedPoseModal = ({
|
||||
width={260}
|
||||
alt="Reset position"
|
||||
/>
|
||||
</StaAlignedPoseModal>
|
||||
</StayAlignedPoseModal>
|
||||
);
|
||||
|
||||
export const FlatRelaxedPoseModal = ({
|
||||
@@ -124,7 +124,7 @@ export const FlatRelaxedPoseModal = ({
|
||||
}: {
|
||||
open: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||
}) => (
|
||||
<StaAlignedPoseModal
|
||||
<StayAlignedPoseModal
|
||||
open={open}
|
||||
title={'onboarding-stay_aligned-relaxed_poses-flat-title'}
|
||||
descriptionKeys={[
|
||||
@@ -138,5 +138,5 @@ export const FlatRelaxedPoseModal = ({
|
||||
width={560}
|
||||
alt="Reset position"
|
||||
/>
|
||||
</StaAlignedPoseModal>
|
||||
</StayAlignedPoseModal>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Dropdown } from '@/components/commons/Dropdown';
|
||||
import { Radio } from '@/components/commons/Radio';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { ASSIGNMENT_MODES } from '@/components/onboarding/BodyAssignment';
|
||||
import { AssignMode, defaultConfig } from '@/hooks/config';
|
||||
import { UpdateManifest, type ChannelName } from '@slimevr/update-manifest';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
// Ordered collection of assign modes with the number of IMU trackers
|
||||
const ASSIGN_MODE_OPTIONS = [
|
||||
AssignMode.LowerBody,
|
||||
AssignMode.Core,
|
||||
AssignMode.EnhancedCore,
|
||||
AssignMode.FullBody,
|
||||
AssignMode.All,
|
||||
].reduce(
|
||||
(options, mode) => ({ ...options, [mode]: ASSIGNMENT_MODES[mode].length }),
|
||||
{} as Record<AssignMode, number>
|
||||
);
|
||||
|
||||
const ItemContent = ({
|
||||
name: channel,
|
||||
description,
|
||||
}: {
|
||||
name: ChannelName;
|
||||
description?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Typography>{channel}</Typography>
|
||||
|
||||
{channel === defaultConfig.updateChannel && (
|
||||
/* TODO(devminer): add translations */
|
||||
<div className="bg-background-70 px-1.5 py-0.5 rounded-md leading-[1rem] text-[0.625rem]">
|
||||
default
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Typography variant="standard" color="secondary">
|
||||
{description}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function UpdateChannelOptions({
|
||||
manifest,
|
||||
value,
|
||||
onSelect,
|
||||
variant = 'radio',
|
||||
asList = true,
|
||||
}: {
|
||||
manifest: UpdateManifest;
|
||||
value: ChannelName;
|
||||
onSelect: (channel: ChannelName) => void;
|
||||
variant?: 'radio' | 'dropdown';
|
||||
asList?: boolean;
|
||||
}) {
|
||||
const { control, watch } = useForm<{
|
||||
updateChannel: ChannelName;
|
||||
}>({
|
||||
defaultValues: {
|
||||
updateChannel: value,
|
||||
},
|
||||
});
|
||||
const { updateChannel } = watch();
|
||||
|
||||
useEffect(() => {
|
||||
onSelect(updateChannel);
|
||||
}, [updateChannel]);
|
||||
|
||||
if (variant == 'dropdown')
|
||||
return (
|
||||
<Dropdown
|
||||
control={control}
|
||||
name="updateChannel"
|
||||
display="block"
|
||||
direction="down"
|
||||
placeholder=""
|
||||
items={Object.entries(manifest.channels).map(([channel, data]) => ({
|
||||
component: (
|
||||
<div className="flex flex-row gap-2 py-1 text-left">
|
||||
<ItemContent
|
||||
name={channel as ChannelName}
|
||||
description={data.description}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
value: channel,
|
||||
}))}
|
||||
></Dropdown>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames('flex gap-2', asList && 'flex-col')}>
|
||||
{Object.entries(manifest.channels).map(([channel, data]) => (
|
||||
<Radio
|
||||
key={channel}
|
||||
name="updateChannel"
|
||||
control={control}
|
||||
value={channel}
|
||||
className="hidden"
|
||||
>
|
||||
<div className="flex flex-row md:gap-4 gap-2">
|
||||
<ItemContent
|
||||
name={channel as ChannelName}
|
||||
description={data.description}
|
||||
/>
|
||||
</div>
|
||||
</Radio>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Dropdown } from '@/components/commons/Dropdown';
|
||||
import { WarningIcon } from '@/components/commons/icon/WarningIcon.js';
|
||||
import { Radio } from '@/components/commons/Radio';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useUpdateContext } from '@/hooks/update.js';
|
||||
import {
|
||||
UpdateManifest,
|
||||
Version,
|
||||
type ChannelName,
|
||||
} from '@slimevr/update-manifest';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { compare } from 'semver';
|
||||
|
||||
const ItemContent = ({
|
||||
version,
|
||||
isLatest,
|
||||
isAlreadyInstalled,
|
||||
hasBuildsAvailableForThisPlatform,
|
||||
}: {
|
||||
version: string;
|
||||
isLatest: boolean;
|
||||
isAlreadyInstalled: boolean;
|
||||
hasBuildsAvailableForThisPlatform: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Typography>{version}</Typography>
|
||||
|
||||
{isLatest && (
|
||||
/* TODO(devminer): add translations */
|
||||
<div className="bg-background-70 px-1.5 py-0.5 rounded-md leading-[1rem] text-[0.625rem]">
|
||||
latest
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAlreadyInstalled && (
|
||||
<div className="text-yellow-background-300 flex gap-1 items-center">
|
||||
<WarningIcon className="size-5 min-w-5" />
|
||||
{/* TODO(devminer): add translations */}
|
||||
<Typography variant="standard" color="secondary">
|
||||
already installed
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{!hasBuildsAvailableForThisPlatform && (
|
||||
<div className="text-yellow-background-300 flex gap-1 items-center">
|
||||
<WarningIcon className="size-5 min-w-5" />
|
||||
{/* TODO(devminer): add translations */}
|
||||
<Typography variant="standard" color="secondary">
|
||||
no build available for your platform
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function UpdateChannelVersionOptions({
|
||||
manifest,
|
||||
channel,
|
||||
value,
|
||||
variant = 'radio',
|
||||
onSelect,
|
||||
}: {
|
||||
manifest: UpdateManifest;
|
||||
channel: ChannelName;
|
||||
value: string;
|
||||
variant: 'radio' | 'dropdown';
|
||||
onSelect: (version: string) => void;
|
||||
}) {
|
||||
const { channel: currentChannel, checkCompatibilityFromVersionInfo } =
|
||||
useUpdateContext();
|
||||
const { control, watch } = useForm<{
|
||||
version: string;
|
||||
}>({
|
||||
defaultValues: {
|
||||
version: value,
|
||||
},
|
||||
});
|
||||
const { version } = watch();
|
||||
|
||||
const ch = manifest.channels[channel];
|
||||
|
||||
useEffect(() => {
|
||||
onSelect(version);
|
||||
}, [version]);
|
||||
|
||||
if (variant == 'dropdown')
|
||||
return (
|
||||
<Dropdown
|
||||
control={control}
|
||||
name="version"
|
||||
display="block"
|
||||
direction="down"
|
||||
placeholder=""
|
||||
maxHeight="300px"
|
||||
items={Object.entries(ch.versions)
|
||||
.map(([tag, version]) => [tag as Version, version] as const)
|
||||
.sort(([a, _12], [b, _22]) => compare(b, a))
|
||||
.map(([version, versionInfo]) => {
|
||||
const res = checkCompatibilityFromVersionInfo(
|
||||
channel,
|
||||
version,
|
||||
versionInfo
|
||||
);
|
||||
|
||||
return {
|
||||
value: version,
|
||||
component: (
|
||||
<div className="flex flex-row gap-2 py-1 text-left">
|
||||
<ItemContent
|
||||
version={version}
|
||||
isLatest={ch.current_version === version}
|
||||
isAlreadyInstalled={res.alreadyInstalled}
|
||||
hasBuildsAvailableForThisPlatform={!!res.build}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{Object.entries(ch.versions)
|
||||
.map(([tag, version]) => [tag as Version, version] as const)
|
||||
.sort(([a, _12], [b, _22]) => compare(b, a))
|
||||
.map(([version, versionInfo]) => {
|
||||
const res = checkCompatibilityFromVersionInfo(
|
||||
channel,
|
||||
version,
|
||||
versionInfo
|
||||
);
|
||||
|
||||
return (
|
||||
<Radio
|
||||
key={version}
|
||||
name="version"
|
||||
control={control}
|
||||
value={version}
|
||||
className="hidden"
|
||||
>
|
||||
<div className="flex flex-row md:gap-4 gap-2">
|
||||
<ItemContent
|
||||
version={version}
|
||||
isLatest={ch.current_version === version}
|
||||
isAlreadyInstalled={res.alreadyInstalled}
|
||||
hasBuildsAvailableForThisPlatform={!!res.build}
|
||||
/>
|
||||
</div>
|
||||
</Radio>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import {
|
||||
defaultValues as defaultDevSettings,
|
||||
DeveloperModeWidgetForm,
|
||||
} from '@/components/widgets/DeveloperModeWidget';
|
||||
import { error } from '@/utils/logging';
|
||||
import { useDebouncedEffect } from './timeout';
|
||||
import { createStore, Store } from '@tauri-apps/plugin-store';
|
||||
import { useIsTauri } from './breakpoint';
|
||||
import { waitUntil } from '@/utils/a11y';
|
||||
import { error } from '@/utils/logging';
|
||||
import { type ChannelName } from '@slimevr/update-manifest';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { createStore, Store } from '@tauri-apps/plugin-store';
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import { useIsTauri } from './breakpoint';
|
||||
import { useDebouncedEffect } from './timeout';
|
||||
|
||||
export const STABLE_CHANNEL = 'stable' as ChannelName;
|
||||
|
||||
export interface WindowConfig {
|
||||
width: number;
|
||||
@@ -26,6 +29,8 @@ export enum AssignMode {
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
updateChannel: ChannelName;
|
||||
notifyOnAvailableUpdates: boolean;
|
||||
debug: boolean;
|
||||
lang: string;
|
||||
doneOnboarding: boolean;
|
||||
@@ -55,6 +60,8 @@ export interface ConfigContext {
|
||||
}
|
||||
|
||||
export const defaultConfig: Config = {
|
||||
updateChannel: STABLE_CHANNEL,
|
||||
notifyOnAvailableUpdates: true,
|
||||
lang: 'en',
|
||||
debug: false,
|
||||
doneOnboarding: false,
|
||||
|
||||
54
gui/src/hooks/update.ts
Normal file
54
gui/src/hooks/update.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
UpdateManifestChannel,
|
||||
UpdateManifestChannelVersion,
|
||||
UpdateManifestChannelVersionBuild,
|
||||
type ChannelName,
|
||||
type UpdateManifest,
|
||||
type Version,
|
||||
} from '@slimevr/update-manifest';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface UpdateContext {
|
||||
channel: ChannelName;
|
||||
notifyOnAvailableUpdates: boolean;
|
||||
|
||||
isUpToDate: boolean;
|
||||
latestVersionOnChannel: Version | null;
|
||||
|
||||
manifest: UpdateManifest | null;
|
||||
|
||||
platform: string;
|
||||
architecture: string;
|
||||
changeSystem(platform: string, architecture: string): void;
|
||||
|
||||
checkCompatibility(
|
||||
channelName: ChannelName,
|
||||
version: Version
|
||||
): {
|
||||
alreadyInstalled: boolean;
|
||||
channel: UpdateManifestChannel | null;
|
||||
version: UpdateManifestChannelVersion | null;
|
||||
build: UpdateManifestChannelVersionBuild | null;
|
||||
isInstallable: boolean;
|
||||
};
|
||||
checkCompatibilityFromVersionInfo(
|
||||
channelName: ChannelName,
|
||||
version: Version,
|
||||
versionInfo: UpdateManifestChannelVersion
|
||||
): {
|
||||
alreadyInstalled: boolean;
|
||||
build: UpdateManifestChannelVersionBuild | null;
|
||||
isInstallable: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const UpdateContextC = createContext<UpdateContext | null>(null);
|
||||
|
||||
export function useUpdateContext() {
|
||||
const context = useContext(UpdateContextC);
|
||||
if (!context) {
|
||||
throw new Error('useUpdateContext must be within a UpdateContext Provider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
90
gui/src/utils/update.ts
Normal file
90
gui/src/utils/update.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
ChannelName,
|
||||
UpdateManifest,
|
||||
UpdateManifestChannel,
|
||||
UpdateManifestChannelVersion,
|
||||
UpdateManifestChannelVersionBuild,
|
||||
Version,
|
||||
} from '@slimevr/update-manifest';
|
||||
|
||||
type System = {
|
||||
platform: string;
|
||||
architecture: string;
|
||||
};
|
||||
|
||||
export function checkVersionCompatibility(
|
||||
manifest: UpdateManifest,
|
||||
channelName: ChannelName,
|
||||
version: Version,
|
||||
currentChannelName: ChannelName,
|
||||
system: System
|
||||
): {
|
||||
alreadyInstalled: boolean;
|
||||
isInstallable: boolean;
|
||||
channel: UpdateManifestChannel | null;
|
||||
version: UpdateManifestChannelVersion | null;
|
||||
build: UpdateManifestChannelVersionBuild | null;
|
||||
} {
|
||||
const alreadyInstalled = isAlreadyInstalled(channelName, version, currentChannelName);
|
||||
|
||||
const channel = manifest.channels[channelName];
|
||||
|
||||
const versionInfo = channel.versions[version];
|
||||
if (!versionInfo)
|
||||
return {
|
||||
alreadyInstalled,
|
||||
channel,
|
||||
version: null,
|
||||
build: null,
|
||||
isInstallable: false,
|
||||
};
|
||||
|
||||
const build = findBuildForPlatformAndArchitecture(versionInfo, system);
|
||||
|
||||
return {
|
||||
alreadyInstalled,
|
||||
channel,
|
||||
version: versionInfo,
|
||||
build,
|
||||
isInstallable: !alreadyInstalled && build !== null,
|
||||
};
|
||||
}
|
||||
|
||||
function isAlreadyInstalled(
|
||||
channelName: ChannelName,
|
||||
version: Version,
|
||||
currentChannelName: ChannelName
|
||||
) {
|
||||
return currentChannelName === channelName && __VERSION_TAG__ === version;
|
||||
}
|
||||
|
||||
export function checkVersionCompatibility2(
|
||||
channelName: ChannelName,
|
||||
version: Version,
|
||||
versionInfo: UpdateManifestChannelVersion,
|
||||
currentChannelName: ChannelName,
|
||||
system: System
|
||||
) {
|
||||
const alreadyInstalled = isAlreadyInstalled(channelName, version, currentChannelName);
|
||||
|
||||
const build = findBuildForPlatformAndArchitecture(versionInfo, system);
|
||||
|
||||
return {
|
||||
alreadyInstalled,
|
||||
build,
|
||||
isInstallable: !alreadyInstalled && build !== null,
|
||||
};
|
||||
}
|
||||
|
||||
function findBuildForPlatformAndArchitecture(
|
||||
versionInfo: UpdateManifestChannelVersion,
|
||||
system: System
|
||||
) {
|
||||
const buildsForPlatform = versionInfo.builds[system.platform];
|
||||
if (!buildsForPlatform) return null;
|
||||
|
||||
const buildForPlatformAndArchitecture = buildsForPlatform[system.architecture];
|
||||
if (!buildForPlatformAndArchitecture) return null;
|
||||
|
||||
return buildForPlatformAndArchitecture;
|
||||
}
|
||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -47,6 +47,9 @@ importers:
|
||||
'@sentry/vite-plugin':
|
||||
specifier: ^2.22.7
|
||||
version: 2.22.7
|
||||
'@slimevr/update-manifest':
|
||||
specifier: ^0.0.1
|
||||
version: 0.0.1(typescript@5.6.3)
|
||||
'@tailwindcss/typography':
|
||||
specifier: ^0.5.15
|
||||
version: 0.5.15(tailwindcss@3.4.14(ts-node@9.1.1(typescript@5.6.3)))
|
||||
@@ -1083,6 +1086,9 @@ packages:
|
||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
'@slimevr/update-manifest@0.0.1':
|
||||
resolution: {integrity: sha512-SqPUQuyhSRdO2kQZG6LPHv+X6mRKzR7/oBJULSiNUQrUhT9NbqssDjGDhZX49UZX8R0lM+BK6468p+7l3oQxMQ==}
|
||||
|
||||
'@swc/core-darwin-arm64@1.6.5':
|
||||
resolution: {integrity: sha512-RGQhMdni2v1/ANQ/2K+F+QYdzaucekYBewZcX1ogqJ8G5sbPaBdYdDN1qQ4kHLCIkPtGP6qC7c71qPEqL2RidQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4271,6 +4277,14 @@ packages:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
|
||||
valibot@1.1.0:
|
||||
resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==}
|
||||
peerDependencies:
|
||||
typescript: '>=5'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
vfile-message@4.0.2:
|
||||
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
|
||||
|
||||
@@ -5337,6 +5351,12 @@ snapshots:
|
||||
|
||||
'@sindresorhus/is@5.6.0': {}
|
||||
|
||||
'@slimevr/update-manifest@0.0.1(typescript@5.6.3)':
|
||||
dependencies:
|
||||
valibot: 1.1.0(typescript@5.6.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@swc/core-darwin-arm64@1.6.5':
|
||||
optional: true
|
||||
|
||||
@@ -9032,6 +9052,10 @@ snapshots:
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
valibot@1.1.0(typescript@5.6.3):
|
||||
optionalDependencies:
|
||||
typescript: 5.6.3
|
||||
|
||||
vfile-message@4.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
Reference in New Issue
Block a user