Merge branch 'main' into main
This commit is contained in:
commit
d46611c660
26 changed files with 3916 additions and 3006 deletions
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -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,
|
||||||
|
|
6
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
6
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
|
@ -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
|
||||||
|
|
4
.github/workflows/winget-submission.yml
vendored
4
.github/workflows/winget-submission.yml
vendored
|
@ -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
1
.npmrc
|
@ -1 +1,2 @@
|
||||||
node-linker=hoisted
|
node-linker=hoisted
|
||||||
|
package-manager-strict=false
|
|
@ -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
|
||||||
|
|
63
package.json
63
package.json
|
@ -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
14
patches/arrpc@3.4.0.patch
Normal 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];
|
5877
pnpm-lock.yaml
5877
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -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 {
|
||||||
|
|
|
@ -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(","));
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -40,5 +40,3 @@ if (IS_DEV) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
VesktopNative.spellcheck.setLanguages(window.navigator.languages);
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }));
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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",
|
||||||
|
|
17
src/shared/settings.d.ts
vendored
17
src/shared/settings.d.ts
vendored
|
@ -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;
|
||||||
|
|
|
@ -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 =>
|
||||||
|
|
Loading…
Reference in a new issue