diff --git a/README.md b/README.md index 9bf1275..a3f8893 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ If you don't know the difference, pick the Installer. ### 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) -- [arm64 / aarch64](https://vencord.dev/download/vesktop/arm64/dmg) +- [Intel build (amd64)](https://vencord.dev/download/vesktop/amd64/dmg) +- [Apple Silicon (arm64)](https://vencord.dev/download/vesktop/arm64/dmg) ### Linux @@ -54,6 +54,7 @@ Below you can find unofficial packages created by the community. They are not of - Arch Linux: [Vesktop on the Arch user repository](https://aur.archlinux.org/packages?K=vesktop) - NixOS: https://nixos.wiki/wiki/Discord#Vesktop +- Windows - Scoop: https://scoop.sh/#/apps?q=Vesktop ## Building from Source diff --git a/package.json b/package.json index 04b2132..145d207 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vesktop", - "version": "1.5.0", + "version": "1.5.2", "private": true, "description": "", "keywords": [], @@ -24,36 +24,36 @@ "updateMeta": "tsx scripts/utils/updateMeta.mts" }, "dependencies": { - "arrpc": "github:OpenAsar/arrpc#98879cae0565e6fce34e4cb6f544bf42c6a7e7c8" + "arrpc": "github:OpenAsar/arrpc#6960a8fd4d65d566da93dbdb8a7ca474aa0a3c9c" }, "optionalDependencies": { - "@vencord/venmic": "^3.3.2" + "@vencord/venmic": "^3.4.2" }, "devDependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@types/node": "^20.11.2", - "@types/react": "^18.2.48", - "@typescript-eslint/eslint-plugin": "^6.19.0", - "@typescript-eslint/parser": "^6.19.0", + "@types/node": "^20.11.26", + "@types/react": "^18.2.65", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", "@vencord/types": "^0.1.2", - "dotenv": "^16.3.1", - "electron": "^28.1.3", - "electron-builder": "^24.9.1", - "esbuild": "^0.19.11", - "eslint": "^8.56.0", + "dotenv": "^16.4.5", + "electron": "^29.1.1", + "electron-builder": "^24.13.3", + "esbuild": "^0.20.1", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-license-header": "^0.6.0", "eslint-plugin-path-alias": "^1.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-simple-import-sort": "^10.0.0", - "eslint-plugin-unused-imports": "^3.0.0", - "prettier": "^3.2.2", + "eslint-plugin-simple-import-sort": "^12.0.0", + "eslint-plugin-unused-imports": "^3.1.0", + "prettier": "^3.2.5", "source-map-support": "^0.5.21", - "tsx": "^4.7.0", - "type-fest": "^4.9.0", - "typescript": "^5.3.3", - "xml-formatter": "^3.6.0" + "tsx": "^4.7.1", + "type-fest": "^4.12.0", + "typescript": "^5.4.2", + "xml-formatter": "^3.6.2" }, "packageManager": "pnpm@8.11.0", "engines": { @@ -131,6 +131,26 @@ "com.apple.security.device.camera": true } }, + "dmg": { + "background": "build/background.tiff", + "icon": "build/icon.icns", + "iconSize": 105, + "window": { + "width": 512, + "height": 340 + }, + + "contents": [{ + "x": 140, + "y": 160 + }, + { + "x": 372, + "y": 160, + "type": "link", + "path": "/Applications" + }] + }, "nsis": { "include": "build/installer.nsh", "oneClick": false diff --git a/scripts/start.ts b/scripts/start.ts index 72ce3ab..21660b0 100644 --- a/scripts/start.ts +++ b/scripts/start.ts @@ -8,4 +8,4 @@ import "./utils/dotenv"; import { spawnNodeModuleBin } from "./utils/spawn.mjs"; -spawnNodeModuleBin("electron", [".", ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]); +spawnNodeModuleBin("electron", [process.cwd(), ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]); diff --git a/src/main/autoStart.ts b/src/main/autoStart.ts index fbbc30e..19d7e00 100644 --- a/src/main/autoStart.ts +++ b/src/main/autoStart.ts @@ -5,7 +5,7 @@ */ import { app } from "electron"; -import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"; +import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs"; import { join } from "path"; interface AutoStart { @@ -17,7 +17,16 @@ interface AutoStart { function makeAutoStartLinux(): AutoStart { const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config"); 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 { isEnabled: () => existsSync(file), @@ -25,12 +34,11 @@ function makeAutoStartLinux(): AutoStart { const desktopFile = ` [Desktop Entry] Type=Application -Version=1.0 -Name=Vencord -Comment=Vencord autostart script -Exec=${process.execPath} -Terminal=false +Name=Vesktop +Comment=Vesktop autostart script +Exec=${commandLine} StartupNotify=false +Terminal=false `.trim(); mkdirSync(dir, { recursive: true }); diff --git a/src/main/constants.ts b/src/main/constants.ts index 5c4ecd7..1a02d5e 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -48,14 +48,14 @@ export const DEFAULT_HEIGHT = 720; export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"]; -const UserAgents = { - 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", - linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", +const BrowserUserAgents = { + 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/122.0.0.0 Safari/537.36", 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 { Default, diff --git a/src/main/index.ts b/src/main/index.ts index 4a59e9c..dd952d5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -27,23 +27,35 @@ process.env.VENCORD_USER_DATA_DIR = DATA_DIR; function init() { const { disableSmoothScroll, hardwareAcceleration, splashAnimationPath } = Settings.store; - if (hardwareAcceleration === false) app.disableHardwareAcceleration(); + const enabledFeatures = app.commandLine.getSwitchValue("enable-features").split(","); + const disabledFeatures = app.commandLine.getSwitchValue("disable-features").split(","); + + if (hardwareAcceleration === false) { + app.disableHardwareAcceleration(); + } else { + enabledFeatures.push("VaapiVideoDecodeLinuxGL", "VaapiVideoEncoder", "VaapiVideoDecoder"); + } + if (disableSmoothScroll) { app.commandLine.appendSwitch("disable-smooth-scrolling"); } // work around chrome 66 disabling autoplay by default app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); - // WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows. // HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service. // // WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790 - app.commandLine.appendSwitch( - "disable-features", - "WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService,WidgetLayering" + disabledFeatures.push( + "WinRetrieveSuggestionsOnlyOnDemand", + "HardwareMediaKeyHandling", + "MediaSessionService", + "WidgetLayering" ); + app.commandLine.appendSwitch("enable-features", [...new Set(enabledFeatures)].filter(Boolean).join(",")); + app.commandLine.appendSwitch("disable-features", [...new Set(disabledFeatures)].filter(Boolean).join(",")); + // In the Flatpak on SteamOS the theme is detected as light, but SteamOS only has a dark mode, so we just override it if (isDeckGameMode) nativeTheme.themeSource = "dark"; @@ -62,7 +74,6 @@ function init() { registerScreenShareHandler(); registerMediaPermissionsHandler(); - //register file handler so we can load the custom splash animation from the user's filesystem protocol.handle("splash-animation", () => { return net.fetch("file:///"+splashAnimationPath); diff --git a/src/main/mainWindow.ts b/src/main/mainWindow.ts index 62a2559..7e0afde 100644 --- a/src/main/mainWindow.ts +++ b/src/main/mainWindow.ts @@ -25,13 +25,13 @@ import { ICON_PATH } from "../shared/paths"; import { createAboutWindow } from "./about"; import { initArRPC } from "./arrpc"; import { + BrowserUserAgent, DATA_DIR, DEFAULT_HEIGHT, DEFAULT_WIDTH, MessageBoxChoice, MIN_HEIGHT, MIN_WIDTH, - UserAgent, VENCORD_FILES_DIR } from "./constants"; import { Settings, State, VencordSettings } from "./settings"; @@ -73,6 +73,10 @@ const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpe const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings); function initTray(win: BrowserWindow) { + const onTrayClick = () => { + if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide(); + else win.show(); + }; const trayMenu = Menu.buildFromTemplate([ { label: "Open", @@ -120,7 +124,7 @@ function initTray(win: BrowserWindow) { tray = new Tray(ICON_PATH); tray.setToolTip("Vesktop"); tray.setContextMenu(trayMenu); - tray.on("click", () => win.show()); + tray.on("click", onTrayClick); } async function clearData(win: BrowserWindow) { @@ -426,7 +430,7 @@ function createMainWindow() { initSettingsListeners(win); initSpellCheck(win); - win.webContents.setUserAgent(UserAgent); + win.webContents.setUserAgent(BrowserUserAgent); const subdomain = Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb" diff --git a/src/main/splash.ts b/src/main/splash.ts index e88f0ce..ee4a4f3 100644 --- a/src/main/splash.ts +++ b/src/main/splash.ts @@ -19,7 +19,7 @@ export function createSplashWindow(startMinimized = false) { icon: ICON_PATH, show: !startMinimized }); - + splash.loadFile(join(VIEW_DIR, "splash.html")); if (splashTheming) { @@ -34,7 +34,7 @@ export function createSplashWindow(startMinimized = false) { splash.webContents.insertCSS(`body { --bg: ${splashBackground} !important }`); } } - + if (splashAnimationPath) { splash.webContents.executeJavaScript(` document.getElementById("animation").src = "splash-animation://img"; @@ -46,6 +46,6 @@ export function createSplashWindow(startMinimized = false) { document.getElementById("animation").src = "../shiggy.gif"; `); } - + return splash; } diff --git a/src/main/utils/http.ts b/src/main/utils/http.ts index 9a98384..baee81e 100644 --- a/src/main/utils/http.ts +++ b/src/main/utils/http.ts @@ -5,41 +5,54 @@ */ import { createWriteStream } from "fs"; -import type { IncomingMessage } from "http"; -import { get, RequestOptions } from "https"; -import { finished } from "stream/promises"; +import { Readable } from "stream"; +import { pipeline } from "stream/promises"; +import { setTimeout } from "timers/promises"; -export async function downloadFile(url: string, file: string, options: RequestOptions = {}) { - const res = await simpleReq(url, options); - await finished( - res.pipe( - createWriteStream(file, { - autoClose: true - }) - ) +interface FetchieOptions { + retryOnNetworkError?: boolean; +} + +export async function downloadFile(url: string, file: string, options: RequestInit = {}, fetchieOpts?: FetchieOptions) { + const res = await fetchie(url, options, fetchieOpts); + await pipeline( + // @ts-expect-error odd type error + Readable.fromWeb(res.body!), + createWriteStream(file, { + autoClose: true + }) ); } -export function simpleReq(url: string, options: RequestOptions = {}) { - return new Promise((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); +const ONE_MINUTE_MS = 1000 * 60; - resolve(res); - }); - }); -} - -export async function simpleGet(url: string, options: RequestOptions = {}) { - const res = await simpleReq(url, options); - - return new Promise((resolve, reject) => { - const chunks = [] as Buffer[]; - - res.once("error", reject); - res.on("data", chunk => chunks.push(chunk)); - res.once("end", () => resolve(Buffer.concat(chunks))); - }); +export async function fetchie(url: string, options?: RequestInit, { retryOnNetworkError }: FetchieOptions = {}) { + let res: Response | undefined; + + try { + res = await fetch(url, options); + } catch (err) { + if (retryOnNetworkError) { + console.error("Failed to fetch", url + ".", "Gonna retry with backoff."); + + for (let tries = 0, delayMs = 500; tries < 20; tries++, delayMs = Math.min(2 * delayMs, ONE_MINUTE_MS)) { + await setTimeout(delayMs); + try { + res = await fetch(url, options); + 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); } diff --git a/src/main/utils/vencordLoader.ts b/src/main/utils/vencordLoader.ts index 293654a..1bac37a 100644 --- a/src/main/utils/vencordLoader.ts +++ b/src/main/utils/vencordLoader.ts @@ -5,11 +5,10 @@ */ import { existsSync, mkdirSync } from "fs"; -import type { RequestOptions } from "https"; import { join } from "path"; 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"; @@ -31,27 +30,29 @@ export interface ReleaseData { } export async function githubGet(endpoint: string) { - const opts: RequestOptions = { + const opts: RequestInit = { headers: { Accept: "application/vnd.github+json", "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() { 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( assets .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 }) + ) ); } diff --git a/src/main/venmic.ts b/src/main/venmic.ts index c6f9d44..404c0d2 100644 --- a/src/main/venmic.ts +++ b/src/main/venmic.ts @@ -4,17 +4,58 @@ * Copyright (c) 2023 Vendicated and Vencord contributors */ -import type { PatchBay } from "@vencord/venmic"; +import type { PatchBay as PatchBayType } from "@vencord/venmic"; import { app, ipcMain } from "electron"; import { join } from "path"; import { IpcEvents } from "shared/IpcEvents"; import { STATIC_DIR } from "shared/paths"; -type LinkData = Parameters[0]; +type LinkData = Parameters[0]; +let PatchBay: typeof PatchBayType | undefined; +let patchBayInstance: PatchBayType | undefined; + +let imported = false; let initialized = false; -let patchBay: import("@vencord/venmic").PatchBay | undefined; -let isGlibcxxToOld = false; + +let hasPipewirePulse = false; +let isGlibCxxOutdated = false; + +function importVenmic() { + if (imported) { + return; + } + + imported = true; + + try { + PatchBay = (require(join(STATIC_DIR, `dist/venmic-${process.arch}.node`)) as typeof import("@vencord/venmic")) + .PatchBay; + + hasPipewirePulse = PatchBay.hasPipeWire(); + } catch (e: any) { + console.error("Failed to import venmic", e); + isGlibCxxOutdated = (e?.stack || e?.message || "").toLowerCase().includes("glibc"); + } +} + +function obtainVenmic() { + if (!imported) { + importVenmic(); + } + + if (PatchBay && !initialized) { + initialized = true; + + try { + patchBayInstance = new PatchBay(); + } catch (e: any) { + console.error("Failed to instantiate venmic", e); + } + } + + return patchBayInstance; +} function getRendererAudioServicePid() { return ( @@ -25,33 +66,17 @@ function getRendererAudioServicePid() { ); } -function obtainVenmic() { - if (!initialized) { - initialized = true; - try { - const { PatchBay } = require( - join(STATIC_DIR, `dist/venmic-${process.arch}.node`) - ) as typeof import("@vencord/venmic"); - patchBay = new PatchBay(); - } catch (e: any) { - console.error("Failed to initialise venmic. Make sure you're using pipewire", e); - isGlibcxxToOld = (e?.stack || e?.message || "").toLowerCase().includes("glibc"); - } - } - - return patchBay; -} - ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => { const audioPid = getRendererAudioServicePid(); + const list = obtainVenmic() ?.list() .filter(s => s["application.process.id"] !== audioPid) .map(s => s["application.name"]); - return list - ? { ok: true, targets: [...new Set(list)] } // Remove duplicates - : { ok: false, isGlibcxxToOld }; + const uniqueTargets = [...new Set(list)]; + + return list ? { ok: true, targets: uniqueTargets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated }; }); ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[], workaround?: boolean) => { @@ -72,11 +97,12 @@ ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, targets: string[], workaround?: boo return obtainVenmic()?.link(data); }); -ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, workaround?: boolean) => { +ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, workaround?: boolean, onlyDefaultSpeakers?: boolean) => { const pid = getRendererAudioServicePid(); const data: LinkData = { - exclude: [{ key: "application.process.id", value: pid }] + exclude: [{ key: "application.process.id", value: pid }], + only_default_speakers: onlyDefaultSpeakers }; if (workaround) { diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts index ead4d63..af4b9f1 100644 --- a/src/preload/VesktopNative.ts +++ b/src/preload/VesktopNative.ts @@ -63,9 +63,12 @@ export const VesktopNative = { /** only available on Linux. */ virtmic: { list: () => - invoke<{ ok: false; isGlibcxxToOld: boolean } | { ok: true; targets: string[] }>(IpcEvents.VIRT_MIC_LIST), + invoke< + { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: string[]; hasPipewirePulse: boolean } + >(IpcEvents.VIRT_MIC_LIST), start: (targets: string[], workaround?: boolean) => invoke(IpcEvents.VIRT_MIC_START, targets, workaround), - startSystem: (workaround?: boolean) => invoke(IpcEvents.VIRT_MIC_START_SYSTEM, workaround), + startSystem: (workaround?: boolean, onlyDefaultSpeakers?: boolean) => + invoke(IpcEvents.VIRT_MIC_START_SYSTEM, workaround, onlyDefaultSpeakers), stop: () => invoke(IpcEvents.VIRT_MIC_STOP) }, arrpc: { diff --git a/src/renderer/components/ScreenSharePicker.tsx b/src/renderer/components/ScreenSharePicker.tsx index 9842afe..e9fd78e 100644 --- a/src/renderer/components/ScreenSharePicker.tsx +++ b/src/renderer/components/ScreenSharePicker.tsx @@ -6,7 +6,7 @@ import "./screenSharePicker.css"; -import { closeModal, Margins, Modals, openModal, useAwaiter } from "@vencord/types/utils"; +import { closeModal, Logger, Margins, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils"; import { findStoreLazy, onceReady } from "@vencord/types/webpack"; import { Button, @@ -36,7 +36,9 @@ interface StreamSettings { fps: StreamFps; audio: boolean; audioSource?: string; + contentHint?: string; workaround?: boolean; + onlyDefaultSpeakers?: boolean; } export interface StreamPick extends StreamSettings { @@ -49,7 +51,9 @@ interface Source { url: string; } -let currentSettings: StreamSettings | null = null; +export let currentSettings: StreamSettings | null = null; + +const logger = new Logger("VesktopScreenShare"); addPatch({ patches: [ @@ -59,6 +63,20 @@ addPatch({ match: /this.localWant=/, 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) { @@ -73,6 +91,14 @@ addPatch({ bitrateMax: 8000000, bitrateTarget: 600000 }); + if (opts?.encode) { + Object.assign(opts.encode, { + framerate, + width, + height, + pixelCount: height * width + }); + } Object.assign(opts.capture, { framerate, width, @@ -167,74 +193,127 @@ function StreamSettings({ ); return ( -
- What you're streaming - - - {source.name} - +
+
+ What you're streaming + + + {source.name} + - Stream Settings + Stream Settings - -
-
- Resolution -
- {StreamResolutions.map(res => ( - - ))} -
-
+ +
+
+ Resolution +
+ {StreamResolutions.map(res => ( + + ))} +
+
-
- Frame Rate -
- {StreamFps.map(fps => ( - - ))} -
-
-
- - {isWindows && ( - setSettings(s => ({ ...s, audio: checked }))} - hideBorder - className="vcd-screen-picker-audio" - > - Stream With Audio - - )} +
+ Frame Rate +
+ {StreamFps.map(fps => ( + + ))} +
+
+
+
+
+ Content Type +
+
+ + +
+
+

+ Choosing "Prefer Clarity" will result in a significantly lower framerate in + exchange for a much sharper and clearer image. +

+
+
+ {isWindows && ( + setSettings(s => ({ ...s, audio: checked }))} + hideBorder + className="vcd-screen-picker-audio" + > + Stream With Audio + + )} +
+
+
+
+
{isLinux && ( setSettings(s => ({ ...s, audioSource: source }))} - setWorkaround={workaround => setSettings(s => ({ ...s, workaround: workaround }))} + setWorkaround={value => setSettings(s => ({ ...s, workaround: value }))} + setOnlyDefaultSpeakers={value => setSettings(s => ({ ...s, onlyDefaultSpeakers: value }))} /> )} - +
); } @@ -242,63 +321,102 @@ function StreamSettings({ function AudioSourcePickerLinux({ audioSource, workaround, + onlyDefaultSpeakers, setAudioSource, - setWorkaround + setWorkaround, + setOnlyDefaultSpeakers }: { audioSource?: string; workaround?: boolean; + onlyDefaultSpeakers?: boolean; setAudioSource(s: string): void; setWorkaround(b: boolean): void; + setOnlyDefaultSpeakers(b: boolean): void; }) { const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { - fallbackValue: { ok: true, targets: [] } + fallbackValue: { ok: true, targets: [], hasPipewirePulse: true } }); + const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null; + const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true; + + const [ignorePulseWarning, setIgnorePulseWarning] = useState(false); return ( -
- Audio - {loading && Loading Audio sources...} - {!sources.ok && - (sources.isGlibcxxToOld ? ( + <> + Audio Settings + + {loading ? ( + Loading Audio Sources... + ) : ( + Audio Source + )} + + {!sources.ok && sources.isGlibCxxOutdated && ( - Failed to retrieve Audio Sources because your C++ library is too old to run venmic. If you would - like to stream with Audio, see{" "} + Failed to retrieve Audio Sources because your C++ library is too old to run + + venmic + + . See{" "} this guide - + {" "} + for possible solutions. + )} + + {hasPipewirePulse || ignorePulseWarning ? ( + allSources && ( + ({ label: s, value: s, default: s === "None" }))} - isSelected={s => s === audioSource} - select={setAudioSource} - serialize={String} - /> - )} + - + + 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 + - - 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 - -
+ + 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 + + + ); } @@ -319,16 +437,16 @@ function ModalComponent({ const [settings, setSettings] = useState({ resolution: "1080", fps: "60", + contentHint: "motion", audio: true }); return ( - + ScreenShare - {!selected ? ( @@ -341,35 +459,62 @@ function ModalComponent({ /> )} - + +
+ + + ); +}; diff --git a/src/renderer/components/settings/DiscordBranchPicker.tsx b/src/renderer/components/settings/DiscordBranchPicker.tsx new file mode 100644 index 0000000..c0b840d --- /dev/null +++ b/src/renderer/components/settings/DiscordBranchPicker.tsx @@ -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 ( + (settings.transparencyOption = v)} + isSelected={v => v === settings.transparencyOption} + serialize={s => s} + /> + + + + ); +}; diff --git a/src/renderer/components/settings/settings.css b/src/renderer/components/settings/settings.css new file mode 100644 index 0000000..d55ff50 --- /dev/null +++ b/src/renderer/components/settings/settings.css @@ -0,0 +1,14 @@ +.vcd-location-btns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5em; + margin-top: 0.5em; +} + +.vcd-settings-section { + margin-top: 1.5rem; +} + +.vcd-settings-title { + margin-bottom: 0.5rem; +} \ No newline at end of file diff --git a/src/renderer/fixes.css b/src/renderer/fixes.css new file mode 100644 index 0000000..aeec1bf --- /dev/null +++ b/src/renderer/fixes.css @@ -0,0 +1,11 @@ +/* Download Desktop button in guilds list */ +[class^=listItem_]:has([data-list-item-id=guildsnav___app-download-button]), +[class^=listItem_]:has(+ [class^=listItem_] [data-list-item-id=guildsnav___app-download-button]) { + display: none; +} + +/* FIXME: remove this once Discord fixes their css to not explode scrollbars on chromium >=121 */ +* { + scrollbar-width: unset !important; + scrollbar-color: unset !important; +} \ No newline at end of file diff --git a/src/renderer/fixes.ts b/src/renderer/fixes.ts index 2524023..2758b5c 100644 --- a/src/renderer/fixes.ts +++ b/src/renderer/fixes.ts @@ -4,7 +4,7 @@ * Copyright (c) 2023 Vendicated and Vencord contributors */ -import "./hideGarbage.css"; +import "./fixes.css"; import { isWindows, localStorage } from "./utils"; diff --git a/src/renderer/index.ts b/src/renderer/index.ts index ebe6bc6..1ccc2e4 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -15,7 +15,7 @@ export * as Components from "./components"; import { findByPropsLazy } from "@vencord/types/webpack"; import { FluxDispatcher } from "@vencord/types/webpack/common"; -import SettingsUi from "./components/Settings"; +import SettingsUi from "./components/settings/Settings"; import { Settings } from "./settings"; export { Settings }; diff --git a/src/renderer/patches/hideSwitchDevice.tsx b/src/renderer/patches/hideSwitchDevice.tsx new file mode 100644 index 0000000..911aed7 --- /dev/null +++ b/src/renderer/patches/hideSwitchDevice.tsx @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { addPatch } from "./shared"; + +addPatch({ + patches: [ + { + find: "lastOutputSystemDevice.justChanged", + replacement: { + // eslint-disable-next-line no-useless-escape + match: /(\i)\.default\.getState\(\).neverShowModal/, + replace: "$& || $self.shouldIgnore($1)" + } + } + ], + + shouldIgnore(state: any) { + return Object.keys(state?.default?.lastDeviceConnected ?? {})?.[0] === "vencord-screen-share"; + } +}); diff --git a/src/renderer/patches/hideVenmicInput.tsx b/src/renderer/patches/hideVenmicInput.tsx new file mode 100644 index 0000000..ca706ce --- /dev/null +++ b/src/renderer/patches/hideVenmicInput.tsx @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { addPatch } from "./shared"; + +addPatch({ + patches: [ + { + find: 'setSinkId"in', + replacement: { + // eslint-disable-next-line no-useless-escape + match: /return (\i)\?navigator\.mediaDevices\.enumerateDevices/, + replace: "return $1 ? $self.filteredDevices" + } + } + ], + + async filteredDevices() { + const original = await navigator.mediaDevices.enumerateDevices(); + return original.filter(x => x.label !== "vencord-screen-share"); + } +}); diff --git a/src/renderer/patches/index.ts b/src/renderer/patches/index.ts index 7d4c4b3..aabff3e 100644 --- a/src/renderer/patches/index.ts +++ b/src/renderer/patches/index.ts @@ -7,6 +7,8 @@ // TODO: Possibly auto generate glob if we have more patches in the future import "./enableNotificationsByDefault"; import "./platformClass"; -import "./screenShareAudio"; +import "./hideSwitchDevice"; +import "./hideVenmicInput"; +import "./screenShareFixes"; import "./spellCheck"; import "./windowsTitleBar"; diff --git a/src/renderer/patches/screenShareFixes.ts b/src/renderer/patches/screenShareFixes.ts new file mode 100644 index 0000000..66e4b14 --- /dev/null +++ b/src/renderer/patches/screenShareFixes.ts @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Logger } from "@vencord/types/utils"; +import { currentSettings } from "renderer/components/ScreenSharePicker"; +import { isLinux } from "renderer/utils"; + +const logger = new Logger("VesktopStreamFixes"); + +if (isLinux) { + const original = navigator.mediaDevices.getDisplayMedia; + + async function getVirtmic() { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const audioDevice = devices.find(({ label }) => label === "vencord-screen-share"); + return audioDevice?.deviceId; + } catch (error) { + return null; + } + } + + navigator.mediaDevices.getDisplayMedia = async function (opts) { + const stream = await original.call(this, opts); + const id = await getVirtmic(); + + const frameRate = Number(currentSettings?.fps); + const height = Number(currentSettings?.resolution); + const width = Math.round(height * (16 / 9)); + const track = stream.getVideoTracks()[0]; + + track.contentHint = String(currentSettings?.contentHint); + + const constraints = { + ...track.getConstraints(), + frameRate, + width: { min: 640, ideal: width, max: width }, + height: { min: 480, ideal: height, max: height }, + advanced: [{ width: width, height: height }], + resizeMode: "none" + }; + + track + .applyConstraints(constraints) + .then(() => { + logger.info("Applied constraints successfully. New constraints: ", track.getConstraints()); + }) + .catch(e => logger.error("Failed to apply constraints.", e)); + + if (id) { + const audio = await navigator.mediaDevices.getUserMedia({ + audio: { + deviceId: { + exact: id + }, + autoGainControl: false, + echoCancellation: false, + noiseSuppression: false + } + }); + audio.getAudioTracks().forEach(t => stream.addTrack(t)); + } + + return stream; + }; +} diff --git a/src/renderer/patches/spellCheck.tsx b/src/renderer/patches/spellCheck.tsx index 038f06a..9f0dbbd 100644 --- a/src/renderer/patches/spellCheck.tsx +++ b/src/renderer/patches/spellCheck.tsx @@ -6,7 +6,7 @@ import { addContextMenuPatch } from "@vencord/types/api/ContextMenu"; 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"; @@ -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); children.push( @@ -71,11 +72,9 @@ addContextMenuPatch("textarea-context", children => () => { { FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" }); - // Haven't found a good way to update state, so just close for now 🤷‍♀️ - ContextMenu.close(); }} /> diff --git a/src/shared/settings.d.ts b/src/shared/settings.d.ts index d6e9dc2..fa46bac 100644 --- a/src/shared/settings.d.ts +++ b/src/shared/settings.d.ts @@ -20,7 +20,7 @@ export interface Settings { arRPC?: boolean; appBadge?: boolean; disableMinSize?: boolean; - + clickTrayToShowHide?: boolean; /** @deprecated use customTitleBar */ discordWindowsTitleBar?: boolean; customTitleBar?: boolean; @@ -28,8 +28,8 @@ export interface Settings { checkUpdates?: boolean; splashTheming?: boolean; - splashAnimationPath?: string; splashColor?: string; + splashAnimationPath?: string; splashBackground?: string; } diff --git a/src/updater/main.ts b/src/updater/main.ts index 059afb9..207687e 100644 --- a/src/updater/main.ts +++ b/src/updater/main.ts @@ -81,7 +81,7 @@ export async function checkUpdates() { try { 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 newVersion = data.tag_name.replace(/^v/, ""); diff --git a/static/views/splash.html b/static/views/splash.html index 85e0e45..a44b273 100644 --- a/static/views/splash.html +++ b/static/views/splash.html @@ -24,7 +24,7 @@ img { width: 128px; - height: 128px + height: 128px; }