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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 literal 0 HcmV?d00001