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