From 6b26c12bfa1f28d40834478b50d2f7b09c9f54fb Mon Sep 17 00:00:00 2001
From: V <vendicated@riseup.net>
Date: Tue, 4 Apr 2023 01:16:29 +0200
Subject: Add additional build flavours for Vencord Desktop (#765)

---
 src/Vencord.ts                                |  12 +-
 src/components/VencordSettings/Updater.tsx    |   3 +-
 src/components/VencordSettings/VencordTab.tsx |   4 +-
 src/globals.d.ts                              |   3 +
 src/ipcMain/constants.ts                      |  35 -----
 src/ipcMain/crxToZip.ts                       |  57 --------
 src/ipcMain/extensions.ts                     |  85 -----------
 src/ipcMain/index.ts                          |  99 -------------
 src/ipcMain/simpleGet.ts                      |  37 -----
 src/ipcMain/updater/common.ts                 |  59 --------
 src/ipcMain/updater/git.ts                    |  83 -----------
 src/ipcMain/updater/http.ts                   |  88 -----------
 src/ipcMain/updater/index.ts                  |  19 ---
 src/main/index.ts                             | 108 ++++++++++++++
 src/main/ipcMain.ts                           | 107 ++++++++++++++
 src/main/patchWin32Updater.ts                 |  99 +++++++++++++
 src/main/patcher.ts                           | 120 +++++++++++++++
 src/main/updater/common.ts                    |  59 ++++++++
 src/main/updater/git.ts                       |  83 +++++++++++
 src/main/updater/http.ts                      | 104 +++++++++++++
 src/main/updater/index.ts                     |  19 +++
 src/main/utils/constants.ts                   |  37 +++++
 src/main/utils/crxToZip.ts                    |  57 ++++++++
 src/main/utils/extensions.ts                  |  85 +++++++++++
 src/main/utils/simpleGet.ts                   |  37 +++++
 src/patchWin32Updater.ts                      |  99 -------------
 src/patcher.ts                                | 202 --------------------------
 src/plugins/consoleShortcuts.ts               |   3 +-
 src/plugins/settings.tsx                      |   1 +
 src/preload.ts                                |   4 +-
 src/utils/native.ts                           |  24 +++
 src/utils/settingsSync.ts                     |  42 +++---
 src/utils/updater.ts                          |   7 +-
 src/webpack/webpack.ts                        |   2 +-
 34 files changed, 988 insertions(+), 895 deletions(-)
 delete mode 100644 src/ipcMain/constants.ts
 delete mode 100644 src/ipcMain/crxToZip.ts
 delete mode 100644 src/ipcMain/extensions.ts
 delete mode 100644 src/ipcMain/index.ts
 delete mode 100644 src/ipcMain/simpleGet.ts
 delete mode 100644 src/ipcMain/updater/common.ts
 delete mode 100644 src/ipcMain/updater/git.ts
 delete mode 100644 src/ipcMain/updater/http.ts
 delete mode 100644 src/ipcMain/updater/index.ts
 create mode 100644 src/main/index.ts
 create mode 100644 src/main/ipcMain.ts
 create mode 100644 src/main/patchWin32Updater.ts
 create mode 100644 src/main/patcher.ts
 create mode 100644 src/main/updater/common.ts
 create mode 100644 src/main/updater/git.ts
 create mode 100644 src/main/updater/http.ts
 create mode 100644 src/main/updater/index.ts
 create mode 100644 src/main/utils/constants.ts
 create mode 100644 src/main/utils/crxToZip.ts
 create mode 100644 src/main/utils/extensions.ts
 create mode 100644 src/main/utils/simpleGet.ts
 delete mode 100644 src/patchWin32Updater.ts
 delete mode 100644 src/patcher.ts
 create mode 100644 src/utils/native.ts

(limited to 'src')

diff --git a/src/Vencord.ts b/src/Vencord.ts
index 00f8a58..73b53e8 100644
--- a/src/Vencord.ts
+++ b/src/Vencord.ts
@@ -30,7 +30,7 @@ import "./webpack/patchWebpack";
 import { showNotification } from "./api/Notifications";
 import { PlainSettings, Settings } from "./api/settings";
 import { patches, PMLogger, startAllPlugins } from "./plugins";
-import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
+import { checkForUpdates, rebuild, update,UpdateLogger } from "./utils/updater";
 import { onceReady } from "./webpack";
 import { SettingsRouter } from "./webpack/common";
 
@@ -56,8 +56,12 @@ async function init() {
                         permanent: true,
                         noPersist: true,
                         onClick() {
-                            if (needsFullRestart)
-                                window.DiscordNative.app.relaunch();
+                            if (needsFullRestart) {
+                                if (IS_DISCORD_DESKTOP)
+                                    window.DiscordNative.app.relaunch();
+                                else
+                                    window.VencordDesktop.app.relaunch();
+                            }
                             else
                                 location.reload();
                         }
@@ -96,7 +100,7 @@ async function init() {
 
 init();
 
-if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
+if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
     document.addEventListener("DOMContentLoaded", () => {
         document.head.append(Object.assign(document.createElement("style"), {
             id: "vencord-native-titlebar-style",
diff --git a/src/components/VencordSettings/Updater.tsx b/src/components/VencordSettings/Updater.tsx
index 3c3eb91..15a8c87 100644
--- a/src/components/VencordSettings/Updater.tsx
+++ b/src/components/VencordSettings/Updater.tsx
@@ -24,6 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
 import { Link } from "@components/Link";
 import { Margins } from "@utils/margins";
 import { classes, useAwaiter } from "@utils/misc";
+import { relaunch } from "@utils/native";
 import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
 import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
 
@@ -133,7 +134,7 @@ function Updatable(props: CommonProps) {
                                     cancelText: "Not now!",
                                     onConfirm() {
                                         if (needFullRestart)
-                                            window.DiscordNative.app.relaunch();
+                                            relaunch();
                                         else
                                             location.reload();
                                         r();
diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx
index 7113421..8b86968 100644
--- a/src/components/VencordSettings/VencordTab.tsx
+++ b/src/components/VencordSettings/VencordTab.tsx
@@ -26,6 +26,7 @@ import { ErrorCard } from "@components/ErrorCard";
 import IpcEvents from "@utils/IpcEvents";
 import { Margins } from "@utils/margins";
 import { identity, useAwaiter } from "@utils/misc";
+import { relaunch } from "@utils/native";
 import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
 
 const cl = classNameFactory("vc-settings-");
@@ -100,7 +101,7 @@ function VencordSettings() {
                     ) : (
                         <React.Fragment>
                             <Button
-                                onClick={() => window.DiscordNative.app.relaunch()}
+                                onClick={relaunch}
                                 size={Button.Sizes.SMALL}>
                                 Restart Client
                             </Button>
@@ -111,6 +112,7 @@ function VencordSettings() {
                                 Open QuickCSS File
                             </Button>
                             <Button
+                                // FIXME: Vencord Desktop support
                                 onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
                                 size={Button.Sizes.SMALL}
                                 disabled={settingsDirPending}>
diff --git a/src/globals.d.ts b/src/globals.d.ts
index 7c494e2..98eccde 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -35,6 +35,8 @@ declare global {
     export var IS_WEB: boolean;
     export var IS_DEV: boolean;
     export var IS_STANDALONE: boolean;
+    export var IS_DISCORD_DESKTOP: boolean;
+    export var IS_VENCORD_DESKTOP: boolean;
 
     export var VencordNative: typeof import("./VencordNative").default;
     export var Vencord: typeof import("./Vencord");
@@ -54,6 +56,7 @@ declare global {
      * If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
      */
     export var DiscordNative: any;
+    export var VencordDesktop: any;
 
     interface Window {
         webpackChunkdiscord_app: {
diff --git a/src/ipcMain/constants.ts b/src/ipcMain/constants.ts
deleted file mode 100644
index 7133757..0000000
--- a/src/ipcMain/constants.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { app } from "electron";
-import { join } from "path";
-
-export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? (
-    process.env.DISCORD_USER_DATA_DIR
-        ? join(process.env.DISCORD_USER_DATA_DIR, "..", "VencordData")
-        : join(app.getPath("userData"), "..", "Vencord")
-);
-export const SETTINGS_DIR = join(DATA_DIR, "settings");
-export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
-export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
-export const ALLOWED_PROTOCOLS = [
-    "https:",
-    "http:",
-    "steam:",
-    "spotify:"
-];
diff --git a/src/ipcMain/crxToZip.ts b/src/ipcMain/crxToZip.ts
deleted file mode 100644
index ca43890..0000000
--- a/src/ipcMain/crxToZip.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/* eslint-disable header/header */
-
-/*!
- * crxToZip
- * Copyright (c) 2013 Rob Wu <rob@robwu.nl>
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-export function crxToZip(buf: Buffer) {
-    function calcLength(a: number, b: number, c: number, d: number) {
-        let length = 0;
-
-        length += a << 0;
-        length += b << 8;
-        length += c << 16;
-        length += d << 24 >>> 0;
-        return length;
-    }
-
-    // 50 4b 03 04
-    // This is actually a zip file
-    if (buf[0] === 80 && buf[1] === 75 && buf[2] === 3 && buf[3] === 4) {
-        return buf;
-    }
-
-    // 43 72 32 34 (Cr24)
-    if (buf[0] !== 67 || buf[1] !== 114 || buf[2] !== 50 || buf[3] !== 52) {
-        throw new Error("Invalid header: Does not start with Cr24");
-    }
-
-    // 02 00 00 00
-    // or
-    // 03 00 00 00
-    const isV3 = buf[4] === 3;
-    const isV2 = buf[4] === 2;
-
-    if ((!isV2 && !isV3) || buf[5] || buf[6] || buf[7]) {
-        throw new Error("Unexpected crx format version number.");
-    }
-
-    if (isV2) {
-        const publicKeyLength = calcLength(buf[8], buf[9], buf[10], buf[11]);
-        const signatureLength = calcLength(buf[12], buf[13], buf[14], buf[15]);
-
-        // 16 = Magic number (4), CRX format version (4), lengths (2x4)
-        const zipStartOffset = 16 + publicKeyLength + signatureLength;
-
-        return buf.subarray(zipStartOffset, buf.length);
-    }
-    // v3 format has header size and then header
-    const headerSize = calcLength(buf[8], buf[9], buf[10], buf[11]);
-    const zipStartOffset = 12 + headerSize;
-
-    return buf.subarray(zipStartOffset, buf.length);
-}
diff --git a/src/ipcMain/extensions.ts b/src/ipcMain/extensions.ts
deleted file mode 100644
index d8f8437..0000000
--- a/src/ipcMain/extensions.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { session } from "electron";
-import { unzip } from "fflate";
-import { constants as fsConstants } from "fs";
-import { access, mkdir, rm, writeFile } from "fs/promises";
-import { join } from "path";
-
-import { DATA_DIR } from "./constants";
-import { crxToZip } from "./crxToZip";
-import { get } from "./simpleGet";
-
-const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
-
-async function extract(data: Buffer, outDir: string) {
-    await mkdir(outDir, { recursive: true });
-    return new Promise<void>((resolve, reject) => {
-        unzip(data, (err, files) => {
-            if (err) return void reject(err);
-            Promise.all(Object.keys(files).map(async f => {
-                // Signature stuff
-                // 'Cannot load extension with file or directory name
-                // _metadata. Filenames starting with "_" are reserved for use by the system.';
-                if (f.startsWith("_metadata/")) return;
-
-                if (f.endsWith("/")) return void mkdir(join(outDir, f), { recursive: true });
-
-                const pathElements = f.split("/");
-                const name = pathElements.pop()!;
-                const directories = pathElements.join("/");
-                const dir = join(outDir, directories);
-
-                if (directories) {
-                    await mkdir(dir, { recursive: true });
-                }
-
-                await writeFile(join(dir, name), files[f]);
-            }))
-                .then(() => resolve())
-                .catch(err => {
-                    rm(outDir, { recursive: true, force: true });
-                    reject(err);
-                });
-        });
-    });
-}
-
-export async function installExt(id: string) {
-    const extDir = join(extensionCacheDir, `${id}`);
-
-    try {
-        await access(extDir, fsConstants.F_OK);
-    } catch (err) {
-        const url = id === "fmkadmapgofadopljbjfkapdkoienihi"
-            // React Devtools v4.25
-            // v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843
-            // Unfortunately, Google does not serve old versions, so this is the only way
-            ? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip"
-            : `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
-        const buf = await get(url, {
-            headers: {
-                "User-Agent": "Vencord (https://github.com/Vendicated/Vencord)"
-            }
-        });
-        await extract(crxToZip(buf), extDir).catch(console.error);
-    }
-
-    session.defaultSession.loadExtension(extDir);
-}
diff --git a/src/ipcMain/index.ts b/src/ipcMain/index.ts
deleted file mode 100644
index 6fb31d1..0000000
--- a/src/ipcMain/index.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import "./updater";
-
-import { debounce } from "@utils/debounce";
-import IpcEvents from "@utils/IpcEvents";
-import { Queue } from "@utils/Queue";
-import { BrowserWindow, ipcMain, shell } from "electron";
-import { mkdirSync, readFileSync, watch } from "fs";
-import { open, readFile, writeFile } from "fs/promises";
-import { join } from "path";
-
-import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
-
-import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
-
-mkdirSync(SETTINGS_DIR, { recursive: true });
-
-function readCss() {
-    return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
-}
-
-export function readSettings() {
-    try {
-        return readFileSync(SETTINGS_FILE, "utf-8");
-    } catch {
-        return "{}";
-    }
-}
-
-ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
-
-ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
-    try {
-        var { protocol } = new URL(url);
-    } catch {
-        throw "Malformed URL";
-    }
-    if (!ALLOWED_PROTOCOLS.includes(protocol))
-        throw "Disallowed protocol.";
-
-    shell.openExternal(url);
-});
-
-const cssWriteQueue = new Queue();
-const settingsWriteQueue = new Queue();
-
-ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
-ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
-    cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
-);
-
-ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
-ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
-
-ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
-    settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
-});
-
-
-export function initIpc(mainWindow: BrowserWindow) {
-    open(QUICKCSS_PATH, "a+").then(fd => {
-        fd.close();
-        watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
-            mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
-        }, 50));
-    });
-}
-
-ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
-    const win = new BrowserWindow({
-        title: "QuickCss Editor",
-        autoHideMenuBar: true,
-        darkTheme: true,
-        webPreferences: {
-            preload: join(__dirname, "preload.js"),
-            contextIsolation: true,
-            nodeIntegration: false,
-            sandbox: false
-        }
-    });
-    await win.loadURL(`data:text/html;base64,${monacoHtml}`);
-});
diff --git a/src/ipcMain/simpleGet.ts b/src/ipcMain/simpleGet.ts
deleted file mode 100644
index 1a8302c..0000000
--- a/src/ipcMain/simpleGet.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import https from "https";
-
-export function get(url: string, options: https.RequestOptions = {}) {
-    return new Promise<Buffer>((resolve, reject) => {
-        https.get(url, options, res => {
-            const { statusCode, statusMessage, headers } = res;
-            if (statusCode! >= 400)
-                return void reject(`${statusCode}: ${statusMessage} - ${url}`);
-            if (statusCode! >= 300)
-                return void resolve(get(headers.location!, options));
-
-            const chunks = [] as Buffer[];
-            res.on("error", reject);
-
-            res.on("data", chunk => chunks.push(chunk));
-            res.once("end", () => resolve(Buffer.concat(chunks)));
-        });
-    });
-}
diff --git a/src/ipcMain/updater/common.ts b/src/ipcMain/updater/common.ts
deleted file mode 100644
index 3729c6d..0000000
--- a/src/ipcMain/updater/common.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { createHash } from "crypto";
-import { createReadStream } from "fs";
-import { join } from "path";
-
-export async function calculateHashes() {
-    const hashes = {} as Record<string, string>;
-
-    await Promise.all(
-        ["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => {
-            const fis = createReadStream(join(__dirname, file));
-            const hash = createHash("sha1", { encoding: "hex" });
-            fis.once("end", () => {
-                hash.end();
-                hashes[file] = hash.read();
-                r();
-            });
-            fis.pipe(hash);
-        }))
-    );
-
-    return hashes;
-}
-
-export function serializeErrors(func: (...args: any[]) => any) {
-    return async function () {
-        try {
-            return {
-                ok: true,
-                value: await func(...arguments)
-            };
-        } catch (e: any) {
-            return {
-                ok: false,
-                error: e instanceof Error ? {
-                    // prototypes get lost, so turn error into plain object
-                    ...e
-                } : e
-            };
-        }
-    };
-}
diff --git a/src/ipcMain/updater/git.ts b/src/ipcMain/updater/git.ts
deleted file mode 100644
index 89c2d3c..0000000
--- a/src/ipcMain/updater/git.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import IpcEvents from "@utils/IpcEvents";
-import { execFile as cpExecFile } from "child_process";
-import { ipcMain } from "electron";
-import { join } from "path";
-import { promisify } from "util";
-
-import { calculateHashes, serializeErrors } from "./common";
-
-const VENCORD_SRC_DIR = join(__dirname, "..");
-
-const execFile = promisify(cpExecFile);
-
-const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
-
-if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
-
-function git(...args: string[]) {
-    const opts = { cwd: VENCORD_SRC_DIR };
-
-    if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts);
-    else return execFile("git", args, opts);
-}
-
-async function getRepo() {
-    const res = await git("remote", "get-url", "origin");
-    return res.stdout.trim()
-        .replace(/git@(.+):/, "https://$1/")
-        .replace(/\.git$/, "");
-}
-
-async function calculateGitChanges() {
-    await git("fetch");
-
-    const res = await git("log", "HEAD...origin/main", "--pretty=format:%an/%h/%s");
-
-    const commits = res.stdout.trim();
-    return commits ? commits.split("\n").map(line => {
-        const [author, hash, ...rest] = line.split("/");
-        return {
-            hash, author, message: rest.join("/")
-        };
-    }) : [];
-}
-
-async function pull() {
-    const res = await git("pull");
-    return res.stdout.includes("Fast-forward");
-}
-
-async function build() {
-    const opts = { cwd: VENCORD_SRC_DIR };
-
-    const command = isFlatpak ? "flatpak-spawn" : "node";
-    const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"];
-
-    const res = await execFile(command, args, opts);
-
-    return !res.stderr.includes("Build failed");
-}
-
-ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
-ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
-ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
-ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
-ipcMain.handle(IpcEvents.BUILD, serializeErrors(build));
diff --git a/src/ipcMain/updater/http.ts b/src/ipcMain/updater/http.ts
deleted file mode 100644
index cc10631..0000000
--- a/src/ipcMain/updater/http.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { VENCORD_USER_AGENT } from "@utils/constants";
-import IpcEvents from "@utils/IpcEvents";
-import { ipcMain } from "electron";
-import { writeFile } from "fs/promises";
-import { join } from "path";
-
-import gitHash from "~git-hash";
-import gitRemote from "~git-remote";
-
-import { get } from "../simpleGet";
-import { calculateHashes, serializeErrors } from "./common";
-
-const API_BASE = `https://api.github.com/repos/${gitRemote}`;
-let PendingUpdates = [] as [string, string][];
-
-async function githubGet(endpoint: string) {
-    return get(API_BASE + endpoint, {
-        headers: {
-            Accept: "application/vnd.github+json",
-            // "All API requests MUST include a valid User-Agent header.
-            // Requests with no User-Agent header will be rejected."
-            "User-Agent": VENCORD_USER_AGENT
-        }
-    });
-}
-
-async function calculateGitChanges() {
-    const isOutdated = await fetchUpdates();
-    if (!isOutdated) return [];
-
-    const res = await githubGet(`/compare/${gitHash}...HEAD`);
-
-    const data = JSON.parse(res.toString("utf-8"));
-    return data.commits.map((c: any) => ({
-        // github api only sends the long sha
-        hash: c.sha.slice(0, 7),
-        author: c.author.login,
-        message: c.commit.message
-    }));
-}
-
-async function fetchUpdates() {
-    const release = await githubGet("/releases/latest");
-
-    const data = JSON.parse(release.toString());
-    const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
-    if (hash === gitHash)
-        return false;
-
-    data.assets.forEach(({ name, browser_download_url }) => {
-        if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) {
-            PendingUpdates.push([name, browser_download_url]);
-        }
-    });
-    return true;
-}
-
-async function applyUpdates() {
-    await Promise.all(PendingUpdates.map(
-        async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
-    );
-    PendingUpdates = [];
-    return true;
-}
-
-ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
-ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`));
-ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
-ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));
-ipcMain.handle(IpcEvents.BUILD, serializeErrors(applyUpdates));
diff --git a/src/ipcMain/updater/index.ts b/src/ipcMain/updater/index.ts
deleted file mode 100644
index 7036112..0000000
--- a/src/ipcMain/updater/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import(IS_STANDALONE ? "./http" : "./git");
diff --git a/src/main/index.ts b/src/main/index.ts
new file mode 100644
index 0000000..c35635e
--- /dev/null
+++ b/src/main/index.ts
@@ -0,0 +1,108 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { app, protocol, session } from "electron";
+import { join } from "path";
+
+import { getSettings } from "./ipcMain";
+import { IS_VANILLA } from "./utils/constants";
+import { installExt } from "./utils/extensions";
+
+if (IS_VENCORD_DESKTOP || !IS_VANILLA) {
+    app.whenReady().then(() => {
+        // Source Maps! Maybe there's a better way but since the renderer is executed
+        // from a string I don't think any other form of sourcemaps would work
+        protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
+            let url = unsafeUrl.slice("vencord://".length);
+            if (url.endsWith("/")) url = url.slice(0, -1);
+            switch (url) {
+                case "renderer.js.map":
+                case "preload.js.map":
+                case "patcher.js.map": // doubt
+                    cb(join(__dirname, url));
+                    break;
+                default:
+                    cb({ statusCode: 403 });
+            }
+        });
+
+        try {
+            if (getSettings().enableReactDevtools)
+                installExt("fmkadmapgofadopljbjfkapdkoienihi")
+                    .then(() => console.info("[Vencord] Installed React Developer Tools"))
+                    .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
+        } catch { }
+
+
+        // Remove CSP
+        type PolicyResult = Record<string, string[]>;
+
+        const parsePolicy = (policy: string): PolicyResult => {
+            const result: PolicyResult = {};
+            policy.split(";").forEach(directive => {
+                const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
+                if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
+                    result[directiveKey] = directiveValue;
+                }
+            });
+            return result;
+        };
+        const stringifyPolicy = (policy: PolicyResult): string =>
+            Object.entries(policy)
+                .filter(([, values]) => values?.length)
+                .map(directive => directive.flat().join(" "))
+                .join("; ");
+
+        function patchCsp(headers: Record<string, string[]>, header: string) {
+            if (header in headers) {
+                const csp = parsePolicy(headers[header][0]);
+
+                for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
+                    csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
+                }
+                // TODO: Restrict this to only imported packages with fixed version.
+                // Perhaps auto generate with esbuild
+                csp["script-src"] ??= [];
+                csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
+                headers[header] = [stringifyPolicy(csp)];
+            }
+        }
+
+        session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
+            if (responseHeaders) {
+                if (resourceType === "mainFrame")
+                    patchCsp(responseHeaders, "content-security-policy");
+
+                // Fix hosts that don't properly set the css content type, such as
+                // raw.githubusercontent.com
+                if (resourceType === "stylesheet")
+                    responseHeaders["content-type"] = ["text/css"];
+            }
+            cb({ cancel: false, responseHeaders });
+        });
+
+        // assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
+        // For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
+        // impossible to load css from github raw despite our fix above
+        session.defaultSession.webRequest.onHeadersReceived = () => { };
+    });
+}
+
+if (IS_DISCORD_DESKTOP) {
+    require("./patcher");
+}
diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts
new file mode 100644
index 0000000..b0f8fc3
--- /dev/null
+++ b/src/main/ipcMain.ts
@@ -0,0 +1,107 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import "./updater";
+
+import { debounce } from "@utils/debounce";
+import IpcEvents from "@utils/IpcEvents";
+import { Queue } from "@utils/Queue";
+import { BrowserWindow, ipcMain, shell } from "electron";
+import { mkdirSync, readFileSync, watch } from "fs";
+import { open, readFile, writeFile } from "fs/promises";
+import { join } from "path";
+
+import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
+
+import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
+
+mkdirSync(SETTINGS_DIR, { recursive: true });
+
+function readCss() {
+    return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
+}
+
+export function readSettings() {
+    try {
+        return readFileSync(SETTINGS_FILE, "utf-8");
+    } catch {
+        return "{}";
+    }
+}
+
+export function getSettings(): typeof import("@api/settings").Settings {
+    try {
+        return JSON.parse(readSettings());
+    } catch {
+        return {} as any;
+    }
+}
+
+ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
+
+ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
+    try {
+        var { protocol } = new URL(url);
+    } catch {
+        throw "Malformed URL";
+    }
+    if (!ALLOWED_PROTOCOLS.includes(protocol))
+        throw "Disallowed protocol.";
+
+    shell.openExternal(url);
+});
+
+const cssWriteQueue = new Queue();
+const settingsWriteQueue = new Queue();
+
+ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
+ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
+    cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
+);
+
+ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
+ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
+
+ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
+    settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
+});
+
+
+export function initIpc(mainWindow: BrowserWindow) {
+    open(QUICKCSS_PATH, "a+").then(fd => {
+        fd.close();
+        watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
+            mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
+        }, 50));
+    });
+}
+
+ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
+    const win = new BrowserWindow({
+        title: "QuickCss Editor",
+        autoHideMenuBar: true,
+        darkTheme: true,
+        webPreferences: {
+            preload: join(__dirname, "preload.js"),
+            contextIsolation: true,
+            nodeIntegration: false,
+            sandbox: false
+        }
+    });
+    await win.loadURL(`data:text/html;base64,${monacoHtml}`);
+});
diff --git a/src/main/patchWin32Updater.ts b/src/main/patchWin32Updater.ts
new file mode 100644
index 0000000..e08e37d
--- /dev/null
+++ b/src/main/patchWin32Updater.ts
@@ -0,0 +1,99 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { app, autoUpdater } from "electron";
+import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
+import { basename, dirname, join } from "path";
+
+const { setAppUserModelId } = app;
+
+// Apparently requiring Discords updater too early leads into issues,
+// copied this workaround from powerCord
+app.setAppUserModelId = function (id: string) {
+    app.setAppUserModelId = setAppUserModelId;
+
+    setAppUserModelId.call(this, id);
+
+    patchUpdater();
+};
+
+function isNewer($new: string, old: string) {
+    const newParts = $new.slice(4).split(".").map(Number);
+    const oldParts = old.slice(4).split(".").map(Number);
+
+    for (let i = 0; i < oldParts.length; i++) {
+        if (newParts[i] > oldParts[i]) return true;
+        if (newParts[i] < oldParts[i]) return false;
+    }
+    return false;
+}
+
+function patchLatest() {
+    try {
+        const currentAppPath = dirname(process.execPath);
+        const currentVersion = basename(currentAppPath);
+        const discordPath = join(currentAppPath, "..");
+
+        const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
+            return (curr.startsWith("app-") && isNewer(curr, prev))
+                ? curr
+                : prev;
+        }, currentVersion as string);
+
+        if (latestVersion === currentVersion) return;
+
+        const resources = join(discordPath, latestVersion, "resources");
+        const app = join(resources, "app.asar");
+        const _app = join(resources, "_app.asar");
+
+        if (!existsSync(app) || statSync(app).isDirectory()) return;
+
+        console.info("[Vencord] Detected Host Update. Repatching...");
+
+        renameSync(app, _app);
+        mkdirSync(app);
+        writeFileSync(join(app, "package.json"), JSON.stringify({
+            name: "discord",
+            main: "index.js"
+        }));
+        writeFileSync(join(app, "index.js"), `require(${JSON.stringify(join(__dirname, "patcher.js"))});`);
+    } catch (err) {
+        console.error("[Vencord] Failed to repatch latest host update", err);
+    }
+}
+
+// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
+// need to reinject
+function patchUpdater() {
+    try {
+        const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js");
+        const { update } = require(autoStartScript);
+
+        require.cache[autoStartScript]!.exports.update = function () {
+            update.apply(this, arguments);
+            patchLatest();
+        };
+    } catch {
+        // OpenAsar uses electrons autoUpdater on Windows
+        const { quitAndInstall } = autoUpdater;
+        autoUpdater.quitAndInstall = function () {
+            patchLatest();
+            quitAndInstall.call(this);
+        };
+    }
+}
diff --git a/src/main/patcher.ts b/src/main/patcher.ts
new file mode 100644
index 0000000..c45a299
--- /dev/null
+++ b/src/main/patcher.ts
@@ -0,0 +1,120 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { onceDefined } from "@utils/onceDefined";
+import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
+import { dirname, join } from "path";
+
+import { getSettings, initIpc } from "./ipcMain";
+import { IS_VANILLA } from "./utils/constants";
+
+console.log("[Vencord] Starting up...");
+
+// Our injector file at app/index.js
+const injectorPath = require.main!.filename;
+
+// special discord_arch_electron injection method
+const asarName = require.main!.path.endsWith("app.asar") ? "_app.asar" : "app.asar";
+
+// The original app.asar
+const asarPath = join(dirname(injectorPath), "..", asarName);
+
+const discordPkg = require(join(asarPath, "package.json"));
+require.main!.filename = join(asarPath, discordPkg.main);
+
+// @ts-ignore Untyped method? Dies from cringe
+app.setAppPath(asarPath);
+
+if (!IS_VANILLA) {
+    const settings = getSettings();
+
+    // Repatch after host updates on Windows
+    if (process.platform === "win32") {
+        require("./patchWin32Updater");
+
+        if (settings.winCtrlQ) {
+            const originalBuild = Menu.buildFromTemplate;
+            Menu.buildFromTemplate = function (template) {
+                if (template[0]?.label === "&File") {
+                    const { submenu } = template[0];
+                    if (Array.isArray(submenu)) {
+                        submenu.push({
+                            label: "Quit (Hidden)",
+                            visible: false,
+                            acceleratorWorksWhenHidden: true,
+                            accelerator: "Control+Q",
+                            click: () => app.quit()
+                        });
+                    }
+                }
+                return originalBuild.call(this, template);
+            };
+        }
+    }
+
+    class BrowserWindow extends electron.BrowserWindow {
+        constructor(options: BrowserWindowConstructorOptions) {
+            if (options?.webPreferences?.preload && options.title) {
+                const original = options.webPreferences.preload;
+                options.webPreferences.preload = join(__dirname, "preload.js");
+                options.webPreferences.sandbox = false;
+                if (settings.frameless) {
+                    options.frame = false;
+                } else if (process.platform === "win32" && settings.winNativeTitleBar) {
+                    delete options.frame;
+                }
+
+                // This causes electron to freeze / white screen for some people
+                if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
+                    options.transparent = true;
+                    options.backgroundColor = "#00000000";
+                }
+
+                process.env.DISCORD_PRELOAD = original;
+
+                super(options);
+                initIpc(this);
+            } else super(options);
+        }
+    }
+    Object.assign(BrowserWindow, electron.BrowserWindow);
+    // esbuild may rename our BrowserWindow, which leads to it being excluded
+    // from getFocusedWindow(), so this is necessary
+    // https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
+    Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
+
+    // Replace electrons exports with our custom BrowserWindow
+    const electronPath = require.resolve("electron");
+    delete require.cache[electronPath]!.exports;
+    require.cache[electronPath]!.exports = {
+        ...electron,
+        BrowserWindow
+    };
+
+    // Patch appSettings to force enable devtools
+    onceDefined(global, "appSettings", s =>
+        s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
+    );
+
+    process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
+} else {
+    console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
+}
+
+console.log("[Vencord] Loading original Discord app.asar");
+require(require.main!.filename);
diff --git a/src/main/updater/common.ts b/src/main/updater/common.ts
new file mode 100644
index 0000000..3729c6d
--- /dev/null
+++ b/src/main/updater/common.ts
@@ -0,0 +1,59 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { createHash } from "crypto";
+import { createReadStream } from "fs";
+import { join } from "path";
+
+export async function calculateHashes() {
+    const hashes = {} as Record<string, string>;
+
+    await Promise.all(
+        ["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => {
+            const fis = createReadStream(join(__dirname, file));
+            const hash = createHash("sha1", { encoding: "hex" });
+            fis.once("end", () => {
+                hash.end();
+                hashes[file] = hash.read();
+                r();
+            });
+            fis.pipe(hash);
+        }))
+    );
+
+    return hashes;
+}
+
+export function serializeErrors(func: (...args: any[]) => any) {
+    return async function () {
+        try {
+            return {
+                ok: true,
+                value: await func(...arguments)
+            };
+        } catch (e: any) {
+            return {
+                ok: false,
+                error: e instanceof Error ? {
+                    // prototypes get lost, so turn error into plain object
+                    ...e
+                } : e
+            };
+        }
+    };
+}
diff --git a/src/main/updater/git.ts b/src/main/updater/git.ts
new file mode 100644
index 0000000..89c2d3c
--- /dev/null
+++ b/src/main/updater/git.ts
@@ -0,0 +1,83 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import IpcEvents from "@utils/IpcEvents";
+import { execFile as cpExecFile } from "child_process";
+import { ipcMain } from "electron";
+import { join } from "path";
+import { promisify } from "util";
+
+import { calculateHashes, serializeErrors } from "./common";
+
+const VENCORD_SRC_DIR = join(__dirname, "..");
+
+const execFile = promisify(cpExecFile);
+
+const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
+
+if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
+
+function git(...args: string[]) {
+    const opts = { cwd: VENCORD_SRC_DIR };
+
+    if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts);
+    else return execFile("git", args, opts);
+}
+
+async function getRepo() {
+    const res = await git("remote", "get-url", "origin");
+    return res.stdout.trim()
+        .replace(/git@(.+):/, "https://$1/")
+        .replace(/\.git$/, "");
+}
+
+async function calculateGitChanges() {
+    await git("fetch");
+
+    const res = await git("log", "HEAD...origin/main", "--pretty=format:%an/%h/%s");
+
+    const commits = res.stdout.trim();
+    return commits ? commits.split("\n").map(line => {
+        const [author, hash, ...rest] = line.split("/");
+        return {
+            hash, author, message: rest.join("/")
+        };
+    }) : [];
+}
+
+async function pull() {
+    const res = await git("pull");
+    return res.stdout.includes("Fast-forward");
+}
+
+async function build() {
+    const opts = { cwd: VENCORD_SRC_DIR };
+
+    const command = isFlatpak ? "flatpak-spawn" : "node";
+    const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"];
+
+    const res = await execFile(command, args, opts);
+
+    return !res.stderr.includes("Build failed");
+}
+
+ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
+ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
+ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
+ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
+ipcMain.handle(IpcEvents.BUILD, serializeErrors(build));
diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts
new file mode 100644
index 0000000..a2e2aad
--- /dev/null
+++ b/src/main/updater/http.ts
@@ -0,0 +1,104 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { VENCORD_USER_AGENT } from "@utils/constants";
+import IpcEvents from "@utils/IpcEvents";
+import { ipcMain } from "electron";
+import { writeFile } from "fs/promises";
+import { join } from "path";
+
+import gitHash from "~git-hash";
+import gitRemote from "~git-remote";
+
+import { get } from "../utils/simpleGet";
+import { calculateHashes, serializeErrors } from "./common";
+
+const API_BASE = `https://api.github.com/repos/${gitRemote}`;
+let PendingUpdates = [] as [string, string][];
+
+async function githubGet(endpoint: string) {
+    return get(API_BASE + endpoint, {
+        headers: {
+            Accept: "application/vnd.github+json",
+            // "All API requests MUST include a valid User-Agent header.
+            // Requests with no User-Agent header will be rejected."
+            "User-Agent": VENCORD_USER_AGENT
+        }
+    });
+}
+
+async function calculateGitChanges() {
+    const isOutdated = await fetchUpdates();
+    if (!isOutdated) return [];
+
+    const res = await githubGet(`/compare/${gitHash}...HEAD`);
+
+    const data = JSON.parse(res.toString("utf-8"));
+    return data.commits.map((c: any) => ({
+        // github api only sends the long sha
+        hash: c.sha.slice(0, 7),
+        author: c.author.login,
+        message: c.commit.message
+    }));
+}
+
+const FILES_TO_DOWNLOAD = [
+    IS_DISCORD_DESKTOP ? "patcher.js" : "vencordDesktopMain.js",
+    "preload.js",
+    IS_DISCORD_DESKTOP ? "renderer.js" : "vencordDesktopRenderer.js",
+    "renderer.css"
+];
+
+async function fetchUpdates() {
+    const release = await githubGet("/releases/latest");
+
+    const data = JSON.parse(release.toString());
+    const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
+    if (hash === gitHash)
+        return false;
+
+    data.assets.forEach(({ name, browser_download_url }) => {
+        if (FILES_TO_DOWNLOAD.some(s => name.startsWith(s))) {
+            PendingUpdates.push([name, browser_download_url]);
+        }
+    });
+    return true;
+}
+
+async function applyUpdates() {
+    await Promise.all(PendingUpdates.map(
+        async ([name, data]) => writeFile(
+            join(
+                __dirname,
+                IS_VENCORD_DESKTOP
+                    // vencordDesktopRenderer.js -> renderer.js
+                    ? name.replace(/vencordDesktop(\w)/, (_, c) => c.toLowerCase())
+                    : name
+            ),
+            await get(data)
+        )
+    ));
+    PendingUpdates = [];
+    return true;
+}
+
+ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
+ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`));
+ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
+ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));
+ipcMain.handle(IpcEvents.BUILD, serializeErrors(applyUpdates));
diff --git a/src/main/updater/index.ts b/src/main/updater/index.ts
new file mode 100644
index 0000000..7036112
--- /dev/null
+++ b/src/main/updater/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import(IS_STANDALONE ? "./http" : "./git");
diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts
new file mode 100644
index 0000000..cc9f459
--- /dev/null
+++ b/src/main/utils/constants.ts
@@ -0,0 +1,37 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { app } from "electron";
+import { join } from "path";
+
+export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? (
+    process.env.DISCORD_USER_DATA_DIR
+        ? join(process.env.DISCORD_USER_DATA_DIR, "..", "VencordData")
+        : join(app.getPath("userData"), "..", "Vencord")
+);
+export const SETTINGS_DIR = join(DATA_DIR, "settings");
+export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
+export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
+export const ALLOWED_PROTOCOLS = [
+    "https:",
+    "http:",
+    "steam:",
+    "spotify:"
+];
+
+export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
diff --git a/src/main/utils/crxToZip.ts b/src/main/utils/crxToZip.ts
new file mode 100644
index 0000000..ca43890
--- /dev/null
+++ b/src/main/utils/crxToZip.ts
@@ -0,0 +1,57 @@
+/* eslint-disable header/header */
+
+/*!
+ * crxToZip
+ * Copyright (c) 2013 Rob Wu <rob@robwu.nl>
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+export function crxToZip(buf: Buffer) {
+    function calcLength(a: number, b: number, c: number, d: number) {
+        let length = 0;
+
+        length += a << 0;
+        length += b << 8;
+        length += c << 16;
+        length += d << 24 >>> 0;
+        return length;
+    }
+
+    // 50 4b 03 04
+    // This is actually a zip file
+    if (buf[0] === 80 && buf[1] === 75 && buf[2] === 3 && buf[3] === 4) {
+        return buf;
+    }
+
+    // 43 72 32 34 (Cr24)
+    if (buf[0] !== 67 || buf[1] !== 114 || buf[2] !== 50 || buf[3] !== 52) {
+        throw new Error("Invalid header: Does not start with Cr24");
+    }
+
+    // 02 00 00 00
+    // or
+    // 03 00 00 00
+    const isV3 = buf[4] === 3;
+    const isV2 = buf[4] === 2;
+
+    if ((!isV2 && !isV3) || buf[5] || buf[6] || buf[7]) {
+        throw new Error("Unexpected crx format version number.");
+    }
+
+    if (isV2) {
+        const publicKeyLength = calcLength(buf[8], buf[9], buf[10], buf[11]);
+        const signatureLength = calcLength(buf[12], buf[13], buf[14], buf[15]);
+
+        // 16 = Magic number (4), CRX format version (4), lengths (2x4)
+        const zipStartOffset = 16 + publicKeyLength + signatureLength;
+
+        return buf.subarray(zipStartOffset, buf.length);
+    }
+    // v3 format has header size and then header
+    const headerSize = calcLength(buf[8], buf[9], buf[10], buf[11]);
+    const zipStartOffset = 12 + headerSize;
+
+    return buf.subarray(zipStartOffset, buf.length);
+}
diff --git a/src/main/utils/extensions.ts b/src/main/utils/extensions.ts
new file mode 100644
index 0000000..d8f8437
--- /dev/null
+++ b/src/main/utils/extensions.ts
@@ -0,0 +1,85 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { session } from "electron";
+import { unzip } from "fflate";
+import { constants as fsConstants } from "fs";
+import { access, mkdir, rm, writeFile } from "fs/promises";
+import { join } from "path";
+
+import { DATA_DIR } from "./constants";
+import { crxToZip } from "./crxToZip";
+import { get } from "./simpleGet";
+
+const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
+
+async function extract(data: Buffer, outDir: string) {
+    await mkdir(outDir, { recursive: true });
+    return new Promise<void>((resolve, reject) => {
+        unzip(data, (err, files) => {
+            if (err) return void reject(err);
+            Promise.all(Object.keys(files).map(async f => {
+                // Signature stuff
+                // 'Cannot load extension with file or directory name
+                // _metadata. Filenames starting with "_" are reserved for use by the system.';
+                if (f.startsWith("_metadata/")) return;
+
+                if (f.endsWith("/")) return void mkdir(join(outDir, f), { recursive: true });
+
+                const pathElements = f.split("/");
+                const name = pathElements.pop()!;
+                const directories = pathElements.join("/");
+                const dir = join(outDir, directories);
+
+                if (directories) {
+                    await mkdir(dir, { recursive: true });
+                }
+
+                await writeFile(join(dir, name), files[f]);
+            }))
+                .then(() => resolve())
+                .catch(err => {
+                    rm(outDir, { recursive: true, force: true });
+                    reject(err);
+                });
+        });
+    });
+}
+
+export async function installExt(id: string) {
+    const extDir = join(extensionCacheDir, `${id}`);
+
+    try {
+        await access(extDir, fsConstants.F_OK);
+    } catch (err) {
+        const url = id === "fmkadmapgofadopljbjfkapdkoienihi"
+            // React Devtools v4.25
+            // v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843
+            // Unfortunately, Google does not serve old versions, so this is the only way
+            ? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip"
+            : `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
+        const buf = await get(url, {
+            headers: {
+                "User-Agent": "Vencord (https://github.com/Vendicated/Vencord)"
+            }
+        });
+        await extract(crxToZip(buf), extDir).catch(console.error);
+    }
+
+    session.defaultSession.loadExtension(extDir);
+}
diff --git a/src/main/utils/simpleGet.ts b/src/main/utils/simpleGet.ts
new file mode 100644
index 0000000..1a8302c
--- /dev/null
+++ b/src/main/utils/simpleGet.ts
@@ -0,0 +1,37 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import https from "https";
+
+export function get(url: string, options: https.RequestOptions = {}) {
+    return new Promise<Buffer>((resolve, reject) => {
+        https.get(url, options, res => {
+            const { statusCode, statusMessage, headers } = res;
+            if (statusCode! >= 400)
+                return void reject(`${statusCode}: ${statusMessage} - ${url}`);
+            if (statusCode! >= 300)
+                return void resolve(get(headers.location!, options));
+
+            const chunks = [] as Buffer[];
+            res.on("error", reject);
+
+            res.on("data", chunk => chunks.push(chunk));
+            res.once("end", () => resolve(Buffer.concat(chunks)));
+        });
+    });
+}
diff --git a/src/patchWin32Updater.ts b/src/patchWin32Updater.ts
deleted file mode 100644
index e08e37d..0000000
--- a/src/patchWin32Updater.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { app, autoUpdater } from "electron";
-import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
-import { basename, dirname, join } from "path";
-
-const { setAppUserModelId } = app;
-
-// Apparently requiring Discords updater too early leads into issues,
-// copied this workaround from powerCord
-app.setAppUserModelId = function (id: string) {
-    app.setAppUserModelId = setAppUserModelId;
-
-    setAppUserModelId.call(this, id);
-
-    patchUpdater();
-};
-
-function isNewer($new: string, old: string) {
-    const newParts = $new.slice(4).split(".").map(Number);
-    const oldParts = old.slice(4).split(".").map(Number);
-
-    for (let i = 0; i < oldParts.length; i++) {
-        if (newParts[i] > oldParts[i]) return true;
-        if (newParts[i] < oldParts[i]) return false;
-    }
-    return false;
-}
-
-function patchLatest() {
-    try {
-        const currentAppPath = dirname(process.execPath);
-        const currentVersion = basename(currentAppPath);
-        const discordPath = join(currentAppPath, "..");
-
-        const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
-            return (curr.startsWith("app-") && isNewer(curr, prev))
-                ? curr
-                : prev;
-        }, currentVersion as string);
-
-        if (latestVersion === currentVersion) return;
-
-        const resources = join(discordPath, latestVersion, "resources");
-        const app = join(resources, "app.asar");
-        const _app = join(resources, "_app.asar");
-
-        if (!existsSync(app) || statSync(app).isDirectory()) return;
-
-        console.info("[Vencord] Detected Host Update. Repatching...");
-
-        renameSync(app, _app);
-        mkdirSync(app);
-        writeFileSync(join(app, "package.json"), JSON.stringify({
-            name: "discord",
-            main: "index.js"
-        }));
-        writeFileSync(join(app, "index.js"), `require(${JSON.stringify(join(__dirname, "patcher.js"))});`);
-    } catch (err) {
-        console.error("[Vencord] Failed to repatch latest host update", err);
-    }
-}
-
-// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
-// need to reinject
-function patchUpdater() {
-    try {
-        const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js");
-        const { update } = require(autoStartScript);
-
-        require.cache[autoStartScript]!.exports.update = function () {
-            update.apply(this, arguments);
-            patchLatest();
-        };
-    } catch {
-        // OpenAsar uses electrons autoUpdater on Windows
-        const { quitAndInstall } = autoUpdater;
-        autoUpdater.quitAndInstall = function () {
-            patchLatest();
-            quitAndInstall.call(this);
-        };
-    }
-}
diff --git a/src/patcher.ts b/src/patcher.ts
deleted file mode 100644
index b1f47b0..0000000
--- a/src/patcher.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Vencord, a modification for Discord's desktop app
- * Copyright (c) 2022 Vendicated and contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { onceDefined } from "@utils/onceDefined";
-import electron, { app, BrowserWindowConstructorOptions, Menu, protocol, session } from "electron";
-import { dirname, join } from "path";
-
-import { initIpc } from "./ipcMain";
-import { installExt } from "./ipcMain/extensions";
-import { readSettings } from "./ipcMain/index";
-
-console.log("[Vencord] Starting up...");
-
-// Our injector file at app/index.js
-const injectorPath = require.main!.filename;
-
-// special discord_arch_electron injection method
-const asarName = require.main!.path.endsWith("app.asar") ? "_app.asar" : "app.asar";
-
-// The original app.asar
-const asarPath = join(dirname(injectorPath), "..", asarName);
-
-const discordPkg = require(join(asarPath, "package.json"));
-require.main!.filename = join(asarPath, discordPkg.main);
-
-// @ts-ignore Untyped method? Dies from cringe
-app.setAppPath(asarPath);
-
-if (!process.argv.includes("--vanilla")) {
-    let settings: typeof import("@api/settings").Settings = {} as any;
-    try {
-        settings = JSON.parse(readSettings());
-    } catch { }
-
-    // Repatch after host updates on Windows
-    if (process.platform === "win32") {
-        require("./patchWin32Updater");
-
-        if (settings.winCtrlQ) {
-            const originalBuild = Menu.buildFromTemplate;
-            Menu.buildFromTemplate = function (template) {
-                if (template[0]?.label === "&File") {
-                    const { submenu } = template[0];
-                    if (Array.isArray(submenu)) {
-                        submenu.push({
-                            label: "Quit (Hidden)",
-                            visible: false,
-                            acceleratorWorksWhenHidden: true,
-                            accelerator: "Control+Q",
-                            click: () => app.quit()
-                        });
-                    }
-                }
-                return originalBuild.call(this, template);
-            };
-        }
-    }
-
-    class BrowserWindow extends electron.BrowserWindow {
-        constructor(options: BrowserWindowConstructorOptions) {
-            if (options?.webPreferences?.preload && options.title) {
-                const original = options.webPreferences.preload;
-                options.webPreferences.preload = join(__dirname, "preload.js");
-                options.webPreferences.sandbox = false;
-                if (settings.frameless) {
-                    options.frame = false;
-                } else if (process.platform === "win32" && settings.winNativeTitleBar) {
-                    delete options.frame;
-                }
-
-                // This causes electron to freeze / white screen for some people
-                if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
-                    options.transparent = true;
-                    options.backgroundColor = "#00000000";
-                }
-
-                process.env.DISCORD_PRELOAD = original;
-
-                super(options);
-                initIpc(this);
-            } else super(options);
-        }
-    }
-    Object.assign(BrowserWindow, electron.BrowserWindow);
-    // esbuild may rename our BrowserWindow, which leads to it being excluded
-    // from getFocusedWindow(), so this is necessary
-    // https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
-    Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
-
-    // Replace electrons exports with our custom BrowserWindow
-    const electronPath = require.resolve("electron");
-    delete require.cache[electronPath]!.exports;
-    require.cache[electronPath]!.exports = {
-        ...electron,
-        BrowserWindow
-    };
-
-    // Patch appSettings to force enable devtools
-    onceDefined(global, "appSettings", s =>
-        s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
-    );
-
-    process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
-
-    app.whenReady().then(() => {
-        // Source Maps! Maybe there's a better way but since the renderer is executed
-        // from a string I don't think any other form of sourcemaps would work
-        protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
-            let url = unsafeUrl.slice("vencord://".length);
-            if (url.endsWith("/")) url = url.slice(0, -1);
-            switch (url) {
-                case "renderer.js.map":
-                case "preload.js.map":
-                case "patcher.js.map": // doubt
-                    cb(join(__dirname, url));
-                    break;
-                default:
-                    cb({ statusCode: 403 });
-            }
-        });
-
-        try {
-            if (settings?.enableReactDevtools)
-                installExt("fmkadmapgofadopljbjfkapdkoienihi")
-                    .then(() => console.info("[Vencord] Installed React Developer Tools"))
-                    .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
-        } catch { }
-
-
-        // Remove CSP
-        type PolicyResult = Record<string, string[]>;
-
-        const parsePolicy = (policy: string): PolicyResult => {
-            const result: PolicyResult = {};
-            policy.split(";").forEach(directive => {
-                const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
-                if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
-                    result[directiveKey] = directiveValue;
-                }
-            });
-            return result;
-        };
-        const stringifyPolicy = (policy: PolicyResult): string =>
-            Object.entries(policy)
-                .filter(([, values]) => values?.length)
-                .map(directive => directive.flat().join(" "))
-                .join("; ");
-
-        function patchCsp(headers: Record<string, string[]>, header: string) {
-            if (header in headers) {
-                const csp = parsePolicy(headers[header][0]);
-
-                for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
-                    csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
-                }
-                // TODO: Restrict this to only imported packages with fixed version.
-                // Perhaps auto generate with esbuild
-                csp["script-src"] ??= [];
-                csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
-                headers[header] = [stringifyPolicy(csp)];
-            }
-        }
-
-        session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
-            if (responseHeaders) {
-                if (resourceType === "mainFrame")
-                    patchCsp(responseHeaders, "content-security-policy");
-
-                // Fix hosts that don't properly set the css content type, such as
-                // raw.githubusercontent.com
-                if (resourceType === "stylesheet")
-                    responseHeaders["content-type"] = ["text/css"];
-            }
-            cb({ cancel: false, responseHeaders });
-        });
-
-        // assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
-        // For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
-        // impossible to load css from github raw despite our fix above
-        session.defaultSession.webRequest.onHeadersReceived = () => { };
-    });
-} else {
-    console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
-}
-
-console.log("[Vencord] Loading original Discord app.asar");
-require(require.main!.filename);
diff --git a/src/plugins/consoleShortcuts.ts b/src/plugins/consoleShortcuts.ts
index 70a9875..056d246 100644
--- a/src/plugins/consoleShortcuts.ts
+++ b/src/plugins/consoleShortcuts.ts
@@ -17,6 +17,7 @@
 */
 
 import { Devs } from "@utils/constants";
+import { relaunch } from "@utils/native";
 import definePlugin from "@utils/types";
 import * as Webpack from "@webpack";
 import { extract, filters, findAll, search } from "@webpack";
@@ -77,7 +78,7 @@ export default definePlugin({
             Settings: Vencord.Settings,
             Api: Vencord.Api,
             reload: () => location.reload(),
-            restart: IS_WEB ? WEB_ONLY("restart") : window.DiscordNative.app.relaunch
+            restart: IS_WEB ? WEB_ONLY("restart") : relaunch
         };
     },
 
diff --git a/src/plugins/settings.tsx b/src/plugins/settings.tsx
index 67d1f8d..8db0a4e 100644
--- a/src/plugins/settings.tsx
+++ b/src/plugins/settings.tsx
@@ -168,6 +168,7 @@ export default definePlugin({
     get additionalInfo() {
         if (IS_DEV) return " (Dev)";
         if (IS_WEB) return " (Web)";
+        if (IS_VENCORD_DESKTOP) return " (Vencord Desktop)";
         if (IS_STANDALONE) return " (Standalone)";
         return "";
     },
diff --git a/src/preload.ts b/src/preload.ts
index 820b655..b75574a 100644
--- a/src/preload.ts
+++ b/src/preload.ts
@@ -54,7 +54,9 @@ if (location.protocol !== "data:") {
             document.getElementById("vencord-css-core")!.textContent = readFileSync(rendererCss, "utf-8");
         });
     }
-    require(process.env.DISCORD_PRELOAD!);
+
+    if (process.env.DISCORD_PRELOAD)
+        require(process.env.DISCORD_PRELOAD);
 } else {
     // Monaco Popout
     contextBridge.exposeInMainWorld("setCss", debounce(s => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, s)));
diff --git a/src/utils/native.ts b/src/utils/native.ts
new file mode 100644
index 0000000..70e4c0e
--- /dev/null
+++ b/src/utils/native.ts
@@ -0,0 +1,24 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export function relaunch() {
+    if (IS_DISCORD_DESKTOP)
+        window.DiscordNative.app.relaunch();
+    else
+        window.VencordDesktop.app.relaunch();
+}
diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts
index 18e1854..781899f 100644
--- a/src/utils/settingsSync.ts
+++ b/src/utils/settingsSync.ts
@@ -47,7 +47,9 @@ export async function downloadSettingsBackup() {
     const backup = await exportSettings();
     const data = new TextEncoder().encode(backup);
 
-    if (IS_WEB) {
+    if (IS_DISCORD_DESKTOP) {
+        DiscordNative.fileManager.saveWithDialog(data, filename);
+    } else {
         const file = new File([data], filename, { type: "application/json" });
         const a = document.createElement("a");
         a.href = URL.createObjectURL(file);
@@ -59,8 +61,6 @@ export async function downloadSettingsBackup() {
             URL.revokeObjectURL(a.href);
             document.body.removeChild(a);
         });
-    } else {
-        DiscordNative.fileManager.saveWithDialog(data, filename);
     }
 }
 
@@ -77,7 +77,24 @@ const toastFailure = (err: any) => Toasts.show({
 });
 
 export async function uploadSettingsBackup(showToast = true): Promise<void> {
-    if (IS_WEB) {
+    if (IS_DISCORD_DESKTOP) {
+        const [file] = await DiscordNative.fileManager.openFiles({
+            filters: [
+                { name: "Vencord Settings Backup", extensions: ["json"] },
+                { name: "all", extensions: ["*"] }
+            ]
+        });
+
+        if (file) {
+            try {
+                await importSettings(new TextDecoder().decode(file.data));
+                if (showToast) toastSuccess();
+            } catch (err) {
+                new Logger("SettingsSync").error(err);
+                if (showToast) toastFailure(err);
+            }
+        }
+    } else {
         const input = document.createElement("input");
         input.type = "file";
         input.style.display = "none";
@@ -102,22 +119,5 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> {
         document.body.appendChild(input);
         input.click();
         setImmediate(() => document.body.removeChild(input));
-    } else {
-        const [file] = await DiscordNative.fileManager.openFiles({
-            filters: [
-                { name: "Vencord Settings Backup", extensions: ["json"] },
-                { name: "all", extensions: ["*"] }
-            ]
-        });
-
-        if (file) {
-            try {
-                await importSettings(new TextDecoder().decode(file.data));
-                if (showToast) toastSuccess();
-            } catch (err) {
-                new Logger("SettingsSync").error(err);
-                if (showToast) toastFailure(err);
-            }
-        }
     }
 }
diff --git a/src/utils/updater.ts b/src/utils/updater.ts
index e13f5cf..d0b1fdc 100644
--- a/src/utils/updater.ts
+++ b/src/utils/updater.ts
@@ -20,6 +20,7 @@ import gitHash from "~git-hash";
 
 import IpcEvents from "./IpcEvents";
 import Logger from "./Logger";
+import { relaunch } from "./native";
 import { IpcRes } from "./types";
 
 export const UpdateLogger = /* #__PURE__*/ new Logger("Updater", "white");
@@ -90,8 +91,10 @@ export async function maybePromptToUpdate(confirmMessage: string, checkForDev =
             if (wantsUpdate) {
                 await update();
                 const needFullRestart = await rebuild();
-                if (needFullRestart) DiscordNative.app.relaunch();
-                else location.reload();
+                if (needFullRestart)
+                    relaunch();
+                else
+                    location.reload();
             }
         }
     } catch (err) {
diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts
index 0d95587..a37fe6d 100644
--- a/src/webpack/webpack.ts
+++ b/src/webpack/webpack.ts
@@ -67,7 +67,7 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
     instance.pop();
 }
 
-if (IS_DEV && !IS_WEB) {
+if (IS_DEV && IS_DISCORD_DESKTOP) {
     var devToolsOpen = false;
     // At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed
     setTimeout(() => {
-- 
cgit