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 0000000..1dc9894 Binary files /dev/null and b/static/icon.ico differ