Merge branch 'main' into threaded-arrpc

This commit is contained in:
Justin 2025-02-06 10:05:33 -05:00
commit 51a9f0214f
No known key found for this signature in database
GPG key ID: A260B29AA42DE5A6
33 changed files with 930 additions and 520 deletions

View file

@ -112,10 +112,8 @@ body:
attributes: attributes:
label: Debug Logs label: Debug Logs
description: Run vesktop from the command line. Include the relevant command line output here. If there are any lines that seem relevant, try googling them or searching existing issues description: Run vesktop from the command line. Include the relevant command line output here. If there are any lines that seem relevant, try googling them or searching existing issues
value: | placeholder: Paste your crash-log here.
``` render: shell
Replace this text with your crash-log. Do not remove the backticks
```
validations: validations:
required: true required: true

View file

@ -28,6 +28,21 @@
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="1.5.5" date="2025-02-06" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.5</url>
<description>
<p>What's Changed</p>
<ul>
<li>Now remembers your previous Screenshare resolution &amp; FPS</li>
<li>You can now disable the splash screen in Vesktop Settings</li>
<li>Now supports deep links (opening Discord Message Links in Vesktop) by @Covkie</li>
<li>Now supports discord:// uri scheme, allowing it to open things like invites from your Browser even while closed by @Covkie</li>
<li>Added 4k resolution to screenshare by @makindotcc</li>
<li>Fixed some performance issues caused by a recent Discord update</li>
<li>Updated Electron to v34 &amp; chromium to v132, bringing new features and fixes</li>
</ul>
</description>
</release>
<release version="1.5.4" date="2024-12-05" type="stable"> <release version="1.5.4" date="2024-12-05" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.4</url> <url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.4</url>
<description> <description>

View file

@ -1,6 +1,6 @@
{ {
"name": "vesktop", "name": "vesktop",
"version": "1.5.4", "version": "1.5.5",
"private": true, "private": true,
"description": "Vesktop is a custom Discord desktop app", "description": "Vesktop is a custom Discord desktop app",
"keywords": [], "keywords": [],
@ -34,19 +34,19 @@
}, },
"devDependencies": { "devDependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@stylistic/eslint-plugin": "^2.13.0", "@stylistic/eslint-plugin": "^3.0.1",
"@types/node": "^22.10.7", "@types/node": "^22.13.1",
"@types/react": "^18.3.12", "@types/react": "18.3.12",
"@vencord/types": "^1.8.4", "@vencord/types": "^1.8.4",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"electron": "^34.0.1", "electron": "^34.1.0",
"electron-builder": "^25.1.8", "electron-builder": "^25.1.8",
"esbuild": "^0.24.2", "esbuild": "^0.24.2",
"eslint": "^9.18.0", "eslint": "^9.19.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "^2.1.0", "eslint-plugin-path-alias": "^2.1.0",
"eslint-plugin-prettier": "^5.2.3", "eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-simple-header": "^1.2.1", "eslint-plugin-simple-header": "^1.2.2",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"prettier": "^3.4.2", "prettier": "^3.4.2",
@ -54,8 +54,8 @@
"tsx": "^4.19.2", "tsx": "^4.19.2",
"type-fest": "^4.33.0", "type-fest": "^4.33.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.21.0", "typescript-eslint": "^8.23.0",
"xml-formatter": "^3.6.3" "xml-formatter": "^3.6.4"
}, },
"packageManager": "pnpm@9.1.0", "packageManager": "pnpm@9.1.0",
"engines": { "engines": {
@ -73,6 +73,12 @@
"package.json", "package.json",
"LICENSE" "LICENSE"
], ],
"protocols": {
"name": "Discord",
"schemes": [
"discord"
]
},
"beforePack": "scripts/build/sandboxFix.js", "beforePack": "scripts/build/sandboxFix.js",
"linux": { "linux": {
"icon": "build/icon.icns", "icon": "build/icon.icns",
@ -113,7 +119,8 @@
"GenericName": "Internet Messenger", "GenericName": "Internet Messenger",
"Type": "Application", "Type": "Application",
"Categories": "Network;InstantMessaging;Chat;", "Categories": "Network;InstantMessaging;Chat;",
"Keywords": "discord;vencord;electron;chat;" "Keywords": "discord;vencord;electron;chat;",
"MimeType": "x-scheme-handler/discord"
} }
}, },
"mac": { "mac": {

View file

@ -1,14 +1,27 @@
diff --git a/src/process/index.js b/src/process/index.js diff --git a/src/process/index.js b/src/process/index.js
index 97ea6514b54dd9c5df588c78f0397d31ab5f882a..c2bdbd6aaa5611bc6ff1d993beeb380b1f5ec575 100644 index 389b0845256a34b4536d6da99edb00d17f13a6b4..f17a0ac687e9110ebfd33cb91fd2f6250d318643 100644
--- a/src/process/index.js --- a/src/process/index.js
+++ b/src/process/index.js +++ b/src/process/index.js
@@ -5,8 +5,7 @@ import fs from 'node:fs'; @@ -5,8 +5,20 @@ import fs from 'node:fs';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
-const __dirname = dirname(fileURLToPath(import.meta.url)); -const __dirname = dirname(fileURLToPath(import.meta.url));
-const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8')); -const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8'));
+const DetectableDB = require('./detectable.json'); +const DetectableDB = require('./detectable.json');
+DetectableDB.push(
+ {
+ aliases: ["Obs"],
+ executables: [
+ { is_launcher: false, name: "obs", os: "linux" },
+ { is_launcher: false, name: "obs.exe", os: "win32" },
+ { is_launcher: false, name: "obs.app", os: "darwin" }
+ ],
+ hook: true,
+ id: "STREAMERMODE",
+ name: "OBS"
+ }
+);
import * as Natives from './native/index.js'; import * as Natives from './native/index.js';
const Native = Natives[process.platform]; const Native = Natives[process.platform];

453
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -5,10 +5,10 @@
*/ */
import { resolve } from "path"; import { resolve } from "path";
import { IpcEvents } from "shared/IpcEvents"; import { IpcCommands, IpcEvents } from "shared/IpcEvents";
import { MessageChannel, Worker } from "worker_threads"; import { MessageChannel, Worker } from "worker_threads";
import { mainWin } from "./mainWindow"; import { sendRendererCommand } from "./ipcCommands";
import { Settings } from "./settings"; import { Settings } from "./settings";
import { ArrpcEvent, ArrpcHostEvent } from "./utils/arrpcWorkerTypes"; import { ArrpcEvent, ArrpcHostEvent } from "./utils/arrpcWorkerTypes";
@ -27,10 +27,10 @@ export async function initArRPC() {
}, },
transferList: [workerPort] transferList: [workerPort]
}); });
hostPort.on("message", (e: ArrpcEvent) => { hostPort.on("message", async (e: ArrpcEvent) => {
switch (e.eventType) { switch (e.eventType) {
case IpcEvents.ARRPC_ACTIVITY: { case IpcEvents.ARRPC_ACTIVITY: {
mainWin.webContents.send(IpcEvents.ARRPC_ACTIVITY, e.data); sendRendererCommand(IpcCommands.RPC_ACTIVITY, JSON.stringify(e.data));
break; break;
} }
case "invite": { case "invite": {
@ -45,11 +45,7 @@ export async function initArRPC() {
return hostPort.postMessage(hostEvent); return hostPort.postMessage(hostEvent);
} }
mainWin.webContents await sendRendererCommand(IpcCommands.RPC_INVITE, invite).then(() => {
// 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(() => {
const hostEvent: ArrpcHostEvent = { const hostEvent: ArrpcHostEvent = {
eventType: "ack-invite", eventType: "ack-invite",
data: true, data: true,
@ -57,6 +53,29 @@ export async function initArRPC() {
}; };
hostPort.postMessage(hostEvent); hostPort.postMessage(hostEvent);
}); });
break;
}
case "link": {
const link = String(e.data);
if (!inviteCodeRegex.test(link)) {
const hostEvent: ArrpcHostEvent = {
eventType: "ack-link",
data: false,
linkId: e.linkId
};
return hostPort.postMessage(hostEvent);
}
await sendRendererCommand(IpcCommands.RPC_DEEP_LINK, link).then(() => {
const hostEvent: ArrpcHostEvent = {
eventType: "ack-link",
data: true,
linkId: e.linkId
};
hostPort.postMessage(hostEvent);
});
break; break;
} }
} }

View file

@ -13,8 +13,10 @@ import { ArrpcEvent, ArrpcHostEvent } from "./utils/arrpcWorkerTypes";
let server: any; let server: any;
type InviteCallback = (valid: boolean) => void; type InviteCallback = (valid: boolean) => void;
type LinkCallback = InviteCallback;
let inviteCallbacks: Array<InviteCallback> = []; let inviteCallbacks: Array<InviteCallback> = [];
let linkCallbacks: Array<LinkCallback> = [];
(async function () { (async function () {
const { workerPort }: { workerPort: MessagePort } = workerData; const { workerPort }: { workerPort: MessagePort } = workerData;
@ -36,7 +38,17 @@ let inviteCallbacks: Array<InviteCallback> = [];
}); });
workerPort.on("message", (e: ArrpcHostEvent) => { workerPort.on("message", (e: ArrpcHostEvent) => {
switch (e.eventType) {
case "ack-invite": {
inviteCallbacks[e.inviteId](e.data); inviteCallbacks[e.inviteId](e.data);
inviteCallbacks = [...inviteCallbacks.slice(0, e.inviteId), ...inviteCallbacks.slice(e.inviteId + 1)]; inviteCallbacks = [...inviteCallbacks.slice(0, e.inviteId), ...inviteCallbacks.slice(e.inviteId + 1)];
break;
}
case "ack-link": {
linkCallbacks[e.linkId](e.data);
linkCallbacks = [...inviteCallbacks.slice(0, e.linkId), ...inviteCallbacks.slice(e.linkId + 1)];
break;
}
}
}); });
})(); })();

View file

@ -28,6 +28,7 @@ interface Data {
export function createFirstLaunchTour() { export function createFirstLaunchTour() {
const win = new BrowserWindow({ const win = new BrowserWindow({
...SplashProps, ...SplashProps,
transparent: false,
frame: true, frame: true,
autoHideMenuBar: true, autoHideMenuBar: true,
height: 470, height: 470,

View file

@ -23,10 +23,14 @@ if (IS_DEV) {
autoUpdater.checkForUpdatesAndNotify(); autoUpdater.checkForUpdatesAndNotify();
} }
console.log("Vesktop v" + app.getVersion());
// Make the Vencord files use our DATA_DIR // Make the Vencord files use our DATA_DIR
process.env.VENCORD_USER_DATA_DIR = DATA_DIR; process.env.VENCORD_USER_DATA_DIR = DATA_DIR;
function init() { function init() {
app.setAsDefaultProtocolClient("discord");
const { disableSmoothScroll, hardwareAcceleration } = Settings.store; const { disableSmoothScroll, hardwareAcceleration } = Settings.store;
const enabledFeatures = app.commandLine.getSwitchValue("enable-features").split(","); const enabledFeatures = app.commandLine.getSwitchValue("enable-features").split(",");
@ -35,7 +39,12 @@ function init() {
if (hardwareAcceleration === false) { if (hardwareAcceleration === false) {
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
} else { } else {
enabledFeatures.push("VaapiVideoDecodeLinuxGL", "VaapiVideoEncoder", "VaapiVideoDecoder"); enabledFeatures.push(
"AcceleratedVideoDecodeLinuxGL",
"AcceleratedVideoEncoder",
"AcceleratedVideoDecoder",
"AcceleratedVideoDecodeLinuxZeroCopyGL"
);
} }
if (disableSmoothScroll) { if (disableSmoothScroll) {
@ -112,6 +121,12 @@ async function bootstrap() {
} }
} }
// MacOS only event
export let darwinURL: string | undefined;
app.on("open-url", (_, url) => {
darwinURL = url;
});
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit(); if (process.platform !== "darwin") app.quit();
}); });

56
src/main/ipcCommands.ts Normal file
View file

@ -0,0 +1,56 @@
/*
* 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 { randomUUID } from "crypto";
import { ipcMain } from "electron";
import { IpcEvents } from "shared/IpcEvents";
import { mainWin } from "./mainWindow";
const resolvers = new Map<string, Record<"resolve" | "reject", (data: any) => void>>();
export interface IpcMessage {
nonce: string;
message: string;
data?: any;
}
export interface IpcResponse {
nonce: string;
ok: boolean;
data?: any;
}
/**
* Sends a message to the renderer process and waits for a response.
* `data` must be serializable as it will be sent over IPC.
*
* You must add a handler for the message in the renderer process.
*/
export function sendRendererCommand<T = any>(message: string, data?: any) {
const nonce = randomUUID();
const promise = new Promise<T>((resolve, reject) => {
resolvers.set(nonce, { resolve, reject });
});
mainWin.webContents.send(IpcEvents.IPC_COMMAND, { nonce, message, data });
return promise;
}
ipcMain.on(IpcEvents.IPC_COMMAND, (_event, { nonce, ok, data }: IpcResponse) => {
const resolver = resolvers.get(nonce);
if (!resolver) throw new Error(`Unknown message: ${nonce}`);
if (ok) {
resolver.resolve(data);
} else {
resolver.reject(data);
}
resolvers.delete(nonce);
});

View file

@ -18,7 +18,7 @@ import {
} from "electron"; } from "electron";
import { rm } from "fs/promises"; import { rm } from "fs/promises";
import { join } from "path"; import { join } from "path";
import { IpcEvents } from "shared/IpcEvents"; import { IpcCommands, IpcEvents } from "shared/IpcEvents";
import { isTruthy } from "shared/utils/guards"; import { isTruthy } from "shared/utils/guards";
import { once } from "shared/utils/once"; import { once } from "shared/utils/once";
import type { SettingsStore } from "shared/utils/SettingsStore"; import type { SettingsStore } from "shared/utils/SettingsStore";
@ -36,6 +36,8 @@ import {
MIN_WIDTH, MIN_WIDTH,
VENCORD_FILES_DIR VENCORD_FILES_DIR
} from "./constants"; } from "./constants";
import { darwinURL } from "./index";
import { sendRendererCommand } from "./ipcCommands";
import { Settings, State, VencordSettings } from "./settings"; import { Settings, State, VencordSettings } from "./settings";
import { createSplashWindow } from "./splash"; import { createSplashWindow } from "./splash";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
@ -198,9 +200,7 @@ function initMenuBar(win: BrowserWindow) {
label: "Settings", label: "Settings",
accelerator: "CmdOrCtrl+,", accelerator: "CmdOrCtrl+,",
async click() { async click() {
mainWin.webContents.executeJavaScript( sendRendererCommand(IpcCommands.NAVIGATE_SETTINGS);
"Vencord.Webpack.Common.SettingsRouter.open('My Account')"
);
} }
}, },
{ {
@ -271,7 +271,7 @@ function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
height: height ?? DEFAULT_HEIGHT height: height ?? DEFAULT_HEIGHT
} as BrowserWindowConstructorOptions; } as BrowserWindowConstructorOptions;
const storedDisplay = screen.getAllDisplays().find(display => display.id === State.store.displayid); const storedDisplay = screen.getAllDisplays().find(display => display.id === State.store.displayId);
if (x != null && y != null && storedDisplay) { if (x != null && y != null && storedDisplay) {
options.x = x; options.x = x;
@ -299,7 +299,7 @@ function getDarwinOptions(): BrowserWindowConstructorOptions {
options.vibrancy = "sidebar"; options.vibrancy = "sidebar";
options.backgroundColor = "#ffffff00"; options.backgroundColor = "#ffffff00";
} else { } else {
if (splashTheming) { if (splashTheming !== false) {
options.backgroundColor = splashBackground; options.backgroundColor = splashBackground;
} else { } else {
options.backgroundColor = nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff"; options.backgroundColor = nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
@ -321,7 +321,7 @@ function initWindowBoundsListeners(win: BrowserWindow) {
const saveBounds = () => { const saveBounds = () => {
State.store.windowBounds = win.getBounds(); State.store.windowBounds = win.getBounds();
State.store.displayid = screen.getDisplayMatching(State.store.windowBounds).id; State.store.displayId = screen.getDisplayMatching(State.store.windowBounds).id;
}; };
win.on("resize", saveBounds); win.on("resize", saveBounds);
@ -333,6 +333,7 @@ function initSettingsListeners(win: BrowserWindow) {
if (enable) initTray(win); if (enable) initTray(win);
else tray?.destroy(); else tray?.destroy();
}); });
addSettingsListener("disableMinSize", disable => { addSettingsListener("disableMinSize", disable => {
if (disable) { if (disable) {
// 0 no work // 0 no work
@ -366,7 +367,7 @@ function initSettingsListeners(win: BrowserWindow) {
} }
async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) { async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) {
languages ??= await win.webContents.executeJavaScript("[...new Set(navigator.languages)]").catch(() => []); languages ??= await sendRendererCommand(IpcCommands.GET_LANGUAGES);
if (!languages) return; if (!languages) return;
const ses = session.defaultSession; const ses = session.defaultSession;
@ -384,19 +385,38 @@ function initSpellCheck(win: BrowserWindow) {
initSpellCheckLanguages(win, Settings.store.spellCheckLanguages); initSpellCheckLanguages(win, Settings.store.spellCheckLanguages);
} }
function initStaticTitle(win: BrowserWindow) {
const listener = (e: { preventDefault: Function }) => e.preventDefault();
if (Settings.store.staticTitle) win.on("page-title-updated", listener);
addSettingsListener("staticTitle", enabled => {
if (enabled) {
win.setTitle("Vesktop");
win.on("page-title-updated", listener);
} else {
win.off("page-title-updated", listener);
}
});
}
function createMainWindow() { function createMainWindow() {
// Clear up previous settings listeners // Clear up previous settings listeners
removeSettingsListeners(); removeSettingsListeners();
removeVencordSettingsListeners(); removeVencordSettingsListeners();
const { staticTitle, transparencyOption, enableMenu, customTitleBar } = Settings.store; const { staticTitle, transparencyOption, enableMenu, customTitleBar, splashTheming, splashBackground } =
Settings.store;
const { frameless, transparent } = VencordSettings.store; const { frameless, transparent } = VencordSettings.store;
const noFrame = frameless === true || customTitleBar === true; const noFrame = frameless === true || customTitleBar === true;
const backgroundColor =
splashTheming !== false ? splashBackground : nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
const win = (mainWin = new BrowserWindow({ const win = (mainWin = new BrowserWindow({
show: false, show: Settings.store.enableSplashScreen === false,
backgroundColor,
webPreferences: { webPreferences: {
nodeIntegration: false, nodeIntegration: false,
sandbox: false, sandbox: false,
@ -444,44 +464,51 @@ function createMainWindow() {
return false; return false;
}); });
if (Settings.store.staticTitle) win.on("page-title-updated", e => e.preventDefault());
initWindowBoundsListeners(win); initWindowBoundsListeners(win);
if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win); if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win);
initMenuBar(win); initMenuBar(win);
makeLinksOpenExternally(win); makeLinksOpenExternally(win);
initSettingsListeners(win); initSettingsListeners(win);
initSpellCheck(win); initSpellCheck(win);
initStaticTitle(win);
win.webContents.setUserAgent(BrowserUserAgent); win.webContents.setUserAgent(BrowserUserAgent);
const subdomain = // if the open-url event is fired (in index.ts) while starting up, darwinURL will be set. If not fall back to checking the process args (which Windows and Linux use for URI calling.)
Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb" loadUrl(darwinURL || process.argv.find(arg => arg.startsWith("discord://")));
? `${Settings.store.discordBranch}.`
: "";
win.loadURL(`https://${subdomain}discord.com/app`);
return win; return win;
} }
const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js"))); const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js")));
export function loadUrl(uri: string | undefined) {
const branch = Settings.store.discordBranch;
const subdomain = branch === "canary" || branch === "ptb" ? `${branch}.` : "";
mainWin.loadURL(`https://${subdomain}discord.com/${uri ? new URL(uri).pathname.slice(1) || "app" : "app"}`);
}
export async function createWindows() { export async function createWindows() {
const startMinimized = process.argv.includes("--start-minimized"); const startMinimized = process.argv.includes("--start-minimized");
const splash = createSplashWindow(startMinimized);
let splash: BrowserWindow | undefined;
if (Settings.store.enableSplashScreen !== false) {
splash = createSplashWindow(startMinimized);
// SteamOS letterboxes and scales it terribly, so just full screen it // SteamOS letterboxes and scales it terribly, so just full screen it
if (isDeckGameMode) splash.setFullScreen(true); if (isDeckGameMode) splash.setFullScreen(true);
}
await ensureVencordFiles(); await ensureVencordFiles();
runVencordMain(); runVencordMain();
mainWin = createMainWindow(); mainWin = createMainWindow();
mainWin.webContents.on("did-finish-load", () => { mainWin.webContents.on("did-finish-load", () => {
splash.destroy(); splash?.destroy();
if (!startMinimized) { if (!startMinimized) {
mainWin!.show(); if (splash) mainWin!.show();
if (State.store.maximized && !isDeckGameMode) mainWin!.maximize(); if (State.store.maximized && !isDeckGameMode) mainWin!.maximize();
} }
@ -499,5 +526,13 @@ export async function createWindows() {
}); });
}); });
mainWin.webContents.on("did-navigate", (_, url: string, responseCode: number) => {
// check url to ensure app doesn't loop
if (responseCode >= 300 && new URL(url).pathname !== `/app`) {
loadUrl(undefined);
console.warn(`'did-navigate': Caught bad page response: ${responseCode}, redirecting to main app`);
}
});
initArRPC(); initArRPC();
} }

View file

@ -22,7 +22,7 @@ export function createSplashWindow(startMinimized = false) {
const { splashBackground, splashColor, splashTheming } = Settings.store; const { splashBackground, splashColor, splashTheming } = Settings.store;
if (splashTheming) { if (splashTheming !== false) {
if (splashColor) { if (splashColor) {
const semiTransparentSplashColor = splashColor.replace("rgb(", "rgba(").replace(")", ", 0.2)"); const semiTransparentSplashColor = splashColor.replace("rgb(", "rgba(").replace(")", ", 0.2)");

View file

@ -6,7 +6,8 @@
import { IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
export type ArrpcEvent = ArrpcActivityEvent | ArrpcInviteEvent; export type ArrpcEvent = ArrpcActivityEvent | ArrpcInviteEvent | ArrpcLinkEvent;
export type ArrpcHostEvent = ArrpcHostAckInviteEvent | ArrpcHostAckLinkEvent;
export interface ArrpcActivityEvent { export interface ArrpcActivityEvent {
eventType: IpcEvents.ARRPC_ACTIVITY; eventType: IpcEvents.ARRPC_ACTIVITY;
@ -19,8 +20,20 @@ export interface ArrpcInviteEvent {
inviteId: number; inviteId: number;
} }
export interface ArrpcHostEvent { export interface ArrpcLinkEvent {
eventType: "link";
data: string;
linkId: number;
}
export interface ArrpcHostAckInviteEvent {
eventType: "ack-invite"; eventType: "ack-invite";
inviteId: number; inviteId: number;
data: boolean; data: boolean;
} }
export interface ArrpcHostAckLinkEvent {
eventType: "ack-link";
linkId: number;
data: boolean;
}

View file

@ -6,6 +6,7 @@
import { Node } from "@vencord/venmic"; import { Node } from "@vencord/venmic";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { IpcMessage, IpcResponse } from "main/ipcCommands";
import type { Settings } from "shared/settings"; import type { Settings } from "shared/settings";
import { IpcEvents } from "../shared/IpcEvents"; import { IpcEvents } from "../shared/IpcEvents";
@ -70,11 +71,6 @@ export const VesktopNative = {
startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude), startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude),
stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP) stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP)
}, },
arrpc: {
onActivity(cb: (data: string) => void) {
ipcRenderer.on(IpcEvents.ARRPC_ACTIVITY, (_, data: string) => cb(data));
}
},
clipboard: { clipboard: {
copyImage: (imageBuffer: Uint8Array, imageSrc: string) => copyImage: (imageBuffer: Uint8Array, imageSrc: string) =>
invoke<void>(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc) invoke<void>(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc)
@ -82,5 +78,11 @@ export const VesktopNative = {
debug: { debug: {
launchGpu: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_GPU), launchGpu: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_GPU),
launchWebrtcInternals: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS) launchWebrtcInternals: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS)
},
commands: {
onCommand(cb: (message: IpcMessage) => void) {
ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message));
},
respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response)
} }
}; };

View file

@ -7,6 +7,7 @@
import { filters, waitFor } from "@vencord/types/webpack"; import { filters, waitFor } from "@vencord/types/webpack";
import { RelationshipStore } from "@vencord/types/webpack/common"; import { RelationshipStore } from "@vencord/types/webpack/common";
import { VesktopLogger } from "./logger";
import { Settings } from "./settings"; import { Settings } from "./settings";
let GuildReadStateStore: any; let GuildReadStateStore: any;
@ -26,7 +27,7 @@ export function setBadge() {
VesktopNative.app.setBadgeCount(totalCount); VesktopNative.app.setBadgeCount(totalCount);
} catch (e) { } catch (e) {
console.error(e); VesktopLogger.error("Failed to update badge count", e);
} }
} }

68
src/renderer/arrpc.ts Normal file
View file

@ -0,0 +1,68 @@
/*
* 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 { findLazy, findStoreLazy, onceReady } from "@vencord/types/webpack";
import { FluxDispatcher, InviteActions } from "@vencord/types/webpack/common";
import { IpcCommands } from "shared/IpcEvents";
import { onIpcCommand } from "./ipcCommands";
import { Settings } from "./settings";
const logger = new Logger("VesktopRPC", "#5865f2");
const StreamerModeStore = findStoreLazy("StreamerModeStore");
const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as any as {
handleEvent(e: MessageEvent): void;
};
onIpcCommand(IpcCommands.RPC_ACTIVITY, async jsonData => {
if (!Settings.store.arRPC) return;
await onceReady;
const data = JSON.parse(jsonData);
if (data.socketId === "STREAMERMODE" && StreamerModeStore.autoToggle) {
FluxDispatcher.dispatch({
type: "STREAMER_MODE_UPDATE",
key: "enabled",
value: data.activity?.application_id === "STREAMERMODE"
});
return;
}
arRPC.handleEvent(new MessageEvent("message", { data: jsonData }));
});
onIpcCommand(IpcCommands.RPC_INVITE, async code => {
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 { DEEP_LINK } = findLazy(m => m.DEEP_LINK?.handler);
onIpcCommand(IpcCommands.RPC_DEEP_LINK, async data => {
logger.debug("Opening deep link:", data);
try {
DEEP_LINK.handler({ args: data });
return true;
} catch (err) {
logger.error("Failed to open deep link:", err);
return false;
}
});

View file

@ -22,12 +22,14 @@ import {
import { Node } from "@vencord/venmic"; import { Node } from "@vencord/venmic";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { addPatch } from "renderer/patches/shared"; import { addPatch } from "renderer/patches/shared";
import { useSettings } from "renderer/settings"; import { State, useSettings, useVesktopState } from "renderer/settings";
import { isLinux, isWindows } from "renderer/utils"; import { classNameFactory, isLinux, isWindows } from "renderer/utils";
const StreamResolutions = ["480", "720", "1080", "1440", "2160"] as const; const StreamResolutions = ["480", "720", "1080", "1440", "2160"] as const;
const StreamFps = ["15", "30", "60"] as const; const StreamFps = ["15", "30", "60"] as const;
const cl = classNameFactory("vcd-screen-picker-");
const MediaEngineStore = findStoreLazy("MediaEngineStore"); const MediaEngineStore = findStoreLazy("MediaEngineStore");
export type StreamResolution = (typeof StreamResolutions)[number]; export type StreamResolution = (typeof StreamResolutions)[number];
@ -44,8 +46,6 @@ interface AudioItem {
} }
interface StreamSettings { interface StreamSettings {
resolution: StreamResolution;
fps: StreamFps;
audio: boolean; audio: boolean;
contentHint?: string; contentHint?: string;
includeSources?: AudioSources; includeSources?: AudioSources;
@ -77,10 +77,11 @@ addPatch({
} }
], ],
patchStreamQuality(opts: any) { patchStreamQuality(opts: any) {
if (!currentSettings) return; const { screenshareQuality } = State.store;
if (!screenshareQuality) return;
const framerate = Number(currentSettings.fps); const framerate = Number(screenshareQuality.frameRate);
const height = Number(currentSettings.resolution); const height = Number(screenshareQuality.resolution);
const width = Math.round(height * (16 / 9)); const width = Math.round(height * (16 / 9));
Object.assign(opts, { Object.assign(opts, {
@ -161,13 +162,21 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) { function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) {
return ( return (
<div className="vcd-screen-picker-grid"> <div className={cl("screen-grid")}>
{screens.map(({ id, name, url }) => ( {screens.map(({ id, name, url }) => (
<label key={id}> <label key={id} className={cl("screen-label")}>
<input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} /> <input
type="radio"
className={cl("screen-radio")}
name="screen"
value={id}
onChange={() => chooseScreen(id)}
/>
<img src={url} alt="" /> <img src={url} alt="" />
<Text variant="text-sm/normal">{name}</Text> <Text className={cl("screen-name")} variant="text-sm/normal">
{name}
</Text>
</label> </label>
))} ))}
</div> </div>
@ -187,11 +196,13 @@ function AudioSettingsModal({
return ( return (
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}> <Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Modals.ModalHeader className="vcd-screen-picker-header"> <Modals.ModalHeader className={cl("header")}>
<Forms.FormTitle tag="h2">Venmic Settings</Forms.FormTitle> <Forms.FormTitle tag="h2" className={cl("header-title")}>
Venmic Settings
</Forms.FormTitle>
<Modals.ModalCloseButton onClick={close} /> <Modals.ModalCloseButton onClick={close} />
</Modals.ModalHeader> </Modals.ModalHeader>
<Modals.ModalContent className="vcd-screen-picker-modal"> <Modals.ModalContent className={cl("modal")}>
<Switch <Switch
hideBorder hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, workaround: v })} onChange={v => (Settings.audio = { ...Settings.audio, workaround: v })}
@ -295,7 +306,7 @@ function AudioSettingsModal({
Device Selection Device Selection
</Switch> </Switch>
</Modals.ModalContent> </Modals.ModalContent>
<Modals.ModalFooter className="vcd-screen-picker-footer"> <Modals.ModalFooter className={cl("footer")}>
<Button color={Button.Colors.TRANSPARENT} onClick={close}> <Button color={Button.Colors.TRANSPARENT} onClick={close}>
Back Back
</Button> </Button>
@ -304,7 +315,35 @@ function AudioSettingsModal({
); );
} }
function StreamSettings({ function OptionRadio<Settings extends object, Key extends keyof Settings>(props: {
options: Array<string> | ReadonlyArray<string>;
labels?: Array<string>;
settings: Settings;
settingsKey: Key;
onChange: (option: string) => void;
}) {
const { options, settings, settingsKey, labels, onChange } = props;
return (
<div className={cl("option-radios")}>
{(options as string[]).map((option, idx) => (
<label className={cl("option-radio")} data-checked={settings[settingsKey] === option} key={option}>
<Text variant="text-sm/bold">{labels?.[idx] ?? option}</Text>
<input
className={cl("option-input")}
type="radio"
name="fps"
value={option}
checked={settings[settingsKey] === option}
onChange={() => onChange(option)}
/>
</label>
))}
</div>
);
}
function StreamSettingsUi({
source, source,
settings, settings,
setSettings, setSettings,
@ -316,6 +355,7 @@ function StreamSettings({
skipPicker: boolean; skipPicker: boolean;
}) { }) {
const Settings = useSettings(); const Settings = useSettings();
const qualitySettings = State.store.screenshareQuality!;
const [thumb] = useAwaiter( const [thumb] = useAwaiter(
() => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)), () => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)),
@ -340,88 +380,47 @@ function StreamSettings({
return ( return (
<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={cl("card", "preview")}>
<img <img src={thumb} alt="" className={cl(isLinux ? "preview-img-linux" : "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> <Text variant="text-sm/normal">{source.name}</Text>
</Card> </Card>
<Forms.FormTitle>Stream Settings</Forms.FormTitle> <Forms.FormTitle>Stream Settings</Forms.FormTitle>
<Card className="vcd-screen-picker-card"> <Card className={cl("card")}>
<div className="vcd-screen-picker-quality"> <div className={cl("quality")}>
<section> <section className={cl("quality-section")}>
<Forms.FormTitle>Resolution</Forms.FormTitle> <Forms.FormTitle>Resolution</Forms.FormTitle>
<div className="vcd-screen-picker-radios"> <OptionRadio
{StreamResolutions.map(res => ( options={StreamResolutions}
<label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}> settings={qualitySettings}
<Text variant="text-sm/bold">{res}</Text> settingsKey="resolution"
<input onChange={value => (qualitySettings.resolution = value)}
type="radio"
name="resolution"
value={res}
checked={settings.resolution === res}
onChange={() => setSettings(s => ({ ...s, resolution: res }))}
/> />
</label>
))}
</div>
</section> </section>
<section> <section className={cl("quality-section")}>
<Forms.FormTitle>Frame Rate</Forms.FormTitle> <Forms.FormTitle>Frame Rate</Forms.FormTitle>
<div className="vcd-screen-picker-radios"> <OptionRadio
{StreamFps.map(fps => ( options={StreamFps}
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}> settings={qualitySettings}
<Text variant="text-sm/bold">{fps}</Text> settingsKey="frameRate"
<input onChange={value => (qualitySettings.frameRate = value)}
type="radio"
name="fps"
value={fps}
checked={settings.fps === fps}
onChange={() => setSettings(s => ({ ...s, fps }))}
/> />
</label>
))}
</div>
</section> </section>
</div> </div>
<div className="vcd-screen-picker-quality"> <div className={cl("quality")}>
<section> <section className={cl("quality-section")}>
<Forms.FormTitle>Content Type</Forms.FormTitle> <Forms.FormTitle>Content Type</Forms.FormTitle>
<div> <div>
<div className="vcd-screen-picker-radios"> <OptionRadio
<label options={["motion", "detail"]}
className="vcd-screen-picker-radio" labels={["Prefer Smoothness", "Prefer Clarity"]}
data-checked={settings.contentHint === "motion"} settings={settings}
> settingsKey="contentHint"
<Text variant="text-sm/bold">Prefer Smoothness</Text> onChange={option => setSettings(s => ({ ...s, contentHint: option }))}
<input
type="radio"
name="contenthint"
value="motion"
checked={settings.contentHint === "motion"}
onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))}
/> />
</label> <div className={cl("hint-description")}>
<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> <p>
Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange
for a much sharper and clearer image. for a much sharper and clearer image.
@ -433,7 +432,7 @@ function StreamSettings({
value={settings.audio} value={settings.audio}
onChange={checked => setSettings(s => ({ ...s, audio: checked }))} onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
hideBorder hideBorder
className="vcd-screen-picker-audio" className={cl("audio")}
> >
Stream With Audio Stream With Audio
</Switch> </Switch>
@ -639,7 +638,7 @@ function AudioSourcePickerLinux({
return ( return (
<> <>
<div className={includeSources === "Entire System" ? "vcd-screen-picker-quality" : undefined}> <div className={cl({ quality: includeSources === "Entire System" })}>
<section> <section>
<Forms.FormTitle>{loading ? "Loading Sources..." : "Audio Sources"}</Forms.FormTitle> <Forms.FormTitle>{loading ? "Loading Sources..." : "Audio Sources"}</Forms.FormTitle>
<Select <Select
@ -675,11 +674,7 @@ function AudioSourcePickerLinux({
</section> </section>
)} )}
</div> </div>
<Button <Button color={Button.Colors.TRANSPARENT} onClick={openSettings} className={cl("settings-button")}>
color={Button.Colors.TRANSPARENT}
onClick={openSettings}
className="vcd-screen-picker-settings-button"
>
Open Audio Settings Open Audio Settings
</Button> </Button>
</> </>
@ -701,24 +696,26 @@ function ModalComponent({
}) { }) {
const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0); const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0);
const [settings, setSettings] = useState<StreamSettings>({ const [settings, setSettings] = useState<StreamSettings>({
resolution: "720",
fps: "30",
contentHint: "motion", contentHint: "motion",
audio: true, audio: true,
includeSources: "None" includeSources: "None"
}); });
const qualitySettings = (useVesktopState().screenshareQuality ??= {
resolution: "720",
frameRate: "30"
});
return ( return (
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}> <Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Modals.ModalHeader className="vcd-screen-picker-header"> <Modals.ModalHeader className={cl("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={cl("modal")}>
{!selected ? ( {!selected ? (
<ScreenPicker screens={screens} chooseScreen={setSelected} /> <ScreenPicker screens={screens} chooseScreen={setSelected} />
) : ( ) : (
<StreamSettings <StreamSettingsUi
source={screens.find(s => s.id === selected)!} source={screens.find(s => s.id === selected)!}
settings={settings} settings={settings}
setSettings={setSettings} setSettings={setSettings}
@ -726,14 +723,14 @@ function ModalComponent({
/> />
)} )}
</Modals.ModalContent> </Modals.ModalContent>
<Modals.ModalFooter className="vcd-screen-picker-footer"> <Modals.ModalFooter className={cl("footer")}>
<Button <Button
disabled={!selected} disabled={!selected}
onClick={() => { onClick={() => {
currentSettings = settings; currentSettings = settings;
try { try {
const frameRate = Number(settings.fps); const frameRate = Number(qualitySettings.frameRate);
const height = Number(settings.resolution); const height = Number(qualitySettings.resolution);
const width = Math.round(height * (16 / 9)); const width = Math.round(height * (16 / 9));
const conn = [...MediaEngineStore.getMediaEngine().connections].find( const conn = [...MediaEngineStore.getMediaEngine().connections].find(

View file

@ -2,7 +2,7 @@
padding: 1em; padding: 1em;
} }
.vcd-screen-picker-header h1 { .vcd-screen-picker-header-title {
margin: 0; margin: 0;
} }
@ -15,23 +15,20 @@
flex-grow: 1; flex-grow: 1;
} }
.vcd-screen-picker-grid {
/* Screen Grid */
.vcd-screen-picker-screen-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 2em 1em; gap: 2em 1em;
} }
.vcd-screen-picker-grid input { .vcd-screen-picker-screen-radio {
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;
} }
.vcd-screen-picker-selected img { .vcd-screen-picker-screen-label {
border: 2px solid var(--brand-500);
border-radius: 3px;
}
.vcd-screen-picker-grid label {
overflow: hidden; overflow: hidden;
padding: 8px; padding: 8px;
cursor: pointer; cursor: pointer;
@ -39,11 +36,11 @@
justify-items: center; justify-items: center;
} }
.vcd-screen-picker-grid label:hover { .vcd-screen-picker-screen-label:hover {
outline: 2px solid var(--brand-500); outline: 2px solid var(--brand-500);
} }
.vcd-screen-picker-grid div { .vcd-screen-picker-screen-name {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
@ -75,37 +72,48 @@
margin-bottom: 1em; margin-bottom: 1em;
} }
.vcd-screen-picker-radio input {
display: none; /* Option Radios */
.vcd-screen-picker-option-radios {
display: flex;
width: 100%;
border-radius: 3px;
} }
.vcd-screen-picker-radio { .vcd-screen-picker-option-radio {
flex: 1 1 auto;
text-align: center;
background-color: var(--background-secondary); background-color: var(--background-secondary);
border: 1px solid var(--primary-800); border: 1px solid var(--primary-800);
padding: 0.3em; padding: 0.3em;
cursor: pointer; cursor: pointer;
} }
.vcd-screen-picker-radio h2 { .vcd-screen-picker-option-radio:first-child {
margin: 0; border-radius: 3px 0 0 3px;
} }
.vcd-screen-picker-radio[data-checked="true"] { .vcd-screen-picker-option-radio:last-child {
border-radius: 0 3px 3px 0;
}
.vcd-screen-picker-option-input {
display: none;
}
.vcd-screen-picker-option-radio[data-checked="true"] {
background-color: var(--brand-500); background-color: var(--brand-500);
border-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 { .vcd-screen-picker-quality {
display: flex; display: flex;
gap: 1em; gap: 1em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.vcd-screen-picker-quality section { .vcd-screen-picker-quality-section {
flex: 1 1 auto; flex: 1 1 auto;
} }
@ -114,24 +122,6 @@
margin-top: 0.3rem; 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 { .vcd-screen-picker-audio {
margin-bottom: 0; margin-bottom: 0;

View file

@ -6,6 +6,7 @@
import "./settings.css"; import "./settings.css";
import { ErrorBoundary } from "@vencord/types/components";
import { Forms, Switch, Text } from "@vencord/types/webpack/common"; import { Forms, Switch, Text } from "@vencord/types/webpack/common";
import { ComponentType } from "react"; import { ComponentType } from "react";
import { Settings, useSettings } from "renderer/settings"; import { Settings, useSettings } from "renderer/settings";
@ -59,11 +60,18 @@ const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>>
defaultValue: false, defaultValue: false,
disabled: () => Settings.store.customTitleBar ?? isWindows disabled: () => Settings.store.customTitleBar ?? isWindows
}, },
{
key: "enableSplashScreen",
title: "Enable Splash Screen",
description:
"Shows a small splash screen while Vesktop is loading. Disabling this option will show the main window earlier while it's still loading.",
defaultValue: true
},
{ {
key: "splashTheming", key: "splashTheming",
title: "Splash theming", title: "Splash theming",
description: "Adapt the splash window colors to your custom theme", description: "Adapt the splash window colors to your custom theme",
defaultValue: false defaultValue: true
}, },
WindowsTransparencyControls WindowsTransparencyControls
], ],
@ -155,7 +163,8 @@ function SettingsSections() {
return <>{sections}</>; return <>{sections}</>;
} }
export default function SettingsUi() { export default ErrorBoundary.wrap(
function SettingsUI() {
return ( return (
<Forms.FormSection> <Forms.FormSection>
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2"> <Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">
@ -165,4 +174,9 @@ export default function SettingsUi() {
<SettingsSections /> <SettingsSections />
</Forms.FormSection> </Forms.FormSection>
); );
} },
{
message:
"Failed to render the Vesktop Settings tab. If this issue persists, try to right click the Vesktop tray icon, then click 'Repair Vencord'. And make sure your Vesktop is up to date."
}
);

View file

@ -1,15 +1,3 @@
/* 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 */ /* Workaround for making things in the draggable area clickable again on macOS */
.platform-osx [class*=topic_], .platform-osx [class*=notice_] button { .platform-osx [class*=topic_], .platform-osx [class*=notice_] button {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;

View file

@ -6,7 +6,7 @@
import "./fixes.css"; import "./fixes.css";
import { isWindows, localStorage } from "./utils"; import { localStorage } from "./utils";
// Make clicking Notifications focus the window // Make clicking Notifications focus the window
const originalSetOnClick = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick")!.set!; const originalSetOnClick = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick")!.set!;
@ -22,14 +22,3 @@ Object.defineProperty(Notification.prototype, "onclick", {
// Hide "Download Discord Desktop now!!!!" banner // Hide "Download Discord Desktop now!!!!" banner
localStorage.setItem("hideNag", "true"); 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 {}

View file

@ -4,38 +4,22 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import "./fixes"; import "./themedSplash";
import "./ipcCommands";
import "./appBadge"; import "./appBadge";
import "./patches"; import "./patches";
import "./themedSplash"; import "./fixes";
import "./arrpc";
console.log("read if cute :3");
export * as Components from "./components"; 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 SettingsUi from "./components/settings/Settings";
import { VesktopLogger } from "./logger";
import { Settings } from "./settings"; import { Settings } from "./settings";
export { Settings }; export { Settings };
const InviteActions = findByPropsLazy("resolveInvite"); VesktopLogger.log("read if cute :3");
VesktopLogger.log("Vesktop v" + VesktopNative.app.getVersion());
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 = ( const customSettingsSections = (
Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record<string, unknown>) => any)[] } Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record<string, unknown>) => any)[] }
@ -47,31 +31,3 @@ customSettingsSections.push(() => ({
element: SettingsUi, element: SettingsUi,
className: "vc-vesktop-settings" 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
)
);
}

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 { SettingsRouter } from "@vencord/types/webpack/common";
import { IpcCommands } from "shared/IpcEvents";
type IpcCommandHandler = (data: any) => any;
const handlers = new Map<string, IpcCommandHandler>();
function respond(nonce: string, ok: boolean, data: any) {
VesktopNative.commands.respond({ nonce, ok, data });
}
VesktopNative.commands.onCommand(async ({ message, nonce, data }) => {
const handler = handlers.get(message);
if (!handler) {
return respond(nonce, false, `No handler for message: ${message}`);
}
try {
const result = await handler(data);
respond(nonce, true, result);
} catch (err) {
respond(nonce, false, String(err));
}
});
export function onIpcCommand(channel: string, handler: IpcCommandHandler) {
if (handlers.has(channel)) {
throw new Error(`Handler for message ${channel} already exists`);
}
handlers.set(channel, handler);
}
export function offIpcCommand(channel: string) {
handlers.delete(channel);
}
/* Generic Handlers */
onIpcCommand(IpcCommands.NAVIGATE_SETTINGS, () => {
SettingsRouter.open("My Account");
});
onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages);

9
src/renderer/logger.ts Normal file
View file

@ -0,0 +1,9 @@
/*
* 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";
export const VesktopLogger = new Logger("Vesktop", "#d3869b");

View file

@ -12,3 +12,5 @@ import "./hideVenmicInput";
import "./screenShareFixes"; import "./screenShareFixes";
import "./spellCheck"; import "./spellCheck";
import "./windowsTitleBar"; import "./windowsTitleBar";
import "./streamerMode";
import "./nativeFocus";

View file

@ -0,0 +1,23 @@
/*
* 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: ".DEEP_LINK]:{",
replacement: [
{
// TODO: Fix eslint rule
// eslint-disable-next-line no-useless-escape
match: /(?<=\.DEEP_LINK.{0,200}?)\i\.\i\.focus\(\)/,
replace: "VesktopNative.win.focus()"
}
]
}
]
});

View file

@ -6,6 +6,7 @@
import { Logger } from "@vencord/types/utils"; import { Logger } from "@vencord/types/utils";
import { currentSettings } from "renderer/components/ScreenSharePicker"; import { currentSettings } from "renderer/components/ScreenSharePicker";
import { State } from "renderer/settings";
import { isLinux } from "renderer/utils"; import { isLinux } from "renderer/utils";
const logger = new Logger("VesktopStreamFixes"); const logger = new Logger("VesktopStreamFixes");
@ -27,8 +28,8 @@ if (isLinux) {
const stream = await original.call(this, opts); const stream = await original.call(this, opts);
const id = await getVirtmic(); const id = await getVirtmic();
const frameRate = Number(currentSettings?.fps); const frameRate = Number(State.store.screenshareQuality?.frameRate ?? 30);
const height = Number(currentSettings?.resolution); const height = Number(State.store.screenshareQuality?.resolution ?? 720);
const width = Math.round(height * (16 / 9)); const width = Math.round(height * (16 / 9));
const track = stream.getVideoTracks()[0]; const track = stream.getVideoTracks()[0];

View file

@ -0,0 +1,21 @@
/*
* 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: ".STREAMER_MODE_ENABLE,",
replacement: {
// remove if (platformEmbedded) check from streamer mode toggle
// eslint-disable-next-line no-useless-escape
match: /if\(\i\.\i\)(?=return.{0,200}?"autoToggle")/g,
replace: ""
}
}
]
});

View file

@ -7,6 +7,9 @@
import { useEffect, useReducer } from "@vencord/types/webpack/common"; import { useEffect, useReducer } from "@vencord/types/webpack/common";
import { SettingsStore } from "shared/utils/SettingsStore"; import { SettingsStore } from "shared/utils/SettingsStore";
import { VesktopLogger } from "./logger";
import { localStorage } from "./utils";
export const Settings = new SettingsStore(VesktopNative.settings.get()); export const Settings = new SettingsStore(VesktopNative.settings.get());
Settings.addGlobalChangeListener((o, p) => VesktopNative.settings.set(o, p)); Settings.addGlobalChangeListener((o, p) => VesktopNative.settings.set(o, p));
@ -28,3 +31,38 @@ export function getValueAndOnChange(key: keyof typeof Settings.store) {
onChange: (value: any) => (Settings.store[key] = value) onChange: (value: any) => (Settings.store[key] = value)
}; };
} }
interface TState {
screenshareQuality?: {
resolution: string;
frameRate: string;
};
}
const stateKey = "VesktopState";
const currentState: TState = (() => {
const stored = localStorage.getItem(stateKey);
if (!stored) return {};
try {
return JSON.parse(stored);
} catch (e) {
VesktopLogger.error("Failed to parse stored state", e);
return {};
}
})();
export const State = new SettingsStore<TState>(currentState);
State.addGlobalChangeListener((o, p) => localStorage.setItem(stateKey, JSON.stringify(o)));
export function useVesktopState() {
const [, update] = useReducer(x => x + 1, 0);
useEffect(() => {
State.addGlobalChangeListener(update);
return () => State.removeGlobalChangeListener(update);
}, []);
return State.store;
}

View file

@ -10,15 +10,49 @@ function isValidColor(color: CSSStyleValue | undefined): color is CSSUnparsedVal
return color instanceof CSSUnparsedValue && typeof color[0] === "string" && CSS.supports("color", color[0]); return color instanceof CSSUnparsedValue && typeof color[0] === "string" && CSS.supports("color", color[0]);
} }
// https://gist.github.com/earthbound19/e7fe15fdf8ca3ef814750a61bc75b5ce
function clamp(value: number, min: number, max: number) {
return Math.max(Math.min(value, max), min);
}
const linearToGamma = (c: number) => (c >= 0.0031308 ? 1.055 * Math.pow(c, 1 / 2.4) - 0.055 : 12.92 * c);
function oklabToSRGB({ L, a, b }: { L: number; a: number; b: number }) {
let l = L + a * +0.3963377774 + b * +0.2158037573;
let m = L + a * -0.1055613458 + b * -0.0638541728;
let s = L + a * -0.0894841775 + b * -1.291485548;
l **= 3;
m **= 3;
s **= 3;
let R = l * +4.0767416621 + m * -3.3077115913 + s * +0.2309699292;
let G = l * -1.2684380046 + m * +2.6097574011 + s * -0.3413193965;
let B = l * -0.0041960863 + m * -0.7034186147 + s * +1.707614701;
R = 255 * linearToGamma(R);
G = 255 * linearToGamma(G);
B = 255 * linearToGamma(B);
R = Math.round(clamp(R, 0, 255));
G = Math.round(clamp(G, 0, 255));
B = Math.round(clamp(B, 0, 255));
return `rgb(${R}, ${G}, ${B})`;
}
function resolveColor(color: string) { function resolveColor(color: string) {
const span = document.createElement("span"); const span = document.createElement("span");
span.style.color = color; span.style.color = color;
span.style.display = "none"; span.style.display = "none";
document.body.append(span); document.body.append(span);
const rgbColor = getComputedStyle(span).color; let rgbColor = getComputedStyle(span).color;
span.remove(); span.remove();
if (rgbColor.startsWith("oklab(")) {
// scam
const [_, L, a, b] = rgbColor.match(/oklab\((.+?)[, ]+(.+?)[, ]+(.+?)\)/) ?? [];
if (L && a && b) {
rgbColor = oklabToSRGB({ L: parseFloat(L), a: parseFloat(a), b: parseFloat(b) });
}
}
return rgbColor; return rgbColor;
} }

View file

@ -4,6 +4,7 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
// Discord deletes this from the window so we need to capture it in a variable
export const { localStorage } = window; export const { localStorage } = window;
export const isFirstRun = (() => { export const isFirstRun = (() => {
@ -18,3 +19,26 @@ const { platform } = navigator;
export const isWindows = platform.startsWith("Win"); export const isWindows = platform.startsWith("Win");
export const isMac = platform.startsWith("Mac"); export const isMac = platform.startsWith("Mac");
export const isLinux = platform.startsWith("Linux"); export const isLinux = platform.startsWith("Linux");
type ClassNameFactoryArg = string | string[] | Record<string, unknown> | false | null | undefined | 0 | "";
/**
* @param prefix The prefix to add to each class, defaults to `""`
* @returns A classname generator function
* @example
* const cl = classNameFactory("plugin-");
*
* cl("base", ["item", "editable"], { selected: null, disabled: true })
* // => "plugin-base plugin-item plugin-editable plugin-disabled"
*/
export const classNameFactory =
(prefix: string = "") =>
(...args: ClassNameFactoryArg[]) => {
const classNames = new Set<string>();
for (const arg of args) {
if (arg && typeof arg === "string") classNames.add(arg);
else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name));
else if (arg && typeof arg === "object")
Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));
}
return Array.from(classNames, name => prefix + name).join(" ");
};

View file

@ -53,5 +53,15 @@ export const enum IpcEvents {
CLIPBOARD_COPY_IMAGE = "VCD_CLIPBOARD_COPY_IMAGE", CLIPBOARD_COPY_IMAGE = "VCD_CLIPBOARD_COPY_IMAGE",
DEBUG_LAUNCH_GPU = "VCD_DEBUG_LAUNCH_GPU", DEBUG_LAUNCH_GPU = "VCD_DEBUG_LAUNCH_GPU",
DEBUG_LAUNCH_WEBRTC_INTERNALS = "VCD_DEBUG_LAUNCH_WEBRTC" DEBUG_LAUNCH_WEBRTC_INTERNALS = "VCD_DEBUG_LAUNCH_WEBRTC",
IPC_COMMAND = "VCD_IPC_COMMAND"
}
export const enum IpcCommands {
RPC_ACTIVITY = "rpc:activity",
RPC_INVITE = "rpc:invite",
RPC_DEEP_LINK = "rpc:link",
NAVIGATE_SETTINGS = "navigate:settings",
GET_LANGUAGES = "navigator.languages"
} }

View file

@ -22,6 +22,7 @@ export interface Settings {
clickTrayToShowHide?: boolean; clickTrayToShowHide?: boolean;
customTitleBar?: boolean; customTitleBar?: boolean;
enableSplashScreen?: boolean;
splashTheming?: boolean; splashTheming?: boolean;
splashColor?: string; splashColor?: string;
splashBackground?: string; splashBackground?: string;
@ -47,7 +48,7 @@ export interface State {
maximized?: boolean; maximized?: boolean;
minimized?: boolean; minimized?: boolean;
windowBounds?: Rectangle; windowBounds?: Rectangle;
displayid: int; displayId: int;
firstLaunch?: boolean; firstLaunch?: boolean;