|
@ -0,0 +1,239 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<!--Created with jdAppStreamEdit 7.1-->
|
||||||
|
<id>dev.vencord.Vesktop</id>
|
||||||
|
<name>Vesktop</name>
|
||||||
|
<summary>Snappier Discord app with Vencord</summary>
|
||||||
|
<developer_name>Vencord Contributors</developer_name>
|
||||||
|
<launchable type="desktop-id">dev.vencord.Vesktop.desktop</launchable>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>GPL-3.0</project_license>
|
||||||
|
<project_group>Vencord</project_group>
|
||||||
|
<description>
|
||||||
|
<p>Vesktop is a cross platform desktop app aiming to give you a snappier Discord experience with Vencord pre-installed.</p>
|
||||||
|
<p>Vesktop comes bundled with Venmic, a purpose-built library to provide functioning audio screenshare.</p>
|
||||||
|
</description>
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<caption>Vencord settings page and about window open</caption>
|
||||||
|
<image type="source">https://vencord.dev/assets/screenshots/vesktop-1-appstream.png</image>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<caption>A dialog showing screenshare options</caption>
|
||||||
|
<image type="source">https://vencord.dev/assets/screenshots/vesktop-2-appstream.png</image>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<caption>A screenshot of a Discord server</caption>
|
||||||
|
<image type="source">https://vencord.dev/assets/screenshots/vesktop-3-appstream.png</image>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
<releases>
|
||||||
|
<release version="1.5.3" date="2024-07-04" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.3</url>
|
||||||
|
<description>
|
||||||
|
<p>Features</p>
|
||||||
|
<ul>
|
||||||
|
<li>added arm64 Windows support</li>
|
||||||
|
<li>windows & macOS builds are now universal</li>
|
||||||
|
<li>added option to configure spellcheck languages</li>
|
||||||
|
<li>will auto-update from now on</li>
|
||||||
|
<li>updated electron to 31 & Chromium to 126</li>
|
||||||
|
<li>macOS: Added customized dmg background by @khcrysalis</li>
|
||||||
|
<li>Windows Portable: store settings in portable folder by @MrGarlic1</li>
|
||||||
|
<li>linux audioshare: added granular selection, more options, better ui by @Curve</li>
|
||||||
|
<li>changed default screen-sharing quality to 720p 30 FPS by @Tiagoquix</li>
|
||||||
|
</ul>
|
||||||
|
<p>Fixes</p>
|
||||||
|
<ul>
|
||||||
|
<li>macOS: Added workaround for making things in draggable area clickable by @HAHALOSAH</li>
|
||||||
|
<li>fixed Screenshare UI for non-linux systems by @PolisanTheEasyNick</li>
|
||||||
|
<li>fixed opening on screen that was disconnected by @MrGarlic1</li>
|
||||||
|
<li>mac: hide native window controls with custom titlebar enabled by @MrGarlic1</li>
|
||||||
|
<li>fixed some broken patches by @D3SOX</li>
|
||||||
|
<li>fixed framerate in constraints by @kittykel</li>
|
||||||
|
<li>fixed some first launch switches not applying</li>
|
||||||
|
<li>fixed potential sandbox escape via custom vencord location</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.5.2" date="2024-05-01" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.2</url>
|
||||||
|
<description>
|
||||||
|
<p>What's Changed</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed scrollbars looking wrong (actually Discord's fault)</li>
|
||||||
|
<li>Tray: Added left click hide/show feature by @0bCdian</li>
|
||||||
|
<li>MacOS: Fixed the app not properly requesting microphone permissions by @ssalggnikool</li>
|
||||||
|
<li>Linux: Various fixed related to audio screenshare by @Curve</li>
|
||||||
|
<li>Linux: Overhauled & improved screenshare with better framerate by @kaitlynkittyy</li>
|
||||||
|
<li>Users can now pass --enable/disable-features command line flags by @takase1121</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.5.1" date="2024-03-12" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.1</url>
|
||||||
|
<description>
|
||||||
|
<p>New Features</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added categories to Vesktop settings to reduce visual clutter by @justin13888</li>
|
||||||
|
<li>Added support for Vencord's transparent window options</li>
|
||||||
|
</ul>
|
||||||
|
<p>Fixes</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed ugly error popups when starting Vesktop without working internet connection</li>
|
||||||
|
<li>Fixed popout title bars on Windows</li>
|
||||||
|
<li>Fixed spellcheck entries</li>
|
||||||
|
<li>Fixed screenshare audio using microphone on debian, by @Curve</li>
|
||||||
|
<li>Fixed a bug where autostart on Linux won't preserve command line flags</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.5.0" date="2024-01-16" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.0</url>
|
||||||
|
<description>
|
||||||
|
<p>What's Changed</p>
|
||||||
|
<ul>
|
||||||
|
<li>fully renamed to Vesktop. You will likely have to login to Discord again. You might have to re-create your vesktop shortcut</li>
|
||||||
|
<li>added option to disable smooth scrolling by @ZirixCZ</li>
|
||||||
|
<li>added setting to disable hardware acceleration by @zt64</li>
|
||||||
|
<li>fixed adding connections</li>
|
||||||
|
<li>fixed / improved discord popouts</li>
|
||||||
|
<li>you can now use the custom discord titlebar on linux/mac</li>
|
||||||
|
<li>the splash window is now draggable</li>
|
||||||
|
<li>now signed on mac</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.4.4" date="2023-12-02" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.4</url>
|
||||||
|
<description>
|
||||||
|
<p>What's Changed</p>
|
||||||
|
<ul>
|
||||||
|
<li>improve venmic system compatibility by @Curve</li>
|
||||||
|
<li>Update steamdeck controller layout by @AAGaming00</li>
|
||||||
|
<li>feat: Add option to disable smooth scrolling by @ZirixCZ</li>
|
||||||
|
<li>unblur shiggy in splash screen by @viacoro</li>
|
||||||
|
<li>update electron & arrpc @D3SOX</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.4.3" date="2023-11-01" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.3</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.4.2" date="2023-10-26" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.2</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.4.1" date="2023-10-24" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.1</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.4.0" date="2023-10-21" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.0</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.3.3" date="2023-09-30" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.3</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.3.2" date="2023-09-25" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.2</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.3.1" date="2023-09-25" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.1</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.3.0" date="2023-08-16" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.0</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.9" date="2023-08-12" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.9</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.8" date="2023-08-02" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.8</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.7" date="2023-07-26" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.7</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.6" date="2023-07-04" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.6</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.5" date="2023-06-26" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.5</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.4" date="2023-06-25" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.4</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.3" date="2023-06-23" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.3</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.2" date="2023-06-21" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.2</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.1" date="2023-06-21" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.1</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.0" date="2023-05-03" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.0</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.9" date="2023-04-27" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.9</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.8" date="2023-04-15" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.8</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.7" date="2023-04-15" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.7</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.6" date="2023-04-11" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.6</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.5" date="2023-04-10" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.5</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.4" date="2023-04-09" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.4</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.3" date="2023-04-06" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.3</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.2" date="2023-04-05" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.2</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.1" date="2023-04-04" type="stable">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.1</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.0" date="2023-04-04" type="development">
|
||||||
|
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.0</url>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
<url type="homepage">https://vencord.dev/</url>
|
||||||
|
<url type="bugtracker">https://github.com/Vencord/Vesktop/issues</url>
|
||||||
|
<url type="faq">https://vencord.dev/faq/</url>
|
||||||
|
<url type="help">https://github.com/Vencord/Vesktop/issues</url>
|
||||||
|
<url type="donation">https://github.com/sponsors/Vendicated</url>
|
||||||
|
<url type="vcs-browser">https://github.com/Vencord/Vesktop</url>
|
||||||
|
<categories>
|
||||||
|
<category>InstantMessaging</category>
|
||||||
|
<category>Network</category>
|
||||||
|
</categories>
|
||||||
|
<requires>
|
||||||
|
<control>pointing</control>
|
||||||
|
<control>keyboard</control>
|
||||||
|
<display_length compare="ge">420</display_length>
|
||||||
|
<internet>always</internet>
|
||||||
|
</requires>
|
||||||
|
<recommends>
|
||||||
|
<control>voice</control>
|
||||||
|
<display_length compare="ge">760</display_length>
|
||||||
|
<display_length compare="le">1200</display_length>
|
||||||
|
</recommends>
|
||||||
|
<content_rating type="oars-1.1">
|
||||||
|
<content_attribute id="social-chat">intense</content_attribute>
|
||||||
|
<content_attribute id="social-audio">intense</content_attribute>
|
||||||
|
<content_attribute id="social-contacts">intense</content_attribute>
|
||||||
|
<content_attribute id="social-info">intense</content_attribute>
|
||||||
|
</content_rating>
|
||||||
|
<keywords>
|
||||||
|
<keyword>Discord</keyword>
|
||||||
|
<keyword>Vencord</keyword>
|
||||||
|
<keyword>Vesktop</keyword>
|
||||||
|
<keyword>Privacy</keyword>
|
||||||
|
<keyword>Mod</keyword>
|
||||||
|
</keywords>
|
||||||
|
</component>
|
|
@ -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];
|
|
@ -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);
|
||||||
|
});
|
|
@ -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);
|
|
@ -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;
|
|
@ -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.+$/
|
||||||
|
});
|
|
@ -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
|
||||||
|
*/
|
|
@ -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(" ") ?? [])]);
|
|
@ -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"]);
|
|
@ -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();
|
|
@ -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);
|
||||||
|
}
|
|
@ -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");
|
|
@ -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 {};
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<number, NativeImage>();
|
||||||
|
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`];
|
||||||
|
}
|
|
@ -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);
|
|
@ -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;
|
|
@ -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
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
|
@ -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: `<img src="${src.replaceAll('"', '\\"')}">`,
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
);
|
|
@ -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 extends object>(o: SettingsStore<O>) {
|
||||||
|
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<MenuItemConstructorOptions | false>;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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<T extends object = any>(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<TSettings>(SETTINGS_FILE, "Vesktop settings");
|
||||||
|
|
||||||
|
export const VencordSettings = loadSettings<any>(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<TState>(STATE_FILE, "Vesktop state");
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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<string, BrowserWindow>();
|
||||||
|
|
||||||
|
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 <const>{ action: "deny" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return <const>{
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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());
|
|
@ -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<SpellCheckerResultCallback>();
|
||||||
|
|
||||||
|
ipcRenderer.on(IpcEvents.SPELLCHECK_RESULT, (_, w: string, s: string[]) => {
|
||||||
|
spellCheckCallbacks.forEach(cb => cb(w, s));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const VesktopNative = {
|
||||||
|
app: {
|
||||||
|
relaunch: () => invoke<void>(IpcEvents.RELAUNCH),
|
||||||
|
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION),
|
||||||
|
setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
|
||||||
|
supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY)
|
||||||
|
},
|
||||||
|
autostart: {
|
||||||
|
isEnabled: () => sendSync<boolean>(IpcEvents.AUTOSTART_ENABLED),
|
||||||
|
enable: () => invoke<void>(IpcEvents.ENABLE_AUTOSTART),
|
||||||
|
disable: () => invoke<void>(IpcEvents.DISABLE_AUTOSTART)
|
||||||
|
},
|
||||||
|
fileManager: {
|
||||||
|
showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path),
|
||||||
|
getVencordDir: () => sendSync<string | undefined>(IpcEvents.GET_VENCORD_DIR),
|
||||||
|
selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value)
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
||||||
|
set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path)
|
||||||
|
},
|
||||||
|
spellcheck: {
|
||||||
|
getAvailableLanguages: () => sendSync<string[]>(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES),
|
||||||
|
onSpellcheckResult(cb: SpellCheckerResultCallback) {
|
||||||
|
spellCheckCallbacks.add(cb);
|
||||||
|
},
|
||||||
|
offSpellcheckResult(cb: SpellCheckerResultCallback) {
|
||||||
|
spellCheckCallbacks.delete(cb);
|
||||||
|
},
|
||||||
|
replaceMisspelling: (word: string) => invoke<void>(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, word),
|
||||||
|
addToDictionary: (word: string) => invoke<void>(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, word)
|
||||||
|
},
|
||||||
|
win: {
|
||||||
|
focus: () => invoke<void>(IpcEvents.FOCUS),
|
||||||
|
close: (key?: string) => invoke<void>(IpcEvents.CLOSE, key),
|
||||||
|
minimize: () => invoke<void>(IpcEvents.MINIMIZE),
|
||||||
|
maximize: () => invoke<void>(IpcEvents.MAXIMIZE)
|
||||||
|
},
|
||||||
|
capturer: {
|
||||||
|
getLargeThumbnail: (id: string) => invoke<string>(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<void>(IpcEvents.VIRT_MIC_START, include),
|
||||||
|
startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude),
|
||||||
|
stop: () => invoke<void>(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<void>(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc)
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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<T = any>(event: IpcEvents, ...args: any[]) {
|
||||||
|
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
|
||||||
|
return ipcRenderer.sendSync(event, ...args) as T;
|
||||||
|
}
|
|
@ -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");
|
|
@ -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<StreamPick>((resolve, reject) => {
|
||||||
|
const key = openModal(
|
||||||
|
props => (
|
||||||
|
<ModalComponent
|
||||||
|
screens={screens}
|
||||||
|
modalProps={props}
|
||||||
|
submit={async v => {
|
||||||
|
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 (
|
||||||
|
<div className="vcd-screen-picker-grid">
|
||||||
|
{screens.map(({ id, name, url }) => (
|
||||||
|
<label key={id}>
|
||||||
|
<input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} />
|
||||||
|
|
||||||
|
<img src={url} alt="" />
|
||||||
|
<Text variant="text-sm/normal">{name}</Text>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AudioSettingsModal({
|
||||||
|
modalProps,
|
||||||
|
close,
|
||||||
|
setAudioSources
|
||||||
|
}: {
|
||||||
|
modalProps: any;
|
||||||
|
close: () => void;
|
||||||
|
setAudioSources: (s: AudioSources) => void;
|
||||||
|
}) {
|
||||||
|
const Settings = useSettings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||||
|
<Modals.ModalHeader className="vcd-screen-picker-header">
|
||||||
|
<Forms.FormTitle tag="h2">Venmic Settings</Forms.FormTitle>
|
||||||
|
<Modals.ModalCloseButton onClick={close} />
|
||||||
|
</Modals.ModalHeader>
|
||||||
|
<Modals.ModalContent className="vcd-screen-picker-modal">
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (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
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (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
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })}
|
||||||
|
value={Settings.audio?.onlyDefaultSpeakers ?? true}
|
||||||
|
note={
|
||||||
|
<>
|
||||||
|
When sharing entire desktop audio, only share apps that play to the <b>default</b> speakers.
|
||||||
|
You may want to disable this when using "mix bussing".
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Only Default Speakers
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })}
|
||||||
|
value={Settings.audio?.ignoreInputMedia ?? true}
|
||||||
|
note={<>Exclude nodes that are intended to capture audio.</>}
|
||||||
|
>
|
||||||
|
Ignore Inputs
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v => (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
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={v =>
|
||||||
|
(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
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={value => {
|
||||||
|
Settings.audio = { ...Settings.audio, granularSelect: value };
|
||||||
|
setAudioSources("None");
|
||||||
|
}}
|
||||||
|
value={Settings.audio?.granularSelect ?? false}
|
||||||
|
note={<>Allow to select applications more granularly.</>}
|
||||||
|
>
|
||||||
|
Granular Selection
|
||||||
|
</Switch>
|
||||||
|
<Switch
|
||||||
|
hideBorder
|
||||||
|
onChange={value => {
|
||||||
|
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 <b>Ignore Devices</b> to be turned
|
||||||
|
off.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Device Selection
|
||||||
|
</Switch>
|
||||||
|
</Modals.ModalContent>
|
||||||
|
<Modals.ModalFooter className="vcd-screen-picker-footer">
|
||||||
|
<Button color={Button.Colors.TRANSPARENT} onClick={close}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</Modals.ModalFooter>
|
||||||
|
</Modals.ModalRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StreamSettings({
|
||||||
|
source,
|
||||||
|
settings,
|
||||||
|
setSettings,
|
||||||
|
skipPicker
|
||||||
|
}: {
|
||||||
|
source: Source;
|
||||||
|
settings: StreamSettings;
|
||||||
|
setSettings: Dispatch<SetStateAction<StreamSettings>>;
|
||||||
|
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 => (
|
||||||
|
<AudioSettingsModal
|
||||||
|
modalProps={props}
|
||||||
|
close={() => props.onClose()}
|
||||||
|
setAudioSources={sources =>
|
||||||
|
setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Forms.FormTitle>What you're streaming</Forms.FormTitle>
|
||||||
|
<Card className="vcd-screen-picker-card vcd-screen-picker-preview">
|
||||||
|
<img
|
||||||
|
src={thumb}
|
||||||
|
alt=""
|
||||||
|
className={isLinux ? "vcd-screen-picker-preview-img-linux" : "vcd-screen-picker-preview-img"}
|
||||||
|
/>
|
||||||
|
<Text variant="text-sm/normal">{source.name}</Text>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Forms.FormTitle>Stream Settings</Forms.FormTitle>
|
||||||
|
|
||||||
|
<Card className="vcd-screen-picker-card">
|
||||||
|
<div className="vcd-screen-picker-quality">
|
||||||
|
<section>
|
||||||
|
<Forms.FormTitle>Resolution</Forms.FormTitle>
|
||||||
|
<div className="vcd-screen-picker-radios">
|
||||||
|
{StreamResolutions.map(res => (
|
||||||
|
<label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}>
|
||||||
|
<Text variant="text-sm/bold">{res}</Text>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="resolution"
|
||||||
|
value={res}
|
||||||
|
checked={settings.resolution === res}
|
||||||
|
onChange={() => setSettings(s => ({ ...s, resolution: res }))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Forms.FormTitle>Frame Rate</Forms.FormTitle>
|
||||||
|
<div className="vcd-screen-picker-radios">
|
||||||
|
{StreamFps.map(fps => (
|
||||||
|
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
|
||||||
|
<Text variant="text-sm/bold">{fps}</Text>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="fps"
|
||||||
|
value={fps}
|
||||||
|
checked={settings.fps === fps}
|
||||||
|
onChange={() => setSettings(s => ({ ...s, fps }))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div className="vcd-screen-picker-quality">
|
||||||
|
<section>
|
||||||
|
<Forms.FormTitle>Content Type</Forms.FormTitle>
|
||||||
|
<div>
|
||||||
|
<div className="vcd-screen-picker-radios">
|
||||||
|
<label
|
||||||
|
className="vcd-screen-picker-radio"
|
||||||
|
data-checked={settings.contentHint === "motion"}
|
||||||
|
>
|
||||||
|
<Text variant="text-sm/bold">Prefer Smoothness</Text>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="contenthint"
|
||||||
|
value="motion"
|
||||||
|
checked={settings.contentHint === "motion"}
|
||||||
|
onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className="vcd-screen-picker-radio"
|
||||||
|
data-checked={settings.contentHint === "detail"}
|
||||||
|
>
|
||||||
|
<Text variant="text-sm/bold">Prefer Clarity</Text>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="contenthint"
|
||||||
|
value="detail"
|
||||||
|
checked={settings.contentHint === "detail"}
|
||||||
|
onChange={() => setSettings(s => ({ ...s, contentHint: "detail" }))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="vcd-screen-picker-hint-description">
|
||||||
|
<p>
|
||||||
|
Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange
|
||||||
|
for a much sharper and clearer image.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isWindows && (
|
||||||
|
<Switch
|
||||||
|
value={settings.audio}
|
||||||
|
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
|
||||||
|
hideBorder
|
||||||
|
className="vcd-screen-picker-audio"
|
||||||
|
>
|
||||||
|
Stream With Audio
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLinux && (
|
||||||
|
<AudioSourcePickerLinux
|
||||||
|
openSettings={openSettings}
|
||||||
|
includeSources={settings.includeSources}
|
||||||
|
excludeSources={settings.excludeSources}
|
||||||
|
deviceSelect={Settings.audio?.deviceSelect}
|
||||||
|
granularSelect={Settings.audio?.granularSelect}
|
||||||
|
setIncludeSources={sources => setSettings(s => ({ ...s, includeSources: sources }))}
|
||||||
|
setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Forms.FormText>
|
||||||
|
Failed to retrieve Audio Sources because your C++ library is too old to run
|
||||||
|
<a href="https://github.com/Vencord/venmic" target="_blank">
|
||||||
|
venmic
|
||||||
|
</a>
|
||||||
|
. See{" "}
|
||||||
|
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
|
||||||
|
this guide
|
||||||
|
</a>{" "}
|
||||||
|
for possible solutions.
|
||||||
|
</Forms.FormText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPipewirePulse && !ignorePulseWarning) {
|
||||||
|
return (
|
||||||
|
<Text variant="text-sm/normal">
|
||||||
|
Could not find pipewire-pulse. See{" "}
|
||||||
|
<a href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install" target="_blank">
|
||||||
|
this guide
|
||||||
|
</a>{" "}
|
||||||
|
on how to switch to pipewire. <br />
|
||||||
|
You can still continue, however, please{" "}
|
||||||
|
<b>beware that you can only share audio of apps that are running under pipewire</b>.{" "}
|
||||||
|
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing!</a>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div className={includeSources === "Entire System" ? "vcd-screen-picker-quality" : undefined}>
|
||||||
|
<section>
|
||||||
|
<Forms.FormTitle>{loading ? "Loading Sources..." : "Audio Sources"}</Forms.FormTitle>
|
||||||
|
<Select
|
||||||
|
options={allSources.map(({ name, value }) => ({
|
||||||
|
label: name,
|
||||||
|
value: value,
|
||||||
|
default: name === "None"
|
||||||
|
}))}
|
||||||
|
isSelected={isItemSelected(includeSources)}
|
||||||
|
select={updateItems(setIncludeSources, includeSources)}
|
||||||
|
serialize={String}
|
||||||
|
popoutPosition="top"
|
||||||
|
closeOnSelect={false}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
{includeSources === "Entire System" && (
|
||||||
|
<section>
|
||||||
|
<Forms.FormTitle>Exclude Sources</Forms.FormTitle>
|
||||||
|
<Select
|
||||||
|
options={allSources
|
||||||
|
.filter(x => 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}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
color={Button.Colors.TRANSPARENT}
|
||||||
|
onClick={openSettings}
|
||||||
|
className="vcd-screen-picker-settings-button"
|
||||||
|
>
|
||||||
|
Open Audio Settings
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModalComponent({
|
||||||
|
screens,
|
||||||
|
modalProps,
|
||||||
|
submit,
|
||||||
|
close,
|
||||||
|
skipPicker
|
||||||
|
}: {
|
||||||
|
screens: Source[];
|
||||||
|
modalProps: any;
|
||||||
|
submit: (data: StreamPick) => void;
|
||||||
|
close: () => void;
|
||||||
|
skipPicker: boolean;
|
||||||
|
}) {
|
||||||
|
const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0);
|
||||||
|
const [settings, setSettings] = useState<StreamSettings>({
|
||||||
|
resolution: "720",
|
||||||
|
fps: "30",
|
||||||
|
contentHint: "motion",
|
||||||
|
audio: true,
|
||||||
|
includeSources: "None"
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||||
|
<Modals.ModalHeader className="vcd-screen-picker-header">
|
||||||
|
<Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle>
|
||||||
|
<Modals.ModalCloseButton onClick={close} />
|
||||||
|
</Modals.ModalHeader>
|
||||||
|
<Modals.ModalContent className="vcd-screen-picker-modal">
|
||||||
|
{!selected ? (
|
||||||
|
<ScreenPicker screens={screens} chooseScreen={setSelected} />
|
||||||
|
) : (
|
||||||
|
<StreamSettings
|
||||||
|
source={screens.find(s => s.id === selected)!}
|
||||||
|
settings={settings}
|
||||||
|
setSettings={setSettings}
|
||||||
|
skipPicker={skipPicker}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modals.ModalContent>
|
||||||
|
<Modals.ModalFooter className="vcd-screen-picker-footer">
|
||||||
|
<Button
|
||||||
|
disabled={!selected}
|
||||||
|
onClick={() => {
|
||||||
|
currentSettings = settings;
|
||||||
|
try {
|
||||||
|
const frameRate = Number(settings.fps);
|
||||||
|
const height = Number(settings.resolution);
|
||||||
|
const width = Math.round(height * (16 / 9));
|
||||||
|
|
||||||
|
const conn = [...MediaEngineStore.getMediaEngine().connections].find(
|
||||||
|
connection => connection.streamUserId === UserStore.getCurrentUser().id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conn) {
|
||||||
|
conn.videoStreamParameters[0].maxFrameRate = frameRate;
|
||||||
|
conn.videoStreamParameters[0].maxResolution.height = height;
|
||||||
|
conn.videoStreamParameters[0].maxResolution.width = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
submit({
|
||||||
|
id: selected!,
|
||||||
|
...settings
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
const conn = [...MediaEngineStore.getMediaEngine().connections].find(
|
||||||
|
connection => connection.streamUserId === UserStore.getCurrentUser().id
|
||||||
|
);
|
||||||
|
if (!conn) return;
|
||||||
|
|
||||||
|
const track = conn.input.stream.getVideoTracks()[0];
|
||||||
|
|
||||||
|
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"
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await track.applyConstraints(constraints);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Applied constraints successfully. New constraints:",
|
||||||
|
track.getConstraints()
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to apply constraints.", e);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error while submitting stream.", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Go Live
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{selected && !skipPicker ? (
|
||||||
|
<Button color={Button.Colors.TRANSPARENT} onClick={() => setSelected(void 0)}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button color={Button.Colors.TRANSPARENT} onClick={close}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Modals.ModalFooter>
|
||||||
|
</Modals.ModalRoot>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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";
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<Switch
|
||||||
|
value={autoStartEnabled}
|
||||||
|
onChange={async v => {
|
||||||
|
await VesktopNative.autostart[v ? "enable" : "disable"]();
|
||||||
|
setAutoStartEnabled(v);
|
||||||
|
}}
|
||||||
|
note="Automatically start Vesktop on computer start-up"
|
||||||
|
>
|
||||||
|
Start With System
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
<Select
|
||||||
|
placeholder="Stable"
|
||||||
|
options={[
|
||||||
|
{ label: "Stable", value: "stable", default: true },
|
||||||
|
{ label: "Canary", value: "canary" },
|
||||||
|
{ label: "PTB", value: "ptb" }
|
||||||
|
]}
|
||||||
|
closeOnSelect={true}
|
||||||
|
select={v => (settings.discordBranch = v)}
|
||||||
|
isSelected={v => v === settings.discordBranch}
|
||||||
|
serialize={s => s}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 } from "@vencord/types/webpack/common";
|
||||||
|
import { setBadge } from "renderer/appBadge";
|
||||||
|
|
||||||
|
import { SettingsComponent } from "./Settings";
|
||||||
|
|
||||||
|
export const NotificationBadgeToggle: SettingsComponent = ({ settings }) => {
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
value={settings.appBadge ?? true}
|
||||||
|
onChange={v => {
|
||||||
|
settings.appBadge = v;
|
||||||
|
if (v) setBadge();
|
||||||
|
else VesktopNative.app.setBadgeCount(0);
|
||||||
|
}}
|
||||||
|
note="Show mention badge on the app icon"
|
||||||
|
>
|
||||||
|
Notification Badge
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,178 @@
|
||||||
|
/*
|
||||||
|
* 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.css";
|
||||||
|
|
||||||
|
import { Forms, Switch, Text } from "@vencord/types/webpack/common";
|
||||||
|
import { ComponentType } from "react";
|
||||||
|
import { Settings, useSettings } from "renderer/settings";
|
||||||
|
import { isMac, isWindows } from "renderer/utils";
|
||||||
|
|
||||||
|
import { AutoStartToggle } from "./AutoStartToggle";
|
||||||
|
import { DiscordBranchPicker } from "./DiscordBranchPicker";
|
||||||
|
import { NotificationBadgeToggle } from "./NotificationBadgeToggle";
|
||||||
|
import { VencordLocationPicker } from "./VencordLocationPicker";
|
||||||
|
import { WindowsTransparencyControls } from "./WindowsTransparencyControls";
|
||||||
|
|
||||||
|
interface BooleanSetting {
|
||||||
|
key: keyof typeof Settings.store;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
defaultValue: boolean;
|
||||||
|
disabled?(): boolean;
|
||||||
|
invisible?(): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SettingsComponent = ComponentType<{ settings: typeof Settings.store }>;
|
||||||
|
|
||||||
|
const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>> = {
|
||||||
|
"Discord Branch": [DiscordBranchPicker],
|
||||||
|
"System Startup & Performance": [
|
||||||
|
AutoStartToggle,
|
||||||
|
{
|
||||||
|
key: "hardwareAcceleration",
|
||||||
|
title: "Hardware Acceleration",
|
||||||
|
description: "Enable hardware acceleration",
|
||||||
|
defaultValue: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"User Interface": [
|
||||||
|
{
|
||||||
|
key: "customTitleBar",
|
||||||
|
title: "Discord Titlebar",
|
||||||
|
description: "Use Discord's custom title bar instead of the native system one. Requires a full restart.",
|
||||||
|
defaultValue: isWindows
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "staticTitle",
|
||||||
|
title: "Static Title",
|
||||||
|
description: 'Makes the window title "Vesktop" instead of changing to the current page',
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "enableMenu",
|
||||||
|
title: "Enable Menu Bar",
|
||||||
|
description: "Enables the application menu bar. Press ALT to toggle visibility.",
|
||||||
|
defaultValue: false,
|
||||||
|
disabled: () => Settings.store.customTitleBar ?? isWindows
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "splashTheming",
|
||||||
|
title: "Splash theming",
|
||||||
|
description: "Adapt the splash window colors to your custom theme",
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
WindowsTransparencyControls
|
||||||
|
],
|
||||||
|
Behaviour: [
|
||||||
|
{
|
||||||
|
key: "tray",
|
||||||
|
title: "Tray Icon",
|
||||||
|
description: "Add a tray icon for Vesktop",
|
||||||
|
defaultValue: true,
|
||||||
|
invisible: () => isMac
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "minimizeToTray",
|
||||||
|
title: "Minimize to tray",
|
||||||
|
description: "Hitting X will make Vesktop minimize to the tray instead of closing",
|
||||||
|
defaultValue: true,
|
||||||
|
invisible: () => isMac,
|
||||||
|
disabled: () => Settings.store.tray === false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "clickTrayToShowHide",
|
||||||
|
title: "Hide/Show on tray click",
|
||||||
|
description: "Left clicking tray icon will toggle the vesktop window visibility.",
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "disableMinSize",
|
||||||
|
title: "Disable minimum window size",
|
||||||
|
description: "Allows you to make the window as small as your heart desires",
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "disableSmoothScroll",
|
||||||
|
title: "Disable smooth scrolling",
|
||||||
|
description: "Disables smooth scrolling",
|
||||||
|
defaultValue: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Notifications & Updates": [
|
||||||
|
NotificationBadgeToggle,
|
||||||
|
{
|
||||||
|
key: "checkUpdates",
|
||||||
|
title: "Check for updates",
|
||||||
|
description: "Automatically check for Vesktop updates",
|
||||||
|
defaultValue: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Miscellaneous: [
|
||||||
|
{
|
||||||
|
key: "arRPC",
|
||||||
|
title: "Rich Presence",
|
||||||
|
description: "Enables Rich Presence via arRPC",
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: "openLinksWithElectron",
|
||||||
|
title: "Open Links in app (experimental)",
|
||||||
|
description: "Opens links in a new Vesktop window instead of your web browser",
|
||||||
|
defaultValue: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Vencord Location": [VencordLocationPicker]
|
||||||
|
};
|
||||||
|
|
||||||
|
function SettingsSections() {
|
||||||
|
const Settings = useSettings();
|
||||||
|
|
||||||
|
const sections = Object.entries(SettingsOptions).map(([title, settings]) => (
|
||||||
|
<Forms.FormSection
|
||||||
|
title={title}
|
||||||
|
key={title}
|
||||||
|
className="vcd-settings-section"
|
||||||
|
titleClassName="vcd-settings-title"
|
||||||
|
>
|
||||||
|
{settings.map(Setting => {
|
||||||
|
if (typeof Setting === "function") return <Setting settings={Settings} />;
|
||||||
|
|
||||||
|
const { defaultValue, title, description, key, disabled, invisible } = Setting;
|
||||||
|
if (invisible?.()) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
value={Settings[key as any] ?? defaultValue}
|
||||||
|
onChange={v => (Settings[key as any] = v)}
|
||||||
|
note={description}
|
||||||
|
disabled={disabled?.()}
|
||||||
|
key={key}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Forms.FormSection>
|
||||||
|
));
|
||||||
|
|
||||||
|
return <>{sections}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsUi() {
|
||||||
|
return (
|
||||||
|
<Forms.FormSection>
|
||||||
|
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">
|
||||||
|
Vesktop Settings
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<SettingsSections />
|
||||||
|
</Forms.FormSection>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 { useForceUpdater } from "@vencord/types/utils";
|
||||||
|
import { Button, Forms, Toasts } from "@vencord/types/webpack/common";
|
||||||
|
|
||||||
|
import { SettingsComponent } from "./Settings";
|
||||||
|
|
||||||
|
export const VencordLocationPicker: SettingsComponent = ({ settings }) => {
|
||||||
|
const forceUpdate = useForceUpdater();
|
||||||
|
const vencordDir = VesktopNative.fileManager.getVencordDir();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormText>
|
||||||
|
Vencord files are loaded from{" "}
|
||||||
|
{vencordDir ? (
|
||||||
|
<a
|
||||||
|
href="about:blank"
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
VesktopNative.fileManager.showItemInFolder(vencordDir!);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{vencordDir}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"the default location"
|
||||||
|
)}
|
||||||
|
</Forms.FormText>
|
||||||
|
<div className="vcd-location-btns">
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
onClick={async () => {
|
||||||
|
const choice = await VesktopNative.fileManager.selectVencordDir();
|
||||||
|
switch (choice) {
|
||||||
|
case "cancelled":
|
||||||
|
break;
|
||||||
|
case "ok":
|
||||||
|
Toasts.show({
|
||||||
|
message: "Vencord install changed. Fully restart Vesktop to apply.",
|
||||||
|
id: Toasts.genId(),
|
||||||
|
type: Toasts.Type.SUCCESS
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "invalid":
|
||||||
|
Toasts.show({
|
||||||
|
message:
|
||||||
|
"You did not choose a valid Vencord install. Make sure you're selecting the dist dir!",
|
||||||
|
id: Toasts.genId(),
|
||||||
|
type: Toasts.Type.FAILURE
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
forceUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size={Button.Sizes.SMALL}
|
||||||
|
color={Button.Colors.RED}
|
||||||
|
onClick={async () => {
|
||||||
|
await VesktopNative.fileManager.selectVencordDir(null);
|
||||||
|
forceUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* 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 { Margins } from "@vencord/types/utils";
|
||||||
|
import { Forms, Select } from "@vencord/types/webpack/common";
|
||||||
|
|
||||||
|
import { SettingsComponent } from "./Settings";
|
||||||
|
|
||||||
|
export const WindowsTransparencyControls: SettingsComponent = ({ settings }) => {
|
||||||
|
if (!VesktopNative.app.supportsWindowsTransparency()) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormTitle className={Margins.top16 + " " + Margins.bottom8}>Transparency Options</Forms.FormTitle>
|
||||||
|
<Forms.FormText className={Margins.bottom8}>
|
||||||
|
Requires a full restart. You will need a theme that supports transparency for this to work.
|
||||||
|
</Forms.FormText>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
placeholder="None"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "None",
|
||||||
|
value: "none",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Mica (incorporates system theme + desktop wallpaper to paint the background)",
|
||||||
|
value: "mica"
|
||||||
|
},
|
||||||
|
{ label: "Tabbed (variant of Mica with stronger background tinting)", value: "tabbed" },
|
||||||
|
{
|
||||||
|
label: "Acrylic (blurs the window behind Vesktop for a translucent background)",
|
||||||
|
value: "acrylic"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
closeOnSelect={true}
|
||||||
|
select={v => (settings.transparencyOption = v)}
|
||||||
|
isSelected={v => v === settings.transparencyOption}
|
||||||
|
serialize={s => s}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -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<string, unknown>) => 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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -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";
|
||||||
|
}
|
||||||
|
});
|
|
@ -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");
|
||||||
|
}
|
||||||
|
});
|
|
@ -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";
|
|
@ -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";
|
||||||
|
}
|
||||||
|
});
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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<Patch, "plugin">[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPatch<P extends PatchData>(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);
|
||||||
|
}
|
|
@ -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,
|
||||||
|
<Menu.MenuGroup>
|
||||||
|
{hasCorrections && (
|
||||||
|
<>
|
||||||
|
{corrections.map(c => (
|
||||||
|
<Menu.MenuItem
|
||||||
|
id={"vcd-spellcheck-suggestion-" + c}
|
||||||
|
label={c}
|
||||||
|
action={() => VesktopNative.spellcheck.replaceMisspelling(c)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Menu.MenuSeparator />
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vcd-spellcheck-learn"
|
||||||
|
label={`Add ${word} to dictionary`}
|
||||||
|
action={() => VesktopNative.spellcheck.addToDictionary(word)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Menu.MenuItem id="vcd-spellcheck-settings" label="Spellcheck Settings">
|
||||||
|
<Menu.MenuCheckboxItem
|
||||||
|
id="vcd-spellcheck-enabled"
|
||||||
|
label="Enable Spellcheck"
|
||||||
|
checked={spellCheckEnabled}
|
||||||
|
action={() => {
|
||||||
|
FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Menu.MenuItem id="vcd-spellcheck-languages" label="Languages" disabled={!spellCheckEnabled}>
|
||||||
|
{availableLanguages.map(lang => {
|
||||||
|
const isEnabled = spellCheckLanguages.includes(lang);
|
||||||
|
return (
|
||||||
|
<Menu.MenuCheckboxItem
|
||||||
|
id={"vcd-spellcheck-lang-" + lang}
|
||||||
|
label={lang}
|
||||||
|
checked={isEnabled}
|
||||||
|
disabled={!isEnabled && spellCheckLanguages.length >= 5}
|
||||||
|
action={() => {
|
||||||
|
const newSpellCheckLanguages = spellCheckLanguages.filter(l => l !== lang);
|
||||||
|
if (newSpellCheckLanguages.length === spellCheckLanguages.length) {
|
||||||
|
newSpellCheckLanguages.push(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.spellCheckLanguages = newSpellCheckLanguages;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu.MenuItem>
|
||||||
|
</Menu.MenuItem>
|
||||||
|
</Menu.MenuGroup>
|
||||||
|
);
|
||||||
|
});
|
|
@ -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}`
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -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)
|
||||||
|
};
|
||||||
|
}
|
|
@ -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);
|
|
@ -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");
|
|
@ -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"
|
||||||
|
}
|
|
@ -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
|
||||||
|
};
|
|
@ -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");
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<T, P> = P extends `${infer Pre}.${infer Suf}`
|
||||||
|
? Pre extends keyof T
|
||||||
|
? ResolvePropDeep<T[Pre], Suf>
|
||||||
|
: 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<T extends object> {
|
||||||
|
private pathListeners = new Map<string, Set<(newData: any) => 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<P extends LiteralUnion<keyof T, string>>(
|
||||||
|
path: P,
|
||||||
|
cb: (data: ResolvePropDeep<T, P>) => 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<keyof T, string>, 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, ""));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T extends Function>(func: T, delay = 300): T {
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
return function (...args: any[]) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
func(...args);
|
||||||
|
}, delay);
|
||||||
|
} as any;
|
||||||
|
}
|
|
@ -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<T>(item: T): item is Exclude<T, 0 | "" | false | null | undefined> {
|
||||||
|
return Boolean(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNonNullish<T>(item: T): item is Exclude<T, null | undefined> {
|
||||||
|
return item != null;
|
||||||
|
}
|
|
@ -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<T extends Function>(fn: T): T {
|
||||||
|
let called = false;
|
||||||
|
return function (this: any, ...args: any[]) {
|
||||||
|
if (called) return;
|
||||||
|
called = true;
|
||||||
|
return fn.apply(this, args);
|
||||||
|
} as any;
|
||||||
|
}
|
|
@ -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<void> {
|
||||||
|
return new Promise(r => setTimeout(r, ms));
|
||||||
|
}
|
|
@ -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"));
|
||||||
|
}
|
|
@ -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<UpdateData>(IpcEvents.UPDATER_GET_DATA),
|
||||||
|
download: () => {
|
||||||
|
invoke<void>(IpcEvents.UPDATER_DOWNLOAD);
|
||||||
|
invoke<void>(IpcEvents.CLOSE);
|
||||||
|
},
|
||||||
|
ignore: () => invoke<void>(IpcEvents.UPDATE_IGNORE),
|
||||||
|
close: () => invoke<void>(IpcEvents.CLOSE)
|
||||||
|
});
|
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 201 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 1.2 MiB |
|
@ -0,0 +1,56 @@
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="./style.css" type="text/css" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1 id="title">Aerocord</h1>
|
||||||
|
<p>
|
||||||
|
Aerocord is a Vesktop fork made to work with Windows Vista/7/8.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Aerocord Gitea:</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://git.randomserver.top/administrator/aerocord" target="_blank">Aerocord Source Code</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Credits to the original Vesktop developer:</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://vencord.dev" target="_blank">Vencord Website</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/Vencord/Vesktop" target="_blank">Vesktop Source Code</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<div>
|
||||||
|
<a href="https://git.randomserver.top/administrator/aerocord/src/branch/main/documentation.md"
|
||||||
|
target="_blank">Documentation can be found here</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="https://git.randomserver.top/administrator/aerocord/src/branch/main/build.md" target="_blank">Building
|
||||||
|
instructions can be found here</a>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
const data = await Updater.getData();
|
||||||
|
if (data.currentVersion) {
|
||||||
|
const title = document.getElementById("title");
|
||||||
|
|
||||||
|
title.textContent += ` v${data.currentVersion}`;
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,168 @@
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="./style.css" type="text/css" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
padding: 1.5em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
|
||||||
|
border: 1px solid var(--fg-semi-trans);
|
||||||
|
border-top: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
padding: 0.3em;
|
||||||
|
margin: -0.3em;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0.4em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1em 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:has(input[type="checkbox"]),
|
||||||
|
select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:not(:last-child)::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--fg-secondary);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
label div {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label span {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--fg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.6em;
|
||||||
|
background: red;
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 200ms filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
filter: brightness(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit {
|
||||||
|
background: green;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Welcome to Aerocord - a Vesktop fork meant for Windows Vista, 7 and 8</h1>
|
||||||
|
<p>Let's customise your experience!</p>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<label>
|
||||||
|
<h2>Discord Branch</h2>
|
||||||
|
<select name="discordBranch">
|
||||||
|
<option value="stable">stable</option>
|
||||||
|
<option value="canary">canary</option>
|
||||||
|
<option value="ptb">ptb</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<div>
|
||||||
|
<h2>Start with System</h2>
|
||||||
|
<span>Automatically open Aerocord when your computer starts</span>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" name="autoStart" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<div>
|
||||||
|
<h2>Rich Presence</h2>
|
||||||
|
<span>Enable Rich presence (game activity) via
|
||||||
|
<a href="https://github.com/OpenAsar/arrpc" target="_blank">arRPC</a></span>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" name="richPresence" checked />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<div>
|
||||||
|
<h2>Import Settings</h2>
|
||||||
|
<span>Import Settings from existing Vencord install (if found)</span>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" name="importSettings" checked />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<div>
|
||||||
|
<h2>Minimise to Tray</h2>
|
||||||
|
<span>Minimise to Tray when closing</span>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" name="minimizeToTray" checked />
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
<div id="buttons">
|
||||||
|
<button id="cancel">Quit</button>
|
||||||
|
<button id="submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
cancel.onclick = () => console.info("cancel");
|
||||||
|
submit.onclick = e => {
|
||||||
|
const form = document.querySelector("form");
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
console.info("form:" + JSON.stringify(data));
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="./style.css" type="text/css" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--fg-semi-trans);
|
||||||
|
color: rgb(255, 238, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
text-align: center;
|
||||||
|
color: rgb(78, 78, 78)
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<img draggable="false" src="../shiggy.gif" alt="Windows 7 jumpscare" role="presentation" />
|
||||||
|
<p>Loading Aerocord...</p>
|
||||||
|
<h6>Made by the RandomServer Community!</h6>
|
||||||
|
</div>
|
||||||
|
</body>
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="./style.css" type="text/css" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5em;
|
||||||
|
color: var(--fg);
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: filter 0.2 ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
button:active {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.green {
|
||||||
|
background-color: #248046;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
background-color: #ed4245;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<section>
|
||||||
|
<h1>Update Available</h1>
|
||||||
|
<p>There's a new update for Aerocord! Update now to get new fixes and features!</p>
|
||||||
|
<p>
|
||||||
|
Current: <span id="current"></span>
|
||||||
|
<br />
|
||||||
|
Latest: <span id="latest"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Changelog</h2>
|
||||||
|
<p id="changelog">Loading...</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<label id="disable-remind">
|
||||||
|
<input type="checkbox" />
|
||||||
|
<span>Do not remind again for </span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button name="download" class="green">Download Update</button>
|
||||||
|
<button name="close" class="red">Close</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
const data = await Updater.getData();
|
||||||
|
document.getElementById("current").textContent = data.currentVersion;
|
||||||
|
document.getElementById("latest").textContent = data.latestVersion;
|
||||||
|
|
||||||
|
document.querySelector("#disable-remind > span").textContent += data.latestVersion;
|
||||||
|
|
||||||
|
function checkDisableRemind() {
|
||||||
|
const checkbox = document.querySelector("#disable-remind > input");
|
||||||
|
if (checkbox.checked) {
|
||||||
|
Updater.ignore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClicks = {
|
||||||
|
download() {
|
||||||
|
checkDisableRemind();
|
||||||
|
Updater.download();
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
checkDisableRemind();
|
||||||
|
Updater.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const name in onClicks) {
|
||||||
|
document.querySelectorAll(`button[name="${name}"]`).forEach(button => {
|
||||||
|
button.addEventListener("click", onClicks[name]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import { micromark } from "https://esm.sh/micromark@3?bundle";
|
||||||
|
import { gfm, gfmHtml } from "https://esm.sh/micromark-extension-gfm@2?bundle";
|
||||||
|
|
||||||
|
const changelog = (await Updater.getData()).release.body;
|
||||||
|
if (changelog)
|
||||||
|
document.getElementById("changelog").innerHTML = micromark(changelog, {
|
||||||
|
extensions: [gfm()],
|
||||||
|
htmlExtensions: [gfmHtml()]
|
||||||
|
})
|
||||||
|
.replace(/h1>/g, "h3>")
|
||||||
|
.replace(/<a /g, '<a target="_blank" ');
|
||||||
|
</script>
|