From 64b0dceb50836fe8472349da0ceeef3c39a1bcd2 Mon Sep 17 00:00:00 2001 From: Erimel Date: Fri, 19 May 2023 13:21:30 -0400 Subject: [PATCH] Basic version update checking (#690) Co-authored-by: Uriel --- Cargo.lock | 1 + gui/package.json | 1 + gui/public/i18n/en/translation.ftl | 6 + gui/src-tauri/Cargo.toml | 2 +- gui/src-tauri/tauri.conf.json | 3 +- gui/src/App.tsx | 50 +++-- gui/src/components/TopBar.tsx | 39 +++- gui/src/components/VersionUpdateModal.tsx | 206 ++++++++++++++++++ .../components/commons/icon/DownloadIcon.tsx | 16 ++ package-lock.json | 14 +- 10 files changed, 312 insertions(+), 26 deletions(-) create mode 100644 gui/src/components/VersionUpdateModal.tsx create mode 100644 gui/src/components/commons/icon/DownloadIcon.tsx diff --git a/Cargo.lock b/Cargo.lock index 943337d85..178f76d25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2943,6 +2943,7 @@ dependencies = [ "ignore", "objc", "once_cell", + "open", "os_info", "os_pipe", "percent-encoding", diff --git a/gui/package.json b/gui/package.json index afeeff942..e823e2d1b 100644 --- a/gui/package.json +++ b/gui/package.json @@ -27,6 +27,7 @@ "react-hook-form": "^7.29.0", "react-modal": "3.15.1", "react-router-dom": "^6.2.2", + "semver": "^7.5.0", "solarxr-protocol": "file:../solarxr-protocol", "three": "^0.148.0", "typescript": "^4.6.3" diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 08d884a95..df4194415 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -9,6 +9,12 @@ websocket-connecting = Connecting to the server websocket-connection_lost = Connection lost to the server. Trying to reconnect... +## Update notification +version_update-title = New version available: { $version } +version_update-description = Clicking "Update" will download the SlimeVR installer for you. +version_update-update = Update +version_update-close = Close + ## Tips tips-find_tracker = Not sure which tracker is which? Shake a tracker and it will highlight the corresponding item. tips-do_not_move_heels = Ensure your heels do not move during recording! diff --git a/gui/src-tauri/Cargo.toml b/gui/src-tauri/Cargo.toml index 9c8152cff..29ab769b9 100644 --- a/gui/src-tauri/Cargo.toml +++ b/gui/src-tauri/Cargo.toml @@ -28,7 +28,7 @@ shadow-rs = "0.21" [dependencies] serde_json = "1" serde = { version = "1", features = ["derive"] } -tauri = { version = "1.2", features = ["devtools", "dialog", "fs-all", "os-all", "path-all", "shell-execute", "window-close", "window-maximize", "window-minimize", "window-set-resizable", "window-set-size", "window-set-title", "window-start-dragging", "window-unmaximize", "window-unminimize"] } +tauri = { version = "1.2", features = ["devtools", "dialog", "fs-all", "os-all", "path-all", "shell-execute", "shell-open", "window-close", "window-maximize", "window-minimize", "window-set-resizable", "window-set-size", "window-set-title", "window-start-dragging", "window-unmaximize", "window-unminimize"] } tauri-runtime = "0.12.1" tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } pretty_env_logger = "0.4" diff --git a/gui/src-tauri/tauri.conf.json b/gui/src-tauri/tauri.conf.json index 87e887f03..5e1904d9b 100644 --- a/gui/src-tauri/tauri.conf.json +++ b/gui/src-tauri/tauri.conf.json @@ -56,7 +56,8 @@ "allowlist": { "shell": { "all": false, - "execute": true + "execute": true, + "open": true }, "fs": { "scope": ["$APP/*", "$APP"], diff --git a/gui/src/App.tsx b/gui/src/App.tsx index d8fe2217b..69e6e16d4 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { createContext, useEffect, useState } from 'react'; import { BrowserRouter as Router, Outlet, @@ -43,6 +43,10 @@ import { VMCSettings } from './components/settings/pages/VMCSettings'; import { MountingChoose } from './components/onboarding/pages/mounting/MountingChoose'; import { ProportionsChoose } from './components/onboarding/pages/body-proportions/ProportionsChoose'; import { LogicalSize, appWindow } from '@tauri-apps/api/window'; +import { Release, VersionUpdateModal } from './components/VersionUpdateModal'; + +export const GH_REPO = 'SlimeVR/SlimeVR-Server'; +export const VersionContext = createContext(''); import { CalibrationTutorialPage } from './components/onboarding/pages/CalibrationTutorial'; function Layout() { @@ -52,6 +56,7 @@ function Layout() { return ( <> + { + async function fetchReleases() { + const releases: Release[] = await fetch( + `https://api.github.com/repos/${GH_REPO}/releases` + ).then((res) => res.json()); + + if (__VERSION_TAG__ && releases[0].tag_name !== __VERSION_TAG__) { + setUpdateFound(releases[0].tag_name); + } + } + fetchReleases().catch(() => console.error('failed to fetch releases')); + }, []); useEffect(() => { os.type() @@ -211,21 +229,23 @@ export default function App() { -
-
- {!websocketAPI.isConnected && ( - <> - -
- {websocketAPI.isFirstConnection - ? l10n.getString('websocket-connecting') - : l10n.getString('websocket-connection_lost')} -
- - )} - {websocketAPI.isConnected && } + +
+
+ {!websocketAPI.isConnected && ( + <> + +
+ {websocketAPI.isFirstConnection + ? l10n.getString('websocket-connecting') + : l10n.getString('websocket-connection_lost')} +
+ + )} + {websocketAPI.isConnected && } +
-
+ diff --git a/gui/src/components/TopBar.tsx b/gui/src/components/TopBar.tsx index 3bedeff7e..3ab59affa 100644 --- a/gui/src/components/TopBar.tsx +++ b/gui/src/components/TopBar.tsx @@ -1,5 +1,5 @@ import { appWindow } from '@tauri-apps/api/window'; -import { ReactNode, useEffect, useState } from 'react'; +import { ReactNode, useContext, useEffect, useState } from 'react'; import { NavLink, useMatch } from 'react-router-dom'; import { RpcMessage, @@ -13,6 +13,10 @@ import { MinimiseIcon } from './commons/icon/MinimiseIcon'; 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/api/shell'; +import { GH_REPO, VersionContext } from '../App'; +import classNames from 'classnames'; export function TopBar({ progress, @@ -21,6 +25,7 @@ export function TopBar({ progress?: number; }) { const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); + const version = useContext(VersionContext); const [localIp, setLocalIp] = useState(null); const doesMatchSettings = useMatch({ path: '/settings/*', @@ -54,15 +59,43 @@ export function TopBar({
SlimeVR
-
+
{ + const url = `https://github.com/${GH_REPO}/releases`; + open(url).catch(() => window.open(url, '_blank')); + }} + > {(__VERSION_TAG__ || __COMMIT_HASH__) + (__GIT_CLEAN__ ? '' : '-dirty')}
{doesMatchSettings && ( -
+
{localIp || 'unknown local ip'}
)} + {version && ( +
{ + const url = document.body.classList.contains('windows_nt') + ? 'https://slimevr.dev/download' + : `https://github.com/${GH_REPO}/releases/latest`; + open(url).catch(() => window.open(url, '_blank')); + }} + > + +
+ )}
{ + localStorage.setItem('lastVersionFound', newVersion); + setForceClose(true); + }; + let isVersionNew = false; + try { + if (newVersion) { + isVersionNew = semver.gt( + newVersion, + localStorage.getItem('lastVersionFound') || 'v0.0.0' + ); + } + } catch { + console.error('failed to parse new version'); + } + + return ( + +
+ <> +
+
+ + {l10n.getString('version_update-title', { + version: newVersion, + })} + + + {l10n.getString('version_update-description')} + +
+
+ + + + +
+
+ ); +} + +/** + * A GitHub release. + */ +export interface Release { + url: string; + html_url: string; + assets_url: string; + upload_url: string; + tarball_url: string | null; + zipball_url: string | null; + id: number; + node_id: string; + /** + * The name of the tag. + */ + tag_name: string; + /** + * Specifies the commitish value that determines where the Git tag is created from. + */ + target_commitish: string; + name: string | null; + body?: string | null; + /** + * true to create a draft (unpublished) release, false to create a published one. + */ + draft: boolean; + /** + * Whether to identify the release as a prerelease or a full release. + */ + prerelease: boolean; + created_at: string; + published_at: string | null; + author: SimpleUser; + assets: ReleaseAsset[]; + body_html?: string; + body_text?: string; + mentions_count?: number; + /** + * The URL of the release discussion. + */ + discussion_url?: string; + reactions?: ReactionRollup; + [k: string]: unknown; +} +/** + * A GitHub user. + */ +export interface SimpleUser { + name?: string | null; + email?: string | null; + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string | null; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + starred_at?: string; + [k: string]: unknown; +} +/** + * Data related to a release. + */ +export interface ReleaseAsset { + url: string; + browser_download_url: string; + id: number; + node_id: string; + /** + * The file name of the asset. + */ + name: string; + label: string | null; + /** + * State of the release asset. + */ + state: 'uploaded' | 'open'; + content_type: string; + size: number; + download_count: number; + created_at: string; + updated_at: string; + uploader: null | SimpleUser1; + [k: string]: unknown; +} +/** + * A GitHub user. + */ +export interface SimpleUser1 { + name?: string | null; + email?: string | null; + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string | null; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + starred_at?: string; + [k: string]: unknown; +} +export interface ReactionRollup { + url: string; + total_count: number; + '+1': number; + '-1': number; + laugh: number; + confused: number; + heart: number; + hooray: number; + eyes: number; + rocket: number; + [k: string]: unknown; +} diff --git a/gui/src/components/commons/icon/DownloadIcon.tsx b/gui/src/components/commons/icon/DownloadIcon.tsx new file mode 100644 index 000000000..c1e7fed14 --- /dev/null +++ b/gui/src/components/commons/icon/DownloadIcon.tsx @@ -0,0 +1,16 @@ +export function DownloadIcon({ width = 22 }: { width?: number }) { + return ( + + + + ); +} diff --git a/package-lock.json b/package-lock.json index 51e165907..e40f09a7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "react-hook-form": "^7.29.0", "react-modal": "3.15.1", "react-router-dom": "^6.2.2", + "semver": "^7.5.0", "solarxr-protocol": "file:../solarxr-protocol", "three": "^0.148.0", "typescript": "^4.6.3" @@ -8573,9 +8574,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -15354,9 +15355,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "requires": { "lru-cache": "^6.0.0" } @@ -15461,6 +15462,7 @@ "react-hook-form": "^7.29.0", "react-modal": "3.15.1", "react-router-dom": "^6.2.2", + "semver": "^7.5.0", "solarxr-protocol": "file:../solarxr-protocol", "tailwind-gradient-mask-image": "^1.0.0", "tailwindcss": "^3.3.1",