From f45d6482acf6fa2bf035fe9e4b47a145874b7356 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 4 Apr 2023 00:41:52 +0200 Subject: [PATCH] Add Vencord Loading & tray icon --- src/main/constants.ts | 7 +++++ src/main/index.ts | 20 +++++++++--- src/main/ipc.ts | 8 +++++ src/main/mainWindow.ts | 39 +++++++++++++++++++++-- src/main/splash.ts | 2 +- src/main/utils/http.ts | 41 ++++++++++++++++++++++++ src/main/utils/vencordLoader.ts | 54 ++++++++++++++++++++++++++++++++ src/preload/index.ts | 5 +-- src/shared/IpcEvents.ts | 1 + src/shared/util.ts | 2 -- src/shared/utils/once.ts | 8 +++++ static/icon.ico | Bin 0 -> 12368 bytes 12 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 src/main/constants.ts create mode 100644 src/main/utils/http.ts create mode 100644 src/main/utils/vencordLoader.ts delete mode 100644 src/shared/util.ts create mode 100644 src/shared/utils/once.ts create mode 100644 static/icon.ico diff --git a/src/main/constants.ts b/src/main/constants.ts new file mode 100644 index 0000000..7c39526 --- /dev/null +++ b/src/main/constants.ts @@ -0,0 +1,7 @@ +import { app } from "electron"; +import { join } from "path"; + +export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? join(app.getPath("userData"), "VencordDesktop"); +export const VENCORD_FILES_DIR = join(DATA_DIR, "vencordDist"); + +export const USER_AGENT = `VencordDesktop/${app.getVersion()} (https://github.com/Vencord/Electron)`; diff --git a/src/main/index.ts b/src/main/index.ts index 8d150ef..57e5535 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,21 +3,33 @@ import { createMainWindow } from "./mainWindow"; import { createSplashWindow } from "./splash"; import { join } from "path"; + +import { DATA_DIR, VENCORD_FILES_DIR } from "./constants"; + +import { once } from "../shared/utils/once"; import "./ipc"; +import { ensureVencordFiles } from "./utils/vencordLoader"; -require(join(__dirname, "Vencord/main.js")); +// Make the Vencord files use our DATA_DIR +process.env.VENCORD_USER_DATA_DIR = DATA_DIR; -function createWindows() { - const mainWindow = createMainWindow(); +const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "main.js"))); + +async function createWindows() { const splash = createSplashWindow(); + await ensureVencordFiles(); + runVencordMain(); + + const mainWindow = createMainWindow(); + mainWindow.once("ready-to-show", () => { splash.destroy(); mainWindow.show(); }); } -app.whenReady().then(() => { +app.whenReady().then(async () => { createWindows(); app.on('activate', () => { diff --git a/src/main/ipc.ts b/src/main/ipc.ts index e69de29..7661134 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -0,0 +1,8 @@ +import { ipcMain } from "electron"; +import { join } from "path"; +import { GET_PRELOAD_FILE } from "../shared/IpcEvents"; +import { VENCORD_FILES_DIR } from "./constants"; + +ipcMain.on(GET_PRELOAD_FILE, e => { + e.returnValue = join(VENCORD_FILES_DIR, "preload.js"); +}); diff --git a/src/main/mainWindow.ts b/src/main/mainWindow.ts index d8bf999..57103c4 100644 --- a/src/main/mainWindow.ts +++ b/src/main/mainWindow.ts @@ -1,7 +1,9 @@ -import { BrowserWindow } from "electron"; +import { BrowserWindow, Menu, Tray, app } from "electron"; import { join } from "path"; export function createMainWindow() { + let isQuitting = false; + const win = new BrowserWindow({ show: false, webPreferences: { @@ -10,9 +12,42 @@ export function createMainWindow() { contextIsolation: true, devTools: true, preload: join(__dirname, "preload.js") - } + }, + icon: join(__dirname, "..", "..", "static", "icon.ico") }); + app.on("before-quit", () => { + isQuitting = true; + }); + + win.on("close", e => { + if (isQuitting) return; + + e.preventDefault(); + win.hide(); + + return false; + }); + + const tray = new Tray(join(__dirname, "..", "..", "static", "icon.ico")); + tray.setToolTip("Vencord Desktop"); + tray.setContextMenu(Menu.buildFromTemplate([ + { + label: "Open", + click() { + win.show(); + } + }, + { + label: "Quit", + click() { + isQuitting = true; + app.quit(); + } + } + ])); + tray.on("click", () => win.show()); + win.loadURL("https://discord.com/app"); return win; diff --git a/src/main/splash.ts b/src/main/splash.ts index e878f38..ad4502a 100644 --- a/src/main/splash.ts +++ b/src/main/splash.ts @@ -12,7 +12,7 @@ export function createSplashWindow() { maximizable: false }); - splash.loadFile(join(__dirname, "..", "static", "splash.html")); + splash.loadFile(join(__dirname, "..", "..", "static", "splash.html")); return splash; } diff --git a/src/main/utils/http.ts b/src/main/utils/http.ts new file mode 100644 index 0000000..fcbbb74 --- /dev/null +++ b/src/main/utils/http.ts @@ -0,0 +1,41 @@ +import { createWriteStream } from "fs"; +import type { IncomingMessage } from "http"; +import { RequestOptions, get } from "https"; +import { finished } from "stream/promises"; + +export async function downloadFile(url: string, file: string, options: RequestOptions = {}) { + const res = await simpleReq(url, options); + await finished( + res.pipe(createWriteStream(file, { + autoClose: true + })) + ); +} + +export function simpleReq(url: string, options: RequestOptions = {}) { + return new Promise((resolve, reject) => { + get(url, options, res => { + const { statusCode, statusMessage, headers } = res; + if (statusCode! >= 400) + return void reject(`${statusCode}: ${statusMessage} - ${url}`); + if (statusCode! >= 300) + return simpleReq(headers.location!, options) + .then(resolve) + .catch(reject); + + resolve(res); + }); + }); +} + +export async function simpleGet(url: string, options: RequestOptions = {}) { + const res = await simpleReq(url, options); + + return new Promise((resolve, reject) => { + const chunks = [] as Buffer[]; + + res.once("error", reject); + res.on("data", chunk => chunks.push(chunk)); + res.once("end", () => resolve(Buffer.concat(chunks))); + }); +} diff --git a/src/main/utils/vencordLoader.ts b/src/main/utils/vencordLoader.ts new file mode 100644 index 0000000..bac666b --- /dev/null +++ b/src/main/utils/vencordLoader.ts @@ -0,0 +1,54 @@ +import { existsSync, mkdirSync } from "fs"; +import { join } from "path"; +import { USER_AGENT, VENCORD_FILES_DIR } from "../constants"; +import { downloadFile, simpleGet } from "./http"; + +// TODO: Setting to switch repo +const API_BASE = "https://api.github.com/repos/Vendicated/VencordDev"; + +const FILES_TO_DOWNLOAD = [ + "vencordDesktopMain.js", + "preload.js", + "vencordDesktopRenderer.js", + "renderer.css" +]; + +export async function githubGet(endpoint: string) { + return simpleGet(API_BASE + endpoint, { + headers: { + Accept: "application/vnd.github+json", + "User-Agent": USER_AGENT + } + }); +} + +export async function downloadVencordFiles() { + const release = await githubGet("/releases/latest"); + + const data = JSON.parse(release.toString("utf-8")); + const assets = data.assets as Array<{ + name: string; + browser_download_url: string; + }>; + + await Promise.all( + assets + .filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f))) + .map(({ name, browser_download_url }) => + downloadFile( + browser_download_url, + join( + VENCORD_FILES_DIR, + name.replace(/vencordDesktop(\w)/, (_, c) => c.toLowerCase()) + ) + ) + ) + ); +} + +export async function ensureVencordFiles() { + if (existsSync(join(VENCORD_FILES_DIR, "main.js"))) return; + mkdirSync(VENCORD_FILES_DIR, { recursive: true }); + + await downloadVencordFiles(); +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 1157456..28a6286 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,3 +1,4 @@ -import { join } from "path"; +import { ipcRenderer } from "electron"; +import { GET_PRELOAD_FILE } from "../shared/IpcEvents"; -require(join(__dirname, "Vencord/preload.js")); +require(ipcRenderer.sendSync(GET_PRELOAD_FILE)); diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index e69de29..373e29e 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -0,0 +1 @@ +export const GET_PRELOAD_FILE = "VCD_GET_PRELOAD_FILE"; diff --git a/src/shared/util.ts b/src/shared/util.ts deleted file mode 100644 index 139597f..0000000 --- a/src/shared/util.ts +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/shared/utils/once.ts b/src/shared/utils/once.ts new file mode 100644 index 0000000..3da6768 --- /dev/null +++ b/src/shared/utils/once.ts @@ -0,0 +1,8 @@ +export function once(fn: T): T { + let called = false; + return function (this: any, ...args: any[]) { + if (called) return; + called = true; + return fn.apply(this, args); + } as any; +} diff --git a/static/icon.ico b/static/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1dc9894dae524195e6c5a7301084712470a9bc34 GIT binary patch literal 12368 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 l=j8v{!J7c^ae${M=DDpoQxX$i@R