diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 424bbbfb1..f47fe196d 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -266,6 +266,11 @@ tracker-settings-name_section-label = Tracker name tracker-settings-forget = Forget tracker tracker-settings-forget-description = Removes the tracker from the SlimeVR Server and prevent it from connecting to it until the server is restarted. The configuration of the tracker won't be lost. tracker-settings-forget-label = Forget tracker +tracker-settings-update-unavailable = Cannot be updated (DIY) +tracker-settings-update-up_to_date = Up to date +tracker-settings-update-available = { $versionName } is now available +tracker-settings-update = Update now +tracker-settings-update-title = Firmware version ## Tracker part card info tracker-part_card-no_name = No name @@ -1210,8 +1215,8 @@ firmware-update_status_ERROR_UNKNOWN = Unknown error ## Dedicated Firmware Update Page firmware-update_title = Firmware update firmware-update_devices = Available Devices -firmware-update_devices_desc = Please select the trackers you want to update to the lastest version of SlimeVR firmware -firmware-update_no-devices = Plase make sure that the trackers you want to update are ON and connected to Wi-Fi! +firmware-update_devices_desc = Please select the trackers you want to update to the latest version of SlimeVR firmware +firmware-update_no-devices = Plase make sure that the trackers you want to update are ON and connected to the Wi-Fi! firmware-update_changelog_title = Updating to {$version} firmware-update_looking-for-devices = Looking for devices to update... firmware-update_retry = Retry diff --git a/gui/src/components/commons/A.tsx b/gui/src/components/commons/A.tsx index 6b42733dd..9085b8a97 100644 --- a/gui/src/components/commons/A.tsx +++ b/gui/src/components/commons/A.tsx @@ -1,11 +1,13 @@ import { open } from '@tauri-apps/plugin-shell'; import { ReactNode } from 'react'; -export function A({ href, children }: { href: string; children?: ReactNode }) { +export function A({ href, children }: { href?: string; children?: ReactNode }) { return ( open(href).catch(() => window.open(href, '_blank'))} + onClick={() => + href && open(href).catch(() => window.open(href, '_blank')) + } className="underline" > {children} diff --git a/gui/src/components/firmware-update/FirmwareUpdate.tsx b/gui/src/components/firmware-update/FirmwareUpdate.tsx index 3df4a88c2..a87a512a3 100644 --- a/gui/src/components/firmware-update/FirmwareUpdate.tsx +++ b/gui/src/components/firmware-update/FirmwareUpdate.tsx @@ -10,6 +10,7 @@ import { FirmwareUpdateStatus, FirmwareUpdateStatusResponseT, FirmwareUpdateStopQueuesRequestT, + HardwareInfoT, RpcMessage, TrackerStatus, } from 'solarxr-protocol'; @@ -19,7 +20,7 @@ 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 { FirmwareRelease, useAppContext } from '@/hooks/app'; import { DeviceCardControl } from '@/components/firmware-tool/DeviceCard'; import { Control, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; @@ -32,6 +33,23 @@ import { import { yupResolver } from '@hookform/resolvers/yup'; import { object } from 'yup'; import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon'; +import { A } from '@/components/commons/A'; + +export function checkForUpdate( + currentFirmwareRelease: FirmwareRelease, + hardwareInfo: HardwareInfoT +) { + return ( + // TODO: This is temporary, end goal is to support all board types + hardwareInfo.officialBoardType === BoardType.SLIMEVR && + semver.valid(currentFirmwareRelease.version) && + semver.valid(hardwareInfo.firmwareVersion?.toString() ?? 'none') && + semver.lt( + hardwareInfo.firmwareVersion?.toString() ?? 'none', + currentFirmwareRelease.version + ) + ); +} interface FirmwareUpdateForm { selectedDevices: { [key: string]: boolean }; @@ -88,9 +106,7 @@ const StatusList = ({ status }: { status: Record }) => { }; const MarkdownLink = (props: ComponentProps<'a'>) => ( - - {props.children} - + {props.children} ); export function FirmwareUpdate() { @@ -105,15 +121,10 @@ export function FirmwareUpdate() { state.datafeed?.devices.filter( ({ trackers, hardwareInfo }) => trackers.length > 0 && - hardwareInfo?.officialBoardType === BoardType.SLIMEVR && currentFirmwareRelease && - semver.valid(currentFirmwareRelease.version) && - semver.valid(hardwareInfo?.firmwareVersion?.toString() ?? 'none') && - semver.lt( - hardwareInfo?.firmwareVersion?.toString() ?? 'none', - currentFirmwareRelease.version - ) && - trackers.every(({ status }) => status == TrackerStatus.OK) + hardwareInfo && + checkForUpdate(currentFirmwareRelease, hardwareInfo) && + trackers.every(({ status }) => status === TrackerStatus.OK) ) || []; useRPCPacket( @@ -127,7 +138,7 @@ export function FirmwareUpdate() { if (!id) throw new Error('invalid device id'); const selectedDevice = selectedDevices?.find( - ({ deviceId }) => deviceId == id.toString() + ({ deviceId }) => deviceId === id.toString() ); // We skip the status as it can be old trackers still sending status @@ -186,6 +197,7 @@ export function FirmwareUpdate() { useEffect(() => { if (!currentFirmwareRelease) { navigate('/'); + return; } return () => { clear(); @@ -196,7 +208,6 @@ export function FirmwareUpdate() { clear(); const firmwareFile = currentFirmwareRelease?.firmwareFile; if (!firmwareFile) throw new Error('invalid state - no firmware file'); - console.log(firmwareFile); const requests = getFlashingRequests( selectedDevices, [{ isFirmware: true, firmwareId: '', url: firmwareFile, offset: 0 }], @@ -209,8 +220,12 @@ export function FirmwareUpdate() { }); }; - const trackerWithErrors = Object.keys(status).filter((id) => - firmwareUpdateErrorStatus.includes(status[id].status) + const trackerWithErrors = useMemo( + () => + Object.keys(status).filter((id) => + firmwareUpdateErrorStatus.includes(status[id].status) + ), + [status] ); const hasPendingTrackers = useMemo( @@ -252,7 +267,7 @@ export function FirmwareUpdate() { reset({ selectedDevices: devices.reduce( - (curr, { deviceId }) => ({ ...curr, [deviceId]: false }), + (curr, { deviceId }) => ({ ...curr, [deviceId]: true }), {} ), }); @@ -263,7 +278,7 @@ export function FirmwareUpdate() { const selectedDevices = Object.keys(selectedDevicesForm) .filter((d) => selectedDevicesForm[d]) .map((id) => { - const device = devices.find(({ id: dId }) => id == dId?.id.toString()); + const device = devices.find(({ id: dId }) => id === dId?.id.toString()); if (!device) throw new Error('no device found'); return { @@ -285,22 +300,20 @@ export function FirmwareUpdate() { const statusKeys = Object.keys(status); return ( -
-
+
+
-
+
-
+
{devices.length === 0 && !hasPendingTrackers && statusKeys.length == 0 && ( @@ -309,7 +322,7 @@ export function FirmwareUpdate() { )} {shouldShowRebootWarning && ( - + Warning )} @@ -346,7 +359,9 @@ export function FirmwareUpdate() { 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-a:text-background-20 prose-strong:text-background-10 prose-code:text-background-20' + 'w-full text-sm prose-xl prose text-background-10 prose-h1:text-background-10', + 'prose-h2:text-background-10 prose-a:text-background-20 prose-strong:text-background-10', + 'prose-code:text-background-20' )} > {currentFirmwareRelease?.changelog} @@ -354,7 +369,7 @@ export function FirmwareUpdate() {
-
+
diff --git a/gui/src/components/tracker/TrackerCard.tsx b/gui/src/components/tracker/TrackerCard.tsx index b67287b9e..b8350f476 100644 --- a/gui/src/components/tracker/TrackerCard.tsx +++ b/gui/src/components/tracker/TrackerCard.tsx @@ -1,6 +1,5 @@ import { MouseEventHandler } from 'react'; import { - BoardType, DeviceDataT, TrackerDataT, TrackerStatus as TrackerStatusEnum, @@ -13,9 +12,9 @@ import classNames from 'classnames'; import { useTracker } from '@/hooks/tracker'; import { BodyPartIcon } from '@/components/commons/BodyPartIcon'; import { DownloadIcon } from '@/components/commons/icon/DownloadIcon'; -import semver from 'semver'; import { Link } from 'react-router-dom'; import { useAppContext } from '@/hooks/app'; +import { checkForUpdate } from '@/components/firmware-update/FirmwareUpdate'; function TrackerBig({ device, @@ -169,17 +168,9 @@ export function TrackerCard({
{showUpdates && tracker.status !== TrackerStatusEnum.DISCONNECTED && - // This is temporary, end goal is to support all board types - device?.hardwareInfo?.officialBoardType === BoardType.SLIMEVR && currentFirmwareRelease && - semver.valid(currentFirmwareRelease.version) && - semver.valid( - device?.hardwareInfo?.firmwareVersion?.toString() ?? 'none' - ) && - semver.lt( - device?.hardwareInfo?.firmwareVersion?.toString() ?? 'none', - currentFirmwareRelease.version - ) && ( + device?.hardwareInfo && + checkForUpdate(currentFirmwareRelease, device.hardwareInfo) && (
diff --git a/gui/src/components/tracker/TrackerSettings.tsx b/gui/src/components/tracker/TrackerSettings.tsx index aba33e3e9..0062943df 100644 --- a/gui/src/components/tracker/TrackerSettings.tsx +++ b/gui/src/components/tracker/TrackerSettings.tsx @@ -1,4 +1,4 @@ -import { useLocalization } from '@fluent/react'; +import { Localized, useLocalization } from '@fluent/react'; import classNames from 'classnames'; import { IPv4 } from 'ip-num/IPNumber'; import { useEffect, useMemo, useState } from 'react'; @@ -38,6 +38,7 @@ import { Quaternion } from 'three'; import { useAppContext } from '@/hooks/app'; import { MagnetometerToggleSetting } from '@/components/settings/pages/MagnetometerToggleSetting'; import semver from 'semver'; +import { checkForUpdate } from '@/components/firmware-update/FirmwareUpdate'; const rotationsLabels: [Quaternion, string][] = [ [rotationToQuatMap.BACK, 'tracker-rotation-back'], @@ -186,16 +187,9 @@ export function TrackerSettingsPage() { const { currentFirmwareRelease } = useAppContext(); const needUpdate = - tracker?.device?.hardwareInfo?.officialBoardType === BoardType.SLIMEVR && currentFirmwareRelease && - semver.valid(currentFirmwareRelease.version) && - semver.valid( - tracker?.device?.hardwareInfo?.firmwareVersion?.toString() ?? 'none' - ) && - semver.lt( - tracker?.device?.hardwareInfo?.firmwareVersion?.toString() ?? 'none', - currentFirmwareRelease.version - ); + tracker?.device?.hardwareInfo && + checkForUpdate(currentFirmwareRelease, tracker?.device?.hardwareInfo); const updateUnavailable = tracker?.device?.hardwareInfo?.officialBoardType !== BoardType.SLIMEVR || !semver.valid( @@ -231,33 +225,50 @@ export function TrackerSettingsPage() { )} {
- Firmware version + + + Firmware version + +
v{tracker?.device?.hardwareInfo?.firmwareVersion} - {updateUnavailable && ( - Cannot be updated (DIY) + + Cannot be updated (DIY) + )} {!updateUnavailable && ( <> - {!needUpdate && Up to date} + {!needUpdate && ( + + Up to date + + )} {needUpdate && ( - - New version available {currentFirmwareRelease?.name} - + + + New version available + + )} )}
- + + +
} diff --git a/gui/src/hooks/app.ts b/gui/src/hooks/app.ts index c2cb8574e..13e2150c9 100644 --- a/gui/src/hooks/app.ts +++ b/gui/src/hooks/app.ts @@ -135,7 +135,7 @@ export function useProvideAppContext(): AppContext { () => fetch('https://api.github.com/repos/SlimeVR/SlimeVR-Tracker-ESP/releases') .then((res) => res.text()) - .catch((_) => 'null'), + .catch(() => 'null'), 1000 * 60 * 60 ) ); @@ -159,7 +159,7 @@ export function useProvideAppContext(): AppContext { if (firstRelease) { return { name: firstRelease.name, - version: version, + version, changelog: firstRelease.body, firmwareFile: firstRelease.assets.find( (asset: any) => diff --git a/gui/src/hooks/cache.ts b/gui/src/hooks/cache.ts index 237bcf136..371424d04 100644 --- a/gui/src/hooks/cache.ts +++ b/gui/src/hooks/cache.ts @@ -56,7 +56,7 @@ export async function cacheWrap( const realItem = await store.get(key); if (!realItem) { const defaultItem = await orDefault(); - cacheSet(key, defaultItem, ttl); + await cacheSet(key, defaultItem, ttl); return defaultItem; } else { return (await cacheGet(key))!; diff --git a/gui/src/index.scss b/gui/src/index.scss index 8ed7c77c7..6bbdd8876 100644 --- a/gui/src/index.scss +++ b/gui/src/index.scss @@ -382,6 +382,14 @@ body { background: theme('colors.background.60'); } +.bg-background-60::-webkit-scrollbar-thumb:hover { + background: theme('colors.background.40'); +} + +.bg-background-60 { + scrollbar-color: theme('colors.background.50') transparent; +} + .dropdown-scroll { scrollbar-color: theme('colors.background.40') theme('colors.background.50'); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d937e843..6162293df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,7 +191,7 @@ importers: version: 8.57.1 eslint-config-airbnb: specifier: ^19.0.4 - version: 19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.1(eslint@8.57.1))(eslint@8.57.1) + version: 19.0.4(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.1(eslint@8.57.1))(eslint@8.57.1) eslint-import-resolver-typescript: specifier: ^3.6.3 version: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1) @@ -6195,7 +6195,7 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1): + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.1 @@ -6204,10 +6204,10 @@ snapshots: object.entries: 1.1.8 semver: 6.3.1 - eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.1(eslint@8.57.1))(eslint@8.57.1): + eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0)(eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.1(eslint@8.57.1))(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1) eslint-plugin-react: 7.37.1(eslint@8.57.1) @@ -6229,7 +6229,7 @@ snapshots: debug: 4.3.5 enhanced-resolve: 5.17.0 eslint: 8.57.1 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-bun-module: 1.2.1 @@ -6242,7 +6242,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -6253,7 +6253,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -6274,7 +6274,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3