commit 6abe230c1e01a03ae8c3fa27709fc22e4b2e28c9 Author: caca Date: Fri Dec 20 18:20:32 2024 -0800 a diff --git a/meta/dev.vencord.Vesktop.metainfo.xml b/meta/dev.vencord.Vesktop.metainfo.xml new file mode 100644 index 0000000..853d520 --- /dev/null +++ b/meta/dev.vencord.Vesktop.metainfo.xml @@ -0,0 +1,239 @@ + + + + dev.vencord.Vesktop + Vesktop + Snappier Discord app with Vencord + Vencord Contributors + dev.vencord.Vesktop.desktop + CC0-1.0 + GPL-3.0 + Vencord + +

Vesktop is a cross platform desktop app aiming to give you a snappier Discord experience with Vencord pre-installed.

+

Vesktop comes bundled with Venmic, a purpose-built library to provide functioning audio screenshare.

+
+ + + Vencord settings page and about window open + https://vencord.dev/assets/screenshots/vesktop-1-appstream.png + + + A dialog showing screenshare options + https://vencord.dev/assets/screenshots/vesktop-2-appstream.png + + + A screenshot of a Discord server + https://vencord.dev/assets/screenshots/vesktop-3-appstream.png + + + + + https://github.com/Vencord/Vesktop/releases/tag/v1.5.3 + +

Features

+
    +
  • added arm64 Windows support
  • +
  • windows & macOS builds are now universal
  • +
  • added option to configure spellcheck languages
  • +
  • will auto-update from now on
  • +
  • updated electron to 31 & Chromium to 126
  • +
  • macOS: Added customized dmg background by @khcrysalis
  • +
  • Windows Portable: store settings in portable folder by @MrGarlic1
  • +
  • linux audioshare: added granular selection, more options, better ui by @Curve
  • +
  • changed default screen-sharing quality to 720p 30 FPS by @Tiagoquix
  • +
+

Fixes

+
    +
  • macOS: Added workaround for making things in draggable area clickable by @HAHALOSAH
  • +
  • fixed Screenshare UI for non-linux systems by @PolisanTheEasyNick
  • +
  • fixed opening on screen that was disconnected by @MrGarlic1
  • +
  • mac: hide native window controls with custom titlebar enabled by @MrGarlic1
  • +
  • fixed some broken patches by @D3SOX
  • +
  • fixed framerate in constraints by @kittykel
  • +
  • fixed some first launch switches not applying
  • +
  • fixed potential sandbox escape via custom vencord location
  • +
+
+
+ + https://github.com/Vencord/Vesktop/releases/tag/v1.5.2 + +

What's Changed

+
    +
  • Fixed scrollbars looking wrong (actually Discord's fault)
  • +
  • Tray: Added left click hide/show feature by @0bCdian
  • +
  • MacOS: Fixed the app not properly requesting microphone permissions by @ssalggnikool
  • +
  • Linux: Various fixed related to audio screenshare by @Curve
  • +
  • Linux: Overhauled & improved screenshare with better framerate by @kaitlynkittyy
  • +
  • Users can now pass --enable/disable-features command line flags by @takase1121
  • +
+
+
+ + https://github.com/Vencord/Vesktop/releases/tag/v1.5.1 + +

New Features

+
    +
  • Added categories to Vesktop settings to reduce visual clutter by @justin13888
  • +
  • Added support for Vencord's transparent window options
  • +
+

Fixes

+
    +
  • Fixed ugly error popups when starting Vesktop without working internet connection
  • +
  • Fixed popout title bars on Windows
  • +
  • Fixed spellcheck entries
  • +
  • Fixed screenshare audio using microphone on debian, by @Curve
  • +
  • Fixed a bug where autostart on Linux won't preserve command line flags
  • +
+
+
+ + https://github.com/Vencord/Vesktop/releases/tag/v1.5.0 + +

What's Changed

+
    +
  • fully renamed to Vesktop. You will likely have to login to Discord again. You might have to re-create your vesktop shortcut
  • +
  • added option to disable smooth scrolling by @ZirixCZ
  • +
  • added setting to disable hardware acceleration by @zt64
  • +
  • fixed adding connections
  • +
  • fixed / improved discord popouts
  • +
  • you can now use the custom discord titlebar on linux/mac
  • +
  • the splash window is now draggable
  • +
  • now signed on mac
  • +
+
+
+ + https://github.com/Vencord/Vesktop/releases/tag/v0.4.4 + +

What's Changed

+
    +
  • improve venmic system compatibility by @Curve
  • +
  • Update steamdeck controller layout by @AAGaming00
  • +
  • feat: Add option to disable smooth scrolling by @ZirixCZ
  • +
  • unblur shiggy in splash screen by @viacoro
  • +
  • update electron & arrpc @D3SOX
  • +
+
+
+ + https://github.com/Vencord/Vesktop/releases/tag/v0.4.3 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.4.2 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.4.1 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.4.0 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.3.3 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.3.2 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.3.1 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.3.0 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.9 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.8 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.7 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.6 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.5 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.4 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.3 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.2 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.1 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.2.0 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.9 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.8 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.7 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.6 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.5 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.4 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.3 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.2 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.1 + + + https://github.com/Vencord/Vesktop/releases/tag/v0.1.0 + +
+ https://vencord.dev/ + https://github.com/Vencord/Vesktop/issues + https://vencord.dev/faq/ + https://github.com/Vencord/Vesktop/issues + https://github.com/sponsors/Vendicated + https://github.com/Vencord/Vesktop + + InstantMessaging + Network + + + pointing + keyboard + 420 + always + + + voice + 760 + 1200 + + + intense + intense + intense + intense + + + Discord + Vencord + Vesktop + Privacy + Mod + +
\ No newline at end of file diff --git a/patches/arrpc@3.5.0.patch b/patches/arrpc@3.5.0.patch new file mode 100644 index 0000000..8dfd9f6 --- /dev/null +++ b/patches/arrpc@3.5.0.patch @@ -0,0 +1,14 @@ +diff --git a/src/process/index.js b/src/process/index.js +index 97ea6514b54dd9c5df588c78f0397d31ab5f882a..c2bdbd6aaa5611bc6ff1d993beeb380b1f5ec575 100644 +--- a/src/process/index.js ++++ b/src/process/index.js +@@ -5,8 +5,7 @@ import fs from 'node:fs'; + import { dirname, join } from 'path'; + import { fileURLToPath } from 'url'; + +-const __dirname = dirname(fileURLToPath(import.meta.url)); +-const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8')); ++const DetectableDB = require('./detectable.json'); + + import * as Natives from './native/index.js'; + const Native = Natives[process.platform]; diff --git a/scripts/build/build.mts b/scripts/build/build.mts new file mode 100644 index 0000000..9a9d945 --- /dev/null +++ b/scripts/build/build.mts @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { BuildOptions, build } from "esbuild"; + +const isDev = process.argv.includes("--dev"); + +const CommonOpts: BuildOptions = { + minify: true, + bundle: true, + sourcemap: "inline", + logLevel: "info" +}; + +const NodeCommonOpts: BuildOptions = { + ...CommonOpts, + format: "cjs", + platform: "node", + external: ["electron"], + target: ["esnext"], + define: { + IS_DEV: JSON.stringify(isDev) + } +}; + +async function buildUnpacked() { + await Promise.all([ + build({ + ...NodeCommonOpts, + entryPoints: ["src/main/index.ts"], + outfile: "dist/js/main.js", + footer: { js: "//# sourceURL=VCDMain" } + }), + build({ + ...NodeCommonOpts, + entryPoints: ["src/preload/index.ts"], + outfile: "dist/js/preload.js", + footer: { js: "//# sourceURL=VCDPreload" } + }), + build({ + ...NodeCommonOpts, + entryPoints: ["src/updater/preload.ts"], + outfile: "dist/js/updaterPreload.js", + footer: { js: "//# sourceURL=VCDUpdaterPreload" } + }), + build({ + ...CommonOpts, + globalName: "Vesktop", + entryPoints: ["src/renderer/index.ts"], + outfile: "dist/js/renderer.js", + format: "iife", + inject: ["./scripts/build/injectReact.mjs"], + jsxFactory: "VencordCreateElement", + jsxFragment: "VencordFragment", + external: ["@vencord/types/*"], + footer: { js: "//# sourceURL=VCDRenderer" } + }) + ]); +} + +buildUnpacked().catch(err => { + console.error("Build failed:", err); + process.exit(1); +}); diff --git a/scripts/build/injectReact.mjs b/scripts/build/injectReact.mjs new file mode 100644 index 0000000..ce4cc5f --- /dev/null +++ b/scripts/build/injectReact.mjs @@ -0,0 +1,11 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment"); +export let VencordCreateElement = (...args) => + (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args); diff --git a/scripts/build/sandboxFix.js b/scripts/build/sandboxFix.js new file mode 100644 index 0000000..9f34f06 --- /dev/null +++ b/scripts/build/sandboxFix.js @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +// Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js + +const fs = require("fs/promises"); +const path = require("path"); +let isApplied = false; + +const hook = async () => { + if (isApplied) return; + isApplied = true; + if (process.platform !== "linux") { + // this fix is only required on linux + return; + } + const AppImageTarget = require("app-builder-lib/out/targets/AppImageTarget"); + const oldBuildMethod = AppImageTarget.default.prototype.build; + AppImageTarget.default.prototype.build = async function (...args) { + console.log("Running AppImage builder hook", args); + const oldPath = args[0]; + const newPath = oldPath + "-appimage-sandbox-fix"; + // just in case + try { + await fs.rm(newPath, { + recursive: true + }); + } catch {} + + console.log("Copying to apply appimage fix", oldPath, newPath); + await fs.cp(oldPath, newPath, { + recursive: true + }); + args[0] = newPath; + + const executable = path.join(newPath, this.packager.executableName); + + const loaderScript = ` +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )" +IS_STEAMOS=0 + +if [[ "$SteamOS" == "1" && "$SteamGamepadUI" == "1" ]]; then + echo "Running Vesktop on SteamOS, disabling sandbox" + IS_STEAMOS=1 +fi + +exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ] && echo '--no-sandbox')" "$@" + `.trim(); + + try { + await fs.rename(executable, executable + ".bin"); + await fs.writeFile(executable, loaderScript); + await fs.chmod(executable, 0o755); + } catch (e) { + console.error("failed to create loder for sandbox fix: " + e.message); + throw new Error("Failed to create loader for sandbox fix"); + } + + const ret = await oldBuildMethod.apply(this, args); + + await fs.rm(newPath, { + recursive: true + }); + + return ret; + }; +}; + +module.exports = hook; diff --git a/scripts/build/vencordDep.mts b/scripts/build/vencordDep.mts new file mode 100644 index 0000000..5e211c6 --- /dev/null +++ b/scripts/build/vencordDep.mts @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { globalExternalsWithRegExp } from "@fal-works/esbuild-plugin-global-externals"; + +const names = { + webpack: "Vencord.Webpack", + "webpack/common": "Vencord.Webpack.Common", + utils: "Vencord.Util", + api: "Vencord.Api", + "api/settings": "Vencord", + components: "Vencord.Components" +}; + +export default globalExternalsWithRegExp({ + getModuleInfo(modulePath) { + const path = modulePath.replace("@vencord/types/", ""); + let varName = names[path]; + if (!varName) { + const altMapping = names[path.split("/")[0]]; + if (!altMapping) throw new Error("Unknown module path: " + modulePath); + + varName = + altMapping + + "." + + // @ts-ignore + path.split("/")[1].replaceAll("/", "."); + } + return { + varName, + type: "cjs" + }; + }, + modulePathFilter: /^@vencord\/types.+$/ +}); diff --git a/scripts/header.txt b/scripts/header.txt new file mode 100644 index 0000000..5009304 --- /dev/null +++ b/scripts/header.txt @@ -0,0 +1,7 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ diff --git a/scripts/start.ts b/scripts/start.ts new file mode 100644 index 0000000..a3f980a --- /dev/null +++ b/scripts/start.ts @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import "./utils/dotenv"; + +import { spawnNodeModuleBin } from "./utils/spawn.mjs"; + +spawnNodeModuleBin("electron", [process.cwd(), ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]); diff --git a/scripts/startWatch.mts b/scripts/startWatch.mts new file mode 100644 index 0000000..a115da9 --- /dev/null +++ b/scripts/startWatch.mts @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import "./start"; + +import { spawnNodeModuleBin } from "./utils/spawn.mjs"; +spawnNodeModuleBin("tsx", ["scripts/build/build.mts", "--", "--watch", "--dev"]); diff --git a/scripts/utils/dotenv.ts b/scripts/utils/dotenv.ts new file mode 100644 index 0000000..ddc7bbc --- /dev/null +++ b/scripts/utils/dotenv.ts @@ -0,0 +1,11 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { config } from "dotenv"; + +config(); diff --git a/scripts/utils/spawn.mts b/scripts/utils/spawn.mts new file mode 100644 index 0000000..c5eade0 --- /dev/null +++ b/scripts/utils/spawn.mts @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { spawn as spaaawn, SpawnOptions } from "child_process"; +import { join } from "path"; + +const EXT = process.platform === "win32" ? ".cmd" : ""; + +const OPTS: SpawnOptions = { + stdio: "inherit" +}; + +export function spawnNodeModuleBin(bin: string, args: string[]) { + spaaawn(join("node_modules", ".bin", bin + EXT), args, OPTS); +} diff --git a/scripts/utils/updateMeta.mts b/scripts/utils/updateMeta.mts new file mode 100644 index 0000000..09232f4 --- /dev/null +++ b/scripts/utils/updateMeta.mts @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { promises as fs } from "node:fs"; + +import { DOMParser, XMLSerializer } from "@xmldom/xmldom"; +import xmlFormat from "xml-formatter"; + +function generateDescription(description: string, descriptionNode: Element) { + const lines = description.replace(/\r/g, "").split("\n"); + let currentList: Element | null = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.includes("New Contributors")) { + // we're done, don't parse any more since the new contributors section is the last one + break; + } + + if (line.startsWith("## ")) { + const pNode = descriptionNode.ownerDocument.createElement("p"); + pNode.textContent = line.slice(3); + descriptionNode.appendChild(pNode); + } else if (line.startsWith("* ")) { + const liNode = descriptionNode.ownerDocument.createElement("li"); + liNode.textContent = line.slice(2).split("in https://github.com")[0].trim(); // don't include links to github + + if (!currentList) { + currentList = descriptionNode.ownerDocument.createElement("ul"); + } + + currentList.appendChild(liNode); + } + + if (currentList && !lines[i + 1].startsWith("* ")) { + descriptionNode.appendChild(currentList); + currentList = null; + } + } +} + +const latestReleaseInformation = await fetch("https://api.github.com/repos/Vencord/Vesktop/releases/latest", { + headers: { + Accept: "application/vnd.github+json", + "X-Github-Api-Version": "2022-11-28" + } +}).then(res => res.json()); + +const metaInfo = await fs.readFile("./meta/dev.vencord.Vesktop.metainfo.xml", "utf-8"); + +const parser = new DOMParser().parseFromString(metaInfo, "text/xml"); + +const releaseList = parser.getElementsByTagName("releases")[0]; + +for (let i = 0; i < releaseList.childNodes.length; i++) { + const release = releaseList.childNodes[i] as Element; + + if (release.nodeType === 1 && release.getAttribute("version") === latestReleaseInformation.name) { + console.log("Latest release already added, nothing to be done"); + process.exit(0); + } +} + +const release = parser.createElement("release"); +release.setAttribute("version", latestReleaseInformation.name); +release.setAttribute("date", latestReleaseInformation.published_at.split("T")[0]); +release.setAttribute("type", "stable"); + +const releaseUrl = parser.createElement("url"); +releaseUrl.textContent = latestReleaseInformation.html_url; + +release.appendChild(releaseUrl); + +const description = parser.createElement("description"); + +// we're not using a full markdown parser here since we don't have a lot of formatting options to begin with +generateDescription(latestReleaseInformation.body, description); + +release.appendChild(description); + +releaseList.insertBefore(release, releaseList.childNodes[0]); + +const output = xmlFormat(new XMLSerializer().serializeToString(parser), { + lineSeparator: "\n", + collapseContent: true, + indentation: " " +}); + +await fs.writeFile("./meta/dev.vencord.Vesktop.metainfo.xml", output, "utf-8"); diff --git a/src/globals.d.ts b/src/globals.d.ts new file mode 100644 index 0000000..318d632 --- /dev/null +++ b/src/globals.d.ts @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +declare global { + export var VesktopNative: typeof import("preload/VesktopNative").VesktopNative; + export var Vesktop: typeof import("renderer/index"); + export var VCDP: any; + + export var IS_DEV: boolean; +} + +export {}; diff --git a/src/main/about.ts b/src/main/about.ts new file mode 100644 index 0000000..f53931c --- /dev/null +++ b/src/main/about.ts @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { BrowserWindow } from "electron"; +import { join } from "path"; +import { ICON_PATH, VIEW_DIR } from "shared/paths"; + +import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; + +export function createAboutWindow() { + const about = new BrowserWindow({ + center: true, + autoHideMenuBar: true, + icon: ICON_PATH, + webPreferences: { + preload: join(__dirname, "updaterPreload.js") + } + }); + + makeLinksOpenExternally(about); + + about.loadFile(join(VIEW_DIR, "about.html")); + + return about; +} diff --git a/src/main/appBadge.ts b/src/main/appBadge.ts new file mode 100644 index 0000000..7eb8a47 --- /dev/null +++ b/src/main/appBadge.ts @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { app, NativeImage, nativeImage } from "electron"; +import { join } from "path"; +import { BADGE_DIR } from "shared/paths"; + +const imgCache = new Map(); +function loadBadge(index: number) { + const cached = imgCache.get(index); + if (cached) return cached; + + const img = nativeImage.createFromPath(join(BADGE_DIR, `${index}.ico`)); + imgCache.set(index, img); + + return img; +} + +let lastIndex: null | number = -1; + +export function setBadgeCount(count: number) { + switch (process.platform) { + case "linux": + if (count === -1) count = 0; + app.setBadgeCount(count); + break; + case "darwin": + if (count === 0) { + app.dock.setBadge(""); + break; + } + app.dock.setBadge(count === -1 ? "•" : count.toString()); + break; + case "win32": + const [index, description] = getBadgeIndexAndDescription(count); + if (lastIndex === index) break; + + lastIndex = index; + + // circular import shenanigans + const { mainWin } = require("./mainWindow") as typeof import("./mainWindow"); + mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description); + break; + } +} + +function getBadgeIndexAndDescription(count: number): [number | null, string] { + if (count === -1) return [11, "Unread Messages"]; + if (count === 0) return [null, "No Notifications"]; + + const index = Math.max(1, Math.min(count, 10)); + return [index, `${index} Notification`]; +} diff --git a/src/main/arrpc.ts b/src/main/arrpc.ts new file mode 100644 index 0000000..d6062d1 --- /dev/null +++ b/src/main/arrpc.ts @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import Server from "arrpc"; +import { IpcEvents } from "shared/IpcEvents"; + +import { mainWin } from "./mainWindow"; +import { Settings } from "./settings"; + +let server: any; + +const inviteCodeRegex = /^(\w|-)+$/; + +export async function initArRPC() { + if (server || !Settings.store.arRPC) return; + + try { + server = await new Server(); + server.on("activity", (data: any) => mainWin.webContents.send(IpcEvents.ARRPC_ACTIVITY, JSON.stringify(data))); + server.on("invite", (invite: string, callback: (valid: boolean) => void) => { + invite = String(invite); + if (!inviteCodeRegex.test(invite)) return callback(false); + + mainWin.webContents + // Safety: Result of JSON.stringify should always be safe to equal + // Also, just to be super super safe, invite is regex validated above + .executeJavaScript(`Vesktop.openInviteModal(${JSON.stringify(invite)})`) + .then(callback); + }); + } catch (e) { + console.error("Failed to start arRPC server", e); + } +} + +Settings.addChangeListener("arRPC", initArRPC); diff --git a/src/main/autoStart.ts b/src/main/autoStart.ts new file mode 100644 index 0000000..6f832bc --- /dev/null +++ b/src/main/autoStart.ts @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { app } from "electron"; +import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs"; +import { join } from "path"; + +interface AutoStart { + isEnabled(): boolean; + enable(): void; + disable(): void; +} + +function makeAutoStartLinux(): AutoStart { + const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config"); + const dir = join(configDir, "autostart"); + const file = join(dir, "vesktop.desktop"); + + // IM STUPID + const legacyName = join(dir, "vencord.desktop"); + if (existsSync(legacyName)) renameSync(legacyName, file); + + // "Quoting must be done by enclosing the argument between double quotes and escaping the double quote character, + // backtick character ("`"), dollar sign ("$") and backslash character ("\") by preceding it with an additional backslash character" + // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables + const commandLine = process.argv.map(arg => '"' + arg.replace(/["$`\\]/g, "\\$&") + '"').join(" "); + + return { + isEnabled: () => existsSync(file), + enable() { + const desktopFile = ` +[Desktop Entry] +Type=Application +Name=Vesktop +Comment=Vesktop autostart script +Exec=${commandLine} +StartupNotify=false +Terminal=false +`.trim(); + + mkdirSync(dir, { recursive: true }); + writeFileSync(file, desktopFile); + }, + disable: () => rmSync(file, { force: true }) + }; +} + +const autoStartWindowsMac: AutoStart = { + isEnabled: () => app.getLoginItemSettings().openAtLogin, + enable: () => app.setLoginItemSettings({ openAtLogin: true }), + disable: () => app.setLoginItemSettings({ openAtLogin: false }) +}; + +export const autoStart = process.platform === "linux" ? makeAutoStartLinux() : autoStartWindowsMac; diff --git a/src/main/constants.ts b/src/main/constants.ts new file mode 100644 index 0000000..34231fd --- /dev/null +++ b/src/main/constants.ts @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { app } from "electron"; +import { existsSync, mkdirSync, readdirSync, renameSync, rmdirSync } from "fs"; +import { dirname, join } from "path"; + +const vesktopDir = dirname(process.execPath); + +export const PORTABLE = + process.platform === "win32" && + !process.execPath.toLowerCase().endsWith("electron.exe") && + !existsSync(join(vesktopDir, "Uninstall Vesktop.exe")); + +const LEGACY_DATA_DIR = join(app.getPath("appData"), "VencordDesktop", "VencordDesktop"); +export const DATA_DIR = + process.env.VENCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Aerocord_Data") : join(app.getPath("userData"))); + +mkdirSync(DATA_DIR, { recursive: true }); + +// TODO: remove eventually +if (existsSync(LEGACY_DATA_DIR)) { + try { + console.warn("Detected legacy settings dir", LEGACY_DATA_DIR + ".", "migrating to", DATA_DIR); + for (const file of readdirSync(LEGACY_DATA_DIR)) { + renameSync(join(LEGACY_DATA_DIR, file), join(DATA_DIR, file)); + } + rmdirSync(LEGACY_DATA_DIR); + renameSync( + join(app.getPath("appData"), "VencordDesktop", "IndexedDB"), + join(DATA_DIR, "sessionData", "IndexedDB") + ); + } catch (e) { + console.error("Migration failed", e); + } +} +const SESSION_DATA_DIR = join(DATA_DIR, "sessionData"); +app.setPath("sessionData", SESSION_DATA_DIR); + +export const VENCORD_SETTINGS_DIR = join(DATA_DIR, "settings"); +export const VENCORD_QUICKCSS_FILE = join(VENCORD_SETTINGS_DIR, "quickCss.css"); +export const VENCORD_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json"); +export const VENCORD_THEMES_DIR = join(DATA_DIR, "themes"); + +// needs to be inline require because of circular dependency +// as otherwise "DATA_DIR" (which is used by ./settings) will be uninitialised +export const VENCORD_FILES_DIR = + (require("./settings") as typeof import("./settings")).State.store.vencordDir || + join(SESSION_DATA_DIR, "vencordFiles"); + +export const USER_AGENT = `Vesktop/${app.getVersion()} (https://github.com/Vencord/Vesktop)`; + +// dimensions shamelessly stolen from Discord Desktop :3 +export const MIN_WIDTH = 940; +export const MIN_HEIGHT = 500; +export const DEFAULT_WIDTH = 1280; +export const DEFAULT_HEIGHT = 720; + +export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"]; + +const VersionString = `AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome.split(".")[0]}.0.0.0 Safari/537.36`; +const BrowserUserAgents = { + darwin: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ${VersionString}`, + linux: `Mozilla/5.0 (X11; Linux x86_64) ${VersionString}`, + windows: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) ${VersionString}` +}; + +export const BrowserUserAgent = BrowserUserAgents[process.platform] || BrowserUserAgents.windows; + +export const enum MessageBoxChoice { + Default, + Cancel +} diff --git a/src/main/firstLaunch.ts b/src/main/firstLaunch.ts new file mode 100644 index 0000000..d588e83 --- /dev/null +++ b/src/main/firstLaunch.ts @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { app } from "electron"; +import { BrowserWindow } from "electron/main"; +import { copyFileSync, mkdirSync, readdirSync } from "fs"; +import { join } from "path"; +import { SplashProps } from "shared/browserWinProperties"; +import { ICON_PATH, VIEW_DIR } from "shared/paths"; + +import { autoStart } from "./autoStart"; +import { DATA_DIR } from "./constants"; +import { createWindows } from "./mainWindow"; +import { Settings, State } from "./settings"; +import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; + +interface Data { + discordBranch: "stable" | "canary" | "ptb"; + minimizeToTray?: "on"; + autoStart?: "on"; + importSettings?: "on"; + richPresence?: "on"; +} + +export function createFirstLaunchTour() { + const win = new BrowserWindow({ + ...SplashProps, + frame: true, + autoHideMenuBar: true, + height: 470, + width: 550, + icon: ICON_PATH + }); + + makeLinksOpenExternally(win); + + win.loadFile(join(VIEW_DIR, "first-launch.html")); + win.webContents.addListener("console-message", (_e, _l, msg) => { + if (msg === "cancel") return app.exit(); + + if (!msg.startsWith("form:")) return; + const data = JSON.parse(msg.slice(5)) as Data; + + console.log(data); + State.store.firstLaunch = false; + Settings.store.discordBranch = data.discordBranch; + Settings.store.minimizeToTray = !!data.minimizeToTray; + Settings.store.arRPC = !!data.richPresence; + + if (data.autoStart) autoStart.enable(); + + if (data.importSettings) { + const from = join(app.getPath("userData"), "..", "Vencord", "settings"); + const to = join(DATA_DIR, "settings"); + try { + const files = readdirSync(from); + mkdirSync(to, { recursive: true }); + + for (const file of files) { + copyFileSync(join(from, file), join(to, file)); + } + } catch (e) { + console.error("Failed to import settings:", e); + } + } + + win.close(); + + createWindows(); + }); +} diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..7f1225d --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,117 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import "./ipc"; + +import { app, BrowserWindow, nativeTheme } from "electron"; +import { checkUpdates } from "updater/main"; + +import { DATA_DIR } from "./constants"; +import { createFirstLaunchTour } from "./firstLaunch"; +import { createWindows, mainWin } from "./mainWindow"; +import { registerMediaPermissionsHandler } from "./mediaPermissions"; +import { registerScreenShareHandler } from "./screenShare"; +import { Settings, State } from "./settings"; +import { isDeckGameMode } from "./utils/steamOS"; + +if (IS_DEV) { + require("source-map-support").install(); +} + +process.env.VENCORD_USER_DATA_DIR = DATA_DIR; + +function init() { + const { disableSmoothScroll, hardwareAcceleration } = Settings.store; + + const enabledFeatures = app.commandLine.getSwitchValue("enable-features").split(","); + const disabledFeatures = app.commandLine.getSwitchValue("disable-features").split(","); + + if (hardwareAcceleration === false) { + app.disableHardwareAcceleration(); + } else { + enabledFeatures.push("VaapiVideoDecodeLinuxGL", "VaapiVideoEncoder", "VaapiVideoDecoder"); + } + + if (disableSmoothScroll) { + app.commandLine.appendSwitch("disable-smooth-scrolling"); + } + + // disable renderer backgrounding to prevent the app from unloading when in the background + // https://github.com/electron/electron/issues/2822 + // https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling + app.commandLine.appendSwitch("disable-renderer-backgrounding"); + app.commandLine.appendSwitch("disable-background-timer-throttling"); + app.commandLine.appendSwitch("disable-backgrounding-occluded-windows"); + if (process.platform === "win32") { + disabledFeatures.push("CalculateNativeWinOcclusion"); + } + + // work around chrome 66 disabling autoplay by default + app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); + // WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows. + // HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service. + // + // WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790 + disabledFeatures.push("WinRetrieveSuggestionsOnlyOnDemand", "HardwareMediaKeyHandling", "MediaSessionService"); + + // Support TTS on Linux using speech-dispatcher + app.commandLine.appendSwitch("enable-speech-dispatcher"); + + app.commandLine.appendSwitch("enable-features", [...new Set(enabledFeatures)].filter(Boolean).join(",")); + app.commandLine.appendSwitch("disable-features", [...new Set(disabledFeatures)].filter(Boolean).join(",")); + + // In the Flatpak on SteamOS the theme is detected as light, but SteamOS only has a dark mode, so we just override it + if (isDeckGameMode) nativeTheme.themeSource = "dark"; + + app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => { + if (data.IS_DEV) app.quit(); + else if (mainWin) { + if (mainWin.isMinimized()) mainWin.restore(); + if (!mainWin.isVisible()) mainWin.show(); + mainWin.focus(); + } + }); + + app.whenReady().then(async () => { + checkUpdates(); + if (process.platform === "win32") app.setAppUserModelId("dev.vencord.vesktop"); + + registerScreenShareHandler(); + registerMediaPermissionsHandler(); + + bootstrap(); + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) createWindows(); + }); + }); +} + +if (!app.requestSingleInstanceLock({ IS_DEV })) { + if (IS_DEV) { + console.log("Vesktop is already running. Quitting previous instance..."); + init(); + } else { + console.log("Vesktop is already running. Quitting..."); + app.quit(); + } +} else { + init(); +} + +async function bootstrap() { + if (!Object.hasOwn(State.store, "firstLaunch")) { + createFirstLaunchTour(); + } else { + createWindows(); + } +} + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); +}); diff --git a/src/main/ipc.ts b/src/main/ipc.ts new file mode 100644 index 0000000..c7fad79 --- /dev/null +++ b/src/main/ipc.ts @@ -0,0 +1,168 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +if (process.platform === "linux") import("./venmic"); + +import { execFile } from "child_process"; +import { app, BrowserWindow, clipboard, dialog, nativeImage, RelaunchOptions, session, shell } from "electron"; +import { mkdirSync, readFileSync, watch } from "fs"; +import { open, readFile } from "fs/promises"; +import { release } from "os"; +import { join } from "path"; +import { debounce } from "shared/utils/debounce"; + +import { IpcEvents } from "../shared/IpcEvents"; +import { setBadgeCount } from "./appBadge"; +import { autoStart } from "./autoStart"; +import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants"; +import { mainWin } from "./mainWindow"; +import { Settings, State } from "./settings"; +import { handle, handleSync } from "./utils/ipcWrappers"; +import { PopoutWindows } from "./utils/popout"; +import { isDeckGameMode, showGamePage } from "./utils/steamOS"; +import { isValidVencordInstall } from "./utils/vencordLoader"; +import { isvencorddisabled } from "main/mainWindow"; + +if (!isvencorddisabled) { + handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload.js")); +} else { + handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload1.js")); +} + +handleSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT, () => + readFileSync(join(VENCORD_FILES_DIR, "vencordDesktopRenderer.js"), "utf-8") +); + +handleSync(IpcEvents.GET_RENDERER_SCRIPT, () => readFileSync(join(__dirname, "renderer.js"), "utf-8")); +handleSync(IpcEvents.GET_RENDERER_CSS_FILE, () => join(__dirname, "renderer.css")); + +handleSync(IpcEvents.GET_SETTINGS, () => Settings.plain); +handleSync(IpcEvents.GET_VERSION, () => app.getVersion()); + +handleSync( + IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY, + () => process.platform === "win32" && Number(release().split(".").pop()) >= 22621 +); + +handleSync(IpcEvents.AUTOSTART_ENABLED, () => autoStart.isEnabled()); +handle(IpcEvents.ENABLE_AUTOSTART, autoStart.enable); +handle(IpcEvents.DISABLE_AUTOSTART, autoStart.disable); + +handle(IpcEvents.SET_SETTINGS, (_, settings: typeof Settings.store, path?: string) => { + Settings.setData(settings, path); +}); + +handle(IpcEvents.RELAUNCH, async () => { + const options: RelaunchOptions = { + args: process.argv.slice(1).concat(["--relaunch"]) + }; + if (isDeckGameMode) { + // We can't properly relaunch when running under gamescope, but we can at least navigate to our page in Steam. + await showGamePage(); + } else if (app.isPackaged && process.env.APPIMAGE) { + execFile(process.env.APPIMAGE, options.args); + } else { + app.relaunch(options); + } + app.exit(); +}); + +handle(IpcEvents.SHOW_ITEM_IN_FOLDER, (_, path) => { + shell.showItemInFolder(path); +}); + +handle(IpcEvents.FOCUS, () => { + mainWin.show(); + mainWin.setSkipTaskbar(false); +}); + +handle(IpcEvents.CLOSE, (e, key?: string) => { + const popout = PopoutWindows.get(key!); + if (popout) return popout.close(); + + const win = BrowserWindow.fromWebContents(e.sender) ?? e.sender; + win.close(); +}); + +handle(IpcEvents.MINIMIZE, e => { + mainWin.minimize(); +}); + +handle(IpcEvents.MAXIMIZE, e => { + if (mainWin.isMaximized()) { + mainWin.unmaximize(); + } else { + mainWin.maximize(); + } +}); + +handleSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES, e => { + e.returnValue = session.defaultSession.availableSpellCheckerLanguages; +}); + +handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => { + e.sender.replaceMisspelling(word); +}); + +handle(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, (e, word: string) => { + e.sender.session.addWordToSpellCheckerDictionary(word); +}); + +handleSync(IpcEvents.GET_VENCORD_DIR, e => (e.returnValue = State.store.vencordDir)); + +handle(IpcEvents.SELECT_VENCORD_DIR, async (_e, value?: null) => { + if (value === null) { + delete State.store.vencordDir; + return "ok"; + } + + const res = await dialog.showOpenDialog(mainWin!, { + properties: ["openDirectory"] + }); + if (!res.filePaths.length) return "cancelled"; + + const dir = res.filePaths[0]; + if (!isValidVencordInstall(dir)) return "invalid"; + + State.store.vencordDir = dir; + + return "ok"; +}); + +handle(IpcEvents.SET_BADGE_COUNT, (_, count: number) => setBadgeCount(count)); + +handle(IpcEvents.CLIPBOARD_COPY_IMAGE, async (_, buf: ArrayBuffer, src: string) => { + clipboard.write({ + html: ``, + image: nativeImage.createFromBuffer(Buffer.from(buf)) + }); +}); + +function readCss() { + return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => ""); +} + +open(VENCORD_QUICKCSS_FILE, "a+").then(fd => { + fd.close(); + watch( + VENCORD_QUICKCSS_FILE, + { persistent: false }, + debounce(async () => { + mainWin?.webContents.postMessage("VencordQuickCssUpdate", await readCss()); + }, 50) + ); +}); + +mkdirSync(VENCORD_THEMES_DIR, { recursive: true }); +watch( + VENCORD_THEMES_DIR, + { persistent: false }, + debounce(() => { + mainWin?.webContents.postMessage("VencordThemeUpdate", void 0); + }) +); diff --git a/src/main/mainWindow.ts b/src/main/mainWindow.ts new file mode 100644 index 0000000..d23841a --- /dev/null +++ b/src/main/mainWindow.ts @@ -0,0 +1,534 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { + app, + BrowserWindow, + BrowserWindowConstructorOptions, + dialog, + Menu, + MenuItemConstructorOptions, + nativeTheme, + screen, + shell, + session, + Tray +} from "electron"; +import { rm } from "fs/promises"; +import { join } from "path"; +import { IpcEvents } from "shared/IpcEvents"; +import { isTruthy } from "shared/utils/guards"; +import { once } from "shared/utils/once"; +import type { SettingsStore } from "shared/utils/SettingsStore"; + +import { ICON_PATH } from "../shared/paths"; +import { createAboutWindow } from "./about"; +import { initArRPC } from "./arrpc"; +import { + BrowserUserAgent, + DATA_DIR, + DEFAULT_HEIGHT, + DEFAULT_WIDTH, + MessageBoxChoice, + MIN_HEIGHT, + MIN_WIDTH, + VENCORD_FILES_DIR +} from "./constants"; +import { Settings, State, VencordSettings } from "./settings"; +import { createSplashWindow } from "./splash"; +import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; +import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS"; +import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader"; + +let isQuitting = false; +let tray: Tray; + +applyDeckKeyboardFix(); + +app.on("before-quit", () => { + isQuitting = true; +}); + +export let mainWin: BrowserWindow; + +function makeSettingsListenerHelpers(o: SettingsStore) { + const listeners = new Map<(data: any) => void, PropertyKey>(); + + const addListener: typeof o.addChangeListener = (path, cb) => { + listeners.set(cb, path); + o.addChangeListener(path, cb); + }; + const removeAllListeners = () => { + for (const [listener, path] of listeners) { + o.removeChangeListener(path as any, listener); + } + + listeners.clear(); + }; + + return [addListener, removeAllListeners] as const; +} + +const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpers(Settings); +const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings); + +function initTray(win: BrowserWindow) { + const onTrayClick = () => { + if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide(); + else win.show(); + }; + const trayMenu = Menu.buildFromTemplate([ + { + label: "Open", + click() { + win.show(); + } + }, + { + label: "About", + click: createAboutWindow + }, + { + label: "Repair Vencord", + async click() { + await downloadVencordFiles(); + app.relaunch(); + app.quit(); + } + }, + { + label: "Reset User Data", + async click() { + await clearData(win); + } + }, + { + type: "separator" + }, + { + label: "Open Updater", + click() { + const updaterPath = join(app.getPath('exe'), '..', 'Updater.exe'); + shell.openPath(updaterPath); + } + }, + { + label: "Restart", + click() { + app.relaunch(); + app.quit(); + } + }, + { + label: "Close", + click() { + isQuitting = true; + app.quit(); + } + } + ]); + + tray = new Tray(ICON_PATH); + tray.setToolTip("Aerocord"); + tray.setContextMenu(trayMenu); + tray.on("click", onTrayClick); +} + +async function clearData(win: BrowserWindow) { + const { response } = await dialog.showMessageBox(win, { + message: "Are you sure you want to reset Aerocord?", + detail: "This will log you out, clear caches and reset all your settings!\n\Aerocord will automatically restart after this operation.", + buttons: ["Yes", "No"], + cancelId: MessageBoxChoice.Cancel, + defaultId: MessageBoxChoice.Default, + type: "warning" + }); + + if (response === MessageBoxChoice.Cancel) return; + + win.close(); + + await win.webContents.session.clearStorageData(); + await win.webContents.session.clearCache(); + await win.webContents.session.clearCodeCaches({}); + await rm(DATA_DIR, { force: true, recursive: true }); + + app.relaunch(); + app.quit(); +} + +type MenuItemList = Array; + +function initMenuBar(win: BrowserWindow) { + const isWindows = process.platform === "win32"; + const isDarwin = process.platform === "darwin"; + const wantCtrlQ = !isWindows || VencordSettings.store.winCtrlQ; + + const subMenu = [ + { + label: "About Vesktop", + click: createAboutWindow + }, + { + label: "Force Update Vencord", + async click() { + await downloadVencordFiles(); + app.relaunch(); + app.quit(); + }, + toolTip: "Vesktop will automatically restart after this operation" + }, + { + label: "Reset Aerocord", + async click() { + await clearData(win); + }, + toolTip: "Vesktop will automatically restart after this operation" + }, + { + label: "Relaunch", + accelerator: "CmdOrCtrl+Shift+R", + click() { + app.relaunch(); + app.quit(); + } + }, + ...(!isDarwin + ? [] + : ([ + { + type: "separator" + }, + { + label: "Settings", + accelerator: "CmdOrCtrl+,", + async click() { + mainWin.webContents.executeJavaScript( + "Vencord.Webpack.Common.SettingsRouter.open('My Account')" + ); + } + }, + { + type: "separator" + }, + { + role: "hide" + }, + { + role: "hideOthers" + }, + { + role: "unhide" + }, + { + type: "separator" + } + ] satisfies MenuItemList)), + { + label: "Quit", + accelerator: wantCtrlQ ? "CmdOrCtrl+Q" : void 0, + visible: !isWindows, + role: "quit", + click() { + app.quit(); + } + }, + isWindows && { + label: "Quit", + accelerator: "Alt+F4", + role: "quit", + click() { + app.quit(); + } + }, + // See https://github.com/electron/electron/issues/14742 and https://github.com/electron/electron/issues/5256 + { + label: "Zoom in (hidden, hack for Qwertz and others)", + accelerator: "CmdOrCtrl+=", + role: "zoomIn", + visible: false + } + ] satisfies MenuItemList; + + const menu = Menu.buildFromTemplate([ + { + label: "Vesktop", + role: "appMenu", + submenu: subMenu.filter(isTruthy) + }, + { role: "fileMenu" }, + { role: "editMenu" }, + { role: "viewMenu" }, + { role: "windowMenu" } + ]); + + Menu.setApplicationMenu(menu); +} + +function getWindowBoundsOptions(): BrowserWindowConstructorOptions { + // We want the default window behaivour to apply in game mode since it expects everything to be fullscreen and maximized. + if (isDeckGameMode) return {}; + + const { x, y, width, height } = State.store.windowBounds ?? {}; + + const options = { + width: width ?? DEFAULT_WIDTH, + height: height ?? DEFAULT_HEIGHT + } as BrowserWindowConstructorOptions; + + const storedDisplay = screen.getAllDisplays().find(display => display.id === State.store.displayid); + + if (x != null && y != null && storedDisplay) { + options.x = x; + options.y = y; + } + + if (!Settings.store.disableMinSize) { + options.minWidth = MIN_WIDTH; + options.minHeight = MIN_HEIGHT; + } + + return options; +} + +function getDarwinOptions(): BrowserWindowConstructorOptions { + const options = { + titleBarStyle: "hidden", + trafficLightPosition: { x: 10, y: 10 } + } as BrowserWindowConstructorOptions; + + const { splashTheming, splashBackground } = Settings.store; + const { macosTranslucency } = VencordSettings.store; + + if (macosTranslucency) { + options.vibrancy = "sidebar"; + options.backgroundColor = "#ffffff00"; + } else { + if (splashTheming) { + options.backgroundColor = splashBackground; + } else { + options.backgroundColor = nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff"; + } + } + + return options; +} + +function initWindowBoundsListeners(win: BrowserWindow) { + const saveState = () => { + State.store.maximized = win.isMaximized(); + State.store.minimized = win.isMinimized(); + }; + + win.on("maximize", saveState); + win.on("minimize", saveState); + win.on("unmaximize", saveState); + + const saveBounds = () => { + State.store.windowBounds = win.getBounds(); + State.store.displayid = screen.getDisplayMatching(State.store.windowBounds).id; + }; + + win.on("resize", saveBounds); + win.on("move", saveBounds); +} + +function initSettingsListeners(win: BrowserWindow) { + addSettingsListener("tray", enable => { + if (enable) initTray(win); + else tray?.destroy(); + }); + addSettingsListener("disableMinSize", disable => { + if (disable) { + // 0 no work + win.setMinimumSize(1, 1); + } else { + win.setMinimumSize(MIN_WIDTH, MIN_HEIGHT); + + const { width, height } = win.getBounds(); + win.setBounds({ + width: Math.max(width, MIN_WIDTH), + height: Math.max(height, MIN_HEIGHT) + }); + } + }); + + addVencordSettingsListener("macosTranslucency", enabled => { + if (enabled) { + win.setVibrancy("sidebar"); + win.setBackgroundColor("#ffffff00"); + } else { + win.setVibrancy(null); + win.setBackgroundColor("#ffffff"); + } + }); + + addSettingsListener("enableMenu", enabled => { + win.setAutoHideMenuBar(enabled ?? false); + }); + + addSettingsListener("spellCheckLanguages", languages => initSpellCheckLanguages(win, languages)); +} + +async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) { + languages ??= await win.webContents.executeJavaScript("[...new Set(navigator.languages)]").catch(() => []); + if (!languages) return; + + const ses = session.defaultSession; + + const available = ses.availableSpellCheckerLanguages; + const applicable = languages.filter(l => available.includes(l)).slice(0, 5); + if (applicable.length) ses.setSpellCheckerLanguages(applicable); +} + +function initSpellCheck(win: BrowserWindow) { + win.webContents.on("context-menu", (_, data) => { + win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions); + }); + + initSpellCheckLanguages(win, Settings.store.spellCheckLanguages); +} + +function createMainWindow() { + // Clear up previous settings listeners + removeSettingsListeners(); + removeVencordSettingsListeners(); + + const { staticTitle, transparencyOption, enableMenu, customTitleBar } = Settings.store; + + const { frameless, transparent } = VencordSettings.store; + + const noFrame = frameless === true || customTitleBar === true; + + const win = (mainWin = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: false, + sandbox: false, + contextIsolation: true, + devTools: true, + preload: join(__dirname, "preload.js"), + spellcheck: true, + // disable renderer backgrounding to prevent the app from unloading when in the background + backgroundThrottling: false + }, + icon: ICON_PATH, + frame: !noFrame, + ...(transparent && { + transparent: true, + backgroundColor: "#00000000" + }), + ...(transparencyOption && + transparencyOption !== "none" && { + backgroundColor: "#00000000", + backgroundMaterial: transparencyOption + }), + // Fix transparencyOption for custom discord titlebar + ...(customTitleBar && + transparencyOption && + transparencyOption !== "none" && { + transparent: true + }), + ...(staticTitle && { title: "Vesktop" }), + ...(process.platform === "darwin" && getDarwinOptions()), + ...getWindowBoundsOptions(), + autoHideMenuBar: enableMenu + })); + win.setMenuBarVisibility(false); + if (process.platform === "darwin" && customTitleBar) win.setWindowButtonVisibility(false); + + win.on("close", e => { + const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false; + if (isQuitting || (process.platform !== "darwin" && !useTray)) return; + + e.preventDefault(); + + if (process.platform === "darwin") app.hide(); + else win.hide(); + + return false; + }); + + if (Settings.store.staticTitle) win.on("page-title-updated", e => e.preventDefault()); + + initWindowBoundsListeners(win); + if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win); + initMenuBar(win); + makeLinksOpenExternally(win); + initSettingsListeners(win); + initSpellCheck(win); + + win.webContents.setUserAgent(BrowserUserAgent); + + const subdomain = + Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb" + ? `${Settings.store.discordBranch}.` + : ""; + + win.loadURL(`https://${subdomain}discord.com/app`); + + return win; +} +const fs = require('fs'); +const path = require('path'); + +export const isvencorddisabled: boolean = fs.existsSync(path.join(app.getPath('exe'), '..', 'be_gone_vendicated.txt')); +const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js"))); + +const extension = path.join(app.getPath('exe'), '..', 'extension'); + +function vendicated() { + if (!isvencorddisabled) { + runVencordMain(); + console.log('vencord is enabled'); + } else { + console.log('vencord is disabled'); + } + } + +export async function createWindows() { + const startMinimized = process.argv.includes("--start-minimized"); + const splash = createSplashWindow(startMinimized); + // SteamOS letterboxes and scales it terribly, so just full screen it + if (isDeckGameMode) splash.setFullScreen(true); + await ensureVencordFiles(); + vendicated(); + + mainWin = createMainWindow(); + + session.defaultSession.loadExtension(extension) + .then(() => { + console.log('extension is loaded'); + }) + .catch(() => { + console.warn('extension is not loaded'); + }) + .finally(() => { + mainWin.webContents.on("did-finish-load", () => { + splash.destroy(); + + if (!startMinimized) { + mainWin.show(); + if (State.store.maximized && !isDeckGameMode) mainWin.maximize(); + } + + if (isDeckGameMode) { + mainWin.setFullScreen(true); + askToApplySteamLayout(mainWin); + } + + mainWin.once("show", () => { + if (State.store.maximized && !mainWin.isMaximized() && !isDeckGameMode) { + mainWin.maximize(); + } + }); + }); + + initArRPC(); + }); +} diff --git a/src/main/mediaPermissions.ts b/src/main/mediaPermissions.ts new file mode 100644 index 0000000..ee3cbc9 --- /dev/null +++ b/src/main/mediaPermissions.ts @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { session, systemPreferences } from "electron"; + +export function registerMediaPermissionsHandler() { + if (process.platform !== "darwin") return; + + session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => { + let granted = true; + + if ("mediaTypes" in details) { + if (details.mediaTypes?.includes("audio")) { + granted &&= await systemPreferences.askForMediaAccess("microphone"); + } + if (details.mediaTypes?.includes("video")) { + granted &&= await systemPreferences.askForMediaAccess("camera"); + } + } + + callback(granted); + }); +} diff --git a/src/main/screenShare.ts b/src/main/screenShare.ts new file mode 100644 index 0000000..08021f2 --- /dev/null +++ b/src/main/screenShare.ts @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { desktopCapturer, session, Streams } from "electron"; +import type { StreamPick } from "renderer/components/ScreenSharePicker"; +import { IpcEvents } from "shared/IpcEvents"; + +import { handle } from "./utils/ipcWrappers"; + +const isWayland = + process.platform === "linux" && (process.env.XDG_SESSION_TYPE === "wayland" || !!process.env.WAYLAND_DISPLAY); + +export function registerScreenShareHandler() { + handle(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, async (_, id: string) => { + const sources = await desktopCapturer.getSources({ + types: ["window", "screen"], + thumbnailSize: { + width: 1920, + height: 1080 + } + }); + return sources.find(s => s.id === id)?.thumbnail.toDataURL(); + }); + + session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => { + // request full resolution on wayland right away because we always only end up with one result anyway + const width = isWayland ? 1920 : 176; + const sources = await desktopCapturer + .getSources({ + types: ["window", "screen"], + thumbnailSize: { + width, + height: width * (9 / 16) + } + }) + .catch(err => console.error("Error during screenshare picker", err)); + + if (!sources) return callback({}); + + const data = sources.map(({ id, name, thumbnail }) => ({ + id, + name, + url: thumbnail.toDataURL() + })); + + if (isWayland) { + const video = data[0]; + if (video) { + const stream = await request + .frame!.executeJavaScript( + `Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify([video])},true)` + ) + .catch(() => null); + if (stream === null) return callback({}); + } + + callback(video ? { video: sources[0] } : {}); + return; + } + + const choice = await request + .frame!.executeJavaScript(`Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify(data)})`) + .then(e => e as StreamPick) + .catch(e => { + console.error("Error during screenshare picker", e); + return null; + }); + + if (!choice) return callback({}); + + const source = sources.find(s => s.id === choice.id); + if (!source) return callback({}); + + const streams: Streams = { + video: source + }; + if (choice.audio && process.platform === "win32") streams.audio = "loopback"; + + callback(streams); + }); +} diff --git a/src/main/settings.ts b/src/main/settings.ts new file mode 100644 index 0000000..58c8906 --- /dev/null +++ b/src/main/settings.ts @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { dirname, join } from "path"; +import type { Settings as TSettings, State as TState } from "shared/settings"; +import { SettingsStore } from "shared/utils/SettingsStore"; + +import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants"; + +const SETTINGS_FILE = join(DATA_DIR, "settings.json"); +const STATE_FILE = join(DATA_DIR, "state.json"); + +function loadSettings(file: string, name: string) { + let settings = {} as T; + try { + const content = readFileSync(file, "utf8"); + try { + settings = JSON.parse(content); + } catch (err) { + console.error(`Failed to parse ${name}.json:`, err); + } + } catch {} + + const store = new SettingsStore(settings); + store.addGlobalChangeListener(o => { + mkdirSync(dirname(file), { recursive: true }); + writeFileSync(file, JSON.stringify(o, null, 4)); + }); + + return store; +} + +export const Settings = loadSettings(SETTINGS_FILE, "Vesktop settings"); + +export const VencordSettings = loadSettings(VENCORD_SETTINGS_FILE, "Vencord settings"); + +if (Object.hasOwn(Settings.plain, "firstLaunch") && !existsSync(STATE_FILE)) { + console.warn("legacy state in settings.json detected. migrating to state.json"); + const state = {} as TState; + for (const prop of [ + "firstLaunch", + "maximized", + "minimized", + "skippedUpdate", + "steamOSLayoutVersion", + "windowBounds" + ] as const) { state[prop] = Settings.plain[prop]; + delete Settings.plain[prop]; + } + Settings.markAsChanged(); + writeFileSync(STATE_FILE, JSON.stringify(state, null, 4)); +} + +export const State = loadSettings(STATE_FILE, "Vesktop state"); diff --git a/src/main/splash.ts b/src/main/splash.ts new file mode 100644 index 0000000..a8cadcd --- /dev/null +++ b/src/main/splash.ts @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { BrowserWindow } from "electron"; +import { join } from "path"; +import { SplashProps } from "shared/browserWinProperties"; +import { ICON_PATH, VIEW_DIR } from "shared/paths"; + +import { Settings } from "./settings"; + +export function createSplashWindow(startMinimized = false) { + const splash = new BrowserWindow({ + ...SplashProps, + icon: ICON_PATH, + show: !startMinimized + }); + + splash.loadFile(join(VIEW_DIR, "splash.html")); + + const { splashBackground, splashColor, splashTheming } = Settings.store; + + if (splashTheming) { + if (splashColor) { + const semiTransparentSplashColor = splashColor.replace("rgb(", "rgba(").replace(")", ", 0.2)"); + + splash.webContents.insertCSS(`body { --fg: ${splashColor} !important }`); + splash.webContents.insertCSS(`body { --fg-semi-trans: ${semiTransparentSplashColor} !important }`); + } + + if (splashBackground) { + splash.webContents.insertCSS(`body { --bg: ${splashBackground} !important }`); + } + } + + return splash; +} diff --git a/src/main/utils/http.ts b/src/main/utils/http.ts new file mode 100644 index 0000000..54998b2 --- /dev/null +++ b/src/main/utils/http.ts @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { createWriteStream } from "fs"; +import { Readable } from "stream"; +import { pipeline } from "stream/promises"; +import { setTimeout } from "timers/promises"; + +interface FetchieOptions { + retryOnNetworkError?: boolean; +} + +export async function downloadFile(url: string, file: string, options: RequestInit = {}, fetchieOpts?: FetchieOptions) { + const res = await fetchie(url, options, fetchieOpts); + await pipeline( + // @ts-expect-error odd type error + Readable.fromWeb(res.body!), + createWriteStream(file, { + autoClose: true + }) + ); +} + +const ONE_MINUTE_MS = 1000 * 60; + +export async function fetchie(url: string, options?: RequestInit, { retryOnNetworkError }: FetchieOptions = {}) { + let res: Response | undefined; + + try { + res = await fetch(url, options); + } catch (err) { + if (retryOnNetworkError) { + console.error("Failed to fetch", url + ".", "Gonna retry with backoff."); + + for (let tries = 0, delayMs = 500; tries < 20; tries++, delayMs = Math.min(2 * delayMs, ONE_MINUTE_MS)) { + await setTimeout(delayMs); + try { + res = await fetch(url, options); + break; + } catch {} + } + } + + if (!res) throw new Error(`Failed to fetch ${url}\n${err}`); + } + + if (res.ok) return res; + + let msg = `Got non-OK response for ${url}: ${res.status} ${res.statusText}`; + + const reason = await res.text().catch(() => ""); + if (reason) msg += `\n${reason}`; + + throw new Error(msg); +} diff --git a/src/main/utils/ipcWrappers.ts b/src/main/utils/ipcWrappers.ts new file mode 100644 index 0000000..af0086a --- /dev/null +++ b/src/main/utils/ipcWrappers.ts @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron"; +import { DISCORD_HOSTNAMES } from "main/constants"; +import { IpcEvents } from "shared/IpcEvents"; + +export function validateSender(frame: WebFrameMain | null) { + if (!frame) throw new Error("ipc: No sender frame"); + + const { hostname, protocol } = new URL(frame.url); + if (protocol === "file:") return; + + if (!DISCORD_HOSTNAMES.includes(hostname)) throw new Error("ipc: Disallowed host " + hostname); +} + +export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) { + ipcMain.on(event, (e, ...args) => { + validateSender(e.senderFrame); + e.returnValue = cb(e, ...args); + }); +} + +export function handle(event: IpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) { + ipcMain.handle(event, (e, ...args) => { + validateSender(e.senderFrame); + return cb(e, ...args); + }); +} diff --git a/src/main/utils/makeLinksOpenExternally.ts b/src/main/utils/makeLinksOpenExternally.ts new file mode 100644 index 0000000..ddd7427 --- /dev/null +++ b/src/main/utils/makeLinksOpenExternally.ts @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { BrowserWindow, shell } from "electron"; +import { DISCORD_HOSTNAMES } from "main/constants"; + +import { Settings } from "../settings"; +import { createOrFocusPopup, setupPopout } from "./popout"; +import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS"; + +export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } { + if (protocol == null) { + try { + protocol = new URL(url).protocol; + } catch { + return { action: "deny" }; + } + } + + switch (protocol) { + case "http:": + case "https:": + if (Settings.store.openLinksWithElectron) { + return { action: "allow" }; + } + // eslint-disable-next-line no-fallthrough + case "mailto:": + case "spotify:": + if (isDeckGameMode) { + steamOpenURL(url); + } else { + shell.openExternal(url); + } + break; + case "steam:": + if (isDeckGameMode) { + execSteamURL(url); + } else { + shell.openExternal(url); + } + break; + } + + return { action: "deny" }; +} + +export function makeLinksOpenExternally(win: BrowserWindow) { + win.webContents.setWindowOpenHandler(({ url, frameName, features }) => { + try { + var { protocol, hostname, pathname } = new URL(url); + } catch { + return { action: "deny" }; + } + + if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) { + return createOrFocusPopup(frameName, features); + } + + if (url === "about:blank" || (frameName === "authorize" && DISCORD_HOSTNAMES.includes(hostname))) + return { action: "allow" }; + + return handleExternalUrl(url, protocol); + }); + + win.webContents.on("did-create-window", (win, { frameName }) => { + if (frameName.startsWith("DISCORD_")) setupPopout(win, frameName); + }); +} diff --git a/src/main/utils/popout.ts b/src/main/utils/popout.ts new file mode 100644 index 0000000..feb0dab --- /dev/null +++ b/src/main/utils/popout.ts @@ -0,0 +1,118 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { BrowserWindow, BrowserWindowConstructorOptions } from "electron"; +import { Settings } from "main/settings"; + +import { handleExternalUrl } from "./makeLinksOpenExternally"; + +const ALLOWED_FEATURES = new Set([ + "width", + "height", + "left", + "top", + "resizable", + "movable", + "alwaysOnTop", + "frame", + "transparent", + "hasShadow", + "closable", + "skipTaskbar", + "backgroundColor", + "menubar", + "toolbar", + "location", + "directories", + "titleBarStyle" +]); + +const MIN_POPOUT_WIDTH = 320; +const MIN_POPOUT_HEIGHT = 180; +const DEFAULT_POPOUT_OPTIONS: BrowserWindowConstructorOptions = { + title: "Discord Popout", + backgroundColor: "#2f3136", + minWidth: MIN_POPOUT_WIDTH, + minHeight: MIN_POPOUT_HEIGHT, + frame: Settings.store.customTitleBar !== true, + titleBarStyle: process.platform === "darwin" ? "hidden" : undefined, + trafficLightPosition: + process.platform === "darwin" + ? { + x: 10, + y: 3 + } + : undefined, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + }, + autoHideMenuBar: Settings.store.enableMenu +}; + +export const PopoutWindows = new Map(); + +function focusWindow(window: BrowserWindow) { + window.setAlwaysOnTop(true); + window.focus(); + window.setAlwaysOnTop(false); +} + +function parseFeatureValue(feature: string) { + if (feature === "yes") return true; + if (feature === "no") return false; + + const n = Number(feature); + if (!isNaN(n)) return n; + + return feature; +} + +function parseWindowFeatures(features: string) { + const keyValuesParsed = features.split(","); + + return keyValuesParsed.reduce((features, feature) => { + const [key, value] = feature.split("="); + if (ALLOWED_FEATURES.has(key)) features[key] = parseFeatureValue(value); + + return features; + }, {}); +} + +export function createOrFocusPopup(key: string, features: string) { + const existingWindow = PopoutWindows.get(key); + if (existingWindow) { + focusWindow(existingWindow); + return { action: "deny" }; + } + + return { + action: "allow", + overrideBrowserWindowOptions: { + ...DEFAULT_POPOUT_OPTIONS, + ...parseWindowFeatures(features) + } + }; +} + +export function setupPopout(win: BrowserWindow, key: string) { + win.setMenuBarVisibility(false); + + PopoutWindows.set(key, win); + + /* win.webContents.on("will-navigate", (evt, url) => { + // maybe prevent if not origin match + })*/ + + win.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url)); + + win.once("closed", () => { + win.removeAllListeners(); + PopoutWindows.delete(key); + }); +} diff --git a/src/main/utils/steamOS.ts b/src/main/utils/steamOS.ts new file mode 100644 index 0000000..d2fb430 --- /dev/null +++ b/src/main/utils/steamOS.ts @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { BrowserWindow, dialog } from "electron"; +import { writeFile } from "fs/promises"; +import { join } from "path"; + +import { MessageBoxChoice } from "../constants"; +import { State } from "../settings"; + +// Bump this to re-show the prompt +const layoutVersion = 2; +// Get this from "show details" on the profile after exporting as a shared personal layout or using share with community +const layoutId = "3080264545"; // Vesktop Layout v2 +const numberRegex = /^[0-9]*$/; + +let steamPipeQueue = Promise.resolve(); + +export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1"; + +export function applyDeckKeyboardFix() { + if (!isDeckGameMode) return; + // Prevent constant virtual keyboard spam that eventually crashes Steam. + process.env.GTK_IM_MODULE = "None"; +} + +// For some reason SteamAppId is always 0 for non-steam apps so we do this insanity instead. +function getAppId(): string | null { + // /home/deck/.local/share/Steam/steamapps/shadercache/APPID/fozmediav1 + const path = process.env.STEAM_COMPAT_MEDIA_PATH; + if (!path) return null; + const pathElems = path?.split("/"); + const appId = pathElems[pathElems.length - 2]; + if (appId.match(numberRegex)) { + console.log(`Got Steam App ID ${appId}`); + return appId; + } + return null; +} + +export function execSteamURL(url: string) { + // This doesn't allow arbitrary execution despite the weird syntax. + steamPipeQueue = steamPipeQueue.then(() => + writeFile( + join(process.env.HOME || "/home/deck", ".steam", "steam.pipe"), + // replace ' to prevent argument injection + `'${process.env.HOME}/.local/share/Steam/ubuntu12_32/steam' '-ifrunning' '${url.replaceAll("'", "%27")}'\n`, + "utf-8" + ) + ); +} + +export function steamOpenURL(url: string) { + execSteamURL(`steam://openurl/${url}`); +} + +export async function showGamePage() { + const appId = getAppId(); + if (!appId) return; + await execSteamURL(`steam://nav/games/details/${appId}`); +} + +async function showLayout(appId: string) { + execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`); +} + +export async function askToApplySteamLayout(win: BrowserWindow) { + const appId = getAppId(); + if (!appId) return; + if (State.store.steamOSLayoutVersion === layoutVersion) return; + const update = Boolean(State.store.steamOSLayoutVersion); + + // Touch screen breaks in some menus when native touch mode is enabled on latest SteamOS beta, remove most of the update specific text once that's fixed. + const { response } = await dialog.showMessageBox(win, { + message: `${update ? "Update" : "Apply"} Vesktop Steam Input Layout?`, + detail: `Would you like to ${update ? "Update" : "Apply"} Vesktop's recommended Steam Deck controller settings? +${update ? "Click yes using the touchpad" : "Tap yes"}, then press the X button or tap Apply Layout to confirm.${ + update ? " Doing so will undo any customizations you have made." : "" + } +${update ? "Click" : "Tap"} no to keep your current layout.`, + buttons: ["Yes", "No"], + cancelId: MessageBoxChoice.Cancel, + defaultId: MessageBoxChoice.Default, + type: "question" + }); + + if (State.store.steamOSLayoutVersion !== layoutVersion) { + State.store.steamOSLayoutVersion = layoutVersion; + } + + if (response === MessageBoxChoice.Cancel) return; + + await showLayout(appId); +} diff --git a/src/main/utils/vencordLoader.ts b/src/main/utils/vencordLoader.ts new file mode 100644 index 0000000..644d3a3 --- /dev/null +++ b/src/main/utils/vencordLoader.ts @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { mkdirSync } from "fs"; +import { access, constants as FsConstants } from "fs/promises"; +import { join } from "path"; + +import { USER_AGENT, VENCORD_FILES_DIR } from "../constants"; +import { downloadFile, fetchie } from "./http"; + +const API_BASE = "https://api.github.com"; + +export const FILES_TO_DOWNLOAD = [ + "vencordDesktopMain.js", + "vencordDesktopPreload.js", + "vencordDesktopRenderer.js", + "vencordDesktopRenderer.css" +]; + +export interface ReleaseData { + name: string; + tag_name: string; + html_url: string; + assets: Array<{ + name: string; + browser_download_url: string; + }>; +} + +export async function githubGet(endpoint: string) { + const opts: RequestInit = { + headers: { + Accept: "application/vnd.github+json", + "User-Agent": USER_AGENT + } + }; + + if (process.env.GITHUB_TOKEN) (opts.headers! as any).Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + + return fetchie(API_BASE + endpoint, opts, { retryOnNetworkError: true }); +} + +export async function downloadVencordFiles() { + const release = await githubGet("/repos/Vendicated/Vencord/releases/latest"); + + const { assets }: ReleaseData = await release.json(); + + 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), {}, { retryOnNetworkError: true }) + ) + ); +} + +const existsAsync = (path: string) => + access(path, FsConstants.F_OK) + .then(() => true) + .catch(() => false); + +export async function isValidVencordInstall(dir: string) { + return Promise.all(FILES_TO_DOWNLOAD.map(f => existsAsync(join(dir, f)))).then(arr => !arr.includes(false)); +} + +export async function ensureVencordFiles() { + if (await isValidVencordInstall(VENCORD_FILES_DIR)) return; + + mkdirSync(VENCORD_FILES_DIR, { recursive: true }); + + await downloadVencordFiles(); +} diff --git a/src/main/venmic.ts b/src/main/venmic.ts new file mode 100644 index 0000000..999ea99 --- /dev/null +++ b/src/main/venmic.ts @@ -0,0 +1,137 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic"; +import { app, ipcMain } from "electron"; +import { join } from "path"; +import { IpcEvents } from "shared/IpcEvents"; +import { STATIC_DIR } from "shared/paths"; + +import { Settings } from "./settings"; + +let PatchBay: typeof PatchBayType | undefined; +let patchBayInstance: PatchBayType | undefined; + +let imported = false; +let initialized = false; + +let hasPipewirePulse = false; +let isGlibCxxOutdated = false; + +function importVenmic() { + if (imported) { + return; + } + + imported = true; + + try { + PatchBay = (require(join(STATIC_DIR, `dist/venmic-${process.arch}.node`)) as typeof import("@vencord/venmic")) + .PatchBay; + + hasPipewirePulse = PatchBay.hasPipeWire(); + } catch (e: any) { + console.error("Failed to import venmic", e); + isGlibCxxOutdated = (e?.stack || e?.message || "").toLowerCase().includes("glibc"); + } +} + +function obtainVenmic() { + if (!imported) { + importVenmic(); + } + + if (PatchBay && !initialized) { + initialized = true; + + try { + patchBayInstance = new PatchBay(); + } catch (e: any) { + console.error("Failed to instantiate venmic", e); + } + } + + return patchBayInstance; +} + +function getRendererAudioServicePid() { + return ( + app + .getAppMetrics() + .find(proc => proc.name === "Audio Service") + ?.pid?.toString() ?? "owo" + ); +} + +ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => { + const audioPid = getRendererAudioServicePid(); + + const { granularSelect } = Settings.store.audio ?? {}; + + const targets = obtainVenmic() + ?.list(granularSelect ? ["node.name"] : undefined) + .filter(s => s["application.process.id"] !== audioPid); + + return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated }; +}); + +ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => { + const pid = getRendererAudioServicePid(); + const { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {}; + + const data: LinkData = { + include, + exclude: [{ "application.process.id": pid }], + ignore_devices: ignoreDevices + }; + + if (ignoreInputMedia ?? true) { + data.exclude.push({ "media.class": "Stream/Input/Audio" }); + } + + if (ignoreVirtual) { + data.exclude.push({ "node.virtual": "true" }); + } + + if (workaround) { + data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }]; + } + + return obtainVenmic()?.link(data); +}); + +ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, exclude: Node[]) => { + const pid = getRendererAudioServicePid(); + + const { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } = + Settings.store.audio ?? {}; + + const data: LinkData = { + include: [], + exclude: [{ "application.process.id": pid }, ...exclude], + only_speakers: onlySpeakers, + ignore_devices: ignoreDevices, + only_default_speakers: onlyDefaultSpeakers + }; + + if (ignoreInputMedia ?? true) { + data.exclude.push({ "media.class": "Stream/Input/Audio" }); + } + + if (ignoreVirtual) { + data.exclude.push({ "node.virtual": "true" }); + } + + if (workaround) { + data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }]; + } + + return obtainVenmic()?.link(data); +}); + +ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink()); diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts new file mode 100644 index 0000000..050327d --- /dev/null +++ b/src/preload/VesktopNative.ts @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Node } from "@vencord/venmic"; +import { ipcRenderer } from "electron"; +import type { Settings } from "shared/settings"; + +import { IpcEvents } from "../shared/IpcEvents"; +import { invoke, sendSync } from "./typedIpc"; + +type SpellCheckerResultCallback = (word: string, suggestions: string[]) => void; + +const spellCheckCallbacks = new Set(); + +ipcRenderer.on(IpcEvents.SPELLCHECK_RESULT, (_, w: string, s: string[]) => { + spellCheckCallbacks.forEach(cb => cb(w, s)); +}); + +export const VesktopNative = { + app: { + relaunch: () => invoke(IpcEvents.RELAUNCH), + getVersion: () => sendSync(IpcEvents.GET_VERSION), + setBadgeCount: (count: number) => invoke(IpcEvents.SET_BADGE_COUNT, count), + supportsWindowsTransparency: () => sendSync(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY) + }, + autostart: { + isEnabled: () => sendSync(IpcEvents.AUTOSTART_ENABLED), + enable: () => invoke(IpcEvents.ENABLE_AUTOSTART), + disable: () => invoke(IpcEvents.DISABLE_AUTOSTART) + }, + fileManager: { + showItemInFolder: (path: string) => invoke(IpcEvents.SHOW_ITEM_IN_FOLDER, path), + getVencordDir: () => sendSync(IpcEvents.GET_VENCORD_DIR), + selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value) + }, + settings: { + get: () => sendSync(IpcEvents.GET_SETTINGS), + set: (settings: Settings, path?: string) => invoke(IpcEvents.SET_SETTINGS, settings, path) + }, + spellcheck: { + getAvailableLanguages: () => sendSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES), + onSpellcheckResult(cb: SpellCheckerResultCallback) { + spellCheckCallbacks.add(cb); + }, + offSpellcheckResult(cb: SpellCheckerResultCallback) { + spellCheckCallbacks.delete(cb); + }, + replaceMisspelling: (word: string) => invoke(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, word), + addToDictionary: (word: string) => invoke(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, word) + }, + win: { + focus: () => invoke(IpcEvents.FOCUS), + close: (key?: string) => invoke(IpcEvents.CLOSE, key), + minimize: () => invoke(IpcEvents.MINIMIZE), + maximize: () => invoke(IpcEvents.MAXIMIZE) + }, + capturer: { + getLargeThumbnail: (id: string) => invoke(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id) + }, + /** only available on Linux. */ + virtmic: { + list: () => + invoke< + { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean } + >(IpcEvents.VIRT_MIC_LIST), + start: (include: Node[]) => invoke(IpcEvents.VIRT_MIC_START, include), + startSystem: (exclude: Node[]) => invoke(IpcEvents.VIRT_MIC_START_SYSTEM, exclude), + stop: () => invoke(IpcEvents.VIRT_MIC_STOP) + }, + arrpc: { + onActivity(cb: (data: string) => void) { + ipcRenderer.on(IpcEvents.ARRPC_ACTIVITY, (_, data: string) => cb(data)); + } + }, + clipboard: { + copyImage: (imageBuffer: Uint8Array, imageSrc: string) => + invoke(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc) + } +}; diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 0000000..97ed3ba --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { contextBridge, ipcRenderer, webFrame } from "electron"; +import { readFileSync, watch } from "fs"; + +import { IpcEvents } from "../shared/IpcEvents"; +import { VesktopNative } from "./VesktopNative"; + + contextBridge.exposeInMainWorld("VesktopNative", VesktopNative); + + require(ipcRenderer.sendSync(IpcEvents.GET_VENCORD_PRELOAD_FILE)); + +webFrame.executeJavaScript(ipcRenderer.sendSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT)); +webFrame.executeJavaScript(ipcRenderer.sendSync(IpcEvents.GET_RENDERER_SCRIPT)); + + +// #region css +const rendererCss = ipcRenderer.sendSync(IpcEvents.GET_RENDERER_CSS_FILE); + +const style = document.createElement("style"); +style.id = "vcd-css-core"; +style.textContent = readFileSync(rendererCss, "utf-8"); + +if (document.readyState === "complete") { + document.documentElement.appendChild(style); +} else { + document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(style), { + once: true + }); +} + +if (IS_DEV) { + // persistent means keep process running if watcher is the only thing still running + // which we obviously don't want + watch(rendererCss, { persistent: false }, () => { + document.getElementById("vcd-css-core")!.textContent = readFileSync(rendererCss, "utf-8"); + }); +} +// #endregion + + diff --git a/src/preload/typedIpc.ts b/src/preload/typedIpc.ts new file mode 100644 index 0000000..08b8c3b --- /dev/null +++ b/src/preload/typedIpc.ts @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { ipcRenderer } from "electron"; +import { IpcEvents } from "shared/IpcEvents"; + +export function invoke(event: IpcEvents, ...args: any[]) { + return ipcRenderer.invoke(event, ...args) as Promise; +} + +export function sendSync(event: IpcEvents, ...args: any[]) { + return ipcRenderer.sendSync(event, ...args) as T; +} diff --git a/src/renderer/appBadge.ts b/src/renderer/appBadge.ts new file mode 100644 index 0000000..214023f --- /dev/null +++ b/src/renderer/appBadge.ts @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { filters, waitFor } from "@vencord/types/webpack"; +import { RelationshipStore } from "@vencord/types/webpack/common"; + +import { Settings } from "./settings"; + +let GuildReadStateStore: any; +let NotificationSettingsStore: any; + +export function setBadge() { + if (Settings.store.appBadge === false) return; + + try { + const mentionCount = GuildReadStateStore.getTotalMentionCount(); + const pendingRequests = RelationshipStore.getPendingCount(); + const hasUnread = GuildReadStateStore.hasAnyUnread(); + const disableUnreadBadge = NotificationSettingsStore.getDisableUnreadBadge(); + + let totalCount = mentionCount + pendingRequests; + if (!totalCount && hasUnread && !disableUnreadBadge) totalCount = -1; + + VesktopNative.app.setBadgeCount(totalCount); + } catch (e) { + console.error(e); + } +} + +let toFind = 3; + +function waitForAndSubscribeToStore(name: string, cb?: (m: any) => void) { + waitFor(filters.byStoreName(name), store => { + cb?.(store); + store.addChangeListener(setBadge); + + toFind--; + if (toFind === 0) setBadge(); + }); +} + +waitForAndSubscribeToStore("GuildReadStateStore", store => (GuildReadStateStore = store)); +waitForAndSubscribeToStore("NotificationSettingsStore", store => (NotificationSettingsStore = store)); +waitForAndSubscribeToStore("RelationshipStore"); diff --git a/src/renderer/components/ScreenSharePicker.tsx b/src/renderer/components/ScreenSharePicker.tsx new file mode 100644 index 0000000..34c48a8 --- /dev/null +++ b/src/renderer/components/ScreenSharePicker.tsx @@ -0,0 +1,806 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import "./screenSharePicker.css"; + +import { closeModal, Logger, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils"; +import { findStoreLazy, onceReady } from "@vencord/types/webpack"; +import { + Button, + Card, + FluxDispatcher, + Forms, + Select, + Switch, + Text, + UserStore, + useState +} from "@vencord/types/webpack/common"; +import { Node } from "@vencord/venmic"; +import type { Dispatch, SetStateAction } from "react"; +import { addPatch } from "renderer/patches/shared"; +import { useSettings } from "renderer/settings"; +import { isLinux, isWindows } from "renderer/utils"; + +const StreamResolutions = ["480", "720", "1080", "1440"] as const; +const StreamFps = ["15", "30", "60"] as const; + +const MediaEngineStore = findStoreLazy("MediaEngineStore"); + +export type StreamResolution = (typeof StreamResolutions)[number]; +export type StreamFps = (typeof StreamFps)[number]; + +type SpecialSource = "None" | "Entire System"; + +type AudioSource = SpecialSource | Node; +type AudioSources = SpecialSource | Node[]; + +interface AudioItem { + name: string; + value: AudioSource; +} + +interface StreamSettings { + resolution: StreamResolution; + fps: StreamFps; + audio: boolean; + contentHint?: string; + includeSources?: AudioSources; + excludeSources?: AudioSources; +} + +export interface StreamPick extends StreamSettings { + id: string; +} + +interface Source { + id: string; + name: string; + url: string; +} + +export let currentSettings: StreamSettings | null = null; + +const logger = new Logger("VesktopScreenShare"); + +addPatch({ + patches: [ + { + find: "this.localWant=", + replacement: { + match: /this.localWant=/, + replace: "$self.patchStreamQuality(this);$&" + } + } + ], + patchStreamQuality(opts: any) { + if (!currentSettings) return; + + const framerate = Number(currentSettings.fps); + const height = Number(currentSettings.resolution); + const width = Math.round(height * (16 / 9)); + + Object.assign(opts, { + bitrateMin: 500000, + bitrateMax: 8000000, + bitrateTarget: 600000 + }); + if (opts?.encode) { + Object.assign(opts.encode, { + framerate, + width, + height, + pixelCount: height * width + }); + } + Object.assign(opts.capture, { + framerate, + width, + height, + pixelCount: height * width + }); + } +}); + +if (isLinux) { + onceReady.then(() => { + FluxDispatcher.subscribe("STREAM_CLOSE", ({ streamKey }: { streamKey: string }) => { + const owner = streamKey.split(":").at(-1); + + if (owner !== UserStore.getCurrentUser().id) { + return; + } + + VesktopNative.virtmic.stop(); + }); + }); +} + +export function openScreenSharePicker(screens: Source[], skipPicker: boolean) { + let didSubmit = false; + return new Promise((resolve, reject) => { + const key = openModal( + props => ( + { + didSubmit = true; + + if (v.includeSources && v.includeSources !== "None") { + if (v.includeSources === "Entire System") { + await VesktopNative.virtmic.startSystem( + !v.excludeSources || isSpecialSource(v.excludeSources) ? [] : v.excludeSources + ); + } else { + await VesktopNative.virtmic.start(v.includeSources); + } + } + + resolve(v); + }} + close={() => { + props.onClose(); + if (!didSubmit) reject("Aborted"); + }} + skipPicker={skipPicker} + /> + ), + { + onCloseRequest() { + closeModal(key); + reject("Aborted"); + } + } + ); + }); +} + +function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) { + return ( +
+ {screens.map(({ id, name, url }) => ( + + ))} +
+ ); +} + +function AudioSettingsModal({ + modalProps, + close, + setAudioSources +}: { + modalProps: any; + close: () => void; + setAudioSources: (s: AudioSources) => void; +}) { + const Settings = useSettings(); + + return ( + + + Venmic Settings + + + + (Settings.audio = { ...Settings.audio, workaround: v })} + value={Settings.audio?.workaround ?? false} + note={ + <> + Work around an issue that causes the microphone to be shared instead of the correct audio. + Only enable if you're experiencing this issue. + + } + > + Microphone Workaround + + (Settings.audio = { ...Settings.audio, onlySpeakers: v })} + value={Settings.audio?.onlySpeakers ?? true} + note={ + <> + When sharing entire desktop audio, only share apps that play to a speaker. You may want to + disable this when using "mix bussing". + + } + > + Only Speakers + + (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })} + value={Settings.audio?.onlyDefaultSpeakers ?? true} + note={ + <> + When sharing entire desktop audio, only share apps that play to the default speakers. + You may want to disable this when using "mix bussing". + + } + > + Only Default Speakers + + (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })} + value={Settings.audio?.ignoreInputMedia ?? true} + note={<>Exclude nodes that are intended to capture audio.} + > + Ignore Inputs + + (Settings.audio = { ...Settings.audio, ignoreVirtual: v })} + value={Settings.audio?.ignoreVirtual ?? false} + note={ + <> + Exclude virtual nodes, such as nodes belonging to loopbacks. This might be useful when using + "mix bussing". + + } + > + Ignore Virtual + + + (Settings.audio = { + ...Settings.audio, + ignoreDevices: v, + deviceSelect: v ? false : Settings.audio?.deviceSelect + }) + } + value={Settings.audio?.ignoreDevices ?? true} + note={<>Exclude device nodes, such as nodes belonging to microphones or speakers.} + > + Ignore Devices + + { + Settings.audio = { ...Settings.audio, granularSelect: value }; + setAudioSources("None"); + }} + value={Settings.audio?.granularSelect ?? false} + note={<>Allow to select applications more granularly.} + > + Granular Selection + + { + Settings.audio = { ...Settings.audio, deviceSelect: value }; + setAudioSources("None"); + }} + value={Settings.audio?.deviceSelect ?? false} + disabled={Settings.audio?.ignoreDevices} + note={ + <> + Allow to select devices such as microphones. Requires Ignore Devices to be turned + off. + + } + > + Device Selection + + + + + + + ); +} + +function StreamSettings({ + source, + settings, + setSettings, + skipPicker +}: { + source: Source; + settings: StreamSettings; + setSettings: Dispatch>; + skipPicker: boolean; +}) { + const Settings = useSettings(); + + const [thumb] = useAwaiter( + () => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)), + { + fallbackValue: source.url, + deps: [source.id] + } + ); + + const openSettings = () => { + const key = openModal(props => ( + props.onClose()} + setAudioSources={sources => + setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources })) + } + /> + )); + }; + + return ( +
+ What you're streaming + + + {source.name} + + + Stream Settings + + +
+
+ Resolution +
+ {StreamResolutions.map(res => ( + + ))} +
+
+ +
+ Frame Rate +
+ {StreamFps.map(fps => ( + + ))} +
+
+
+
+
+ Content Type +
+
+ + +
+
+

+ Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange + for a much sharper and clearer image. +

+
+
+ {isWindows && ( + setSettings(s => ({ ...s, audio: checked }))} + hideBorder + className="vcd-screen-picker-audio" + > + Stream With Audio + + )} +
+
+ + {isLinux && ( + setSettings(s => ({ ...s, includeSources: sources }))} + setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))} + /> + )} +
+
+ ); +} + +function isSpecialSource(value?: AudioSource | AudioSources): value is SpecialSource { + return typeof value === "string"; +} + +function hasMatchingProps(value: Node, other: Node) { + return Object.keys(value).every(key => value[key] === other[key]); +} + +function mapToAudioItem(node: AudioSource, granularSelect?: boolean, deviceSelect?: boolean): AudioItem[] { + if (isSpecialSource(node)) { + return [{ name: node, value: node }]; + } + + const rtn: AudioItem[] = []; + + const mediaClass = node["media.class"]; + + if (mediaClass?.includes("Video") || mediaClass?.includes("Midi")) { + return rtn; + } + + if (!deviceSelect && node["device.id"]) { + return rtn; + } + + const name = node["application.name"]; + + if (name) { + rtn.push({ name: name, value: { "application.name": name } }); + } + + if (!granularSelect) { + return rtn; + } + + const rawName = node["node.name"]; + + if (!name) { + rtn.push({ name: rawName, value: { "node.name": rawName } }); + } + + const binary = node["application.process.binary"]; + + if (!name && binary) { + rtn.push({ name: binary, value: { "application.process.binary": binary } }); + } + + const pid = node["application.process.id"]; + + const first = rtn[0]; + const firstValues = first.value as Node; + + if (pid) { + rtn.push({ + name: `${first.name} (${pid})`, + value: { ...firstValues, "application.process.id": pid } + }); + } + + const mediaName = node["media.name"]; + + if (mediaName) { + rtn.push({ + name: `${first.name} [${mediaName}]`, + value: { ...firstValues, "media.name": mediaName } + }); + } + + if (mediaClass) { + rtn.push({ + name: `${first.name} [${mediaClass}]`, + value: { ...firstValues, "media.class": mediaClass } + }); + } + + return rtn; +} + +function isItemSelected(sources?: AudioSources) { + return (value: AudioSource) => { + if (!sources) { + return false; + } + + if (isSpecialSource(sources) || isSpecialSource(value)) { + return sources === value; + } + + return sources.some(source => hasMatchingProps(source, value)); + }; +} + +function updateItems(setSources: (s: AudioSources) => void, sources?: AudioSources) { + return (value: AudioSource) => { + if (isSpecialSource(value)) { + setSources(value); + return; + } + + if (isSpecialSource(sources)) { + setSources([value]); + return; + } + + if (isItemSelected(sources)(value)) { + setSources(sources?.filter(x => !hasMatchingProps(x, value)) ?? "None"); + return; + } + + setSources([...(sources || []), value]); + }; +} + +function AudioSourcePickerLinux({ + includeSources, + excludeSources, + deviceSelect, + granularSelect, + openSettings, + setIncludeSources, + setExcludeSources +}: { + includeSources?: AudioSources; + excludeSources?: AudioSources; + deviceSelect?: boolean; + granularSelect?: boolean; + openSettings: () => void; + setIncludeSources: (s: AudioSources) => void; + setExcludeSources: (s: AudioSources) => void; +}) { + const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { + fallbackValue: { ok: true, targets: [], hasPipewirePulse: true } + }); + + const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true; + const [ignorePulseWarning, setIgnorePulseWarning] = useState(false); + + if (!sources.ok && sources.isGlibCxxOutdated) { + return ( + + Failed to retrieve Audio Sources because your C++ library is too old to run + + venmic + + . See{" "} + + this guide + {" "} + for possible solutions. + + ); + } + + if (!hasPipewirePulse && !ignorePulseWarning) { + return ( + + Could not find pipewire-pulse. See{" "} + + this guide + {" "} + on how to switch to pipewire.
+ You can still continue, however, please{" "} + beware that you can only share audio of apps that are running under pipewire.{" "} + setIgnorePulseWarning(true)}>I know what I'm doing! +
+ ); + } + + const specialSources: SpecialSource[] = ["None", "Entire System"] as const; + + const uniqueName = (value: AudioItem, index: number, list: AudioItem[]) => + list.findIndex(x => x.name === value.name) === index; + + const allSources = sources.ok + ? [...specialSources, ...sources.targets] + .map(target => mapToAudioItem(target, granularSelect, deviceSelect)) + .flat() + .filter(uniqueName) + : []; + + return ( + <> +
+
+ {loading ? "Loading Sources..." : "Audio Sources"} + x.name !== "Entire System") + .map(({ name, value }) => ({ + label: name, + value: value, + default: name === "None" + }))} + isSelected={isItemSelected(excludeSources)} + select={updateItems(setExcludeSources, excludeSources)} + serialize={String} + popoutPosition="top" + closeOnSelect={false} + /> +
+ )} +
+ + + ); +} + +function ModalComponent({ + screens, + modalProps, + submit, + close, + skipPicker +}: { + screens: Source[]; + modalProps: any; + submit: (data: StreamPick) => void; + close: () => void; + skipPicker: boolean; +}) { + const [selected, setSelected] = useState(skipPicker ? screens[0].id : void 0); + const [settings, setSettings] = useState({ + resolution: "720", + fps: "30", + contentHint: "motion", + audio: true, + includeSources: "None" + }); + + return ( + + + ScreenShare + + + + {!selected ? ( + + ) : ( + s.id === selected)!} + settings={settings} + setSettings={setSettings} + skipPicker={skipPicker} + /> + )} + + + + + {selected && !skipPicker ? ( + + ) : ( + + )} + + + ); +} diff --git a/src/renderer/components/index.ts b/src/renderer/components/index.ts new file mode 100644 index 0000000..b0101c7 --- /dev/null +++ b/src/renderer/components/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +export * as ScreenShare from "./ScreenSharePicker"; diff --git a/src/renderer/components/screenSharePicker.css b/src/renderer/components/screenSharePicker.css new file mode 100644 index 0000000..e3dc7d2 --- /dev/null +++ b/src/renderer/components/screenSharePicker.css @@ -0,0 +1,145 @@ +.vcd-screen-picker-modal { + padding: 1em; +} + +.vcd-screen-picker-header h1 { + margin: 0; +} + +.vcd-screen-picker-footer { + display: flex; + gap: 1em; +} + +.vcd-screen-picker-card { + flex-grow: 1; +} + +.vcd-screen-picker-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2em 1em; +} + +.vcd-screen-picker-grid input { + appearance: none; + cursor: pointer; +} + +.vcd-screen-picker-selected img { + border: 2px solid var(--brand-500); + border-radius: 3px; +} + +.vcd-screen-picker-grid label { + overflow: hidden; + padding: 8px; + cursor: pointer; + display: grid; + justify-items: center; +} + +.vcd-screen-picker-grid label:hover { + outline: 2px solid var(--brand-500); +} + +.vcd-screen-picker-grid div { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + text-align: center; + font-weight: 600; + margin-inline: 0.5em; +} + +.vcd-screen-picker-card { + padding: 0.5em; + box-sizing: border-box; +} + +.vcd-screen-picker-preview-img-linux { + width: 60%; + margin-bottom: 0.5em; +} + +.vcd-screen-picker-preview-img { + width: 90%; + margin-bottom: 0.5em; +} + +.vcd-screen-picker-preview { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-bottom: 1em; +} + +.vcd-screen-picker-radio input { + display: none; +} + +.vcd-screen-picker-radio { + background-color: var(--background-secondary); + border: 1px solid var(--primary-800); + padding: 0.3em; + cursor: pointer; +} + +.vcd-screen-picker-radio h2 { + margin: 0; +} + +.vcd-screen-picker-radio[data-checked="true"] { + background-color: var(--brand-500); + border-color: var(--brand-500); +} + +.vcd-screen-picker-radio[data-checked="true"] h2 { + color: var(--interactive-active); +} + +.vcd-screen-picker-quality { + display: flex; + gap: 1em; + margin-bottom: 0.5em; +} + +.vcd-screen-picker-quality section { + flex: 1 1 auto; +} + +.vcd-screen-picker-settings-button { + margin-left: auto; + margin-top: 0.3rem; +} + +.vcd-screen-picker-radios { + display: flex; + width: 100%; + border-radius: 3px; +} + +.vcd-screen-picker-radios label { + flex: 1 1 auto; + text-align: center; +} + +.vcd-screen-picker-radios label:first-child { + border-radius: 3px 0 0 3px; +} + +.vcd-screen-picker-radios label:last-child { + border-radius: 0 3px 3px 0; +} + +.vcd-screen-picker-audio { + margin-bottom: 0; +} + +.vcd-screen-picker-hint-description { + color: var(--header-secondary); + font-size: 14px; + line-height: 20px; + font-weight: 400; +} diff --git a/src/renderer/components/settings/AutoStartToggle.tsx b/src/renderer/components/settings/AutoStartToggle.tsx new file mode 100644 index 0000000..a37fd28 --- /dev/null +++ b/src/renderer/components/settings/AutoStartToggle.tsx @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Switch, useState } from "@vencord/types/webpack/common"; + +import { SettingsComponent } from "./Settings"; + +export const AutoStartToggle: SettingsComponent = () => { + const [autoStartEnabled, setAutoStartEnabled] = useState(VesktopNative.autostart.isEnabled()); + + return ( + { + await VesktopNative.autostart[v ? "enable" : "disable"](); + setAutoStartEnabled(v); + }} + note="Automatically start Vesktop on computer start-up" + > + Start With System + + ); +}; diff --git a/src/renderer/components/settings/DiscordBranchPicker.tsx b/src/renderer/components/settings/DiscordBranchPicker.tsx new file mode 100644 index 0000000..87e0a22 --- /dev/null +++ b/src/renderer/components/settings/DiscordBranchPicker.tsx @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Select } from "@vencord/types/webpack/common"; + +import { SettingsComponent } from "./Settings"; + +export const DiscordBranchPicker: SettingsComponent = ({ settings }) => { + return ( + (settings.transparencyOption = v)} + isSelected={v => v === settings.transparencyOption} + serialize={s => s} + /> + + + + ); +}; diff --git a/src/renderer/components/settings/settings.css b/src/renderer/components/settings/settings.css new file mode 100644 index 0000000..d55ff50 --- /dev/null +++ b/src/renderer/components/settings/settings.css @@ -0,0 +1,14 @@ +.vcd-location-btns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5em; + margin-top: 0.5em; +} + +.vcd-settings-section { + margin-top: 1.5rem; +} + +.vcd-settings-title { + margin-bottom: 0.5rem; +} \ No newline at end of file diff --git a/src/renderer/fixes.css b/src/renderer/fixes.css new file mode 100644 index 0000000..20e80aa --- /dev/null +++ b/src/renderer/fixes.css @@ -0,0 +1,16 @@ +/* Download Desktop button in guilds list */ +[class^=listItem_]:has([data-list-item-id=guildsnav___app-download-button]), +[class^=listItem_]:has(+ [class^=listItem_] [data-list-item-id=guildsnav___app-download-button]) { + display: none; +} + +/* FIXME: remove this once Discord fixes their css to not explode scrollbars on chromium >=121 */ +* { + scrollbar-width: unset !important; + scrollbar-color: unset !important; +} + +/* Workaround for making things in the draggable area clickable again on macOS */ +.platform-osx [class*=topic_], .platform-osx [class*=notice_] button { + -webkit-app-region: no-drag; +} diff --git a/src/renderer/fixes.ts b/src/renderer/fixes.ts new file mode 100644 index 0000000..8abdac4 --- /dev/null +++ b/src/renderer/fixes.ts @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import "./fixes.css"; + +import { isWindows, localStorage } from "./utils"; + +// Make clicking Notifications focus the window +const originalSetOnClick = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick")!.set!; +Object.defineProperty(Notification.prototype, "onclick", { + set(onClick) { + originalSetOnClick.call(this, function (this: unknown) { + onClick.apply(this, arguments); + VesktopNative.win.focus(); + }); + }, + configurable: true +}); + +// Hide "Download Discord Desktop now!!!!" banner +localStorage.setItem("hideNag", "true"); + +// FIXME: Remove eventually. +// Originally, Vencord always used a Windows user agent. This seems to cause captchas +// Now, we use a platform specific UA - HOWEVER, discord FOR SOME REASON????? caches +// device props in localStorage. This code fixes their cache to properly update the platform in SuperProps +if (!isWindows) + try { + const deviceProperties = localStorage.getItem("deviceProperties"); + if (deviceProperties && JSON.parse(deviceProperties).os === "Windows") + localStorage.removeItem("deviceProperties"); + } catch {} diff --git a/src/renderer/index.ts b/src/renderer/index.ts new file mode 100644 index 0000000..6ab5144 --- /dev/null +++ b/src/renderer/index.ts @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import "./fixes"; +import "./appBadge"; +import "./patches"; +import "./themedSplash"; + +console.log("read if cute :3"); + +export * as Components from "./components"; +import { findByPropsLazy, onceReady } from "@vencord/types/webpack"; +import { Alerts, FluxDispatcher } from "@vencord/types/webpack/common"; + +import SettingsUi from "./components/settings/Settings"; +import { Settings } from "./settings"; +export { Settings }; + +const InviteActions = findByPropsLazy("resolveInvite"); + +export async function openInviteModal(code: string) { + const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal"); + if (!invite) return false; + + VesktopNative.win.focus(); + + FluxDispatcher.dispatch({ + type: "INVITE_MODAL_OPEN", + invite, + code, + context: "APP" + }); + + return true; +} + +const customSettingsSections = ( + Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record) => any)[] } +).customSections; + +customSettingsSections.push(() => ({ + section: "Vesktop", + label: "Vesktop Settings", + element: SettingsUi, + className: "vc-vesktop-settings" +})); + +const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as any as { + handleEvent(e: MessageEvent): void; +}; + +VesktopNative.arrpc.onActivity(async data => { + if (!Settings.store.arRPC) return; + + await onceReady; + + arRPC.handleEvent(new MessageEvent("message", { data })); +}); + +// TODO: remove soon +const vencordDir = "vencordDir" as keyof typeof Settings.store; +if (Settings.store[vencordDir]) { + onceReady.then(() => + setTimeout( + () => + Alerts.show({ + title: "Custom Vencord Location", + body: "Due to security hardening changes in Vesktop, your custom Vencord location had to be reset. Please configure it again in the settings.", + onConfirm: () => delete Settings.store[vencordDir] + }), + 5000 + ) + ); +} diff --git a/src/renderer/patches/enableNotificationsByDefault.ts b/src/renderer/patches/enableNotificationsByDefault.ts new file mode 100644 index 0000000..ca27157 --- /dev/null +++ b/src/renderer/patches/enableNotificationsByDefault.ts @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { addPatch } from "./shared"; + +addPatch({ + patches: [ + { + find: '"NotificationSettingsStore', + replacement: { + // FIXME: fix eslint rule + // eslint-disable-next-line no-useless-escape + match: /\.isPlatformEmbedded(?=\?\i\.\i\.ALL)/g, + replace: "$&||true" + } + } + ] +}); diff --git a/src/renderer/patches/hideSwitchDevice.tsx b/src/renderer/patches/hideSwitchDevice.tsx new file mode 100644 index 0000000..1aeb242 --- /dev/null +++ b/src/renderer/patches/hideSwitchDevice.tsx @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { addPatch } from "./shared"; + +addPatch({ + patches: [ + { + find: "lastOutputSystemDevice.justChanged", + replacement: { + // eslint-disable-next-line no-useless-escape + match: /(\i)\.\i\.getState\(\).neverShowModal/, + replace: "$& || $self.shouldIgnore($1)" + } + } + ], + + shouldIgnore(state: any) { + return Object.keys(state?.default?.lastDeviceConnected ?? {})?.[0] === "vencord-screen-share"; + } +}); diff --git a/src/renderer/patches/hideVenmicInput.tsx b/src/renderer/patches/hideVenmicInput.tsx new file mode 100644 index 0000000..08af99e --- /dev/null +++ b/src/renderer/patches/hideVenmicInput.tsx @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { addPatch } from "./shared"; + +addPatch({ + patches: [ + { + find: 'setSinkId"in', + replacement: { + // eslint-disable-next-line no-useless-escape + match: /return (\i)\?navigator\.mediaDevices\.enumerateDevices/, + replace: "return $1 ? $self.filteredDevices" + } + } + ], + + async filteredDevices() { + const original = await navigator.mediaDevices.enumerateDevices(); + return original.filter(x => x.label !== "vencord-screen-share"); + } +}); diff --git a/src/renderer/patches/index.ts b/src/renderer/patches/index.ts new file mode 100644 index 0000000..1232851 --- /dev/null +++ b/src/renderer/patches/index.ts @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +// TODO: Possibly auto generate glob if we have more patches in the future +import "./enableNotificationsByDefault"; +import "./platformClass"; +import "./hideSwitchDevice"; +import "./hideVenmicInput"; +import "./screenShareFixes"; +import "./spellCheck"; +import "./windowsTitleBar"; diff --git a/src/renderer/patches/platformClass.tsx b/src/renderer/patches/platformClass.tsx new file mode 100644 index 0000000..8bd69f9 --- /dev/null +++ b/src/renderer/patches/platformClass.tsx @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Settings } from "renderer/settings"; +import { isMac } from "renderer/utils"; + +import { addPatch } from "./shared"; + +addPatch({ + patches: [ + { + find: "platform-web", + replacement: { + // eslint-disable-next-line no-useless-escape + match: /(?<=" platform-overlay"\):)\i/, + replace: "$self.getPlatformClass()" + } + } + ], + + getPlatformClass() { + if (Settings.store.customTitleBar) return "platform-win"; + if (isMac) return "platform-osx"; + return "platform-web"; + } +}); diff --git a/src/renderer/patches/screenShareFixes.ts b/src/renderer/patches/screenShareFixes.ts new file mode 100644 index 0000000..44fef79 --- /dev/null +++ b/src/renderer/patches/screenShareFixes.ts @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Logger } from "@vencord/types/utils"; +import { currentSettings } from "renderer/components/ScreenSharePicker"; +import { isLinux } from "renderer/utils"; + +const logger = new Logger("VesktopStreamFixes"); + +if (isLinux) { + const original = navigator.mediaDevices.getDisplayMedia; + + async function getVirtmic() { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const audioDevice = devices.find(({ label }) => label === "vencord-screen-share"); + return audioDevice?.deviceId; + } catch (error) { + return null; + } + } + + navigator.mediaDevices.getDisplayMedia = async function (opts) { + const stream = await original.call(this, opts); + const id = await getVirtmic(); + + const frameRate = Number(currentSettings?.fps); + const height = Number(currentSettings?.resolution); + const width = Math.round(height * (16 / 9)); + const track = stream.getVideoTracks()[0]; + + track.contentHint = String(currentSettings?.contentHint); + + const constraints = { + ...track.getConstraints(), + frameRate: { min: frameRate, ideal: frameRate }, + width: { min: 640, ideal: width, max: width }, + height: { min: 480, ideal: height, max: height }, + advanced: [{ width: width, height: height }], + resizeMode: "none" + }; + + track + .applyConstraints(constraints) + .then(() => { + logger.info("Applied constraints successfully. New constraints: ", track.getConstraints()); + }) + .catch(e => logger.error("Failed to apply constraints.", e)); + + if (id) { + const audio = await navigator.mediaDevices.getUserMedia({ + audio: { + deviceId: { + exact: id + }, + autoGainControl: false, + echoCancellation: false, + noiseSuppression: false + } + }); + audio.getAudioTracks().forEach(t => stream.addTrack(t)); + } + + return stream; + }; +} diff --git a/src/renderer/patches/shared.ts b/src/renderer/patches/shared.ts new file mode 100644 index 0000000..974fb11 --- /dev/null +++ b/src/renderer/patches/shared.ts @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Patch } from "@vencord/types/utils/types"; + +window.VCDP = {}; + +interface PatchData { + patches: Omit[]; + [key: string]: any; +} + +export function addPatch

(p: P) { + const { patches, ...globals } = p; + + for (const patch of patches as Patch[]) { + if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement]; + for (const r of patch.replacement) { + if (typeof r.replace === "string") r.replace = r.replace.replaceAll("$self", "VCDP"); + } + + patch.plugin = "Vesktop"; + Vencord.Plugins.patches.push(patch); + } + + Object.assign(VCDP, globals); +} diff --git a/src/renderer/patches/spellCheck.tsx b/src/renderer/patches/spellCheck.tsx new file mode 100644 index 0000000..cff8835 --- /dev/null +++ b/src/renderer/patches/spellCheck.tsx @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { addContextMenuPatch } from "@vencord/types/api/ContextMenu"; +import { findStoreLazy } from "@vencord/types/webpack"; +import { FluxDispatcher, Menu, useMemo, useStateFromStores } from "@vencord/types/webpack/common"; +import { useSettings } from "renderer/settings"; + +import { addPatch } from "./shared"; + +let word: string; +let corrections: string[]; + +const SpellCheckStore = findStoreLazy("SpellcheckStore"); + +// Make spellcheck suggestions work +addPatch({ + patches: [ + { + find: ".enableSpellCheck)", + replacement: { + // if (isDesktop) { DiscordNative.onSpellcheck(openMenu(props)) } else { e.preventDefault(); openMenu(props) } + match: /else (.{1,3})\.preventDefault\(\),(.{1,3}\(.{1,3}\))(?<=:(.{1,3})\.enableSpellCheck\).+?)/, + // ... else { $self.onSlateContext(() => openMenu(props)) } + replace: "else {$self.onSlateContext($1, $3?.enableSpellCheck, () => $2)}" + } + } + ], + + onSlateContext(e: MouseEvent, hasSpellcheck: boolean | undefined, openMenu: () => void) { + if (!hasSpellcheck) { + e.preventDefault(); + openMenu(); + return; + } + + const cb = (w: string, c: string[]) => { + VesktopNative.spellcheck.offSpellcheckResult(cb); + word = w; + corrections = c; + openMenu(); + }; + VesktopNative.spellcheck.onSpellcheckResult(cb); + } +}); + +addContextMenuPatch("textarea-context", children => { + const spellCheckEnabled = useStateFromStores([SpellCheckStore], () => SpellCheckStore.isEnabled()); + const hasCorrections = Boolean(word && corrections?.length); + + const availableLanguages = useMemo(VesktopNative.spellcheck.getAvailableLanguages, []); + + const settings = useSettings(); + const spellCheckLanguages = (settings.spellCheckLanguages ??= [...new Set(navigator.languages)]); + + const pasteSectionIndex = children.findIndex(c => c?.props?.children?.some(c => c?.props?.id === "paste")); + + children.splice( + pasteSectionIndex === -1 ? children.length : pasteSectionIndex, + 0, + + {hasCorrections && ( + <> + {corrections.map(c => ( + VesktopNative.spellcheck.replaceMisspelling(c)} + /> + ))} + + VesktopNative.spellcheck.addToDictionary(word)} + /> + + )} + + + { + FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" }); + }} + /> + + + {availableLanguages.map(lang => { + const isEnabled = spellCheckLanguages.includes(lang); + return ( + = 5} + action={() => { + const newSpellCheckLanguages = spellCheckLanguages.filter(l => l !== lang); + if (newSpellCheckLanguages.length === spellCheckLanguages.length) { + newSpellCheckLanguages.push(lang); + } + + settings.spellCheckLanguages = newSpellCheckLanguages; + }} + /> + ); + })} + + + + ); +}); diff --git a/src/renderer/patches/windowsTitleBar.tsx b/src/renderer/patches/windowsTitleBar.tsx new file mode 100644 index 0000000..0501216 --- /dev/null +++ b/src/renderer/patches/windowsTitleBar.tsx @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Settings } from "renderer/settings"; + +import { addPatch } from "./shared"; + +if (Settings.store.customTitleBar) + addPatch({ + patches: [ + { + find: ".wordmarkWindows", + replacement: [ + { + // TODO: Fix eslint rule + // eslint-disable-next-line no-useless-escape + match: /case \i\.\i\.WINDOWS:/, + replace: 'case "WEB":' + }, + ...["close", "minimize", "maximize"].map(op => ({ + match: new RegExp(String.raw`\i\.\i\.${op}\b`), + replace: `VesktopNative.win.${op}` + })) + ] + } + ] + }); diff --git a/src/renderer/settings.ts b/src/renderer/settings.ts new file mode 100644 index 0000000..66ddec5 --- /dev/null +++ b/src/renderer/settings.ts @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { useEffect, useReducer } from "@vencord/types/webpack/common"; +import { SettingsStore } from "shared/utils/SettingsStore"; + +export const Settings = new SettingsStore(VesktopNative.settings.get()); +Settings.addGlobalChangeListener((o, p) => VesktopNative.settings.set(o, p)); + +export function useSettings() { + const [, update] = useReducer(x => x + 1, 0); + + useEffect(() => { + Settings.addGlobalChangeListener(update); + + return () => Settings.removeGlobalChangeListener(update); + }, []); + + return Settings.store; +} + +export function getValueAndOnChange(key: keyof typeof Settings.store) { + return { + value: Settings.store[key] as any, + onChange: (value: any) => (Settings.store[key] = value) + }; +} diff --git a/src/renderer/themedSplash.ts b/src/renderer/themedSplash.ts new file mode 100644 index 0000000..b10a865 --- /dev/null +++ b/src/renderer/themedSplash.ts @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Settings } from "./settings"; + +function isValidColor(color: CSSStyleValue | undefined): color is CSSUnparsedValue & { [0]: string } { + return color instanceof CSSUnparsedValue && typeof color[0] === "string" && CSS.supports("color", color[0]); +} + +function resolveColor(color: string) { + const span = document.createElement("span"); + span.style.color = color; + span.style.display = "none"; + + document.body.append(span); + const rgbColor = getComputedStyle(span).color; + span.remove(); + + return rgbColor; +} + +const updateSplashColors = () => { + const bodyStyles = document.body.computedStyleMap(); + + const color = bodyStyles.get("--text-normal"); + const backgroundColor = bodyStyles.get("--background-primary"); + + if (isValidColor(color)) { + Settings.store.splashColor = resolveColor(color[0]); + } + + if (isValidColor(backgroundColor)) { + Settings.store.splashBackground = resolveColor(backgroundColor[0]); + } +}; + +if (document.readyState === "complete") { + updateSplashColors(); +} else { + window.addEventListener("load", updateSplashColors); +} + +window.addEventListener("beforeunload", updateSplashColors); diff --git a/src/renderer/utils.ts b/src/renderer/utils.ts new file mode 100644 index 0000000..03e624c --- /dev/null +++ b/src/renderer/utils.ts @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +export const { localStorage } = window; + +export const isFirstRun = (() => { + const key = "VCD_FIRST_RUN"; + if (localStorage.getItem(key) !== null) return false; + localStorage.setItem(key, "false"); + return true; +})(); + +const { platform } = navigator; + +export const isWindows = platform.startsWith("Win"); +export const isMac = platform.startsWith("Mac"); +export const isLinux = platform.startsWith("Linux"); diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts new file mode 100644 index 0000000..4e939e4 --- /dev/null +++ b/src/shared/IpcEvents.ts @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +export const enum IpcEvents { + GET_VENCORD_PRELOAD_FILE = "VCD_GET_VC_PRELOAD_FILE", + GET_VENCORD_RENDERER_SCRIPT = "VCD_GET_VC_RENDERER_SCRIPT", + GET_RENDERER_SCRIPT = "VCD_GET_RENDERER_SCRIPT", + GET_RENDERER_CSS_FILE = "VCD_GET_RENDERER_CSS_FILE", + + GET_VERSION = "VCD_GET_VERSION", + SUPPORTS_WINDOWS_TRANSPARENCY = "VCD_SUPPORTS_WINDOWS_TRANSPARENCY", + + RELAUNCH = "VCD_RELAUNCH", + CLOSE = "VCD_CLOSE", + FOCUS = "VCD_FOCUS", + MINIMIZE = "VCD_MINIMIZE", + MAXIMIZE = "VCD_MAXIMIZE", + + SHOW_ITEM_IN_FOLDER = "VCD_SHOW_ITEM_IN_FOLDER", + GET_SETTINGS = "VCD_GET_SETTINGS", + SET_SETTINGS = "VCD_SET_SETTINGS", + + GET_VENCORD_DIR = "VCD_GET_VENCORD_DIR", + SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR", + + UPDATER_GET_DATA = "VCD_UPDATER_GET_DATA", + UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD", + UPDATE_IGNORE = "VCD_UPDATE_IGNORE", + + SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES", + SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT", + SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING", + SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY", + + SET_BADGE_COUNT = "VCD_SET_BADGE_COUNT", + + CAPTURER_GET_LARGE_THUMBNAIL = "VCD_CAPTURER_GET_LARGE_THUMBNAIL", + + AUTOSTART_ENABLED = "VCD_AUTOSTART_ENABLED", + ENABLE_AUTOSTART = "VCD_ENABLE_AUTOSTART", + DISABLE_AUTOSTART = "VCD_DISABLE_AUTOSTART", + + VIRT_MIC_LIST = "VCD_VIRT_MIC_LIST", + VIRT_MIC_START = "VCD_VIRT_MIC_START", + VIRT_MIC_START_SYSTEM = "VCD_VIRT_MIC_START_ALL", + VIRT_MIC_STOP = "VCD_VIRT_MIC_STOP", + + ARRPC_ACTIVITY = "VCD_ARRPC_ACTIVITY", + + CLIPBOARD_COPY_IMAGE = "VCD_CLIPBOARD_COPY_IMAGE" +} diff --git a/src/shared/browserWinProperties.ts b/src/shared/browserWinProperties.ts new file mode 100644 index 0000000..7d258a3 --- /dev/null +++ b/src/shared/browserWinProperties.ts @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import type { BrowserWindowConstructorOptions } from "electron"; + +export const SplashProps: BrowserWindowConstructorOptions = { + transparent: true, + frame: false, + height: 350, + width: 300, + center: true, + resizable: false, + maximizable: false, + alwaysOnTop: true +}; diff --git a/src/shared/paths.ts b/src/shared/paths.ts new file mode 100644 index 0000000..3858c64 --- /dev/null +++ b/src/shared/paths.ts @@ -0,0 +1,14 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { join } from "path"; + +export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static"); +export const VIEW_DIR = /* @__PURE__ */ join(STATIC_DIR, "views"); +export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges"); +export const ICON_PATH = /* @__PURE__ */ join(STATIC_DIR, "icon.png"); diff --git a/src/shared/settings.d.ts b/src/shared/settings.d.ts new file mode 100644 index 0000000..f394caf --- /dev/null +++ b/src/shared/settings.d.ts @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import type { Rectangle } from "electron"; + +export interface Settings { + discordBranch?: "stable" | "canary" | "ptb"; + transparencyOption?: "none" | "mica" | "tabbed" | "acrylic"; + tray?: boolean; + minimizeToTray?: boolean; + openLinksWithElectron?: boolean; + staticTitle?: boolean; + enableMenu?: boolean; + disableSmoothScroll?: boolean; + hardwareAcceleration?: boolean; + arRPC?: boolean; + appBadge?: boolean; + disableMinSize?: boolean; + clickTrayToShowHide?: boolean; + customTitleBar?: boolean; + checkUpdates?: boolean; + splashTheming?: boolean; + splashColor?: string; + splashBackground?: string; + + spellCheckLanguages?: string[]; + + audio?: { + workaround?: boolean; + + deviceSelect?: boolean; + granularSelect?: boolean; + + ignoreVirtual?: boolean; + ignoreDevices?: boolean; + ignoreInputMedia?: boolean; + + onlySpeakers?: boolean; + onlyDefaultSpeakers?: boolean; + }; +} + +export interface State { + maximized?: boolean; + minimized?: boolean; + windowBounds?: Rectangle; + displayid: int; + skippedUpdate?: string; + firstLaunch?: boolean; + + steamOSLayoutVersion?: number; + + vencordDir?: string; +} diff --git a/src/shared/utils/SettingsStore.ts b/src/shared/utils/SettingsStore.ts new file mode 100644 index 0000000..fb05c81 --- /dev/null +++ b/src/shared/utils/SettingsStore.ts @@ -0,0 +1,169 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { LiteralUnion } from "type-fest"; + +// Resolves a possibly nested prop in the form of "some.nested.prop" to type of T.some.nested.prop +type ResolvePropDeep = P extends `${infer Pre}.${infer Suf}` + ? Pre extends keyof T + ? ResolvePropDeep + : any + : P extends keyof T + ? T[P] + : any; + +/** + * The SettingsStore allows you to easily create a mutable store that + * has support for global and path-based change listeners. + */ +export class SettingsStore { + private pathListeners = new Map void>>(); + private globalListeners = new Set<(newData: T, path: string) => void>(); + + /** + * The store object. Making changes to this object will trigger the applicable change listeners + */ + declare public store: T; + /** + * The plain data. Changes to this object will not trigger any change listeners + */ + declare public plain: T; + + public constructor(plain: T) { + this.plain = plain; + this.store = this.makeProxy(plain); + } + + private makeProxy(object: any, root: T = object, path: string = "") { + const self = this; + + return new Proxy(object, { + get(target, key: string) { + const v = target[key]; + + if (typeof v === "object" && v !== null && !Array.isArray(v)) + return self.makeProxy(v, root, `${path}${path && "."}${key}`); + + return v; + }, + set(target, key: string, value) { + if (target[key] === value) return true; + + Reflect.set(target, key, value); + const setPath = `${path}${path && "."}${key}`; + + self.globalListeners.forEach(cb => cb(root, setPath)); + self.pathListeners.get(setPath)?.forEach(cb => cb(value)); + + return true; + }, + deleteProperty(target, key: string) { + if (!(key in target)) return true; + + const res = Reflect.deleteProperty(target, key); + if (!res) return false; + + const setPath = `${path}${path && "."}${key}`; + + self.globalListeners.forEach(cb => cb(root, setPath)); + self.pathListeners.get(setPath)?.forEach(cb => cb(undefined)); + + return res; + } + }); + } + + /** + * Set the data of the store. + * This will update this.store and this.plain (and old references to them will be stale! Avoid storing them in variables) + * + * Additionally, all global listeners (and those for pathToNotify, if specified) will be called with the new data + * @param value New data + * @param pathToNotify Optional path to notify instead of globally. Used to transfer path via ipc + */ + public setData(value: T, pathToNotify?: string) { + this.plain = value; + this.store = this.makeProxy(value); + + if (pathToNotify) { + let v = value; + + const path = pathToNotify.split("."); + for (const p of path) { + if (!v) { + console.warn( + `Settings#setData: Path ${pathToNotify} does not exist in new data. Not dispatching update` + ); + return; + } + v = v[p]; + } + + this.pathListeners.get(pathToNotify)?.forEach(cb => cb(v)); + } + + this.globalListeners.forEach(cb => cb(value, "")); + } + + /** + * Add a global change listener, that will fire whenever any setting is changed + */ + public addGlobalChangeListener(cb: (data: T, path: string) => void) { + this.globalListeners.add(cb); + } + + /** + * Add a scoped change listener that will fire whenever a setting matching the specified path is changed. + * + * For example if path is `"foo.bar"`, the listener will fire on + * ```js + * Setting.store.foo.bar = "hi" + * ``` + * but not on + * ```js + * Setting.store.foo.baz = "hi" + * ``` + * @param path + * @param cb + */ + public addChangeListener

>( + path: P, + cb: (data: ResolvePropDeep) => void + ) { + const listeners = this.pathListeners.get(path as string) ?? new Set(); + listeners.add(cb); + this.pathListeners.set(path as string, listeners); + } + + /** + * Remove a global listener + * @see {@link addGlobalChangeListener} + */ + public removeGlobalChangeListener(cb: (data: T, path: string) => void) { + this.globalListeners.delete(cb); + } + + /** + * Remove a scoped listener + * @see {@link addChangeListener} + */ + public removeChangeListener(path: LiteralUnion, cb: (data: any) => void) { + const listeners = this.pathListeners.get(path as string); + if (!listeners) return; + + listeners.delete(cb); + if (!listeners.size) this.pathListeners.delete(path as string); + } + + /** + * Call all global change listeners + */ + public markAsChanged() { + this.globalListeners.forEach(cb => cb(this.plain, "")); + } +} diff --git a/src/shared/utils/debounce.ts b/src/shared/utils/debounce.ts new file mode 100644 index 0000000..90da454 --- /dev/null +++ b/src/shared/utils/debounce.ts @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +/** + * Returns a new function that will only be called after the given delay. + * Subsequent calls will cancel the previous timeout and start a new one from 0 + * + * Useful for grouping multiple calls into one + */ +export function debounce(func: T, delay = 300): T { + let timeout: NodeJS.Timeout; + return function (...args: any[]) { + clearTimeout(timeout); + timeout = setTimeout(() => { + func(...args); + }, delay); + } as any; +} diff --git a/src/shared/utils/guards.ts b/src/shared/utils/guards.ts new file mode 100644 index 0000000..01f6729 --- /dev/null +++ b/src/shared/utils/guards.ts @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +export function isTruthy(item: T): item is Exclude { + return Boolean(item); +} + +export function isNonNullish(item: T): item is Exclude { + return item != null; +} diff --git a/src/shared/utils/once.ts b/src/shared/utils/once.ts new file mode 100644 index 0000000..ba6d681 --- /dev/null +++ b/src/shared/utils/once.ts @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +/** + * Wraps the given function so that it can only be called once + * @param fn Function to wrap + * @returns New function that can only be called once + */ +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/src/shared/utils/sleep.ts b/src/shared/utils/sleep.ts new file mode 100644 index 0000000..42f499b --- /dev/null +++ b/src/shared/utils/sleep.ts @@ -0,0 +1,11 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Modified for Aerocord, originally part of Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2024-2024 Aiek + * Copyright (c) 2024 RandomServer Community + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +export function sleep(ms: number): Promise { + return new Promise(r => setTimeout(r, ms)); +} diff --git a/src/updater/main.ts b/src/updater/main.ts new file mode 100644 index 0000000..dcad0f8 --- /dev/null +++ b/src/updater/main.ts @@ -0,0 +1,97 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3. + * Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! + */ + +// lets just use our own c# updater cuz why not +import { app, BrowserWindow, shell } from "electron"; +import { Settings, State } from "main/settings"; +import { handle } from "main/utils/ipcWrappers"; +import { makeLinksOpenExternally } from "main/utils/makeLinksOpenExternally"; +import { githubGet, ReleaseData } from "main/utils/vencordLoader"; +import { join } from "path"; +import { IpcEvents } from "shared/IpcEvents"; +import { ICON_PATH, VIEW_DIR } from "shared/paths"; + +export interface UpdateData { + currentVersion: string; + latestVersion: string; + release: ReleaseData; +} + +let updateData: UpdateData; + +handle(IpcEvents.UPDATER_GET_DATA, () => updateData); +handle(IpcEvents.UPDATER_DOWNLOAD, () => { + const updaterPath = join(app.getPath('exe'), '..', 'Updater.exe') + shell.openPath(updaterPath); +}); + +handle(IpcEvents.UPDATE_IGNORE, () => { + State.store.skippedUpdate = updateData.latestVersion; +}); + +function isOutdated(oldVersion: string, newVersion: string) { + const oldParts = oldVersion.split("."); + const newParts = newVersion.split("."); + + if (oldParts.length !== newParts.length) + throw new Error(`Incompatible version strings (old: ${oldVersion}, new: ${newVersion})`); + + for (let i = 0; i < oldParts.length; i++) { + const oldPart = Number(oldParts[i]); + const newPart = Number(newParts[i]); + + if (isNaN(oldPart) || isNaN(newPart)) + throw new Error(`Invalid version string (old: ${oldVersion}, new: ${newVersion})`); + + if (oldPart < newPart) return true; + if (oldPart > newPart) return false; + } + + return false; +} + +export async function checkUpdates() { + if (Settings.store.checkUpdates === false) return; + + try { // make this work with gitea cuz FUK GITHUB! + const raw = await fetch("https://git.randomserver.top/api/v1/repos/aiek/aerocord/releases/latest"); + const data = await raw.json(); + + const oldVersion = app.getVersion(); + const newVersion = data.tag_name.replace(/^v/, ""); + updateData = { + currentVersion: oldVersion, + latestVersion: newVersion, + release: data + }; + + + if (State.store.skippedUpdate !== newVersion && isOutdated(oldVersion, newVersion)) { + openNewUpdateWindow(); + } + } catch (e) { + console.error("AppUpdater: Failed to check for updates\n", e); + } +} + +function openNewUpdateWindow() { + const win = new BrowserWindow({ + width: 500, + autoHideMenuBar: true, + alwaysOnTop: true, + webPreferences: { + preload: join(__dirname, "updaterPreload.js"), + nodeIntegration: false, + contextIsolation: true, + sandbox: true + }, + icon: ICON_PATH + }); + + makeLinksOpenExternally(win); + + win.loadFile(join(VIEW_DIR, "updater.html")); +} \ No newline at end of file diff --git a/src/updater/preload.ts b/src/updater/preload.ts new file mode 100644 index 0000000..7d7d921 --- /dev/null +++ b/src/updater/preload.ts @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Aerocord, a vesktop fork for older microsoft NT releases such as NT 6.0, 6.1, 6.2 and 6.3. + * Credits to vendicated and the rest of the vesktop contribuitors for making Vesktop! + */ + +import { contextBridge } from "electron"; +import { invoke } from "preload/typedIpc"; +import { IpcEvents } from "shared/IpcEvents"; + +import type { UpdateData } from "./main"; + +contextBridge.exposeInMainWorld("Updater", { + getData: () => invoke(IpcEvents.UPDATER_GET_DATA), + download: () => { + invoke(IpcEvents.UPDATER_DOWNLOAD); + invoke(IpcEvents.CLOSE); + }, + ignore: () => invoke(IpcEvents.UPDATE_IGNORE), + close: () => invoke(IpcEvents.CLOSE) +}); diff --git a/static/badges/1.ico b/static/badges/1.ico new file mode 100644 index 0000000..0e2003f Binary files /dev/null and b/static/badges/1.ico differ diff --git a/static/badges/10.ico b/static/badges/10.ico new file mode 100644 index 0000000..1e02de3 Binary files /dev/null and b/static/badges/10.ico differ diff --git a/static/badges/11.ico b/static/badges/11.ico new file mode 100644 index 0000000..7ebd2ed Binary files /dev/null and b/static/badges/11.ico differ diff --git a/static/badges/2.ico b/static/badges/2.ico new file mode 100644 index 0000000..d7b6f44 Binary files /dev/null and b/static/badges/2.ico differ diff --git a/static/badges/3.ico b/static/badges/3.ico new file mode 100644 index 0000000..43ce7cf Binary files /dev/null and b/static/badges/3.ico differ diff --git a/static/badges/4.ico b/static/badges/4.ico new file mode 100644 index 0000000..91bd61f Binary files /dev/null and b/static/badges/4.ico differ diff --git a/static/badges/5.ico b/static/badges/5.ico new file mode 100644 index 0000000..1d6bf8f Binary files /dev/null and b/static/badges/5.ico differ diff --git a/static/badges/6.ico b/static/badges/6.ico new file mode 100644 index 0000000..d0c0cd6 Binary files /dev/null and b/static/badges/6.ico differ diff --git a/static/badges/7.ico b/static/badges/7.ico new file mode 100644 index 0000000..b50750c Binary files /dev/null and b/static/badges/7.ico differ diff --git a/static/badges/8.ico b/static/badges/8.ico new file mode 100644 index 0000000..8d0ca36 Binary files /dev/null and b/static/badges/8.ico differ diff --git a/static/badges/9.ico b/static/badges/9.ico new file mode 100644 index 0000000..c153779 Binary files /dev/null and b/static/badges/9.ico differ diff --git a/static/icon.ico b/static/icon.ico new file mode 100644 index 0000000..677a275 Binary files /dev/null and b/static/icon.ico differ diff --git a/static/icon.png b/static/icon.png new file mode 100644 index 0000000..69778aa Binary files /dev/null and b/static/icon.png differ diff --git a/static/shiggy.gif b/static/shiggy.gif new file mode 100644 index 0000000..f1fa9eb Binary files /dev/null and b/static/shiggy.gif differ diff --git a/static/views/about.html b/static/views/about.html new file mode 100644 index 0000000..1c9b5e3 --- /dev/null +++ b/static/views/about.html @@ -0,0 +1,56 @@ + + + + + + + +

Aerocord

+

+ Aerocord is a Vesktop fork made to work with Windows Vista/7/8. +

+ +
+

Aerocord Gitea:

+ +
+
+

Credits to the original Vesktop developer:

+ +
+ +
+ Building + instructions can be found here + + + \ No newline at end of file diff --git a/static/views/first-launch.html b/static/views/first-launch.html new file mode 100644 index 0000000..a467cab --- /dev/null +++ b/static/views/first-launch.html @@ -0,0 +1,168 @@ + + + + + + + +

Welcome to Aerocord - a Vesktop fork meant for Windows Vista, 7 and 8

+

Let's customise your experience!

+ +
+ + + + + + + + + +
+
+ + +
+ + + \ No newline at end of file diff --git a/static/views/splash.html b/static/views/splash.html new file mode 100644 index 0000000..a751f80 --- /dev/null +++ b/static/views/splash.html @@ -0,0 +1,45 @@ + + + + + + + +
+ Windows 7 jumpscare +

Loading Aerocord...

+
Made by the RandomServer Community!
+
+ \ No newline at end of file diff --git a/static/views/style.css b/static/views/style.css new file mode 100644 index 0000000..cc59940 --- /dev/null +++ b/static/views/style.css @@ -0,0 +1,30 @@ +:root { + --bg: black; + --fg: white; + --fg-secondary: #313338; + --fg-semi-trans: rgb(0 0 0 / 0.2); + --link: #006ce7; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: hsl(223 6.7% 20.6%); + --fg: cyan; + --fg-secondary: #b5bac1; + --fg-semi-trans: rgb(255 255 255 / 0.2); + --link: #00a8fc; + } +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, + "Open Sans", "Helvetica Neue", sans-serif; + margin: 0; + padding: 0; + background: var(--bg); + color: var(--fg); +} + +a { + color: var(--link); +} diff --git a/static/views/updater.html b/static/views/updater.html new file mode 100644 index 0000000..e211fec --- /dev/null +++ b/static/views/updater.html @@ -0,0 +1,123 @@ + + + + + + + +
+
+

Update Available

+

There's a new update for Aerocord! Update now to get new fixes and features!

+

+ Current: +
+ Latest: +

+ +

Changelog

+

Loading...

+
+ +
+ + +
+ + +
+
+
+ + + + + \ No newline at end of file