Compare commits
53 Commits
v1.5.8
...
preload-sa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0a87cad8f | ||
|
|
8e91df0376 | ||
|
|
81e6caa05a | ||
|
|
da8e8f52ab | ||
|
|
98b0ba85a3 | ||
|
|
d83aa6675e | ||
|
|
b3d595ef98 | ||
|
|
24047d7fdd | ||
|
|
5c992d66a6 | ||
|
|
4cec16be1b | ||
|
|
6c320983e2 | ||
|
|
d0e7bd479a | ||
|
|
02ab7165aa | ||
|
|
0483a1abdc | ||
|
|
34c92cfb59 | ||
|
|
28aeab979b | ||
|
|
8ca3e4f3a1 | ||
|
|
ae47c204fa | ||
|
|
f57245f297 | ||
|
|
9193ed58c9 | ||
|
|
38b7716b2f | ||
|
|
4b735b9739 | ||
|
|
707dbf4f75 | ||
|
|
a242d5d694 | ||
|
|
b75ed0766d | ||
|
|
03aa30dfc6 | ||
|
|
cd1a40e6c7 | ||
|
|
3aa0bb806e | ||
|
|
800a97105c | ||
|
|
b02acd6a7b | ||
|
|
d293b166fe | ||
|
|
28a13be709 | ||
|
|
02907d3248 | ||
|
|
c82cc7a963 | ||
|
|
6a43e135d0 | ||
|
|
d232797889 | ||
|
|
fa23c630cb | ||
|
|
9f0af48355 | ||
|
|
eb3dae897d | ||
|
|
d005dd5ebd | ||
|
|
6aeacaaf21 | ||
|
|
40d9cba2f0 | ||
|
|
5734a1d33c | ||
|
|
8cc34e217c | ||
|
|
a55b1f0250 | ||
|
|
e79635f15e | ||
|
|
6e2da1d294 | ||
|
|
0d9ca2270c | ||
|
|
497c251d72 | ||
|
|
b221882c5b | ||
|
|
0ee194698d | ||
|
|
27293d4ae9 | ||
|
|
432e54ace5 |
10
.github/workflows/meta.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update:
|
||||
@@ -26,17 +26,13 @@ jobs:
|
||||
run: pnpm i
|
||||
|
||||
- name: Update metainfo
|
||||
run: pnpm updateMeta
|
||||
run: pnpm generateMeta
|
||||
|
||||
- name: Commit and merge in changes
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
gh release upload "${{ github.event.release.tag_name }}" meta/dev.vencord.Vesktop.metainfo.xml
|
||||
|
||||
git add meta/dev.vencord.Vesktop.metainfo.xml
|
||||
git commit -m "metainfo: add entry for ${{ github.event.release.tag_name }}"
|
||||
git push origin HEAD:main
|
||||
gh release upload "${{ github.event.release.tag_name }}" dist/dev.vencord.Vesktop.metainfo.xml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
54
README.md
@@ -17,48 +17,7 @@ Vesktop is a custom Discord desktop app
|
||||
|
||||
## Installing
|
||||
|
||||
### Windows
|
||||
|
||||
If you don't know the difference, pick the Installer.
|
||||
|
||||
- [Installer](https://vencord.dev/download/vesktop/universal/windows)
|
||||
- Portable:
|
||||
- [x64 / amd64](https://vencord.dev/download/vesktop/amd64/windows-portable)
|
||||
- [Arm® 64](https://vencord.dev/download/vesktop/arm64/windows-portable)
|
||||
|
||||
### Mac
|
||||
|
||||
Download the latest [Vesktop.dmg](https://vencord.dev/download/vesktop/universal/dmg) or use [Homebrew](https://brew.sh/)
|
||||
|
||||
```sh
|
||||
brew install vesktop
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
[](https://flathub.org/apps/dev.vencord.Vesktop)
|
||||
|
||||
If you don't know the difference, pick amd64.
|
||||
|
||||
- amd64 / x86_64
|
||||
- [AppImage](https://vencord.dev/download/vesktop/amd64/appimage)
|
||||
- [Ubuntu/Debian (.deb)](https://vencord.dev/download/vesktop/amd64/deb)
|
||||
- [Fedora/RHEL (.rpm)](https://vencord.dev/download/vesktop/amd64/rpm)
|
||||
- [tarball](https://vencord.dev/download/vesktop/amd64/tar)
|
||||
- Arm® 64 / aarch64
|
||||
- [AppImage](https://vencord.dev/download/vesktop/arm64/appimage)
|
||||
- [Ubuntu/Debian (.deb)](https://vencord.dev/download/vesktop/arm64/deb)
|
||||
- [Fedora/RHEL (.rpm)](https://vencord.dev/download/vesktop/arm64/rpm)
|
||||
- [tarball](https://vencord.dev/download/vesktop/arm64/tar)
|
||||
|
||||
#### Community packages
|
||||
|
||||
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)
|
||||
- NixOS: https://wiki.nixos.org/wiki/Discord#Vesktop
|
||||
- Slackware: [Vesktop on the SlackBuilds](https://slackbuilds.org/result/?search=vesktop)
|
||||
- Windows - Scoop: https://scoop.sh/#/apps?q=Vesktop
|
||||
Visit https://vesktop.dev/install
|
||||
|
||||
## Building from Source
|
||||
|
||||
@@ -88,3 +47,14 @@ pnpm package --linux pacman
|
||||
# Or package to a directory only
|
||||
pnpm package:dir
|
||||
```
|
||||
|
||||
## Building LibVesktop from Source
|
||||
|
||||
This is a small C++ helper library Vesktop uses on Linux to emit D-Bus events. By default, prebuilt binaries for x64 and arm64 are used.
|
||||
|
||||
If you want to build it from source:
|
||||
1. Install build dependencies:
|
||||
- Debian/Ubuntu: `apt install build-essential python3 curl pkg-config libglib2.0-dev`
|
||||
- Fedora: `dnf install @c-development @development-tools python3 curl pkgconf-pkg-config glib2-devel`
|
||||
2. Run `pnpm buildLibVesktop`
|
||||
3. From now on, building Vesktop will use your own build
|
||||
BIN
build/Assets.car
Normal file
BIN
build/icon.icns
BIN
build/icon.ico
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 9.5 KiB |
1
build/icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" style="isolation:isolate" viewBox="0 0 1024 1024"><path fill="#eb7396" d="M336.23 886.35c56.07 31.62 150.18 51.4 247.12 37.3C816.1 889.78 977.57 673.33 943.7 440.57 909.85 207.83 693.41 46.36 460.65 80.22 227.9 114.08 66.43 330.53 100.3 563.28c6.86 47.2 21.23 91.46 29.65 108.01 6.67 13.1 9.03 35.28 5.28 49.5l-26.8 101.43c-16.75 63.41 21.03 100.93 84.32 83.73l94.59-25.7c14.18-3.86 36.1-1.12 48.9 6.1z" style="display:inline;isolation:isolate"/><g style="display:inline;isolation:isolate"><g style="isolation:isolate"><path d="M494.586 109.162c-3.861.14-7.743.508-11.621 1.004-72.122 6.505-117.149 63.067-128.176 116.693l-17.812 86.508-35.555-76.785v-.002c-23.455-50.637-77.415-90.65-141.998-81.877-32.285 4.386-60.608 22.22-76.123 48.385-13.722 23.14-16.564 52.439-7.66 78.943v1.608l157.394 336.037c24.863 53.102 81.404 87.116 143.43 78.681 61.475-8.36 109.47-52.362 123.914-110.074l91.158-364.25c7.385-29.508 1.643-63.11-20.129-86.328-19.05-20.315-47.417-29.61-76.822-28.543" style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#1d2021;stop-color:#000;stop-opacity:1"/><path fill="#fff" d="m118.49 274.1 153.35 327.4c16 34.18 58.517 59.883 98.852 54.398s78.958-41.418 88.118-78.018l91.16-364.25c9.16-36.6-14.593-67.398-62.253-60.918-51.41 4.189-83.357 45.818-90.957 82.778l-37.84 183.78c-3.42 16.62-11.98 17.61-19.1 2.22l-77.28-166.9c-15.86-34.24-55.539-63.108-97.349-57.428S102.48 239.92 118.49 274.09Z" style="display:inline;fill:#ededed;fill-opacity:1"/><path d="M701.543 396.742a266.4 266.4 0 0 0-100.73 17.746l-.004.002h-.002a271.8 271.8 0 0 0-81.118 48.942 275 275 0 0 0-59.822 74.15v.002a277 277 0 0 0-31.572 93.527v.006l-.002.006c-1.984 13.48-3.023 27.136-3.024 40.893v.014a275.13 275.13 0 0 0 64.01 176.617l.004.004.002.004a269.5 269.5 0 0 0 72.846 61.008l.02.011.02.012a266.5 266.5 0 0 0 93.054 32.19l.017.002a266.3 266.3 0 0 0 114.906-8.061l.02-.006.02-.006a270.3 270.3 0 0 0 67.706-30.816l.004-.002c34.575-21.867 52.68-62.732 45.645-103.032-7.032-40.29-37.897-72.6-77.82-81.47a101.46 101.46 0 0 0-76.338 13.314l-.024.016-.023.015a67 67 0 0 1-16.83 7.67 63.3 63.3 0 0 1-17.957 2.6h-.081a63.86 63.86 0 0 1-31.726-8.37 67 67 0 0 1-17.951-15.042l-.006-.008a72.44 72.44 0 0 1-16.838-46.524v-.016a76.6 76.6 0 0 1 9.367-36.642 72.5 72.5 0 0 1 15.688-19.395l.012-.012.011-.011a69.3 69.3 0 0 1 20.617-12.455 63.97 63.97 0 0 1 59.061 6.896 68.5 68.5 0 0 1 19.91 21.201h.002v.002c28.529 47.605 91.489 63.35 139.068 34.78l.01-.006.01-.006c47.508-28.562 63.227-91.41 34.76-138.975l-.013-.025-.016-.026a271.8 271.8 0 0 0-79.111-84.105l-.026-.02-.027-.017a266.5 266.5 0 0 0-111.506-43.725 266 266 0 0 0-34.223-2.857" style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#ededed;stop-color:#000;stop-opacity:1"/><path fill="#1d2021" d="M751.65 766.94a59.8 59.8 0 0 1 45.03-7.85c23.607 5.237 41.724 24.197 45.882 48.018 4.157 23.821-6.465 47.797-26.902 60.722a228.7 228.7 0 0 1-57.33 26.1 224.7 224.7 0 0 1-96.97 6.8 224.9 224.9 0 0 1-78.56-27.17 227.9 227.9 0 0 1-61.6-51.59 233.5 233.5 0 0 1-54.33-149.94c0-11.67.88-23.3 2.58-34.85a235.4 235.4 0 0 1 26.83-79.48 233.4 233.4 0 0 1 50.78-62.94 230.2 230.2 0 0 1 68.7-41.45 224.8 224.8 0 0 1 113.91-12.56c33.75 5 65.94 17.62 94.1 36.9a230.15 230.15 0 0 1 67 71.23c16.936 28.299 7.765 64.967-20.5 81.96-28.294 16.99-65.005 7.81-81.97-20.5a110.1 110.1 0 0 0-32.08-34.14 105.7 105.7 0 0 0-97.52-11.4 110.9 110.9 0 0 0-33.05 19.96 114 114 0 0 0-24.79 30.69 118.2 118.2 0 0 0-14.51 56.66 114.07 114.07 0 0 0 26.51 73.24 108.6 108.6 0 0 0 29.24 24.5 105.5 105.5 0 0 0 52.45 13.85c10.08 0 20.12-1.45 29.8-4.32a108.6 108.6 0 0 0 27.3-12.44" style="display:inline"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -6,17 +6,34 @@
|
||||
|
||||
//@ts-check
|
||||
|
||||
import { defineConfig } from "eslint/config";
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
import pathAlias from "eslint-plugin-path-alias";
|
||||
import react from "eslint-plugin-react";
|
||||
import simpleHeader from "eslint-plugin-simple-header";
|
||||
import importSort from "eslint-plugin-simple-import-sort";
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import tseslint from "typescript-eslint";
|
||||
import prettier from "eslint-plugin-prettier";
|
||||
|
||||
export default tseslint.config(
|
||||
export default defineConfig(
|
||||
{ ignores: ["dist"] },
|
||||
|
||||
{
|
||||
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}"],
|
||||
settings: {
|
||||
react: {
|
||||
version: "19"
|
||||
}
|
||||
},
|
||||
...react.configs.flat.recommended,
|
||||
rules: {
|
||||
...react.configs.flat.recommended.rules,
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
"react/no-unescaped-entities": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}"],
|
||||
plugins: {
|
||||
@@ -24,8 +41,10 @@ export default tseslint.config(
|
||||
stylistic,
|
||||
importSort,
|
||||
unusedImports,
|
||||
// @ts-expect-error Missing types
|
||||
pathAlias,
|
||||
prettier
|
||||
prettier,
|
||||
"@typescript-eslint": tseslint.plugin
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
@@ -51,7 +70,6 @@ export default tseslint.config(
|
||||
],
|
||||
|
||||
// ESLint Rules
|
||||
|
||||
yoda: "error",
|
||||
eqeqeq: ["error", "always", { null: "ignore" }],
|
||||
"prefer-destructuring": [
|
||||
@@ -67,8 +85,19 @@ export default tseslint.config(
|
||||
"no-invalid-regexp": "error",
|
||||
"no-constant-condition": ["error", { checkLoops: false }],
|
||||
"no-duplicate-imports": "error",
|
||||
"dot-notation": "error",
|
||||
"no-useless-escape": "error",
|
||||
"@typescript-eslint/dot-notation": [
|
||||
"error",
|
||||
{
|
||||
allowPrivateClassPropertyAccess: true,
|
||||
allowProtectedClassPropertyAccess: true
|
||||
}
|
||||
],
|
||||
"no-useless-escape": [
|
||||
"error",
|
||||
{
|
||||
allowRegexCharacters: ["i"]
|
||||
}
|
||||
],
|
||||
"no-fallthrough": "error",
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
@@ -85,7 +114,7 @@ export default tseslint.config(
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"use-isnan": "error",
|
||||
"prefer-const": "error",
|
||||
"prefer-const": ["error", { destructuring: "all" }],
|
||||
"prefer-spread": "error",
|
||||
|
||||
// Styling Rules
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component type="desktop-application">
|
||||
<!--Created with jdAppStreamEdit 7.1-->
|
||||
<id>dev.vencord.Vesktop</id>
|
||||
<name>Vesktop</name>
|
||||
<summary>Snappier Discord app with Vencord</summary>
|
||||
<developer_name>Vencord Contributors</developer_name>
|
||||
<launchable type="desktop-id">dev.vencord.Vesktop.desktop</launchable>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<project_group>Vencord</project_group>
|
||||
<description>
|
||||
<p>Vesktop is a cross platform desktop app aiming to give you a snappier Discord experience with Vencord pre-installed.</p>
|
||||
<p>Vesktop comes bundled with Venmic, a purpose-built library to provide functioning audio screenshare.</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>Vencord settings page and about window open</caption>
|
||||
<image type="source">https://vencord.dev/assets/screenshots/vesktop-1-appstream.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>A dialog showing screenshare options</caption>
|
||||
<image type="source">https://vencord.dev/assets/screenshots/vesktop-2-appstream.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>A screenshot of a Discord server</caption>
|
||||
<image type="source">https://vencord.dev/assets/screenshots/vesktop-3-appstream.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="1.5.7" date="2025-06-08" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.7</url>
|
||||
<description>
|
||||
<p>What's Changed</p>
|
||||
<ul>
|
||||
<li>Starting a screenshare on Windows will no longer mute system audio</li>
|
||||
<li>Fixed showing "Invalid URL" errors in some edge cases (spotty network)</li>
|
||||
<li>Now respects your Autogain preference</li>
|
||||
<li>Improved the flow for linking third party accounts</li>
|
||||
<li>Fixed issue that would cause the AppImage to have no icon and be unpinnable</li>
|
||||
<li>Added the ability to screenshare with stereo audio</li>
|
||||
<li>Added Video Hardware Acceleration switch - Used to always be enabled. Now it is opt-in (disabled by default) to fix graphical glitches that were affecting many users</li>
|
||||
<li>Fixed Discord titlebar buttons</li>
|
||||
<li>Fixed "Object has been destroyed" error in some edge cases</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.5.6" date="2025-04-13" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.6</url>
|
||||
<description>
|
||||
<p>What's Changed</p>
|
||||
<ul>
|
||||
<li>Fixes the new Discord titlebar @Sqaaakoi</li>
|
||||
<li>Fixes Clipboard copy actions not working</li>
|
||||
<li>Should now properly hide the "Download Apps" button again</li>
|
||||
<li>No longer responds to Browser Tab shortcuts like Ctrl+W by @rushiiMachine</li>
|
||||
<li>DevTools keybind should now work properly on Mac @ryanccn</li>
|
||||
<li>Should no longer throw Sandbox errors on Ubuntu</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.5.5" date="2025-02-06" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.5</url>
|
||||
<description>
|
||||
<p>What's Changed</p>
|
||||
<ul>
|
||||
<li>Now remembers your previous Screenshare resolution & FPS</li>
|
||||
<li>You can now disable the splash screen in Vesktop Settings</li>
|
||||
<li>Now supports deep links (opening Discord Message Links in Vesktop) by @Covkie</li>
|
||||
<li>Now supports discord:// uri scheme, allowing it to open things like invites from your Browser even while closed by @Covkie</li>
|
||||
<li>Added 4k resolution to screenshare by @makindotcc</li>
|
||||
<li>Fixed some performance issues caused by a recent Discord update</li>
|
||||
<li>Updated Electron to v34 & chromium to v132, bringing new features and fixes</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.5.4" date="2024-12-05" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.4</url>
|
||||
<description>
|
||||
<p>What's Changed</p>
|
||||
<ul>
|
||||
<li>Upgraded electron to version 33 which brings many improvements and bug fixes</li>
|
||||
<li>AudioShare: add even more granular selection, Allow device sharing by @Curve</li>
|
||||
<li>Enable speech-dispatcher support for TTS on linux by @adryd325</li>
|
||||
<li>fixed screenshare picker window subtitle alignment by @ryawaa</li>
|
||||
<li>fixed splash corners by @Covkie</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.5.3" date="2024-07-04" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.3</url>
|
||||
<description>
|
||||
<p>Features</p>
|
||||
<ul>
|
||||
<li>added arm64 Windows support</li>
|
||||
<li>windows & macOS builds are now universal</li>
|
||||
<li>added option to configure spellcheck languages</li>
|
||||
<li>will auto-update from now on</li>
|
||||
<li>updated electron to 31 & Chromium to 126</li>
|
||||
<li>macOS: Added customized dmg background by @khcrysalis</li>
|
||||
<li>Windows Portable: store settings in portable folder by @MrGarlic1</li>
|
||||
<li>linux audioshare: added granular selection, more options, better ui by @Curve</li>
|
||||
<li>changed default screen-sharing quality to 720p 30 FPS by @Tiagoquix</li>
|
||||
</ul>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>macOS: Added workaround for making things in draggable area clickable by @HAHALOSAH</li>
|
||||
<li>fixed Screenshare UI for non-linux systems by @PolisanTheEasyNick</li>
|
||||
<li>fixed opening on screen that was disconnected by @MrGarlic1</li>
|
||||
<li>mac: hide native window controls with custom titlebar enabled by @MrGarlic1</li>
|
||||
<li>fixed some broken patches by @D3SOX</li>
|
||||
<li>fixed framerate in constraints by @kittykel</li>
|
||||
<li>fixed some first launch switches not applying</li>
|
||||
<li>fixed potential sandbox escape via custom vencord location</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.5.2" date="2024-05-01" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.2</url>
|
||||
<description>
|
||||
<p>What's Changed</p>
|
||||
<ul>
|
||||
<li>Fixed scrollbars looking wrong (actually Discord's fault)</li>
|
||||
<li>Tray: Added left click hide/show feature by @0bCdian</li>
|
||||
<li>MacOS: Fixed the app not properly requesting microphone permissions by @ssalggnikool</li>
|
||||
<li>Linux: Various fixed related to audio screenshare by @Curve</li>
|
||||
<li>Linux: Overhauled & improved screenshare with better framerate by @kaitlynkittyy</li>
|
||||
<li>Users can now pass --enable/disable-features command line flags by @takase1121</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<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">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v1.5.0</url>
|
||||
<description>
|
||||
<p>What's Changed</p>
|
||||
<ul>
|
||||
<li>fully renamed to Vesktop. You will likely have to login to Discord again. You might have to re-create your vesktop shortcut</li>
|
||||
<li>added option to disable smooth scrolling by @ZirixCZ</li>
|
||||
<li>added setting to disable hardware acceleration by @zt64</li>
|
||||
<li>fixed adding connections</li>
|
||||
<li>fixed / improved discord popouts</li>
|
||||
<li>you can now use the custom discord titlebar on linux/mac</li>
|
||||
<li>the splash window is now draggable</li>
|
||||
<li>now signed on mac</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.4.4" date="2023-12-02" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.4</url>
|
||||
<description>
|
||||
<p>What's Changed</p>
|
||||
<ul>
|
||||
<li>improve venmic system compatibility by @Curve</li>
|
||||
<li>Update steamdeck controller layout by @AAGaming00</li>
|
||||
<li>feat: Add option to disable smooth scrolling by @ZirixCZ</li>
|
||||
<li>unblur shiggy in splash screen by @viacoro</li>
|
||||
<li>update electron & arrpc @D3SOX</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.4.3" date="2023-11-01" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.3</url>
|
||||
</release>
|
||||
<release version="0.4.2" date="2023-10-26" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.2</url>
|
||||
</release>
|
||||
<release version="0.4.1" date="2023-10-24" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.1</url>
|
||||
</release>
|
||||
<release version="0.4.0" date="2023-10-21" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.0</url>
|
||||
</release>
|
||||
<release version="0.3.3" date="2023-09-30" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.3</url>
|
||||
</release>
|
||||
<release version="0.3.2" date="2023-09-25" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.2</url>
|
||||
</release>
|
||||
<release version="0.3.1" date="2023-09-25" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.1</url>
|
||||
</release>
|
||||
<release version="0.3.0" date="2023-08-16" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.0</url>
|
||||
</release>
|
||||
<release version="0.2.9" date="2023-08-12" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.9</url>
|
||||
</release>
|
||||
<release version="0.2.8" date="2023-08-02" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.8</url>
|
||||
</release>
|
||||
<release version="0.2.7" date="2023-07-26" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.7</url>
|
||||
</release>
|
||||
<release version="0.2.6" date="2023-07-04" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.6</url>
|
||||
</release>
|
||||
<release version="0.2.5" date="2023-06-26" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.5</url>
|
||||
</release>
|
||||
<release version="0.2.4" date="2023-06-25" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.4</url>
|
||||
</release>
|
||||
<release version="0.2.3" date="2023-06-23" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.3</url>
|
||||
</release>
|
||||
<release version="0.2.2" date="2023-06-21" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.2</url>
|
||||
</release>
|
||||
<release version="0.2.1" date="2023-06-21" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.1</url>
|
||||
</release>
|
||||
<release version="0.2.0" date="2023-05-03" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.0</url>
|
||||
</release>
|
||||
<release version="0.1.9" date="2023-04-27" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.9</url>
|
||||
</release>
|
||||
<release version="0.1.8" date="2023-04-15" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.8</url>
|
||||
</release>
|
||||
<release version="0.1.7" date="2023-04-15" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.7</url>
|
||||
</release>
|
||||
<release version="0.1.6" date="2023-04-11" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.6</url>
|
||||
</release>
|
||||
<release version="0.1.5" date="2023-04-10" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.5</url>
|
||||
</release>
|
||||
<release version="0.1.4" date="2023-04-09" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.4</url>
|
||||
</release>
|
||||
<release version="0.1.3" date="2023-04-06" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.3</url>
|
||||
</release>
|
||||
<release version="0.1.2" date="2023-04-05" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.2</url>
|
||||
</release>
|
||||
<release version="0.1.1" date="2023-04-04" type="stable">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.1</url>
|
||||
</release>
|
||||
<release version="0.1.0" date="2023-04-04" type="development">
|
||||
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.0</url>
|
||||
</release>
|
||||
</releases>
|
||||
<url type="homepage">https://vencord.dev/</url>
|
||||
<url type="bugtracker">https://github.com/Vencord/Vesktop/issues</url>
|
||||
<url type="faq">https://vencord.dev/faq/</url>
|
||||
<url type="help">https://github.com/Vencord/Vesktop/issues</url>
|
||||
<url type="donation">https://github.com/sponsors/Vendicated</url>
|
||||
<url type="vcs-browser">https://github.com/Vencord/Vesktop</url>
|
||||
<categories>
|
||||
<category>InstantMessaging</category>
|
||||
<category>Network</category>
|
||||
</categories>
|
||||
<requires>
|
||||
<control>pointing</control>
|
||||
<control>keyboard</control>
|
||||
<display_length compare="ge">420</display_length>
|
||||
<internet>always</internet>
|
||||
</requires>
|
||||
<recommends>
|
||||
<control>voice</control>
|
||||
<display_length compare="ge">760</display_length>
|
||||
<display_length compare="le">1200</display_length>
|
||||
</recommends>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
<content_attribute id="social-audio">intense</content_attribute>
|
||||
<content_attribute id="social-contacts">intense</content_attribute>
|
||||
<content_attribute id="social-info">intense</content_attribute>
|
||||
</content_rating>
|
||||
<keywords>
|
||||
<keyword>Discord</keyword>
|
||||
<keyword>Vencord</keyword>
|
||||
<keyword>Vesktop</keyword>
|
||||
<keyword>Privacy</keyword>
|
||||
<keyword>Mod</keyword>
|
||||
</keywords>
|
||||
</component>
|
||||
49
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vesktop",
|
||||
"version": "1.5.8",
|
||||
"version": "1.6.1",
|
||||
"private": true,
|
||||
"description": "Vesktop is a custom Discord desktop app",
|
||||
"keywords": [],
|
||||
@@ -11,6 +11,7 @@
|
||||
"scripts": {
|
||||
"build": "tsx scripts/build/build.mts",
|
||||
"build:dev": "pnpm build --dev",
|
||||
"buildLibVesktop": "pnpm -C packages/libvesktop install && pnpm -C packages/libvesktop run build",
|
||||
"package": "pnpm build && electron-builder",
|
||||
"package:dir": "pnpm build && electron-builder --dir",
|
||||
"lint": "eslint",
|
||||
@@ -21,7 +22,7 @@
|
||||
"test": "pnpm lint && pnpm testTypes",
|
||||
"testTypes": "tsc --noEmit",
|
||||
"watch": "pnpm build --watch",
|
||||
"updateMeta": "tsx scripts/utils/updateMeta.mts",
|
||||
"generateMeta": "tsx scripts/utils/generateMeta.mts",
|
||||
"updateArrpcDB": "node ./node_modules/arrpc/update_db.js",
|
||||
"postinstall": "pnpm updateArrpcDB"
|
||||
},
|
||||
@@ -34,28 +35,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
|
||||
"@stylistic/eslint-plugin": "^5.1.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "18.3.1",
|
||||
"@vencord/types": "^1.11.5",
|
||||
"dotenv": "^16.5.0",
|
||||
"electron": "^37.2.0",
|
||||
"@stylistic/eslint-plugin": "^5.6.1",
|
||||
"@types/node": "^25.0.1",
|
||||
"@types/react": "19.2.1",
|
||||
"@vencord/types": "^1.13.7",
|
||||
"dotenv": "^17.2.3",
|
||||
"electron": "^39.2.7",
|
||||
"electron-builder": "^26.0.12",
|
||||
"esbuild": "^0.25.5",
|
||||
"eslint": "^9.30.1",
|
||||
"esbuild": "^0.27.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-path-alias": "^2.1.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-simple-header": "^1.2.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"prettier": "^3.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"libvesktop": "link:packages/libvesktop",
|
||||
"prettier": "^3.7.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tsx": "^4.20.3",
|
||||
"type-fest": "^4.41.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.35.1",
|
||||
"xml-formatter": "^3.6.6"
|
||||
"tsx": "^4.21.0",
|
||||
"type-fest": "^5.3.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.49.0",
|
||||
"xml-formatter": "^3.6.7"
|
||||
},
|
||||
"packageManager": "pnpm@10.7.1",
|
||||
"engines": {
|
||||
@@ -65,6 +68,7 @@
|
||||
"build": {
|
||||
"appId": "dev.vencord.vesktop",
|
||||
"productName": "Vesktop",
|
||||
"executableName": "vesktop",
|
||||
"files": [
|
||||
"!*",
|
||||
"!node_modules",
|
||||
@@ -79,9 +83,10 @@
|
||||
"discord"
|
||||
]
|
||||
},
|
||||
"beforePack": "scripts/build/sandboxFix.js",
|
||||
"beforePack": "scripts/build/beforePack.mjs",
|
||||
"afterPack": "scripts/build/afterPack.mjs",
|
||||
"linux": {
|
||||
"icon": "build/icon.icns",
|
||||
"icon": "build/icon.svg",
|
||||
"category": "Network",
|
||||
"maintainer": "vendicated+vesktop@riseup.net",
|
||||
"target": [
|
||||
@@ -141,7 +146,8 @@
|
||||
"NSMicrophoneUsageDescription": "This app needs access to the microphone",
|
||||
"NSCameraUsageDescription": "This app needs access to the camera",
|
||||
"com.apple.security.device.audio-input": true,
|
||||
"com.apple.security.device.camera": true
|
||||
"com.apple.security.device.camera": true,
|
||||
"CFBundleIconName": "Icon"
|
||||
},
|
||||
"notarize": true
|
||||
},
|
||||
@@ -171,6 +177,7 @@
|
||||
"oneClick": false
|
||||
},
|
||||
"win": {
|
||||
"icon": "build/icon.ico",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
|
||||
1
packages/libvesktop/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
19
packages/libvesktop/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
# Dockerfile for building both x64 and arm64 on an old distro for maximum compatibility.
|
||||
|
||||
# ubuntu20 is dead but debian11 is still supported for now
|
||||
FROM debian:11
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN dpkg --add-architecture arm64
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential python3 curl pkg-config \
|
||||
g++-aarch64-linux-gnu libglib2.0-dev:amd64 libglib2.0-dev:arm64 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get update && apt-get install -y nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src
|
||||
19
packages/libvesktop/binding.gyp
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "libvesktop",
|
||||
"sources": [ "src/libvesktop.cc" ],
|
||||
"include_dirs": [
|
||||
"<!@(node -p \"require('node-addon-api').include\")"
|
||||
],
|
||||
"cflags_cc": [
|
||||
"<!(pkg-config --cflags glib-2.0 gio-2.0)",
|
||||
"-O3"
|
||||
],
|
||||
"libraries": [
|
||||
"<!@(pkg-config --libs-only-l --libs-only-other glib-2.0 gio-2.0)"
|
||||
],
|
||||
"cflags_cc!": ["-fno-exceptions"],
|
||||
}
|
||||
]
|
||||
}
|
||||
17
packages/libvesktop/build.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
docker build -t libvesktop-builder -f Dockerfile .
|
||||
|
||||
docker run --rm -v "$PWD":/src -w /src libvesktop-builder bash -c "
|
||||
set -e
|
||||
|
||||
echo '=== Building x64 ==='
|
||||
npx node-gyp rebuild --arch=x64
|
||||
mv build/Release/vesktop.node prebuilds/vesktop-x64.node
|
||||
|
||||
echo '=== Building arm64 ==='
|
||||
export CXX=aarch64-linux-gnu-g++
|
||||
npx node-gyp rebuild --arch=arm64
|
||||
mv build/Release/vesktop.node prebuilds/vesktop-arm64.node
|
||||
"
|
||||
3
packages/libvesktop/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export function getAccentColor(): number | null;
|
||||
export function requestBackground(autoStart: boolean, commandLine: string[]): boolean;
|
||||
export function updateUnityLauncherCount(count: number): boolean;
|
||||
14
packages/libvesktop/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "libvesktop",
|
||||
"main": "build/Release/vesktop.node",
|
||||
"types": "index.d.ts",
|
||||
"devDependencies": {
|
||||
"node-addon-api": "^8.5.0",
|
||||
"node-gyp": "^11.4.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node-gyp configure build",
|
||||
"clean": "node-gyp clean",
|
||||
"test": "npm run build && node test.js"
|
||||
}
|
||||
}
|
||||
730
packages/libvesktop/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,730 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
node-addon-api:
|
||||
specifier: ^8.5.0
|
||||
version: 8.5.0
|
||||
node-gyp:
|
||||
specifier: ^11.4.2
|
||||
version: 11.4.2
|
||||
|
||||
packages:
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@npmcli/agent@3.0.0':
|
||||
resolution: {integrity: sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
'@npmcli/fs@4.0.0':
|
||||
resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
abbrev@3.0.1:
|
||||
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-regex@6.2.2:
|
||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
brace-expansion@2.0.2:
|
||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||
|
||||
cacache@19.0.1:
|
||||
resolution: {integrity: sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
chownr@3.0.0:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
encoding@0.1.13:
|
||||
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
|
||||
|
||||
env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
err-code@2.0.3:
|
||||
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
|
||||
|
||||
exponential-backoff@3.1.2:
|
||||
resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
fs-minipass@3.0.3:
|
||||
resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
hasBin: true
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
http-cache-semantics@4.2.0:
|
||||
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
imurmurhash@0.1.4:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
|
||||
ip-address@10.0.1:
|
||||
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
isexe@3.1.1:
|
||||
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
make-fetch-happen@14.0.3:
|
||||
resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minipass-collect@2.0.1:
|
||||
resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minipass-fetch@4.0.1:
|
||||
resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
minipass-flush@1.0.5:
|
||||
resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
minipass-pipeline@1.2.4:
|
||||
resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
minipass-sized@1.0.3:
|
||||
resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
minipass@3.3.6:
|
||||
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minizlib@3.1.0:
|
||||
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
node-addon-api@8.5.0:
|
||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-gyp@11.4.2:
|
||||
resolution: {integrity: sha512-3gD+6zsrLQH7DyYOUIutaauuXrcyxeTPyQuZQCQoNPZMHMMS5m4y0xclNpvYzoK3VNzuyxT6eF4mkIL4WSZ1eQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
hasBin: true
|
||||
|
||||
nopt@8.1.0:
|
||||
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
hasBin: true
|
||||
|
||||
p-map@7.0.3:
|
||||
resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
picomatch@4.0.3:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
proc-log@5.0.0:
|
||||
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
promise-retry@2.0.1:
|
||||
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
retry@0.12.0:
|
||||
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
semver@7.7.2:
|
||||
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shebang-regex@3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
smart-buffer@4.2.0:
|
||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
socks-proxy-agent@8.0.5:
|
||||
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
socks@2.8.7:
|
||||
resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
|
||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
ssri@12.0.0:
|
||||
resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@5.1.2:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
tar@7.5.1:
|
||||
resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
unique-filename@4.0.0:
|
||||
resolution: {integrity: sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
unique-slug@5.0.0:
|
||||
resolution: {integrity: sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
which@5.0.0:
|
||||
resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yallist@5.0.0:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.2
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
'@npmcli/agent@3.0.0':
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
lru-cache: 10.4.3
|
||||
socks-proxy-agent: 8.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@npmcli/fs@4.0.0':
|
||||
dependencies:
|
||||
semver: 7.7.2
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
abbrev@3.0.1: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
brace-expansion@2.0.2:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
cacache@19.0.1:
|
||||
dependencies:
|
||||
'@npmcli/fs': 4.0.0
|
||||
fs-minipass: 3.0.3
|
||||
glob: 10.4.5
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
minipass-collect: 2.0.1
|
||||
minipass-flush: 1.0.5
|
||||
minipass-pipeline: 1.2.4
|
||||
p-map: 7.0.3
|
||||
ssri: 12.0.0
|
||||
tar: 7.5.1
|
||||
unique-filename: 4.0.0
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
encoding@0.1.13:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
optional: true
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
err-code@2.0.3: {}
|
||||
|
||||
exponential-backoff@3.1.2: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fs-minipass@3.0.3:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
glob@10.4.5:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
http-cache-semantics@4.2.0: {}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
optional: true
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
ip-address@10.0.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
isexe@3.1.1: {}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
make-fetch-happen@14.0.3:
|
||||
dependencies:
|
||||
'@npmcli/agent': 3.0.0
|
||||
cacache: 19.0.1
|
||||
http-cache-semantics: 4.2.0
|
||||
minipass: 7.1.2
|
||||
minipass-fetch: 4.0.1
|
||||
minipass-flush: 1.0.5
|
||||
minipass-pipeline: 1.2.4
|
||||
negotiator: 1.0.0
|
||||
proc-log: 5.0.0
|
||||
promise-retry: 2.0.1
|
||||
ssri: 12.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minipass-collect@2.0.1:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
minipass-fetch@4.0.1:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
minipass-sized: 1.0.3
|
||||
minizlib: 3.1.0
|
||||
optionalDependencies:
|
||||
encoding: 0.1.13
|
||||
|
||||
minipass-flush@1.0.5:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
|
||||
minipass-pipeline@1.2.4:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
|
||||
minipass-sized@1.0.3:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
|
||||
minipass@3.3.6:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
minizlib@3.1.0:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
node-addon-api@8.5.0: {}
|
||||
|
||||
node-gyp@11.4.2:
|
||||
dependencies:
|
||||
env-paths: 2.2.1
|
||||
exponential-backoff: 3.1.2
|
||||
graceful-fs: 4.2.11
|
||||
make-fetch-happen: 14.0.3
|
||||
nopt: 8.1.0
|
||||
proc-log: 5.0.0
|
||||
semver: 7.7.2
|
||||
tar: 7.5.1
|
||||
tinyglobby: 0.2.15
|
||||
which: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
nopt@8.1.0:
|
||||
dependencies:
|
||||
abbrev: 3.0.1
|
||||
|
||||
p-map@7.0.3: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
||||
proc-log@5.0.0: {}
|
||||
|
||||
promise-retry@2.0.1:
|
||||
dependencies:
|
||||
err-code: 2.0.3
|
||||
retry: 0.12.0
|
||||
|
||||
retry@0.12.0: {}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
optional: true
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
smart-buffer@4.2.0: {}
|
||||
|
||||
socks-proxy-agent@8.0.5:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
socks: 2.8.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
socks@2.8.7:
|
||||
dependencies:
|
||||
ip-address: 10.0.1
|
||||
smart-buffer: 4.2.0
|
||||
|
||||
ssri@12.0.0:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string-width@5.1.2:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
dependencies:
|
||||
ansi-regex: 6.2.2
|
||||
|
||||
tar@7.5.1:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
minipass: 7.1.2
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
unique-filename@4.0.0:
|
||||
dependencies:
|
||||
unique-slug: 5.0.0
|
||||
|
||||
unique-slug@5.0.0:
|
||||
dependencies:
|
||||
imurmurhash: 0.1.4
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
which@5.0.0:
|
||||
dependencies:
|
||||
isexe: 3.1.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
BIN
packages/libvesktop/prebuilds/vesktop-arm64.node
Executable file
BIN
packages/libvesktop/prebuilds/vesktop-x64.node
Executable file
269
packages/libvesktop/src/libvesktop.cc
Normal file
@@ -0,0 +1,269 @@
|
||||
#include <gio/gio.h>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <napi.h>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
template <typename T>
|
||||
struct GObjectDeleter
|
||||
{
|
||||
void operator()(T *obj) const
|
||||
{
|
||||
if (obj)
|
||||
g_object_unref(obj);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using GObjectPtr = std::unique_ptr<T, GObjectDeleter<T>>;
|
||||
|
||||
struct GVariantDeleter
|
||||
{
|
||||
void operator()(GVariant *variant) const
|
||||
{
|
||||
if (variant)
|
||||
g_variant_unref(variant);
|
||||
}
|
||||
};
|
||||
|
||||
using GVariantPtr = std::unique_ptr<GVariant, GVariantDeleter>;
|
||||
|
||||
struct GErrorDeleter
|
||||
{
|
||||
void operator()(GError *error) const
|
||||
{
|
||||
if (error)
|
||||
g_error_free(error);
|
||||
}
|
||||
};
|
||||
|
||||
using GErrorPtr = std::unique_ptr<GError, GErrorDeleter>;
|
||||
|
||||
bool update_launcher_count(int count)
|
||||
{
|
||||
GError *error = nullptr;
|
||||
|
||||
const char *chromeDesktop = std::getenv("CHROME_DESKTOP");
|
||||
std::string desktop_id = std::string("application://") + (chromeDesktop ? chromeDesktop : "vesktop.desktop");
|
||||
|
||||
GObjectPtr<GDBusConnection> bus(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
|
||||
if (!bus)
|
||||
{
|
||||
GErrorPtr error_ptr(error);
|
||||
std::cerr << "[libvesktop::update_launcher_count] Failed to connect to session bus: "
|
||||
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
|
||||
g_variant_builder_add(&builder, "{sv}", "count", g_variant_new_int64(count));
|
||||
g_variant_builder_add(&builder, "{sv}", "count-visible", g_variant_new_boolean(count != 0));
|
||||
|
||||
gboolean result = g_dbus_connection_emit_signal(
|
||||
bus.get(),
|
||||
nullptr,
|
||||
"/",
|
||||
"com.canonical.Unity.LauncherEntry",
|
||||
"Update",
|
||||
g_variant_new("(sa{sv})", desktop_id.c_str(), &builder),
|
||||
&error);
|
||||
|
||||
if (!result || error)
|
||||
{
|
||||
GErrorPtr error_ptr(error);
|
||||
std::cerr << "[libvesktop::update_launcher_count] Failed to emit Update signal: "
|
||||
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<int32_t> get_accent_color()
|
||||
{
|
||||
GError *error = nullptr;
|
||||
|
||||
GObjectPtr<GDBusConnection> bus(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
|
||||
if (!bus)
|
||||
{
|
||||
GErrorPtr error_ptr(error);
|
||||
std::cerr << "[libvesktop::get_accent_color] Failed to connect to session bus: "
|
||||
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
GVariantPtr reply(g_dbus_connection_call_sync(
|
||||
bus.get(),
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"Read",
|
||||
g_variant_new("(ss)", "org.freedesktop.appearance", "accent-color"),
|
||||
nullptr,
|
||||
G_DBUS_CALL_FLAGS_NONE,
|
||||
5000,
|
||||
nullptr,
|
||||
&error));
|
||||
|
||||
if (!reply)
|
||||
{
|
||||
GErrorPtr error_ptr(error);
|
||||
std::cerr << "[libvesktop::get_accent_color] Failed to call Read: "
|
||||
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
GVariant *inner_raw = nullptr;
|
||||
g_variant_get(reply.get(), "(v)", &inner_raw);
|
||||
if (!inner_raw)
|
||||
{
|
||||
std::cerr << "[libvesktop::get_accent_color] Inner variant is null" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
GVariantPtr inner(inner_raw);
|
||||
|
||||
// Unwrap nested variants
|
||||
while (g_variant_is_of_type(inner.get(), G_VARIANT_TYPE_VARIANT))
|
||||
{
|
||||
GVariant *next = g_variant_get_variant(inner.get());
|
||||
inner.reset(next);
|
||||
}
|
||||
|
||||
if (!g_variant_is_of_type(inner.get(), G_VARIANT_TYPE_TUPLE) ||
|
||||
g_variant_n_children(inner.get()) < 3)
|
||||
{
|
||||
std::cerr << "[libvesktop::get_accent_color] Inner variant is not a tuple of 3 doubles" << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
double r = 0.0, g = 0.0, b = 0.0;
|
||||
g_variant_get(inner.get(), "(ddd)", &r, &g, &b);
|
||||
|
||||
bool discard = false;
|
||||
auto toInt = [&discard](double v) -> int
|
||||
{
|
||||
if (!std::isfinite(v) || v < 0.0 || v > 1.0)
|
||||
{
|
||||
discard = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return static_cast<int>(std::round(v * 255.0));
|
||||
};
|
||||
|
||||
int32_t rgb = (toInt(r) << 16) | (toInt(g) << 8) | toInt(b);
|
||||
if (discard)
|
||||
return std::nullopt;
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
bool request_background(bool autostart, const std::vector<std::string> &commandline)
|
||||
{
|
||||
GError *error = nullptr;
|
||||
|
||||
GObjectPtr<GDBusConnection> bus(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
|
||||
if (!bus)
|
||||
{
|
||||
GErrorPtr error_ptr(error);
|
||||
std::cerr << "[libvesktop::request_background] Failed to connect to session bus: "
|
||||
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
|
||||
g_variant_builder_add(&builder, "{sv}", "autostart", g_variant_new_boolean(autostart));
|
||||
|
||||
if (!commandline.empty())
|
||||
{
|
||||
GVariantBuilder cmd_builder;
|
||||
g_variant_builder_init(&cmd_builder, G_VARIANT_TYPE("as"));
|
||||
for (const auto &s : commandline)
|
||||
g_variant_builder_add(&cmd_builder, "s", s.c_str());
|
||||
g_variant_builder_add(&builder, "{sv}", "commandline", g_variant_builder_end(&cmd_builder));
|
||||
}
|
||||
|
||||
GVariantPtr reply(g_dbus_connection_call_sync(
|
||||
bus.get(),
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Background",
|
||||
"RequestBackground",
|
||||
g_variant_new("(sa{sv})", "", &builder),
|
||||
nullptr,
|
||||
G_DBUS_CALL_FLAGS_NONE,
|
||||
5000,
|
||||
nullptr,
|
||||
&error));
|
||||
|
||||
if (!reply)
|
||||
{
|
||||
GErrorPtr error_ptr(error);
|
||||
std::cerr << "[libvesktop::request_background] Failed to call RequestBackground: "
|
||||
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Napi::Value updateUnityLauncherCount(Napi::CallbackInfo const &info)
|
||||
{
|
||||
if (info.Length() < 1 || !info[0].IsNumber())
|
||||
{
|
||||
Napi::TypeError::New(info.Env(), "Expected (number)").ThrowAsJavaScriptException();
|
||||
return info.Env().Undefined();
|
||||
}
|
||||
|
||||
int count = info[0].As<Napi::Number>().Int32Value();
|
||||
bool success = update_launcher_count(count);
|
||||
return Napi::Boolean::New(info.Env(), success);
|
||||
}
|
||||
|
||||
Napi::Value getAccentColor(const Napi::CallbackInfo &info)
|
||||
{
|
||||
auto color = get_accent_color();
|
||||
if (color)
|
||||
return Napi::Number::New(info.Env(), *color);
|
||||
return info.Env().Null();
|
||||
}
|
||||
|
||||
Napi::Value RequestBackground(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (info.Length() < 2 || !info[0].IsBoolean() || !info[1].IsArray())
|
||||
{
|
||||
Napi::TypeError::New(env, "Expected (boolean, string[])").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
bool autostart = info[0].As<Napi::Boolean>();
|
||||
Napi::Array arr = info[1].As<Napi::Array>();
|
||||
std::vector<std::string> commandline;
|
||||
for (uint32_t i = 0; i < arr.Length(); i++)
|
||||
{
|
||||
Napi::Value v = arr.Get(i);
|
||||
if (v.IsString())
|
||||
commandline.push_back(v.As<Napi::String>().Utf8Value());
|
||||
}
|
||||
|
||||
bool ok = request_background(autostart, commandline);
|
||||
return Napi::Boolean::New(env, ok);
|
||||
}
|
||||
|
||||
Napi::Object Init(Napi::Env env, Napi::Object exports)
|
||||
{
|
||||
exports.Set("updateUnityLauncherCount", Napi::Function::New(env, updateUnityLauncherCount));
|
||||
exports.Set("getAccentColor", Napi::Function::New(env, getAccentColor));
|
||||
exports.Set("requestBackground", Napi::Function::New(env, RequestBackground));
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_API_MODULE(libvesktop, Init)
|
||||
22
packages/libvesktop/test.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @type {typeof import(".")}
|
||||
*/
|
||||
const libVesktop = require(".");
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
test("getAccentColor should return a number", () => {
|
||||
const color = libVesktop.getAccentColor();
|
||||
assert.strictEqual(typeof color, "number");
|
||||
});
|
||||
|
||||
test("updateUnityLauncherCount should return true (success)", () => {
|
||||
assert.strictEqual(libVesktop.updateUnityLauncherCount(5), true);
|
||||
assert.strictEqual(libVesktop.updateUnityLauncherCount(0), true);
|
||||
assert.strictEqual(libVesktop.updateUnityLauncherCount(10), true);
|
||||
});
|
||||
|
||||
test("requestBackground should return true (success)", () => {
|
||||
assert.strictEqual(libVesktop.requestBackground(true, ["bash"]), true);
|
||||
assert.strictEqual(libVesktop.requestBackground(false, []), true);
|
||||
});
|
||||
1533
pnpm-lock.yaml
generated
24
scripts/build/addAssetsCar.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
import { copyFile, readdir } from "fs/promises";
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* readonly appOutDir: string;
|
||||
* readonly arch: Arch;
|
||||
* readonly electronPlatformName: string;
|
||||
* readonly outDir: string;
|
||||
* readonly packager: PlatformPackager;
|
||||
* readonly targets: Target[];
|
||||
* }} context
|
||||
*/
|
||||
export async function addAssetsCar({ appOutDir }) {
|
||||
if (process.platform !== "darwin") return;
|
||||
|
||||
const appName = (await readdir(appOutDir)).find(item => item.endsWith(".app"));
|
||||
|
||||
if (!appName) {
|
||||
console.warn(`Could not find .app directory in ${appOutDir}. Skipping adding assets.car`);
|
||||
return;
|
||||
}
|
||||
|
||||
await copyFile("build/Assets.car", `${appOutDir}/${appName}/Contents/Resources/Assets.car`);
|
||||
}
|
||||
5
scripts/build/afterPack.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
import { addAssetsCar } from "./addAssetsCar.mjs";
|
||||
|
||||
export default async function afterPack(context) {
|
||||
await addAssetsCar(context);
|
||||
}
|
||||
5
scripts/build/beforePack.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
import { applyAppImageSandboxFix } from "./sandboxFix.mjs";
|
||||
|
||||
export default async function beforePack() {
|
||||
await applyAppImageSandboxFix();
|
||||
}
|
||||
@@ -25,6 +25,9 @@ const NodeCommonOpts: BuildOptions = {
|
||||
platform: "node",
|
||||
external: ["electron"],
|
||||
target: ["esnext"],
|
||||
loader: {
|
||||
".node": "file"
|
||||
},
|
||||
define: {
|
||||
IS_DEV: JSON.stringify(isDev)
|
||||
}
|
||||
@@ -50,30 +53,58 @@ async function copyVenmic() {
|
||||
]).catch(() => console.warn("Failed to copy venmic. Building without venmic support"));
|
||||
}
|
||||
|
||||
async function copyLibVesktop() {
|
||||
if (process.platform !== "linux") return;
|
||||
|
||||
try {
|
||||
await copyFile(
|
||||
"./packages/libvesktop/build/Release/vesktop.node",
|
||||
`./static/dist/libvesktop-${process.arch}.node`
|
||||
);
|
||||
console.log("Using local libvesktop build");
|
||||
} catch {
|
||||
console.log(
|
||||
"Using prebuilt libvesktop binaries. Run `pnpm buildLibVesktop` and build again to build from source - see README.md for more details"
|
||||
);
|
||||
return Promise.all([
|
||||
copyFile("./packages/libvesktop/prebuilds/vesktop-x64.node", "./static/dist/libvesktop-x64.node"),
|
||||
copyFile("./packages/libvesktop/prebuilds/vesktop-arm64.node", "./static/dist/libvesktop-arm64.node")
|
||||
]).catch(() => console.warn("Failed to copy libvesktop. Building without libvesktop support"));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
copyVenmic(),
|
||||
copyLibVesktop(),
|
||||
createContext({
|
||||
...NodeCommonOpts,
|
||||
entryPoints: ["src/main/index.ts"],
|
||||
outfile: "dist/js/main.js",
|
||||
footer: { js: "//# sourceURL=VCDMain" }
|
||||
footer: { js: "//# sourceURL=VesktopMain" }
|
||||
}),
|
||||
createContext({
|
||||
...NodeCommonOpts,
|
||||
entryPoints: ["src/main/arrpc/worker.ts"],
|
||||
outfile: "dist/js/arRpcWorker.js",
|
||||
footer: { js: "//# sourceURL=VCDArRpcWorker" }
|
||||
footer: { js: "//# sourceURL=VesktopArRpcWorker" }
|
||||
}),
|
||||
createContext({
|
||||
...NodeCommonOpts,
|
||||
entryPoints: ["src/preload/index.ts"],
|
||||
outfile: "dist/js/preload.js",
|
||||
footer: { js: "//# sourceURL=VCDPreload" }
|
||||
footer: { js: "//# sourceURL=VesktopPreload" }
|
||||
}),
|
||||
createContext({
|
||||
...NodeCommonOpts,
|
||||
entryPoints: ["src/preload/splash.ts"],
|
||||
outfile: "dist/js/splashPreload.js"
|
||||
outfile: "dist/js/splashPreload.js",
|
||||
footer: { js: "//# sourceURL=VesktopSplashPreload" }
|
||||
}),
|
||||
createContext({
|
||||
...NodeCommonOpts,
|
||||
entryPoints: ["src/preload/updater.ts"],
|
||||
outfile: "dist/js/updaterPreload.js",
|
||||
footer: { js: "//# sourceURL=VesktopUpdaterPreload" }
|
||||
}),
|
||||
createContext({
|
||||
...CommonOpts,
|
||||
@@ -86,7 +117,7 @@ await Promise.all([
|
||||
jsxFragment: "VencordFragment",
|
||||
external: ["@vencord/types/*"],
|
||||
plugins: [vencordDep, includeDirPlugin("patches", "src/renderer/patches")],
|
||||
footer: { js: "//# sourceURL=VCDRenderer" }
|
||||
footer: { js: "//# sourceURL=VesktopRenderer" }
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
@@ -6,18 +6,21 @@
|
||||
|
||||
// Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js
|
||||
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import AppImageTarget from "app-builder-lib/out/targets/AppImageTarget.js";
|
||||
|
||||
let isApplied = false;
|
||||
|
||||
const hook = async () => {
|
||||
if (isApplied) return;
|
||||
isApplied = true;
|
||||
export async function applyAppImageSandboxFix() {
|
||||
if (process.platform !== "linux") {
|
||||
// this fix is only required on linux
|
||||
return;
|
||||
}
|
||||
const AppImageTarget = require("app-builder-lib/out/targets/AppImageTarget");
|
||||
|
||||
if (isApplied) return;
|
||||
isApplied = true;
|
||||
|
||||
const oldBuildMethod = AppImageTarget.default.prototype.build;
|
||||
AppImageTarget.default.prototype.build = async function (...args) {
|
||||
console.log("Running AppImage builder hook", args);
|
||||
@@ -69,6 +72,4 @@ exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ]
|
||||
|
||||
return ret;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = hook;
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { promises as fs } from "node:fs";
|
||||
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
|
||||
import xmlFormat from "xml-formatter";
|
||||
|
||||
@@ -43,14 +43,25 @@ function generateDescription(description: string, descriptionNode: Element) {
|
||||
}
|
||||
}
|
||||
|
||||
const latestReleaseInformation = await fetch("https://api.github.com/repos/Vencord/Vesktop/releases/latest", {
|
||||
const releases = await fetch("https://api.github.com/repos/Vencord/Vesktop/releases", {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-Github-Api-Version": "2022-11-28"
|
||||
}
|
||||
}).then(res => res.json());
|
||||
|
||||
const metaInfo = await fs.readFile("./meta/dev.vencord.Vesktop.metainfo.xml", "utf-8");
|
||||
const latestReleaseInformation = releases[0];
|
||||
|
||||
const metaInfo = await (async () => {
|
||||
for (const release of releases) {
|
||||
const metaAsset = release.assets.find((a: any) => a.name === "dev.vencord.Vesktop.metainfo.xml");
|
||||
if (metaAsset) return fetch(metaAsset.browser_download_url).then(res => res.text());
|
||||
}
|
||||
})();
|
||||
|
||||
if (!metaInfo) {
|
||||
throw new Error("Could not find existing meta information from any release");
|
||||
}
|
||||
|
||||
const parser = new DOMParser().parseFromString(metaInfo, "text/xml");
|
||||
|
||||
@@ -90,4 +101,7 @@ const output = xmlFormat(new XMLSerializer().serializeToString(parser), {
|
||||
indentation: " "
|
||||
});
|
||||
|
||||
await fs.writeFile("./meta/dev.vencord.Vesktop.metainfo.xml", output, "utf-8");
|
||||
await mkdir("./dist", { recursive: true });
|
||||
await fs.writeFile("./dist/dev.vencord.Vesktop.metainfo.xml", output, "utf-8");
|
||||
|
||||
console.log("Updated meta information written to ./dist/dev.vencord.Vesktop.metainfo.xml");
|
||||
@@ -5,10 +5,9 @@
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { join } from "path";
|
||||
import { ICON_PATH, VIEW_DIR } from "shared/paths";
|
||||
|
||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||
import { loadView } from "./vesktopStatic";
|
||||
|
||||
export async function createAboutWindow() {
|
||||
const height = 750;
|
||||
@@ -17,7 +16,6 @@ export async function createAboutWindow() {
|
||||
const about = new BrowserWindow({
|
||||
center: true,
|
||||
autoHideMenuBar: true,
|
||||
icon: ICON_PATH,
|
||||
height,
|
||||
width
|
||||
});
|
||||
@@ -28,9 +26,7 @@ export async function createAboutWindow() {
|
||||
APP_VERSION: app.getVersion()
|
||||
});
|
||||
|
||||
about.loadFile(join(VIEW_DIR, "about.html"), {
|
||||
search: data.toString()
|
||||
});
|
||||
loadView(about, "about.html", data);
|
||||
|
||||
return about;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ import { app, NativeImage, nativeImage } from "electron";
|
||||
import { join } from "path";
|
||||
import { BADGE_DIR } from "shared/paths";
|
||||
|
||||
import { updateUnityLauncherCount } from "./dbus";
|
||||
import { AppEvents } from "./events";
|
||||
import { mainWin } from "./mainWindow";
|
||||
|
||||
const imgCache = new Map<number, NativeImage>();
|
||||
function loadBadge(index: number) {
|
||||
const cached = imgCache.get(index);
|
||||
@@ -21,11 +25,17 @@ function loadBadge(index: number) {
|
||||
|
||||
let lastIndex: null | number = -1;
|
||||
|
||||
/**
|
||||
* -1 = show unread indicator
|
||||
* 0 = clear
|
||||
*/
|
||||
export function setBadgeCount(count: number) {
|
||||
AppEvents.emit("setTrayVariant", count !== 0 ? "trayUnread" : "tray");
|
||||
|
||||
switch (process.platform) {
|
||||
case "linux":
|
||||
if (count === -1) count = 0;
|
||||
app.setBadgeCount(count);
|
||||
updateUnityLauncherCount(count);
|
||||
break;
|
||||
case "darwin":
|
||||
if (count === 0) {
|
||||
@@ -40,8 +50,6 @@ export function setBadgeCount(count: number) {
|
||||
|
||||
lastIndex = index;
|
||||
|
||||
// circular import shenanigans
|
||||
const { mainWin } = require("./mainWindow") as typeof import("./mainWindow");
|
||||
mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,30 +5,32 @@
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { stripIndent } from "shared/utils/text";
|
||||
|
||||
import { IS_FLATPAK } from "./constants";
|
||||
import { requestBackground } from "./dbus";
|
||||
import { Settings, State } from "./settings";
|
||||
import { escapeDesktopFileArgument } from "./utils/desktopFileEscape";
|
||||
|
||||
interface AutoStart {
|
||||
isEnabled(): boolean;
|
||||
enable(): void;
|
||||
disable(): void;
|
||||
}
|
||||
|
||||
function makeAutoStartLinux(): AutoStart {
|
||||
function getEscapedCommandLine() {
|
||||
const args = process.argv.map(escapeDesktopFileArgument);
|
||||
if (Settings.store.autoStartMinimized) args.push("--start-minimized");
|
||||
return args;
|
||||
}
|
||||
|
||||
function makeAutoStartLinuxDesktop(): AutoStart {
|
||||
const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config");
|
||||
const dir = join(configDir, "autostart");
|
||||
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),
|
||||
enable() {
|
||||
@@ -37,9 +39,10 @@ function makeAutoStartLinux(): AutoStart {
|
||||
Type=Application
|
||||
Name=Vesktop
|
||||
Comment=Vesktop autostart script
|
||||
Exec=${commandLine}
|
||||
Exec=${getEscapedCommandLine().join(" ")}
|
||||
StartupNotify=false
|
||||
Terminal=false
|
||||
Icon=vesktop
|
||||
`;
|
||||
|
||||
mkdirSync(dir, { recursive: true });
|
||||
@@ -49,10 +52,49 @@ function makeAutoStartLinux(): AutoStart {
|
||||
};
|
||||
}
|
||||
|
||||
function makeAutoStartLinuxPortal() {
|
||||
return {
|
||||
isEnabled: () => State.store.linuxAutoStartEnabled === true,
|
||||
enable() {
|
||||
const success = requestBackground(true, getEscapedCommandLine());
|
||||
if (success) {
|
||||
State.store.linuxAutoStartEnabled = true;
|
||||
}
|
||||
return success;
|
||||
},
|
||||
disable() {
|
||||
const success = requestBackground(false, []);
|
||||
if (success) {
|
||||
State.store.linuxAutoStartEnabled = false;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const autoStartWindowsMac: AutoStart = {
|
||||
isEnabled: () => app.getLoginItemSettings().openAtLogin,
|
||||
enable: () => app.setLoginItemSettings({ openAtLogin: true }),
|
||||
enable: () =>
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: true,
|
||||
args: Settings.store.autoStartMinimized ? ["--start-minimized"] : []
|
||||
}),
|
||||
disable: () => app.setLoginItemSettings({ openAtLogin: false })
|
||||
};
|
||||
|
||||
export const autoStart = process.platform === "linux" ? makeAutoStartLinux() : autoStartWindowsMac;
|
||||
// The portal call uses the app id by default, which is org.chromium.Chromium, even in packaged Vesktop.
|
||||
// This leads to an autostart entry named "Chromium" instead of "Vesktop".
|
||||
// Thus, only use the portal inside Flatpak, where the app is actually correct.
|
||||
// Maybe there is a way to fix it outside of flatpak, but I couldn't figure it out.
|
||||
export const autoStart =
|
||||
process.platform !== "linux"
|
||||
? autoStartWindowsMac
|
||||
: IS_FLATPAK
|
||||
? makeAutoStartLinuxPortal()
|
||||
: makeAutoStartLinuxDesktop();
|
||||
|
||||
Settings.addChangeListener("autoStartMinimized", () => {
|
||||
if (!autoStart.isEnabled()) return;
|
||||
|
||||
autoStart.enable();
|
||||
});
|
||||
|
||||
145
src/main/cli.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { basename } from "path";
|
||||
import { stripIndent } from "shared/utils/text";
|
||||
import { parseArgs, ParseArgsOptionDescriptor } from "util";
|
||||
|
||||
type Option = ParseArgsOptionDescriptor & {
|
||||
description: string;
|
||||
hidden?: boolean;
|
||||
options?: string[];
|
||||
argumentName?: string;
|
||||
};
|
||||
|
||||
const options = {
|
||||
"start-minimized": {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
short: "m",
|
||||
description: "Start the application minimized to the system tray"
|
||||
},
|
||||
version: {
|
||||
type: "boolean",
|
||||
short: "v",
|
||||
description: "Print the application version and exit"
|
||||
},
|
||||
help: {
|
||||
type: "boolean",
|
||||
short: "h",
|
||||
description: "Print help information and exit"
|
||||
},
|
||||
"user-agent": {
|
||||
type: "string",
|
||||
argumentName: "ua",
|
||||
description: "Set a custom User-Agent. May trigger anti-spam or break voice chat"
|
||||
},
|
||||
"user-agent-os": {
|
||||
type: "string",
|
||||
description: "Set User-Agent to a specific operating system. May trigger anti-spam or break voice chat",
|
||||
options: ["windows", "linux", "darwin"]
|
||||
}
|
||||
} satisfies Record<string, Option>;
|
||||
|
||||
// only for help display
|
||||
const extraOptions = {
|
||||
"enable-features": {
|
||||
type: "string",
|
||||
description: "Enable specific Chromium features",
|
||||
argumentName: "feature1,feature2,…"
|
||||
},
|
||||
"disable-features": {
|
||||
type: "string",
|
||||
description: "Disable specific Chromium features",
|
||||
argumentName: "feature1,feature2,…"
|
||||
},
|
||||
"ozone-platform": {
|
||||
hidden: process.platform !== "linux",
|
||||
type: "string",
|
||||
description: "Whether to run Vesktop in Wayland or X11 (XWayland)",
|
||||
options: ["x11", "wayland"]
|
||||
}
|
||||
} satisfies Record<string, Option>;
|
||||
|
||||
const args = basename(process.argv[0]).toLowerCase().startsWith("electron")
|
||||
? process.argv.slice(2)
|
||||
: process.argv.slice(1);
|
||||
|
||||
export const CommandLine = parseArgs({
|
||||
args,
|
||||
options,
|
||||
strict: false as true, // we manually check later, so cast to true to get better types
|
||||
allowPositionals: true
|
||||
});
|
||||
|
||||
export function checkCommandLineForHelpOrVersion() {
|
||||
const { help, version } = CommandLine.values;
|
||||
|
||||
if (version) {
|
||||
console.log(`Vesktop v${app.getVersion()}`);
|
||||
app.exit(0);
|
||||
}
|
||||
|
||||
if (help) {
|
||||
const base = stripIndent`
|
||||
Vesktop v${app.getVersion()}
|
||||
|
||||
Usage: ${basename(process.execPath)} [options] [url]
|
||||
|
||||
Electron Options:
|
||||
See <https://www.electronjs.org/docs/latest/api/command-line-switches#electron-cli-flags>
|
||||
|
||||
Chromium Options:
|
||||
See <https://peter.sh/experiments/chromium-command-line-switches> - only some of them work
|
||||
|
||||
Vesktop Options:
|
||||
`;
|
||||
|
||||
const optionLines = Object.entries(options)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.concat(Object.entries(extraOptions))
|
||||
.filter(([, opt]) => !("hidden" in opt && opt.hidden))
|
||||
.map(([name, opt]) => {
|
||||
const flags = [
|
||||
"short" in opt && `-${opt.short}`,
|
||||
`--${name}`,
|
||||
opt.type !== "boolean" &&
|
||||
("options" in opt ? `<${opt.options.join(" | ")}>` : `<${opt.argumentName ?? opt.type}>`)
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return [flags, opt.description];
|
||||
});
|
||||
|
||||
const padding = optionLines.reduce((max, [flags]) => Math.max(max, flags.length), 0) + 4;
|
||||
|
||||
const optionsHelp = optionLines
|
||||
.map(([flags, description]) => ` ${flags.padEnd(padding, " ")}${description}`)
|
||||
.join("\n");
|
||||
|
||||
console.log(base + "\n" + optionsHelp);
|
||||
app.exit(0);
|
||||
}
|
||||
|
||||
for (const [name, def] of Object.entries(options)) {
|
||||
const value = CommandLine.values[name];
|
||||
if (value == null) continue;
|
||||
|
||||
if (typeof value !== def.type) {
|
||||
console.error(`Invalid options. Expected ${def.type === "boolean" ? "no" : "an"} argument for --${name}`);
|
||||
app.exit(1);
|
||||
}
|
||||
|
||||
if ("options" in def && !def.options?.includes(value as string)) {
|
||||
console.error(`Invalid value for --${name}: ${value}\nExpected one of: ${def.options.join(", ")}`);
|
||||
app.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkCommandLineForHelpOrVersion();
|
||||
@@ -8,6 +8,8 @@ import { app } from "electron";
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
import { CommandLine } from "./cli";
|
||||
|
||||
const vesktopDir = dirname(process.execPath);
|
||||
|
||||
export const PORTABLE =
|
||||
@@ -20,20 +22,15 @@ export const DATA_DIR =
|
||||
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
|
||||
export const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
|
||||
app.setPath("sessionData", SESSION_DATA_DIR);
|
||||
|
||||
export const VENCORD_SETTINGS_DIR = join(DATA_DIR, "settings");
|
||||
mkdirSync(VENCORD_SETTINGS_DIR, { recursive: true });
|
||||
export const VENCORD_QUICKCSS_FILE = join(VENCORD_SETTINGS_DIR, "quickCss.css");
|
||||
export const VENCORD_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json");
|
||||
export const VENCORD_THEMES_DIR = join(DATA_DIR, "themes");
|
||||
|
||||
// needs to be inline require because of circular dependency
|
||||
// as otherwise "DATA_DIR" (which is used by ./settings) will be uninitialised
|
||||
export const VENCORD_FILES_DIR =
|
||||
(require("./settings") as typeof import("./settings")).State.store.vencordDir ||
|
||||
join(SESSION_DATA_DIR, "vencordFiles");
|
||||
|
||||
export const USER_AGENT = `Vesktop/${app.getVersion()} (https://github.com/Vencord/Vesktop)`;
|
||||
|
||||
// dimensions shamelessly stolen from Discord Desktop :3
|
||||
@@ -51,9 +48,14 @@ const BrowserUserAgents = {
|
||||
windows: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) ${VersionString}`
|
||||
};
|
||||
|
||||
export const BrowserUserAgent = BrowserUserAgents[process.platform] || BrowserUserAgents.windows;
|
||||
export const BrowserUserAgent =
|
||||
CommandLine.values["user-agent"] ||
|
||||
BrowserUserAgents[CommandLine.values["user-agent-os"] || process.platform] ||
|
||||
BrowserUserAgents.windows;
|
||||
|
||||
export const enum MessageBoxChoice {
|
||||
Default,
|
||||
Cancel
|
||||
}
|
||||
|
||||
export const IS_FLATPAK = process.env.FLATPAK_ID !== undefined;
|
||||
|
||||
40
src/main/dbus.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { join } from "path";
|
||||
import { STATIC_DIR } from "shared/paths";
|
||||
|
||||
let libVesktop: typeof import("libvesktop") | null = null;
|
||||
|
||||
function loadLibVesktop() {
|
||||
try {
|
||||
if (!libVesktop) {
|
||||
libVesktop = require(join(STATIC_DIR, `dist/libvesktop-${process.arch}.node`));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load libvesktop:", e);
|
||||
}
|
||||
|
||||
return libVesktop;
|
||||
}
|
||||
|
||||
export function getAccentColor() {
|
||||
return loadLibVesktop()?.getAccentColor() ?? null;
|
||||
}
|
||||
|
||||
export function updateUnityLauncherCount(count: number) {
|
||||
const libVesktop = loadLibVesktop();
|
||||
if (!libVesktop) {
|
||||
return app.setBadgeCount(count);
|
||||
}
|
||||
|
||||
return libVesktop.updateUnityLauncherCount(count);
|
||||
}
|
||||
|
||||
export function requestBackground(autoStart: boolean, commandLine: string[]) {
|
||||
return loadLibVesktop()?.requestBackground(autoStart, commandLine) ?? false;
|
||||
}
|
||||
15
src/main/events.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import { UserAssetType } from "./userAssets";
|
||||
|
||||
export const AppEvents = new EventEmitter<{
|
||||
appLoaded: [];
|
||||
userAssetChanged: [UserAssetType];
|
||||
setTrayVariant: ["tray" | "trayUnread"];
|
||||
}>();
|
||||
@@ -9,13 +9,13 @@ import { BrowserWindow } from "electron/main";
|
||||
import { copyFileSync, mkdirSync, readdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { SplashProps } from "shared/browserWinProperties";
|
||||
import { ICON_PATH, VIEW_DIR } from "shared/paths";
|
||||
|
||||
import { autoStart } from "./autoStart";
|
||||
import { DATA_DIR } from "./constants";
|
||||
import { createWindows } from "./mainWindow";
|
||||
import { Settings, State } from "./settings";
|
||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||
import { loadView } from "./vesktopStatic";
|
||||
|
||||
interface Data {
|
||||
discordBranch: "stable" | "canary" | "ptb";
|
||||
@@ -31,21 +31,19 @@ export function createFirstLaunchTour() {
|
||||
transparent: false,
|
||||
frame: true,
|
||||
autoHideMenuBar: true,
|
||||
height: 470,
|
||||
width: 550,
|
||||
icon: ICON_PATH
|
||||
height: 550,
|
||||
width: 600
|
||||
});
|
||||
|
||||
makeLinksOpenExternally(win);
|
||||
|
||||
win.loadFile(join(VIEW_DIR, "first-launch.html"));
|
||||
loadView(win, "first-launch.html");
|
||||
win.webContents.addListener("console-message", (_e, _l, msg) => {
|
||||
if (msg === "cancel") return app.exit();
|
||||
|
||||
if (!msg.startsWith("form:")) return;
|
||||
const data = JSON.parse(msg.slice(5)) as Data;
|
||||
|
||||
console.log(data);
|
||||
State.store.firstLaunch = false;
|
||||
Settings.store.discordBranch = data.discordBranch;
|
||||
Settings.store.minimizeToTray = !!data.minimizeToTray;
|
||||
@@ -64,7 +62,11 @@ export function createFirstLaunchTour() {
|
||||
copyFileSync(join(from, file), join(to, file));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to import settings:", e);
|
||||
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
|
||||
console.log("No Vencord settings found to import.");
|
||||
} else {
|
||||
console.error("Failed to import Vencord settings:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./cli";
|
||||
import "./updater";
|
||||
import "./ipc";
|
||||
import "./userAssets";
|
||||
import "./vesktopProtocol";
|
||||
|
||||
import { app, BrowserWindow, nativeTheme } from "electron";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
import { DATA_DIR } from "./constants";
|
||||
import { createFirstLaunchTour } from "./firstLaunch";
|
||||
@@ -15,12 +18,9 @@ import { createWindows, mainWin } from "./mainWindow";
|
||||
import { registerMediaPermissionsHandler } from "./mediaPermissions";
|
||||
import { registerScreenShareHandler } from "./screenShare";
|
||||
import { Settings, State } from "./settings";
|
||||
import { setAsDefaultProtocolClient } from "./utils/setAsDefaultProtocolClient";
|
||||
import { isDeckGameMode } from "./utils/steamOS";
|
||||
|
||||
if (!IS_DEV) {
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
}
|
||||
|
||||
console.log("Vesktop v" + app.getVersion());
|
||||
|
||||
// Make the Vencord files use our DATA_DIR
|
||||
@@ -31,7 +31,7 @@ const isLinux = process.platform === "linux";
|
||||
export let enableHardwareAcceleration = true;
|
||||
|
||||
function init() {
|
||||
app.setAsDefaultProtocolClient("discord");
|
||||
setAsDefaultProtocolClient("discord");
|
||||
|
||||
const { disableSmoothScroll, hardwareAcceleration, hardwareVideoAcceleration } = Settings.store;
|
||||
|
||||
|
||||
@@ -18,31 +18,46 @@ import {
|
||||
session,
|
||||
shell
|
||||
} from "electron";
|
||||
import { mkdirSync, readFileSync, watch } from "fs";
|
||||
import { open, readFile } from "fs/promises";
|
||||
import { readFileSync, watch } from "fs";
|
||||
import { readFile } from "fs/promises";
|
||||
import { enableHardwareAcceleration } from "main";
|
||||
import { release } from "os";
|
||||
import { join } from "path";
|
||||
import { debounce } from "shared/utils/debounce";
|
||||
|
||||
import { IpcEvents } from "../shared/IpcEvents";
|
||||
import { setBadgeCount } from "./appBadge";
|
||||
import { autoStart } from "./autoStart";
|
||||
import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
|
||||
import { mainWin } from "./mainWindow";
|
||||
import { Settings, State } from "./settings";
|
||||
import { handle, handleSync } from "./utils/ipcWrappers";
|
||||
import { PopoutWindows } from "./utils/popout";
|
||||
import { isDeckGameMode, showGamePage } from "./utils/steamOS";
|
||||
import { isValidVencordInstall } from "./utils/vencordLoader";
|
||||
import { VENCORD_FILES_DIR } from "./vencordFilesDir";
|
||||
|
||||
handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload.js"));
|
||||
handleSync(IpcEvents.DEPRECATED_GET_VENCORD_PRELOAD_SCRIPT_PATH, () =>
|
||||
join(VENCORD_FILES_DIR, "vencordDesktopPreload.js")
|
||||
);
|
||||
handleSync(IpcEvents.GET_VENCORD_PRELOAD_SCRIPT, () =>
|
||||
readFileSync(join(VENCORD_FILES_DIR, "vencordDesktopPreload.js"), "utf-8")
|
||||
);
|
||||
handleSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT, () =>
|
||||
readFileSync(join(VENCORD_FILES_DIR, "vencordDesktopRenderer.js"), "utf-8")
|
||||
);
|
||||
|
||||
handleSync(IpcEvents.GET_RENDERER_SCRIPT, () => readFileSync(join(__dirname, "renderer.js"), "utf-8"));
|
||||
handleSync(IpcEvents.GET_RENDERER_CSS_FILE, () => join(__dirname, "renderer.css"));
|
||||
const VESKTOP_RENDERER_JS_PATH = join(__dirname, "renderer.js");
|
||||
const VESKTOP_RENDERER_CSS_PATH = join(__dirname, "renderer.css");
|
||||
handleSync(IpcEvents.GET_VESKTOP_RENDERER_SCRIPT, () => readFileSync(VESKTOP_RENDERER_JS_PATH, "utf-8"));
|
||||
handle(IpcEvents.GET_VESKTOP_RENDERER_CSS, () => readFile(VESKTOP_RENDERER_CSS_PATH, "utf-8"));
|
||||
|
||||
if (IS_DEV) {
|
||||
watch(VESKTOP_RENDERER_CSS_PATH, { persistent: false }, async () => {
|
||||
mainWin?.webContents.postMessage(
|
||||
IpcEvents.VESKTOP_RENDERER_CSS_UPDATE,
|
||||
await readFile(VESKTOP_RENDERER_CSS_PATH, "utf-8")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
handleSync(IpcEvents.GET_SETTINGS, () => Settings.plain);
|
||||
handleSync(IpcEvents.GET_VERSION, () => app.getVersion());
|
||||
@@ -141,6 +156,11 @@ handle(IpcEvents.SELECT_VENCORD_DIR, async (_e, value?: null) => {
|
||||
|
||||
handle(IpcEvents.SET_BADGE_COUNT, (_, count: number) => setBadgeCount(count));
|
||||
|
||||
handle(IpcEvents.FLASH_FRAME, (_, flag: boolean) => {
|
||||
if (!mainWin || mainWin.isDestroyed() || (flag && mainWin.isFocused())) return;
|
||||
mainWin.flashFrame(flag);
|
||||
});
|
||||
|
||||
handle(IpcEvents.CLIPBOARD_COPY_IMAGE, async (_, buf: ArrayBuffer, src: string) => {
|
||||
clipboard.write({
|
||||
html: `<img src="${src.replaceAll('"', '\\"')}">`,
|
||||
@@ -158,27 +178,3 @@ function openDebugPage(page: string) {
|
||||
|
||||
handle(IpcEvents.DEBUG_LAUNCH_GPU, () => openDebugPage("chrome://gpu"));
|
||||
handle(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS, () => openDebugPage("chrome://webrtc-internals"));
|
||||
|
||||
function readCss() {
|
||||
return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => "");
|
||||
}
|
||||
|
||||
open(VENCORD_QUICKCSS_FILE, "a+").then(fd => {
|
||||
fd.close();
|
||||
watch(
|
||||
VENCORD_QUICKCSS_FILE,
|
||||
{ persistent: false },
|
||||
debounce(async () => {
|
||||
mainWin?.webContents.postMessage("VencordQuickCssUpdate", await readCss());
|
||||
}, 50)
|
||||
);
|
||||
});
|
||||
|
||||
mkdirSync(VENCORD_THEMES_DIR, { recursive: true });
|
||||
watch(
|
||||
VENCORD_THEMES_DIR,
|
||||
{ persistent: false },
|
||||
debounce(() => {
|
||||
mainWin?.webContents.postMessage("VencordThemeUpdate", void 0);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -8,45 +8,36 @@ import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
dialog,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
nativeTheme,
|
||||
Rectangle,
|
||||
screen,
|
||||
session,
|
||||
Tray
|
||||
session
|
||||
} from "electron";
|
||||
import { EventEmitter } from "events";
|
||||
import { rm } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { IpcCommands, IpcEvents } from "shared/IpcEvents";
|
||||
import { isTruthy } from "shared/utils/guards";
|
||||
import { once } from "shared/utils/once";
|
||||
import type { SettingsStore } from "shared/utils/SettingsStore";
|
||||
|
||||
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,
|
||||
VENCORD_FILES_DIR
|
||||
} from "./constants";
|
||||
import { CommandLine } from "./cli";
|
||||
import { BrowserUserAgent, DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH } from "./constants";
|
||||
import { AppEvents } from "./events";
|
||||
import { darwinURL } from "./index";
|
||||
import { sendRendererCommand } from "./ipcCommands";
|
||||
import { Settings, State, VencordSettings } from "./settings";
|
||||
import { createSplashWindow, updateSplashMessage } from "./splash";
|
||||
import { destroyTray, initTray } from "./tray";
|
||||
import { clearData } from "./utils/clearData";
|
||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||
import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS";
|
||||
import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
|
||||
import { downloadVencordFiles, ensureVencordFiles, vencordSupportsSandboxing } from "./utils/vencordLoader";
|
||||
import { VENCORD_FILES_DIR } from "./vencordFilesDir";
|
||||
|
||||
let isQuitting = false;
|
||||
let tray: Tray;
|
||||
|
||||
applyDeckKeyboardFix();
|
||||
|
||||
@@ -77,84 +68,6 @@ function makeSettingsListenerHelpers<O extends object>(o: SettingsStore<O>) {
|
||||
const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpers(Settings);
|
||||
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",
|
||||
click() {
|
||||
win.show();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "About",
|
||||
click: createAboutWindow
|
||||
},
|
||||
{
|
||||
label: "Repair Vencord",
|
||||
async click() {
|
||||
await downloadVencordFiles();
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Reset Vesktop",
|
||||
async click() {
|
||||
await clearData(win);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "separator"
|
||||
},
|
||||
{
|
||||
label: "Restart",
|
||||
click() {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Quit",
|
||||
click() {
|
||||
isQuitting = true;
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
tray = new Tray(ICON_PATH);
|
||||
tray.setToolTip("Vesktop");
|
||||
tray.setContextMenu(trayMenu);
|
||||
tray.on("click", onTrayClick);
|
||||
}
|
||||
|
||||
async function clearData(win: BrowserWindow) {
|
||||
const { response } = await dialog.showMessageBox(win, {
|
||||
message: "Are you sure you want to reset Vesktop?",
|
||||
detail: "This will log you out, clear caches and reset all your settings!\n\nVesktop will automatically restart after this operation.",
|
||||
buttons: ["Yes", "No"],
|
||||
cancelId: MessageBoxChoice.Cancel,
|
||||
defaultId: MessageBoxChoice.Default,
|
||||
type: "warning"
|
||||
});
|
||||
|
||||
if (response === MessageBoxChoice.Cancel) return;
|
||||
|
||||
win.close();
|
||||
|
||||
await win.webContents.session.clearStorageData();
|
||||
await win.webContents.session.clearCache();
|
||||
await win.webContents.session.clearCodeCaches({});
|
||||
await rm(DATA_DIR, { force: true, recursive: true });
|
||||
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
}
|
||||
|
||||
type MenuItemList = Array<MenuItemConstructorOptions | false>;
|
||||
|
||||
function initMenuBar(win: BrowserWindow) {
|
||||
@@ -263,55 +176,6 @@ function initMenuBar(win: BrowserWindow) {
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
|
||||
// We want the default window behaivour to apply in game mode since it expects everything to be fullscreen and maximized.
|
||||
if (isDeckGameMode) return {};
|
||||
|
||||
const { x, y, width, height } = State.store.windowBounds ?? {};
|
||||
|
||||
const options = {
|
||||
width: width ?? DEFAULT_WIDTH,
|
||||
height: height ?? DEFAULT_HEIGHT
|
||||
} as BrowserWindowConstructorOptions;
|
||||
|
||||
const storedDisplay = screen.getAllDisplays().find(display => display.id === State.store.displayId);
|
||||
|
||||
if (x != null && y != null && storedDisplay) {
|
||||
options.x = x;
|
||||
options.y = y;
|
||||
}
|
||||
|
||||
if (!Settings.store.disableMinSize) {
|
||||
options.minWidth = MIN_WIDTH;
|
||||
options.minHeight = MIN_HEIGHT;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function getDarwinOptions(): BrowserWindowConstructorOptions {
|
||||
const options = {
|
||||
titleBarStyle: "hidden",
|
||||
trafficLightPosition: { x: 10, y: 10 }
|
||||
} as BrowserWindowConstructorOptions;
|
||||
|
||||
const { splashTheming, splashBackground } = Settings.store;
|
||||
const { macosTranslucency } = VencordSettings.store;
|
||||
|
||||
if (macosTranslucency) {
|
||||
options.vibrancy = "sidebar";
|
||||
options.backgroundColor = "#ffffff00";
|
||||
} else {
|
||||
if (splashTheming !== false) {
|
||||
options.backgroundColor = splashBackground;
|
||||
} else {
|
||||
options.backgroundColor = nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function initWindowBoundsListeners(win: BrowserWindow) {
|
||||
const saveState = () => {
|
||||
State.store.maximized = win.isMaximized();
|
||||
@@ -324,7 +188,6 @@ function initWindowBoundsListeners(win: BrowserWindow) {
|
||||
|
||||
const saveBounds = () => {
|
||||
State.store.windowBounds = win.getBounds();
|
||||
State.store.displayId = screen.getDisplayMatching(State.store.windowBounds).id;
|
||||
};
|
||||
|
||||
win.on("resize", saveBounds);
|
||||
@@ -333,8 +196,8 @@ function initWindowBoundsListeners(win: BrowserWindow) {
|
||||
|
||||
function initSettingsListeners(win: BrowserWindow) {
|
||||
addSettingsListener("tray", enable => {
|
||||
if (enable) initTray(win);
|
||||
else tray?.destroy();
|
||||
if (enable) initTray(win, q => (isQuitting = q));
|
||||
else destroyTray();
|
||||
});
|
||||
|
||||
addSettingsListener("disableMinSize", disable => {
|
||||
@@ -412,26 +275,55 @@ function initStaticTitle(win: BrowserWindow) {
|
||||
});
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
// Clear up previous settings listeners
|
||||
removeSettingsListeners();
|
||||
removeVencordSettingsListeners();
|
||||
function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
|
||||
// We want the default window behaviour to apply in game mode since it expects everything to be fullscreen and maximized.
|
||||
if (isDeckGameMode) return {};
|
||||
|
||||
const { x, y, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = State.store.windowBounds ?? {};
|
||||
|
||||
const options = { width, height } as BrowserWindowConstructorOptions;
|
||||
|
||||
if (x != null && y != null) {
|
||||
function isInBounds(rect: Rectangle, display: Rectangle) {
|
||||
return !(
|
||||
rect.x + rect.width < display.x ||
|
||||
rect.x > display.x + display.width ||
|
||||
rect.y + rect.height < display.y ||
|
||||
rect.y > display.y + display.height
|
||||
);
|
||||
}
|
||||
|
||||
const inBounds = screen.getAllDisplays().some(d => isInBounds({ x, y, width, height }, d.bounds));
|
||||
if (inBounds) {
|
||||
options.x = x;
|
||||
options.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Settings.store.disableMinSize) {
|
||||
options.minWidth = MIN_WIDTH;
|
||||
options.minHeight = MIN_HEIGHT;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function buildBrowserWindowOptions(): BrowserWindowConstructorOptions {
|
||||
const { staticTitle, transparencyOption, enableMenu, customTitleBar, splashTheming, splashBackground } =
|
||||
Settings.store;
|
||||
|
||||
const { frameless, transparent } = VencordSettings.store;
|
||||
const { frameless, transparent, macosVibrancyStyle } = VencordSettings.store;
|
||||
|
||||
const noFrame = frameless === true || customTitleBar === true;
|
||||
const backgroundColor =
|
||||
splashTheming !== false ? splashBackground : nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
|
||||
|
||||
const win = (mainWin = new BrowserWindow({
|
||||
show: Settings.store.enableSplashScreen === false,
|
||||
const options: BrowserWindowConstructorOptions = {
|
||||
show: Settings.store.enableSplashScreen === false && !CommandLine.values["start-minimized"],
|
||||
backgroundColor,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
sandbox: vencordSupportsSandboxing(),
|
||||
contextIsolation: true,
|
||||
devTools: true,
|
||||
preload: join(__dirname, "preload.js"),
|
||||
@@ -439,30 +331,51 @@ function createMainWindow() {
|
||||
// disable renderer backgrounding to prevent the app from unloading when in the background
|
||||
backgroundThrottling: false
|
||||
},
|
||||
icon: ICON_PATH,
|
||||
frame: !noFrame,
|
||||
...(transparent && {
|
||||
transparent: true,
|
||||
backgroundColor: "#00000000"
|
||||
}),
|
||||
...(transparencyOption &&
|
||||
transparencyOption !== "none" && {
|
||||
backgroundColor: "#00000000",
|
||||
backgroundMaterial: transparencyOption
|
||||
}),
|
||||
// Fix transparencyOption for custom discord titlebar
|
||||
...(customTitleBar &&
|
||||
transparencyOption &&
|
||||
transparencyOption !== "none" && {
|
||||
transparent: true
|
||||
}),
|
||||
...(staticTitle && { title: "Vesktop" }),
|
||||
...(process.platform === "darwin" && getDarwinOptions()),
|
||||
...getWindowBoundsOptions(),
|
||||
autoHideMenuBar: enableMenu
|
||||
}));
|
||||
autoHideMenuBar: enableMenu,
|
||||
...getWindowBoundsOptions()
|
||||
};
|
||||
|
||||
if (transparent) {
|
||||
options.transparent = true;
|
||||
options.backgroundColor = "#00000000";
|
||||
}
|
||||
|
||||
if (transparencyOption && transparencyOption !== "none") {
|
||||
options.backgroundColor = "#00000000";
|
||||
options.backgroundMaterial = transparencyOption;
|
||||
|
||||
if (customTitleBar) {
|
||||
options.transparent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (staticTitle) {
|
||||
options.title = "Vesktop";
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
options.titleBarStyle = "hidden";
|
||||
options.trafficLightPosition = { x: 10, y: 10 };
|
||||
|
||||
if (macosVibrancyStyle) {
|
||||
options.vibrancy = macosVibrancyStyle;
|
||||
options.backgroundColor = "#00000000";
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
// Clear up previous settings listeners
|
||||
removeSettingsListeners();
|
||||
removeVencordSettingsListeners();
|
||||
|
||||
const win = (mainWin = new BrowserWindow(buildBrowserWindowOptions()));
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
if (process.platform === "darwin" && customTitleBar) win.setWindowButtonVisibility(false);
|
||||
if (process.platform === "darwin" && Settings.store.customTitleBar) win.setWindowButtonVisibility(false);
|
||||
|
||||
win.on("close", e => {
|
||||
const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false;
|
||||
@@ -476,8 +389,14 @@ function createMainWindow() {
|
||||
return false;
|
||||
});
|
||||
|
||||
win.on("focus", () => {
|
||||
win.flashFrame(false);
|
||||
});
|
||||
|
||||
initWindowBoundsListeners(win);
|
||||
if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win);
|
||||
if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin")
|
||||
initTray(win, q => (isQuitting = q));
|
||||
|
||||
initMenuBar(win);
|
||||
makeLinksOpenExternally(win);
|
||||
initSettingsListeners(win);
|
||||
@@ -497,8 +416,6 @@ function createMainWindow() {
|
||||
|
||||
const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js")));
|
||||
|
||||
const loadEvents = new EventEmitter();
|
||||
|
||||
export function loadUrl(uri: string | undefined) {
|
||||
const branch = Settings.store.discordBranch;
|
||||
const subdomain = branch === "canary" || branch === "ptb" ? `${branch}.` : "";
|
||||
@@ -506,7 +423,7 @@ export function loadUrl(uri: string | undefined) {
|
||||
// we do not rely on 'did-finish-load' because it fires even if loadURL fails which triggers early detruction of the splash
|
||||
mainWin
|
||||
.loadURL(`https://${subdomain}discord.com/${uri ? new URL(uri).pathname.slice(1) || "app" : "app"}`)
|
||||
.then(() => loadEvents.emit("app-loaded"))
|
||||
.then(() => AppEvents.emit("appLoaded"))
|
||||
.catch(error => retryUrl(error.url, error.code));
|
||||
}
|
||||
|
||||
@@ -518,7 +435,7 @@ function retryUrl(url: string, description: string) {
|
||||
}
|
||||
|
||||
export async function createWindows() {
|
||||
const startMinimized = process.argv.includes("--start-minimized");
|
||||
const startMinimized = CommandLine.values["start-minimized"];
|
||||
|
||||
let splash: BrowserWindow | undefined;
|
||||
if (Settings.store.enableSplashScreen !== false) {
|
||||
@@ -533,7 +450,7 @@ export async function createWindows() {
|
||||
|
||||
mainWin = createMainWindow();
|
||||
|
||||
loadEvents.on("app-loaded", () => {
|
||||
AppEvents.on("appLoaded", () => {
|
||||
splash?.destroy();
|
||||
|
||||
if (!startMinimized) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { type Settings as TVencordSettings } from "@vencord/types/Vencord";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import type { Settings as TSettings, State as TState } from "shared/settings";
|
||||
@@ -27,13 +28,17 @@ function loadSettings<T extends object = any>(file: string, name: string) {
|
||||
|
||||
const store = new SettingsStore(settings);
|
||||
store.addGlobalChangeListener(o => {
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
writeFileSync(file, JSON.stringify(o, null, 4));
|
||||
try {
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
writeFileSync(file, JSON.stringify(o, null, 4));
|
||||
} catch (err) {
|
||||
console.error(`Failed to save settings to ${name}.json:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop settings");
|
||||
export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord settings");
|
||||
export const VencordSettings = loadSettings<TVencordSettings>(VENCORD_SETTINGS_FILE, "Vencord settings");
|
||||
export const State = loadSettings<TState>(STATE_FILE, "Vesktop state");
|
||||
|
||||
@@ -7,25 +7,24 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { join } from "path";
|
||||
import { SplashProps } from "shared/browserWinProperties";
|
||||
import { ICON_PATH, VIEW_DIR } from "shared/paths";
|
||||
|
||||
import { Settings } from "./settings";
|
||||
import { loadView } from "./vesktopStatic";
|
||||
|
||||
let splash: BrowserWindow | undefined;
|
||||
|
||||
export function createSplashWindow(startMinimized = false) {
|
||||
splash = new BrowserWindow({
|
||||
...SplashProps,
|
||||
icon: ICON_PATH,
|
||||
show: !startMinimized,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "splashPreload.js")
|
||||
}
|
||||
});
|
||||
|
||||
splash.loadFile(join(VIEW_DIR, "splash.html"));
|
||||
loadView(splash, "splash.html");
|
||||
|
||||
const { splashBackground, splashColor, splashTheming } = Settings.store;
|
||||
const { splashBackground, splashColor, splashTheming, splashPixelated } = Settings.store;
|
||||
|
||||
if (splashTheming !== false) {
|
||||
if (splashColor) {
|
||||
@@ -40,6 +39,10 @@ export function createSplashWindow(startMinimized = false) {
|
||||
}
|
||||
}
|
||||
|
||||
if (splashPixelated) {
|
||||
splash.webContents.insertCSS(`img { image-rendering: pixelated; }`);
|
||||
}
|
||||
|
||||
return splash;
|
||||
}
|
||||
|
||||
|
||||
92
src/main/tray.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, Menu, Tray } from "electron";
|
||||
|
||||
import { createAboutWindow } from "./about";
|
||||
import { AppEvents } from "./events";
|
||||
import { Settings } from "./settings";
|
||||
import { resolveAssetPath } from "./userAssets";
|
||||
import { clearData } from "./utils/clearData";
|
||||
import { downloadVencordFiles } from "./utils/vencordLoader";
|
||||
|
||||
let tray: Tray;
|
||||
let trayVariant: "tray" | "trayUnread" = "tray";
|
||||
|
||||
AppEvents.on("userAssetChanged", async asset => {
|
||||
if (tray && (asset === "tray" || asset === "trayUnread")) {
|
||||
tray.setImage(await resolveAssetPath(trayVariant));
|
||||
}
|
||||
});
|
||||
|
||||
AppEvents.on("setTrayVariant", async variant => {
|
||||
if (trayVariant === variant) return;
|
||||
|
||||
trayVariant = variant;
|
||||
if (!tray) return;
|
||||
|
||||
tray.setImage(await resolveAssetPath(trayVariant));
|
||||
});
|
||||
|
||||
export function destroyTray() {
|
||||
tray?.destroy();
|
||||
}
|
||||
|
||||
export async function initTray(win: BrowserWindow, setIsQuitting: (val: boolean) => void) {
|
||||
const onTrayClick = () => {
|
||||
if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide();
|
||||
else win.show();
|
||||
};
|
||||
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Open",
|
||||
click() {
|
||||
win.show();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "About",
|
||||
click: createAboutWindow
|
||||
},
|
||||
{
|
||||
label: "Repair Vencord",
|
||||
async click() {
|
||||
await downloadVencordFiles();
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Reset Vesktop",
|
||||
async click() {
|
||||
await clearData(win);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "separator"
|
||||
},
|
||||
{
|
||||
label: "Restart",
|
||||
click() {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Quit",
|
||||
click() {
|
||||
setIsQuitting(true);
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
tray = new Tray(await resolveAssetPath(trayVariant));
|
||||
tray.setToolTip("Vesktop");
|
||||
tray.setContextMenu(trayMenu);
|
||||
tray.on("click", onTrayClick);
|
||||
}
|
||||
81
src/main/updater.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, ipcMain } from "electron";
|
||||
import { autoUpdater, UpdateInfo } from "electron-updater";
|
||||
import { join } from "path";
|
||||
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
|
||||
import { Millis } from "shared/utils/millis";
|
||||
|
||||
import { State } from "./settings";
|
||||
import { handle } from "./utils/ipcWrappers";
|
||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||
import { loadView } from "./vesktopStatic";
|
||||
|
||||
let updaterWindow: BrowserWindow | null = null;
|
||||
|
||||
autoUpdater.on("update-available", update => {
|
||||
if (State.store.updater?.ignoredVersion === update.version) return;
|
||||
if ((State.store.updater?.snoozeUntil ?? 0) > Date.now()) return;
|
||||
|
||||
openUpdater(update);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", () => setTimeout(() => autoUpdater.quitAndInstall(), 100));
|
||||
autoUpdater.on("download-progress", p =>
|
||||
updaterWindow?.webContents.send(UpdaterIpcEvents.DOWNLOAD_PROGRESS, p.percent)
|
||||
);
|
||||
autoUpdater.on("error", err => updaterWindow?.webContents.send(UpdaterIpcEvents.ERROR, err.message));
|
||||
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
autoUpdater.fullChangelog = true;
|
||||
|
||||
const isOutdated = autoUpdater.checkForUpdates().then(res => Boolean(res?.isUpdateAvailable));
|
||||
|
||||
handle(IpcEvents.UPDATER_IS_OUTDATED, () => isOutdated);
|
||||
handle(IpcEvents.UPDATER_OPEN, async () => {
|
||||
const res = await autoUpdater.checkForUpdates();
|
||||
if (res?.isUpdateAvailable && res.updateInfo) openUpdater(res.updateInfo);
|
||||
});
|
||||
|
||||
function openUpdater(update: UpdateInfo) {
|
||||
updaterWindow = new BrowserWindow({
|
||||
title: "Vesktop Updater",
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "updaterPreload.js")
|
||||
},
|
||||
minHeight: 400,
|
||||
minWidth: 750
|
||||
});
|
||||
makeLinksOpenExternally(updaterWindow);
|
||||
|
||||
handle(UpdaterIpcEvents.GET_DATA, () => ({ update, version: app.getVersion() }));
|
||||
handle(UpdaterIpcEvents.INSTALL, async () => {
|
||||
await autoUpdater.downloadUpdate();
|
||||
});
|
||||
handle(UpdaterIpcEvents.SNOOZE_UPDATE, () => {
|
||||
State.store.updater ??= {};
|
||||
State.store.updater.snoozeUntil = Date.now() + 1 * Millis.DAY;
|
||||
updaterWindow?.close();
|
||||
});
|
||||
handle(UpdaterIpcEvents.IGNORE_UPDATE, () => {
|
||||
State.store.updater ??= {};
|
||||
State.store.updater.ignoredVersion = update.version;
|
||||
updaterWindow?.close();
|
||||
});
|
||||
|
||||
updaterWindow.on("closed", () => {
|
||||
ipcMain.removeHandler(UpdaterIpcEvents.GET_DATA);
|
||||
ipcMain.removeHandler(UpdaterIpcEvents.INSTALL);
|
||||
ipcMain.removeHandler(UpdaterIpcEvents.SNOOZE_UPDATE);
|
||||
ipcMain.removeHandler(UpdaterIpcEvents.IGNORE_UPDATE);
|
||||
updaterWindow = null;
|
||||
});
|
||||
|
||||
loadView(updaterWindow, "updater/index.html");
|
||||
}
|
||||
101
src/main/userAssets.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app, dialog, net } from "electron";
|
||||
import { copyFile, mkdir, rm } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
import { STATIC_DIR } from "shared/paths";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
import { DATA_DIR } from "./constants";
|
||||
import { AppEvents } from "./events";
|
||||
import { mainWin } from "./mainWindow";
|
||||
import { fileExistsAsync } from "./utils/fileExists";
|
||||
import { handle } from "./utils/ipcWrappers";
|
||||
|
||||
const CUSTOMIZABLE_ASSETS = ["splash", "tray", "trayUnread"] as const;
|
||||
export type UserAssetType = (typeof CUSTOMIZABLE_ASSETS)[number];
|
||||
|
||||
const DEFAULT_ASSETS: Record<UserAssetType, string> = {
|
||||
splash: "splash.webp",
|
||||
tray: `tray/${process.platform === "darwin" ? "trayTemplate" : "tray"}.png`,
|
||||
trayUnread: "tray/trayUnread.png"
|
||||
};
|
||||
|
||||
const UserAssetFolder = join(DATA_DIR, "userAssets");
|
||||
|
||||
export async function resolveAssetPath(asset: UserAssetType) {
|
||||
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
|
||||
throw new Error(`Invalid asset: ${asset}`);
|
||||
}
|
||||
|
||||
const assetPath = join(UserAssetFolder, asset);
|
||||
if (await fileExistsAsync(assetPath)) {
|
||||
return assetPath;
|
||||
}
|
||||
|
||||
return join(STATIC_DIR, DEFAULT_ASSETS[asset]);
|
||||
}
|
||||
|
||||
export async function handleVesktopAssetsProtocol(path: string, req: Request) {
|
||||
const asset = path.slice(1);
|
||||
|
||||
// @ts-expect-error dumb types
|
||||
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href);
|
||||
if (res.ok) return res;
|
||||
} catch {}
|
||||
|
||||
return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href);
|
||||
}
|
||||
|
||||
handle(IpcEvents.CHOOSE_USER_ASSET, async (_event, asset: UserAssetType, value?: null) => {
|
||||
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
|
||||
throw `Invalid asset: ${asset}`;
|
||||
}
|
||||
|
||||
const assetPath = join(UserAssetFolder, asset);
|
||||
|
||||
if (value === null) {
|
||||
try {
|
||||
await rm(assetPath, { force: true });
|
||||
AppEvents.emit("userAssetChanged", asset);
|
||||
return "ok";
|
||||
} catch (e) {
|
||||
console.error(`Failed to remove user asset ${asset}:`, e);
|
||||
return "failed";
|
||||
}
|
||||
}
|
||||
|
||||
const res = await dialog.showOpenDialog(mainWin, {
|
||||
properties: ["openFile"],
|
||||
title: `Select an image to use as ${asset}`,
|
||||
defaultPath: app.getPath("pictures"),
|
||||
filters: [
|
||||
{
|
||||
name: "Images",
|
||||
extensions: ["png", "jpg", "jpeg", "webp", "gif", "avif", "svg"]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (res.canceled || !res.filePaths.length) return "cancelled";
|
||||
|
||||
try {
|
||||
await mkdir(UserAssetFolder, { recursive: true });
|
||||
await copyFile(res.filePaths[0], assetPath);
|
||||
AppEvents.emit("userAssetChanged", asset);
|
||||
return "ok";
|
||||
} catch (e) {
|
||||
console.error(`Failed to copy user asset ${asset}:`, e);
|
||||
return "failed";
|
||||
}
|
||||
});
|
||||
32
src/main/utils/clearData.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, dialog } from "electron";
|
||||
import { rm } from "fs/promises";
|
||||
import { DATA_DIR, MessageBoxChoice } from "main/constants";
|
||||
|
||||
export async function clearData(win: BrowserWindow) {
|
||||
const { response } = await dialog.showMessageBox(win, {
|
||||
message: "Are you sure you want to reset Vesktop?",
|
||||
detail: "This will log you out, clear caches and reset all your settings!\n\nVesktop will automatically restart after this operation.",
|
||||
buttons: ["Yes", "No"],
|
||||
cancelId: MessageBoxChoice.Cancel,
|
||||
defaultId: MessageBoxChoice.Default,
|
||||
type: "warning"
|
||||
});
|
||||
|
||||
if (response === MessageBoxChoice.Cancel) return;
|
||||
|
||||
win.close();
|
||||
|
||||
await win.webContents.session.clearStorageData();
|
||||
await win.webContents.session.clearCache();
|
||||
await win.webContents.session.clearCodeCaches({});
|
||||
await rm(DATA_DIR, { force: true, recursive: true });
|
||||
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
}
|
||||
56
src/main/utils/desktopFileEscape.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html
|
||||
|
||||
// "If an argument contains a reserved character the argument must be quoted."
|
||||
const desktopFileReservedChars = new Set([
|
||||
" ",
|
||||
"\t",
|
||||
"\n",
|
||||
'"',
|
||||
"'",
|
||||
"\\",
|
||||
">",
|
||||
"<",
|
||||
"~",
|
||||
"|",
|
||||
"&",
|
||||
";",
|
||||
"$",
|
||||
"*",
|
||||
"?",
|
||||
"#",
|
||||
"(",
|
||||
")",
|
||||
"`"
|
||||
]);
|
||||
|
||||
export function escapeDesktopFileArgument(arg: string) {
|
||||
let needsQuoting = false;
|
||||
let out = "";
|
||||
|
||||
for (const c of arg) {
|
||||
if (desktopFileReservedChars.has(c)) {
|
||||
// "Quoting must be done by enclosing the argument between double quotes"
|
||||
needsQuoting = true;
|
||||
// "and escaping the double quote character, backtick character ("`"), dollar sign ("$")
|
||||
// and backslash character ("\") by preceding it with an additional backslash character"
|
||||
if (c === '"' || c === "`" || c === "$" || c === "\\") {
|
||||
out += "\\";
|
||||
}
|
||||
}
|
||||
|
||||
// "Literal percentage characters must be escaped as %%"
|
||||
if (c === "%") {
|
||||
out += "%%";
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
|
||||
return needsQuoting ? `"${out}"` : out;
|
||||
}
|
||||
13
src/main/utils/fileExists.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { access, constants } from "fs/promises";
|
||||
|
||||
export async function fileExistsAsync(path: string) {
|
||||
return await access(path, constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron";
|
||||
import { DISCORD_HOSTNAMES } from "main/constants";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
|
||||
|
||||
export function validateSender(frame: WebFrameMain | null, event: string) {
|
||||
if (!frame) throw new Error(`ipc[${event}]: No sender frame`);
|
||||
@@ -18,21 +18,21 @@ export function validateSender(frame: WebFrameMain | null, event: string) {
|
||||
throw new Error(`ipc[${event}]: Invalid URL ${frame.url}`);
|
||||
}
|
||||
|
||||
if (protocol === "file:") return;
|
||||
if (protocol === "file:" || protocol === "vesktop:") return;
|
||||
|
||||
if (!DISCORD_HOSTNAMES.includes(hostname)) {
|
||||
throw new Error(`ipc[${event}]: Disallowed hostname ${hostname}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) {
|
||||
export function handleSync(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) {
|
||||
ipcMain.on(event, (e, ...args) => {
|
||||
validateSender(e.senderFrame, event);
|
||||
e.returnValue = cb(e, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
export function handle(event: IpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) {
|
||||
export function handle(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) {
|
||||
ipcMain.handle(event, (e, ...args) => {
|
||||
validateSender(e.senderFrame, event);
|
||||
return cb(e, ...args);
|
||||
|
||||
16
src/main/utils/isPathInDirectory.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { resolve, sep } from "path";
|
||||
|
||||
export function isPathInDirectory(filePath: string, directory: string) {
|
||||
const resolvedPath = resolve(filePath);
|
||||
const resolvedDirectory = resolve(directory);
|
||||
|
||||
const normalizedDirectory = resolvedDirectory.endsWith(sep) ? resolvedDirectory : resolvedDirectory + sep;
|
||||
|
||||
return resolvedPath.startsWith(normalizedDirectory) || resolvedPath === resolvedDirectory;
|
||||
}
|
||||
29
src/main/utils/setAsDefaultProtocolClient.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { execFile } from "child_process";
|
||||
import { app } from "electron";
|
||||
|
||||
export async function setAsDefaultProtocolClient(protocol: string) {
|
||||
if (process.platform !== "linux") {
|
||||
return app.setAsDefaultProtocolClient(protocol);
|
||||
}
|
||||
|
||||
// electron setAsDefaultProtocolClient uses xdg-settings instead of xdg-mime.
|
||||
// xdg-settings had a bug where it would also register the app as a handler for text/html,
|
||||
// aka become your browser. This bug was fixed years ago (xdg-utils 1.2.0) but Ubuntu ships
|
||||
// 7 (YES, SEVEN) years out of date xdg-utils which STILL has the bug.
|
||||
// FIXME: remove this workaround when Ubuntu updates their xdg-utils or electron switches to xdg-mime.
|
||||
|
||||
const { CHROME_DESKTOP } = process.env;
|
||||
if (!CHROME_DESKTOP) return false;
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
execFile("xdg-mime", ["default", CHROME_DESKTOP, `x-scheme-handler/${protocol}`], err => {
|
||||
resolve(err == null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4,11 +4,12 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { mkdirSync } from "fs";
|
||||
import { mkdirSync, readFileSync } from "fs";
|
||||
import { access, constants as FsConstants, writeFile } from "fs/promises";
|
||||
import { VENCORD_FILES_DIR } from "main/vencordFilesDir";
|
||||
import { join } from "path";
|
||||
|
||||
import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
|
||||
import { USER_AGENT } from "../constants";
|
||||
import { downloadFile, fetchie } from "./http";
|
||||
|
||||
const API_BASE = "https://api.github.com";
|
||||
@@ -74,3 +75,16 @@ export async function ensureVencordFiles() {
|
||||
|
||||
await Promise.all([downloadVencordFiles(), writeFile(join(VENCORD_FILES_DIR, "package.json"), "{}")]);
|
||||
}
|
||||
|
||||
// TODO: remove this once enough time has passed
|
||||
export function vencordSupportsSandboxing() {
|
||||
const supports = readFileSync(join(VENCORD_FILES_DIR, "vencordDesktopMain.js"), "utf-8").includes(
|
||||
"VencordGetRendererCss"
|
||||
);
|
||||
if (!supports) {
|
||||
console.warn(
|
||||
"⚠️ [VencordLoader] Vencord version is outdated and does not support sandboxing. Please update Vencord to the latest version."
|
||||
);
|
||||
}
|
||||
return supports;
|
||||
}
|
||||
|
||||
13
src/main/vencordFilesDir.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { join } from "path";
|
||||
|
||||
import { SESSION_DATA_DIR } from "./constants";
|
||||
import { State } from "./settings";
|
||||
|
||||
// this is in a separate file to avoid circular dependencies
|
||||
export const VENCORD_FILES_DIR = State.store.vencordDir || join(SESSION_DATA_DIR, "vencordFiles");
|
||||
25
src/main/vesktopProtocol.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app, protocol } from "electron";
|
||||
|
||||
import { handleVesktopAssetsProtocol } from "./userAssets";
|
||||
import { handleVesktopStaticProtocol } from "./vesktopStatic";
|
||||
|
||||
app.whenReady().then(() => {
|
||||
protocol.handle("vesktop", async req => {
|
||||
const url = new URL(req.url);
|
||||
|
||||
switch (url.hostname) {
|
||||
case "assets":
|
||||
return handleVesktopAssetsProtocol(url.pathname, req);
|
||||
case "static":
|
||||
return handleVesktopStaticProtocol(url.pathname, req);
|
||||
default:
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
});
|
||||
});
|
||||
31
src/main/vesktopStatic.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { BrowserWindow, net } from "electron";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
import { isPathInDirectory } from "./utils/isPathInDirectory";
|
||||
|
||||
const STATIC_DIR = join(__dirname, "..", "..", "static");
|
||||
|
||||
export async function handleVesktopStaticProtocol(path: string, req: Request) {
|
||||
const fullPath = join(STATIC_DIR, path);
|
||||
if (!isPathInDirectory(fullPath, STATIC_DIR)) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
return net.fetch(pathToFileURL(fullPath).href);
|
||||
}
|
||||
|
||||
export function loadView(browserWindow: BrowserWindow, view: string, params?: URLSearchParams) {
|
||||
const url = new URL(`vesktop://static/views/${view}`);
|
||||
if (params) {
|
||||
url.search = params.toString();
|
||||
}
|
||||
|
||||
return browserWindow.loadURL(url.toString());
|
||||
}
|
||||
@@ -4,9 +4,9 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Node } from "@vencord/venmic";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { IpcMessage, IpcResponse } from "main/ipcCommands";
|
||||
import type { Node } from "@vencord/venmic";
|
||||
import { ipcRenderer } from "electron/renderer";
|
||||
import type { IpcMessage, IpcResponse } from "main/ipcCommands";
|
||||
import type { Settings } from "shared/settings";
|
||||
|
||||
import { IpcEvents } from "../shared/IpcEvents";
|
||||
@@ -32,7 +32,16 @@ export const VesktopNative = {
|
||||
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION),
|
||||
setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
|
||||
supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY),
|
||||
getEnableHardwareAcceleration: () => sendSync<boolean>(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION)
|
||||
getEnableHardwareAcceleration: () => sendSync<boolean>(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION),
|
||||
isOutdated: () => invoke<boolean>(IpcEvents.UPDATER_IS_OUTDATED),
|
||||
openUpdater: () => invoke<void>(IpcEvents.UPDATER_OPEN),
|
||||
// used by vencord
|
||||
getRendererCss: () => invoke<string>(IpcEvents.GET_VESKTOP_RENDERER_CSS),
|
||||
onRendererCssUpdate: (cb: (newCss: string) => void) => {
|
||||
if (!IS_DEV) return;
|
||||
|
||||
ipcRenderer.on(IpcEvents.VESKTOP_RENDERER_CSS_UPDATE, (_e, newCss: string) => cb(newCss));
|
||||
}
|
||||
},
|
||||
autostart: {
|
||||
isEnabled: () => sendSync<boolean>(IpcEvents.AUTOSTART_ENABLED),
|
||||
@@ -42,7 +51,9 @@ export const VesktopNative = {
|
||||
fileManager: {
|
||||
showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path),
|
||||
getVencordDir: () => sendSync<string | undefined>(IpcEvents.GET_VENCORD_DIR),
|
||||
selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value)
|
||||
selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value),
|
||||
chooseUserAsset: (asset: string, value?: null) =>
|
||||
invoke<"cancelled" | "invalid" | "ok" | "failed">(IpcEvents.CHOOSE_USER_ASSET, asset, value)
|
||||
},
|
||||
settings: {
|
||||
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
||||
@@ -64,6 +75,7 @@ export const VesktopNative = {
|
||||
close: (key?: string) => invoke<void>(IpcEvents.CLOSE, key),
|
||||
minimize: (key?: string) => invoke<void>(IpcEvents.MINIMIZE, key),
|
||||
maximize: (key?: string) => invoke<void>(IpcEvents.MAXIMIZE, key),
|
||||
flashFrame: (flag: boolean) => invoke<void>(IpcEvents.FLASH_FRAME, flag),
|
||||
setDevtoolsCallbacks: (onOpen: () => void, onClose: () => void) => {
|
||||
onDevtoolsOpen = onOpen;
|
||||
onDevtoolsClose = onClose;
|
||||
|
||||
@@ -4,39 +4,29 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer, webFrame } from "electron";
|
||||
import { readFileSync, watch } from "fs";
|
||||
import { contextBridge, ipcRenderer, webFrame } from "electron/renderer";
|
||||
|
||||
import { IpcEvents } from "../shared/IpcEvents";
|
||||
import { VesktopNative } from "./VesktopNative";
|
||||
|
||||
contextBridge.exposeInMainWorld("VesktopNative", VesktopNative);
|
||||
|
||||
require(ipcRenderer.sendSync(IpcEvents.GET_VENCORD_PRELOAD_FILE));
|
||||
// TODO: remove this legacy workaround once some time has passed
|
||||
const isSandboxed = typeof __dirname === "undefined";
|
||||
if (isSandboxed) {
|
||||
// While sandboxed, Electron "polyfills" these APIs as local variables.
|
||||
// We have to pass them as arguments as they are not global
|
||||
Function(
|
||||
"require",
|
||||
"Buffer",
|
||||
"process",
|
||||
"clearImmediate",
|
||||
"setImmediate",
|
||||
ipcRenderer.sendSync(IpcEvents.GET_VENCORD_PRELOAD_SCRIPT)
|
||||
)(require, Buffer, process, clearImmediate, setImmediate);
|
||||
} else {
|
||||
require(ipcRenderer.sendSync(IpcEvents.DEPRECATED_GET_VENCORD_PRELOAD_SCRIPT_PATH));
|
||||
}
|
||||
|
||||
webFrame.executeJavaScript(ipcRenderer.sendSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT));
|
||||
webFrame.executeJavaScript(ipcRenderer.sendSync(IpcEvents.GET_RENDERER_SCRIPT));
|
||||
|
||||
// #region css
|
||||
const rendererCss = ipcRenderer.sendSync(IpcEvents.GET_RENDERER_CSS_FILE);
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "vcd-css-core";
|
||||
style.textContent = readFileSync(rendererCss, "utf-8");
|
||||
|
||||
if (document.readyState === "complete") {
|
||||
document.documentElement.appendChild(style);
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(style), {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_DEV) {
|
||||
// persistent means keep process running if watcher is the only thing still running
|
||||
// which we obviously don't want
|
||||
watch(rendererCss, { persistent: false }, () => {
|
||||
document.getElementById("vcd-css-core")!.textContent = readFileSync(rendererCss, "utf-8");
|
||||
});
|
||||
}
|
||||
// #endregion
|
||||
webFrame.executeJavaScript(ipcRenderer.sendSync(IpcEvents.GET_VESKTOP_RENDERER_SCRIPT));
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import { contextBridge, ipcRenderer } from "electron/renderer";
|
||||
|
||||
contextBridge.exposeInMainWorld("VesktopSplashNative", {
|
||||
onUpdateMessage(callback: (message: string) => void) {
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { ipcRenderer } from "electron";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
import { ipcRenderer } from "electron/renderer";
|
||||
import type { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
|
||||
|
||||
export function invoke<T = any>(event: IpcEvents, ...args: any[]) {
|
||||
export function invoke<T = any>(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) {
|
||||
return ipcRenderer.invoke(event, ...args) as Promise<T>;
|
||||
}
|
||||
|
||||
export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
|
||||
export function sendSync<T = any>(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) {
|
||||
return ipcRenderer.sendSync(event, ...args) as T;
|
||||
}
|
||||
|
||||
24
src/preload/updater.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer } from "electron/renderer";
|
||||
import type { UpdateInfo } from "electron-updater";
|
||||
import { UpdaterIpcEvents } from "shared/IpcEvents";
|
||||
|
||||
import { invoke } from "./typedIpc";
|
||||
|
||||
contextBridge.exposeInMainWorld("VesktopUpdaterNative", {
|
||||
getData: () => invoke<UpdateInfo>(UpdaterIpcEvents.GET_DATA),
|
||||
installUpdate: () => invoke(UpdaterIpcEvents.INSTALL),
|
||||
onProgress: (cb: (percent: number) => void) => {
|
||||
ipcRenderer.on(UpdaterIpcEvents.DOWNLOAD_PROGRESS, (_, percent: number) => cb(percent));
|
||||
},
|
||||
onError: (cb: (message: string) => void) => {
|
||||
ipcRenderer.on(UpdaterIpcEvents.ERROR, (_, message: string) => cb(message));
|
||||
},
|
||||
snoozeUpdate: () => invoke(UpdaterIpcEvents.SNOOZE_UPDATE),
|
||||
ignoreUpdate: () => invoke(UpdaterIpcEvents.IGNORE_UPDATE)
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type arRpcPlugin from "@vencord/types/plugins/arRPC.web";
|
||||
import { Logger } from "@vencord/types/utils";
|
||||
import { findLazy, findStoreLazy, onceReady } from "@vencord/types/webpack";
|
||||
import { FluxDispatcher, InviteActions } from "@vencord/types/webpack/common";
|
||||
@@ -15,9 +16,7 @@ import { Settings } from "./settings";
|
||||
const logger = new Logger("VesktopRPC", "#5865f2");
|
||||
const StreamerModeStore = findStoreLazy("StreamerModeStore");
|
||||
|
||||
const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as any as {
|
||||
handleEvent(e: MessageEvent): void;
|
||||
};
|
||||
const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as typeof arRpcPlugin;
|
||||
|
||||
onIpcCommand(IpcCommands.RPC_ACTIVITY, async jsonData => {
|
||||
if (!Settings.store.arRPC) return;
|
||||
|
||||
@@ -6,25 +6,40 @@
|
||||
|
||||
import "./screenSharePicker.css";
|
||||
|
||||
import { closeModal, Logger, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils";
|
||||
import { onceReady } from "@vencord/types/webpack";
|
||||
import { classNameFactory } from "@vencord/types/api/Styles";
|
||||
import {
|
||||
BaseText,
|
||||
Button,
|
||||
Card,
|
||||
FluxDispatcher,
|
||||
Forms,
|
||||
Select,
|
||||
Switch,
|
||||
Text,
|
||||
UserStore,
|
||||
useState
|
||||
} from "@vencord/types/webpack/common";
|
||||
CogWheel,
|
||||
FormSwitch,
|
||||
Heading,
|
||||
HeadingTertiary,
|
||||
Margins,
|
||||
Paragraph,
|
||||
RestartIcon,
|
||||
Span
|
||||
} from "@vencord/types/components";
|
||||
import {
|
||||
closeModal,
|
||||
Logger,
|
||||
ModalCloseButton,
|
||||
Modals,
|
||||
ModalSize,
|
||||
openModal,
|
||||
useAwaiter,
|
||||
useForceUpdater
|
||||
} from "@vencord/types/utils";
|
||||
import { onceReady } from "@vencord/types/webpack";
|
||||
import { FluxDispatcher, Select, UserStore, useState } from "@vencord/types/webpack/common";
|
||||
import { Node } from "@vencord/venmic";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { MediaEngineStore } from "renderer/common";
|
||||
import { addPatch } from "renderer/patches/shared";
|
||||
import { State, useSettings, useVesktopState } from "renderer/settings";
|
||||
import { classNameFactory, isLinux, isWindows } from "renderer/utils";
|
||||
import { isLinux, isWindows } from "renderer/utils";
|
||||
|
||||
import { SimpleErrorBoundary } from "./SimpleErrorBoundary";
|
||||
|
||||
const StreamResolutions = ["480", "720", "1080", "1440", "2160"] as const;
|
||||
const StreamFps = ["15", "30", "60"] as const;
|
||||
@@ -68,16 +83,16 @@ const logger = new Logger("VesktopScreenShare");
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
find: "this.localWant=",
|
||||
find: "this.getDefaultGoliveQuality()",
|
||||
replacement: {
|
||||
match: /this.localWant=/,
|
||||
replace: "$self.patchStreamQuality(this);$&"
|
||||
match: /this\.getDefaultGoliveQuality\(\)/,
|
||||
replace: "$self.patchStreamQuality($&)"
|
||||
}
|
||||
}
|
||||
],
|
||||
patchStreamQuality(opts: any) {
|
||||
const { screenshareQuality } = State.store;
|
||||
if (!screenshareQuality) return;
|
||||
if (!screenshareQuality) return opts;
|
||||
|
||||
const framerate = Number(screenshareQuality.frameRate);
|
||||
const height = Number(screenshareQuality.resolution);
|
||||
@@ -102,6 +117,7 @@ addPatch({
|
||||
height,
|
||||
pixelCount: height * width
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -153,6 +169,9 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
|
||||
onCloseRequest() {
|
||||
closeModal(key);
|
||||
reject("Aborted");
|
||||
},
|
||||
onCloseCallback() {
|
||||
reject("Aborted");
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -173,9 +192,7 @@ function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScre
|
||||
/>
|
||||
|
||||
<img src={url} alt="" />
|
||||
<Text className={cl("screen-name")} variant="text-sm/normal">
|
||||
{name}
|
||||
</Text>
|
||||
<Paragraph className={cl("screen-name")}>{name}</Paragraph>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -196,73 +213,60 @@ function AudioSettingsModal({
|
||||
return (
|
||||
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<Modals.ModalHeader className={cl("header")}>
|
||||
<Forms.FormTitle tag="h2" className={cl("header-title")}>
|
||||
Venmic Settings
|
||||
</Forms.FormTitle>
|
||||
<Modals.ModalCloseButton onClick={close} />
|
||||
<BaseText size="lg" weight="semibold" tag="h3" style={{ flexGrow: 1 }}>
|
||||
Audio Settings
|
||||
</BaseText>
|
||||
<ModalCloseButton onClick={close} />
|
||||
</Modals.ModalHeader>
|
||||
<Modals.ModalContent className={cl("modal")}>
|
||||
<Switch
|
||||
|
||||
<Modals.ModalContent className={cl("modal", "venmic-settings")}>
|
||||
<FormSwitch
|
||||
title="Microphone Workaround"
|
||||
description="Work around an issue that causes the microphone to be shared instead of the correct audio. Only enable if you're experiencing this issue."
|
||||
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.
|
||||
</>
|
||||
/>
|
||||
<FormSwitch
|
||||
title="Only Speakers"
|
||||
description={
|
||||
'When sharing entire desktop audio, only share apps that play to a speaker. You may want to disable this when using "mix bussing".'
|
||||
}
|
||||
>
|
||||
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={
|
||||
/>
|
||||
<FormSwitch
|
||||
title="Only Default Speakers"
|
||||
description={
|
||||
<>
|
||||
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, onlyDefaultSpeakers: v })}
|
||||
value={Settings.audio?.onlyDefaultSpeakers ?? true}
|
||||
/>
|
||||
<FormSwitch
|
||||
title="Ignore Inputs"
|
||||
description="Exclude nodes that are intended to capture audio."
|
||||
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
|
||||
/>
|
||||
<FormSwitch
|
||||
title="Ignore Virtual"
|
||||
description={
|
||||
'Exclude virtual nodes, such as nodes belonging to loopbacks. This might be useful when using "mix bussing".'
|
||||
}
|
||||
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
|
||||
/>
|
||||
<FormSwitch
|
||||
title="Ignore Devices"
|
||||
description="Exclude device nodes, such as nodes belonging to microphones or speakers."
|
||||
hideBorder
|
||||
onChange={v =>
|
||||
(Settings.audio = {
|
||||
@@ -272,22 +276,25 @@ function AudioSettingsModal({
|
||||
})
|
||||
}
|
||||
value={Settings.audio?.ignoreDevices ?? true}
|
||||
note={<>Exclude device nodes, such as nodes belonging to microphones or speakers.</>}
|
||||
>
|
||||
Ignore Devices
|
||||
</Switch>
|
||||
<Switch
|
||||
/>
|
||||
<FormSwitch
|
||||
title="Granular Selection"
|
||||
description="Allow to select applications more granularly."
|
||||
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>
|
||||
<Switch
|
||||
/>
|
||||
<FormSwitch
|
||||
title="Device Selection"
|
||||
description={
|
||||
<>
|
||||
Allow to select devices such as microphones. Requires <b>Ignore Devices</b> to be turned
|
||||
off.
|
||||
</>
|
||||
}
|
||||
hideBorder
|
||||
onChange={value => {
|
||||
Settings.audio = { ...Settings.audio, deviceSelect: value };
|
||||
@@ -295,18 +302,10 @@ function AudioSettingsModal({
|
||||
}}
|
||||
value={Settings.audio?.deviceSelect ?? false}
|
||||
disabled={Settings.audio?.ignoreDevices}
|
||||
note={
|
||||
<>
|
||||
Allow to select devices such as microphones. Requires <b>Ignore Devices</b> to be turned
|
||||
off.
|
||||
</>
|
||||
}
|
||||
>
|
||||
Device Selection
|
||||
</Switch>
|
||||
/>
|
||||
</Modals.ModalContent>
|
||||
<Modals.ModalFooter className={cl("footer")}>
|
||||
<Button color={Button.Colors.TRANSPARENT} onClick={close}>
|
||||
<Button variant="secondary" onClick={close}>
|
||||
Back
|
||||
</Button>
|
||||
</Modals.ModalFooter>
|
||||
@@ -327,7 +326,7 @@ function OptionRadio<Settings extends object, Key extends keyof Settings>(props:
|
||||
<div className={cl("option-radios")}>
|
||||
{(options as string[]).map((option, idx) => (
|
||||
<label className={cl("option-radio")} data-checked={settings[settingsKey] === option} key={option}>
|
||||
<Text variant="text-sm/bold">{labels?.[idx] ?? option}</Text>
|
||||
<Span weight="bold">{labels?.[idx] ?? option}</Span>
|
||||
<input
|
||||
className={cl("option-input")}
|
||||
type="radio"
|
||||
@@ -365,7 +364,7 @@ function StreamSettingsUi({
|
||||
);
|
||||
|
||||
const openSettings = () => {
|
||||
const key = openModal(props => (
|
||||
openModal(props => (
|
||||
<AudioSettingsModal
|
||||
modalProps={props}
|
||||
close={() => props.onClose()}
|
||||
@@ -378,18 +377,18 @@ function StreamSettingsUi({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Forms.FormTitle>What you're streaming</Forms.FormTitle>
|
||||
<HeadingTertiary className={Margins.bottom8}>What you're streaming</HeadingTertiary>
|
||||
<Card className={cl("card", "preview")}>
|
||||
<img src={thumb} alt="" className={cl(isLinux ? "preview-img-linux" : "preview-img")} />
|
||||
<Text variant="text-sm/normal">{source.name}</Text>
|
||||
<Paragraph>{source.name}</Paragraph>
|
||||
</Card>
|
||||
|
||||
<Forms.FormTitle>Stream Settings</Forms.FormTitle>
|
||||
<HeadingTertiary className={Margins.bottom8}>Stream Settings</HeadingTertiary>
|
||||
|
||||
<Card className={cl("card")}>
|
||||
<div className={cl("quality")}>
|
||||
<section className={cl("quality-section")}>
|
||||
<Forms.FormTitle>Resolution</Forms.FormTitle>
|
||||
<Heading tag="h5">Resolution</Heading>
|
||||
<OptionRadio
|
||||
options={StreamResolutions}
|
||||
settings={qualitySettings}
|
||||
@@ -399,7 +398,7 @@ function StreamSettingsUi({
|
||||
</section>
|
||||
|
||||
<section className={cl("quality-section")}>
|
||||
<Forms.FormTitle>Frame Rate</Forms.FormTitle>
|
||||
<Heading tag="h5">Frame Rate</Heading>
|
||||
<OptionRadio
|
||||
options={StreamFps}
|
||||
settings={qualitySettings}
|
||||
@@ -410,7 +409,7 @@ function StreamSettingsUi({
|
||||
</div>
|
||||
<div className={cl("quality")}>
|
||||
<section className={cl("quality-section")}>
|
||||
<Forms.FormTitle>Content Type</Forms.FormTitle>
|
||||
<Heading tag="h5">Content Type</Heading>
|
||||
<div>
|
||||
<OptionRadio
|
||||
options={["motion", "detail"]}
|
||||
@@ -419,22 +418,20 @@ function StreamSettingsUi({
|
||||
settingsKey="contentHint"
|
||||
onChange={option => setSettings(s => ({ ...s, contentHint: option }))}
|
||||
/>
|
||||
<div className={cl("hint-description")}>
|
||||
<p>
|
||||
Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange
|
||||
for a much sharper and clearer image.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Paragraph className={Margins.top8}>
|
||||
Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange for
|
||||
a much sharper and clearer image.
|
||||
</Paragraph>
|
||||
</div>
|
||||
{isWindows && (
|
||||
<Switch
|
||||
<FormSwitch
|
||||
title="Stream With Audio"
|
||||
hideBorder
|
||||
value={settings.audio}
|
||||
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
|
||||
hideBorder
|
||||
className={cl("audio")}
|
||||
>
|
||||
Stream With Audio
|
||||
</Switch>
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
@@ -585,8 +582,10 @@ function AudioSourcePickerLinux({
|
||||
setIncludeSources: (s: AudioSources) => void;
|
||||
setExcludeSources: (s: AudioSources) => void;
|
||||
}) {
|
||||
const [audioSourcesSignal, refreshAudioSources] = useForceUpdater(true);
|
||||
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
|
||||
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true }
|
||||
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true },
|
||||
deps: [audioSourcesSignal]
|
||||
});
|
||||
|
||||
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
|
||||
@@ -594,32 +593,40 @@ function AudioSourcePickerLinux({
|
||||
|
||||
if (!sources.ok && sources.isGlibCxxOutdated) {
|
||||
return (
|
||||
<Forms.FormText>
|
||||
<Paragraph>
|
||||
Failed to retrieve Audio Sources because your C++ library is too old to run
|
||||
<a href="https://github.com/Vencord/venmic" target="_blank">
|
||||
<a href="https://github.com/Vencord/venmic" target="_blank" rel="noreferrer">
|
||||
venmic
|
||||
</a>
|
||||
. See{" "}
|
||||
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
|
||||
<a
|
||||
href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
this guide
|
||||
</a>{" "}
|
||||
for possible solutions.
|
||||
</Forms.FormText>
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPipewirePulse && !ignorePulseWarning) {
|
||||
return (
|
||||
<Text variant="text-sm/normal">
|
||||
<Paragraph>
|
||||
Could not find pipewire-pulse. See{" "}
|
||||
<a href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install" target="_blank">
|
||||
<a
|
||||
href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
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>
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -637,45 +644,56 @@ function AudioSourcePickerLinux({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cl({ quality: includeSources === "Entire System" })}>
|
||||
<div className={cl("audio-sources")}>
|
||||
<section>
|
||||
<Forms.FormTitle>{loading ? "Loading Sources..." : "Audio Sources"}</Forms.FormTitle>
|
||||
<Select
|
||||
options={allSources.map(({ name, value }) => ({
|
||||
label: name,
|
||||
value: value,
|
||||
default: name === "None"
|
||||
}))}
|
||||
isSelected={isItemSelected(includeSources)}
|
||||
select={updateItems(setIncludeSources, includeSources)}
|
||||
serialize={String}
|
||||
popoutPosition="top"
|
||||
closeOnSelect={false}
|
||||
/>
|
||||
</section>
|
||||
{includeSources === "Entire System" && (
|
||||
<section>
|
||||
<Forms.FormTitle>Exclude Sources</Forms.FormTitle>
|
||||
<Heading tag="h5">{loading ? "Loading Sources..." : "Audio Sources"}</Heading>
|
||||
<SimpleErrorBoundary>
|
||||
<Select
|
||||
options={allSources
|
||||
.filter(x => x.name !== "Entire System")
|
||||
.map(({ name, value }) => ({
|
||||
label: name,
|
||||
value: value,
|
||||
default: name === "None"
|
||||
}))}
|
||||
isSelected={isItemSelected(excludeSources)}
|
||||
select={updateItems(setExcludeSources, excludeSources)}
|
||||
options={allSources.map(({ name, value }) => ({
|
||||
label: name,
|
||||
value: value,
|
||||
default: name === "None"
|
||||
}))}
|
||||
isSelected={isItemSelected(includeSources)}
|
||||
select={updateItems(setIncludeSources, includeSources)}
|
||||
serialize={String}
|
||||
popoutPosition="top"
|
||||
closeOnSelect={false}
|
||||
/>
|
||||
</SimpleErrorBoundary>
|
||||
</section>
|
||||
{includeSources === "Entire System" && (
|
||||
<section>
|
||||
<Heading tag="h5">Exclude Sources</Heading>
|
||||
<SimpleErrorBoundary>
|
||||
<Select
|
||||
options={allSources
|
||||
.filter(x => x.name !== "Entire System")
|
||||
.map(({ name, value }) => ({
|
||||
label: name,
|
||||
value: value,
|
||||
default: name === "None"
|
||||
}))}
|
||||
isSelected={isItemSelected(excludeSources)}
|
||||
select={updateItems(setExcludeSources, excludeSources)}
|
||||
serialize={String}
|
||||
popoutPosition="top"
|
||||
closeOnSelect={false}
|
||||
/>
|
||||
</SimpleErrorBoundary>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
<Button color={Button.Colors.TRANSPARENT} onClick={openSettings} className={cl("settings-button")}>
|
||||
Open Audio Settings
|
||||
</Button>
|
||||
<div className={cl("settings-buttons")}>
|
||||
<Button variant="secondary" onClick={refreshAudioSources} className={cl("settings-button")}>
|
||||
<RestartIcon className={cl("settings-button-icon")} />
|
||||
Refresh Audio Sources
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={openSettings} className={cl("settings-button")}>
|
||||
<CogWheel className={cl("settings-button-icon")} />
|
||||
Open Audio Settings
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -707,8 +725,10 @@ function ModalComponent({
|
||||
return (
|
||||
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<Modals.ModalHeader className={cl("header")}>
|
||||
<Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle>
|
||||
<Modals.ModalCloseButton onClick={close} />
|
||||
<BaseText size="lg" weight="semibold" tag="h3" style={{ flexGrow: 1 }}>
|
||||
Screen Share Picker
|
||||
</BaseText>
|
||||
<ModalCloseButton onClick={close} />
|
||||
</Modals.ModalHeader>
|
||||
<Modals.ModalContent className={cl("modal")}>
|
||||
{!selected ? (
|
||||
@@ -786,11 +806,11 @@ function ModalComponent({
|
||||
</Button>
|
||||
|
||||
{selected && !skipPicker ? (
|
||||
<Button color={Button.Colors.TRANSPARENT} onClick={() => setSelected(void 0)}>
|
||||
<Button variant="secondary" onClick={() => setSelected(void 0)}>
|
||||
Back
|
||||
</Button>
|
||||
) : (
|
||||
<Button color={Button.Colors.TRANSPARENT} onClick={close}>
|
||||
<Button variant="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
46
src/renderer/components/SimpleErrorBoundary.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Card, ErrorBoundary, HeadingTertiary, Paragraph, TextButton } from "@vencord/types/components";
|
||||
import { FluxDispatcher, InviteActions } from "@vencord/types/webpack/common";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
async function openSupportChannel() {
|
||||
const code = "YVbdG2ZRG4";
|
||||
|
||||
try {
|
||||
const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal");
|
||||
|
||||
if (!invite) throw 0;
|
||||
|
||||
await FluxDispatcher.dispatch({
|
||||
type: "INVITE_MODAL_OPEN",
|
||||
invite,
|
||||
code,
|
||||
context: "APP"
|
||||
});
|
||||
} catch {
|
||||
window.open(`https://discord.gg/${code}`, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
function Fallback() {
|
||||
return (
|
||||
<Card variant="danger">
|
||||
<HeadingTertiary>Something went wrong.</HeadingTertiary>
|
||||
<Paragraph>
|
||||
Please make sure Vencord and Vesktop are fully up to date. You can get help in our{" "}
|
||||
<TextButton variant="link" onClick={openSupportChannel}>
|
||||
Support Channel
|
||||
</TextButton>
|
||||
</Paragraph>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function SimpleErrorBoundary({ children }: PropsWithChildren<{}>) {
|
||||
return <ErrorBoundary fallback={Fallback}>{children}</ErrorBoundary>;
|
||||
}
|
||||
@@ -2,8 +2,13 @@
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-header-title {
|
||||
margin: 0;
|
||||
.vcd-screen-picker-header-close-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-venmic-settings {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-footer {
|
||||
@@ -76,13 +81,13 @@
|
||||
/* Option Radios */
|
||||
|
||||
.vcd-screen-picker-option-radios {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-option-radio {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--primary-800);
|
||||
@@ -117,19 +122,33 @@
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-settings-button {
|
||||
margin-left: auto;
|
||||
margin-top: 0.3rem;
|
||||
.vcd-screen-picker-settings-buttons {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: 0.5em;
|
||||
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-settings-button {
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
padding-inline: 0.5em 1em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-settings-button-icon {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-audio {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-hint-description {
|
||||
color: var(--header-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.vcd-screen-picker-audio-sources {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
|
||||
>section {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,28 @@ import { useState } from "@vencord/types/webpack/common";
|
||||
import { SettingsComponent } from "./Settings";
|
||||
import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch";
|
||||
|
||||
export const AutoStartToggle: SettingsComponent = () => {
|
||||
export const AutoStartToggle: SettingsComponent = ({ settings }) => {
|
||||
const [autoStartEnabled, setAutoStartEnabled] = useState(VesktopNative.autostart.isEnabled());
|
||||
|
||||
return (
|
||||
<VesktopSettingsSwitch
|
||||
value={autoStartEnabled}
|
||||
onChange={async v => {
|
||||
await VesktopNative.autostart[v ? "enable" : "disable"]();
|
||||
setAutoStartEnabled(v);
|
||||
}}
|
||||
note="Automatically start Vesktop on computer start-up"
|
||||
>
|
||||
Start With System
|
||||
</VesktopSettingsSwitch>
|
||||
<>
|
||||
<VesktopSettingsSwitch
|
||||
title="Start With System"
|
||||
description="Automatically start Vesktop on computer start-up"
|
||||
value={autoStartEnabled}
|
||||
onChange={async v => {
|
||||
await VesktopNative.autostart[v ? "enable" : "disable"]();
|
||||
setAutoStartEnabled(v);
|
||||
}}
|
||||
/>
|
||||
|
||||
<VesktopSettingsSwitch
|
||||
title="Auto Start Minimized"
|
||||
description={"Start Vesktop minimized when starting with system"}
|
||||
value={settings.autoStartMinimized ?? false}
|
||||
onChange={v => (settings.autoStartMinimized = v)}
|
||||
disabled={!autoStartEnabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { BaseText, Button, Heading, Paragraph, TextButton } from "@vencord/types/components";
|
||||
import {
|
||||
Margins,
|
||||
ModalCloseButton,
|
||||
@@ -14,10 +15,10 @@ import {
|
||||
openModal,
|
||||
useForceUpdater
|
||||
} from "@vencord/types/utils";
|
||||
import { Button, Forms, Text, Toasts } from "@vencord/types/webpack/common";
|
||||
import { Toasts } from "@vencord/types/webpack/common";
|
||||
import { Settings } from "shared/settings";
|
||||
|
||||
import { SettingsComponent } from "./Settings";
|
||||
import { cl, SettingsComponent } from "./Settings";
|
||||
|
||||
export const DeveloperOptionsButton: SettingsComponent = ({ settings }) => {
|
||||
return <Button onClick={() => openDeveloperOptionsModal(settings)}>Open Developer Settings</Button>;
|
||||
@@ -27,21 +28,21 @@ function openDeveloperOptionsModal(settings: Settings) {
|
||||
openModal(props => (
|
||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>
|
||||
<BaseText size="lg" weight="semibold" tag="h3" style={{ flexGrow: 1 }}>
|
||||
Vesktop Developer Options
|
||||
</Text>
|
||||
</BaseText>
|
||||
<ModalCloseButton onClick={props.onClose} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<div style={{ padding: "1em 0" }}>
|
||||
<Forms.FormTitle tag="h5">Vencord Location</Forms.FormTitle>
|
||||
<Heading tag="h5">Vencord Location</Heading>
|
||||
<VencordLocationPicker settings={settings} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16}>
|
||||
<Heading tag="h5" className={Margins.top16}>
|
||||
Debugging
|
||||
</Forms.FormTitle>
|
||||
<div className="vcd-settings-button-grid">
|
||||
</Heading>
|
||||
<div className={cl("button-grid")}>
|
||||
<Button onClick={() => VesktopNative.debug.launchGpu()}>Open chrome://gpu</Button>
|
||||
<Button onClick={() => VesktopNative.debug.launchWebrtcInternals()}>
|
||||
Open chrome://webrtc-internals
|
||||
@@ -59,25 +60,24 @@ const VencordLocationPicker: SettingsComponent = ({ settings }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormText>
|
||||
<Paragraph>
|
||||
Vencord files are loaded from{" "}
|
||||
{vencordDir ? (
|
||||
<a
|
||||
href="about:blank"
|
||||
<TextButton
|
||||
variant="link"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
VesktopNative.fileManager.showItemInFolder(vencordDir!);
|
||||
}}
|
||||
>
|
||||
{vencordDir}
|
||||
</a>
|
||||
</TextButton>
|
||||
) : (
|
||||
"the default location"
|
||||
)}
|
||||
</Forms.FormText>
|
||||
<div className="vcd-settings-button-grid">
|
||||
</Paragraph>
|
||||
<div className={cl("button-grid")}>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={async () => {
|
||||
const choice = await VesktopNative.fileManager.selectVencordDir();
|
||||
switch (choice) {
|
||||
@@ -105,8 +105,7 @@ const VencordLocationPicker: SettingsComponent = ({ settings }) => {
|
||||
Change
|
||||
</Button>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.RED}
|
||||
variant="dangerPrimary"
|
||||
onClick={async () => {
|
||||
await VesktopNative.fileManager.selectVencordDir(null);
|
||||
forceUpdate();
|
||||
|
||||
@@ -6,21 +6,24 @@
|
||||
|
||||
import { Select } from "@vencord/types/webpack/common";
|
||||
|
||||
import { SimpleErrorBoundary } from "../SimpleErrorBoundary";
|
||||
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}
|
||||
/>
|
||||
<SimpleErrorBoundary>
|
||||
<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}
|
||||
/>
|
||||
</SimpleErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,15 +12,14 @@ import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch";
|
||||
export const NotificationBadgeToggle: SettingsComponent = ({ settings }) => {
|
||||
return (
|
||||
<VesktopSettingsSwitch
|
||||
title="Notification Badge"
|
||||
description="Show mention badge on the app icon"
|
||||
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
|
||||
</VesktopSettingsSwitch>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
27
src/renderer/components/settings/OutdatedVesktopWarning.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Button, Card, HeadingTertiary, Paragraph } from "@vencord/types/components";
|
||||
import { useAwaiter } from "@vencord/types/utils";
|
||||
|
||||
import { cl } from "./Settings";
|
||||
|
||||
export function OutdatedVesktopWarning() {
|
||||
const [isOutdated] = useAwaiter(VesktopNative.app.isOutdated);
|
||||
|
||||
if (!isOutdated) return null;
|
||||
|
||||
return (
|
||||
<Card variant="warning" className={cl("updater-card")}>
|
||||
<HeadingTertiary>Your Vesktop is outdated!</HeadingTertiary>
|
||||
<Paragraph>Staying up to date is important for security and stability.</Paragraph>
|
||||
|
||||
<Button onClick={() => VesktopNative.app.openUpdater()} variant="secondary">
|
||||
Open Updater
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
import "./settings.css";
|
||||
|
||||
import { ErrorBoundary } from "@vencord/types/components";
|
||||
import { Forms, Text } from "@vencord/types/webpack/common";
|
||||
import { classNameFactory } from "@vencord/types/api/Styles";
|
||||
import { BaseText, Divider, ErrorBoundary } from "@vencord/types/components";
|
||||
import { ComponentType } from "react";
|
||||
import { Settings, useSettings } from "renderer/settings";
|
||||
import { isMac, isWindows } from "renderer/utils";
|
||||
@@ -16,6 +16,8 @@ import { AutoStartToggle } from "./AutoStartToggle";
|
||||
import { DeveloperOptionsButton } from "./DeveloperOptions";
|
||||
import { DiscordBranchPicker } from "./DiscordBranchPicker";
|
||||
import { NotificationBadgeToggle } from "./NotificationBadgeToggle";
|
||||
import { OutdatedVesktopWarning } from "./OutdatedVesktopWarning";
|
||||
import { UserAssetsButton } from "./UserAssets";
|
||||
import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch";
|
||||
import { WindowsTransparencyControls } from "./WindowsTransparencyControls";
|
||||
|
||||
@@ -28,6 +30,8 @@ interface BooleanSetting {
|
||||
invisible?(): boolean;
|
||||
}
|
||||
|
||||
export const cl = classNameFactory("vcd-settings-");
|
||||
|
||||
export type SettingsComponent = ComponentType<{ settings: typeof Settings.store }>;
|
||||
|
||||
const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>> = {
|
||||
@@ -82,7 +86,8 @@ const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>>
|
||||
description: "Adapt the splash window colors to your custom theme",
|
||||
defaultValue: true
|
||||
},
|
||||
WindowsTransparencyControls
|
||||
WindowsTransparencyControls,
|
||||
UserAssetsButton
|
||||
],
|
||||
Behaviour: [
|
||||
{
|
||||
@@ -119,7 +124,15 @@ const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>>
|
||||
defaultValue: false
|
||||
}
|
||||
],
|
||||
Notifications: [NotificationBadgeToggle],
|
||||
Notifications: [
|
||||
NotificationBadgeToggle,
|
||||
{
|
||||
key: "enableTaskbarFlashing",
|
||||
title: "Enable Taskbar Flashing",
|
||||
description: "Flashes the app in your taskbar when you have new notifications.",
|
||||
defaultValue: false
|
||||
}
|
||||
],
|
||||
Miscellaneous: [
|
||||
{
|
||||
key: "arRPC",
|
||||
@@ -142,33 +155,32 @@ function SettingsSections() {
|
||||
const Settings = useSettings();
|
||||
|
||||
const sections = Object.entries(SettingsOptions).map(([title, settings], i, arr) => (
|
||||
<div key={title} className="vcd-settings-category">
|
||||
<Text variant="heading-lg/semibold" color="header-primary" className="vcd-settings-category-title">
|
||||
<div key={title} className={cl("category")}>
|
||||
<BaseText size="lg" weight="semibold" tag="h3" className={cl("category-title")}>
|
||||
{title}
|
||||
</Text>
|
||||
</BaseText>
|
||||
|
||||
<div className="vcd-settings-category-content">
|
||||
{settings.map(Setting => {
|
||||
if (typeof Setting === "function") return <Setting settings={Settings} />;
|
||||
<div className={cl("category-content")}>
|
||||
{settings.map((Setting, i) => {
|
||||
if (typeof Setting === "function") return <Setting key={`Custom-${i}`} settings={Settings} />;
|
||||
|
||||
const { defaultValue, title, description, key, disabled, invisible } = Setting;
|
||||
if (invisible?.()) return null;
|
||||
|
||||
return (
|
||||
<VesktopSettingsSwitch
|
||||
title={title}
|
||||
description={description}
|
||||
value={Settings[key as any] ?? defaultValue}
|
||||
onChange={v => (Settings[key as any] = v)}
|
||||
note={description}
|
||||
disabled={disabled?.()}
|
||||
key={key}
|
||||
>
|
||||
{title}
|
||||
</VesktopSettingsSwitch>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{i < arr.length - 1 && <Forms.FormDivider className="vcd-settings-category-divider" />}
|
||||
{i < arr.length - 1 && <Divider className={cl("category-divider")} />}
|
||||
</div>
|
||||
));
|
||||
|
||||
@@ -178,14 +190,10 @@ function SettingsSections() {
|
||||
export default ErrorBoundary.wrap(
|
||||
function SettingsUI() {
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
{/* FIXME: Outdated type */}
|
||||
{/* @ts-expect-error Outdated type */}
|
||||
<Text variant="heading-xl/semibold" color="header-primary" className="vcd-settings-title">
|
||||
Vesktop Settings
|
||||
</Text>
|
||||
<section>
|
||||
<OutdatedVesktopWarning />
|
||||
<SettingsSections />
|
||||
</Forms.FormSection>
|
||||
</section>
|
||||
);
|
||||
},
|
||||
{
|
||||
|
||||
31
src/renderer/components/settings/UserAssets.css
Normal file
@@ -0,0 +1,31 @@
|
||||
.vcd-user-assets {
|
||||
display: flex;
|
||||
margin-block: 1em 2em;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.vcd-user-assets-asset {
|
||||
display: flex;
|
||||
margin-top: 0.5em;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.vcd-user-assets-actions {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
gap: 0.5em;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.vcd-user-assets-buttons {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.vcd-user-assets-image {
|
||||
height: 7.5em;
|
||||
width: 7.5em;
|
||||
object-fit: contain;
|
||||
}
|
||||
106
src/renderer/components/settings/UserAssets.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./UserAssets.css";
|
||||
|
||||
import { BaseText, Button, FormSwitch } from "@vencord/types/components";
|
||||
import {
|
||||
Margins,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalRoot,
|
||||
ModalSize,
|
||||
openModal,
|
||||
wordsFromCamel,
|
||||
wordsToTitle
|
||||
} from "@vencord/types/utils";
|
||||
import { showToast, useState } from "@vencord/types/webpack/common";
|
||||
import { UserAssetType } from "main/userAssets";
|
||||
import { useSettings } from "renderer/settings";
|
||||
|
||||
import { SettingsComponent } from "./Settings";
|
||||
|
||||
const CUSTOMIZABLE_ASSETS: UserAssetType[] = ["splash", "tray", "trayUnread"];
|
||||
|
||||
export const UserAssetsButton: SettingsComponent = () => {
|
||||
return <Button onClick={() => openAssetsModal()}>Customize App Assets</Button>;
|
||||
};
|
||||
|
||||
function openAssetsModal() {
|
||||
openModal(props => (
|
||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader>
|
||||
<BaseText size="lg" weight="semibold" tag="h3" style={{ flexGrow: 1 }}>
|
||||
User Assets
|
||||
</BaseText>
|
||||
<ModalCloseButton onClick={props.onClose} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<div className="vcd-user-assets">
|
||||
{CUSTOMIZABLE_ASSETS.map(asset => (
|
||||
<Asset key={asset} asset={asset} />
|
||||
))}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
));
|
||||
}
|
||||
|
||||
function Asset({ asset }: { asset: UserAssetType }) {
|
||||
// cache busting
|
||||
const [version, setVersion] = useState(Date.now());
|
||||
const settings = useSettings();
|
||||
|
||||
const isSplash = asset === "splash";
|
||||
const imageRendering = isSplash && settings.splashPixelated ? "pixelated" : "auto";
|
||||
|
||||
const onChooseAsset = (value?: null) => async () => {
|
||||
const res = await VesktopNative.fileManager.chooseUserAsset(asset, value);
|
||||
if (res === "ok") {
|
||||
setVersion(Date.now());
|
||||
if (isSplash && value === null) {
|
||||
settings.splashPixelated = false;
|
||||
}
|
||||
} else if (res === "failed") {
|
||||
showToast("Something went wrong. Please try again");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<BaseText size="md" weight="medium" tag="h3">
|
||||
{wordsToTitle(wordsFromCamel(asset))}
|
||||
</BaseText>
|
||||
<div className="vcd-user-assets-asset">
|
||||
<img
|
||||
className="vcd-user-assets-image"
|
||||
src={`vesktop://assets/${asset}?v=${version}`}
|
||||
alt=""
|
||||
style={{ imageRendering }}
|
||||
/>
|
||||
<div className="vcd-user-assets-actions">
|
||||
<div className="vcd-user-assets-buttons">
|
||||
<Button onClick={onChooseAsset()}>Customize</Button>
|
||||
<Button variant="secondary" onClick={onChooseAsset(null)}>
|
||||
Reset to default
|
||||
</Button>
|
||||
</div>
|
||||
{isSplash && (
|
||||
<FormSwitch
|
||||
title="Nearest-Neighbor Scaling (for pixel art)"
|
||||
value={settings.splashPixelated ?? false}
|
||||
onChange={val => (settings.splashPixelated = val)}
|
||||
className={Margins.top16}
|
||||
hideBorder
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -4,13 +4,11 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Switch } from "@vencord/types/webpack/common";
|
||||
import { ComponentProps } from "react";
|
||||
import { FormSwitch } from "@vencord/types/components";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
export function VesktopSettingsSwitch(props: ComponentProps<typeof Switch>) {
|
||||
return (
|
||||
<Switch {...props} hideBorder className="vcd-settings-switch">
|
||||
{props.children}
|
||||
</Switch>
|
||||
);
|
||||
import { cl } from "./Settings";
|
||||
|
||||
export function VesktopSettingsSwitch(props: ComponentProps<typeof FormSwitch>) {
|
||||
return <FormSwitch {...props} hideBorder className={cl("switch")} />;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Heading, Paragraph } from "@vencord/types/components";
|
||||
import { Margins } from "@vencord/types/utils";
|
||||
import { Forms, Select } from "@vencord/types/webpack/common";
|
||||
import { Select } from "@vencord/types/webpack/common";
|
||||
|
||||
import { SimpleErrorBoundary } from "../SimpleErrorBoundary";
|
||||
import { SettingsComponent } from "./Settings";
|
||||
|
||||
export const WindowsTransparencyControls: SettingsComponent = ({ settings }) => {
|
||||
@@ -14,34 +16,36 @@ export const WindowsTransparencyControls: SettingsComponent = ({ settings }) =>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Forms.FormTitle className={Margins.bottom8}>Transparency Options</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
<Heading tag="h5">Transparency Options</Heading>
|
||||
<Paragraph className={Margins.bottom8}>
|
||||
Requires a full restart. You will need a theme that supports transparency for this to work.
|
||||
</Forms.FormText>
|
||||
</Paragraph>
|
||||
|
||||
<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}
|
||||
/>
|
||||
<SimpleErrorBoundary>
|
||||
<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}
|
||||
/>
|
||||
</SimpleErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,4 +31,11 @@
|
||||
|
||||
.vcd-settings-switch {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.vcd-settings-updater-card {
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
}
|
||||
@@ -23,7 +23,8 @@ import type SettingsPlugin from "@vencord/types/plugins/_core/settings";
|
||||
VesktopLogger.log("read if cute :3");
|
||||
VesktopLogger.log("Vesktop v" + VesktopNative.app.getVersion());
|
||||
|
||||
const customSettingsSections = (Vencord.Plugins.plugins.Settings as any as typeof SettingsPlugin).customSections;
|
||||
// TODO
|
||||
const customSettingsSections = (Vencord.Plugins.plugins.Settings as typeof SettingsPlugin).customSections;
|
||||
|
||||
customSettingsSections.push(() => ({
|
||||
section: "Vesktop",
|
||||
@@ -31,3 +32,14 @@ customSettingsSections.push(() => ({
|
||||
element: SettingsUi,
|
||||
className: "vc-vesktop-settings"
|
||||
}));
|
||||
|
||||
// TODO: remove this legacy workaround once some time has passed
|
||||
// @ts-expect-error
|
||||
if (!Vencord.Api.Styles.vencordRootNode) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "vesktop-css-core";
|
||||
|
||||
VesktopNative.app.getRendererCss().then(css => (style.textContent = css));
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => document.documentElement.append(style), { once: true });
|
||||
}
|
||||
|
||||
@@ -26,12 +26,10 @@ addPatch({
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /if\(null!=(\i)\)(?=.{0,50}\1\.window\.setDevtoolsCallbacks)/,
|
||||
replace: "if(true)"
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /\b\i\.window\.setDevtoolsCallbacks/g,
|
||||
replace: "VesktopNative.win.setDevtoolsCallbacks"
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ addPatch({
|
||||
{
|
||||
find: '"NotificationSettingsStore',
|
||||
replacement: {
|
||||
// FIXME: fix eslint rule
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /\.isPlatformEmbedded(?=\?\i\.\i\.ALL)/g,
|
||||
replace: "$&||true"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ addPatch({
|
||||
{
|
||||
find: "lastOutputSystemDevice.justChanged",
|
||||
replacement: {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /(\i)\.\i\.getState\(\).neverShowModal/,
|
||||
replace: "$& || $self.shouldIgnoreDevice($1)"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ addPatch({
|
||||
{
|
||||
find: 'setSinkId"in',
|
||||
replacement: {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /return (\i)\?navigator\.mediaDevices\.enumerateDevices/,
|
||||
replace: "return $1 ? $self.filteredDevices"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ addPatch({
|
||||
{
|
||||
find: "platform-web",
|
||||
replacement: {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /(?<=" platform-overlay"\):)\i/,
|
||||
replace: "$self.getPlatformClass()"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ addPatch({
|
||||
find: ".enableSpellCheck)",
|
||||
replacement: {
|
||||
// if (isDesktop) { DiscordNative.onSpellcheck(openMenu(props)) } else { e.preventDefault(); openMenu(props) }
|
||||
match: /else (.{1,3})\.preventDefault\(\),(.{1,3}\(.{1,3}\))(?<=:(.{1,3})\.enableSpellCheck\).+?)/,
|
||||
match: /else (\i)\.preventDefault\(\),(\i\(\i\))(?<=:(\i)\.enableSpellCheck\).+?)/,
|
||||
// ... else { $self.onSlateContext(() => openMenu(props)) }
|
||||
replace: "else {$self.onSlateContext($1, $3?.enableSpellCheck, () => $2)}"
|
||||
}
|
||||
@@ -66,6 +66,7 @@ addContextMenuPatch("textarea-context", children => {
|
||||
<>
|
||||
{corrections.map(c => (
|
||||
<Menu.MenuItem
|
||||
key={c}
|
||||
id={"vcd-spellcheck-suggestion-" + c}
|
||||
label={c}
|
||||
action={() => VesktopNative.spellcheck.replaceMisspelling(c)}
|
||||
@@ -95,6 +96,7 @@ addContextMenuPatch("textarea-context", children => {
|
||||
const isEnabled = spellCheckLanguages.includes(lang);
|
||||
return (
|
||||
<Menu.MenuCheckboxItem
|
||||
key={lang}
|
||||
id={"vcd-spellcheck-lang-" + lang}
|
||||
label={lang}
|
||||
checked={isEnabled}
|
||||
|
||||
@@ -12,7 +12,6 @@ addPatch({
|
||||
find: ".STREAMER_MODE_ENABLE,",
|
||||
replacement: {
|
||||
// remove if (platformEmbedded) check from streamer mode toggle
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /if\(\i\.\i\)(?=return.{0,200}?"autoToggle")/g,
|
||||
replace: ""
|
||||
}
|
||||
|
||||
27
src/renderer/patches/taskBarFlash.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Settings } from "renderer/settings";
|
||||
|
||||
import { addPatch } from "./shared";
|
||||
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
find: ".flashFrame(!0)",
|
||||
replacement: {
|
||||
match: /(\i)&&\i\.\i\.taskbarFlash&&\i\.\i\.flashFrame\(!0\)/,
|
||||
replace: "$self.flashFrame()"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
flashFrame() {
|
||||
if (Settings.store.enableTaskbarFlashing) {
|
||||
VesktopNative.win.flashFrame(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -12,13 +12,10 @@ addPatch({
|
||||
find: ",setSystemTrayApplications",
|
||||
replacement: [
|
||||
{
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /\i\.window\.(close|minimize|maximize)/g,
|
||||
replace: `VesktopNative.win.$1`
|
||||
},
|
||||
{
|
||||
// TODO: Fix eslint rule
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /(focus(\(\i\)){).{0,150}?\.focus\(\i,\i\)/,
|
||||
replace: "$1VesktopNative.win.focus$2"
|
||||
},
|
||||
|
||||
@@ -15,8 +15,6 @@ if (Settings.store.customTitleBar)
|
||||
find: ".wordmarkWindows",
|
||||
replacement: [
|
||||
{
|
||||
// TODO: Fix eslint rule
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /case \i\.\i\.WINDOWS:/,
|
||||
replace: 'case "WEB":'
|
||||
}
|
||||
@@ -27,14 +25,10 @@ if (Settings.store.customTitleBar)
|
||||
find: ".systemBar,",
|
||||
replacement: [
|
||||
{
|
||||
// TODO: Fix eslint rule
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /\i===\i\.PlatformTypes\.WINDOWS/g,
|
||||
replace: "true"
|
||||
},
|
||||
{
|
||||
// TODO: Fix eslint rule
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /\i===\i\.PlatformTypes\.WEB/g,
|
||||
replace: "false"
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ const updateSplashColors = () => {
|
||||
const bodyStyles = document.body.computedStyleMap();
|
||||
|
||||
const color = bodyStyles.get("--text-default");
|
||||
const backgroundColor = bodyStyles.get("--background-primary");
|
||||
const backgroundColor = bodyStyles.get("--background-base-lowest");
|
||||
|
||||
if (isValidColor(color)) {
|
||||
Settings.store.splashColor = resolveColor(color[0]);
|
||||
|
||||
@@ -19,26 +19,3 @@ const { platform } = navigator;
|
||||
export const isWindows = platform.startsWith("Win");
|
||||
export const isMac = platform.startsWith("Mac");
|
||||
export const isLinux = platform.startsWith("Linux");
|
||||
|
||||
type ClassNameFactoryArg = string | string[] | Record<string, unknown> | false | null | undefined | 0 | "";
|
||||
/**
|
||||
* @param prefix The prefix to add to each class, defaults to `""`
|
||||
* @returns A classname generator function
|
||||
* @example
|
||||
* const cl = classNameFactory("plugin-");
|
||||
*
|
||||
* cl("base", ["item", "editable"], { selected: null, disabled: true })
|
||||
* // => "plugin-base plugin-item plugin-editable plugin-disabled"
|
||||
*/
|
||||
export const classNameFactory =
|
||||
(prefix: string = "") =>
|
||||
(...args: ClassNameFactoryArg[]) => {
|
||||
const classNames = new Set<string>();
|
||||
for (const arg of args) {
|
||||
if (arg && typeof arg === "string") classNames.add(arg);
|
||||
else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name));
|
||||
else if (arg && typeof arg === "object")
|
||||
Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));
|
||||
}
|
||||
return Array.from(classNames, name => prefix + name).join(" ");
|
||||
};
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
*/
|
||||
|
||||
export const enum IpcEvents {
|
||||
GET_VENCORD_PRELOAD_FILE = "VCD_GET_VC_PRELOAD_FILE",
|
||||
GET_VENCORD_PRELOAD_SCRIPT = "VCD_GET_VC_PRELOAD_SCRIPT",
|
||||
DEPRECATED_GET_VENCORD_PRELOAD_SCRIPT_PATH = "DEPRECATED_GET_VENCORD_PRELOAD_SCRIPT_PATH",
|
||||
GET_VENCORD_RENDERER_SCRIPT = "VCD_GET_VC_RENDERER_SCRIPT",
|
||||
GET_RENDERER_SCRIPT = "VCD_GET_RENDERER_SCRIPT",
|
||||
GET_RENDERER_CSS_FILE = "VCD_GET_RENDERER_CSS_FILE",
|
||||
|
||||
GET_VESKTOP_RENDERER_SCRIPT = "VCD_GET_RENDERER_SCRIPT",
|
||||
GET_VESKTOP_RENDERER_CSS = "VCD_GET_RENDERER_CSS",
|
||||
VESKTOP_RENDERER_CSS_UPDATE = "VCD_PRELOAD_RENDERER_CSS_UPDATE",
|
||||
|
||||
GET_VERSION = "VCD_GET_VERSION",
|
||||
SUPPORTS_WINDOWS_TRANSPARENCY = "VCD_SUPPORTS_WINDOWS_TRANSPARENCY",
|
||||
@@ -27,9 +30,8 @@ export const enum IpcEvents {
|
||||
GET_VENCORD_DIR = "VCD_GET_VENCORD_DIR",
|
||||
SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR",
|
||||
|
||||
UPDATER_GET_DATA = "VCD_UPDATER_GET_DATA",
|
||||
UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD",
|
||||
UPDATE_IGNORE = "VCD_UPDATE_IGNORE",
|
||||
UPDATER_IS_OUTDATED = "VCD_UPDATER_IS_OUTDATED",
|
||||
UPDATER_OPEN = "VCD_UPDATER_OPEN",
|
||||
|
||||
SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES",
|
||||
SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT",
|
||||
@@ -37,6 +39,7 @@ export const enum IpcEvents {
|
||||
SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY",
|
||||
|
||||
SET_BADGE_COUNT = "VCD_SET_BADGE_COUNT",
|
||||
FLASH_FRAME = "FLASH_FRAME",
|
||||
|
||||
CAPTURER_GET_LARGE_THUMBNAIL = "VCD_CAPTURER_GET_LARGE_THUMBNAIL",
|
||||
|
||||
@@ -57,7 +60,18 @@ export const enum IpcEvents {
|
||||
IPC_COMMAND = "VCD_IPC_COMMAND",
|
||||
|
||||
DEVTOOLS_OPENED = "VCD_DEVTOOLS_OPENED",
|
||||
DEVTOOLS_CLOSED = "VCD_DEVTOOLS_CLOSED"
|
||||
DEVTOOLS_CLOSED = "VCD_DEVTOOLS_CLOSED",
|
||||
|
||||
CHOOSE_USER_ASSET = "VCD_CHOOSE_USER_ASSET"
|
||||
}
|
||||
|
||||
export const enum UpdaterIpcEvents {
|
||||
GET_DATA = "VCD_UPDATER_GET_DATA",
|
||||
INSTALL = "VCD_UPDATER_INSTALL",
|
||||
DOWNLOAD_PROGRESS = "VCD_UPDATER_DOWNLOAD_PROGRESS",
|
||||
ERROR = "VCD_UPDATER_ERROR",
|
||||
SNOOZE_UPDATE = "VCD_UPDATER_SNOOZE_UPDATE",
|
||||
IGNORE_UPDATE = "VCD_UPDATER_IGNORE_UPDATE"
|
||||
}
|
||||
|
||||
export const enum IpcCommands {
|
||||
|
||||
@@ -7,6 +7,4 @@
|
||||
import { join } from "path";
|
||||
|
||||
export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static");
|
||||
export const VIEW_DIR = /* @__PURE__ */ join(STATIC_DIR, "views");
|
||||
export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges");
|
||||
export const ICON_PATH = /* @__PURE__ */ join(STATIC_DIR, "icon.png");
|
||||
|
||||
10
src/shared/settings.d.ts
vendored
@@ -11,6 +11,7 @@ export interface Settings {
|
||||
transparencyOption?: "none" | "mica" | "tabbed" | "acrylic";
|
||||
tray?: boolean;
|
||||
minimizeToTray?: boolean;
|
||||
autoStartMinimized?: boolean;
|
||||
openLinksWithElectron?: boolean;
|
||||
staticTitle?: boolean;
|
||||
enableMenu?: boolean;
|
||||
@@ -19,6 +20,7 @@ export interface Settings {
|
||||
hardwareVideoAcceleration?: boolean;
|
||||
arRPC?: boolean;
|
||||
appBadge?: boolean;
|
||||
enableTaskbarFlashing?: boolean;
|
||||
disableMinSize?: boolean;
|
||||
clickTrayToShowHide?: boolean;
|
||||
customTitleBar?: boolean;
|
||||
@@ -27,6 +29,7 @@ export interface Settings {
|
||||
splashTheming?: boolean;
|
||||
splashColor?: string;
|
||||
splashBackground?: string;
|
||||
splashPixelated?: boolean;
|
||||
|
||||
spellCheckLanguages?: string[];
|
||||
|
||||
@@ -49,11 +52,16 @@ export interface State {
|
||||
maximized?: boolean;
|
||||
minimized?: boolean;
|
||||
windowBounds?: Rectangle;
|
||||
displayId: int;
|
||||
|
||||
firstLaunch?: boolean;
|
||||
|
||||
steamOSLayoutVersion?: number;
|
||||
linuxAutoStartEnabled?: boolean;
|
||||
|
||||
vencordDir?: string;
|
||||
|
||||
updater?: {
|
||||
ignoredVersion?: string;
|
||||
snoozeUntil?: number;
|
||||
};
|
||||
}
|
||||
|
||||
12
src/shared/utils/millis.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2025 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export const enum Millis {
|
||||
SECOND = 1000,
|
||||
MINUTE = 60 * SECOND,
|
||||
HOUR = 60 * MINUTE,
|
||||
DAY = 24 * HOUR
|
||||
}
|
||||
BIN
static/icon.ico
|
Before Width: | Height: | Size: 12 KiB |
BIN
static/icon.png
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 16 KiB |
BIN
static/splash.webp
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
static/tray.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/tray/tray.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/tray/trayTemplate.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
static/tray/trayUnread.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |