Add basic update notifications (#9)

This commit is contained in:
V 2023-04-10 22:53:44 +02:00 committed by GitHub
parent bfb9af05b0
commit 8d51cd5029
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 305 additions and 28 deletions

View file

@ -42,6 +42,11 @@ await Promise.all([
entryPoints: ["src/preload/index.ts"], entryPoints: ["src/preload/index.ts"],
outfile: "dist/js/preload.js" outfile: "dist/js/preload.js"
}), }),
createContext({
...NodeCommonOpts,
entryPoints: ["src/updater/preload.ts"],
outfile: "dist/js/updaterPreload.js"
}),
createContext({ createContext({
...CommonOpts, ...CommonOpts,
globalName: "VencordDesktop", globalName: "VencordDesktop",

View file

@ -8,6 +8,7 @@ import "./ipc";
import { app, BrowserWindow } from "electron"; import { app, BrowserWindow } from "electron";
import { join } from "path"; import { join } from "path";
import { checkUpdates } from "updater/main";
import { ICON_PATH } from "../shared/paths"; import { ICON_PATH } from "../shared/paths";
import { once } from "../shared/utils/once"; import { once } from "../shared/utils/once";
@ -40,6 +41,7 @@ if (!app.requestSingleInstanceLock()) {
}); });
app.whenReady().then(async () => { app.whenReady().then(async () => {
checkUpdates();
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.desktop"); if (process.platform === "win32") app.setAppUserModelId("dev.vencord.desktop");
else if (process.platform === "darwin") app.dock.setIcon(ICON_PATH); else if (process.platform === "darwin") app.dock.setIcon(ICON_PATH);

View file

@ -52,8 +52,12 @@ ipcMain.handle(IpcEvents.SHOW_ITEM_IN_FOLDER, (_, path) => {
shell.showItemInFolder(path); shell.showItemInFolder(path);
}); });
ipcMain.handle(IpcEvents.FOCUS, () => { ipcMain.handle(IpcEvents.FOCUS, e => {
mainWin?.focus(); e.sender.focus();
});
ipcMain.handle(IpcEvents.CLOSE, e => {
e.sender.close();
}); });
ipcMain.handle(IpcEvents.SELECT_VENCORD_DIR, async () => { ipcMain.handle(IpcEvents.SELECT_VENCORD_DIR, async () => {

View file

@ -6,18 +6,11 @@
import { BrowserWindow } from "electron"; import { BrowserWindow } from "electron";
import { join } from "path"; import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { STATIC_DIR } from "shared/paths"; import { STATIC_DIR } from "shared/paths";
export function createSplashWindow() { export function createSplashWindow() {
const splash = new BrowserWindow({ const splash = new BrowserWindow(SplashProps);
transparent: true,
frame: false,
height: 350,
width: 300,
center: true,
resizable: false,
maximizable: false
});
splash.loadFile(join(STATIC_DIR, "splash.html")); splash.loadFile(join(STATIC_DIR, "splash.html"));

View file

@ -11,10 +11,20 @@ import { join } from "path";
import { USER_AGENT, VENCORD_FILES_DIR } from "../constants"; import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
import { downloadFile, simpleGet } from "./http"; import { downloadFile, simpleGet } from "./http";
const API_BASE = "https://api.github.com/repos/Vendicated/Vencord"; const API_BASE = "https://api.github.com";
const FILES_TO_DOWNLOAD = ["vencordDesktopMain.js", "preload.js", "vencordDesktopRenderer.js", "renderer.css"]; const FILES_TO_DOWNLOAD = ["vencordDesktopMain.js", "preload.js", "vencordDesktopRenderer.js", "renderer.css"];
export interface ReleaseData {
name: string;
tag_name: string;
html_url: string;
assets: Array<{
name: string;
browser_download_url: string;
}>;
}
export async function githubGet(endpoint: string) { export async function githubGet(endpoint: string) {
const opts: RequestOptions = { const opts: RequestOptions = {
headers: { headers: {
@ -29,13 +39,9 @@ export async function githubGet(endpoint: string) {
} }
export async function downloadVencordFiles() { export async function downloadVencordFiles() {
const release = await githubGet("/releases/latest"); const release = await githubGet("/repos/Vendicated/Vencord/releases/latest");
const data = JSON.parse(release.toString("utf-8")); const { assets } = JSON.parse(release.toString("utf-8")) as ReleaseData;
const assets = data.assets as Array<{
name: string;
browser_download_url: string;
}>;
await Promise.all( await Promise.all(
assets assets

View file

@ -4,19 +4,11 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import { ipcRenderer } from "electron";
import type { Settings } from "shared/settings"; import type { Settings } from "shared/settings";
import type { LiteralUnion } from "type-fest"; import type { LiteralUnion } from "type-fest";
import { IpcEvents } from "../shared/IpcEvents"; import { IpcEvents } from "../shared/IpcEvents";
import { invoke, sendSync } from "./typedIpcs";
function invoke<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>;
}
function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.sendSync(event, ...args) as T;
}
export const VencordDesktopNative = { export const VencordDesktopNative = {
app: { app: {

16
src/preload/typedIpcs.ts Normal file
View file

@ -0,0 +1,16 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { ipcRenderer } from "electron";
import { IpcEvents } from "shared/IpcEvents";
export function invoke<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>;
}
export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.sendSync(event, ...args) as T;
}

View file

@ -19,5 +19,11 @@ export const enum IpcEvents {
GET_SETTINGS = "VCD_GET_SETTINGS", GET_SETTINGS = "VCD_GET_SETTINGS",
SET_SETTINGS = "VCD_SET_SETTINGS", SET_SETTINGS = "VCD_SET_SETTINGS",
SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR" SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR",
UPDATER_GET_DATA = "VCD_UPDATER_GET_DATA",
UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD",
UPDATE_IGNORE = "VCD_UPDATE_IGNORE",
CLOSE = "VCD_CLOSE"
} }

View file

@ -0,0 +1,18 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import type { BrowserWindowConstructorOptions } from "electron";
export const SplashProps: BrowserWindowConstructorOptions = {
transparent: true,
frame: false,
height: 350,
width: 300,
center: true,
resizable: false,
maximizable: false,
alwaysOnTop: true
};

View file

@ -16,4 +16,5 @@ export interface Settings {
disableMinSize?: boolean; disableMinSize?: boolean;
tray?: boolean; tray?: boolean;
minimizeToTray?: boolean; minimizeToTray?: boolean;
skippedUpdate?: string;
} }

98
src/updater/main.ts Normal file
View file

@ -0,0 +1,98 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { app, BrowserWindow, ipcMain, shell } from "electron";
import { Settings } from "main/settings";
import { githubGet, ReleaseData } from "main/utils/vencordLoader";
import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths";
export interface UpdateData {
currentVersion: string;
latestVersion: string;
release: ReleaseData;
}
let updateData: UpdateData;
ipcMain.handle(IpcEvents.UPDATER_GET_DATA, () => updateData);
ipcMain.handle(IpcEvents.UPDATER_DOWNLOAD, () => {
const { assets } = updateData.release;
const url = (() => {
switch (process.platform) {
case "win32":
return assets.find(a => a.name.endsWith(".exe"))!.browser_download_url;
case "darwin":
return assets.find(a => a.name.endsWith(".dmg"))!.browser_download_url;
case "linux":
return updateData.release.html_url;
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
})();
shell.openExternal(url);
});
ipcMain.handle(IpcEvents.UPDATE_IGNORE, () => {
Settings.store.skippedUpdate = updateData.latestVersion;
});
function isOutdated(oldVersion: string, newVersion: string) {
const oldParts = oldVersion.split(".");
const newParts = newVersion.split(".");
if (oldParts.length !== newParts.length)
throw new Error(`Incompatible version strings (old: ${oldVersion}, new: ${newVersion})`);
for (let i = 0; i < oldParts.length; i++) {
const oldPart = Number(oldParts[i]);
const newPart = Number(newParts[i]);
if (isNaN(oldPart) || isNaN(newPart))
throw new Error(`Invalid version string (old: ${oldVersion}, new: ${newVersion})`);
if (oldPart < newPart) return true;
if (oldPart > newPart) return false;
}
return false;
}
export async function checkUpdates() {
// if (IS_DEV) return;
try {
const raw = await githubGet("/repos/Vencord/Desktop/releases/latest");
const data = JSON.parse(raw.toString("utf-8")) as ReleaseData;
const oldVersion = app.getVersion();
const newVersion = data.tag_name.replace(/^v/, "");
if (Settings.store.skippedUpdate !== newVersion && isOutdated(oldVersion, newVersion)) {
updateData = {
currentVersion: oldVersion,
latestVersion: newVersion,
release: data
};
openNewUpdateWindow();
}
} catch (e) {
console.error("AppUpdater: Failed to check for updates\n", e);
}
}
function openNewUpdateWindow() {
const win = new BrowserWindow({
...SplashProps,
webPreferences: {
preload: join(__dirname, "updaterPreload.js")
}
});
win.loadFile(join(STATIC_DIR, "updater.html"));
}

21
src/updater/preload.ts Normal file
View file

@ -0,0 +1,21 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vencord Desktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { contextBridge } from "electron";
import { invoke } from "preload/typedIpcs";
import { IpcEvents } from "shared/IpcEvents";
import type { UpdateData } from "./main";
contextBridge.exposeInMainWorld("Updater", {
getData: () => invoke<UpdateData>(IpcEvents.UPDATER_GET_DATA),
download: () => {
invoke<void>(IpcEvents.UPDATER_DOWNLOAD);
invoke<void>(IpcEvents.CLOSE);
},
ignore: () => invoke<void>(IpcEvents.UPDATE_IGNORE),
close: () => invoke<void>(IpcEvents.CLOSE)
});

115
static/updater.html Normal file
View file

@ -0,0 +1,115 @@
<head>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Open Sans", "Helvetica Neue", sans-serif;
margin: 0;
padding: 0;
color: rgb(219, 222, 225);
}
.wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
height: 100%;
background-color: #313338;
border-radius: 8px;
border: 1px solid #248046;
padding: 1em;
}
h1 {
text-align: center;
}
.buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5em;
margin-top: 0.25em;
}
button {
cursor: pointer;
padding: 0.5em;
color: white;
border: none;
border-radius: 3px;
font-weight: bold;
transition: filter 0.2 ease-in-out;
}
button:hover,
button:active {
filter: brightness(0.9);
}
.green {
background-color: #248046;
}
.red {
background-color: #ed4245;
}
</style>
</head>
<body>
<div class="wrapper">
<section>
<h1>Update Available</h1>
<p>There's a new update for Vencord Desktop! Update now to get new fixes and features!</p>
<p>
Current: <span id="current"></span>
<br />
Latest: <span id="latest"></span>
</p>
</section>
<section>
<label id="disable-remind">
<input type="checkbox" />
<span>Do not remind again for </span>
</label>
<div class="buttons">
<button name="download" class="green">Download Update</button>
<button name="close" class="red">Close</button>
</div>
</section>
</div>
</body>
<script type="module">
const data = await Updater.getData();
document.getElementById("current").textContent = data.currentVersion;
document.getElementById("latest").textContent = data.latestVersion;
document.querySelector("#disable-remind > span").textContent += data.latestVersion;
function checkDisableRemind() {
const checkbox = document.querySelector("#disable-remind > input");
if (checkbox.checked) {
Updater.ignore();
}
}
const onClicks = {
download() {
checkDisableRemind();
Updater.download();
},
close() {
checkDisableRemind();
Updater.close();
}
};
for (const name in onClicks) {
document.querySelectorAll(`button[name="${name}"]`).forEach(button => {
button.addEventListener("click", onClicks[name]);
});
}
</script>