Merge branch 'main' into integrated-tray-icon

- New file TrayNotificationBadgeToggle.tsx according to refactor
This commit is contained in:
Albert Zhang 2024-03-13 14:32:34 -04:00
commit ba7d0aef41
No known key found for this signature in database
GPG key ID: D74C859E94CA6DDC
27 changed files with 1630 additions and 872 deletions

View file

@ -7,26 +7,52 @@ assignees: ''
--- ---
<!--
Please do not open issues for the following things. We cannot help you with them:
- "Vesktop.app is damaged" on MacOs ~ Fake issue created by crApple. Google how to fix it https://google.it/search?q=fix+app+is+damaged
- Screenshare does not start / is black ~ This is an issue with your desktop environment, specifically its xdg-desktop-portal
- Purely graphical glitches, like flickering, scaling issues, short whitescreens, etc ~ These are most likely issues with your GPU. try to disable hardware acceleration
- Vencord related issues ~ This is the Vesktop repo, not Vencord
- Getting logged out after restart ~ If you use DevTools, make sure you have NoDevtoolsWarning enabled. Otherwise try reinstalling Vesktop
-->
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is.
<!-- A clear and concise description of what the bug is. -->
**To Reproduce** **To Reproduce**
<!--
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
-->
**Expected behavior** **Expected behavior**
A clear and concise description of what you expected to happen.
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem.
<!-- If applicable, add screenshots to help explain your problem. -->
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS/Distro: [e.g. Windows / Fedora Linux / MacOs] - OS/Distro: [e.g. Windows / Fedora Linux / MacOs]
- Desktop Environment (linux only): [e.g. gnome, kde, sway] - Desktop Environment (linux only): [e.g. gnome, kde, sway]
- Version: [e.g. 22] - Version: [e.g. 22]
**Command line output**
<!-- Run vesktop from the command line. Include the relevant command line output here: -->
```
paste inside these backticks
```
**Additional context** **Additional context**
Add any other context about the problem here.
<!-- Add any other context about the problem here. -->

View file

@ -54,6 +54,7 @@ Below you can find unofficial packages created by the community. They are not of
- Arch Linux: [Vesktop on the Arch user repository](https://aur.archlinux.org/packages?K=vesktop) - Arch Linux: [Vesktop on the Arch user repository](https://aur.archlinux.org/packages?K=vesktop)
- NixOS: https://nixos.wiki/wiki/Discord#Vesktop - NixOS: https://nixos.wiki/wiki/Discord#Vesktop
- Windows - Scoop: https://scoop.sh/#/apps?q=Vesktop
## Building from Source ## Building from Source

View file

@ -28,6 +28,24 @@
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="1.5.1" date="2024-03-12" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.1</url>
<description>
<p>New Features</p>
<ul>
<li>Added categories to Vesktop settings to reduce visual clutter by @justin13888</li>
<li>Added support for Vencord's transparent window options</li>
</ul>
<p>Fixes</p>
<ul>
<li>Fixed ugly error popups when starting Vesktop without working internet connection</li>
<li>Fixed popout title bars on Windows</li>
<li>Fixed spellcheck entries</li>
<li>Fixed screenshare audio using microphone on debian, by @Curve</li>
<li>Fixed a bug where autostart on Linux won't preserve command line flags</li>
</ul>
</description>
</release>
<release version="1.5.0" date="2024-01-16" type="stable"> <release version="1.5.0" date="2024-01-16" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.0</url> <url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.0</url>
<description> <description>
@ -162,11 +180,7 @@
<control>voice</control> <control>voice</control>
<display_length compare="ge">760</display_length> <display_length compare="ge">760</display_length>
<display_length compare="le">1200</display_length> <display_length compare="le">1200</display_length>
<internet>always</internet>
</recommends> </recommends>
<supports>
<internet>always</internet>
</supports>
<content_rating type="oars-1.1"> <content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute> <content_attribute id="social-audio">intense</content_attribute>

View file

@ -1,6 +1,6 @@
{ {
"name": "vesktop", "name": "vesktop",
"version": "1.5.0", "version": "1.5.1",
"private": true, "private": true,
"description": "", "description": "",
"keywords": [], "keywords": [],
@ -27,35 +27,35 @@
"arrpc": "github:OpenAsar/arrpc#98879cae0565e6fce34e4cb6f544bf42c6a7e7c8" "arrpc": "github:OpenAsar/arrpc#98879cae0565e6fce34e4cb6f544bf42c6a7e7c8"
}, },
"optionalDependencies": { "optionalDependencies": {
"@vencord/venmic": "^3.2.3" "@vencord/venmic": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@types/node": "^20.11.2", "@types/node": "^20.11.26",
"@types/react": "^18.2.48", "@types/react": "^18.2.65",
"@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^6.19.0", "@typescript-eslint/parser": "^7.2.0",
"@vencord/types": "^0.1.2", "@vencord/types": "^0.1.2",
"dotenv": "^16.3.1", "dotenv": "^16.4.5",
"electron": "^28.1.3", "electron": "^29.1.1",
"electron-builder": "^24.9.1", "electron-builder": "^24.13.3",
"esbuild": "^0.19.11", "esbuild": "^0.20.1",
"eslint": "^8.56.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-license-header": "^0.6.0", "eslint-plugin-license-header": "^0.6.0",
"eslint-plugin-path-alias": "^1.0.0", "eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-unused-imports": "^3.0.0", "eslint-plugin-unused-imports": "^3.1.0",
"prettier": "^3.2.2", "prettier": "^3.2.5",
"sharp": "^0.33.0", "sharp": "^0.33.0",
"sharp-ico": "^0.1.5", "sharp-ico": "^0.1.5",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"tsx": "^4.7.0", "tsx": "^4.7.1",
"type-fest": "^4.9.0", "type-fest": "^4.12.0",
"typescript": "^5.3.3", "typescript": "^5.4.2",
"xml-formatter": "^3.6.0" "xml-formatter": "^3.6.2"
}, },
"packageManager": "pnpm@8.11.0", "packageManager": "pnpm@8.11.0",
"engines": { "engines": {

File diff suppressed because it is too large Load diff

View file

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

View file

@ -5,7 +5,7 @@
*/ */
import { app } from "electron"; import { app } from "electron";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"; import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
import { join } from "path"; import { join } from "path";
interface AutoStart { interface AutoStart {
@ -17,7 +17,16 @@ interface AutoStart {
function makeAutoStartLinux(): AutoStart { function makeAutoStartLinux(): AutoStart {
const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config"); const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config");
const dir = join(configDir, "autostart"); const dir = join(configDir, "autostart");
const file = join(dir, "vencord.desktop"); const file = join(dir, "vesktop.desktop");
// IM STUPID
const legacyName = join(dir, "vencord.desktop");
if (existsSync(legacyName)) renameSync(legacyName, file);
// "Quoting must be done by enclosing the argument between double quotes and escaping the double quote character,
// backtick character ("`"), dollar sign ("$") and backslash character ("\") by preceding it with an additional backslash character"
// https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
const commandLine = process.argv.map(arg => '"' + arg.replace(/["$`\\]/g, "\\$&") + '"').join(" ");
return { return {
isEnabled: () => existsSync(file), isEnabled: () => existsSync(file),
@ -25,12 +34,11 @@ function makeAutoStartLinux(): AutoStart {
const desktopFile = ` const desktopFile = `
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Version=1.0 Name=Vesktop
Name=Vencord Comment=Vesktop autostart script
Comment=Vencord autostart script Exec=${commandLine}
Exec=${process.execPath}
Terminal=false
StartupNotify=false StartupNotify=false
Terminal=false
`.trim(); `.trim();
mkdirSync(dir, { recursive: true }); mkdirSync(dir, { recursive: true });

View file

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

View file

@ -25,13 +25,13 @@ import { ICON_PATH, TRAY_ICON_PATH } from "../shared/paths";
import { createAboutWindow } from "./about"; import { createAboutWindow } from "./about";
import { initArRPC } from "./arrpc"; import { initArRPC } from "./arrpc";
import { import {
BrowserUserAgent,
DATA_DIR, DATA_DIR,
DEFAULT_HEIGHT, DEFAULT_HEIGHT,
DEFAULT_WIDTH, DEFAULT_WIDTH,
MessageBoxChoice, MessageBoxChoice,
MIN_HEIGHT, MIN_HEIGHT,
MIN_WIDTH, MIN_WIDTH,
UserAgent,
VENCORD_FILES_DIR VENCORD_FILES_DIR
} from "./constants"; } from "./constants";
import { Settings, State, VencordSettings } from "./settings"; import { Settings, State, VencordSettings } from "./settings";
@ -430,7 +430,7 @@ function createMainWindow() {
initSettingsListeners(win); initSettingsListeners(win);
initSpellCheck(win); initSpellCheck(win);
win.webContents.setUserAgent(UserAgent); win.webContents.setUserAgent(BrowserUserAgent);
const subdomain = const subdomain =
Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb" Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb"

View file

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

View file

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

View file

@ -4,11 +4,14 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import type { PatchBay } from "@vencord/venmic";
import { app, ipcMain } from "electron"; import { app, ipcMain } from "electron";
import { join } from "path"; import { join } from "path";
import { IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths"; import { STATIC_DIR } from "shared/paths";
type LinkData = Parameters<PatchBay["link"]>[0];
let initialized = false; let initialized = false;
let patchBay: import("@vencord/venmic").PatchBay | undefined; let patchBay: import("@vencord/venmic").PatchBay | undefined;
let isGlibcxxToOld = false; let isGlibcxxToOld = false;
@ -51,17 +54,39 @@ ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
: { ok: false, isGlibcxxToOld }; : { ok: false, isGlibcxxToOld };
}); });
ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[]) => ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[], workaround?: boolean) => {
obtainVenmic()?.link({ const pid = getRendererAudioServicePid();
include: targets.map(target => ({ key: "application.name", value: target })),
exclude: [{ key: "application.process.id", value: getRendererAudioServicePid() }]
})
);
ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, () => const data: LinkData = {
obtainVenmic()?.link({ include: targets.map(target => ({ key: "application.name", value: target })),
exclude: [{ key: "application.process.id", value: getRendererAudioServicePid() }] exclude: [{ key: "application.process.id", value: pid }]
}) };
);
if (workaround) {
data.workaround = [
{ key: "application.process.id", value: pid },
{ key: "media.name", value: "RecordStream" }
];
}
return obtainVenmic()?.link(data);
});
ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, workaround?: boolean) => {
const pid = getRendererAudioServicePid();
const data: LinkData = {
exclude: [{ key: "application.process.id", value: pid }]
};
if (workaround) {
data.workaround = [
{ key: "application.process.id", value: pid },
{ key: "media.name", value: "RecordStream" }
];
}
return obtainVenmic()?.link(data);
});
ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink()); ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink());

View file

@ -63,8 +63,8 @@ export const VesktopNative = {
virtmic: { virtmic: {
list: () => list: () =>
invoke<{ ok: false; isGlibcxxToOld: boolean } | { ok: true; targets: string[] }>(IpcEvents.VIRT_MIC_LIST), invoke<{ ok: false; isGlibcxxToOld: boolean } | { ok: true; targets: string[] }>(IpcEvents.VIRT_MIC_LIST),
start: (targets: string[]) => invoke<void>(IpcEvents.VIRT_MIC_START, targets), start: (targets: string[], workaround?: boolean) => invoke<void>(IpcEvents.VIRT_MIC_START, targets, workaround),
startSystem: () => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM), startSystem: (workaround?: boolean) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, workaround),
stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP) stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP)
}, },
arrpc: { arrpc: {

View file

@ -6,7 +6,7 @@
import "./screenSharePicker.css"; import "./screenSharePicker.css";
import { closeModal, Modals, openModal, useAwaiter } from "@vencord/types/utils"; import { closeModal, Margins, Modals, openModal, useAwaiter } from "@vencord/types/utils";
import { findStoreLazy, onceReady } from "@vencord/types/webpack"; import { findStoreLazy, onceReady } from "@vencord/types/webpack";
import { import {
Button, Button,
@ -36,6 +36,7 @@ interface StreamSettings {
fps: StreamFps; fps: StreamFps;
audio: boolean; audio: boolean;
audioSource?: string; audioSource?: string;
workaround?: boolean;
} }
export interface StreamPick extends StreamSettings { export interface StreamPick extends StreamSettings {
@ -83,11 +84,14 @@ addPatch({
if (isLinux) { if (isLinux) {
onceReady.then(() => { onceReady.then(() => {
FluxDispatcher.subscribe("VOICE_STATE_UPDATES", e => { FluxDispatcher.subscribe("STREAM_CLOSE", ({ streamKey }: { streamKey: string }) => {
for (const state of e.voiceStates) { const owner = streamKey.split(":").at(-1);
if (state.userId === UserStore.getCurrentUser().id && state.oldChannelId && !state.channelId)
VesktopNative.virtmic.stop(); if (owner !== UserStore.getCurrentUser().id) {
return;
} }
VesktopNative.virtmic.stop();
}); });
}); });
} }
@ -104,9 +108,9 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
didSubmit = true; didSubmit = true;
if (v.audioSource && v.audioSource !== "None") { if (v.audioSource && v.audioSource !== "None") {
if (v.audioSource === "Entire System") { if (v.audioSource === "Entire System") {
await VesktopNative.virtmic.startSystem(); await VesktopNative.virtmic.startSystem(v.workaround);
} else { } else {
await VesktopNative.virtmic.start([v.audioSource]); await VesktopNative.virtmic.start([v.audioSource], v.workaround);
} }
} }
resolve(v); resolve(v);
@ -225,7 +229,9 @@ function StreamSettings({
{isLinux && ( {isLinux && (
<AudioSourcePickerLinux <AudioSourcePickerLinux
audioSource={settings.audioSource} audioSource={settings.audioSource}
workaround={settings.workaround}
setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))} setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))}
setWorkaround={workaround => setSettings(s => ({ ...s, workaround: workaround }))}
/> />
)} )}
</Card> </Card>
@ -235,10 +241,14 @@ function StreamSettings({
function AudioSourcePickerLinux({ function AudioSourcePickerLinux({
audioSource, audioSource,
setAudioSource workaround,
setAudioSource,
setWorkaround
}: { }: {
audioSource?: string; audioSource?: string;
workaround?: boolean;
setAudioSource(s: string): void; setAudioSource(s: string): void;
setWorkaround(b: boolean): void;
}) { }) {
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
fallbackValue: { ok: true, targets: [] } fallbackValue: { ok: true, targets: [] }
@ -252,7 +262,7 @@ function AudioSourcePickerLinux({
{!sources.ok && {!sources.ok &&
(sources.isGlibcxxToOld ? ( (sources.isGlibcxxToOld ? (
<Forms.FormText> <Forms.FormText>
Failed to retrieve Audio Sources because your c++ library is too old to run venmic. If you would Failed to retrieve Audio Sources because your C++ library is too old to run venmic. If you would
like to stream with Audio, see{" "} like to stream with Audio, see{" "}
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank"> <a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
this guide this guide
@ -273,6 +283,21 @@ function AudioSourcePickerLinux({
serialize={String} serialize={String}
/> />
)} )}
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
<Switch
onChange={setWorkaround}
value={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>
</section> </section>
); );
} }

View file

@ -1,235 +0,0 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import "./settings.css";
import { Margins } from "@vencord/types/utils";
import { Button, Forms, Select, Switch, Text, Toasts, useState } from "@vencord/types/webpack/common";
import { setBadge } from "renderer/appBadge";
import { useSettings } from "renderer/settings";
import { isMac } from "renderer/utils";
import { isTruthy } from "shared/utils/guards";
export default function SettingsUi() {
const Settings = useSettings();
const supportsWindowsTransparency = VesktopNative.app.supportsWindowsTransparency();
const { autostart } = VesktopNative;
const [autoStartEnabled, setAutoStartEnabled] = useState(autostart.isEnabled());
const allSwitches: Array<false | [keyof typeof Settings, string, string, boolean?, (() => boolean)?]> = [
[
"customTitleBar",
"Discord Titlebar",
"Use Discord's custom title bar instead of the native system one. Requires a full restart."
],
["arRPC", "Rich Presence", "Enables Rich Presence via arRPC", false],
[
"disableMinSize",
"Disable minimum window size",
"Allows you to make the window as small as your heart desires"
],
["staticTitle", "Static Title", 'Makes the window title "Vesktop" instead of changing to the current page'],
[
"enableMenu",
"Enable Menu Bar",
"Enables the application menu bar. Press ALT to toggle visibility. Incompatible with 'Discord Titlebar'"
],
["disableSmoothScroll", "Disable smooth scrolling", "Disables smooth scrolling in Vesktop", false],
["hardwareAcceleration", "Hardware Acceleration", "Enable hardware acceleration", true],
["splashTheming", "Splash theming", "Adapt the splash window colors to your custom theme", false],
[
"openLinksWithElectron",
"Open Links in app (experimental)",
"Opens links in a new Vesktop window instead of your web browser"
],
["checkUpdates", "Check for updates", "Automatically check for Vesktop updates", true]
];
const switches = allSwitches.filter(isTruthy);
return (
<Forms.FormSection>
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">
Vesktop Settings
</Text>
<Forms.FormTitle className={Margins.top16 + " " + Margins.bottom8}>Discord Branch</Forms.FormTitle>
<Select
placeholder="Stable"
options={[
{ label: "Stable", value: "stable", default: true },
{ label: "Canary", value: "canary" },
{ label: "PTB", value: "ptb" }
]}
closeOnSelect={true}
select={v => (Settings.discordBranch = v)}
isSelected={v => v === Settings.discordBranch}
serialize={s => s}
/>
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
<Switch
value={autoStartEnabled}
onChange={async v => {
await autostart[v ? "enable" : "disable"]();
setAutoStartEnabled(v);
}}
note="Automatically start Vesktop on computer start-up"
>
Start With System
</Switch>
<Switch
value={Settings.appBadge ?? true}
onChange={v => {
Settings.appBadge = v;
if (v) setBadge();
else VesktopNative.app.setBadgeCount(0);
}}
note="Show mention badge on the app (taskbar/panel) icon"
>
Notification Badge
</Switch>
{!isMac && (
<>
<Switch
value={Settings.tray ?? true}
onChange={v => {
Settings.tray = v;
if (v && Settings.trayBadge) setBadge();
}}
note="Add a tray icon for Vesktop"
key="tray"
>
Tray Icon
</Switch>
<Switch
value={Settings.minimizeToTray ?? true}
onChange={v => (Settings.minimizeToTray = v)}
disabled={!(Settings.tray ?? true)}
note="Hitting X will make Vesktop minimize to the tray instead of closing"
key="minimizeToTray"
>
Minimize to Tray
</Switch>
<Switch
value={Settings.trayBadge ?? true}
onChange={v => {
Settings.trayBadge = v;
if (v) setBadge();
else VesktopNative.app.setBadgeCount(0);
}}
disabled={!(Settings.tray ?? true)}
note="Show mention badge on the tray icon"
key="trayBadge"
>
Tray Notification Badge
</Switch>
</>
)}
{switches.map(([key, text, note, def, predicate]) => (
<Switch
value={(Settings[key as any] ?? def ?? false) && predicate?.() !== false}
disabled={predicate && !predicate()}
onChange={v => (Settings[key as any] = v)}
note={note}
key={key}
>
{text}
</Switch>
))}
{supportsWindowsTransparency && (
<>
<Forms.FormTitle className={Margins.top16 + " " + Margins.bottom8}>
Transparency Options
</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8}>
Requires a full restart. You will need a theme that supports transparency for this to work.
</Forms.FormText>
<Select
placeholder="None"
options={[
{
label: "None",
value: "none",
default: true
},
{
label: "Mica (incorporates system theme + desktop wallpaper to paint the background)",
value: "mica"
},
{ label: "Tabbed (variant of Mica with stronger background tinting)", value: "tabbed" },
{
label: "Acrylic (blurs the window behind Vesktop for a translucent background)",
value: "acrylic"
}
]}
closeOnSelect={true}
select={v => (Settings.transparencyOption = v)}
isSelected={v => v === Settings.transparencyOption}
serialize={s => s}
/>
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
</>
)}
<Forms.FormTitle>Vencord Location</Forms.FormTitle>
<Forms.FormText>
Vencord files are loaded from{" "}
{Settings.vencordDir ? (
<a
href="about:blank"
onClick={e => {
e.preventDefault();
VesktopNative.fileManager.showItemInFolder(Settings.vencordDir!);
}}
>
{Settings.vencordDir}
</a>
) : (
"the default location"
)}
</Forms.FormText>
<div className="vcd-location-btns">
<Button
size={Button.Sizes.SMALL}
onClick={async () => {
const choice = await VesktopNative.fileManager.selectVencordDir();
switch (choice) {
case "cancelled":
return;
case "invalid":
Toasts.show({
message:
"You did not choose a valid Vencord install. Make sure you're selecting the dist dir!",
id: Toasts.genId(),
type: Toasts.Type.FAILURE
});
return;
}
Settings.vencordDir = choice;
}}
>
Change
</Button>
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
onClick={() => (Settings.vencordDir = void 0)}
>
Reset
</Button>
</div>
</Forms.FormSection>
);
}

View file

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

View file

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { Switch, useState } from "@vencord/types/webpack/common";
import { SettingsComponent } from "./Settings";
export const AutoStartToggle: SettingsComponent = () => {
const [autoStartEnabled, setAutoStartEnabled] = useState(VesktopNative.autostart.isEnabled());
return (
<Switch
value={autoStartEnabled}
onChange={async v => {
await VesktopNative.autostart[v ? "enable" : "disable"]();
setAutoStartEnabled(v);
}}
note="Automatically start Vesktop on computer start-up"
>
Start With System
</Switch>
);
};

View file

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { Select } from "@vencord/types/webpack/common";
import { SettingsComponent } from "./Settings";
export const DiscordBranchPicker: SettingsComponent = ({ settings }) => {
return (
<Select
placeholder="Stable"
options={[
{ label: "Stable", value: "stable", default: true },
{ label: "Canary", value: "canary" },
{ label: "PTB", value: "ptb" }
]}
closeOnSelect={true}
select={v => (settings.discordBranch = v)}
isSelected={v => v === settings.discordBranch}
serialize={s => s}
/>
);
};

View file

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { Switch } from "@vencord/types/webpack/common";
import { setBadge } from "renderer/appBadge";
import { SettingsComponent } from "./Settings";
export const NotificationBadgeToggle: SettingsComponent = ({ settings }) => {
return (
<Switch
value={settings.appBadge ?? true}
onChange={v => {
settings.appBadge = v;
if (v) setBadge();
else VesktopNative.app.setBadgeCount(0);
}}
note="Show mention badge on the app icon"
>
Notification Badge
</Switch>
);
};

View file

@ -0,0 +1,172 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import "./settings.css";
import { Forms, Switch, Text } from "@vencord/types/webpack/common";
import { ComponentType } from "react";
import { Settings, useSettings } from "renderer/settings";
import { isMac, isWindows } from "renderer/utils";
import { AutoStartToggle } from "./AutoStartToggle";
import { DiscordBranchPicker } from "./DiscordBranchPicker";
import { NotificationBadgeToggle } from "./NotificationBadgeToggle";
import { TrayNotificationBadgeToggle } from "./TrayNotificationBadgeToggle";
import { VencordLocationPicker } from "./VencordLocationPicker";
import { WindowsTransparencyControls } from "./WindowsTransparencyControls";
interface BooleanSetting {
key: keyof typeof Settings.store;
title: string;
description: string;
defaultValue: boolean;
disabled?(): boolean;
invisible?(): boolean;
}
export type SettingsComponent = ComponentType<{ settings: typeof Settings.store }>;
const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>> = {
"Discord Branch": [DiscordBranchPicker],
"System Startup & Performance": [
AutoStartToggle,
{
key: "hardwareAcceleration",
title: "Hardware Acceleration",
description: "Enable hardware acceleration",
defaultValue: true
}
],
"User Interface": [
{
key: "customTitleBar",
title: "Discord Titlebar",
description: "Use Discord's custom title bar instead of the native system one. Requires a full restart.",
defaultValue: isWindows
},
{
key: "staticTitle",
title: "Static Title",
description: 'Makes the window title "Vesktop" instead of changing to the current page',
defaultValue: false
},
{
key: "enableMenu",
title: "Enable Menu Bar",
description: "Enables the application menu bar. Press ALT to toggle visibility.",
defaultValue: false,
disabled: () => Settings.store.customTitleBar ?? isWindows
},
{
key: "splashTheming",
title: "Splash theming",
description: "Adapt the splash window colors to your custom theme",
defaultValue: false
},
WindowsTransparencyControls
],
Behaviour: [
{
key: "tray",
title: "Tray Icon",
description: "Add a tray icon for Vesktop",
defaultValue: true,
invisible: () => isMac
},
{
key: "minimizeToTray",
title: "Minimize to tray",
description: "Hitting X will make Vesktop minimize to the tray instead of closing",
defaultValue: true,
invisible: () => isMac,
disabled: () => Settings.store.tray === false
},
{
key: "disableMinSize",
title: "Disable minimum window size",
description: "Allows you to make the window as small as your heart desires",
defaultValue: false
},
{
key: "disableSmoothScroll",
title: "Disable smooth scrolling",
description: "Disables smooth scrolling",
defaultValue: false
}
],
"Notifications & Updates": [
NotificationBadgeToggle,
TrayNotificationBadgeToggle,
{
key: "checkUpdates",
title: "Check for updates",
description: "Automatically check for Vesktop updates",
defaultValue: true
}
],
Miscelleanous: [
{
key: "arRPC",
title: "Rich Presence",
description: "Enables Rich Presence via arRPC",
defaultValue: false
},
{
key: "openLinksWithElectron",
title: "Open Links in app (experimental)",
description: "Opens links in a new Vesktop window instead of your web browser",
defaultValue: false
}
],
"Vencord Location": [VencordLocationPicker]
};
function SettingsSections() {
const Settings = useSettings();
const sections = Object.entries(SettingsOptions).map(([title, settings]) => (
<Forms.FormSection
title={title}
key={title}
className="vcd-settings-section"
titleClassName="vcd-settings-title"
>
{settings.map(Setting => {
if (typeof Setting === "function") return <Setting settings={Settings} />;
const { defaultValue, title, description, key, disabled, invisible } = Setting;
if (invisible?.()) return null;
return (
<Switch
value={Settings[key as any] ?? defaultValue}
onChange={v => (Settings[key as any] = v)}
note={description}
disabled={disabled?.()}
key={key}
>
{title}
</Switch>
);
})}
</Forms.FormSection>
));
return <>{sections}</>;
}
export default function SettingsUi() {
return (
<Forms.FormSection>
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">
Vesktop Settings
</Text>
<SettingsSections />
</Forms.FormSection>
);
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { Switch } from "@vencord/types/webpack/common";
import { setBadge } from "renderer/appBadge";
import { SettingsComponent } from "./Settings";
export const TrayNotificationBadgeToggle: SettingsComponent = ({ settings }) => {
return (
<Switch
value={settings.trayBadge ?? true}
onChange={v => {
settings.trayBadge = v;
if (v) setBadge();
else VesktopNative.app.setBadgeCount(0);
}}
note="Show mention badge on the tray icon"
>
Tray Notification Badge
</Switch>
);
};

View file

@ -0,0 +1,62 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { Button, Forms, Toasts } from "@vencord/types/webpack/common";
import { SettingsComponent } from "./Settings";
export const VencordLocationPicker: SettingsComponent = ({ settings }) => {
return (
<>
<Forms.FormText>
Vencord files are loaded from{" "}
{settings.vencordDir ? (
<a
href="about:blank"
onClick={e => {
e.preventDefault();
VesktopNative.fileManager.showItemInFolder(settings.vencordDir!);
}}
>
{settings.vencordDir}
</a>
) : (
"the default location"
)}
</Forms.FormText>
<div className="vcd-location-btns">
<Button
size={Button.Sizes.SMALL}
onClick={async () => {
const choice = await VesktopNative.fileManager.selectVencordDir();
switch (choice) {
case "cancelled":
return;
case "invalid":
Toasts.show({
message:
"You did not choose a valid Vencord install. Make sure you're selecting the dist dir!",
id: Toasts.genId(),
type: Toasts.Type.FAILURE
});
return;
}
settings.vencordDir = choice;
}}
>
Change
</Button>
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
onClick={() => (settings.vencordDir = void 0)}
>
Reset
</Button>
</div>
</>
);
};

View file

@ -0,0 +1,49 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { Margins } from "@vencord/types/utils";
import { Forms, Select } from "@vencord/types/webpack/common";
import { SettingsComponent } from "./Settings";
export const WindowsTransparencyControls: SettingsComponent = ({ settings }) => {
if (!VesktopNative.app.supportsWindowsTransparency()) return null;
return (
<>
<Forms.FormTitle className={Margins.top16 + " " + Margins.bottom8}>Transparency Options</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8}>
Requires a full restart. You will need a theme that supports transparency for this to work.
</Forms.FormText>
<Select
placeholder="None"
options={[
{
label: "None",
value: "none",
default: true
},
{
label: "Mica (incorporates system theme + desktop wallpaper to paint the background)",
value: "mica"
},
{ label: "Tabbed (variant of Mica with stronger background tinting)", value: "tabbed" },
{
label: "Acrylic (blurs the window behind Vesktop for a translucent background)",
value: "acrylic"
}
]}
closeOnSelect={true}
select={v => (settings.transparencyOption = v)}
isSelected={v => v === settings.transparencyOption}
serialize={s => s}
/>
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
</>
);
};

View file

@ -4,3 +4,11 @@
gap: 0.5em; gap: 0.5em;
margin-top: 0.5em; margin-top: 0.5em;
} }
.vcd-settings-section {
margin-top: 1.5rem;
}
.vcd-settings-title {
margin-bottom: 0.5rem;
}

View file

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

View file

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

View file

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