Merge branch 'main' into main

This commit is contained in:
Tuxinal 2024-07-02 00:22:13 +03:30 committed by GitHub
commit d46611c660
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 3916 additions and 3006 deletions

View file

@ -13,10 +13,11 @@ body:
Make sure both Vesktop and Vencord are fully up to date. You can update Vencord by right-clicking the Vesktop tray icon and pressing "Update Vencord" Make sure both Vesktop and Vencord are fully up to date. You can update Vencord by right-clicking the Vesktop tray icon and pressing "Update Vencord"
Do not report any of the following issues: **DO NOT REPORT** any of the following issues:
- Purely graphical glitches like flickering, scaling issues, etc: Issue with your gpu. Nothing we can do, update drivers or disable hardware acceleration - Purely graphical glitches like flickering, scaling issues, etc: Issue with your gpu. Nothing we can do, update drivers or disable hardware acceleration
- Vencord related issues: This is the Vesktop repo, not Vencord - Vencord related issues: This is the Vesktop repo, not Vencord
- Screenshare not starting / black screening on Linux: Issue with your desktop environment, specifically its xdg-desktop-portal - **SCREENSHARE NOT STARTING** / black screening on Linux: Issue with your desktop environment, specifically its xdg-desktop-portal.
If you're on flatpak, try using native version. If that also doesn't work, you have to fix your systen. Inspect errors and google around.
Linux users: Please only report issues with supported packages (flatpak and any builds from the README / releases). Linux users: Please only report issues with supported packages (flatpak and any builds from the README / releases).
We do not support other packages, like the AUR or Nix packages, so please first make sure your issue is reproducible with official releases, We do not support other packages, like the AUR or Nix packages, so please first make sure your issue is reproducible with official releases,

View file

@ -1,7 +1,7 @@
name: 🛠️ Feature Request name: 🛠️ Feature Request
description: Create a feature request for Vesktop description: Request a feature for Vesktop
labels: [bug] labels: [enhancement]
title: "[Bug] <title>" title: "[Feature Request] <title>"
body: body:
- type: markdown - type: markdown

View file

@ -13,10 +13,10 @@ on:
jobs: jobs:
winget: winget:
name: Publish winget package name: Publish winget package
runs-on: ubuntu-latest runs-on: windows-latest
steps: steps:
- name: Submit package to Winget Community Repo - name: Submit package to Winget Community Repo
uses: vedantmgoyal2009/winget-releaser@e68d386d5d6a1cef8cb0fb5e62b77ebcb83e7d58 # v2 uses: vedantmgoyal2009/winget-releaser@4614300d5812e5df91cb02ef0edbece623d5dea8
with: with:
identifier: Vencord.Vesktop identifier: Vencord.Vesktop
token: ${{ secrets.WINGET_PAT }} token: ${{ secrets.WINGET_PAT }}

1
.npmrc
View file

@ -1 +1,2 @@
node-linker=hoisted node-linker=hoisted
package-manager-strict=false

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
@ -53,7 +53,7 @@ If you don't know the difference, pick amd64.
Below you can find unofficial packages created by the community. They are not officially supported by us, so before reporting issues, please first confirm the issue also happens on official builds. When in doubt, consult with their packager first. The flatpak and AppImage should work on any distro that [supports them](https://flatpak.org/setup/), so I recommend you just use those instead! Below you can find unofficial packages created by the community. They are not officially supported by us, so before reporting issues, please first confirm the issue also happens on official builds. When in doubt, consult with their packager first. The flatpak and AppImage should work on any distro that [supports them](https://flatpak.org/setup/), so I recommend you just use those instead!
- 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://wiki.nixos.org/wiki/Discord#Vesktop
- Windows - Scoop: https://scoop.sh/#/apps?q=Vesktop - Windows - Scoop: https://scoop.sh/#/apps?q=Vesktop
## Building from Source ## Building from Source

View file

@ -24,20 +24,20 @@
"updateMeta": "tsx scripts/utils/updateMeta.mts" "updateMeta": "tsx scripts/utils/updateMeta.mts"
}, },
"dependencies": { "dependencies": {
"arrpc": "github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c" "arrpc": "github:OpenAsar/arrpc#c62ec6a04c8d870530aa6944257fe745f6c59a24"
}, },
"optionalDependencies": { "optionalDependencies": {
"@vencord/venmic": "^3.4.2" "@vencord/venmic": "^6.1.0"
}, },
"devDependencies": { "devDependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@types/node": "^20.11.26", "@types/node": "^20.11.26",
"@types/react": "^18.2.65", "@types/react": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0", "@typescript-eslint/parser": "^7.2.0",
"@vencord/types": "^0.1.2", "@vencord/types": "^1.8.4",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"electron": "^29.1.1", "electron": "^31.0.1",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"esbuild": "^0.20.1", "esbuild": "^0.20.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
@ -55,7 +55,7 @@
"typescript": "^5.4.2", "typescript": "^5.4.2",
"xml-formatter": "^3.6.2" "xml-formatter": "^3.6.2"
}, },
"packageManager": "pnpm@8.11.0", "packageManager": "pnpm@9.1.0",
"engines": { "engines": {
"node": ">=18", "node": ">=18",
"pnpm": ">=8" "pnpm": ">=8"
@ -118,8 +118,7 @@
{ {
"target": "default", "target": "default",
"arch": [ "arch": [
"x64", "universal"
"arm64"
] ]
} }
], ],
@ -136,20 +135,21 @@
"icon": "build/icon.icns", "icon": "build/icon.icns",
"iconSize": 105, "iconSize": 105,
"window": { "window": {
"width": 512, "width": 512,
"height": 340 "height": 340
}, },
"contents": [
"contents": [{ {
"x": 140, "x": 140,
"y": 160 "y": 160
}, },
{ {
"x": 372, "x": 372,
"y": 160, "y": 160,
"type": "link", "type": "link",
"path": "/Applications" "path": "/Applications"
}] }
]
}, },
"nsis": { "nsis": {
"include": "build/installer.nsh", "include": "build/installer.nsh",
@ -157,12 +157,29 @@
}, },
"win": { "win": {
"target": [ "target": [
"nsis", {
"zip" "target": "nsis",
"arch": [
"x64",
"arm64"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
] ]
}, },
"publish": { "publish": {
"provider": "github" "provider": "github"
} }
},
"pnpm": {
"patchedDependencies": {
"arrpc@3.4.0": "patches/arrpc@3.4.0.patch"
}
} }
} }

14
patches/arrpc@3.4.0.patch Normal file
View file

@ -0,0 +1,14 @@
diff --git a/src/process/index.js b/src/process/index.js
index 97ea6514b54dd9c5df588c78f0397d31ab5f882a..c2bdbd6aaa5611bc6ff1d993beeb380b1f5ec575 100644
--- a/src/process/index.js
+++ b/src/process/index.js
@@ -5,8 +5,7 @@ import fs from 'node:fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8'));
+const DetectableDB = require('./detectable.json');
import * as Natives from './native/index.js';
const Native = Natives[process.platform];

File diff suppressed because it is too large Load diff

View file

@ -5,11 +5,22 @@
*/ */
import { app } from "electron"; import { app } from "electron";
import { existsSync, readdirSync, renameSync, rmdirSync } from "fs"; import { existsSync, mkdirSync, readdirSync, renameSync, rmdirSync } from "fs";
import { join } from "path"; import { dirname, join } from "path";
const vesktopDir = dirname(process.execPath);
export const PORTABLE =
process.platform === "win32" &&
!process.execPath.toLowerCase().endsWith("electron.exe") &&
!existsSync(join(vesktopDir, "Uninstall Vesktop.exe"));
const LEGACY_DATA_DIR = join(app.getPath("appData"), "VencordDesktop", "VencordDesktop"); const LEGACY_DATA_DIR = join(app.getPath("appData"), "VencordDesktop", "VencordDesktop");
export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR || join(app.getPath("userData")); export const DATA_DIR =
process.env.VENCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("userData")));
mkdirSync(DATA_DIR, { recursive: true });
// TODO: remove eventually // TODO: remove eventually
if (existsSync(LEGACY_DATA_DIR)) { if (existsSync(LEGACY_DATA_DIR)) {
try { try {

View file

@ -40,18 +40,23 @@ function init() {
app.commandLine.appendSwitch("disable-smooth-scrolling"); app.commandLine.appendSwitch("disable-smooth-scrolling");
} }
// disable renderer backgrounding to prevent the app from unloading when in the background
// https://github.com/electron/electron/issues/2822
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
app.commandLine.appendSwitch("disable-renderer-backgrounding");
app.commandLine.appendSwitch("disable-background-timer-throttling");
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
if (process.platform === "win32") {
disabledFeatures.push("CalculateNativeWinOcclusion");
}
// work around chrome 66 disabling autoplay by default // 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
disabledFeatures.push( disabledFeatures.push("WinRetrieveSuggestionsOnlyOnDemand", "HardwareMediaKeyHandling", "MediaSessionService");
"WinRetrieveSuggestionsOnlyOnDemand",
"HardwareMediaKeyHandling",
"MediaSessionService",
"WidgetLayering"
);
app.commandLine.appendSwitch("enable-features", [...new Set(enabledFeatures)].filter(Boolean).join(",")); app.commandLine.appendSwitch("enable-features", [...new Set(enabledFeatures)].filter(Boolean).join(","));
app.commandLine.appendSwitch("disable-features", [...new Set(disabledFeatures)].filter(Boolean).join(",")); app.commandLine.appendSwitch("disable-features", [...new Set(disabledFeatures)].filter(Boolean).join(","));

View file

@ -93,12 +93,8 @@ handle(IpcEvents.MAXIMIZE, e => {
} }
}); });
handle(IpcEvents.SPELLCHECK_SET_LANGUAGES, (_, languages: string[]) => { handleSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES, e => {
const ses = session.defaultSession; e.returnValue = session.defaultSession.availableSpellCheckerLanguages;
const available = ses.availableSpellCheckerLanguages;
const applicable = languages.filter(l => available.includes(l)).slice(0, 3);
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
}); });
handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => { handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => {

View file

@ -12,6 +12,8 @@ import {
Menu, Menu,
MenuItemConstructorOptions, MenuItemConstructorOptions,
nativeTheme, nativeTheme,
screen,
session,
Tray Tray
} from "electron"; } from "electron";
import { rm } from "fs/promises"; import { rm } from "fs/promises";
@ -269,7 +271,9 @@ function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
height: height ?? DEFAULT_HEIGHT height: height ?? DEFAULT_HEIGHT
} as BrowserWindowConstructorOptions; } as BrowserWindowConstructorOptions;
if (x != null && y != null) { const storedDisplay = screen.getAllDisplays().find(display => display.id === State.store.displayid);
if (x != null && y != null && storedDisplay) {
options.x = x; options.x = x;
options.y = y; options.y = y;
} }
@ -317,6 +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;
}; };
win.on("resize", saveBounds); win.on("resize", saveBounds);
@ -356,12 +361,27 @@ function initSettingsListeners(win: BrowserWindow) {
addSettingsListener("enableMenu", enabled => { addSettingsListener("enableMenu", enabled => {
win.setAutoHideMenuBar(enabled ?? false); win.setAutoHideMenuBar(enabled ?? false);
}); });
addSettingsListener("spellCheckLanguages", languages => initSpellCheckLanguages(win, languages));
}
async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) {
languages ??= await win.webContents.executeJavaScript("[...new Set(navigator.languages)]").catch(() => []);
if (!languages) return;
const ses = session.defaultSession;
const available = ses.availableSpellCheckerLanguages;
const applicable = languages.filter(l => available.includes(l)).slice(0, 5);
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
} }
function initSpellCheck(win: BrowserWindow) { function initSpellCheck(win: BrowserWindow) {
win.webContents.on("context-menu", (_, data) => { win.webContents.on("context-menu", (_, data) => {
win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions); win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions);
}); });
initSpellCheckLanguages(win, Settings.store.spellCheckLanguages);
} }
function createMainWindow() { function createMainWindow() {
@ -383,7 +403,9 @@ function createMainWindow() {
contextIsolation: true, contextIsolation: true,
devTools: true, devTools: true,
preload: join(__dirname, "preload.js"), preload: join(__dirname, "preload.js"),
spellcheck: true spellcheck: true,
// disable renderer backgrounding to prevent the app from unloading when in the background
backgroundThrottling: false
}, },
icon: ICON_PATH, icon: ICON_PATH,
frame: !noFrame, frame: !noFrame,
@ -408,6 +430,7 @@ function createMainWindow() {
autoHideMenuBar: enableMenu autoHideMenuBar: enableMenu
})); }));
win.setMenuBarVisibility(false); win.setMenuBarVisibility(false);
if (process.platform === "darwin" && customTitleBar) win.setWindowButtonVisibility(false);
win.on("close", e => { win.on("close", e => {
const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false; const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false;

View file

@ -12,11 +12,13 @@ export function registerMediaPermissionsHandler() {
session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => { session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => {
let granted = true; let granted = true;
if (details.mediaTypes?.includes("audio")) { if ("mediaTypes" in details) {
granted = await systemPreferences.askForMediaAccess("microphone"); if (details.mediaTypes?.includes("audio")) {
} granted &&= await systemPreferences.askForMediaAccess("microphone");
if (details.mediaTypes?.includes("video")) { }
granted &&= await systemPreferences.askForMediaAccess("camera"); if (details.mediaTypes?.includes("video")) {
granted &&= await systemPreferences.askForMediaAccess("camera");
}
} }
callback(granted); callback(granted);

View file

@ -35,11 +35,6 @@ function loadSettings<T extends object = any>(file: string, name: string) {
} }
export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop settings"); export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop settings");
if (Object.hasOwn(Settings.plain, "discordWindowsTitleBar")) {
Settings.plain.customTitleBar = Settings.plain.discordWindowsTitleBar;
delete Settings.plain.discordWindowsTitleBar;
Settings.markAsChanged();
}
export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord settings"); export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord settings");

View file

@ -4,13 +4,13 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import type { PatchBay as PatchBayType } from "@vencord/venmic"; import type { LinkData, Node, 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<PatchBayType["link"]>[0]; import { Settings } from "./settings";
let PatchBay: typeof PatchBayType | undefined; let PatchBay: typeof PatchBayType | undefined;
let patchBayInstance: PatchBayType | undefined; let patchBayInstance: PatchBayType | undefined;
@ -69,47 +69,64 @@ function getRendererAudioServicePid() {
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => { ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
const audioPid = getRendererAudioServicePid(); const audioPid = getRendererAudioServicePid();
const list = obtainVenmic() const { granularSelect } = Settings.store.audio ?? {};
?.list()
.filter(s => s["application.process.id"] !== audioPid)
.map(s => s["application.name"]);
const uniqueTargets = [...new Set(list)]; const targets = obtainVenmic()
?.list(granularSelect ? ["application.process.id"] : undefined)
.filter(s => s["application.process.id"] !== audioPid);
return list ? { ok: true, targets: uniqueTargets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated }; return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated };
}); });
ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[], workaround?: boolean) => { ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => {
const pid = getRendererAudioServicePid(); const pid = getRendererAudioServicePid();
const { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {};
const data: LinkData = { const data: LinkData = {
include: targets.map(target => ({ key: "application.name", value: target })), include,
exclude: [{ key: "application.process.id", value: pid }] exclude: [{ "application.process.id": pid }],
ignore_devices: ignoreDevices
}; };
if (ignoreInputMedia ?? true) {
data.exclude.push({ "media.class": "Stream/Input/Audio" });
}
if (ignoreVirtual) {
data.exclude.push({ "node.virtual": "true" });
}
if (workaround) { if (workaround) {
data.workaround = [ data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
{ key: "application.process.id", value: pid },
{ key: "media.name", value: "RecordStream" }
];
} }
return obtainVenmic()?.link(data); return obtainVenmic()?.link(data);
}); });
ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, workaround?: boolean, onlyDefaultSpeakers?: boolean) => { ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, exclude: Node[]) => {
const pid = getRendererAudioServicePid(); const pid = getRendererAudioServicePid();
const { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } =
Settings.store.audio ?? {};
const data: LinkData = { const data: LinkData = {
exclude: [{ key: "application.process.id", value: pid }], include: [],
exclude: [{ "application.process.id": pid }, ...exclude],
only_speakers: onlySpeakers,
ignore_devices: ignoreDevices,
only_default_speakers: onlyDefaultSpeakers only_default_speakers: onlyDefaultSpeakers
}; };
if (ignoreInputMedia ?? true) {
data.exclude.push({ "media.class": "Stream/Input/Audio" });
}
if (ignoreVirtual) {
data.exclude.push({ "node.virtual": "true" });
}
if (workaround) { if (workaround) {
data.workaround = [ data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
{ key: "application.process.id", value: pid },
{ key: "media.name", value: "RecordStream" }
];
} }
return obtainVenmic()?.link(data); return obtainVenmic()?.link(data);

View file

@ -4,6 +4,7 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import { Node } from "@vencord/venmic";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import type { Settings } from "shared/settings"; import type { Settings } from "shared/settings";
import type { LiteralUnion } from "type-fest"; import type { LiteralUnion } from "type-fest";
@ -40,7 +41,7 @@ export const VesktopNative = {
set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path) set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path)
}, },
spellcheck: { spellcheck: {
setLanguages: (languages: readonly string[]) => invoke<void>(IpcEvents.SPELLCHECK_SET_LANGUAGES, languages), getAvailableLanguages: () => sendSync<string[]>(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES),
onSpellcheckResult(cb: SpellCheckerResultCallback) { onSpellcheckResult(cb: SpellCheckerResultCallback) {
spellCheckCallbacks.add(cb); spellCheckCallbacks.add(cb);
}, },
@ -63,11 +64,10 @@ export const VesktopNative = {
virtmic: { virtmic: {
list: () => list: () =>
invoke< invoke<
{ ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: string[]; hasPipewirePulse: boolean } { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean }
>(IpcEvents.VIRT_MIC_LIST), >(IpcEvents.VIRT_MIC_LIST),
start: (targets: string[], workaround?: boolean) => invoke<void>(IpcEvents.VIRT_MIC_START, targets, workaround), start: (include: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START, include),
startSystem: (workaround?: boolean, onlyDefaultSpeakers?: boolean) => startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude),
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

@ -40,5 +40,3 @@ if (IS_DEV) {
}); });
} }
// #endregion // #endregion
VesktopNative.spellcheck.setLanguages(window.navigator.languages);

View file

@ -6,7 +6,7 @@
import "./screenSharePicker.css"; import "./screenSharePicker.css";
import { closeModal, Logger, Margins, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils"; import { closeModal, Logger, 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,
@ -19,8 +19,10 @@ import {
UserStore, UserStore,
useState useState
} from "@vencord/types/webpack/common"; } from "@vencord/types/webpack/common";
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 { isLinux, isWindows } from "renderer/utils"; import { isLinux, isWindows } from "renderer/utils";
const StreamResolutions = ["480", "720", "1080", "1440"] as const; const StreamResolutions = ["480", "720", "1080", "1440"] as const;
@ -31,14 +33,23 @@ const MediaEngineStore = findStoreLazy("MediaEngineStore");
export type StreamResolution = (typeof StreamResolutions)[number]; export type StreamResolution = (typeof StreamResolutions)[number];
export type StreamFps = (typeof StreamFps)[number]; export type StreamFps = (typeof StreamFps)[number];
type SpecialSource = "None" | "Entire System";
type AudioSource = SpecialSource | Node;
type AudioSources = SpecialSource | Node[];
interface AudioItem {
name: string;
value: AudioSource;
}
interface StreamSettings { interface StreamSettings {
resolution: StreamResolution; resolution: StreamResolution;
fps: StreamFps; fps: StreamFps;
audio: boolean; audio: boolean;
audioSource?: string;
contentHint?: string; contentHint?: string;
workaround?: boolean; includeSources?: AudioSources;
onlyDefaultSpeakers?: boolean; excludeSources?: AudioSources;
} }
export interface StreamPick extends StreamSettings { export interface StreamPick extends StreamSettings {
@ -63,20 +74,6 @@ 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) {
@ -132,13 +129,17 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
modalProps={props} modalProps={props}
submit={async v => { submit={async v => {
didSubmit = true; didSubmit = true;
if (v.audioSource && v.audioSource !== "None") {
if (v.audioSource === "Entire System") { if (v.includeSources && v.includeSources !== "None") {
await VesktopNative.virtmic.startSystem(v.workaround); if (v.includeSources === "Entire System") {
await VesktopNative.virtmic.startSystem(
!v.excludeSources || isSpecialSource(v.excludeSources) ? [] : v.excludeSources
);
} else { } else {
await VesktopNative.virtmic.start([v.audioSource], v.workaround); await VesktopNative.virtmic.start(v.includeSources);
} }
} }
resolve(v); resolve(v);
}} }}
close={() => { close={() => {
@ -173,6 +174,113 @@ function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScre
); );
} }
function AudioSettingsModal({
modalProps,
close,
setAudioSources
}: {
modalProps: any;
close: () => void;
setAudioSources: (s: AudioSources) => void;
}) {
const Settings = useSettings();
return (
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Modals.ModalHeader className="vcd-screen-picker-header">
<Forms.FormTitle tag="h2">Venmic Settings</Forms.FormTitle>
<Modals.ModalCloseButton onClick={close} />
</Modals.ModalHeader>
<Modals.ModalContent className="vcd-screen-picker-modal">
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, workaround: v })}
value={Settings.audio?.workaround ?? false}
note={
<>
Work around an issue that causes the microphone to be shared instead of the correct audio.
Only enable if you're experiencing this issue.
</>
}
>
Microphone Workaround
</Switch>
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, onlySpeakers: v })}
value={Settings.audio?.onlySpeakers ?? true}
note={
<>
When sharing entire desktop audio, only share apps that play to a speaker. You may want to
disable this when using "mix bussing".
</>
}
>
Only Speakers
</Switch>
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })}
value={Settings.audio?.onlyDefaultSpeakers ?? true}
note={
<>
When sharing entire desktop audio, only share apps that play to the <b>default</b> speakers.
You may want to disable this when using "mix bussing".
</>
}
>
Only Default Speakers
</Switch>
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })}
value={Settings.audio?.ignoreInputMedia ?? true}
note={<>Exclude nodes that are intended to capture audio.</>}
>
Ignore Inputs
</Switch>
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, ignoreVirtual: v })}
value={Settings.audio?.ignoreVirtual ?? false}
note={
<>
Exclude virtual nodes, such as nodes belonging to loopbacks. This might be useful when using
"mix bussing".
</>
}
>
Ignore Virtual
</Switch>
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, ignoreDevices: v })}
value={Settings.audio?.ignoreDevices ?? true}
note={<>Exclude device nodes, such as nodes belonging to microphones or speakers.</>}
>
Ignore Devices
</Switch>
<Switch
hideBorder
onChange={value => {
Settings.audio = { ...Settings.audio, granularSelect: value };
setAudioSources("None");
}}
value={Settings.audio?.granularSelect ?? false}
note={<>Allow to select applications more granularly.</>}
>
Granular Selection
</Switch>
</Modals.ModalContent>
<Modals.ModalFooter className="vcd-screen-picker-footer">
<Button color={Button.Colors.TRANSPARENT} onClick={close}>
Back
</Button>
</Modals.ModalFooter>
</Modals.ModalRoot>
);
}
function StreamSettings({ function StreamSettings({
source, source,
settings, settings,
@ -184,6 +292,8 @@ function StreamSettings({
setSettings: Dispatch<SetStateAction<StreamSettings>>; setSettings: Dispatch<SetStateAction<StreamSettings>>;
skipPicker: boolean; skipPicker: boolean;
}) { }) {
const Settings = useSettings();
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)),
{ {
@ -192,227 +302,346 @@ function StreamSettings({
} }
); );
const openSettings = () => {
const key = openModal(props => (
<AudioSettingsModal
modalProps={props}
close={() => props.onClose()}
setAudioSources={sources =>
setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources }))
}
/>
));
};
return ( return (
<div className="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
<img src={thumb} alt="" /> src={thumb}
<Text variant="text-sm/normal">{source.name}</Text> alt=""
</Card> className={isLinux ? "vcd-screen-picker-preview-img-linux" : "vcd-screen-picker-preview-img"}
/>
<Text variant="text-sm/normal">{source.name}</Text>
</Card>
<Forms.FormTitle>Stream Settings</Forms.FormTitle> <Forms.FormTitle>Stream Settings</Forms.FormTitle>
<Card className="vcd-screen-picker-card"> <Card className="vcd-screen-picker-card">
<div className="vcd-screen-picker-quality"> <div className="vcd-screen-picker-quality">
<section> <section>
<Forms.FormTitle>Resolution</Forms.FormTitle> <Forms.FormTitle>Resolution</Forms.FormTitle>
<div className="vcd-screen-picker-radios">
{StreamResolutions.map(res => (
<label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}>
<Text variant="text-sm/bold">{res}</Text>
<input
type="radio"
name="resolution"
value={res}
checked={settings.resolution === res}
onChange={() => setSettings(s => ({ ...s, resolution: res }))}
/>
</label>
))}
</div>
</section>
<section>
<Forms.FormTitle>Frame Rate</Forms.FormTitle>
<div className="vcd-screen-picker-radios">
{StreamFps.map(fps => (
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
<Text variant="text-sm/bold">{fps}</Text>
<input
type="radio"
name="fps"
value={fps}
checked={settings.fps === fps}
onChange={() => setSettings(s => ({ ...s, fps }))}
/>
</label>
))}
</div>
</section>
</div>
<div className="vcd-screen-picker-quality">
<section>
<Forms.FormTitle>Content Type</Forms.FormTitle>
<div>
<div className="vcd-screen-picker-radios"> <div className="vcd-screen-picker-radios">
{StreamResolutions.map(res => ( <label
<label className="vcd-screen-picker-radio"
className="vcd-screen-picker-radio" data-checked={settings.contentHint === "motion"}
data-checked={settings.resolution === res} >
> <Text variant="text-sm/bold">Prefer Smoothness</Text>
<Text variant="text-sm/bold">{res}</Text> <input
<input type="radio"
type="radio" name="contenthint"
name="resolution" value="motion"
value={res} checked={settings.contentHint === "motion"}
checked={settings.resolution === res} onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))}
onChange={() => setSettings(s => ({ ...s, resolution: res }))} />
/> </label>
</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>
</section> <div className="vcd-screen-picker-hint-description">
<p>
<section> Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange
<Forms.FormTitle>Frame Rate</Forms.FormTitle> for a much sharper and clearer image.
<div className="vcd-screen-picker-radios"> </p>
{StreamFps.map(fps => (
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
<Text variant="text-sm/bold">{fps}</Text>
<input
type="radio"
name="fps"
value={fps}
checked={settings.fps === fps}
onChange={() => setSettings(s => ({ ...s, fps }))}
/>
</label>
))}
</div> </div>
</section> </div>
</div> {isWindows && (
<div className="vcd-screen-picker-quality"> <Switch
<section> value={settings.audio}
<Forms.FormTitle>Content Type</Forms.FormTitle> onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
<div> hideBorder
<div className="vcd-screen-picker-radios"> className="vcd-screen-picker-audio"
<label >
className="vcd-screen-picker-radio" Stream With Audio
data-checked={settings.contentHint === "motion"} </Switch>
> )}
<Text variant="text-sm/bold">Prefer Smoothness</Text> </section>
<input </div>
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>
</section>
</div>
</Card>
</div>
<div>
{isWindows && (
<Switch
value={settings.audio}
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
hideBorder
className="vcd-screen-picker-audio"
>
Stream With Audio
</Switch>
)}
{isLinux && ( {isLinux && (
<AudioSourcePickerLinux <AudioSourcePickerLinux
audioSource={settings.audioSource} openSettings={openSettings}
workaround={settings.workaround} includeSources={settings.includeSources}
onlyDefaultSpeakers={settings.onlyDefaultSpeakers} excludeSources={settings.excludeSources}
setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))} granularSelect={Settings.audio?.granularSelect}
setWorkaround={value => setSettings(s => ({ ...s, workaround: value }))} setIncludeSources={sources => setSettings(s => ({ ...s, includeSources: sources }))}
setOnlyDefaultSpeakers={value => setSettings(s => ({ ...s, onlyDefaultSpeakers: value }))} setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))}
/> />
)} )}
</div> </Card>
</div> </div>
); );
} }
function isSpecialSource(value?: AudioSource | AudioSources): value is SpecialSource {
return typeof value === "string";
}
function hasMatchingProps(value: Node, other: Node) {
return Object.keys(value).every(key => value[key] === other[key]);
}
function mapToAudioItem(node: AudioSource, granularSelect?: boolean): AudioItem[] {
if (isSpecialSource(node)) {
return [{ name: node, value: node }];
}
const rtn: AudioItem[] = [];
const name = node["application.name"];
if (name) {
rtn.push({ name: name, value: { "application.name": name } });
}
if (!granularSelect) {
return rtn;
}
const binary = node["application.process.binary"];
if (!name) {
rtn.push({ name: binary, value: { "application.process.binary": binary } });
}
const pid = node["application.process.id"];
const first = rtn[0];
const firstValues = first.value as Node;
rtn.push({
name: `${first.name} (${pid})`,
value: { ...firstValues, "application.process.id": pid }
});
const mediaName = node["media.name"];
if (mediaName) {
rtn.push({
name: `${first.name} [${mediaName}]`,
value: { ...firstValues, "media.name": mediaName }
});
}
const mediaClass = node["media.class"];
if (!mediaClass) {
return rtn;
}
rtn.push({
name: `${first.name} [${mediaClass}]`,
value: { ...firstValues, "media.class": mediaClass }
});
return rtn;
}
function isItemSelected(sources?: AudioSources) {
return (value: AudioSource) => {
if (!sources) {
return false;
}
if (isSpecialSource(sources) || isSpecialSource(value)) {
return sources === value;
}
return sources.some(source => hasMatchingProps(source, value));
};
}
function updateItems(setSources: (s: AudioSources) => void, sources?: AudioSources) {
return (value: AudioSource) => {
if (isSpecialSource(value)) {
setSources(value);
return;
}
if (isSpecialSource(sources)) {
setSources([value]);
return;
}
if (isItemSelected(sources)(value)) {
setSources(sources?.filter(x => !hasMatchingProps(x, value)) ?? "None");
return;
}
setSources([...(sources || []), value]);
};
}
function AudioSourcePickerLinux({ function AudioSourcePickerLinux({
audioSource, includeSources,
workaround, excludeSources,
onlyDefaultSpeakers, granularSelect,
setAudioSource, openSettings,
setWorkaround, setIncludeSources,
setOnlyDefaultSpeakers setExcludeSources
}: { }: {
audioSource?: string; includeSources?: AudioSources;
workaround?: boolean; excludeSources?: AudioSources;
onlyDefaultSpeakers?: boolean; granularSelect?: boolean;
setAudioSource(s: string): void; openSettings: () => void;
setWorkaround(b: boolean): void; setIncludeSources: (s: AudioSources) => void;
setOnlyDefaultSpeakers(b: boolean): void; setExcludeSources: (s: AudioSources) => void;
}) { }) {
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true } fallbackValue: { ok: true, targets: [], hasPipewirePulse: true }
}); });
const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null;
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true; const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false); const [ignorePulseWarning, setIgnorePulseWarning] = useState(false);
if (!sources.ok && sources.isGlibCxxOutdated) {
return (
<Forms.FormText>
Failed to retrieve Audio Sources because your C++ library is too old to run
<a href="https://github.com/Vencord/venmic" target="_blank">
venmic
</a>
. See{" "}
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
this guide
</a>{" "}
for possible solutions.
</Forms.FormText>
);
}
if (!hasPipewirePulse && !ignorePulseWarning) {
return (
<Text variant="text-sm/normal">
Could not find pipewire-pulse. See{" "}
<a href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install" target="_blank">
this guide
</a>{" "}
on how to switch to pipewire. <br />
You can still continue, however, please{" "}
<b>beware that you can only share audio of apps that are running under pipewire</b>.{" "}
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing!</a>
</Text>
);
}
const specialSources: SpecialSource[] = ["None", "Entire System"] as const;
const uniqueName = (value: AudioItem, index: number, list: AudioItem[]) =>
list.findIndex(x => x.name === value.name) === index;
const allSources = sources.ok
? [...specialSources, ...sources.targets]
.map(target => mapToAudioItem(target, granularSelect))
.flat()
.filter(uniqueName)
: [];
return ( return (
<> <>
<Forms.FormTitle>Audio Settings</Forms.FormTitle> <div className={includeSources === "Entire System" ? "vcd-screen-picker-quality" : undefined}>
<Card className="vcd-screen-picker-card"> <section>
{loading ? ( <Forms.FormTitle>{loading ? "Loading Sources..." : "Audio Sources"}</Forms.FormTitle>
<Forms.FormTitle>Loading Audio Sources...</Forms.FormTitle> <Select
) : ( options={allSources.map(({ name, value }) => ({
<Forms.FormTitle>Audio Source</Forms.FormTitle> label: name,
)} value: value,
default: name === "None"
{!sources.ok && sources.isGlibCxxOutdated && ( }))}
<Forms.FormText> isSelected={isItemSelected(includeSources)}
Failed to retrieve Audio Sources because your C++ library is too old to run select={updateItems(setIncludeSources, includeSources)}
<a href="https://github.com/Vencord/venmic" target="_blank"> serialize={String}
venmic popoutPosition="top"
</a> closeOnSelect={false}
. See{" "} />
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank"> </section>
this guide {includeSources === "Entire System" && (
</a>{" "} <section>
for possible solutions. <Forms.FormTitle>Exclude Sources</Forms.FormTitle>
</Forms.FormText>
)}
{hasPipewirePulse || ignorePulseWarning ? (
allSources && (
<Select <Select
options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))} options={allSources
isSelected={s => s === audioSource} .filter(x => x.name !== "Entire System")
select={setAudioSource} .map(({ name, value }) => ({
label: name,
value: value,
default: name === "None"
}))}
isSelected={isItemSelected(excludeSources)}
select={updateItems(setExcludeSources, excludeSources)}
serialize={String} serialize={String}
popoutPosition="top"
closeOnSelect={false}
/> />
) </section>
) : (
<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>
)} )}
</div>
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} /> <Button
color={Button.Colors.TRANSPARENT}
<Switch onClick={openSettings}
onChange={setWorkaround} className="vcd-screen-picker-settings-button"
value={workaround ?? false} >
note={ Open Audio Settings
<> </Button>
Work around an issue that causes the microphone to be shared instead of the correct audio.
Only enable if you're experiencing this issue.
</>
}
>
Microphone Workaround
</Switch>
<Switch
hideBorder
onChange={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>
</> </>
); );
} }
@ -432,10 +661,11 @@ 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: "1080", resolution: "720",
fps: "60", fps: "30",
contentHint: "motion", contentHint: "motion",
audio: true audio: true,
includeSources: "None"
}); });
return ( return (

View file

@ -11,17 +11,6 @@
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 { .vcd-screen-picker-card {
flex-grow: 1; flex-grow: 1;
} }
@ -38,7 +27,7 @@
} }
.vcd-screen-picker-selected img { .vcd-screen-picker-selected img {
border: 2px solid var(--brand-experiment); border: 2px solid var(--brand-500);
border-radius: 3px; border-radius: 3px;
} }
@ -49,7 +38,7 @@
} }
.vcd-screen-picker-grid label:hover { .vcd-screen-picker-grid label:hover {
outline: 2px solid var(--brand-experiment); outline: 2px solid var(--brand-500);
} }
@ -67,8 +56,13 @@
box-sizing: border-box; box-sizing: border-box;
} }
.vcd-screen-picker-preview img { .vcd-screen-picker-preview-img-linux {
width: 100%; width: 60%;
margin-bottom: 0.5em;
}
.vcd-screen-picker-preview-img {
width: 90%;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@ -96,8 +90,8 @@
} }
.vcd-screen-picker-radio[data-checked="true"] { .vcd-screen-picker-radio[data-checked="true"] {
background-color: var(--brand-experiment); background-color: var(--brand-500);
border-color: var(--brand-experiment); border-color: var(--brand-500);
} }
.vcd-screen-picker-radio[data-checked="true"] h2 { .vcd-screen-picker-radio[data-checked="true"] h2 {
@ -115,6 +109,11 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.vcd-screen-picker-settings-button {
margin-left: auto;
margin-top: 0.3rem;
}
.vcd-screen-picker-radios { .vcd-screen-picker-radios {
display: flex; display: flex;
width: 100%; width: 100%;
@ -143,4 +142,4 @@
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
font-weight: 400; font-weight: 400;
} }

View file

@ -12,7 +12,7 @@ import "./themedSplash";
console.log("read if cute :3"); console.log("read if cute :3");
export * as Components from "./components"; export * as Components from "./components";
import { findByPropsLazy } from "@vencord/types/webpack"; import { findByPropsLazy, onceReady } from "@vencord/types/webpack";
import { FluxDispatcher } from "@vencord/types/webpack/common"; import { FluxDispatcher } from "@vencord/types/webpack/common";
import SettingsUi from "./components/settings/Settings"; import SettingsUi from "./components/settings/Settings";
@ -54,8 +54,10 @@ const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as any as {
handleEvent(e: MessageEvent): void; handleEvent(e: MessageEvent): void;
}; };
VesktopNative.arrpc.onActivity(data => { VesktopNative.arrpc.onActivity(async data => {
if (!Settings.store.arRPC) return; if (!Settings.store.arRPC) return;
await onceReady;
arRPC.handleEvent(new MessageEvent("message", { data })); arRPC.handleEvent(new MessageEvent("message", { data }));
}); });

View file

@ -13,7 +13,7 @@ addPatch({
replacement: { replacement: {
// FIXME: fix eslint rule // FIXME: fix eslint rule
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
match: /\.isPlatformEmbedded(?=\?\i\.DesktopNotificationTypes\.ALL)/g, match: /\.isPlatformEmbedded(?=\?\i\.\i\.ALL)/g,
replace: "$&||true" replace: "$&||true"
} }
} }

View file

@ -12,7 +12,7 @@ addPatch({
find: "lastOutputSystemDevice.justChanged", find: "lastOutputSystemDevice.justChanged",
replacement: { replacement: {
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
match: /(\i)\.default\.getState\(\).neverShowModal/, match: /(\i)\.\i\.getState\(\).neverShowModal/,
replace: "$& || $self.shouldIgnore($1)" replace: "$& || $self.shouldIgnore($1)"
} }
} }

View file

@ -6,7 +6,8 @@
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 { FluxDispatcher, Menu, useStateFromStores } from "@vencord/types/webpack/common"; import { FluxDispatcher, Menu, useMemo, useStateFromStores } from "@vencord/types/webpack/common";
import { useSettings } from "renderer/settings";
import { addPatch } from "./shared"; import { addPatch } from "./shared";
@ -50,7 +51,16 @@ addContextMenuPatch("textarea-context", children => {
const spellCheckEnabled = useStateFromStores([SpellCheckStore], () => SpellCheckStore.isEnabled()); const spellCheckEnabled = useStateFromStores([SpellCheckStore], () => SpellCheckStore.isEnabled());
const hasCorrections = Boolean(word && corrections?.length); const hasCorrections = Boolean(word && corrections?.length);
children.push( const availableLanguages = useMemo(VesktopNative.spellcheck.getAvailableLanguages, []);
const settings = useSettings();
const spellCheckLanguages = (settings.spellCheckLanguages ??= [...new Set(navigator.languages)]);
const pasteSectionIndex = children.findIndex(c => c?.props?.children?.some(c => c?.props?.id === "paste"));
children.splice(
pasteSectionIndex === -1 ? children.length : pasteSectionIndex,
0,
<Menu.MenuGroup> <Menu.MenuGroup>
{hasCorrections && ( {hasCorrections && (
<> <>
@ -69,14 +79,39 @@ addContextMenuPatch("textarea-context", children => {
/> />
</> </>
)} )}
<Menu.MenuCheckboxItem
id="vcd-spellcheck-enabled" <Menu.MenuItem id="vcd-spellcheck-settings" label="Spellcheck Settings">
label="Enable Spellcheck" <Menu.MenuCheckboxItem
checked={spellCheckEnabled} id="vcd-spellcheck-enabled"
action={() => { label="Enable Spellcheck"
FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" }); checked={spellCheckEnabled}
}} action={() => {
/> FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
}}
/>
<Menu.MenuItem id="vcd-spellcheck-languages" label="Languages" disabled={!spellCheckEnabled}>
{availableLanguages.map(lang => {
const isEnabled = spellCheckLanguages.includes(lang);
return (
<Menu.MenuCheckboxItem
id={"vcd-spellcheck-lang-" + lang}
label={lang}
checked={isEnabled}
disabled={!isEnabled && spellCheckLanguages.length >= 5}
action={() => {
const newSpellCheckLanguages = spellCheckLanguages.filter(l => l !== lang);
if (newSpellCheckLanguages.length === spellCheckLanguages.length) {
newSpellCheckLanguages.push(lang);
}
settings.spellCheckLanguages = newSpellCheckLanguages;
}}
/>
);
})}
</Menu.MenuItem>
</Menu.MenuItem>
</Menu.MenuGroup> </Menu.MenuGroup>
); );
}); });

View file

@ -29,7 +29,7 @@ export const enum IpcEvents {
UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD", UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD",
UPDATE_IGNORE = "VCD_UPDATE_IGNORE", UPDATE_IGNORE = "VCD_UPDATE_IGNORE",
SPELLCHECK_SET_LANGUAGES = "VCD_SPELLCHECK_SET_LANGUAGES", SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES",
SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT", SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT",
SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING", SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING",
SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY", SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY",

View file

@ -21,8 +21,6 @@ export interface Settings {
appBadge?: boolean; appBadge?: boolean;
disableMinSize?: boolean; disableMinSize?: boolean;
clickTrayToShowHide?: boolean; clickTrayToShowHide?: boolean;
/** @deprecated use customTitleBar */
discordWindowsTitleBar?: boolean;
customTitleBar?: boolean; customTitleBar?: boolean;
checkUpdates?: boolean; checkUpdates?: boolean;
@ -30,12 +28,27 @@ export interface Settings {
splashTheming?: boolean; splashTheming?: boolean;
splashColor?: string; splashColor?: string;
splashBackground?: string; splashBackground?: string;
spellCheckLanguages?: string[];
audio?: {
workaround?: boolean;
granularSelect?: boolean;
ignoreVirtual?: boolean;
ignoreDevices?: boolean;
ignoreInputMedia?: boolean;
onlySpeakers?: boolean;
onlyDefaultSpeakers?: boolean;
};
} }
export interface State { export interface State {
maximized?: boolean; maximized?: boolean;
minimized?: boolean; minimized?: boolean;
windowBounds?: Rectangle; windowBounds?: Rectangle;
displayid: int;
skippedUpdate?: string; skippedUpdate?: string;
firstLaunch?: boolean; firstLaunch?: boolean;

View file

@ -5,6 +5,7 @@
*/ */
import { app, BrowserWindow, shell } from "electron"; import { app, BrowserWindow, shell } from "electron";
import { PORTABLE } from "main/constants";
import { Settings, State } from "main/settings"; import { Settings, State } from "main/settings";
import { handle } from "main/utils/ipcWrappers"; import { handle } from "main/utils/ipcWrappers";
import { makeLinksOpenExternally } from "main/utils/makeLinksOpenExternally"; import { makeLinksOpenExternally } from "main/utils/makeLinksOpenExternally";
@ -23,17 +24,12 @@ let updateData: UpdateData;
handle(IpcEvents.UPDATER_GET_DATA, () => updateData); handle(IpcEvents.UPDATER_GET_DATA, () => updateData);
handle(IpcEvents.UPDATER_DOWNLOAD, () => { handle(IpcEvents.UPDATER_DOWNLOAD, () => {
const portable = !!process.env.PORTABLE_EXECUTABLE_FILE;
const { assets } = updateData.release; const { assets } = updateData.release;
const url = (() => { const url = (() => {
switch (process.platform) { switch (process.platform) {
case "win32": case "win32":
return assets.find(a => { return assets.find(a => {
if (!a.name.endsWith(".exe")) return false; return a.name.endsWith(PORTABLE ? "win.zip" : ".exe");
const isSetup = a.name.includes("Setup");
return portable ? !isSetup : isSetup;
})!.browser_download_url; })!.browser_download_url;
case "darwin": case "darwin":
return assets.find(a => return assets.find(a =>