add upstream changes

This commit is contained in:
Sammie Zhang 2024-05-08 10:34:24 -07:00
parent 7e733bf806
commit 804858fa55
No known key found for this signature in database
GPG key ID: E8BCC9DD9CE177C6
34 changed files with 1070 additions and 247 deletions

View file

@ -26,10 +26,10 @@ If you don't know the difference, pick the Installer.
### Mac ### Mac
If you don't know the difference, pick amd64 If you don't know the difference, pick the Intel build.
- [amd64 / x86_64](https://vencord.dev/download/vesktop/amd64/dmg) - [Intel build (amd64)](https://vencord.dev/download/vesktop/amd64/dmg)
- [arm64 / aarch64](https://vencord.dev/download/vesktop/arm64/dmg) - [Apple Silicon (arm64)](https://vencord.dev/download/vesktop/arm64/dmg)
### Linux ### Linux
@ -54,6 +54,7 @@ Below you can find unofficial packages created by the community. They are not of
- Arch Linux: [Vesktop on the Arch user repository](https://aur.archlinux.org/packages?K=vesktop) - Arch Linux: [Vesktop on the Arch user repository](https://aur.archlinux.org/packages?K=vesktop)
- NixOS: https://nixos.wiki/wiki/Discord#Vesktop - NixOS: https://nixos.wiki/wiki/Discord#Vesktop
- Windows - Scoop: https://scoop.sh/#/apps?q=Vesktop
## Building from Source ## Building from Source

View file

@ -1,6 +1,6 @@
{ {
"name": "vesktop", "name": "vesktop",
"version": "1.5.0", "version": "1.5.2",
"private": true, "private": true,
"description": "", "description": "",
"keywords": [], "keywords": [],
@ -24,36 +24,36 @@
"updateMeta": "tsx scripts/utils/updateMeta.mts" "updateMeta": "tsx scripts/utils/updateMeta.mts"
}, },
"dependencies": { "dependencies": {
"arrpc": "github:OpenAsar/arrpc#98879cae0565e6fce34e4cb6f544bf42c6a7e7c8" "arrpc": "github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c"
}, },
"optionalDependencies": { "optionalDependencies": {
"@vencord/venmic": "^3.3.2" "@vencord/venmic": "^3.4.2"
}, },
"devDependencies": { "devDependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@types/node": "^20.11.2", "@types/node": "^20.11.26",
"@types/react": "^18.2.48", "@types/react": "^18.2.65",
"@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^6.19.0", "@typescript-eslint/parser": "^7.2.0",
"@vencord/types": "^0.1.2", "@vencord/types": "^0.1.2",
"dotenv": "^16.3.1", "dotenv": "^16.4.5",
"electron": "^28.1.3", "electron": "^29.1.1",
"electron-builder": "^24.9.1", "electron-builder": "^24.13.3",
"esbuild": "^0.19.11", "esbuild": "^0.20.1",
"eslint": "^8.56.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-license-header": "^0.6.0", "eslint-plugin-license-header": "^0.6.0",
"eslint-plugin-path-alias": "^1.0.0", "eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-unused-imports": "^3.0.0", "eslint-plugin-unused-imports": "^3.1.0",
"prettier": "^3.2.2", "prettier": "^3.2.5",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"tsx": "^4.7.0", "tsx": "^4.7.1",
"type-fest": "^4.9.0", "type-fest": "^4.12.0",
"typescript": "^5.3.3", "typescript": "^5.4.2",
"xml-formatter": "^3.6.0" "xml-formatter": "^3.6.2"
}, },
"packageManager": "pnpm@8.11.0", "packageManager": "pnpm@8.11.0",
"engines": { "engines": {
@ -131,6 +131,26 @@
"com.apple.security.device.camera": true "com.apple.security.device.camera": true
} }
}, },
"dmg": {
"background": "build/background.tiff",
"icon": "build/icon.icns",
"iconSize": 105,
"window": {
"width": 512,
"height": 340
},
"contents": [{
"x": 140,
"y": 160
},
{
"x": 372,
"y": 160,
"type": "link",
"path": "/Applications"
}]
},
"nsis": { "nsis": {
"include": "build/installer.nsh", "include": "build/installer.nsh",
"oneClick": false "oneClick": false

View file

@ -8,4 +8,4 @@ import "./utils/dotenv";
import { spawnNodeModuleBin } from "./utils/spawn.mjs"; import { spawnNodeModuleBin } from "./utils/spawn.mjs";
spawnNodeModuleBin("electron", [".", ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]); spawnNodeModuleBin("electron", [process.cwd(), ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]);

View file

@ -5,7 +5,7 @@
*/ */
import { app } from "electron"; import { app } from "electron";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"; import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
import { join } from "path"; import { join } from "path";
interface AutoStart { interface AutoStart {
@ -17,7 +17,16 @@ interface AutoStart {
function makeAutoStartLinux(): AutoStart { function makeAutoStartLinux(): AutoStart {
const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config"); const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config");
const dir = join(configDir, "autostart"); const dir = join(configDir, "autostart");
const file = join(dir, "vencord.desktop"); 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 { return {
isEnabled: () => existsSync(file), isEnabled: () => existsSync(file),
@ -25,12 +34,11 @@ function makeAutoStartLinux(): AutoStart {
const desktopFile = ` const desktopFile = `
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Version=1.0 Name=Vesktop
Name=Vencord Comment=Vesktop autostart script
Comment=Vencord autostart script Exec=${commandLine}
Exec=${process.execPath}
Terminal=false
StartupNotify=false StartupNotify=false
Terminal=false
`.trim(); `.trim();
mkdirSync(dir, { recursive: true }); mkdirSync(dir, { recursive: true });

View file

@ -48,14 +48,14 @@ export const DEFAULT_HEIGHT = 720;
export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"]; export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"];
const UserAgents = { const BrowserUserAgents = {
darwin: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", darwin: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
windows: windows:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
}; };
export const UserAgent = UserAgents[process.platform] || UserAgents.windows; export const BrowserUserAgent = BrowserUserAgents[process.platform] || BrowserUserAgents.windows;
export const enum MessageBoxChoice { export const enum MessageBoxChoice {
Default, Default,

View file

@ -27,23 +27,35 @@ process.env.VENCORD_USER_DATA_DIR = DATA_DIR;
function init() { function init() {
const { disableSmoothScroll, hardwareAcceleration, splashAnimationPath } = Settings.store; const { disableSmoothScroll, hardwareAcceleration, splashAnimationPath } = Settings.store;
if (hardwareAcceleration === false) app.disableHardwareAcceleration(); 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) { if (disableSmoothScroll) {
app.commandLine.appendSwitch("disable-smooth-scrolling"); app.commandLine.appendSwitch("disable-smooth-scrolling");
} }
// work around chrome 66 disabling autoplay by default // work around chrome 66 disabling autoplay by default
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
// WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows. // WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows.
// HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service. // HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service.
// //
// WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790 // WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790
app.commandLine.appendSwitch( disabledFeatures.push(
"disable-features", "WinRetrieveSuggestionsOnlyOnDemand",
"WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService,WidgetLayering" "HardwareMediaKeyHandling",
"MediaSessionService",
"WidgetLayering"
); );
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 // 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"; if (isDeckGameMode) nativeTheme.themeSource = "dark";
@ -62,7 +74,6 @@ function init() {
registerScreenShareHandler(); registerScreenShareHandler();
registerMediaPermissionsHandler(); registerMediaPermissionsHandler();
//register file handler so we can load the custom splash animation from the user's filesystem //register file handler so we can load the custom splash animation from the user's filesystem
protocol.handle("splash-animation", () => { protocol.handle("splash-animation", () => {
return net.fetch("file:///"+splashAnimationPath); return net.fetch("file:///"+splashAnimationPath);

View file

@ -25,13 +25,13 @@ import { ICON_PATH } from "../shared/paths";
import { createAboutWindow } from "./about"; import { createAboutWindow } from "./about";
import { initArRPC } from "./arrpc"; import { initArRPC } from "./arrpc";
import { import {
BrowserUserAgent,
DATA_DIR, DATA_DIR,
DEFAULT_HEIGHT, DEFAULT_HEIGHT,
DEFAULT_WIDTH, DEFAULT_WIDTH,
MessageBoxChoice, MessageBoxChoice,
MIN_HEIGHT, MIN_HEIGHT,
MIN_WIDTH, MIN_WIDTH,
UserAgent,
VENCORD_FILES_DIR VENCORD_FILES_DIR
} from "./constants"; } from "./constants";
import { Settings, State, VencordSettings } from "./settings"; import { Settings, State, VencordSettings } from "./settings";
@ -73,6 +73,10 @@ const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpe
const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings); const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings);
function initTray(win: BrowserWindow) { function initTray(win: BrowserWindow) {
const onTrayClick = () => {
if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide();
else win.show();
};
const trayMenu = Menu.buildFromTemplate([ const trayMenu = Menu.buildFromTemplate([
{ {
label: "Open", label: "Open",
@ -120,7 +124,7 @@ function initTray(win: BrowserWindow) {
tray = new Tray(ICON_PATH); tray = new Tray(ICON_PATH);
tray.setToolTip("Vesktop"); tray.setToolTip("Vesktop");
tray.setContextMenu(trayMenu); tray.setContextMenu(trayMenu);
tray.on("click", () => win.show()); tray.on("click", onTrayClick);
} }
async function clearData(win: BrowserWindow) { async function clearData(win: BrowserWindow) {
@ -426,7 +430,7 @@ function createMainWindow() {
initSettingsListeners(win); initSettingsListeners(win);
initSpellCheck(win); initSpellCheck(win);
win.webContents.setUserAgent(UserAgent); win.webContents.setUserAgent(BrowserUserAgent);
const subdomain = const subdomain =
Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb" Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb"

View file

@ -5,41 +5,54 @@
*/ */
import { createWriteStream } from "fs"; import { createWriteStream } from "fs";
import type { IncomingMessage } from "http"; import { Readable } from "stream";
import { get, RequestOptions } from "https"; import { pipeline } from "stream/promises";
import { finished } from "stream/promises"; import { setTimeout } from "timers/promises";
export async function downloadFile(url: string, file: string, options: RequestOptions = {}) { interface FetchieOptions {
const res = await simpleReq(url, options); retryOnNetworkError?: boolean;
await finished( }
res.pipe(
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, { createWriteStream(file, {
autoClose: true autoClose: true
}) })
)
); );
} }
export function simpleReq(url: string, options: RequestOptions = {}) { const ONE_MINUTE_MS = 1000 * 60;
return new Promise<IncomingMessage>((resolve, reject) => {
get(url, options, res => {
const { statusCode, statusMessage, headers } = res;
if (statusCode! >= 400) return void reject(`${statusCode}: ${statusMessage} - ${url}`);
if (statusCode! >= 300) return simpleReq(headers.location!, options).then(resolve).catch(reject);
resolve(res); export async function fetchie(url: string, options?: RequestInit, { retryOnNetworkError }: FetchieOptions = {}) {
}); let res: Response | undefined;
});
} try {
res = await fetch(url, options);
export async function simpleGet(url: string, options: RequestOptions = {}) { } catch (err) {
const res = await simpleReq(url, options); if (retryOnNetworkError) {
console.error("Failed to fetch", url + ".", "Gonna retry with backoff.");
return new Promise<Buffer>((resolve, reject) => {
const chunks = [] as Buffer[]; for (let tries = 0, delayMs = 500; tries < 20; tries++, delayMs = Math.min(2 * delayMs, ONE_MINUTE_MS)) {
await setTimeout(delayMs);
res.once("error", reject); try {
res.on("data", chunk => chunks.push(chunk)); res = await fetch(url, options);
res.once("end", () => resolve(Buffer.concat(chunks))); 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);
} }

View file

@ -5,11 +5,10 @@
*/ */
import { existsSync, mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
import type { RequestOptions } from "https";
import { join } from "path"; import { join } from "path";
import { USER_AGENT, VENCORD_FILES_DIR } from "../constants"; import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
import { downloadFile, simpleGet } from "./http"; import { downloadFile, fetchie } from "./http";
const API_BASE = "https://api.github.com"; const API_BASE = "https://api.github.com";
@ -31,27 +30,29 @@ export interface ReleaseData {
} }
export async function githubGet(endpoint: string) { export async function githubGet(endpoint: string) {
const opts: RequestOptions = { const opts: RequestInit = {
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
"User-Agent": USER_AGENT "User-Agent": USER_AGENT
} }
}; };
if (process.env.GITHUB_TOKEN) opts.headers!.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; if (process.env.GITHUB_TOKEN) (opts.headers! as any).Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
return simpleGet(API_BASE + endpoint, opts); return fetchie(API_BASE + endpoint, opts, { retryOnNetworkError: true });
} }
export async function downloadVencordFiles() { export async function downloadVencordFiles() {
const release = await githubGet("/repos/Vendicated/Vencord/releases/latest"); const release = await githubGet("/repos/Vendicated/Vencord/releases/latest");
const { assets } = JSON.parse(release.toString("utf-8")) as ReleaseData; const { assets }: ReleaseData = await release.json();
await Promise.all( await Promise.all(
assets assets
.filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f))) .filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f)))
.map(({ name, browser_download_url }) => downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name))) .map(({ name, browser_download_url }) =>
downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name), {}, { retryOnNetworkError: true })
)
); );
} }

View file

@ -4,17 +4,58 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import type { PatchBay } from "@vencord/venmic"; import type { PatchBay as PatchBayType } from "@vencord/venmic";
import { app, ipcMain } from "electron"; import { app, ipcMain } from "electron";
import { join } from "path"; import { join } from "path";
import { IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths"; import { STATIC_DIR } from "shared/paths";
type LinkData = Parameters<PatchBay["link"]>[0]; type LinkData = Parameters<PatchBayType["link"]>[0];
let PatchBay: typeof PatchBayType | undefined;
let patchBayInstance: PatchBayType | undefined;
let imported = false;
let initialized = false; let initialized = false;
let patchBay: import("@vencord/venmic").PatchBay | undefined;
let isGlibcxxToOld = 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() { function getRendererAudioServicePid() {
return ( return (
@ -25,33 +66,17 @@ function getRendererAudioServicePid() {
); );
} }
function obtainVenmic() {
if (!initialized) {
initialized = true;
try {
const { PatchBay } = require(
join(STATIC_DIR, `dist/venmic-${process.arch}.node`)
) as typeof import("@vencord/venmic");
patchBay = new PatchBay();
} catch (e: any) {
console.error("Failed to initialise venmic. Make sure you're using pipewire", e);
isGlibcxxToOld = (e?.stack || e?.message || "").toLowerCase().includes("glibc");
}
}
return patchBay;
}
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => { ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
const audioPid = getRendererAudioServicePid(); const audioPid = getRendererAudioServicePid();
const list = obtainVenmic() const list = obtainVenmic()
?.list() ?.list()
.filter(s => s["application.process.id"] !== audioPid) .filter(s => s["application.process.id"] !== audioPid)
.map(s => s["application.name"]); .map(s => s["application.name"]);
return list const uniqueTargets = [...new Set(list)];
? { ok: true, targets: [...new Set(list)] } // Remove duplicates
: { ok: false, isGlibcxxToOld }; return list ? { ok: true, targets: uniqueTargets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated };
}); });
ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[], workaround?: boolean) => { ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[], workaround?: boolean) => {
@ -72,11 +97,12 @@ ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[], workaround?: boo
return obtainVenmic()?.link(data); return obtainVenmic()?.link(data);
}); });
ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, workaround?: boolean) => { ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, workaround?: boolean, onlyDefaultSpeakers?: boolean) => {
const pid = getRendererAudioServicePid(); const pid = getRendererAudioServicePid();
const data: LinkData = { const data: LinkData = {
exclude: [{ key: "application.process.id", value: pid }] exclude: [{ key: "application.process.id", value: pid }],
only_default_speakers: onlyDefaultSpeakers
}; };
if (workaround) { if (workaround) {

View file

@ -63,9 +63,12 @@ export const VesktopNative = {
/** only available on Linux. */ /** only available on Linux. */
virtmic: { virtmic: {
list: () => list: () =>
invoke<{ ok: false; isGlibcxxToOld: boolean } | { ok: true; targets: string[] }>(IpcEvents.VIRT_MIC_LIST), invoke<
{ ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: string[]; hasPipewirePulse: boolean }
>(IpcEvents.VIRT_MIC_LIST),
start: (targets: string[], workaround?: boolean) => invoke<void>(IpcEvents.VIRT_MIC_START, targets, workaround), start: (targets: string[], workaround?: boolean) => invoke<void>(IpcEvents.VIRT_MIC_START, targets, workaround),
startSystem: (workaround?: boolean) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, workaround), startSystem: (workaround?: boolean, onlyDefaultSpeakers?: boolean) =>
invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, workaround, onlyDefaultSpeakers),
stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP) stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP)
}, },
arrpc: { arrpc: {

View file

@ -6,7 +6,7 @@
import "./screenSharePicker.css"; import "./screenSharePicker.css";
import { closeModal, Margins, Modals, openModal, useAwaiter } from "@vencord/types/utils"; import { closeModal, Logger, Margins, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils";
import { findStoreLazy, onceReady } from "@vencord/types/webpack"; import { findStoreLazy, onceReady } from "@vencord/types/webpack";
import { import {
Button, Button,
@ -36,7 +36,9 @@ interface StreamSettings {
fps: StreamFps; fps: StreamFps;
audio: boolean; audio: boolean;
audioSource?: string; audioSource?: string;
contentHint?: string;
workaround?: boolean; workaround?: boolean;
onlyDefaultSpeakers?: boolean;
} }
export interface StreamPick extends StreamSettings { export interface StreamPick extends StreamSettings {
@ -49,7 +51,9 @@ interface Source {
url: string; url: string;
} }
let currentSettings: StreamSettings | null = null; export let currentSettings: StreamSettings | null = null;
const logger = new Logger("VesktopScreenShare");
addPatch({ addPatch({
patches: [ patches: [
@ -59,6 +63,20 @@ addPatch({
match: /this.localWant=/, match: /this.localWant=/,
replace: "$self.patchStreamQuality(this);$&" replace: "$self.patchStreamQuality(this);$&"
} }
},
{
find: "x-google-max-bitrate",
replacement: [
{
// eslint-disable-next-line no-useless-escape
match: /"x-google-max-bitrate=".concat\(\i\)/,
replace: '"x-google-max-bitrate=".concat("80_000")'
},
{
match: /;level-asymmetry-allowed=1/,
replace: ";b=AS:800000;level-asymmetry-allowed=1"
}
]
} }
], ],
patchStreamQuality(opts: any) { patchStreamQuality(opts: any) {
@ -73,6 +91,14 @@ addPatch({
bitrateMax: 8000000, bitrateMax: 8000000,
bitrateTarget: 600000 bitrateTarget: 600000
}); });
if (opts?.encode) {
Object.assign(opts.encode, {
framerate,
width,
height,
pixelCount: height * width
});
}
Object.assign(opts.capture, { Object.assign(opts.capture, {
framerate, framerate,
width, width,
@ -167,10 +193,15 @@ function StreamSettings({
); );
return ( return (
<div className={isLinux ? "vcd-screen-picker-settings-grid" : ""}>
<div> <div>
<Forms.FormTitle>What you're streaming</Forms.FormTitle> <Forms.FormTitle>What you're streaming</Forms.FormTitle>
<Card className="vcd-screen-picker-card vcd-screen-picker-preview"> <Card className="vcd-screen-picker-card vcd-screen-picker-preview">
<img src={thumb} alt="" /> <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> <Text variant="text-sm/normal">{source.name}</Text>
</Card> </Card>
@ -182,7 +213,10 @@ function StreamSettings({
<Forms.FormTitle>Resolution</Forms.FormTitle> <Forms.FormTitle>Resolution</Forms.FormTitle>
<div className="vcd-screen-picker-radios"> <div className="vcd-screen-picker-radios">
{StreamResolutions.map(res => ( {StreamResolutions.map(res => (
<label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}> <label
className="vcd-screen-picker-radio"
data-checked={settings.resolution === res}
>
<Text variant="text-sm/bold">{res}</Text> <Text variant="text-sm/bold">{res}</Text>
<input <input
type="radio" type="radio"
@ -214,7 +248,45 @@ function StreamSettings({
</div> </div>
</section> </section>
</div> </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 && ( {isWindows && (
<Switch <Switch
value={settings.audio} value={settings.audio}
@ -225,16 +297,23 @@ function StreamSettings({
Stream With Audio Stream With Audio
</Switch> </Switch>
)} )}
</section>
</div>
</Card>
</div>
<div>
{isLinux && ( {isLinux && (
<AudioSourcePickerLinux <AudioSourcePickerLinux
audioSource={settings.audioSource} audioSource={settings.audioSource}
workaround={settings.workaround} workaround={settings.workaround}
onlyDefaultSpeakers={settings.onlyDefaultSpeakers}
setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))} setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))}
setWorkaround={workaround => setSettings(s => ({ ...s, workaround: workaround }))} setWorkaround={value => setSettings(s => ({ ...s, workaround: value }))}
setOnlyDefaultSpeakers={value => setSettings(s => ({ ...s, onlyDefaultSpeakers: value }))}
/> />
)} )}
</Card> </div>
</div> </div>
); );
} }
@ -242,46 +321,69 @@ function StreamSettings({
function AudioSourcePickerLinux({ function AudioSourcePickerLinux({
audioSource, audioSource,
workaround, workaround,
onlyDefaultSpeakers,
setAudioSource, setAudioSource,
setWorkaround setWorkaround,
setOnlyDefaultSpeakers
}: { }: {
audioSource?: string; audioSource?: string;
workaround?: boolean; workaround?: boolean;
onlyDefaultSpeakers?: boolean;
setAudioSource(s: string): void; setAudioSource(s: string): void;
setWorkaround(b: boolean): void; setWorkaround(b: boolean): void;
setOnlyDefaultSpeakers(b: boolean): void;
}) { }) {
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
fallbackValue: { ok: true, targets: [] } fallbackValue: { ok: true, targets: [], hasPipewirePulse: true }
}); });
const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null; const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null;
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false);
return ( return (
<section> <>
<Forms.FormTitle>Audio</Forms.FormTitle> <Forms.FormTitle>Audio Settings</Forms.FormTitle>
{loading && <Forms.FormTitle>Loading Audio sources...</Forms.FormTitle>} <Card className="vcd-screen-picker-card">
{!sources.ok && {loading ? (
(sources.isGlibcxxToOld ? ( <Forms.FormTitle>Loading Audio Sources...</Forms.FormTitle>
) : (
<Forms.FormTitle>Audio Source</Forms.FormTitle>
)}
{!sources.ok && sources.isGlibCxxOutdated && (
<Forms.FormText> <Forms.FormText>
Failed to retrieve Audio Sources because your C++ library is too old to run venmic. If you would Failed to retrieve Audio Sources because your C++ library is too old to run
like to stream with Audio, see{" "} <a href="https://github.com/Vencord/venmic" target="_blank">
venmic
</a>
. See{" "}
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank"> <a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
this guide this guide
</a> </a>{" "}
for possible solutions.
</Forms.FormText> </Forms.FormText>
) : ( )}
<Forms.FormText>
Failed to retrieve Audio Sources. If you would like to stream with Audio, make sure you're using
Pipewire, not Pulseaudio
</Forms.FormText>
))}
{allSources && ( {hasPipewirePulse || ignorePulseWarning ? (
allSources && (
<Select <Select
options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))} options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))}
isSelected={s => s === audioSource} isSelected={s => s === audioSource}
select={setAudioSource} select={setAudioSource}
serialize={String} serialize={String}
/> />
)
) : (
<Text variant="text-sm/normal">
Could not find pipewire-pulse. This usually means that you do not run pipewire as your main
audio-server. <br />
You can still continue, however, please beware that you can only share audio of apps that are
running under pipewire.
<br />
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing</a>
</Text>
)} )}
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} /> <Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
@ -291,14 +393,30 @@ function AudioSourcePickerLinux({
value={workaround ?? false} value={workaround ?? false}
note={ note={
<> <>
Work around an issue that causes the microphone to be shared instead of the correct audio. Only Work around an issue that causes the microphone to be shared instead of the correct audio.
enable if you're experiencing this issue. Only enable if you're experiencing this issue.
</> </>
} }
> >
Microphone Workaround Microphone Workaround
</Switch> </Switch>
</section>
<Switch
hideBorder
onChange={setOnlyDefaultSpeakers}
disabled={audioSource !== "Entire System"}
value={onlyDefaultSpeakers ?? true}
note={
<>
When sharing entire desktop audio, only share apps that play to the default speakers and
ignore apps that play to other speakers or devices.
</>
}
>
Only Default Speakers
</Switch>
</Card>
</>
); );
} }
@ -319,16 +437,16 @@ function ModalComponent({
const [settings, setSettings] = useState<StreamSettings>({ const [settings, setSettings] = useState<StreamSettings>({
resolution: "1080", resolution: "1080",
fps: "60", fps: "60",
contentHint: "motion",
audio: true audio: true
}); });
return ( return (
<Modals.ModalRoot {...modalProps}> <Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Modals.ModalHeader className="vcd-screen-picker-header"> <Modals.ModalHeader className="vcd-screen-picker-header">
<Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle> <Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle>
<Modals.ModalCloseButton onClick={close} /> <Modals.ModalCloseButton onClick={close} />
</Modals.ModalHeader> </Modals.ModalHeader>
<Modals.ModalContent className="vcd-screen-picker-modal"> <Modals.ModalContent className="vcd-screen-picker-modal">
{!selected ? ( {!selected ? (
<ScreenPicker screens={screens} chooseScreen={setSelected} /> <ScreenPicker screens={screens} chooseScreen={setSelected} />
@ -341,29 +459,24 @@ function ModalComponent({
/> />
)} )}
</Modals.ModalContent> </Modals.ModalContent>
<Modals.ModalFooter className="vcd-screen-picker-footer"> <Modals.ModalFooter className="vcd-screen-picker-footer">
<Button <Button
disabled={!selected} disabled={!selected}
onClick={() => { onClick={() => {
currentSettings = settings; currentSettings = settings;
try {
// If there are 2 connections, the second one is the existing stream. const frameRate = Number(settings.fps);
// In that case, we patch its quality
const conn = [...MediaEngineStore.getMediaEngine().connections][1];
if (conn && conn.videoStreamParameters.length > 0) {
const height = Number(settings.resolution); const height = Number(settings.resolution);
const width = Math.round(height * (16 / 9)); const width = Math.round(height * (16 / 9));
Object.assign(conn.videoStreamParameters[0], {
maxFrameRate: Number(settings.fps), const conn = [...MediaEngineStore.getMediaEngine().connections].find(
maxPixelCount: width * height, connection => connection.streamUserId === UserStore.getCurrentUser().id
maxBitrate: 8000000, );
maxResolution: {
type: "fixed", if (conn) {
width, conn.videoStreamParameters[0].maxFrameRate = frameRate;
height conn.videoStreamParameters[0].maxResolution.height = height;
} conn.videoStreamParameters[0].maxResolution.width = width;
});
} }
submit({ submit({
@ -371,6 +484,38 @@ function ModalComponent({
...settings ...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,
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(); close();
}} }}
> >

View file

@ -5,4 +5,3 @@
*/ */
export * as ScreenShare from "./ScreenSharePicker"; export * as ScreenShare from "./ScreenSharePicker";
export { default as Settings } from "./Settings";

View file

@ -11,6 +11,21 @@
gap: 1em; gap: 1em;
} }
.vcd-screen-picker-settings-grid {
gap: 1em;
display: grid;
grid-template-columns: 1fr 1fr;
}
.vcd-screen-picker-settings-grid>div {
display: flex;
flex-direction: column;
}
.vcd-screen-picker-card {
flex-grow: 1;
}
.vcd-screen-picker-grid { .vcd-screen-picker-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@ -52,11 +67,16 @@
box-sizing: border-box; box-sizing: border-box;
} }
.vcd-screen-picker-preview img { .vcd-screen-picker-preview-img-linux {
width: 100%; width: 100%;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.vcd-screen-picker-preview-img {
width: 90%;
margin-bottom: 0.5em;
}
.vcd-screen-picker-preview { .vcd-screen-picker-preview {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -122,3 +142,10 @@
.vcd-screen-picker-audio { .vcd-screen-picker-audio {
margin-bottom: 0; margin-bottom: 0;
} }
.vcd-screen-picker-hint-description {
color: var(--header-secondary);
font-size: 14px;
line-height: 20px;
font-weight: 400;
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* 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>
);
};

View file

@ -0,0 +1,53 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { Button, Forms, Toasts } from "@vencord/types/webpack/common";
import { SettingsComponent } from "./Settings";
export const CustomSplashAnimation: SettingsComponent = ({ settings }) => {
return (
<>
<Forms.FormText>
The animation on the splash window is loaded from{" "}
{settings.splashAnimationPath ? (
<a
href="about:blank"
onClick={e => {
e.preventDefault();
VesktopNative.fileManager.showItemInFolder(settings.splashAnimationPath!);
}}
>
{settings.splashAnimationPath}
</a>
) : (
"the default location"
)}
</Forms.FormText>
<div className="vcd-location-btns" style={{marginBottom: 20}}>
<Button
size={Button.Sizes.SMALL}
onClick={async () => {
const choice = await VesktopNative.fileManager.selectImagePath();
if (choice === "cancelled") return;
settings.splashAnimationPath = choice;
}}
>
Change
</Button>
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
onClick={() => (settings.splashAnimationPath = void 0)}
>
Reset
</Button>
</div>
</>
);
};

View file

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* 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}
/>
);
};

View file

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* 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>
);
};

View file

@ -0,0 +1,178 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* 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";
import { CustomSplashAnimation } from "./CustomSplashAnimation";
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
}
],
Miscelleanous: [
{
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
}
],
"Custom Splash Animation":[CustomSplashAnimation],
"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>
);
}

View file

@ -0,0 +1,63 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { Button, Forms, Toasts } from "@vencord/types/webpack/common";
import { SettingsComponent } from "./Settings";
export const VencordLocationPicker: SettingsComponent = ({ settings }) => {
return (
<>
<Forms.FormText>
Vencord files are loaded from{" "}
{settings.vencordDir ? (
<a
href="about:blank"
onClick={e => {
e.preventDefault();
VesktopNative.fileManager.showItemInFolder(settings.vencordDir!);
}}
>
{settings.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":
return;
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
});
return;
}
settings.vencordDir = choice;
}}
>
Change
</Button>
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
onClick={() => (settings.vencordDir = void 0)}
>
Reset
</Button>
</div>
</>
);
};

View file

@ -0,0 +1,49 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* 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} />
</>
);
};

View file

@ -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;
}

11
src/renderer/fixes.css Normal file
View file

@ -0,0 +1,11 @@
/* 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;
}

View file

@ -4,7 +4,7 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import "./hideGarbage.css"; import "./fixes.css";
import { isWindows, localStorage } from "./utils"; import { isWindows, localStorage } from "./utils";

View file

@ -15,7 +15,7 @@ export * as Components from "./components";
import { findByPropsLazy } from "@vencord/types/webpack"; import { findByPropsLazy } from "@vencord/types/webpack";
import { FluxDispatcher } from "@vencord/types/webpack/common"; import { FluxDispatcher } from "@vencord/types/webpack/common";
import SettingsUi from "./components/Settings"; import SettingsUi from "./components/settings/Settings";
import { Settings } from "./settings"; import { Settings } from "./settings";
export { Settings }; export { Settings };

View file

@ -0,0 +1,24 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* 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)\.default\.getState\(\).neverShowModal/,
replace: "$& || $self.shouldIgnore($1)"
}
}
],
shouldIgnore(state: any) {
return Object.keys(state?.default?.lastDeviceConnected ?? {})?.[0] === "vencord-screen-share";
}
});

View file

@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* 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");
}
});

View file

@ -7,6 +7,8 @@
// TODO: Possibly auto generate glob if we have more patches in the future // TODO: Possibly auto generate glob if we have more patches in the future
import "./enableNotificationsByDefault"; import "./enableNotificationsByDefault";
import "./platformClass"; import "./platformClass";
import "./screenShareAudio"; import "./hideSwitchDevice";
import "./hideVenmicInput";
import "./screenShareFixes";
import "./spellCheck"; import "./spellCheck";
import "./windowsTitleBar"; import "./windowsTitleBar";

View file

@ -0,0 +1,69 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* 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,
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;
};
}

View file

@ -6,7 +6,7 @@
import { addContextMenuPatch } from "@vencord/types/api/ContextMenu"; import { addContextMenuPatch } from "@vencord/types/api/ContextMenu";
import { findStoreLazy } from "@vencord/types/webpack"; import { findStoreLazy } from "@vencord/types/webpack";
import { ContextMenu, FluxDispatcher, Menu } from "@vencord/types/webpack/common"; import { FluxDispatcher, Menu, useStateFromStores } from "@vencord/types/webpack/common";
import { addPatch } from "./shared"; import { addPatch } from "./shared";
@ -46,7 +46,8 @@ addPatch({
} }
}); });
addContextMenuPatch("textarea-context", children => () => { addContextMenuPatch("textarea-context", children => {
const spellCheckEnabled = useStateFromStores([SpellCheckStore], () => SpellCheckStore.isEnabled());
const hasCorrections = Boolean(word && corrections?.length); const hasCorrections = Boolean(word && corrections?.length);
children.push( children.push(
@ -71,11 +72,9 @@ addContextMenuPatch("textarea-context", children => () => {
<Menu.MenuCheckboxItem <Menu.MenuCheckboxItem
id="vcd-spellcheck-enabled" id="vcd-spellcheck-enabled"
label="Enable Spellcheck" label="Enable Spellcheck"
checked={SpellCheckStore.isEnabled()} checked={spellCheckEnabled}
action={() => { action={() => {
FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" }); FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
// Haven't found a good way to update state, so just close for now 🤷‍♀️
ContextMenu.close();
}} }}
/> />
</Menu.MenuGroup> </Menu.MenuGroup>

View file

@ -20,7 +20,7 @@ export interface Settings {
arRPC?: boolean; arRPC?: boolean;
appBadge?: boolean; appBadge?: boolean;
disableMinSize?: boolean; disableMinSize?: boolean;
clickTrayToShowHide?: boolean;
/** @deprecated use customTitleBar */ /** @deprecated use customTitleBar */
discordWindowsTitleBar?: boolean; discordWindowsTitleBar?: boolean;
customTitleBar?: boolean; customTitleBar?: boolean;
@ -28,8 +28,8 @@ export interface Settings {
checkUpdates?: boolean; checkUpdates?: boolean;
splashTheming?: boolean; splashTheming?: boolean;
splashAnimationPath?: string;
splashColor?: string; splashColor?: string;
splashAnimationPath?: string;
splashBackground?: string; splashBackground?: string;
} }

View file

@ -81,7 +81,7 @@ export async function checkUpdates() {
try { try {
const raw = await githubGet("/repos/Vencord/Vesktop/releases/latest"); const raw = await githubGet("/repos/Vencord/Vesktop/releases/latest");
const data = JSON.parse(raw.toString("utf-8")) as ReleaseData; const data: ReleaseData = await raw.json();
const oldVersion = app.getVersion(); const oldVersion = app.getVersion();
const newVersion = data.tag_name.replace(/^v/, ""); const newVersion = data.tag_name.replace(/^v/, "");

View file

@ -24,7 +24,7 @@
img { img {
width: 128px; width: 128px;
height: 128px height: 128px;
} }
</style> </style>
</head> </head>