Basic version update checking (#690)

Co-authored-by: Uriel <urielfontan2002@gmail.com>
This commit is contained in:
Erimel
2023-05-19 13:21:30 -04:00
committed by GitHub
parent d652c3eb9b
commit 64b0dceb50
10 changed files with 312 additions and 26 deletions

1
Cargo.lock generated
View File

@@ -2943,6 +2943,7 @@ dependencies = [
"ignore",
"objc",
"once_cell",
"open",
"os_info",
"os_pipe",
"percent-encoding",

View File

@@ -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"

View File

@@ -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!

View File

@@ -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"

View File

@@ -56,7 +56,8 @@
"allowlist": {
"shell": {
"all": false,
"execute": true
"execute": true,
"open": true
},
"fs": {
"scope": ["$APP/*", "$APP"],

View File

@@ -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 (
<>
<SerialDetectionModal></SerialDetectionModal>
<VersionUpdateModal></VersionUpdateModal>
<Routes>
<Route
path="/"
@@ -129,6 +134,19 @@ const MIN_SIZE = { width: 880, height: 740 };
export default function App() {
const websocketAPI = useProvideWebsocketApi();
const { l10n } = useLocalization();
const [updateFound, setUpdateFound] = useState('');
useEffect(() => {
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() {
<WebSocketApiContext.Provider value={websocketAPI}>
<AppContextProvider>
<OnboardingContextProvider>
<div className="h-full w-full text-standard bg-background-80 text-background-10">
<div className="flex-col h-full">
{!websocketAPI.isConnected && (
<>
<TopBar></TopBar>
<div className="flex w-full h-full justify-center items-center p-2">
{websocketAPI.isFirstConnection
? l10n.getString('websocket-connecting')
: l10n.getString('websocket-connection_lost')}
</div>
</>
)}
{websocketAPI.isConnected && <Layout></Layout>}
<VersionContext.Provider value={updateFound}>
<div className="h-full w-full text-standard bg-background-80 text-background-10">
<div className="flex-col h-full">
{!websocketAPI.isConnected && (
<>
<TopBar></TopBar>
<div className="flex w-full h-full justify-center items-center p-2">
{websocketAPI.isFirstConnection
? l10n.getString('websocket-connecting')
: l10n.getString('websocket-connection_lost')}
</div>
</>
)}
{websocketAPI.isConnected && <Layout></Layout>}
</div>
</div>
</div>
</VersionContext.Provider>
</OnboardingContextProvider>
</AppContextProvider>
</WebSocketApiContext.Provider>

View File

@@ -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<string | null>(null);
const doesMatchSettings = useMatch({
path: '/settings/*',
@@ -54,15 +59,43 @@ export function TopBar({
<div className="flex justify-around flex-col" data-tauri-drag-region>
<Typography>SlimeVR</Typography>
</div>
<div className="flex justify-around flex-col text-standard-bold text-status-success bg-status-success bg-opacity-20 rounded-lg px-3 select-text">
<div
className={classNames(
'flex justify-around flex-col text-standard-bold',
'text-status-success bg-status-success bg-opacity-20 rounded-lg',
'px-3 select-text cursor-pointer'
)}
onClick={() => {
const url = `https://github.com/${GH_REPO}/releases`;
open(url).catch(() => window.open(url, '_blank'));
}}
>
{(__VERSION_TAG__ || __COMMIT_HASH__) +
(__GIT_CLEAN__ ? '' : '-dirty')}
</div>
{doesMatchSettings && (
<div className="flex justify-around flex-col text-standard-bold text-status-special bg-status-special bg-opacity-20 rounded-lg px-3 select-text">
<div
className={classNames(
'flex justify-around flex-col text-standard-bold text-status-special',
'bg-status-special bg-opacity-20 rounded-lg px-3 select-text'
)}
>
{localIp || 'unknown local ip'}
</div>
)}
{version && (
<div
className="cursor-pointer"
onClick={() => {
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'));
}}
>
<DownloadIcon></DownloadIcon>
</div>
)}
</div>
</div>
<div

View File

@@ -0,0 +1,206 @@
import { useLocalization } from '@fluent/react';
import { useContext, useState } from 'react';
import { BaseModal } from './commons/BaseModal';
import { Button } from './commons/Button';
import { Typography } from './commons/Typography';
import { open } from '@tauri-apps/api/shell';
import semver from 'semver';
import { GH_REPO, VersionContext } from '../App';
export function VersionUpdateModal() {
const { l10n } = useLocalization();
const newVersion = useContext(VersionContext);
const [forceClose, setForceClose] = useState(false);
const closeModal = () => {
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 (
<BaseModal
isOpen={!forceClose && !!newVersion && isVersionNew}
onRequestClose={closeModal}
>
<div className="flex flex-col gap-3">
<>
<div className="flex flex-col items-center gap-3 fill-accent-background-20">
<div className="flex flex-col items-center gap-2">
<Typography variant="main-title">
{l10n.getString('version_update-title', {
version: newVersion,
})}
</Typography>
<Typography variant="standard">
{l10n.getString('version_update-description')}
</Typography>
</div>
</div>
<Button
variant="primary"
onClick={async () => {
const url = document.body.classList.contains('windows_nt')
? 'https://slimevr.dev/download'
: `https://github.com/${GH_REPO}/releases/latest`;
await open(url).catch(() => window.open(url, '_blank'));
closeModal();
}}
>
{l10n.getString('version_update-update')}
</Button>
<Button variant="tertiary" onClick={closeModal}>
{l10n.getString('version_update-close')}
</Button>
</>
</div>
</BaseModal>
);
}
/**
* 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;
}

View File

@@ -0,0 +1,16 @@
export function DownloadIcon({ width = 22 }: { width?: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={width}
className="fill-status-success stroke-status-success"
>
<path
fillRule="evenodd"
d="M12 2.25a.75.75 0 01.75.75v11.69l3.22-3.22a.75.75 0 111.06 1.06l-4.5 4.5a.75.75 0 01-1.06 0l-4.5-4.5a.75.75 0 111.06-1.06l3.22 3.22V3a.75.75 0 01.75-.75zm-9 13.5a.75.75 0 01.75.75v2.25a1.5 1.5 0 001.5 1.5h13.5a1.5 1.5 0 001.5-1.5V16.5a.75.75 0 011.5 0v2.25a3 3 0 01-3 3H5.25a3 3 0 01-3-3V16.5a.75.75 0 01.75-.75z"
clipRule="evenodd"
/>
</svg>
);
}

14
package-lock.json generated
View File

@@ -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",