Compare commits
298 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad306a4b5 | ||
|
|
a52ee1dbb6 | ||
|
|
524f8ff277 | ||
|
|
83104e3625 | ||
|
|
fcb61c8f42 | ||
|
|
48e9aea47e | ||
|
|
6e7d912b95 | ||
|
|
475012cbed | ||
|
|
382dac96f7 | ||
|
|
3295a7d344 | ||
|
|
bb3cec0d13 | ||
|
|
1a673f2318 | ||
|
|
d9a7e81f71 | ||
|
|
73848311b1 | ||
|
|
857779e77b | ||
|
|
fa627b384f | ||
|
|
43a8781492 | ||
|
|
b17fef93ba | ||
|
|
fc16fc5404 | ||
|
|
e760e58ed7 | ||
|
|
3936a0a41e | ||
|
|
c7d830c57c | ||
|
|
7bf05bd907 | ||
|
|
ae20445301 | ||
|
|
33d1ac43e3 | ||
|
|
da51ebb0a7 | ||
|
|
d0399cbde4 | ||
|
|
49784bc1aa | ||
|
|
43f59cefda | ||
|
|
c42c1b7bbd | ||
|
|
765ffc0b57 | ||
|
|
3c87c89b3a | ||
|
|
391ad94b85 | ||
|
|
8f94196646 | ||
|
|
878184cab1 | ||
|
|
156ba6ab7b | ||
|
|
fd91a23188 | ||
|
|
9ca9e78da7 | ||
|
|
a1d5e4dcdc | ||
|
|
da5d0f8f19 | ||
|
|
d7bc56660b | ||
|
|
d39c54b3ee | ||
|
|
670c62267e | ||
|
|
8938fe27b2 | ||
|
|
e6c1a03c59 | ||
|
|
68930a1f50 | ||
|
|
3b76c30db2 | ||
|
|
523f657b3b | ||
|
|
d75ab4af1c | ||
|
|
7e33780743 | ||
|
|
9905592b24 | ||
|
|
6e1913cafc | ||
|
|
b620e07445 | ||
|
|
9b0503f49d | ||
|
|
c0b79e6e93 | ||
|
|
57cae6f9f1 | ||
|
|
2e72fa6589 | ||
|
|
4ee57da6f3 | ||
|
|
7560727372 | ||
|
|
6ba562c663 | ||
|
|
872b60be1c | ||
|
|
8cd80f4af1 | ||
|
|
7b5e1ed4da | ||
|
|
56442ae1e9 | ||
|
|
c9be618164 | ||
|
|
eddbe27c4d | ||
|
|
00fb658355 | ||
|
|
67a1847cea | ||
|
|
030ffca499 | ||
|
|
53913c07bf | ||
|
|
f6e29231f5 | ||
|
|
7a19c81964 | ||
|
|
5a72491ab0 | ||
|
|
6c4ecc0d64 | ||
|
|
524fc8bc0a | ||
|
|
5b6c1c6d81 | ||
|
|
852410a43b | ||
|
|
5d675efb64 | ||
|
|
03c7ad4cc0 | ||
|
|
135a369848 | ||
|
|
8993b0d520 | ||
|
|
ccff1ac3ef | ||
|
|
062b536617 | ||
|
|
d008f90399 | ||
|
|
4fdf43ea6a | ||
|
|
b94379f5bd | ||
|
|
37db07807a | ||
|
|
4274647c81 | ||
|
|
24fbf35542 | ||
|
|
c8eccc7e9d | ||
|
|
a318f6b407 | ||
|
|
75354ad8e6 | ||
|
|
af9ed58eef | ||
|
|
e0453418bd | ||
|
|
22344512ad | ||
|
|
9acc6652ff | ||
|
|
61bbd7f6aa | ||
|
|
f31f06f5c4 | ||
|
|
786fe131b8 | ||
|
|
35d6f19e80 | ||
|
|
8798bbce44 | ||
|
|
485eb8eaef | ||
|
|
1e39ec7d39 | ||
|
|
5fa4ae9824 | ||
|
|
281885a87e | ||
|
|
021aca4089 | ||
|
|
f341c5e5bb | ||
|
|
0d203ac21d | ||
|
|
1f12d270ec | ||
|
|
ec3d83f7ca | ||
|
|
05014f747a | ||
|
|
9c44da0cae | ||
|
|
5d5b38b259 | ||
|
|
9e3d83a2ee | ||
|
|
0e49e5e8f6 | ||
|
|
456d5a61cd | ||
|
|
ebd4e6b28e | ||
|
|
3fe2094814 | ||
|
|
9554902704 | ||
|
|
da7f13288f | ||
|
|
1a4d173bb4 | ||
|
|
abd1e8c6da | ||
|
|
7fe79b629e | ||
|
|
d3e8dfa724 | ||
|
|
3772db9eea | ||
|
|
b6c3c8024b | ||
|
|
6b948668b9 | ||
|
|
f232cfc3c8 | ||
|
|
bca8872db1 | ||
|
|
463c42330e | ||
|
|
d3b94fc4df | ||
|
|
a8d72fa665 | ||
|
|
69f14ee611 | ||
|
|
944a699e83 | ||
|
|
d11a9a04ff | ||
|
|
56b96bded9 | ||
|
|
ac35f81476 | ||
|
|
0623a71271 | ||
|
|
a66af353f7 | ||
|
|
cb55cf4b74 | ||
|
|
e9da30e420 | ||
|
|
c52abdc1ee | ||
|
|
55ca2f3091 | ||
|
|
0beb74fad0 | ||
|
|
2733727a40 | ||
|
|
ab9e8579ee | ||
|
|
617ef0fa19 | ||
|
|
2649598361 | ||
|
|
8eaa5206b9 | ||
|
|
df05d12fb2 | ||
|
|
8c6941b8e9 | ||
|
|
6ed59332d1 | ||
|
|
4abae9c708 | ||
|
|
b09f035ea9 | ||
|
|
44627b4cd4 | ||
|
|
97267ef89a | ||
|
|
132adcb733 | ||
|
|
738fa588a6 | ||
|
|
e63cff7a52 | ||
|
|
777a0e83fe | ||
|
|
612d35c96f | ||
|
|
0acd3e160a | ||
|
|
6993b2a7d4 | ||
|
|
2bd8ca96df | ||
|
|
4d82a6f41d | ||
|
|
cb33f1834b | ||
|
|
fb40f4b42d | ||
|
|
808eb56327 | ||
|
|
b636b65e55 | ||
|
|
7d30dcdb47 | ||
|
|
4f1615ecb3 | ||
|
|
463cd6dc46 | ||
|
|
8c007476c3 | ||
|
|
b20c77734c | ||
|
|
79cd2791f6 | ||
|
|
73de0fa535 | ||
|
|
83e74b98d5 | ||
|
|
2e4c834a90 | ||
|
|
2aa0b0fa20 | ||
|
|
3ac0ed3d78 | ||
|
|
eddc1de784 | ||
|
|
6483b3a3d9 | ||
|
|
38f0330eb2 | ||
|
|
15a49a31e1 | ||
|
|
33eb1af2be | ||
|
|
563ba1eebe | ||
|
|
62cf02e7b1 | ||
|
|
0f0bddbef9 | ||
|
|
0881143d57 | ||
|
|
effd950b2d | ||
|
|
4074e8d6ac | ||
|
|
779c8fa516 | ||
|
|
b059516707 | ||
|
|
dfa9d248d3 | ||
|
|
18925ad583 | ||
|
|
0b01732293 | ||
|
|
a89cd9d2ba | ||
|
|
2f35128acf | ||
|
|
7565eb39a4 | ||
|
|
d71d0c1cc4 | ||
|
|
dfc6970756 | ||
|
|
1429815fd1 | ||
|
|
aa397d003c | ||
|
|
40b952d8bf | ||
|
|
4974848a56 | ||
|
|
623fa5d709 | ||
|
|
fc33050496 | ||
|
|
e02acda6fa | ||
|
|
f11a72d4b5 | ||
|
|
49cd558fa1 | ||
|
|
378a1f7464 | ||
|
|
35d2dd6505 | ||
|
|
017c2c847f | ||
|
|
de2b4b7dd8 | ||
|
|
5dc5741771 | ||
|
|
971c490c41 | ||
|
|
0f804f9f6e | ||
|
|
fdf454722f | ||
|
|
208158f4d1 | ||
|
|
032b94e81d | ||
|
|
4988163744 | ||
|
|
ee7e71b9fb | ||
|
|
7efa54687a | ||
|
|
2917ff25e5 | ||
|
|
e3839c35b7 | ||
|
|
c39678d733 | ||
|
|
06dea79c74 | ||
|
|
96b0652a06 | ||
|
|
49e0411be6 | ||
|
|
94819e6f16 | ||
|
|
a232af06ed | ||
|
|
b24535483e | ||
|
|
9521c287b6 | ||
|
|
55b9edec39 | ||
|
|
1e9c70eed9 | ||
|
|
3262e083fa | ||
|
|
ec1c719553 | ||
|
|
cc62903b9c | ||
|
|
b17370cc7b | ||
|
|
886d02f7c3 | ||
|
|
0cad71f6ae | ||
|
|
b5eac15b42 | ||
|
|
19c3112d52 | ||
|
|
1c308d0e2c | ||
|
|
7f6db5eeda | ||
|
|
10b38e5b41 | ||
|
|
28282d1d76 | ||
|
|
e1512b72f4 | ||
|
|
46a2314173 | ||
|
|
e6dc026708 | ||
|
|
cac307d1fc | ||
|
|
4b8f374856 | ||
|
|
e6cc11fc0e | ||
|
|
43ca479fc8 | ||
|
|
7b0f64a9fc | ||
|
|
573a953a2f | ||
|
|
841cdcf672 | ||
|
|
0d93e08e99 | ||
|
|
c445c6194f | ||
|
|
e29d293855 | ||
|
|
e13a4eacb1 | ||
|
|
c070761f9d | ||
|
|
ae86c28247 | ||
|
|
9c95956c96 | ||
|
|
45f56c63a0 | ||
|
|
89af4316d3 | ||
|
|
a9bfb857ae | ||
|
|
b9e411ac90 | ||
|
|
670de01938 | ||
|
|
ef064eba3d | ||
|
|
d5f63da939 | ||
|
|
2aadc61af9 | ||
|
|
061fec44af | ||
|
|
b876f450c3 | ||
|
|
5f5febda9d | ||
|
|
94ba59afb5 | ||
|
|
566737017c | ||
|
|
196ee4e42c | ||
|
|
9bb02f8581 | ||
|
|
c76d7195a5 | ||
|
|
50a103710d | ||
|
|
e6e66e775c | ||
|
|
a5ec031a2f | ||
|
|
4dceadbbd2 | ||
|
|
6ee920ff2c | ||
|
|
28ad4a6f73 | ||
|
|
c5ac3e64a6 | ||
|
|
6dc26aea6a | ||
|
|
9003b94f85 | ||
|
|
f7b7931847 | ||
|
|
b87bcaefe9 | ||
|
|
3108de7c79 | ||
|
|
57006e7e52 | ||
|
|
8f1ea1f440 | ||
|
|
26b6fb13d4 | ||
|
|
e1971f55a0 | ||
|
|
ba2618878e | ||
|
|
87595deae7 |
@@ -4,3 +4,5 @@
|
||||
# all permissions at the defaults (public repos read only, 0 permissions):
|
||||
# https://github.com/settings/personal-access-tokens/new
|
||||
GITHUB_TOKEN=
|
||||
|
||||
ELECTRON_LAUNCH_FLAGS="--enable-source-maps --ozone-platform-hint=auto"
|
||||
@@ -1,63 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ignorePatterns": ["dist", "node_modules"],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"license-header",
|
||||
"simple-import-sort",
|
||||
"unused-imports",
|
||||
"path-alias",
|
||||
"prettier"
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"alias": {
|
||||
"map": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"license-header/header": ["error", "scripts/header.txt"],
|
||||
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
||||
"spaced-comment": ["error", "always", { "markers": ["!"] }],
|
||||
"yoda": "error",
|
||||
"prefer-destructuring": ["error", { "object": true, "array": false }],
|
||||
"operator-assignment": ["error", "always"],
|
||||
"no-useless-computed-key": "error",
|
||||
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
|
||||
"no-invalid-regexp": "error",
|
||||
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-extra-semi": "error",
|
||||
"dot-notation": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-fallthrough": "error",
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-misleading-character-class": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"use-isnan": "error",
|
||||
"prefer-const": "error",
|
||||
"prefer-spread": "error",
|
||||
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
|
||||
"path-alias/no-relative": "error",
|
||||
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Vencord Support Server
|
||||
url: https://discord.gg/D9uwnFnqmd
|
||||
about: "Need Help? Join our support server and ask in the #vesktop-support channel!"
|
||||
15
.github/ISSUE_TEMPLATE/dev-issue.yml
vendored
Normal file
15
.github/ISSUE_TEMPLATE/dev-issue.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Vesktop Developer Issue
|
||||
description: Reserved for Vesktop Developers. Join our support server for support.
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||

|
||||
GitHub Issues are for developers, not support. Please use our [support server](https://vencord.dev/discord) if you are not a developer.
|
||||
- type: textarea
|
||||
id: content
|
||||
attributes:
|
||||
label: Content
|
||||
validations:
|
||||
required: true
|
||||
BIN
.github/ISSUE_TEMPLATE/developer-banner.png
vendored
Normal file
BIN
.github/ISSUE_TEMPLATE/developer-banner.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
42
.github/workflows/meta.yml
vendored
Normal file
42
.github/workflows/meta.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Update metainfo on release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Update metainfo
|
||||
run: pnpm updateMeta
|
||||
|
||||
- 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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
47
.github/workflows/release.yml
vendored
47
.github/workflows/release.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -12,17 +13,47 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
include:
|
||||
- os: macos-latest
|
||||
platform: mac
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
- os: windows-latest
|
||||
platform: windows
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Run Electron Builder
|
||||
uses: samuelmeuli/action-electron-builder@e4b12cd06ddf023422f1ac4e39632bd76f6e6928
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE: true
|
||||
if: ${{ matrix.platform != 'mac' }}
|
||||
run: |
|
||||
pnpm electron-builder --${{ matrix.platform }} --publish always
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run Electron Builder
|
||||
if: ${{ matrix.platform == 'mac' }}
|
||||
run: |
|
||||
echo "$API_KEY" > apple.p8
|
||||
pnpm electron-builder --${{ matrix.platform }} --publish always
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CSC_LINK: ${{ secrets.APPLE_SIGNING_CERT }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_SIGNING_CERT_PASSWORD }}
|
||||
API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY: apple.p8
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -11,13 +11,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
29
.github/workflows/update-vencord-dev.yml
vendored
Normal file
29
.github/workflows/update-vencord-dev.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Update vencord.dev Vesktop version
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update scripts/_latestVesktopVersion.txt file in vencord.dev repo
|
||||
run: |
|
||||
git config --global user.name "$USERNAME"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git repo
|
||||
|
||||
cd repo
|
||||
version="${{ github.event.release.tag_name }}"
|
||||
echo "${version#v}" > scripts/_latestVesktopVersion.txt
|
||||
|
||||
git add scripts/_latestVesktopVersion.txt
|
||||
git commit -m "Update Vesktop version to ${{ github.event.release.tag_name }}"
|
||||
git push https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
|
||||
env:
|
||||
API_TOKEN: ${{ secrets.VENCORD_DEV_GITHUB_TOKEN }}
|
||||
GH_REPO: Vencord/vencord.dev
|
||||
USERNAME: GitHub-Actions
|
||||
25
.github/workflows/winget-submission.yml
vendored
Normal file
25
.github/workflows/winget-submission.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Submit to Winget Community Repo
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
type: string
|
||||
description: The release tag to submit
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
winget:
|
||||
name: Publish winget package
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Submit package to Winget Community Repo
|
||||
uses: vedantmgoyal2009/winget-releaser@0db4f0a478166abd0fa438c631849f0b8dcfb99f
|
||||
with:
|
||||
identifier: Vencord.Vesktop
|
||||
token: ${{ secrets.WINGET_PAT }}
|
||||
installers-regex: '\.exe$'
|
||||
release-tag: ${{ inputs.tag || github.event.release.tag_name }}
|
||||
fork-user: shiggybot
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
dist
|
||||
node_modules
|
||||
.env
|
||||
.DS_Store
|
||||
.idea/
|
||||
.pnpm-store/
|
||||
50
.vscode/settings.json
vendored
50
.vscode/settings.json
vendored
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"cSpell.words": ["Vesktop"]
|
||||
}
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"cSpell.words": ["Vesktop"]
|
||||
}
|
||||
|
||||
78
README.md
78
README.md
@@ -1,47 +1,73 @@
|
||||
# Vesktop
|
||||
|
||||
Vesktop is a cross platform desktop app aiming to give you a snappier Discord experience with Vencord pre-installed
|
||||
Vesktop is a custom Discord desktop app
|
||||
|
||||
**Main features**:
|
||||
- Vencord preinstalled
|
||||
- Much more lightweight and faster than the official Discord app
|
||||
- Linux Screenshare with sound & wayland
|
||||
- Much better privacy, since Discord has no access to your system
|
||||
|
||||
**Not yet supported**:
|
||||
- Global Keybinds
|
||||
- see the [Roadmap](https://github.com/Vencord/Vesktop/issues/324)
|
||||
|
||||
Bug reports, feature requests & contributions are highly appreciated!!
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Installing
|
||||
|
||||
### Windows
|
||||
|
||||
Download and run Vesktop-Setup-VERSION.exe from [releases](https://github.com/Vencord/Vesktop/releases/latest)
|
||||
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 and run Vesktop-VERSION.dmg from [releases](https://github.com/Vencord/Vesktop/releases/latest)
|
||||
Download the latest [Vesktop.dmg](https://vencord.dev/download/vesktop/universal/dmg) or use [Homebrew](https://brew.sh/)
|
||||
|
||||
```sh
|
||||
brew install vesktop
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
#### Arch based
|
||||
[](https://flathub.org/apps/dev.vencord.Vesktop)
|
||||
|
||||
Install [vencord-desktop-git](https://aur.archlinux.org/packages/vencord-desktop-git) from the AUR using your favourite AUR helper, for example [yay](https://github.com/Jguer/yay)
|
||||
If you don't know the difference, pick amd64.
|
||||
|
||||
#### Ubuntu/Debian based
|
||||
- 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)
|
||||
|
||||
Download Vesktop-VERSION.deb from [releases](https://github.com/Vencord/Vesktop/releases/latest)
|
||||
#### Community packages
|
||||
|
||||
#### Fedora/RHEL based
|
||||
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!
|
||||
|
||||
Download Vesktop-VERSION.rpm from [releases](https://github.com/Vencord/Vesktop/releases/latest)
|
||||
- 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
|
||||
|
||||
#### Other
|
||||
## Building from Source
|
||||
|
||||
Either download Vesktop-VERSION.AppImage and just run it directly or grab Vesktop-VERSION.tar.gz, extract it somewhere and run `vencorddesktop`.
|
||||
You need to have the following dependencies installed:
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
- pnpm: `npm install --global pnpm`
|
||||
|
||||
A flatpak is planned, if you want packages for other repos, feel free to create them and they can be linked as unofficial here
|
||||
|
||||
## Building
|
||||
|
||||
Packaging will create builds in the dist/ folder. You can then install them like mentioned above or distribute them
|
||||
Packaging will create builds in the dist/ folder
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Vencord/Vesktop
|
||||
@@ -53,18 +79,12 @@ pnpm i
|
||||
# Either run it without packaging
|
||||
pnpm start
|
||||
|
||||
# Or package
|
||||
# Or package (will build packages for your OS)
|
||||
pnpm package
|
||||
# Or only build the pacman target
|
||||
|
||||
# Or only build the Linux Pacman package
|
||||
pnpm package --linux pacman
|
||||
|
||||
# Or package to a directory only
|
||||
pnpm package:dir
|
||||
```
|
||||
|
||||
## Motivation
|
||||
|
||||
The official Discord Desktop app is very resource heavy compared to Discord in your Browser. There are multiple alternative Electron apps (ArmCord, WebCord, probably more) that prove how much of a performance gain you can gain by using a custom app. ArmCord already supports Vencord but makes it pretty limited for us. Making our own standalone app gives us much more control.
|
||||
|
||||
This is just a random idea I (V) got, and might not actually ever be finished heh
|
||||
|
||||
Gluon also seems very attractive for this because of how lightweight it can be and because unlike electron, streaming just works out of the box like in any chromium browser. However, at the time of writing this, it still lacks some features necessary to make it work (synchronous ipc or a way to get node process variables into the onLoad function for instance, plus onLoad seems to load a little too late sometimes)
|
||||
|
||||
BIN
build/background.tiff
Normal file
BIN
build/background.tiff
Normal file
Binary file not shown.
21
build/entitlements.mac.plist
Normal file
21
build/entitlements.mac.plist
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.bluetooth</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,8 +1,8 @@
|
||||
!macro preInit
|
||||
SetRegView 64
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop"
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop"
|
||||
SetRegView 32
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop"
|
||||
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop"
|
||||
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop"
|
||||
!macroend
|
||||
|
||||
103
eslint.config.mjs
Normal file
103
eslint.config.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
import pathAlias from "eslint-plugin-path-alias";
|
||||
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(
|
||||
{ ignores: ["dist"] },
|
||||
|
||||
{
|
||||
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}"],
|
||||
plugins: {
|
||||
simpleHeader,
|
||||
stylistic,
|
||||
importSort,
|
||||
unusedImports,
|
||||
pathAlias,
|
||||
prettier
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
alias: {
|
||||
map: []
|
||||
}
|
||||
}
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"simpleHeader/header": [
|
||||
"error",
|
||||
{
|
||||
files: ["scripts/header.txt"],
|
||||
templates: { author: [".*", "Vendicated and Vesktop contributors"] }
|
||||
}
|
||||
],
|
||||
|
||||
// ESLint Rules
|
||||
|
||||
yoda: "error",
|
||||
eqeqeq: ["error", "always", { null: "ignore" }],
|
||||
"prefer-destructuring": [
|
||||
"error",
|
||||
{
|
||||
VariableDeclarator: { array: false, object: true },
|
||||
AssignmentExpression: { array: false, object: false }
|
||||
}
|
||||
],
|
||||
"operator-assignment": ["error", "always"],
|
||||
"no-useless-computed-key": "error",
|
||||
"no-unneeded-ternary": ["error", { defaultAssignment: false }],
|
||||
"no-invalid-regexp": "error",
|
||||
"no-constant-condition": ["error", { checkLoops: false }],
|
||||
"no-duplicate-imports": "error",
|
||||
"dot-notation": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-fallthrough": "error",
|
||||
"for-direction": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-misleading-character-class": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"use-isnan": "error",
|
||||
"prefer-const": "error",
|
||||
"prefer-spread": "error",
|
||||
|
||||
// Styling Rules
|
||||
"stylistic/spaced-comment": ["error", "always", { markers: ["!"] }],
|
||||
"stylistic/no-extra-semi": "error",
|
||||
|
||||
// Plugin Rules
|
||||
"importSort/imports": "error",
|
||||
"importSort/exports": "error",
|
||||
"unusedImports/no-unused-imports": "error",
|
||||
"pathAlias/no-relative": "error",
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
||||
);
|
||||
281
meta/dev.vencord.Vesktop.metainfo.xml
Normal file
281
meta/dev.vencord.Vesktop.metainfo.xml
Normal file
@@ -0,0 +1,281 @@
|
||||
<?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.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>
|
||||
188
package.json
188
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "VencordDesktop",
|
||||
"version": "0.2.8",
|
||||
"name": "vesktop",
|
||||
"version": "1.5.7",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"description": "Vesktop is a custom Discord desktop app",
|
||||
"keywords": [],
|
||||
"homepage": "https://vencord.dev/",
|
||||
"license": "GPL-3.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "Vendicated <vendicated@riseup.net>",
|
||||
"main": "dist/js/main.js",
|
||||
"scripts": {
|
||||
@@ -13,73 +13,117 @@
|
||||
"build:dev": "pnpm build --dev",
|
||||
"package": "pnpm build && electron-builder",
|
||||
"package:dir": "pnpm build && electron-builder --dir",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.mts,.mjs",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"start": "pnpm build && electron .",
|
||||
"start:dev": "pnpm build:dev && electron .",
|
||||
"start:watch": "pnpm build:dev && tsx scripts/startWatch.mts",
|
||||
"test": "pnpm lint && pnpm testTypes",
|
||||
"testTypes": "tsc --noEmit",
|
||||
"watch": "pnpm build --watch"
|
||||
"watch": "pnpm build --watch",
|
||||
"updateMeta": "tsx scripts/utils/updateMeta.mts",
|
||||
"updateArrpcDB": "node ./node_modules/arrpc/update_db.js",
|
||||
"postinstall": "pnpm updateArrpcDB"
|
||||
},
|
||||
"dependencies": {
|
||||
"arrpc": "github:OpenAsar/arrpc#bfcba7e3d6e6f7301a5699c4a1eb10c968e7b568"
|
||||
"arrpc": "github:OpenAsar/arrpc#2234e9c9111f4c42ebcc3aa6a2215bfd979eef77",
|
||||
"electron-updater": "^6.6.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@vencord/venmic": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
|
||||
"@types/node": "^20.4.6",
|
||||
"@types/react": "^18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
"@typescript-eslint/parser": "^6.2.1",
|
||||
"@vencord/types": "^0.1.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"electron": "^25.4.0",
|
||||
"electron-builder": "^24.6.3",
|
||||
"esbuild": "^0.18.17",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-prettier": "^8.9.0",
|
||||
"@stylistic/eslint-plugin": "^4.4.1",
|
||||
"@types/node": "^22.15.30",
|
||||
"@types/react": "18.3.1",
|
||||
"@vencord/types": "^1.11.5",
|
||||
"dotenv": "^16.5.0",
|
||||
"electron": "^36.4.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"esbuild": "^0.25.5",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-license-header": "^0.6.0",
|
||||
"eslint-plugin-path-alias": "^1.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"eslint-plugin-path-alias": "^2.1.0",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
"eslint-plugin-simple-header": "^1.2.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"prettier": "^3.5.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tsx": "^3.12.7",
|
||||
"type-fest": "^4.1.0",
|
||||
"typescript": "^5.1.6"
|
||||
"tsx": "^4.19.4",
|
||||
"type-fest": "^4.41.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"xml-formatter": "^3.6.6"
|
||||
},
|
||||
"packageManager": "pnpm@8.1.1",
|
||||
"packageManager": "pnpm@10.7.1",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"build": {
|
||||
"appId": "dev.vencord.desktop",
|
||||
"appId": "dev.vencord.vesktop",
|
||||
"productName": "Vesktop",
|
||||
"files": [
|
||||
"!*",
|
||||
"!node_modules",
|
||||
"dist/js",
|
||||
"static",
|
||||
"package.json",
|
||||
"LICENSE"
|
||||
],
|
||||
"protocols": {
|
||||
"name": "Discord",
|
||||
"schemes": [
|
||||
"discord"
|
||||
]
|
||||
},
|
||||
"beforePack": "scripts/build/sandboxFix.js",
|
||||
"linux": {
|
||||
"icon": "build/icon.icns",
|
||||
"category": "Network",
|
||||
"maintainer": "vendicated+vesktop@riseup.net",
|
||||
"target": [
|
||||
"deb",
|
||||
"tar.gz",
|
||||
"rpm",
|
||||
"AppImage"
|
||||
{
|
||||
"target": "deb",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "tar.gz",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "rpm",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"desktop": {
|
||||
"Name": "Vesktop",
|
||||
"GenericName": "Internet Messenger",
|
||||
"Type": "Application",
|
||||
"Categories": "Network;InstantMessaging;Chat;",
|
||||
"Keywords": "discord;vencord;electron;chat;"
|
||||
"entry": {
|
||||
"Name": "Vesktop",
|
||||
"GenericName": "Internet Messenger",
|
||||
"Type": "Application",
|
||||
"Categories": "Network;InstantMessaging;Chat;",
|
||||
"Keywords": "discord;vencord;electron;chat;",
|
||||
"MimeType": "x-scheme-handler/discord",
|
||||
"StartupWMClass": "vesktop"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mac": {
|
||||
@@ -87,12 +131,40 @@
|
||||
{
|
||||
"target": "default",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
"universal"
|
||||
]
|
||||
}
|
||||
],
|
||||
"category": "Network"
|
||||
"category": "public.app-category.social-networking",
|
||||
"darkModeSupport": true,
|
||||
"extendInfo": {
|
||||
"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
|
||||
},
|
||||
"notarize": true
|
||||
},
|
||||
"dmg": {
|
||||
"background": "build/background.tiff",
|
||||
"icon": "build/icon.icns",
|
||||
"iconSize": 105,
|
||||
"window": {
|
||||
"width": 512,
|
||||
"height": 340
|
||||
},
|
||||
"contents": [
|
||||
{
|
||||
"x": 140,
|
||||
"y": 160
|
||||
},
|
||||
{
|
||||
"x": 372,
|
||||
"y": 160,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"include": "build/installer.nsh",
|
||||
@@ -100,13 +172,39 @@
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
"portable"
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"publish": {
|
||||
"provider": "github",
|
||||
"releaseType": "release"
|
||||
"provider": "github"
|
||||
},
|
||||
"rpm": {
|
||||
"fpm": [
|
||||
"--rpm-rpmbuild-define=_build_id_links none"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"arrpc@3.5.0": "patches/arrpc@3.5.0.patch"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@vencord/venmic",
|
||||
"electron",
|
||||
"esbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
27
patches/arrpc@3.5.0.patch
Normal file
27
patches/arrpc@3.5.0.patch
Normal file
@@ -0,0 +1,27 @@
|
||||
diff --git a/src/process/index.js b/src/process/index.js
|
||||
index 389b0845256a34b4536d6da99edb00d17f13a6b4..f17a0ac687e9110ebfd33cb91fd2f6250d318643 100644
|
||||
--- a/src/process/index.js
|
||||
+++ b/src/process/index.js
|
||||
@@ -5,8 +5,20 @@ import fs from 'node:fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
-const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
-const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8'));
|
||||
+const DetectableDB = require('./detectable.json');
|
||||
+DetectableDB.push(
|
||||
+ {
|
||||
+ aliases: ["Obs"],
|
||||
+ executables: [
|
||||
+ { is_launcher: false, name: "obs", os: "linux" },
|
||||
+ { is_launcher: false, name: "obs.exe", os: "win32" },
|
||||
+ { is_launcher: false, name: "obs.app", os: "darwin" }
|
||||
+ ],
|
||||
+ hook: true,
|
||||
+ id: "STREAMERMODE",
|
||||
+ name: "OBS"
|
||||
+ }
|
||||
+);
|
||||
|
||||
import * as Natives from './native/index.js';
|
||||
const Native = Natives[process.platform];
|
||||
8216
pnpm-lock.yaml
generated
8216
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,14 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { BuildContext, BuildOptions, context } from "esbuild";
|
||||
import { copyFile } from "fs/promises";
|
||||
|
||||
import vencordDep from "./vencordDep.mjs";
|
||||
import { includeDirPlugin } from "./includeDirPlugin.mts";
|
||||
|
||||
const isDev = process.argv.includes("--dev");
|
||||
|
||||
@@ -33,7 +35,23 @@ async function createContext(options: BuildOptions) {
|
||||
contexts.push(await context(options));
|
||||
}
|
||||
|
||||
async function copyVenmic() {
|
||||
if (process.platform !== "linux") return;
|
||||
|
||||
return Promise.all([
|
||||
copyFile(
|
||||
"./node_modules/@vencord/venmic/prebuilds/venmic-addon-linux-x64/node-napi-v7.node",
|
||||
"./static/dist/venmic-x64.node"
|
||||
),
|
||||
copyFile(
|
||||
"./node_modules/@vencord/venmic/prebuilds/venmic-addon-linux-arm64/node-napi-v7.node",
|
||||
"./static/dist/venmic-arm64.node"
|
||||
)
|
||||
]).catch(() => console.warn("Failed to copy venmic. Building without venmic support"));
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
copyVenmic(),
|
||||
createContext({
|
||||
...NodeCommonOpts,
|
||||
entryPoints: ["src/main/index.ts"],
|
||||
@@ -48,9 +66,8 @@ await Promise.all([
|
||||
}),
|
||||
createContext({
|
||||
...NodeCommonOpts,
|
||||
entryPoints: ["src/updater/preload.ts"],
|
||||
outfile: "dist/js/updaterPreload.js",
|
||||
footer: { js: "//# sourceURL=VCDUpdaterPreload" }
|
||||
entryPoints: ["src/preload/splash.ts"],
|
||||
outfile: "dist/js/splashPreload.js"
|
||||
}),
|
||||
createContext({
|
||||
...CommonOpts,
|
||||
@@ -61,15 +78,9 @@ await Promise.all([
|
||||
inject: ["./scripts/build/injectReact.mjs"],
|
||||
jsxFactory: "VencordCreateElement",
|
||||
jsxFragment: "VencordFragment",
|
||||
// Work around https://github.com/evanw/esbuild/issues/2460
|
||||
tsconfig: "./scripts/build/tsconfig.esbuild.json",
|
||||
external: ["@vencord/types/*"],
|
||||
plugins: [vencordDep],
|
||||
// TODO: remove legacy name once main Vencord codebase has migrated and some time has passed.
|
||||
// this 0 is very important. we run this script via webFrame.executeJavaScript and the last
|
||||
// expression will be the return value. Without the 0, the return value would be Vesktop which
|
||||
// leads to "An object could not be cloned"
|
||||
footer: { js: ";window.VencordDesktop=Vesktop;0 \n//# sourceURL=VCDRenderer" }
|
||||
plugins: [vencordDep, includeDirPlugin("patches", "src/renderer/patches")],
|
||||
footer: { js: "//# sourceURL=VCDRenderer" }
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
25
scripts/build/includeDirPlugin.mts
Normal file
25
scripts/build/includeDirPlugin.mts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Plugin } from "esbuild";
|
||||
import { readdir } from "fs/promises";
|
||||
|
||||
const makeImportAllCode = (files: string[]) =>
|
||||
files.map(f => `require("./${f.replace(/\.[cm]?[tj]sx?$/, "")}")`).join("\n");
|
||||
|
||||
const makeImportDirRecursiveCode = (dir: string) => readdir(dir).then(files => makeImportAllCode(files));
|
||||
|
||||
export function includeDirPlugin(namespace: string, path: string): Plugin {
|
||||
return {
|
||||
name: `include-dir-plugin:${namespace}`,
|
||||
setup(build) {
|
||||
const filter = new RegExp(`^__${namespace}__$`);
|
||||
|
||||
build.onResolve({ filter }, args => ({ path: args.path, namespace }));
|
||||
|
||||
build.onLoad({ filter, namespace }, async args => {
|
||||
return {
|
||||
contents: await makeImportDirRecursiveCode(path),
|
||||
resolveDir: path
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,3 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
*/
|
||||
|
||||
export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
|
||||
export let VencordCreateElement = (...args) =>
|
||||
(VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);
|
||||
|
||||
74
scripts/build/sandboxFix.js
Normal file
74
scripts/build/sandboxFix.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js
|
||||
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
let isApplied = false;
|
||||
|
||||
const hook = async () => {
|
||||
if (isApplied) return;
|
||||
isApplied = true;
|
||||
if (process.platform !== "linux") {
|
||||
// this fix is only required on linux
|
||||
return;
|
||||
}
|
||||
const AppImageTarget = require("app-builder-lib/out/targets/AppImageTarget");
|
||||
const oldBuildMethod = AppImageTarget.default.prototype.build;
|
||||
AppImageTarget.default.prototype.build = async function (...args) {
|
||||
console.log("Running AppImage builder hook", args);
|
||||
const oldPath = args[0];
|
||||
const newPath = oldPath + "-appimage-sandbox-fix";
|
||||
// just in case
|
||||
try {
|
||||
await fs.rm(newPath, {
|
||||
recursive: true
|
||||
});
|
||||
} catch {}
|
||||
|
||||
console.log("Copying to apply appimage fix", oldPath, newPath);
|
||||
await fs.cp(oldPath, newPath, {
|
||||
recursive: true
|
||||
});
|
||||
args[0] = newPath;
|
||||
|
||||
const executable = path.join(newPath, this.packager.executableName);
|
||||
|
||||
const loaderScript = `
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )"
|
||||
IS_STEAMOS=0
|
||||
|
||||
if [[ "$SteamOS" == "1" && "$SteamGamepadUI" == "1" ]]; then
|
||||
echo "Running Vesktop on SteamOS, disabling sandbox"
|
||||
IS_STEAMOS=1
|
||||
fi
|
||||
|
||||
exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ] && echo '--no-sandbox')" "$@"
|
||||
`.trim();
|
||||
|
||||
try {
|
||||
await fs.rename(executable, executable + ".bin");
|
||||
await fs.writeFile(executable, loaderScript);
|
||||
await fs.chmod(executable, 0o755);
|
||||
} catch (e) {
|
||||
console.error("failed to create loder for sandbox fix: " + e.message);
|
||||
throw new Error("Failed to create loader for sandbox fix");
|
||||
}
|
||||
|
||||
const ret = await oldBuildMethod.apply(this, args);
|
||||
|
||||
await fs.rm(newPath, {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
return ret;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = hook;
|
||||
@@ -1,7 +0,0 @@
|
||||
// Work around https://github.com/evanw/esbuild/issues/2460
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { globalExternalsWithRegExp } from "@fal-works/esbuild-plugin-global-externals";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* Copyright (c) {year} {author}
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
@@ -8,4 +8,4 @@ import "./utils/dotenv";
|
||||
|
||||
import { spawnNodeModuleBin } from "./utils/spawn.mjs";
|
||||
|
||||
spawnNodeModuleBin("electron", ["."]);
|
||||
spawnNodeModuleBin("electron", [process.cwd(), ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./start";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { config } from "dotenv";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { spawn as spaaawn, SpawnOptions } from "child_process";
|
||||
|
||||
93
scripts/utils/updateMeta.mts
Normal file
93
scripts/utils/updateMeta.mts
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { promises as fs } from "node:fs";
|
||||
|
||||
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
|
||||
import xmlFormat from "xml-formatter";
|
||||
|
||||
function generateDescription(description: string, descriptionNode: Element) {
|
||||
const lines = description.replace(/\r/g, "").split("\n");
|
||||
let currentList: Element | null = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.includes("New Contributors")) {
|
||||
// we're done, don't parse any more since the new contributors section is the last one
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.startsWith("## ")) {
|
||||
const pNode = descriptionNode.ownerDocument.createElement("p");
|
||||
pNode.textContent = line.slice(3);
|
||||
descriptionNode.appendChild(pNode);
|
||||
} else if (line.startsWith("* ")) {
|
||||
const liNode = descriptionNode.ownerDocument.createElement("li");
|
||||
liNode.textContent = line.slice(2).split("in https://github.com")[0].trim(); // don't include links to github
|
||||
|
||||
if (!currentList) {
|
||||
currentList = descriptionNode.ownerDocument.createElement("ul");
|
||||
}
|
||||
|
||||
currentList.appendChild(liNode);
|
||||
}
|
||||
|
||||
if (currentList && !lines[i + 1].startsWith("* ")) {
|
||||
descriptionNode.appendChild(currentList);
|
||||
currentList = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const latestReleaseInformation = await fetch("https://api.github.com/repos/Vencord/Vesktop/releases/latest", {
|
||||
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 parser = new DOMParser().parseFromString(metaInfo, "text/xml");
|
||||
|
||||
const releaseList = parser.getElementsByTagName("releases")[0];
|
||||
|
||||
for (let i = 0; i < releaseList.childNodes.length; i++) {
|
||||
const release = releaseList.childNodes[i] as Element;
|
||||
|
||||
if (release.nodeType === 1 && release.getAttribute("version") === latestReleaseInformation.name) {
|
||||
console.log("Latest release already added, nothing to be done");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const release = parser.createElement("release");
|
||||
release.setAttribute("version", latestReleaseInformation.name);
|
||||
release.setAttribute("date", latestReleaseInformation.published_at.split("T")[0]);
|
||||
release.setAttribute("type", "stable");
|
||||
|
||||
const releaseUrl = parser.createElement("url");
|
||||
releaseUrl.textContent = latestReleaseInformation.html_url;
|
||||
|
||||
release.appendChild(releaseUrl);
|
||||
|
||||
const description = parser.createElement("description");
|
||||
|
||||
// we're not using a full markdown parser here since we don't have a lot of formatting options to begin with
|
||||
generateDescription(latestReleaseInformation.body, description);
|
||||
|
||||
release.appendChild(description);
|
||||
|
||||
releaseList.insertBefore(release, releaseList.childNodes[0]);
|
||||
|
||||
const output = xmlFormat(new XMLSerializer().serializeToString(parser), {
|
||||
lineSeparator: "\n",
|
||||
collapseContent: true,
|
||||
indentation: " "
|
||||
});
|
||||
|
||||
await fs.writeFile("./meta/dev.vencord.Vesktop.metainfo.xml", output, "utf-8");
|
||||
4
src/globals.d.ts
vendored
4
src/globals.d.ts
vendored
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
declare global {
|
||||
export var VesktopNative: typeof import("preload/VesktopNative").VesktopNative;
|
||||
export var Vesktop: typeof import("renderer/index");
|
||||
export var VCDP: any;
|
||||
export var VesktopPatchGlobals: any;
|
||||
|
||||
export var IS_DEV: boolean;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app, NativeImage, nativeImage } from "electron";
|
||||
@@ -23,11 +23,17 @@ let lastIndex: null | number = -1;
|
||||
|
||||
export function setBadgeCount(count: number) {
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
case "linux":
|
||||
if (count === -1) count = 0;
|
||||
app.setBadgeCount(count);
|
||||
break;
|
||||
case "darwin":
|
||||
if (count === 0) {
|
||||
app.dock!.setBadge("");
|
||||
break;
|
||||
}
|
||||
app.dock!.setBadge(count === -1 ? "•" : count.toString());
|
||||
break;
|
||||
case "win32":
|
||||
const [index, description] = getBadgeIndexAndDescription(count);
|
||||
if (lastIndex === index) break;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import Server from "arrpc";
|
||||
import { IpcCommands } from "shared/IpcEvents";
|
||||
|
||||
import { mainWin } from "./mainWindow";
|
||||
import { sendRendererCommand } from "./ipcCommands";
|
||||
import { Settings } from "./settings";
|
||||
|
||||
let server: any;
|
||||
@@ -17,20 +18,16 @@ export async function initArRPC() {
|
||||
if (server || !Settings.store.arRPC) return;
|
||||
|
||||
try {
|
||||
// This module starts a server as a side effect, so it needs to be lazy imported
|
||||
const { send: sendToBridge } = await import("arrpc/src/bridge");
|
||||
|
||||
server = await new Server();
|
||||
server.on("activity", sendToBridge);
|
||||
server.on("invite", (invite: string, callback: (valid: boolean) => void) => {
|
||||
server.on("activity", (data: any) => sendRendererCommand(IpcCommands.RPC_ACTIVITY, JSON.stringify(data)));
|
||||
server.on("invite", async (invite: string, callback: (valid: boolean) => void) => {
|
||||
invite = String(invite);
|
||||
if (!inviteCodeRegex.test(invite)) return callback(false);
|
||||
|
||||
mainWin.webContents
|
||||
// Safety: Result of JSON.stringify should always be safe to equal
|
||||
// Also, just to be super super safe, invite is regex validated above
|
||||
.executeJavaScript(`Vesktop.openInviteModal(${JSON.stringify(invite)})`)
|
||||
.then(callback);
|
||||
await sendRendererCommand(IpcCommands.RPC_INVITE, invite).then(callback);
|
||||
});
|
||||
server.on("link", async (data: any, deepCallback: (valid: boolean) => void) => {
|
||||
await sendRendererCommand(IpcCommands.RPC_DEEP_LINK, data).then(deepCallback);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to start arRPC server", e);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
||||
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { stripIndent } from "shared/utils/text";
|
||||
|
||||
interface AutoStart {
|
||||
isEnabled(): boolean;
|
||||
@@ -17,21 +18,29 @@ interface AutoStart {
|
||||
function makeAutoStartLinux(): AutoStart {
|
||||
const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config");
|
||||
const dir = join(configDir, "autostart");
|
||||
const file = join(dir, "vencord.desktop");
|
||||
const file = join(dir, "vesktop.desktop");
|
||||
|
||||
// IM STUPID
|
||||
const legacyName = join(dir, "vencord.desktop");
|
||||
if (existsSync(legacyName)) renameSync(legacyName, file);
|
||||
|
||||
// "Quoting must be done by enclosing the argument between double quotes and escaping the double quote character,
|
||||
// backtick character ("`"), dollar sign ("$") and backslash character ("\") by preceding it with an additional backslash character"
|
||||
// https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
|
||||
const commandLine = process.argv.map(arg => '"' + arg.replace(/["$`\\]/g, "\\$&") + '"').join(" ");
|
||||
|
||||
return {
|
||||
isEnabled: () => existsSync(file),
|
||||
enable() {
|
||||
const desktopFile = `
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.0
|
||||
Name=Vencord
|
||||
Comment=Vencord autostart script
|
||||
Exec=${process.execPath}
|
||||
Terminal=false
|
||||
StartupNotify=false
|
||||
`.trim();
|
||||
const desktopFile = stripIndent`
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Vesktop
|
||||
Comment=Vesktop autostart script
|
||||
Exec=${commandLine}
|
||||
StartupNotify=false
|
||||
Terminal=false
|
||||
`;
|
||||
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(file, desktopFile);
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { join } from "path";
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
const vesktopDir = dirname(process.execPath);
|
||||
|
||||
export const PORTABLE =
|
||||
process.platform === "win32" &&
|
||||
!process.execPath.toLowerCase().endsWith("electron.exe") &&
|
||||
!existsSync(join(vesktopDir, "Uninstall Vesktop.exe"));
|
||||
|
||||
export const DATA_DIR =
|
||||
process.env.VENCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("userData")));
|
||||
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
|
||||
app.setPath("sessionData", SESSION_DATA_DIR);
|
||||
|
||||
export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR || join(app.getPath("userData"), "VencordDesktop");
|
||||
export const VENCORD_SETTINGS_DIR = join(DATA_DIR, "settings");
|
||||
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")).Settings.store.vencordDir || join(DATA_DIR, "vencordDist");
|
||||
(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)`;
|
||||
|
||||
@@ -24,3 +41,19 @@ export const MIN_WIDTH = 940;
|
||||
export const MIN_HEIGHT = 500;
|
||||
export const DEFAULT_WIDTH = 1280;
|
||||
export const DEFAULT_HEIGHT = 720;
|
||||
|
||||
export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"];
|
||||
|
||||
const VersionString = `AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome.split(".")[0]}.0.0.0 Safari/537.36`;
|
||||
const BrowserUserAgents = {
|
||||
darwin: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ${VersionString}`,
|
||||
linux: `Mozilla/5.0 (X11; Linux x86_64) ${VersionString}`,
|
||||
windows: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) ${VersionString}`
|
||||
};
|
||||
|
||||
export const BrowserUserAgent = BrowserUserAgents[process.platform] || BrowserUserAgents.windows;
|
||||
|
||||
export const enum MessageBoxChoice {
|
||||
Default,
|
||||
Cancel
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
@@ -9,29 +9,31 @@ import { BrowserWindow } from "electron/main";
|
||||
import { copyFileSync, mkdirSync, readdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { SplashProps } from "shared/browserWinProperties";
|
||||
import { VIEW_DIR } from "shared/paths";
|
||||
import { ICON_PATH, VIEW_DIR } from "shared/paths";
|
||||
|
||||
import { autoStart } from "./autoStart";
|
||||
import { DATA_DIR } from "./constants";
|
||||
import { createWindows } from "./mainWindow";
|
||||
import { Settings } from "./settings";
|
||||
import { Settings, State } from "./settings";
|
||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||
|
||||
interface Data {
|
||||
minimizeToTray: boolean;
|
||||
discordBranch: "stable" | "canary" | "ptb";
|
||||
autoStart: boolean;
|
||||
importSettings: boolean;
|
||||
richPresence: boolean;
|
||||
minimizeToTray?: "on";
|
||||
autoStart?: "on";
|
||||
importSettings?: "on";
|
||||
richPresence?: "on";
|
||||
}
|
||||
|
||||
export function createFirstLaunchTour() {
|
||||
const win = new BrowserWindow({
|
||||
...SplashProps,
|
||||
transparent: false,
|
||||
frame: true,
|
||||
autoHideMenuBar: true,
|
||||
height: 470,
|
||||
width: 550
|
||||
width: 550,
|
||||
icon: ICON_PATH
|
||||
});
|
||||
|
||||
makeLinksOpenExternally(win);
|
||||
@@ -43,10 +45,11 @@ export function createFirstLaunchTour() {
|
||||
if (!msg.startsWith("form:")) return;
|
||||
const data = JSON.parse(msg.slice(5)) as Data;
|
||||
|
||||
Settings.store.minimizeToTray = data.minimizeToTray;
|
||||
console.log(data);
|
||||
State.store.firstLaunch = false;
|
||||
Settings.store.discordBranch = data.discordBranch;
|
||||
Settings.store.firstLaunch = false;
|
||||
Settings.store.arRPC = data.richPresence;
|
||||
Settings.store.minimizeToTray = !!data.minimizeToTray;
|
||||
Settings.store.arRPC = !!data.richPresence;
|
||||
|
||||
if (data.autoStart) autoStart.enable();
|
||||
|
||||
|
||||
@@ -1,39 +1,110 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./ipc";
|
||||
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { checkUpdates } from "updater/main";
|
||||
import { app, BrowserWindow, nativeTheme } from "electron";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
import { DATA_DIR } from "./constants";
|
||||
import { createFirstLaunchTour } from "./firstLaunch";
|
||||
import { createWindows, mainWin } from "./mainWindow";
|
||||
import { registerMediaPermissionsHandler } from "./mediaPermissions";
|
||||
import { registerScreenShareHandler } from "./screenShare";
|
||||
import { Settings } from "./settings";
|
||||
import { Settings, State } from "./settings";
|
||||
import { isDeckGameMode } from "./utils/steamOS";
|
||||
|
||||
if (IS_DEV) {
|
||||
require("source-map-support").install();
|
||||
if (!IS_DEV) {
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
}
|
||||
|
||||
console.log("Vesktop v" + app.getVersion());
|
||||
|
||||
// Make the Vencord files use our DATA_DIR
|
||||
process.env.VENCORD_USER_DATA_DIR = DATA_DIR;
|
||||
|
||||
const isLinux = process.platform === "linux";
|
||||
|
||||
export let enableHardwareAcceleration = true;
|
||||
|
||||
function init() {
|
||||
app.setAsDefaultProtocolClient("discord");
|
||||
|
||||
const { disableSmoothScroll, hardwareAcceleration, hardwareVideoAcceleration } = Settings.store;
|
||||
|
||||
const enabledFeatures = new Set(app.commandLine.getSwitchValue("enable-features").split(","));
|
||||
const disabledFeatures = new Set(app.commandLine.getSwitchValue("disable-features").split(","));
|
||||
app.commandLine.removeSwitch("enable-features");
|
||||
app.commandLine.removeSwitch("disable-features");
|
||||
|
||||
if (hardwareAcceleration === false || process.argv.includes("--disable-gpu")) {
|
||||
enableHardwareAcceleration = false;
|
||||
app.disableHardwareAcceleration();
|
||||
} else {
|
||||
if (hardwareVideoAcceleration) {
|
||||
enabledFeatures.add("AcceleratedVideoEncoder");
|
||||
enabledFeatures.add("AcceleratedVideoDecoder");
|
||||
|
||||
if (isLinux) {
|
||||
enabledFeatures.add("AcceleratedVideoDecodeLinuxGL");
|
||||
enabledFeatures.add("AcceleratedVideoDecodeLinuxZeroCopyGL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (disableSmoothScroll) {
|
||||
app.commandLine.appendSwitch("disable-smooth-scrolling");
|
||||
}
|
||||
|
||||
// disable renderer backgrounding to prevent the app from unloading when in the background
|
||||
// https://github.com/electron/electron/issues/2822
|
||||
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
|
||||
app.commandLine.appendSwitch("disable-renderer-backgrounding");
|
||||
app.commandLine.appendSwitch("disable-background-timer-throttling");
|
||||
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
|
||||
if (process.platform === "win32") {
|
||||
disabledFeatures.add("CalculateNativeWinOcclusion");
|
||||
}
|
||||
|
||||
// work around chrome 66 disabling autoplay by default
|
||||
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||
|
||||
// WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows.
|
||||
// HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service.
|
||||
//
|
||||
// WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790
|
||||
app.commandLine.appendSwitch(
|
||||
"disable-features",
|
||||
"WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService,WidgetLayering"
|
||||
);
|
||||
// HardwareMediaKeyHandling, MediaSessionService: Prevent Discord from registering as a media service.
|
||||
disabledFeatures.add("WinRetrieveSuggestionsOnlyOnDemand");
|
||||
disabledFeatures.add("HardwareMediaKeyHandling");
|
||||
disabledFeatures.add("MediaSessionService");
|
||||
|
||||
if (isLinux) {
|
||||
// Support TTS on Linux using https://wiki.archlinux.org/title/Speech_dispatcher
|
||||
app.commandLine.appendSwitch("enable-speech-dispatcher");
|
||||
|
||||
// Work around Gtk-ERROR: GTK 2/3 symbols detected. Using GTK 2/3 and GTK 4 in the same process is not supported
|
||||
// https://github.com/electron/electron/issues/46538
|
||||
// TODO: Remove this when upstream fixes it
|
||||
app.commandLine.appendSwitch("gtk-version", "3");
|
||||
}
|
||||
|
||||
disabledFeatures.forEach(feat => enabledFeatures.delete(feat));
|
||||
|
||||
const enabledFeaturesArray = enabledFeatures.values().filter(Boolean).toArray();
|
||||
const disabledFeaturesArray = disabledFeatures.values().filter(Boolean).toArray();
|
||||
|
||||
if (enabledFeaturesArray.length) {
|
||||
app.commandLine.appendSwitch("enable-features", enabledFeaturesArray.join(","));
|
||||
console.log("Enabled Chromium features:", enabledFeaturesArray.join(", "));
|
||||
}
|
||||
|
||||
if (disabledFeaturesArray.length) {
|
||||
app.commandLine.appendSwitch("disable-features", disabledFeaturesArray.join(","));
|
||||
console.log("Disabled Chromium features:", disabledFeaturesArray.join(", "));
|
||||
}
|
||||
|
||||
// In the Flatpak on SteamOS the theme is detected as light, but SteamOS only has a dark mode, so we just override it
|
||||
if (isDeckGameMode) nativeTheme.themeSource = "dark";
|
||||
|
||||
app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => {
|
||||
if (data.IS_DEV) app.quit();
|
||||
@@ -45,10 +116,11 @@ function init() {
|
||||
});
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
checkUpdates();
|
||||
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.desktop");
|
||||
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.vesktop");
|
||||
|
||||
registerScreenShareHandler();
|
||||
registerMediaPermissionsHandler();
|
||||
|
||||
bootstrap();
|
||||
|
||||
app.on("activate", () => {
|
||||
@@ -70,13 +142,19 @@ if (!app.requestSingleInstanceLock({ IS_DEV })) {
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
if (!Object.hasOwn(Settings.store, "firstLaunch")) {
|
||||
if (!Object.hasOwn(State.store, "firstLaunch")) {
|
||||
createFirstLaunchTour();
|
||||
} else {
|
||||
createWindows();
|
||||
}
|
||||
}
|
||||
|
||||
// MacOS only event
|
||||
export let darwinURL: string | undefined;
|
||||
app.on("open-url", (_, url) => {
|
||||
darwinURL = url;
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
|
||||
174
src/main/ipc.ts
174
src/main/ipc.ts
@@ -1,12 +1,26 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { app, dialog, ipcMain, session, shell } from "electron";
|
||||
import { existsSync, readFileSync, watch } from "fs";
|
||||
if (process.platform === "linux") import("./venmic");
|
||||
|
||||
import { execFile } from "child_process";
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
clipboard,
|
||||
dialog,
|
||||
IpcMainInvokeEvent,
|
||||
nativeImage,
|
||||
RelaunchOptions,
|
||||
session,
|
||||
shell
|
||||
} from "electron";
|
||||
import { mkdirSync, readFileSync, watch } from "fs";
|
||||
import { open, readFile } from "fs/promises";
|
||||
import { enableHardwareAcceleration } from "main";
|
||||
import { release } from "os";
|
||||
import { join } from "path";
|
||||
import { debounce } from "shared/utils/debounce";
|
||||
@@ -14,99 +28,136 @@ 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 } from "./constants";
|
||||
import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
|
||||
import { mainWin } from "./mainWindow";
|
||||
import { Settings } from "./settings";
|
||||
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";
|
||||
|
||||
ipcMain.on(IpcEvents.GET_VENCORD_PRELOAD_FILE, e => {
|
||||
e.returnValue = join(VENCORD_FILES_DIR, "preload.js");
|
||||
});
|
||||
handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload.js"));
|
||||
handleSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT, () =>
|
||||
readFileSync(join(VENCORD_FILES_DIR, "vencordDesktopRenderer.js"), "utf-8")
|
||||
);
|
||||
|
||||
ipcMain.on(IpcEvents.GET_VENCORD_RENDERER_SCRIPT, e => {
|
||||
e.returnValue = 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"));
|
||||
|
||||
ipcMain.on(IpcEvents.GET_RENDERER_SCRIPT, e => {
|
||||
e.returnValue = readFileSync(join(__dirname, "renderer.js"), "utf-8");
|
||||
});
|
||||
handleSync(IpcEvents.GET_SETTINGS, () => Settings.plain);
|
||||
handleSync(IpcEvents.GET_VERSION, () => app.getVersion());
|
||||
handleSync(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION, () => enableHardwareAcceleration);
|
||||
|
||||
ipcMain.on(IpcEvents.GET_RENDERER_CSS_FILE, e => {
|
||||
e.returnValue = join(__dirname, "renderer.css");
|
||||
});
|
||||
handleSync(
|
||||
IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY,
|
||||
() => process.platform === "win32" && Number(release().split(".").pop()) >= 22621
|
||||
);
|
||||
|
||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => {
|
||||
e.returnValue = Settings.plain;
|
||||
});
|
||||
handleSync(IpcEvents.AUTOSTART_ENABLED, () => autoStart.isEnabled());
|
||||
handle(IpcEvents.ENABLE_AUTOSTART, autoStart.enable);
|
||||
handle(IpcEvents.DISABLE_AUTOSTART, autoStart.disable);
|
||||
|
||||
ipcMain.on(IpcEvents.GET_VERSION, e => {
|
||||
e.returnValue = app.getVersion();
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY, e => {
|
||||
e.returnValue = process.platform === "win32" && Number(release().split(".").pop()) >= 22621;
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.AUTOSTART_ENABLED, e => {
|
||||
e.returnValue = autoStart.isEnabled();
|
||||
});
|
||||
ipcMain.handle(IpcEvents.ENABLE_AUTOSTART, autoStart.enable);
|
||||
ipcMain.handle(IpcEvents.DISABLE_AUTOSTART, autoStart.disable);
|
||||
|
||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, settings: typeof Settings.store, path?: string) => {
|
||||
handle(IpcEvents.SET_SETTINGS, (_, settings: typeof Settings.store, path?: string) => {
|
||||
Settings.setData(settings, path);
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.RELAUNCH, () => {
|
||||
app.relaunch();
|
||||
handle(IpcEvents.RELAUNCH, async () => {
|
||||
const options: RelaunchOptions = {
|
||||
args: process.argv.slice(1).concat(["--relaunch"])
|
||||
};
|
||||
if (isDeckGameMode) {
|
||||
// We can't properly relaunch when running under gamescope, but we can at least navigate to our page in Steam.
|
||||
await showGamePage();
|
||||
} else if (app.isPackaged && process.env.APPIMAGE) {
|
||||
execFile(process.env.APPIMAGE, options.args);
|
||||
} else {
|
||||
app.relaunch(options);
|
||||
}
|
||||
app.exit();
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.SHOW_ITEM_IN_FOLDER, (_, path) => {
|
||||
handle(IpcEvents.SHOW_ITEM_IN_FOLDER, (_, path) => {
|
||||
shell.showItemInFolder(path);
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.FOCUS, () => {
|
||||
if (process.platform === "win32") mainWin.minimize(); // Windows is weird
|
||||
function getWindow(e: IpcMainInvokeEvent, key?: string) {
|
||||
return key ? PopoutWindows.get(key)! : (BrowserWindow.fromWebContents(e.sender) ?? mainWin);
|
||||
}
|
||||
|
||||
mainWin.restore();
|
||||
handle(IpcEvents.FOCUS, () => {
|
||||
mainWin.show();
|
||||
mainWin.setSkipTaskbar(false);
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.CLOSE, e => {
|
||||
e.sender.close();
|
||||
handle(IpcEvents.CLOSE, (e, key?: string) => {
|
||||
getWindow(e, key).close();
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.SPELLCHECK_SET_LANGUAGES, (_, languages: string[]) => {
|
||||
const ses = session.defaultSession;
|
||||
|
||||
const available = ses.availableSpellCheckerLanguages;
|
||||
const applicable = languages.filter(l => available.includes(l)).slice(0, 3);
|
||||
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
|
||||
handle(IpcEvents.MINIMIZE, (e, key?: string) => {
|
||||
getWindow(e, key).minimize();
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => {
|
||||
handle(IpcEvents.MAXIMIZE, (e, key?: string) => {
|
||||
const win = getWindow(e, key);
|
||||
if (win.isMaximized()) {
|
||||
win.unmaximize();
|
||||
} else {
|
||||
win.maximize();
|
||||
}
|
||||
});
|
||||
|
||||
handleSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES, e => {
|
||||
e.returnValue = session.defaultSession.availableSpellCheckerLanguages;
|
||||
});
|
||||
|
||||
handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => {
|
||||
e.sender.replaceMisspelling(word);
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, (e, word: string) => {
|
||||
handle(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, (e, word: string) => {
|
||||
e.sender.session.addWordToSpellCheckerDictionary(word);
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.SELECT_VENCORD_DIR, async () => {
|
||||
handleSync(IpcEvents.GET_VENCORD_DIR, e => (e.returnValue = State.store.vencordDir));
|
||||
|
||||
handle(IpcEvents.SELECT_VENCORD_DIR, async (_e, value?: null) => {
|
||||
if (value === null) {
|
||||
delete State.store.vencordDir;
|
||||
return "ok";
|
||||
}
|
||||
|
||||
const res = await dialog.showOpenDialog(mainWin!, {
|
||||
properties: ["openDirectory"]
|
||||
});
|
||||
if (!res.filePaths.length) return "cancelled";
|
||||
|
||||
const dir = res.filePaths[0];
|
||||
for (const file of ["vencordDesktopMain.js", "preload.js", "vencordDesktopRenderer.js", "renderer.css"]) {
|
||||
if (!existsSync(join(dir, file))) return "invalid";
|
||||
}
|
||||
if (!isValidVencordInstall(dir)) return "invalid";
|
||||
|
||||
return dir;
|
||||
State.store.vencordDir = dir;
|
||||
|
||||
return "ok";
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.SET_BADGE_COUNT, (_, count: number) => setBadgeCount(count));
|
||||
handle(IpcEvents.SET_BADGE_COUNT, (_, count: number) => setBadgeCount(count));
|
||||
|
||||
handle(IpcEvents.CLIPBOARD_COPY_IMAGE, async (_, buf: ArrayBuffer, src: string) => {
|
||||
clipboard.write({
|
||||
html: `<img src="${src.replaceAll('"', '\\"')}">`,
|
||||
image: nativeImage.createFromBuffer(Buffer.from(buf))
|
||||
});
|
||||
});
|
||||
|
||||
function openDebugPage(page: string) {
|
||||
const win = new BrowserWindow({
|
||||
autoHideMenuBar: true
|
||||
});
|
||||
|
||||
win.loadURL(page);
|
||||
}
|
||||
|
||||
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(() => "");
|
||||
@@ -122,3 +173,12 @@ open(VENCORD_QUICKCSS_FILE, "a+").then(fd => {
|
||||
}, 50)
|
||||
);
|
||||
});
|
||||
|
||||
mkdirSync(VENCORD_THEMES_DIR, { recursive: true });
|
||||
watch(
|
||||
VENCORD_THEMES_DIR,
|
||||
{ persistent: false },
|
||||
debounce(() => {
|
||||
mainWin?.webContents.postMessage("VencordThemeUpdate", void 0);
|
||||
})
|
||||
);
|
||||
|
||||
61
src/main/ipcCommands.ts
Normal file
61
src/main/ipcCommands.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { randomUUID } from "crypto";
|
||||
import { ipcMain } from "electron";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
|
||||
import { mainWin } from "./mainWindow";
|
||||
|
||||
const resolvers = new Map<string, Record<"resolve" | "reject", (data: any) => void>>();
|
||||
|
||||
export interface IpcMessage {
|
||||
nonce: string;
|
||||
message: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export interface IpcResponse {
|
||||
nonce: string;
|
||||
ok: boolean;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the renderer process and waits for a response.
|
||||
* `data` must be serializable as it will be sent over IPC.
|
||||
*
|
||||
* You must add a handler for the message in the renderer process.
|
||||
*/
|
||||
export function sendRendererCommand<T = any>(message: string, data?: any) {
|
||||
if (mainWin.isDestroyed()) {
|
||||
console.warn("Main window is destroyed, cannot send IPC command:", message);
|
||||
return Promise.reject(new Error("Main window is destroyed"));
|
||||
}
|
||||
|
||||
const nonce = randomUUID();
|
||||
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
resolvers.set(nonce, { resolve, reject });
|
||||
});
|
||||
|
||||
mainWin.webContents.send(IpcEvents.IPC_COMMAND, { nonce, message, data });
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
ipcMain.on(IpcEvents.IPC_COMMAND, (_event, { nonce, ok, data }: IpcResponse) => {
|
||||
const resolver = resolvers.get(nonce);
|
||||
if (!resolver) throw new Error(`Unknown message: ${nonce}`);
|
||||
|
||||
if (ok) {
|
||||
resolver.resolve(data);
|
||||
} else {
|
||||
resolver.reject(data);
|
||||
}
|
||||
|
||||
resolvers.delete(nonce);
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -11,11 +11,15 @@ import {
|
||||
dialog,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
nativeTheme,
|
||||
screen,
|
||||
session,
|
||||
Tray
|
||||
} from "electron";
|
||||
import { EventEmitter } from "events";
|
||||
import { rm } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
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";
|
||||
@@ -23,15 +27,29 @@ import type { SettingsStore } from "shared/utils/SettingsStore";
|
||||
import { ICON_PATH } from "../shared/paths";
|
||||
import { createAboutWindow } from "./about";
|
||||
import { initArRPC } from "./arrpc";
|
||||
import { DATA_DIR, DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH, VENCORD_FILES_DIR } from "./constants";
|
||||
import { Settings, VencordSettings } from "./settings";
|
||||
import { createSplashWindow } from "./splash";
|
||||
import {
|
||||
BrowserUserAgent,
|
||||
DATA_DIR,
|
||||
DEFAULT_HEIGHT,
|
||||
DEFAULT_WIDTH,
|
||||
MessageBoxChoice,
|
||||
MIN_HEIGHT,
|
||||
MIN_WIDTH,
|
||||
VENCORD_FILES_DIR
|
||||
} from "./constants";
|
||||
import { darwinURL } from "./index";
|
||||
import { sendRendererCommand } from "./ipcCommands";
|
||||
import { Settings, State, VencordSettings } from "./settings";
|
||||
import { createSplashWindow, updateSplashMessage } from "./splash";
|
||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||
import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS";
|
||||
import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
|
||||
|
||||
let isQuitting = false;
|
||||
let tray: Tray;
|
||||
|
||||
applyDeckKeyboardFix();
|
||||
|
||||
app.on("before-quit", () => {
|
||||
isQuitting = true;
|
||||
});
|
||||
@@ -60,20 +78,23 @@ const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpe
|
||||
const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings);
|
||||
|
||||
function initTray(win: BrowserWindow) {
|
||||
const onTrayClick = () => {
|
||||
if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide();
|
||||
else win.show();
|
||||
};
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Open",
|
||||
click() {
|
||||
win.show();
|
||||
},
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "About",
|
||||
click: createAboutWindow
|
||||
},
|
||||
{
|
||||
label: "Update Vencord",
|
||||
label: "Repair Vencord",
|
||||
async click() {
|
||||
await downloadVencordFiles();
|
||||
app.relaunch();
|
||||
@@ -90,14 +111,14 @@ function initTray(win: BrowserWindow) {
|
||||
type: "separator"
|
||||
},
|
||||
{
|
||||
label: "Relaunch",
|
||||
label: "Restart",
|
||||
click() {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Quit Vesktop",
|
||||
label: "Quit",
|
||||
click() {
|
||||
isQuitting = true;
|
||||
app.quit();
|
||||
@@ -108,20 +129,7 @@ function initTray(win: BrowserWindow) {
|
||||
tray = new Tray(ICON_PATH);
|
||||
tray.setToolTip("Vesktop");
|
||||
tray.setContextMenu(trayMenu);
|
||||
tray.on("click", () => win.show());
|
||||
|
||||
win.on("show", () => {
|
||||
trayMenu.items[0].enabled = false;
|
||||
});
|
||||
|
||||
win.on("hide", () => {
|
||||
trayMenu.items[0].enabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
const enum MessageBoxChoice {
|
||||
Default,
|
||||
Cancel
|
||||
tray.on("click", onTrayClick);
|
||||
}
|
||||
|
||||
async function clearData(win: BrowserWindow) {
|
||||
@@ -147,6 +155,8 @@ async function clearData(win: BrowserWindow) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
type MenuItemList = Array<MenuItemConstructorOptions | false>;
|
||||
|
||||
function initMenuBar(win: BrowserWindow) {
|
||||
const isWindows = process.platform === "win32";
|
||||
const isDarwin = process.platform === "darwin";
|
||||
@@ -181,14 +191,35 @@ function initMenuBar(win: BrowserWindow) {
|
||||
app.quit();
|
||||
}
|
||||
},
|
||||
isDarwin && {
|
||||
label: "Hide",
|
||||
role: "hide"
|
||||
},
|
||||
isDarwin && {
|
||||
label: "Hide others",
|
||||
role: "hideOthers"
|
||||
},
|
||||
...(!isDarwin
|
||||
? []
|
||||
: ([
|
||||
{
|
||||
type: "separator"
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
accelerator: "CmdOrCtrl+,",
|
||||
async click() {
|
||||
sendRendererCommand(IpcCommands.NAVIGATE_SETTINGS);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "separator"
|
||||
},
|
||||
{
|
||||
role: "hide"
|
||||
},
|
||||
{
|
||||
role: "hideOthers"
|
||||
},
|
||||
{
|
||||
role: "unhide"
|
||||
},
|
||||
{
|
||||
type: "separator"
|
||||
}
|
||||
] satisfies MenuItemList)),
|
||||
{
|
||||
label: "Quit",
|
||||
accelerator: wantCtrlQ ? "CmdOrCtrl+Q" : void 0,
|
||||
@@ -213,9 +244,9 @@ function initMenuBar(win: BrowserWindow) {
|
||||
role: "zoomIn",
|
||||
visible: false
|
||||
}
|
||||
] satisfies Array<MenuItemConstructorOptions | false>;
|
||||
] satisfies MenuItemList;
|
||||
|
||||
const menu = Menu.buildFromTemplate([
|
||||
const menuItems = [
|
||||
{
|
||||
label: "Vesktop",
|
||||
role: "appMenu",
|
||||
@@ -224,21 +255,28 @@ function initMenuBar(win: BrowserWindow) {
|
||||
{ role: "fileMenu" },
|
||||
{ role: "editMenu" },
|
||||
{ role: "viewMenu" },
|
||||
{ role: "windowMenu" }
|
||||
]);
|
||||
isDarwin && { role: "windowMenu" }
|
||||
] satisfies MenuItemList;
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuItems.filter(isTruthy));
|
||||
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
|
||||
const { x, y, width, height } = Settings.store.windowBounds ?? {};
|
||||
// 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;
|
||||
|
||||
if (x != null && y != null) {
|
||||
const storedDisplay = screen.getAllDisplays().find(display => display.id === State.store.displayId);
|
||||
|
||||
if (x != null && y != null && storedDisplay) {
|
||||
options.x = x;
|
||||
options.y = y;
|
||||
}
|
||||
@@ -251,10 +289,33 @@ function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
|
||||
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 = () => {
|
||||
Settings.store.maximized = win.isMaximized();
|
||||
Settings.store.minimized = win.isMinimized();
|
||||
State.store.maximized = win.isMaximized();
|
||||
State.store.minimized = win.isMinimized();
|
||||
};
|
||||
|
||||
win.on("maximize", saveState);
|
||||
@@ -262,7 +323,8 @@ function initWindowBoundsListeners(win: BrowserWindow) {
|
||||
win.on("unmaximize", saveState);
|
||||
|
||||
const saveBounds = () => {
|
||||
Settings.store.windowBounds = win.getBounds();
|
||||
State.store.windowBounds = win.getBounds();
|
||||
State.store.displayId = screen.getDisplayMatching(State.store.windowBounds).id;
|
||||
};
|
||||
|
||||
win.on("resize", saveBounds);
|
||||
@@ -274,6 +336,7 @@ function initSettingsListeners(win: BrowserWindow) {
|
||||
if (enable) initTray(win);
|
||||
else tray?.destroy();
|
||||
});
|
||||
|
||||
addSettingsListener("disableMinSize", disable => {
|
||||
if (disable) {
|
||||
// 0 no work
|
||||
@@ -302,12 +365,42 @@ function initSettingsListeners(win: BrowserWindow) {
|
||||
addSettingsListener("enableMenu", enabled => {
|
||||
win.setAutoHideMenuBar(enabled ?? false);
|
||||
});
|
||||
|
||||
addSettingsListener("spellCheckLanguages", languages => initSpellCheckLanguages(win, languages));
|
||||
}
|
||||
|
||||
async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) {
|
||||
languages ??= await sendRendererCommand(IpcCommands.GET_LANGUAGES);
|
||||
if (!languages) return;
|
||||
|
||||
const ses = session.defaultSession;
|
||||
|
||||
const available = ses.availableSpellCheckerLanguages;
|
||||
const applicable = languages.filter(l => available.includes(l)).slice(0, 5);
|
||||
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
|
||||
}
|
||||
|
||||
function initSpellCheck(win: BrowserWindow) {
|
||||
win.webContents.on("context-menu", (_, data) => {
|
||||
win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions);
|
||||
});
|
||||
|
||||
initSpellCheckLanguages(win, Settings.store.spellCheckLanguages);
|
||||
}
|
||||
|
||||
function initStaticTitle(win: BrowserWindow) {
|
||||
const listener = (e: { preventDefault: Function }) => e.preventDefault();
|
||||
|
||||
if (Settings.store.staticTitle) win.on("page-title-updated", listener);
|
||||
|
||||
addSettingsListener("staticTitle", enabled => {
|
||||
if (enabled) {
|
||||
win.setTitle("Vesktop");
|
||||
win.on("page-title-updated", listener);
|
||||
} else {
|
||||
win.off("page-title-updated", listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
@@ -315,42 +408,55 @@ function createMainWindow() {
|
||||
removeSettingsListeners();
|
||||
removeVencordSettingsListeners();
|
||||
|
||||
const { staticTitle, transparencyOption, enableMenu } = Settings.store;
|
||||
const { frameless, macosTranslucency } = VencordSettings.store;
|
||||
const { staticTitle, transparencyOption, enableMenu, customTitleBar, splashTheming, splashBackground } =
|
||||
Settings.store;
|
||||
|
||||
const { frameless, transparent } = VencordSettings.store;
|
||||
|
||||
const noFrame = frameless === true || customTitleBar === true;
|
||||
const backgroundColor =
|
||||
splashTheming !== false ? splashBackground : nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
|
||||
|
||||
const win = (mainWin = new BrowserWindow({
|
||||
show: false,
|
||||
show: Settings.store.enableSplashScreen === false,
|
||||
backgroundColor,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
contextIsolation: true,
|
||||
devTools: true,
|
||||
preload: join(__dirname, "preload.js"),
|
||||
spellcheck: true
|
||||
spellcheck: true,
|
||||
// disable renderer backgrounding to prevent the app from unloading when in the background
|
||||
backgroundThrottling: false
|
||||
},
|
||||
icon: ICON_PATH,
|
||||
frame: frameless !== true,
|
||||
...(transparencyOption && transparencyOption !== "none"
|
||||
? {
|
||||
backgroundColor: "#00000000",
|
||||
backgroundMaterial: Settings.store.transparencyOption,
|
||||
transparent: true
|
||||
}
|
||||
: {}),
|
||||
...(staticTitle ? { title: "Vencord" } : {}),
|
||||
...(macosTranslucency
|
||||
? {
|
||||
vibrancy: "sidebar",
|
||||
backgroundColor: "#ffffff00"
|
||||
}
|
||||
: {}),
|
||||
...(process.platform === "darwin" ? { titleBarStyle: "hiddenInset" } : {}),
|
||||
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
|
||||
}));
|
||||
win.setMenuBarVisibility(false);
|
||||
if (process.platform === "darwin" && customTitleBar) win.setWindowButtonVisibility(false);
|
||||
|
||||
win.on("close", e => {
|
||||
const useTray = Settings.store.minimizeToTray !== false && Settings.store.tray !== false;
|
||||
const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false;
|
||||
if (isQuitting || (process.platform !== "darwin" && !useTray)) return;
|
||||
|
||||
e.preventDefault();
|
||||
@@ -361,45 +467,91 @@ function createMainWindow() {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (Settings.store.staticTitle) win.on("page-title-updated", e => e.preventDefault());
|
||||
|
||||
initWindowBoundsListeners(win);
|
||||
if ((Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win);
|
||||
if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win);
|
||||
initMenuBar(win);
|
||||
makeLinksOpenExternally(win);
|
||||
initSettingsListeners(win);
|
||||
initSpellCheck(win);
|
||||
initStaticTitle(win);
|
||||
|
||||
win.webContents.setUserAgent(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
|
||||
);
|
||||
win.webContents.setUserAgent(BrowserUserAgent);
|
||||
|
||||
const subdomain =
|
||||
Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb"
|
||||
? `${Settings.store.discordBranch}.`
|
||||
: "";
|
||||
|
||||
win.loadURL(`https://${subdomain}discord.com/app`);
|
||||
// if the open-url event is fired (in index.ts) while starting up, darwinURL will be set. If not fall back to checking the process args (which Windows and Linux use for URI calling.)
|
||||
// win.webContents.session.clearCache().then(() => {
|
||||
loadUrl(darwinURL || process.argv.find(arg => arg.startsWith("discord://")));
|
||||
// });
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
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}.` : "";
|
||||
|
||||
// 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"))
|
||||
.catch(error => retryUrl(error.url, error.code));
|
||||
}
|
||||
|
||||
const retryDelay = 1000;
|
||||
function retryUrl(url: string, description: string) {
|
||||
console.log(`retrying in ${retryDelay}ms`);
|
||||
updateSplashMessage(`Failed to load Discord: ${description}`);
|
||||
setTimeout(() => loadUrl(url), retryDelay);
|
||||
}
|
||||
|
||||
export async function createWindows() {
|
||||
const splash = createSplashWindow();
|
||||
const startMinimized = process.argv.includes("--start-minimized");
|
||||
|
||||
let splash: BrowserWindow | undefined;
|
||||
if (Settings.store.enableSplashScreen !== false) {
|
||||
splash = createSplashWindow(startMinimized);
|
||||
|
||||
// SteamOS letterboxes and scales it terribly, so just full screen it
|
||||
if (isDeckGameMode) splash.setFullScreen(true);
|
||||
}
|
||||
|
||||
await ensureVencordFiles();
|
||||
runVencordMain();
|
||||
|
||||
mainWin = createMainWindow();
|
||||
|
||||
mainWin.once("ready-to-show", () => {
|
||||
splash.destroy();
|
||||
mainWin!.show();
|
||||
loadEvents.on("app-loaded", () => {
|
||||
splash?.destroy();
|
||||
|
||||
if (Settings.store.maximized) {
|
||||
mainWin!.maximize();
|
||||
if (!startMinimized) {
|
||||
if (splash) mainWin!.show();
|
||||
if (State.store.maximized && !isDeckGameMode) mainWin!.maximize();
|
||||
}
|
||||
|
||||
if (isDeckGameMode) {
|
||||
// always use entire display
|
||||
mainWin!.setFullScreen(true);
|
||||
|
||||
askToApplySteamLayout(mainWin);
|
||||
}
|
||||
|
||||
mainWin.once("show", () => {
|
||||
if (State.store.maximized && !mainWin!.isMaximized() && !isDeckGameMode) {
|
||||
mainWin!.maximize();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mainWin.webContents.on("did-navigate", (_, url: string, responseCode: number) => {
|
||||
updateSplashMessage(""); // clear the splash message
|
||||
|
||||
// check url to ensure app doesn't loop
|
||||
if (responseCode >= 300 && new URL(url).pathname !== `/app`) {
|
||||
loadUrl(undefined);
|
||||
console.warn(`'did-navigate': Caught bad page response: ${responseCode}, redirecting to main app`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
26
src/main/mediaPermissions.ts
Normal file
26
src/main/mediaPermissions.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { session, systemPreferences } from "electron";
|
||||
|
||||
export function registerMediaPermissionsHandler() {
|
||||
if (process.platform !== "darwin") return;
|
||||
|
||||
session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => {
|
||||
let granted = true;
|
||||
|
||||
if ("mediaTypes" in details) {
|
||||
if (details.mediaTypes?.includes("audio")) {
|
||||
granted &&= await systemPreferences.askForMediaAccess("microphone");
|
||||
}
|
||||
if (details.mediaTypes?.includes("video")) {
|
||||
granted &&= await systemPreferences.askForMediaAccess("camera");
|
||||
}
|
||||
}
|
||||
|
||||
callback(granted);
|
||||
});
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { desktopCapturer, ipcMain, session, Streams } from "electron";
|
||||
import { desktopCapturer, session, Streams } from "electron";
|
||||
import type { StreamPick } from "renderer/components/ScreenSharePicker";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
import { IpcCommands, IpcEvents } from "shared/IpcEvents";
|
||||
|
||||
import { sendRendererCommand } from "./ipcCommands";
|
||||
import { handle } from "./utils/ipcWrappers";
|
||||
|
||||
const isWayland =
|
||||
process.platform === "linux" && (process.env.XDG_SESSION_TYPE === "wayland" || !!process.env.WAYLAND_DISPLAY);
|
||||
|
||||
export function registerScreenShareHandler() {
|
||||
ipcMain.handle(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, async (_, id: string) => {
|
||||
handle(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, async (_, id: string) => {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ["window", "screen"],
|
||||
thumbnailSize: {
|
||||
@@ -21,13 +27,19 @@ export function registerScreenShareHandler() {
|
||||
});
|
||||
|
||||
session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ["window", "screen"],
|
||||
thumbnailSize: {
|
||||
width: 176,
|
||||
height: 99
|
||||
}
|
||||
});
|
||||
// request full resolution on wayland right away because we always only end up with one result anyway
|
||||
const width = isWayland ? 1920 : 176;
|
||||
const sources = await desktopCapturer
|
||||
.getSources({
|
||||
types: ["window", "screen"],
|
||||
thumbnailSize: {
|
||||
width,
|
||||
height: width * (9 / 16)
|
||||
}
|
||||
})
|
||||
.catch(err => console.error("Error during screenshare picker", err));
|
||||
|
||||
if (!sources) return callback({});
|
||||
|
||||
const data = sources.map(({ id, name, thumbnail }) => ({
|
||||
id,
|
||||
@@ -35,10 +47,28 @@ export function registerScreenShareHandler() {
|
||||
url: thumbnail.toDataURL()
|
||||
}));
|
||||
|
||||
const choice = await request.frame
|
||||
.executeJavaScript(`Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify(data)})`)
|
||||
.then(e => e as StreamPick)
|
||||
.catch(() => null);
|
||||
if (isWayland) {
|
||||
const video = data[0];
|
||||
if (video) {
|
||||
const stream = await sendRendererCommand<StreamPick>(IpcCommands.SCREEN_SHARE_PICKER, {
|
||||
screens: [video],
|
||||
skipPicker: true
|
||||
}).catch(() => null);
|
||||
|
||||
if (stream === null) return callback({});
|
||||
}
|
||||
|
||||
callback(video ? { video: sources[0] } : {});
|
||||
return;
|
||||
}
|
||||
|
||||
const choice = await sendRendererCommand<StreamPick>(IpcCommands.SCREEN_SHARE_PICKER, {
|
||||
screens: data,
|
||||
skipPicker: false
|
||||
}).catch(e => {
|
||||
console.error("Error during screenshare picker", e);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!choice) return callback({});
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import type { Settings as TSettings } from "shared/settings";
|
||||
import type { Settings as TSettings, State as TState } from "shared/settings";
|
||||
import { SettingsStore } from "shared/utils/SettingsStore";
|
||||
|
||||
import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants";
|
||||
|
||||
const SETTINGS_FILE = join(DATA_DIR, "settings.json");
|
||||
const STATE_FILE = join(DATA_DIR, "state.json");
|
||||
|
||||
function loadSettings<T extends object = any>(file: string, name: string) {
|
||||
let settings = {} as T;
|
||||
@@ -20,7 +21,7 @@ function loadSettings<T extends object = any>(file: string, name: string) {
|
||||
try {
|
||||
settings = JSON.parse(content);
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse ${name} settings.json:`, err);
|
||||
console.error(`Failed to parse ${name}.json:`, err);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -33,5 +34,6 @@ function loadSettings<T extends object = any>(file: string, name: string) {
|
||||
return store;
|
||||
}
|
||||
|
||||
export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop");
|
||||
export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord");
|
||||
export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop settings");
|
||||
export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord settings");
|
||||
export const State = loadSettings<TState>(STATE_FILE, "Vesktop state");
|
||||
|
||||
@@ -1,18 +1,48 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { BrowserWindow } from "electron";
|
||||
import { join } from "path";
|
||||
import { SplashProps } from "shared/browserWinProperties";
|
||||
import { VIEW_DIR } from "shared/paths";
|
||||
import { ICON_PATH, VIEW_DIR } from "shared/paths";
|
||||
|
||||
export function createSplashWindow() {
|
||||
const splash = new BrowserWindow(SplashProps);
|
||||
import { Settings } from "./settings";
|
||||
|
||||
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"));
|
||||
|
||||
const { splashBackground, splashColor, splashTheming } = Settings.store;
|
||||
|
||||
if (splashTheming !== false) {
|
||||
if (splashColor) {
|
||||
const semiTransparentSplashColor = splashColor.replace("rgb(", "rgba(").replace(")", ", 0.2)");
|
||||
|
||||
splash.webContents.insertCSS(`body { --fg: ${splashColor} !important }`);
|
||||
splash.webContents.insertCSS(`body { --fg-semi-trans: ${semiTransparentSplashColor} !important }`);
|
||||
}
|
||||
|
||||
if (splashBackground) {
|
||||
splash.webContents.insertCSS(`body { --bg: ${splashBackground} !important }`);
|
||||
}
|
||||
}
|
||||
|
||||
return splash;
|
||||
}
|
||||
|
||||
export function updateSplashMessage(message: string) {
|
||||
if (splash && !splash.isDestroyed()) splash.webContents.send("update-splash-message", message);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,58 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { createWriteStream } from "fs";
|
||||
import type { IncomingMessage } from "http";
|
||||
import { get, RequestOptions } from "https";
|
||||
import { finished } from "stream/promises";
|
||||
import { Readable } from "stream";
|
||||
import { pipeline } from "stream/promises";
|
||||
import { setTimeout } from "timers/promises";
|
||||
|
||||
export async function downloadFile(url: string, file: string, options: RequestOptions = {}) {
|
||||
const res = await simpleReq(url, options);
|
||||
await finished(
|
||||
res.pipe(
|
||||
createWriteStream(file, {
|
||||
autoClose: true
|
||||
})
|
||||
)
|
||||
interface FetchieOptions {
|
||||
retryOnNetworkError?: boolean;
|
||||
}
|
||||
|
||||
export async function downloadFile(url: string, file: string, options: RequestInit = {}, fetchieOpts?: FetchieOptions) {
|
||||
const res = await fetchie(url, options, fetchieOpts);
|
||||
await pipeline(
|
||||
// @ts-expect-error odd type error
|
||||
Readable.fromWeb(res.body!),
|
||||
createWriteStream(file, {
|
||||
autoClose: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function simpleReq(url: string, options: RequestOptions = {}) {
|
||||
return new Promise<IncomingMessage>((resolve, reject) => {
|
||||
get(url, options, res => {
|
||||
const { statusCode, statusMessage, headers } = res;
|
||||
if (statusCode! >= 400) return void reject(`${statusCode}: ${statusMessage} - ${url}`);
|
||||
if (statusCode! >= 300) return simpleReq(headers.location!, options).then(resolve).catch(reject);
|
||||
const ONE_MINUTE_MS = 1000 * 60;
|
||||
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function simpleGet(url: string, options: RequestOptions = {}) {
|
||||
const res = await simpleReq(url, options);
|
||||
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
const chunks = [] as Buffer[];
|
||||
|
||||
res.once("error", reject);
|
||||
res.on("data", chunk => chunks.push(chunk));
|
||||
res.once("end", () => resolve(Buffer.concat(chunks)));
|
||||
});
|
||||
export async function fetchie(url: string, options?: RequestInit, { retryOnNetworkError }: FetchieOptions = {}) {
|
||||
let res: Response | undefined;
|
||||
|
||||
try {
|
||||
res = await fetch(url, options);
|
||||
} catch (err) {
|
||||
if (retryOnNetworkError) {
|
||||
console.error("Failed to fetch", url + ".", "Gonna retry with backoff.");
|
||||
|
||||
for (let tries = 0, delayMs = 500; tries < 20; tries++, delayMs = Math.min(2 * delayMs, ONE_MINUTE_MS)) {
|
||||
await setTimeout(delayMs);
|
||||
try {
|
||||
res = await fetch(url, options);
|
||||
break;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (!res) throw new Error(`Failed to fetch ${url}\n${err}`);
|
||||
}
|
||||
|
||||
if (res.ok) return res;
|
||||
|
||||
let msg = `Got non-OK response for ${url}: ${res.status} ${res.statusText}`;
|
||||
|
||||
const reason = await res.text().catch(() => "");
|
||||
if (reason) msg += `\n${reason}`;
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
40
src/main/utils/ipcWrappers.ts
Normal file
40
src/main/utils/ipcWrappers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron";
|
||||
import { DISCORD_HOSTNAMES } from "main/constants";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
|
||||
export function validateSender(frame: WebFrameMain | null, event: string) {
|
||||
if (!frame) throw new Error(`ipc[${event}]: No sender frame`);
|
||||
if (!frame.url) return;
|
||||
|
||||
try {
|
||||
var { hostname, protocol } = new URL(frame.url);
|
||||
} catch (e) {
|
||||
throw new Error(`ipc[${event}]: Invalid URL ${frame.url}`);
|
||||
}
|
||||
|
||||
if (protocol === "file:") 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) {
|
||||
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) {
|
||||
ipcMain.handle(event, (e, ...args) => {
|
||||
validateSender(e.senderFrame, event);
|
||||
return cb(e, ...args);
|
||||
});
|
||||
}
|
||||
@@ -1,40 +1,73 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { BrowserWindow, shell } from "electron";
|
||||
import { DISCORD_HOSTNAMES } from "main/constants";
|
||||
|
||||
import { Settings } from "../settings";
|
||||
import { createOrFocusPopup, setupPopout } from "./popout";
|
||||
import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS";
|
||||
|
||||
export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } {
|
||||
if (protocol == null) {
|
||||
try {
|
||||
protocol = new URL(url).protocol;
|
||||
} catch {
|
||||
return { action: "deny" };
|
||||
}
|
||||
}
|
||||
|
||||
switch (protocol) {
|
||||
case "http:":
|
||||
case "https:":
|
||||
if (Settings.store.openLinksWithElectron) {
|
||||
return { action: "allow" };
|
||||
}
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case "mailto:":
|
||||
case "spotify:":
|
||||
if (isDeckGameMode) {
|
||||
steamOpenURL(url);
|
||||
} else {
|
||||
shell.openExternal(url);
|
||||
}
|
||||
break;
|
||||
case "steam:":
|
||||
if (isDeckGameMode) {
|
||||
execSteamURL(url);
|
||||
} else {
|
||||
shell.openExternal(url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return { action: "deny" };
|
||||
}
|
||||
|
||||
export function makeLinksOpenExternally(win: BrowserWindow) {
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
switch (url) {
|
||||
case "about:blank":
|
||||
case "https://discord.com/popout":
|
||||
return { action: "allow" };
|
||||
}
|
||||
|
||||
win.webContents.setWindowOpenHandler(({ url, frameName, features }) => {
|
||||
try {
|
||||
var { protocol } = new URL(url);
|
||||
var { protocol, hostname, pathname, searchParams } = new URL(url);
|
||||
} catch {
|
||||
return { action: "deny" };
|
||||
}
|
||||
|
||||
switch (protocol) {
|
||||
case "http:":
|
||||
case "https:":
|
||||
if (Settings.store.openLinksWithElectron) {
|
||||
return { action: "allow" };
|
||||
}
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case "mailto:":
|
||||
case "steam:":
|
||||
case "spotify:":
|
||||
shell.openExternal(url);
|
||||
if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) {
|
||||
return createOrFocusPopup(frameName, features);
|
||||
}
|
||||
|
||||
return { action: "deny" };
|
||||
if (url === "about:blank") return { action: "allow" };
|
||||
|
||||
// Drop the static temp page Discord web loads for the connections popout
|
||||
if (frameName === "authorize" && searchParams.get("loading") === "true") return { action: "deny" };
|
||||
|
||||
return handleExternalUrl(url, protocol);
|
||||
});
|
||||
|
||||
win.webContents.on("did-create-window", (win, { frameName }) => {
|
||||
if (frameName.startsWith("DISCORD_")) setupPopout(win, frameName);
|
||||
});
|
||||
}
|
||||
|
||||
116
src/main/utils/popout.ts
Normal file
116
src/main/utils/popout.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||
import { Settings } from "main/settings";
|
||||
|
||||
import { handleExternalUrl } from "./makeLinksOpenExternally";
|
||||
|
||||
const ALLOWED_FEATURES = new Set([
|
||||
"width",
|
||||
"height",
|
||||
"left",
|
||||
"top",
|
||||
"resizable",
|
||||
"movable",
|
||||
"alwaysOnTop",
|
||||
"frame",
|
||||
"transparent",
|
||||
"hasShadow",
|
||||
"closable",
|
||||
"skipTaskbar",
|
||||
"backgroundColor",
|
||||
"menubar",
|
||||
"toolbar",
|
||||
"location",
|
||||
"directories",
|
||||
"titleBarStyle"
|
||||
]);
|
||||
|
||||
const MIN_POPOUT_WIDTH = 320;
|
||||
const MIN_POPOUT_HEIGHT = 180;
|
||||
const DEFAULT_POPOUT_OPTIONS: BrowserWindowConstructorOptions = {
|
||||
title: "Discord Popout",
|
||||
backgroundColor: "#2f3136",
|
||||
minWidth: MIN_POPOUT_WIDTH,
|
||||
minHeight: MIN_POPOUT_HEIGHT,
|
||||
frame: Settings.store.customTitleBar !== true,
|
||||
titleBarStyle: process.platform === "darwin" ? "hidden" : undefined,
|
||||
trafficLightPosition:
|
||||
process.platform === "darwin"
|
||||
? {
|
||||
x: 10,
|
||||
y: 3
|
||||
}
|
||||
: undefined,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
},
|
||||
autoHideMenuBar: Settings.store.enableMenu
|
||||
};
|
||||
|
||||
export const PopoutWindows = new Map<string, BrowserWindow>();
|
||||
|
||||
function focusWindow(window: BrowserWindow) {
|
||||
window.setAlwaysOnTop(true);
|
||||
window.focus();
|
||||
window.setAlwaysOnTop(false);
|
||||
}
|
||||
|
||||
function parseFeatureValue(feature: string) {
|
||||
if (feature === "yes") return true;
|
||||
if (feature === "no") return false;
|
||||
|
||||
const n = Number(feature);
|
||||
if (!isNaN(n)) return n;
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
function parseWindowFeatures(features: string) {
|
||||
const keyValuesParsed = features.split(",");
|
||||
|
||||
return keyValuesParsed.reduce((features, feature) => {
|
||||
const [key, value] = feature.split("=");
|
||||
if (ALLOWED_FEATURES.has(key)) features[key] = parseFeatureValue(value);
|
||||
|
||||
return features;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function createOrFocusPopup(key: string, features: string) {
|
||||
const existingWindow = PopoutWindows.get(key);
|
||||
if (existingWindow) {
|
||||
focusWindow(existingWindow);
|
||||
return <const>{ action: "deny" };
|
||||
}
|
||||
|
||||
return <const>{
|
||||
action: "allow",
|
||||
overrideBrowserWindowOptions: {
|
||||
...DEFAULT_POPOUT_OPTIONS,
|
||||
...parseWindowFeatures(features)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function setupPopout(win: BrowserWindow, key: string) {
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
PopoutWindows.set(key, win);
|
||||
|
||||
/* win.webContents.on("will-navigate", (evt, url) => {
|
||||
// maybe prevent if not origin match
|
||||
})*/
|
||||
|
||||
win.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url));
|
||||
|
||||
win.once("closed", () => {
|
||||
win.removeAllListeners();
|
||||
PopoutWindows.delete(key);
|
||||
});
|
||||
}
|
||||
97
src/main/utils/steamOS.ts
Normal file
97
src/main/utils/steamOS.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { BrowserWindow, dialog } from "electron";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
import { MessageBoxChoice } from "../constants";
|
||||
import { State } from "../settings";
|
||||
|
||||
// Bump this to re-show the prompt
|
||||
const layoutVersion = 2;
|
||||
// Get this from "show details" on the profile after exporting as a shared personal layout or using share with community
|
||||
const layoutId = "3080264545"; // Vesktop Layout v2
|
||||
const numberRegex = /^[0-9]*$/;
|
||||
|
||||
let steamPipeQueue = Promise.resolve();
|
||||
|
||||
export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1";
|
||||
|
||||
export function applyDeckKeyboardFix() {
|
||||
if (!isDeckGameMode) return;
|
||||
// Prevent constant virtual keyboard spam that eventually crashes Steam.
|
||||
process.env.GTK_IM_MODULE = "None";
|
||||
}
|
||||
|
||||
// For some reason SteamAppId is always 0 for non-steam apps so we do this insanity instead.
|
||||
function getAppId(): string | null {
|
||||
// /home/deck/.local/share/Steam/steamapps/shadercache/APPID/fozmediav1
|
||||
const path = process.env.STEAM_COMPAT_MEDIA_PATH;
|
||||
if (!path) return null;
|
||||
const pathElems = path?.split("/");
|
||||
const appId = pathElems[pathElems.length - 2];
|
||||
if (appId.match(numberRegex)) {
|
||||
console.log(`Got Steam App ID ${appId}`);
|
||||
return appId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function execSteamURL(url: string) {
|
||||
// This doesn't allow arbitrary execution despite the weird syntax.
|
||||
steamPipeQueue = steamPipeQueue.then(() =>
|
||||
writeFile(
|
||||
join(process.env.HOME || "/home/deck", ".steam", "steam.pipe"),
|
||||
// replace ' to prevent argument injection
|
||||
`'${process.env.HOME}/.local/share/Steam/ubuntu12_32/steam' '-ifrunning' '${url.replaceAll("'", "%27")}'\n`,
|
||||
"utf-8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function steamOpenURL(url: string) {
|
||||
execSteamURL(`steam://openurl/${url}`);
|
||||
}
|
||||
|
||||
export async function showGamePage() {
|
||||
const appId = getAppId();
|
||||
if (!appId) return;
|
||||
await execSteamURL(`steam://nav/games/details/${appId}`);
|
||||
}
|
||||
|
||||
async function showLayout(appId: string) {
|
||||
execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`);
|
||||
}
|
||||
|
||||
export async function askToApplySteamLayout(win: BrowserWindow) {
|
||||
const appId = getAppId();
|
||||
if (!appId) return;
|
||||
if (State.store.steamOSLayoutVersion === layoutVersion) return;
|
||||
const update = Boolean(State.store.steamOSLayoutVersion);
|
||||
|
||||
// Touch screen breaks in some menus when native touch mode is enabled on latest SteamOS beta, remove most of the update specific text once that's fixed.
|
||||
const { response } = await dialog.showMessageBox(win, {
|
||||
message: `${update ? "Update" : "Apply"} Vesktop Steam Input Layout?`,
|
||||
detail: `Would you like to ${update ? "Update" : "Apply"} Vesktop's recommended Steam Deck controller settings?
|
||||
${update ? "Click yes using the touchpad" : "Tap yes"}, then press the X button or tap Apply Layout to confirm.${
|
||||
update ? " Doing so will undo any customizations you have made." : ""
|
||||
}
|
||||
${update ? "Click" : "Tap"} no to keep your current layout.`,
|
||||
buttons: ["Yes", "No"],
|
||||
cancelId: MessageBoxChoice.Cancel,
|
||||
defaultId: MessageBoxChoice.Default,
|
||||
type: "question"
|
||||
});
|
||||
|
||||
if (State.store.steamOSLayoutVersion !== layoutVersion) {
|
||||
State.store.steamOSLayoutVersion = layoutVersion;
|
||||
}
|
||||
|
||||
if (response === MessageBoxChoice.Cancel) return;
|
||||
|
||||
await showLayout(appId);
|
||||
}
|
||||
@@ -1,19 +1,24 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import type { RequestOptions } from "https";
|
||||
import { mkdirSync } from "fs";
|
||||
import { access, constants as FsConstants, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
|
||||
import { downloadFile, simpleGet } from "./http";
|
||||
import { downloadFile, fetchie } from "./http";
|
||||
|
||||
const API_BASE = "https://api.github.com";
|
||||
|
||||
const FILES_TO_DOWNLOAD = ["vencordDesktopMain.js", "preload.js", "vencordDesktopRenderer.js", "renderer.css"];
|
||||
export const FILES_TO_DOWNLOAD = [
|
||||
"vencordDesktopMain.js",
|
||||
"vencordDesktopPreload.js",
|
||||
"vencordDesktopRenderer.js",
|
||||
"vencordDesktopRenderer.css"
|
||||
];
|
||||
|
||||
export interface ReleaseData {
|
||||
name: string;
|
||||
@@ -26,33 +31,46 @@ export interface ReleaseData {
|
||||
}
|
||||
|
||||
export async function githubGet(endpoint: string) {
|
||||
const opts: RequestOptions = {
|
||||
const opts: RequestInit = {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": USER_AGENT
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.GITHUB_TOKEN) opts.headers!.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
if (process.env.GITHUB_TOKEN) (opts.headers! as any).Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
|
||||
return simpleGet(API_BASE + endpoint, opts);
|
||||
return fetchie(API_BASE + endpoint, opts, { retryOnNetworkError: true });
|
||||
}
|
||||
|
||||
export async function downloadVencordFiles() {
|
||||
const release = await githubGet("/repos/Vendicated/Vencord/releases/latest");
|
||||
|
||||
const { assets } = JSON.parse(release.toString("utf-8")) as ReleaseData;
|
||||
const { assets }: ReleaseData = await release.json();
|
||||
|
||||
await Promise.all(
|
||||
assets
|
||||
.filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f)))
|
||||
.map(({ name, browser_download_url }) => downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name)))
|
||||
.map(({ name, browser_download_url }) =>
|
||||
downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name), {}, { retryOnNetworkError: true })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const existsAsync = (path: string) =>
|
||||
access(path, FsConstants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
export async function isValidVencordInstall(dir: string) {
|
||||
const results = await Promise.all(["package.json", ...FILES_TO_DOWNLOAD].map(f => existsAsync(join(dir, f))));
|
||||
return !results.includes(false);
|
||||
}
|
||||
|
||||
export async function ensureVencordFiles() {
|
||||
if (existsSync(join(VENCORD_FILES_DIR, "vencordDesktopMain.js"))) return;
|
||||
if (await isValidVencordInstall(VENCORD_FILES_DIR)) return;
|
||||
|
||||
mkdirSync(VENCORD_FILES_DIR, { recursive: true });
|
||||
|
||||
await downloadVencordFiles();
|
||||
await Promise.all([downloadVencordFiles(), writeFile(join(VENCORD_FILES_DIR, "package.json"), "{}")]);
|
||||
}
|
||||
|
||||
135
src/main/venmic.ts
Normal file
135
src/main/venmic.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic";
|
||||
import { app, ipcMain } from "electron";
|
||||
import { join } from "path";
|
||||
import { IpcEvents } from "shared/IpcEvents";
|
||||
import { STATIC_DIR } from "shared/paths";
|
||||
|
||||
import { Settings } from "./settings";
|
||||
|
||||
let PatchBay: typeof PatchBayType | undefined;
|
||||
let patchBayInstance: PatchBayType | undefined;
|
||||
|
||||
let imported = false;
|
||||
let initialized = false;
|
||||
|
||||
let hasPipewirePulse = false;
|
||||
let isGlibCxxOutdated = false;
|
||||
|
||||
function importVenmic() {
|
||||
if (imported) {
|
||||
return;
|
||||
}
|
||||
|
||||
imported = true;
|
||||
|
||||
try {
|
||||
PatchBay = (require(join(STATIC_DIR, `dist/venmic-${process.arch}.node`)) as typeof import("@vencord/venmic"))
|
||||
.PatchBay;
|
||||
|
||||
hasPipewirePulse = PatchBay.hasPipeWire();
|
||||
} catch (e: any) {
|
||||
console.error("Failed to import venmic", e);
|
||||
isGlibCxxOutdated = (e?.stack || e?.message || "").toLowerCase().includes("glibc");
|
||||
}
|
||||
}
|
||||
|
||||
function obtainVenmic() {
|
||||
if (!imported) {
|
||||
importVenmic();
|
||||
}
|
||||
|
||||
if (PatchBay && !initialized) {
|
||||
initialized = true;
|
||||
|
||||
try {
|
||||
patchBayInstance = new PatchBay();
|
||||
} catch (e: any) {
|
||||
console.error("Failed to instantiate venmic", e);
|
||||
}
|
||||
}
|
||||
|
||||
return patchBayInstance;
|
||||
}
|
||||
|
||||
function getRendererAudioServicePid() {
|
||||
return (
|
||||
app
|
||||
.getAppMetrics()
|
||||
.find(proc => proc.name === "Audio Service")
|
||||
?.pid?.toString() ?? "owo"
|
||||
);
|
||||
}
|
||||
|
||||
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
|
||||
const audioPid = getRendererAudioServicePid();
|
||||
|
||||
const { granularSelect } = Settings.store.audio ?? {};
|
||||
|
||||
const targets = obtainVenmic()
|
||||
?.list(granularSelect ? ["node.name"] : undefined)
|
||||
.filter(s => s["application.process.id"] !== audioPid);
|
||||
|
||||
return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated };
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => {
|
||||
const pid = getRendererAudioServicePid();
|
||||
const { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {};
|
||||
|
||||
const data: LinkData = {
|
||||
include,
|
||||
exclude: [{ "application.process.id": pid }],
|
||||
ignore_devices: ignoreDevices
|
||||
};
|
||||
|
||||
if (ignoreInputMedia ?? true) {
|
||||
data.exclude.push({ "media.class": "Stream/Input/Audio" });
|
||||
}
|
||||
|
||||
if (ignoreVirtual) {
|
||||
data.exclude.push({ "node.virtual": "true" });
|
||||
}
|
||||
|
||||
if (workaround) {
|
||||
data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
|
||||
}
|
||||
|
||||
return obtainVenmic()?.link(data);
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, exclude: Node[]) => {
|
||||
const pid = getRendererAudioServicePid();
|
||||
|
||||
const { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } =
|
||||
Settings.store.audio ?? {};
|
||||
|
||||
const data: LinkData = {
|
||||
include: [],
|
||||
exclude: [{ "application.process.id": pid }, ...exclude],
|
||||
only_speakers: onlySpeakers,
|
||||
ignore_devices: ignoreDevices,
|
||||
only_default_speakers: onlyDefaultSpeakers
|
||||
};
|
||||
|
||||
if (ignoreInputMedia ?? true) {
|
||||
data.exclude.push({ "media.class": "Stream/Input/Audio" });
|
||||
}
|
||||
|
||||
if (ignoreVirtual) {
|
||||
data.exclude.push({ "node.virtual": "true" });
|
||||
}
|
||||
|
||||
if (workaround) {
|
||||
data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
|
||||
}
|
||||
|
||||
return obtainVenmic()?.link(data);
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink());
|
||||
10
src/module.d.ts
vendored
Normal file
10
src/module.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
declare module "__patches__" {
|
||||
const never: never;
|
||||
export default never;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* 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 { Settings } from "shared/settings";
|
||||
import type { LiteralUnion } from "type-fest";
|
||||
|
||||
import { IpcEvents } from "../shared/IpcEvents";
|
||||
import { invoke, sendSync } from "./typedIpc";
|
||||
@@ -24,7 +25,8 @@ export const VesktopNative = {
|
||||
relaunch: () => invoke<void>(IpcEvents.RELAUNCH),
|
||||
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION),
|
||||
setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
|
||||
supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY)
|
||||
supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY),
|
||||
getEnableHardwareAcceleration: () => sendSync<boolean>(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION)
|
||||
},
|
||||
autostart: {
|
||||
isEnabled: () => sendSync<boolean>(IpcEvents.AUTOSTART_ENABLED),
|
||||
@@ -33,14 +35,15 @@ export const VesktopNative = {
|
||||
},
|
||||
fileManager: {
|
||||
showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path),
|
||||
selectVencordDir: () => invoke<LiteralUnion<"cancelled" | "invalid", string>>(IpcEvents.SELECT_VENCORD_DIR)
|
||||
getVencordDir: () => sendSync<string | undefined>(IpcEvents.GET_VENCORD_DIR),
|
||||
selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value)
|
||||
},
|
||||
settings: {
|
||||
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
||||
set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path)
|
||||
},
|
||||
spellcheck: {
|
||||
setLanguages: (languages: readonly string[]) => invoke<void>(IpcEvents.SPELLCHECK_SET_LANGUAGES, languages),
|
||||
getAvailableLanguages: () => sendSync<string[]>(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES),
|
||||
onSpellcheckResult(cb: SpellCheckerResultCallback) {
|
||||
spellCheckCallbacks.add(cb);
|
||||
},
|
||||
@@ -51,9 +54,36 @@ export const VesktopNative = {
|
||||
addToDictionary: (word: string) => invoke<void>(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, word)
|
||||
},
|
||||
win: {
|
||||
focus: () => invoke<void>(IpcEvents.FOCUS)
|
||||
focus: () => invoke<void>(IpcEvents.FOCUS),
|
||||
close: (key?: string) => invoke<void>(IpcEvents.CLOSE, key),
|
||||
minimize: (key?: string) => invoke<void>(IpcEvents.MINIMIZE, key),
|
||||
maximize: (key?: string) => invoke<void>(IpcEvents.MAXIMIZE, key)
|
||||
},
|
||||
capturer: {
|
||||
getLargeThumbnail: (id: string) => invoke<string>(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id)
|
||||
},
|
||||
/** only available on Linux. */
|
||||
virtmic: {
|
||||
list: () =>
|
||||
invoke<
|
||||
{ ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean }
|
||||
>(IpcEvents.VIRT_MIC_LIST),
|
||||
start: (include: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START, include),
|
||||
startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude),
|
||||
stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP)
|
||||
},
|
||||
clipboard: {
|
||||
copyImage: (imageBuffer: Uint8Array, imageSrc: string) =>
|
||||
invoke<void>(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc)
|
||||
},
|
||||
debug: {
|
||||
launchGpu: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_GPU),
|
||||
launchWebrtcInternals: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS)
|
||||
},
|
||||
commands: {
|
||||
onCommand(cb: (message: IpcMessage) => void) {
|
||||
ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message));
|
||||
},
|
||||
respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer, webFrame } from "electron";
|
||||
@@ -11,8 +11,6 @@ import { IpcEvents } from "../shared/IpcEvents";
|
||||
import { VesktopNative } from "./VesktopNative";
|
||||
|
||||
contextBridge.exposeInMainWorld("VesktopNative", VesktopNative);
|
||||
// TODO: remove legacy alias once main Vencord codebase has migrated and some time has passed
|
||||
contextBridge.exposeInMainWorld("VencordDesktopNative", VesktopNative);
|
||||
|
||||
require(ipcRenderer.sendSync(IpcEvents.GET_VENCORD_PRELOAD_FILE));
|
||||
|
||||
@@ -42,5 +40,3 @@ if (IS_DEV) {
|
||||
});
|
||||
}
|
||||
// #endregion
|
||||
|
||||
VesktopNative.spellcheck.setLanguages(window.navigator.languages);
|
||||
|
||||
13
src/preload/splash.ts
Normal file
13
src/preload/splash.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 { contextBridge, ipcRenderer } from "electron";
|
||||
|
||||
contextBridge.exposeInMainWorld("VesktopSplashNative", {
|
||||
onUpdateMessage(callback: (message: string) => void) {
|
||||
ipcRenderer.on("update-splash-message", (_, message: string) => callback(message));
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { filters, waitFor } from "@vencord/types/webpack";
|
||||
import { RelationshipStore } from "@vencord/types/webpack/common";
|
||||
|
||||
import { VesktopLogger } from "./logger";
|
||||
import { Settings } from "./settings";
|
||||
|
||||
let GuildReadStateStore: any;
|
||||
@@ -15,15 +16,19 @@ let NotificationSettingsStore: any;
|
||||
export function setBadge() {
|
||||
if (Settings.store.appBadge === false) return;
|
||||
|
||||
const mentionCount = GuildReadStateStore.getTotalMentionCount();
|
||||
const pendingRequests = RelationshipStore.getPendingCount();
|
||||
const hasUnread = GuildReadStateStore.hasAnyUnread();
|
||||
const disableUnreadBadge = NotificationSettingsStore.getDisableUnreadBadge();
|
||||
try {
|
||||
const mentionCount = GuildReadStateStore.getTotalMentionCount();
|
||||
const pendingRequests = RelationshipStore.getPendingCount();
|
||||
const hasUnread = GuildReadStateStore.hasAnyUnread();
|
||||
const disableUnreadBadge = NotificationSettingsStore.getDisableUnreadBadge();
|
||||
|
||||
let totalCount = mentionCount + pendingRequests;
|
||||
if (!totalCount && hasUnread && !disableUnreadBadge) totalCount = -1;
|
||||
let totalCount = mentionCount + pendingRequests;
|
||||
if (!totalCount && hasUnread && !disableUnreadBadge) totalCount = -1;
|
||||
|
||||
VesktopNative.app.setBadgeCount(totalCount);
|
||||
VesktopNative.app.setBadgeCount(totalCount);
|
||||
} catch (e) {
|
||||
VesktopLogger.error("Failed to update badge count", e);
|
||||
}
|
||||
}
|
||||
|
||||
let toFind = 3;
|
||||
|
||||
68
src/renderer/arrpc.ts
Normal file
68
src/renderer/arrpc.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 { Logger } from "@vencord/types/utils";
|
||||
import { findLazy, findStoreLazy, onceReady } from "@vencord/types/webpack";
|
||||
import { FluxDispatcher, InviteActions } from "@vencord/types/webpack/common";
|
||||
import { IpcCommands } from "shared/IpcEvents";
|
||||
|
||||
import { onIpcCommand } from "./ipcCommands";
|
||||
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;
|
||||
};
|
||||
|
||||
onIpcCommand(IpcCommands.RPC_ACTIVITY, async jsonData => {
|
||||
if (!Settings.store.arRPC) return;
|
||||
|
||||
await onceReady;
|
||||
|
||||
const data = JSON.parse(jsonData);
|
||||
|
||||
if (data.socketId === "STREAMERMODE" && StreamerModeStore.autoToggle) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "STREAMER_MODE_UPDATE",
|
||||
key: "enabled",
|
||||
value: data.activity?.application_id === "STREAMERMODE"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
arRPC.handleEvent(new MessageEvent("message", { data: jsonData }));
|
||||
});
|
||||
|
||||
onIpcCommand(IpcCommands.RPC_INVITE, async code => {
|
||||
const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal");
|
||||
if (!invite) return false;
|
||||
|
||||
VesktopNative.win.focus();
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "INVITE_MODAL_OPEN",
|
||||
invite,
|
||||
code,
|
||||
context: "APP"
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const { DEEP_LINK } = findLazy(m => m.DEEP_LINK?.handler);
|
||||
|
||||
onIpcCommand(IpcCommands.RPC_DEEP_LINK, async data => {
|
||||
logger.debug("Opening deep link:", data);
|
||||
try {
|
||||
DEEP_LINK.handler({ args: data });
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error("Failed to open deep link:", err);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
9
src/renderer/common.ts
Normal file
9
src/renderer/common.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* 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 { findStoreLazy } from "@vencord/types/webpack";
|
||||
|
||||
export const MediaEngineStore = findStoreLazy("MediaEngineStore");
|
||||
@@ -1,30 +1,54 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./screenSharePicker.css";
|
||||
|
||||
import { closeModal, Modals, openModal, useAwaiter } from "@vencord/types/utils";
|
||||
import { findStoreLazy } from "@vencord/types/webpack";
|
||||
import { Button, Card, Forms, Switch, Text, useState } from "@vencord/types/webpack/common";
|
||||
import { closeModal, Logger, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils";
|
||||
import { onceReady } from "@vencord/types/webpack";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
FluxDispatcher,
|
||||
Forms,
|
||||
Select,
|
||||
Switch,
|
||||
Text,
|
||||
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 { isWindows } from "renderer/utils";
|
||||
import { State, useSettings, useVesktopState } from "renderer/settings";
|
||||
import { classNameFactory, isLinux, isWindows } from "renderer/utils";
|
||||
|
||||
const StreamResolutions = ["480", "720", "1080", "1440"] as const;
|
||||
const StreamResolutions = ["480", "720", "1080", "1440", "2160"] as const;
|
||||
const StreamFps = ["15", "30", "60"] as const;
|
||||
|
||||
const MediaEngineStore = findStoreLazy("MediaEngineStore");
|
||||
const cl = classNameFactory("vcd-screen-picker-");
|
||||
|
||||
export type StreamResolution = (typeof StreamResolutions)[number];
|
||||
export type StreamFps = (typeof StreamFps)[number];
|
||||
|
||||
type SpecialSource = "None" | "Entire System";
|
||||
|
||||
type AudioSource = SpecialSource | Node;
|
||||
type AudioSources = SpecialSource | Node[];
|
||||
|
||||
interface AudioItem {
|
||||
name: string;
|
||||
value: AudioSource;
|
||||
}
|
||||
|
||||
interface StreamSettings {
|
||||
resolution: StreamResolution;
|
||||
fps: StreamFps;
|
||||
audio: boolean;
|
||||
contentHint?: string;
|
||||
includeSources?: AudioSources;
|
||||
excludeSources?: AudioSources;
|
||||
}
|
||||
|
||||
export interface StreamPick extends StreamSettings {
|
||||
@@ -37,7 +61,9 @@ interface Source {
|
||||
url: string;
|
||||
}
|
||||
|
||||
let currentSettings: StreamSettings | null = null;
|
||||
export let currentSettings: StreamSettings | null = null;
|
||||
|
||||
const logger = new Logger("VesktopScreenShare");
|
||||
|
||||
addPatch({
|
||||
patches: [
|
||||
@@ -50,10 +76,11 @@ addPatch({
|
||||
}
|
||||
],
|
||||
patchStreamQuality(opts: any) {
|
||||
if (!currentSettings) return;
|
||||
const { screenshareQuality } = State.store;
|
||||
if (!screenshareQuality) return;
|
||||
|
||||
const framerate = Number(currentSettings.fps);
|
||||
const height = Number(currentSettings.resolution);
|
||||
const framerate = Number(screenshareQuality.frameRate);
|
||||
const height = Number(screenshareQuality.resolution);
|
||||
const width = Math.round(height * (16 / 9));
|
||||
|
||||
Object.assign(opts, {
|
||||
@@ -61,6 +88,14 @@ addPatch({
|
||||
bitrateMax: 8000000,
|
||||
bitrateTarget: 600000
|
||||
});
|
||||
if (opts?.encode) {
|
||||
Object.assign(opts.encode, {
|
||||
framerate,
|
||||
width,
|
||||
height,
|
||||
pixelCount: height * width
|
||||
});
|
||||
}
|
||||
Object.assign(opts.capture, {
|
||||
framerate,
|
||||
width,
|
||||
@@ -70,18 +105,48 @@ addPatch({
|
||||
}
|
||||
});
|
||||
|
||||
export function openScreenSharePicker(screens: Source[]) {
|
||||
if (isLinux) {
|
||||
onceReady.then(() => {
|
||||
FluxDispatcher.subscribe("STREAM_CLOSE", ({ streamKey }: { streamKey: string }) => {
|
||||
const owner = streamKey.split(":").at(-1);
|
||||
|
||||
if (owner !== UserStore.getCurrentUser().id) {
|
||||
return;
|
||||
}
|
||||
|
||||
VesktopNative.virtmic.stop();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
|
||||
let didSubmit = false;
|
||||
return new Promise<StreamPick>((resolve, reject) => {
|
||||
const key = openModal(
|
||||
props => (
|
||||
<ModalComponent
|
||||
screens={screens}
|
||||
modalProps={props}
|
||||
submit={resolve}
|
||||
submit={async v => {
|
||||
didSubmit = true;
|
||||
|
||||
if (v.includeSources && v.includeSources !== "None") {
|
||||
if (v.includeSources === "Entire System") {
|
||||
await VesktopNative.virtmic.startSystem(
|
||||
!v.excludeSources || isSpecialSource(v.excludeSources) ? [] : v.excludeSources
|
||||
);
|
||||
} else {
|
||||
await VesktopNative.virtmic.start(v.includeSources);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(v);
|
||||
}}
|
||||
close={() => {
|
||||
props.onClose();
|
||||
reject("Aborted");
|
||||
if (!didSubmit) reject("Aborted");
|
||||
}}
|
||||
skipPicker={skipPicker}
|
||||
/>
|
||||
),
|
||||
{
|
||||
@@ -96,162 +161,623 @@ export function openScreenSharePicker(screens: Source[]) {
|
||||
|
||||
function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) {
|
||||
return (
|
||||
<div className="vcd-screen-picker-grid">
|
||||
<div className={cl("screen-grid")}>
|
||||
{screens.map(({ id, name, url }) => (
|
||||
<label key={id}>
|
||||
<input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} />
|
||||
<label key={id} className={cl("screen-label")}>
|
||||
<input
|
||||
type="radio"
|
||||
className={cl("screen-radio")}
|
||||
name="screen"
|
||||
value={id}
|
||||
onChange={() => chooseScreen(id)}
|
||||
/>
|
||||
|
||||
<img src={url} alt="" />
|
||||
<Text variant="text-sm/normal">{name}</Text>
|
||||
<Text className={cl("screen-name")} variant="text-sm/normal">
|
||||
{name}
|
||||
</Text>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamSettings({
|
||||
function AudioSettingsModal({
|
||||
modalProps,
|
||||
close,
|
||||
setAudioSources
|
||||
}: {
|
||||
modalProps: any;
|
||||
close: () => void;
|
||||
setAudioSources: (s: AudioSources) => void;
|
||||
}) {
|
||||
const Settings = useSettings();
|
||||
|
||||
return (
|
||||
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<Modals.ModalHeader className={cl("header")}>
|
||||
<Forms.FormTitle tag="h2" className={cl("header-title")}>
|
||||
Venmic Settings
|
||||
</Forms.FormTitle>
|
||||
<Modals.ModalCloseButton onClick={close} />
|
||||
</Modals.ModalHeader>
|
||||
<Modals.ModalContent className={cl("modal")}>
|
||||
<Switch
|
||||
hideBorder
|
||||
onChange={v => (Settings.audio = { ...Settings.audio, workaround: v })}
|
||||
value={Settings.audio?.workaround ?? false}
|
||||
note={
|
||||
<>
|
||||
Work around an issue that causes the microphone to be shared instead of the correct audio.
|
||||
Only enable if you're experiencing this issue.
|
||||
</>
|
||||
}
|
||||
>
|
||||
Microphone Workaround
|
||||
</Switch>
|
||||
<Switch
|
||||
hideBorder
|
||||
onChange={v => (Settings.audio = { ...Settings.audio, onlySpeakers: v })}
|
||||
value={Settings.audio?.onlySpeakers ?? true}
|
||||
note={
|
||||
<>
|
||||
When sharing entire desktop audio, only share apps that play to a speaker. You may want to
|
||||
disable this when using "mix bussing".
|
||||
</>
|
||||
}
|
||||
>
|
||||
Only Speakers
|
||||
</Switch>
|
||||
<Switch
|
||||
hideBorder
|
||||
onChange={v => (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })}
|
||||
value={Settings.audio?.onlyDefaultSpeakers ?? true}
|
||||
note={
|
||||
<>
|
||||
When sharing entire desktop audio, only share apps that play to the <b>default</b> speakers.
|
||||
You may want to disable this when using "mix bussing".
|
||||
</>
|
||||
}
|
||||
>
|
||||
Only Default Speakers
|
||||
</Switch>
|
||||
<Switch
|
||||
hideBorder
|
||||
onChange={v => (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })}
|
||||
value={Settings.audio?.ignoreInputMedia ?? true}
|
||||
note={<>Exclude nodes that are intended to capture audio.</>}
|
||||
>
|
||||
Ignore Inputs
|
||||
</Switch>
|
||||
<Switch
|
||||
hideBorder
|
||||
onChange={v => (Settings.audio = { ...Settings.audio, ignoreVirtual: v })}
|
||||
value={Settings.audio?.ignoreVirtual ?? false}
|
||||
note={
|
||||
<>
|
||||
Exclude virtual nodes, such as nodes belonging to loopbacks. This might be useful when using
|
||||
"mix bussing".
|
||||
</>
|
||||
}
|
||||
>
|
||||
Ignore Virtual
|
||||
</Switch>
|
||||
<Switch
|
||||
hideBorder
|
||||
onChange={v =>
|
||||
(Settings.audio = {
|
||||
...Settings.audio,
|
||||
ignoreDevices: v,
|
||||
deviceSelect: v ? false : Settings.audio?.deviceSelect
|
||||
})
|
||||
}
|
||||
value={Settings.audio?.ignoreDevices ?? true}
|
||||
note={<>Exclude device nodes, such as nodes belonging to microphones or speakers.</>}
|
||||
>
|
||||
Ignore Devices
|
||||
</Switch>
|
||||
<Switch
|
||||
hideBorder
|
||||
onChange={value => {
|
||||
Settings.audio = { ...Settings.audio, granularSelect: value };
|
||||
setAudioSources("None");
|
||||
}}
|
||||
value={Settings.audio?.granularSelect ?? false}
|
||||
note={<>Allow to select applications more granularly.</>}
|
||||
>
|
||||
Granular Selection
|
||||
</Switch>
|
||||
<Switch
|
||||
hideBorder
|
||||
onChange={value => {
|
||||
Settings.audio = { ...Settings.audio, deviceSelect: value };
|
||||
setAudioSources("None");
|
||||
}}
|
||||
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}>
|
||||
Back
|
||||
</Button>
|
||||
</Modals.ModalFooter>
|
||||
</Modals.ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
function OptionRadio<Settings extends object, Key extends keyof Settings>(props: {
|
||||
options: Array<string> | ReadonlyArray<string>;
|
||||
labels?: Array<string>;
|
||||
settings: Settings;
|
||||
settingsKey: Key;
|
||||
onChange: (option: string) => void;
|
||||
}) {
|
||||
const { options, settings, settingsKey, labels, onChange } = props;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<input
|
||||
className={cl("option-input")}
|
||||
type="radio"
|
||||
name="fps"
|
||||
value={option}
|
||||
checked={settings[settingsKey] === option}
|
||||
onChange={() => onChange(option)}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamSettingsUi({
|
||||
source,
|
||||
settings,
|
||||
setSettings
|
||||
setSettings,
|
||||
skipPicker
|
||||
}: {
|
||||
source: Source;
|
||||
settings: StreamSettings;
|
||||
setSettings: Dispatch<SetStateAction<StreamSettings>>;
|
||||
skipPicker: boolean;
|
||||
}) {
|
||||
const [thumb] = useAwaiter(() => VesktopNative.capturer.getLargeThumbnail(source.id), {
|
||||
fallbackValue: source.url,
|
||||
deps: [source.id]
|
||||
});
|
||||
const Settings = useSettings();
|
||||
const qualitySettings = State.store.screenshareQuality!;
|
||||
|
||||
const [thumb] = useAwaiter(
|
||||
() => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)),
|
||||
{
|
||||
fallbackValue: source.url,
|
||||
deps: [source.id]
|
||||
}
|
||||
);
|
||||
|
||||
const openSettings = () => {
|
||||
const key = openModal(props => (
|
||||
<AudioSettingsModal
|
||||
modalProps={props}
|
||||
close={() => props.onClose()}
|
||||
setAudioSources={sources =>
|
||||
setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources }))
|
||||
}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Forms.FormTitle>What you're streaming</Forms.FormTitle>
|
||||
<Card className="vcd-screen-picker-card vcd-screen-picker-preview">
|
||||
<img src={thumb} alt="" />
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
<Forms.FormTitle>Stream Settings</Forms.FormTitle>
|
||||
|
||||
<Card className="vcd-screen-picker-card">
|
||||
<div className="vcd-screen-picker-quality">
|
||||
<section>
|
||||
<Card className={cl("card")}>
|
||||
<div className={cl("quality")}>
|
||||
<section className={cl("quality-section")}>
|
||||
<Forms.FormTitle>Resolution</Forms.FormTitle>
|
||||
<div className="vcd-screen-picker-radios">
|
||||
{StreamResolutions.map(res => (
|
||||
<label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}>
|
||||
<Text variant="text-sm/bold">{res}</Text>
|
||||
<input
|
||||
type="radio"
|
||||
name="resolution"
|
||||
value={res}
|
||||
checked={settings.resolution === res}
|
||||
onChange={() => setSettings(s => ({ ...s, resolution: res }))}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<OptionRadio
|
||||
options={StreamResolutions}
|
||||
settings={qualitySettings}
|
||||
settingsKey="resolution"
|
||||
onChange={value => (qualitySettings.resolution = value)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section className={cl("quality-section")}>
|
||||
<Forms.FormTitle>Frame Rate</Forms.FormTitle>
|
||||
<div className="vcd-screen-picker-radios">
|
||||
{StreamFps.map(fps => (
|
||||
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
|
||||
<Text variant="text-sm/bold">{fps}</Text>
|
||||
<input
|
||||
type="radio"
|
||||
name="fps"
|
||||
value={fps}
|
||||
checked={settings.fps === fps}
|
||||
onChange={() => setSettings(s => ({ ...s, fps }))}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
<OptionRadio
|
||||
options={StreamFps}
|
||||
settings={qualitySettings}
|
||||
settingsKey="frameRate"
|
||||
onChange={value => (qualitySettings.frameRate = value)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<div className={cl("quality")}>
|
||||
<section className={cl("quality-section")}>
|
||||
<Forms.FormTitle>Content Type</Forms.FormTitle>
|
||||
<div>
|
||||
<OptionRadio
|
||||
options={["motion", "detail"]}
|
||||
labels={["Prefer Smoothness", "Prefer Clarity"]}
|
||||
settings={settings}
|
||||
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>
|
||||
</div>
|
||||
{isWindows && (
|
||||
<Switch
|
||||
value={settings.audio}
|
||||
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
|
||||
hideBorder
|
||||
className={cl("audio")}
|
||||
>
|
||||
Stream With Audio
|
||||
</Switch>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{isWindows && (
|
||||
<Switch
|
||||
value={settings.audio}
|
||||
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
|
||||
hideBorder
|
||||
className="vcd-screen-picker-audio"
|
||||
>
|
||||
Stream With Audio
|
||||
</Switch>
|
||||
{isLinux && (
|
||||
<AudioSourcePickerLinux
|
||||
openSettings={openSettings}
|
||||
includeSources={settings.includeSources}
|
||||
excludeSources={settings.excludeSources}
|
||||
deviceSelect={Settings.audio?.deviceSelect}
|
||||
granularSelect={Settings.audio?.granularSelect}
|
||||
setIncludeSources={sources => setSettings(s => ({ ...s, includeSources: sources }))}
|
||||
setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isSpecialSource(value?: AudioSource | AudioSources): value is SpecialSource {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
function hasMatchingProps(value: Node, other: Node) {
|
||||
return Object.keys(value).every(key => value[key] === other[key]);
|
||||
}
|
||||
|
||||
function mapToAudioItem(node: AudioSource, granularSelect?: boolean, deviceSelect?: boolean): AudioItem[] {
|
||||
if (isSpecialSource(node)) {
|
||||
return [{ name: node, value: node }];
|
||||
}
|
||||
|
||||
const rtn: AudioItem[] = [];
|
||||
|
||||
const mediaClass = node["media.class"];
|
||||
|
||||
if (mediaClass?.includes("Video") || mediaClass?.includes("Midi")) {
|
||||
return rtn;
|
||||
}
|
||||
|
||||
if (!deviceSelect && node["device.id"]) {
|
||||
return rtn;
|
||||
}
|
||||
|
||||
const name = node["application.name"];
|
||||
|
||||
if (name) {
|
||||
rtn.push({ name: name, value: { "application.name": name } });
|
||||
}
|
||||
|
||||
if (!granularSelect) {
|
||||
return rtn;
|
||||
}
|
||||
|
||||
const rawName = node["node.name"];
|
||||
|
||||
if (!name) {
|
||||
rtn.push({ name: rawName, value: { "node.name": rawName } });
|
||||
}
|
||||
|
||||
const binary = node["application.process.binary"];
|
||||
|
||||
if (!name && binary) {
|
||||
rtn.push({ name: binary, value: { "application.process.binary": binary } });
|
||||
}
|
||||
|
||||
const pid = node["application.process.id"];
|
||||
|
||||
const first = rtn[0];
|
||||
const firstValues = first.value as Node;
|
||||
|
||||
if (pid) {
|
||||
rtn.push({
|
||||
name: `${first.name} (${pid})`,
|
||||
value: { ...firstValues, "application.process.id": pid }
|
||||
});
|
||||
}
|
||||
|
||||
const mediaName = node["media.name"];
|
||||
|
||||
if (mediaName) {
|
||||
rtn.push({
|
||||
name: `${first.name} [${mediaName}]`,
|
||||
value: { ...firstValues, "media.name": mediaName }
|
||||
});
|
||||
}
|
||||
|
||||
if (mediaClass) {
|
||||
rtn.push({
|
||||
name: `${first.name} [${mediaClass}]`,
|
||||
value: { ...firstValues, "media.class": mediaClass }
|
||||
});
|
||||
}
|
||||
|
||||
return rtn;
|
||||
}
|
||||
|
||||
function isItemSelected(sources?: AudioSources) {
|
||||
return (value: AudioSource) => {
|
||||
if (!sources) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSpecialSource(sources) || isSpecialSource(value)) {
|
||||
return sources === value;
|
||||
}
|
||||
|
||||
return sources.some(source => hasMatchingProps(source, value));
|
||||
};
|
||||
}
|
||||
|
||||
function updateItems(setSources: (s: AudioSources) => void, sources?: AudioSources) {
|
||||
return (value: AudioSource) => {
|
||||
if (isSpecialSource(value)) {
|
||||
setSources(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSpecialSource(sources)) {
|
||||
setSources([value]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isItemSelected(sources)(value)) {
|
||||
setSources(sources?.filter(x => !hasMatchingProps(x, value)) ?? "None");
|
||||
return;
|
||||
}
|
||||
|
||||
setSources([...(sources || []), value]);
|
||||
};
|
||||
}
|
||||
|
||||
function AudioSourcePickerLinux({
|
||||
includeSources,
|
||||
excludeSources,
|
||||
deviceSelect,
|
||||
granularSelect,
|
||||
openSettings,
|
||||
setIncludeSources,
|
||||
setExcludeSources
|
||||
}: {
|
||||
includeSources?: AudioSources;
|
||||
excludeSources?: AudioSources;
|
||||
deviceSelect?: boolean;
|
||||
granularSelect?: boolean;
|
||||
openSettings: () => void;
|
||||
setIncludeSources: (s: AudioSources) => void;
|
||||
setExcludeSources: (s: AudioSources) => void;
|
||||
}) {
|
||||
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
|
||||
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true }
|
||||
});
|
||||
|
||||
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
|
||||
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false);
|
||||
|
||||
if (!sources.ok && sources.isGlibCxxOutdated) {
|
||||
return (
|
||||
<Forms.FormText>
|
||||
Failed to retrieve Audio Sources because your C++ library is too old to run
|
||||
<a href="https://github.com/Vencord/venmic" target="_blank">
|
||||
venmic
|
||||
</a>
|
||||
. See{" "}
|
||||
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
|
||||
this guide
|
||||
</a>{" "}
|
||||
for possible solutions.
|
||||
</Forms.FormText>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPipewirePulse && !ignorePulseWarning) {
|
||||
return (
|
||||
<Text variant="text-sm/normal">
|
||||
Could not find pipewire-pulse. See{" "}
|
||||
<a href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install" target="_blank">
|
||||
this guide
|
||||
</a>{" "}
|
||||
on how to switch to pipewire. <br />
|
||||
You can still continue, however, please{" "}
|
||||
<b>beware that you can only share audio of apps that are running under pipewire</b>.{" "}
|
||||
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing!</a>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const specialSources: SpecialSource[] = ["None", "Entire System"] as const;
|
||||
|
||||
const uniqueName = (value: AudioItem, index: number, list: AudioItem[]) =>
|
||||
list.findIndex(x => x.name === value.name) === index;
|
||||
|
||||
const allSources = sources.ok
|
||||
? [...specialSources, ...sources.targets]
|
||||
.map(target => mapToAudioItem(target, granularSelect, deviceSelect))
|
||||
.flat()
|
||||
.filter(uniqueName)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cl({ quality: includeSources === "Entire System" })}>
|
||||
<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>
|
||||
<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}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
<Button color={Button.Colors.TRANSPARENT} onClick={openSettings} className={cl("settings-button")}>
|
||||
Open Audio Settings
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ModalComponent({
|
||||
screens,
|
||||
modalProps,
|
||||
submit,
|
||||
close
|
||||
close,
|
||||
skipPicker
|
||||
}: {
|
||||
screens: Source[];
|
||||
modalProps: any;
|
||||
submit: (data: StreamPick) => void;
|
||||
close: () => void;
|
||||
skipPicker: boolean;
|
||||
}) {
|
||||
const [selected, setSelected] = useState<string>();
|
||||
const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0);
|
||||
const [settings, setSettings] = useState<StreamSettings>({
|
||||
resolution: "1080",
|
||||
fps: "60",
|
||||
audio: true
|
||||
contentHint: "motion",
|
||||
audio: true,
|
||||
includeSources: "None"
|
||||
});
|
||||
const qualitySettings = (useVesktopState().screenshareQuality ??= {
|
||||
resolution: "720",
|
||||
frameRate: "30"
|
||||
});
|
||||
|
||||
return (
|
||||
<Modals.ModalRoot {...modalProps}>
|
||||
<Modals.ModalHeader className="vcd-screen-picker-header">
|
||||
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
|
||||
<Modals.ModalHeader className={cl("header")}>
|
||||
<Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle>
|
||||
<Modals.ModalCloseButton onClick={close} />
|
||||
</Modals.ModalHeader>
|
||||
|
||||
<Modals.ModalContent className="vcd-screen-picker-modal">
|
||||
<Modals.ModalContent className={cl("modal")}>
|
||||
{!selected ? (
|
||||
<ScreenPicker screens={screens} chooseScreen={setSelected} />
|
||||
) : (
|
||||
<StreamSettings
|
||||
<StreamSettingsUi
|
||||
source={screens.find(s => s.id === selected)!}
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
skipPicker={skipPicker}
|
||||
/>
|
||||
)}
|
||||
</Modals.ModalContent>
|
||||
|
||||
<Modals.ModalFooter className="vcd-screen-picker-footer">
|
||||
<Modals.ModalFooter className={cl("footer")}>
|
||||
<Button
|
||||
disabled={!selected}
|
||||
onClick={() => {
|
||||
currentSettings = settings;
|
||||
|
||||
// If there are 2 connections, the second one is the existing stream.
|
||||
// In that case, we patch its quality
|
||||
const conn = [...MediaEngineStore.getMediaEngine().connections][1];
|
||||
if (conn && conn.videoStreamParameters.length > 0) {
|
||||
const height = Number(settings.resolution);
|
||||
try {
|
||||
const frameRate = Number(qualitySettings.frameRate);
|
||||
const height = Number(qualitySettings.resolution);
|
||||
const width = Math.round(height * (16 / 9));
|
||||
Object.assign(conn.videoStreamParameters[0], {
|
||||
maxFrameRate: Number(settings.fps),
|
||||
maxPixelCount: width * height,
|
||||
maxBitrate: 8000000,
|
||||
maxResolution: {
|
||||
type: "fixed",
|
||||
width,
|
||||
height
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submit({
|
||||
id: selected!,
|
||||
...settings
|
||||
});
|
||||
const conn = [...MediaEngineStore.getMediaEngine().connections].find(
|
||||
connection => connection.streamUserId === UserStore.getCurrentUser().id
|
||||
);
|
||||
|
||||
if (conn) {
|
||||
conn.videoStreamParameters[0].maxFrameRate = frameRate;
|
||||
conn.videoStreamParameters[0].maxResolution.height = height;
|
||||
conn.videoStreamParameters[0].maxResolution.width = width;
|
||||
}
|
||||
|
||||
submit({
|
||||
id: selected!,
|
||||
...settings
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
const conn = [...MediaEngineStore.getMediaEngine().connections].find(
|
||||
connection => connection.streamUserId === UserStore.getCurrentUser().id
|
||||
);
|
||||
if (!conn) return;
|
||||
|
||||
const track = conn.input.stream.getVideoTracks()[0];
|
||||
|
||||
const constraints = {
|
||||
...track.getConstraints(),
|
||||
frameRate: { min: frameRate, ideal: frameRate },
|
||||
width: { min: 640, ideal: width, max: width },
|
||||
height: { min: 480, ideal: height, max: height },
|
||||
advanced: [{ width: width, height: height }],
|
||||
resizeMode: "none"
|
||||
};
|
||||
|
||||
try {
|
||||
await track.applyConstraints(constraints);
|
||||
|
||||
logger.info(
|
||||
"Applied constraints successfully. New constraints:",
|
||||
track.getConstraints()
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Failed to apply constraints.", e);
|
||||
}
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
logger.error("Error while submitting stream.", error);
|
||||
}
|
||||
|
||||
close();
|
||||
}}
|
||||
@@ -259,7 +785,7 @@ function ModalComponent({
|
||||
Go Live
|
||||
</Button>
|
||||
|
||||
{selected ? (
|
||||
{selected && !skipPicker ? (
|
||||
<Button color={Button.Colors.TRANSPARENT} onClick={() => setSelected(void 0)}>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
*/
|
||||
|
||||
import "./settings.css";
|
||||
|
||||
import { Margins } from "@vencord/types/utils";
|
||||
import { Button, Forms, Select, Switch, Text, useState } from "@vencord/types/webpack/common";
|
||||
import { setBadge } from "renderer/appBadge";
|
||||
import { useSettings } from "renderer/settings";
|
||||
import { isMac } from "renderer/utils";
|
||||
import { isTruthy } from "shared/utils/guards";
|
||||
|
||||
export default function SettingsUi() {
|
||||
const Settings = useSettings();
|
||||
const supportsWindowsTransparency = VesktopNative.app.supportsWindowsTransparency();
|
||||
|
||||
const { autostart } = VesktopNative;
|
||||
const [autoStartEnabled, setAutoStartEnabled] = useState(autostart.isEnabled());
|
||||
|
||||
const allSwitches: Array<false | [keyof typeof Settings, string, string, boolean?, (() => boolean)?]> = [
|
||||
!isMac && ["tray", "Tray Icon", "Add a tray icon for Vesktop", true],
|
||||
!isMac && [
|
||||
"minimizeToTray",
|
||||
"Minimize to tray",
|
||||
"Hitting X will make Vesktop minimize to the tray instead of closing",
|
||||
true,
|
||||
() => Settings.tray ?? true
|
||||
],
|
||||
["arRPC", "Rich Presence", "Enables Rich Presence via arRPC", false],
|
||||
[
|
||||
"disableMinSize",
|
||||
"Disable minimum window size",
|
||||
"Allows you to make the window as small as your heart desires"
|
||||
],
|
||||
[
|
||||
"openLinksWithElectron",
|
||||
"Open Links in app (experimental)",
|
||||
"Opens links in a new Vesktop window instead of your web browser"
|
||||
],
|
||||
["staticTitle", "Static Title", 'Makes the window title "Vencord" instead of changing to the current page'],
|
||||
["enableMenu", "Enable Menu Bar", "Enables the application menu bar. Press ALT to toggle visibility."]
|
||||
];
|
||||
|
||||
const switches = allSwitches.filter(isTruthy);
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">
|
||||
Vesktop Settings
|
||||
</Text>
|
||||
|
||||
<Forms.FormTitle className={Margins.top16 + " " + Margins.bottom8}>Discord Branch</Forms.FormTitle>
|
||||
<Select
|
||||
placeholder="Stable"
|
||||
options={[
|
||||
{ label: "Stable", value: "stable", default: true },
|
||||
{ label: "Canary", value: "canary" },
|
||||
{ label: "PTB", value: "ptb" }
|
||||
]}
|
||||
closeOnSelect={true}
|
||||
select={v => (Settings.discordBranch = v)}
|
||||
isSelected={v => v === Settings.discordBranch}
|
||||
serialize={s => s}
|
||||
/>
|
||||
|
||||
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
|
||||
|
||||
<Switch
|
||||
value={autoStartEnabled}
|
||||
onChange={async v => {
|
||||
await autostart[v ? "enable" : "disable"]();
|
||||
setAutoStartEnabled(v);
|
||||
}}
|
||||
note="Automatically start Vesktop on computer start-up"
|
||||
>
|
||||
Start With System
|
||||
</Switch>
|
||||
|
||||
<Switch
|
||||
value={Settings.appBadge ?? true}
|
||||
onChange={v => {
|
||||
Settings.appBadge = v;
|
||||
if (v) setBadge();
|
||||
else VesktopNative.app.setBadgeCount(0);
|
||||
}}
|
||||
note="Show mention badge on the app icon"
|
||||
>
|
||||
Notification Badge
|
||||
</Switch>
|
||||
|
||||
{switches.map(([key, text, note, def, predicate]) => (
|
||||
<Switch
|
||||
value={(Settings[key as any] ?? def ?? false) && predicate?.() !== false}
|
||||
disabled={predicate && !predicate()}
|
||||
onChange={v => (Settings[key as any] = v)}
|
||||
note={note}
|
||||
key={key}
|
||||
>
|
||||
{text}
|
||||
</Switch>
|
||||
))}
|
||||
|
||||
{supportsWindowsTransparency && (
|
||||
<>
|
||||
<Forms.FormTitle className={Margins.top16 + " " + Margins.bottom8}>
|
||||
Transparency Options
|
||||
</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
Requires a full restart. You will need a theme that supports transparency for this to work.
|
||||
</Forms.FormText>
|
||||
|
||||
<Select
|
||||
placeholder="None"
|
||||
options={[
|
||||
{
|
||||
label: "None",
|
||||
value: "none",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "Mica (incorporates system theme + desktop wallpaper to paint the background)",
|
||||
value: "mica"
|
||||
},
|
||||
{ label: "Tabbed (variant of Mica with stronger background tinting)", value: "tabbed" },
|
||||
{
|
||||
label: "Acrylic (blurs the window behind Vesktop for a translucent background)",
|
||||
value: "acrylic"
|
||||
}
|
||||
]}
|
||||
closeOnSelect={true}
|
||||
select={v => (Settings.transparencyOption = v)}
|
||||
isSelected={v => v === Settings.transparencyOption}
|
||||
serialize={s => s}
|
||||
/>
|
||||
|
||||
<Forms.FormDivider className={Margins.top16 + " " + Margins.bottom16} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Forms.FormTitle>Vencord Location</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
Vencord files are loaded from{" "}
|
||||
{Settings.vencordDir ? (
|
||||
<a
|
||||
href="about:blank"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
VesktopNative.fileManager.showItemInFolder(Settings.vencordDir!);
|
||||
}}
|
||||
>
|
||||
{Settings.vencordDir}
|
||||
</a>
|
||||
) : (
|
||||
"the default location"
|
||||
)}
|
||||
</Forms.FormText>
|
||||
<div className="vcd-location-btns">
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={async () => {
|
||||
const choice = await VesktopNative.fileManager.selectVencordDir();
|
||||
switch (choice) {
|
||||
case "cancelled":
|
||||
case "invalid":
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
Settings.vencordDir = choice;
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => (Settings.vencordDir = void 0)}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export * as ScreenShare from "./ScreenSharePicker";
|
||||
export { default as Settings } from "./Settings";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-header h1 {
|
||||
.vcd-screen-picker-header-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -11,34 +11,36 @@
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-grid {
|
||||
.vcd-screen-picker-card {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
/* Screen Grid */
|
||||
.vcd-screen-picker-screen-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2em 1em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-grid input {
|
||||
.vcd-screen-picker-screen-radio {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-selected img {
|
||||
border: 2px solid var(--brand-experiment);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-grid label {
|
||||
.vcd-screen-picker-screen-label {
|
||||
overflow: hidden;
|
||||
padding: 4px 0px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-grid label:hover {
|
||||
outline: 2px solid var(--brand-experiment);
|
||||
.vcd-screen-picker-screen-label:hover {
|
||||
outline: 2px solid var(--brand-500);
|
||||
}
|
||||
|
||||
|
||||
.vcd-screen-picker-grid div {
|
||||
.vcd-screen-picker-screen-name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -52,8 +54,13 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-preview img {
|
||||
width: 100%;
|
||||
.vcd-screen-picker-preview-img-linux {
|
||||
width: 60%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-preview-img {
|
||||
width: 90%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
@@ -65,60 +72,64 @@
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-radio input {
|
||||
display: none;
|
||||
|
||||
/* Option Radios */
|
||||
|
||||
.vcd-screen-picker-option-radios {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-radio {
|
||||
.vcd-screen-picker-option-radio {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--primary-800);
|
||||
padding: 0.3em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-radio h2 {
|
||||
margin: 0;
|
||||
.vcd-screen-picker-option-radio:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-radio[data-checked="true"] {
|
||||
background-color: var(--brand-experiment);
|
||||
border-color: var(--brand-experiment);
|
||||
.vcd-screen-picker-option-radio:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-radio[data-checked="true"] h2 {
|
||||
color: var(--interactive-active);
|
||||
.vcd-screen-picker-option-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-option-radio[data-checked="true"] {
|
||||
background-color: var(--brand-500);
|
||||
border-color: var(--brand-500);
|
||||
}
|
||||
|
||||
.vcd-screen-picker-quality {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-quality section {
|
||||
.vcd-screen-picker-quality-section {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-radios {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
.vcd-screen-picker-settings-button {
|
||||
margin-left: auto;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-radios label {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-radios label:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.vcd-screen-picker-radios label:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.vcd-location-btns {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
27
src/renderer/components/settings/AutoStartToggle.tsx
Normal file
27
src/renderer/components/settings/AutoStartToggle.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { useState } from "@vencord/types/webpack/common";
|
||||
|
||||
import { SettingsComponent } from "./Settings";
|
||||
import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch";
|
||||
|
||||
export const AutoStartToggle: SettingsComponent = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
120
src/renderer/components/settings/DeveloperOptions.tsx
Normal file
120
src/renderer/components/settings/DeveloperOptions.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 {
|
||||
Margins,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalRoot,
|
||||
ModalSize,
|
||||
openModal,
|
||||
useForceUpdater
|
||||
} from "@vencord/types/utils";
|
||||
import { Button, Forms, Text, Toasts } from "@vencord/types/webpack/common";
|
||||
import { Settings } from "shared/settings";
|
||||
|
||||
import { SettingsComponent } from "./Settings";
|
||||
|
||||
export const DeveloperOptionsButton: SettingsComponent = ({ settings }) => {
|
||||
return <Button onClick={() => openDeveloperOptionsModal(settings)}>Open Developer Settings</Button>;
|
||||
};
|
||||
|
||||
function openDeveloperOptionsModal(settings: Settings) {
|
||||
openModal(props => (
|
||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||
<ModalHeader>
|
||||
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>
|
||||
Vesktop Developer Options
|
||||
</Text>
|
||||
<ModalCloseButton onClick={props.onClose} />
|
||||
</ModalHeader>
|
||||
|
||||
<ModalContent>
|
||||
<div style={{ padding: "1em 0" }}>
|
||||
<Forms.FormTitle tag="h5">Vencord Location</Forms.FormTitle>
|
||||
<VencordLocationPicker settings={settings} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={Margins.top16}>
|
||||
Debugging
|
||||
</Forms.FormTitle>
|
||||
<div className="vcd-settings-button-grid">
|
||||
<Button onClick={() => VesktopNative.debug.launchGpu()}>Open chrome://gpu</Button>
|
||||
<Button onClick={() => VesktopNative.debug.launchWebrtcInternals()}>
|
||||
Open chrome://webrtc-internals
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
));
|
||||
}
|
||||
|
||||
const VencordLocationPicker: SettingsComponent = ({ settings }) => {
|
||||
const forceUpdate = useForceUpdater();
|
||||
const vencordDir = VesktopNative.fileManager.getVencordDir();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormText>
|
||||
Vencord files are loaded from{" "}
|
||||
{vencordDir ? (
|
||||
<a
|
||||
href="about:blank"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
VesktopNative.fileManager.showItemInFolder(vencordDir!);
|
||||
}}
|
||||
>
|
||||
{vencordDir}
|
||||
</a>
|
||||
) : (
|
||||
"the default location"
|
||||
)}
|
||||
</Forms.FormText>
|
||||
<div className="vcd-settings-button-grid">
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={async () => {
|
||||
const choice = await VesktopNative.fileManager.selectVencordDir();
|
||||
switch (choice) {
|
||||
case "cancelled":
|
||||
break;
|
||||
case "ok":
|
||||
Toasts.show({
|
||||
message: "Vencord install changed. Fully restart Vesktop to apply.",
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.SUCCESS
|
||||
});
|
||||
break;
|
||||
case "invalid":
|
||||
Toasts.show({
|
||||
message:
|
||||
"You did not choose a valid Vencord install. Make sure you're selecting the dist dir!",
|
||||
id: Toasts.genId(),
|
||||
type: Toasts.Type.FAILURE
|
||||
});
|
||||
break;
|
||||
}
|
||||
forceUpdate();
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
color={Button.Colors.RED}
|
||||
onClick={async () => {
|
||||
await VesktopNative.fileManager.selectVencordDir(null);
|
||||
forceUpdate();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
26
src/renderer/components/settings/DiscordBranchPicker.tsx
Normal file
26
src/renderer/components/settings/DiscordBranchPicker.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Select } from "@vencord/types/webpack/common";
|
||||
|
||||
import { SettingsComponent } from "./Settings";
|
||||
|
||||
export const DiscordBranchPicker: SettingsComponent = ({ settings }) => {
|
||||
return (
|
||||
<Select
|
||||
placeholder="Stable"
|
||||
options={[
|
||||
{ label: "Stable", value: "stable", default: true },
|
||||
{ label: "Canary", value: "canary" },
|
||||
{ label: "PTB", value: "ptb" }
|
||||
]}
|
||||
closeOnSelect={true}
|
||||
select={v => (settings.discordBranch = v)}
|
||||
isSelected={v => v === settings.discordBranch}
|
||||
serialize={s => s}
|
||||
/>
|
||||
);
|
||||
};
|
||||
26
src/renderer/components/settings/NotificationBadgeToggle.tsx
Normal file
26
src/renderer/components/settings/NotificationBadgeToggle.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { setBadge } from "renderer/appBadge";
|
||||
|
||||
import { SettingsComponent } from "./Settings";
|
||||
import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch";
|
||||
|
||||
export const NotificationBadgeToggle: SettingsComponent = ({ settings }) => {
|
||||
return (
|
||||
<VesktopSettingsSwitch
|
||||
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>
|
||||
);
|
||||
};
|
||||
195
src/renderer/components/settings/Settings.tsx
Normal file
195
src/renderer/components/settings/Settings.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./settings.css";
|
||||
|
||||
import { ErrorBoundary } from "@vencord/types/components";
|
||||
import { Forms, Text } from "@vencord/types/webpack/common";
|
||||
import { ComponentType } from "react";
|
||||
import { Settings, useSettings } from "renderer/settings";
|
||||
import { isMac, isWindows } from "renderer/utils";
|
||||
|
||||
import { AutoStartToggle } from "./AutoStartToggle";
|
||||
import { DeveloperOptionsButton } from "./DeveloperOptions";
|
||||
import { DiscordBranchPicker } from "./DiscordBranchPicker";
|
||||
import { NotificationBadgeToggle } from "./NotificationBadgeToggle";
|
||||
import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch";
|
||||
import { WindowsTransparencyControls } from "./WindowsTransparencyControls";
|
||||
|
||||
interface BooleanSetting {
|
||||
key: keyof typeof Settings.store;
|
||||
title: string;
|
||||
description: string;
|
||||
defaultValue: boolean;
|
||||
disabled?(): boolean;
|
||||
invisible?(): boolean;
|
||||
}
|
||||
|
||||
export type SettingsComponent = ComponentType<{ settings: typeof Settings.store }>;
|
||||
|
||||
const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>> = {
|
||||
"Discord Branch": [DiscordBranchPicker],
|
||||
"System Startup & Performance": [
|
||||
AutoStartToggle,
|
||||
{
|
||||
key: "hardwareAcceleration",
|
||||
title: "Hardware Acceleration",
|
||||
description: "Enable hardware acceleration",
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
key: "hardwareVideoAcceleration",
|
||||
title: "Video Hardware Acceleration",
|
||||
description:
|
||||
"Enable hardware video acceleration. This can improve performance of screenshare and video playback, but may cause graphical glitches and infinitely loading streams.",
|
||||
defaultValue: false,
|
||||
disabled: () => !Settings.store.hardwareAcceleration
|
||||
}
|
||||
],
|
||||
"User Interface": [
|
||||
{
|
||||
key: "customTitleBar",
|
||||
title: "Discord Titlebar",
|
||||
description: "Use Discord's custom title bar instead of the native system one. Requires a full restart.",
|
||||
defaultValue: isWindows
|
||||
},
|
||||
{
|
||||
key: "staticTitle",
|
||||
title: "Static Title",
|
||||
description: 'Makes the window title "Vesktop" instead of changing to the current page',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
key: "enableMenu",
|
||||
title: "Enable Menu Bar",
|
||||
description: "Enables the application menu bar. Press ALT to toggle visibility.",
|
||||
defaultValue: false,
|
||||
disabled: () => Settings.store.customTitleBar ?? isWindows
|
||||
},
|
||||
{
|
||||
key: "enableSplashScreen",
|
||||
title: "Enable Splash Screen",
|
||||
description:
|
||||
"Shows a small splash screen while Vesktop is loading. Disabling this option will show the main window earlier while it's still loading.",
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
key: "splashTheming",
|
||||
title: "Splash theming",
|
||||
description: "Adapt the splash window colors to your custom theme",
|
||||
defaultValue: true
|
||||
},
|
||||
WindowsTransparencyControls
|
||||
],
|
||||
Behaviour: [
|
||||
{
|
||||
key: "tray",
|
||||
title: "Tray Icon",
|
||||
description: "Add a tray icon for Vesktop",
|
||||
defaultValue: true,
|
||||
invisible: () => isMac
|
||||
},
|
||||
{
|
||||
key: "minimizeToTray",
|
||||
title: "Minimize to tray",
|
||||
description: "Hitting X will make Vesktop minimize to the tray instead of closing",
|
||||
defaultValue: true,
|
||||
invisible: () => isMac,
|
||||
disabled: () => Settings.store.tray === false
|
||||
},
|
||||
{
|
||||
key: "clickTrayToShowHide",
|
||||
title: "Hide/Show on tray click",
|
||||
description: "Left clicking tray icon will toggle the vesktop window visibility.",
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
key: "disableMinSize",
|
||||
title: "Disable minimum window size",
|
||||
description: "Allows you to make the window as small as your heart desires",
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
key: "disableSmoothScroll",
|
||||
title: "Disable smooth scrolling",
|
||||
description: "Disables smooth scrolling",
|
||||
defaultValue: false
|
||||
}
|
||||
],
|
||||
Notifications: [NotificationBadgeToggle],
|
||||
Miscellaneous: [
|
||||
{
|
||||
key: "arRPC",
|
||||
title: "Rich Presence",
|
||||
description: "Enables Rich Presence via arRPC",
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
{
|
||||
key: "openLinksWithElectron",
|
||||
title: "Open Links in app (experimental)",
|
||||
description: "Opens links in a new Vesktop window instead of your web browser",
|
||||
defaultValue: false
|
||||
}
|
||||
],
|
||||
"Developer Options": [DeveloperOptionsButton]
|
||||
};
|
||||
|
||||
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">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<div className="vcd-settings-category-content">
|
||||
{settings.map(Setting => {
|
||||
if (typeof Setting === "function") return <Setting settings={Settings} />;
|
||||
|
||||
const { defaultValue, title, description, key, disabled, invisible } = Setting;
|
||||
if (invisible?.()) return null;
|
||||
|
||||
return (
|
||||
<VesktopSettingsSwitch
|
||||
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" />}
|
||||
</div>
|
||||
));
|
||||
|
||||
return <>{sections}</>;
|
||||
}
|
||||
|
||||
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>
|
||||
<SettingsSections />
|
||||
</Forms.FormSection>
|
||||
);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Failed to render the Vesktop Settings tab. If this issue persists, try to right click the Vesktop tray icon, then click 'Repair Vencord'. And make sure your Vesktop is up to date."
|
||||
}
|
||||
);
|
||||
16
src/renderer/components/settings/VesktopSettingsSwitch.tsx
Normal file
16
src/renderer/components/settings/VesktopSettingsSwitch.tsx
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 { Switch } from "@vencord/types/webpack/common";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
export function VesktopSettingsSwitch(props: ComponentProps<typeof Switch>) {
|
||||
return (
|
||||
<Switch {...props} hideBorder className="vcd-settings-switch">
|
||||
{props.children}
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Margins } from "@vencord/types/utils";
|
||||
import { Forms, Select } from "@vencord/types/webpack/common";
|
||||
|
||||
import { SettingsComponent } from "./Settings";
|
||||
|
||||
export const WindowsTransparencyControls: SettingsComponent = ({ settings }) => {
|
||||
if (!VesktopNative.app.supportsWindowsTransparency()) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Forms.FormTitle className={Margins.bottom8}>Transparency Options</Forms.FormTitle>
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
Requires a full restart. You will need a theme that supports transparency for this to work.
|
||||
</Forms.FormText>
|
||||
|
||||
<Select
|
||||
placeholder="None"
|
||||
options={[
|
||||
{
|
||||
label: "None",
|
||||
value: "none",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "Mica (incorporates system theme + desktop wallpaper to paint the background)",
|
||||
value: "mica"
|
||||
},
|
||||
{ label: "Tabbed (variant of Mica with stronger background tinting)", value: "tabbed" },
|
||||
{
|
||||
label: "Acrylic (blurs the window behind Vesktop for a translucent background)",
|
||||
value: "acrylic"
|
||||
}
|
||||
]}
|
||||
closeOnSelect={true}
|
||||
select={v => (settings.transparencyOption = v)}
|
||||
isSelected={v => v === settings.transparencyOption}
|
||||
serialize={s => s}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
34
src/renderer/components/settings/settings.css
Normal file
34
src/renderer/components/settings/settings.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.vcd-settings-button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.vcd-settings-title {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.vcd-settings-category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vcd-settings-category-title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vcd-settings-category-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.vcd-settings-category-divider {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.vcd-settings-switch {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./hideGarbage.css";
|
||||
|
||||
import { waitFor } from "@vencord/types/webpack";
|
||||
|
||||
import { isFirstRun, localStorage } from "./utils";
|
||||
import { localStorage } from "./utils";
|
||||
|
||||
// Make clicking Notifications focus the window
|
||||
const originalSetOnClick = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick")!.set!;
|
||||
@@ -22,12 +18,5 @@ Object.defineProperty(Notification.prototype, "onclick", {
|
||||
configurable: true
|
||||
});
|
||||
|
||||
if (isFirstRun) {
|
||||
// Hide "Download Discord Desktop now!!!!" banner
|
||||
localStorage.setItem("hideNag", "true");
|
||||
|
||||
// Enable Desktop Notifications by default
|
||||
waitFor("setDesktopType", m => {
|
||||
m.setDesktopType("all");
|
||||
});
|
||||
}
|
||||
// Hide "Download Discord Desktop now!!!!" banner
|
||||
localStorage.setItem("hideNag", "true");
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/* Download Desktop button in guilds list */
|
||||
[class|=listItem]:has([data-list-item-id=guildsnav___app-download-button]),
|
||||
[class|=listItem]:has(+ [class|=listItem] [data-list-item-id=guildsnav___app-download-button]) {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,48 +1,33 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./fixes";
|
||||
import "./themedSplash";
|
||||
import "./ipcCommands";
|
||||
import "./appBadge";
|
||||
import "./patches";
|
||||
|
||||
console.log("read if cute :3");
|
||||
import "./fixes";
|
||||
import "./arrpc";
|
||||
import "__patches__"; // auto generated by the build script
|
||||
|
||||
export * as Components from "./components";
|
||||
import { findByPropsLazy } from "@vencord/types/webpack";
|
||||
import { FluxDispatcher } from "@vencord/types/webpack/common";
|
||||
|
||||
import SettingsUi from "./components/settings/Settings";
|
||||
import { VesktopLogger } from "./logger";
|
||||
import { Settings } from "./settings";
|
||||
export { Settings };
|
||||
|
||||
const InviteActions = findByPropsLazy("resolveInvite");
|
||||
import type SettingsPlugin from "@vencord/types/plugins/_core/settings";
|
||||
|
||||
export async function openInviteModal(code: string) {
|
||||
const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal");
|
||||
if (!invite) return false;
|
||||
VesktopLogger.log("read if cute :3");
|
||||
VesktopLogger.log("Vesktop v" + VesktopNative.app.getVersion());
|
||||
|
||||
VesktopNative.win.focus();
|
||||
const customSettingsSections = (Vencord.Plugins.plugins.Settings as any as typeof SettingsPlugin).customSections;
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "INVITE_MODAL_OPEN",
|
||||
invite,
|
||||
code,
|
||||
context: "APP"
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"];
|
||||
|
||||
arRPC.required = !!Settings.store.arRPC;
|
||||
|
||||
Settings.addChangeListener("arRPC", v => {
|
||||
arRPC.required = !!v;
|
||||
if (v && !arRPC.started) Vencord.Plugins.startPlugin(arRPC);
|
||||
else if (arRPC.started) {
|
||||
Vencord.Plugins.stopPlugin(arRPC);
|
||||
}
|
||||
});
|
||||
customSettingsSections.push(() => ({
|
||||
section: "Vesktop",
|
||||
label: "Vesktop Settings",
|
||||
element: SettingsUi,
|
||||
className: "vc-vesktop-settings"
|
||||
}));
|
||||
|
||||
54
src/renderer/ipcCommands.ts
Normal file
54
src/renderer/ipcCommands.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 { SettingsRouter } from "@vencord/types/webpack/common";
|
||||
import { IpcCommands } from "shared/IpcEvents";
|
||||
|
||||
import { openScreenSharePicker } from "./components/ScreenSharePicker";
|
||||
|
||||
type IpcCommandHandler = (data: any) => any;
|
||||
|
||||
const handlers = new Map<string, IpcCommandHandler>();
|
||||
|
||||
function respond(nonce: string, ok: boolean, data: any) {
|
||||
VesktopNative.commands.respond({ nonce, ok, data });
|
||||
}
|
||||
|
||||
VesktopNative.commands.onCommand(async ({ message, nonce, data }) => {
|
||||
const handler = handlers.get(message);
|
||||
if (!handler) {
|
||||
return respond(nonce, false, `No handler for message: ${message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(data);
|
||||
respond(nonce, true, result);
|
||||
} catch (err) {
|
||||
respond(nonce, false, String(err));
|
||||
}
|
||||
});
|
||||
|
||||
export function onIpcCommand(channel: string, handler: IpcCommandHandler) {
|
||||
if (handlers.has(channel)) {
|
||||
throw new Error(`Handler for message ${channel} already exists`);
|
||||
}
|
||||
|
||||
handlers.set(channel, handler);
|
||||
}
|
||||
|
||||
export function offIpcCommand(channel: string) {
|
||||
handlers.delete(channel);
|
||||
}
|
||||
|
||||
/* Generic Handlers */
|
||||
|
||||
onIpcCommand(IpcCommands.NAVIGATE_SETTINGS, () => {
|
||||
SettingsRouter.open("My Account");
|
||||
});
|
||||
|
||||
onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages);
|
||||
|
||||
onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker));
|
||||
9
src/renderer/logger.ts
Normal file
9
src/renderer/logger.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* 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 { Logger } from "@vencord/types/utils";
|
||||
|
||||
export const VesktopLogger = new Logger("Vesktop", "#d3869b");
|
||||
19
src/renderer/patches/allowDevToolsKeybind.ts
Normal file
19
src/renderer/patches/allowDevToolsKeybind.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { addPatch } from "./shared";
|
||||
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
find: '"mod+alt+i"',
|
||||
replacement: {
|
||||
match: /"discord\.com"===location\.host/,
|
||||
replace: "false"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
21
src/renderer/patches/enableNotificationsByDefault.ts
Normal file
21
src/renderer/patches/enableNotificationsByDefault.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addPatch } from "./shared";
|
||||
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
find: '"NotificationSettingsStore',
|
||||
replacement: {
|
||||
// FIXME: fix eslint rule
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /\.isPlatformEmbedded(?=\?\i\.\i\.ALL)/g,
|
||||
replace: "$&||true"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
51
src/renderer/patches/fixAutoGainToggle.ts
Normal file
51
src/renderer/patches/fixAutoGainToggle.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { Logger } from "@vencord/types/utils";
|
||||
import { MediaEngineStore } from "renderer/common";
|
||||
|
||||
const logger = new Logger("FixAutoGain");
|
||||
|
||||
function fixTrackConstraints(constraint: MediaTrackConstraints) {
|
||||
const target = constraint.advanced?.find(opt => Object.hasOwn(opt, "autoGainControl")) ?? constraint;
|
||||
|
||||
target.autoGainControl = MediaEngineStore.getAutomaticGainControl();
|
||||
}
|
||||
|
||||
function fixStreamConstraints(constraints: MediaStreamConstraints | undefined) {
|
||||
if (!constraints?.audio) return;
|
||||
|
||||
if (typeof constraints.audio !== "object") {
|
||||
constraints.audio = {};
|
||||
}
|
||||
|
||||
fixTrackConstraints(constraints.audio);
|
||||
}
|
||||
|
||||
const originalGetUserMedia = navigator.mediaDevices.getUserMedia;
|
||||
navigator.mediaDevices.getUserMedia = function (constraints) {
|
||||
try {
|
||||
fixStreamConstraints(constraints);
|
||||
logger.debug("Fixed getUserMedia constraints", constraints);
|
||||
} catch (e) {
|
||||
logger.error("Failed to fix getUserMedia constraints", e);
|
||||
}
|
||||
|
||||
return originalGetUserMedia.call(this, constraints);
|
||||
};
|
||||
|
||||
const originalApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
|
||||
MediaStreamTrack.prototype.applyConstraints = function (constraints) {
|
||||
if (constraints) {
|
||||
try {
|
||||
fixTrackConstraints(constraints);
|
||||
logger.debug("Fixed applyConstraints constraints", constraints);
|
||||
} catch (e) {
|
||||
logger.error("Failed to fix applyConstraints constraints", e);
|
||||
}
|
||||
}
|
||||
return originalApplyConstraints.call(this, constraints);
|
||||
};
|
||||
19
src/renderer/patches/hideDownloadAppsButton.ts
Normal file
19
src/renderer/patches/hideDownloadAppsButton.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { addPatch } from "./shared";
|
||||
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
find: '"app-download-button"',
|
||||
replacement: {
|
||||
match: /return(?=.{0,50}id:"app-download-button")/,
|
||||
replace: "return null;return"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
24
src/renderer/patches/hideSwitchDevice.tsx
Normal file
24
src/renderer/patches/hideSwitchDevice.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addPatch } from "./shared";
|
||||
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
find: "lastOutputSystemDevice.justChanged",
|
||||
replacement: {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /(\i)\.\i\.getState\(\).neverShowModal/,
|
||||
replace: "$& || $self.shouldIgnoreDevice($1)"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
shouldIgnoreDevice(state: any) {
|
||||
return Object.keys(state?.default?.lastDeviceConnected ?? {})?.[0] === "vencord-screen-share";
|
||||
}
|
||||
});
|
||||
25
src/renderer/patches/hideVenmicInput.tsx
Normal file
25
src/renderer/patches/hideVenmicInput.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addPatch } from "./shared";
|
||||
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
find: 'setSinkId"in',
|
||||
replacement: {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /return (\i)\?navigator\.mediaDevices\.enumerateDevices/,
|
||||
replace: "return $1 ? $self.filteredDevices"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
async filteredDevices() {
|
||||
const original = await navigator.mediaDevices.enumerateDevices();
|
||||
return original.filter(x => x.label !== "vencord-screen-share");
|
||||
}
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
*/
|
||||
|
||||
// TODO: Possibly auto generate glob if we have more patches in the future
|
||||
import "./spellCheck";
|
||||
import "./platformClass";
|
||||
@@ -1,9 +1,12 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Settings } from "renderer/settings";
|
||||
import { isMac } from "renderer/utils";
|
||||
|
||||
import { addPatch } from "./shared";
|
||||
|
||||
addPatch({
|
||||
@@ -18,5 +21,9 @@ addPatch({
|
||||
}
|
||||
],
|
||||
|
||||
getPlatformClass: () => (navigator.platform.toLowerCase().startsWith("mac") ? "platform-osx" : "platform-web")
|
||||
getPlatformClass() {
|
||||
if (Settings.store.customTitleBar) return "platform-win";
|
||||
if (isMac) return "platform-osx";
|
||||
return "platform-web";
|
||||
}
|
||||
});
|
||||
|
||||
75
src/renderer/patches/screenShareFixes.ts
Normal file
75
src/renderer/patches/screenShareFixes.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@vencord/types/utils";
|
||||
import { currentSettings } from "renderer/components/ScreenSharePicker";
|
||||
import { State } from "renderer/settings";
|
||||
import { isLinux } from "renderer/utils";
|
||||
|
||||
const logger = new Logger("VesktopStreamFixes");
|
||||
|
||||
if (isLinux) {
|
||||
const original = navigator.mediaDevices.getDisplayMedia;
|
||||
|
||||
async function getVirtmic() {
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const audioDevice = devices.find(({ label }) => label === "vencord-screen-share");
|
||||
return audioDevice?.deviceId;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
navigator.mediaDevices.getDisplayMedia = async function (opts) {
|
||||
const stream = await original.call(this, opts);
|
||||
const id = await getVirtmic();
|
||||
|
||||
const frameRate = Number(State.store.screenshareQuality?.frameRate ?? 30);
|
||||
const height = Number(State.store.screenshareQuality?.resolution ?? 720);
|
||||
const width = Math.round(height * (16 / 9));
|
||||
const track = stream.getVideoTracks()[0];
|
||||
|
||||
track.contentHint = String(currentSettings?.contentHint);
|
||||
|
||||
const constraints = {
|
||||
...track.getConstraints(),
|
||||
frameRate: { min: frameRate, ideal: frameRate },
|
||||
width: { min: 640, ideal: width, max: width },
|
||||
height: { min: 480, ideal: height, max: height },
|
||||
advanced: [{ width: width, height: height }],
|
||||
resizeMode: "none"
|
||||
};
|
||||
|
||||
track
|
||||
.applyConstraints(constraints)
|
||||
.then(() => {
|
||||
logger.info("Applied constraints successfully. New constraints: ", track.getConstraints());
|
||||
})
|
||||
.catch(e => logger.error("Failed to apply constraints.", e));
|
||||
|
||||
if (id) {
|
||||
const audio = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
deviceId: {
|
||||
exact: id
|
||||
},
|
||||
autoGainControl: false,
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false,
|
||||
channelCount: 2,
|
||||
sampleRate: 48000,
|
||||
sampleSize: 16
|
||||
}
|
||||
});
|
||||
|
||||
stream.getAudioTracks().forEach(t => stream.removeTrack(t));
|
||||
stream.addTrack(audio.getAudioTracks()[0]);
|
||||
}
|
||||
|
||||
return stream;
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Patch } from "@vencord/types/utils/types";
|
||||
|
||||
window.VCDP = {};
|
||||
window.VesktopPatchGlobals = {};
|
||||
|
||||
interface PatchData {
|
||||
patches: Omit<Patch, "plugin">[];
|
||||
@@ -16,15 +16,9 @@ interface PatchData {
|
||||
export function addPatch<P extends PatchData>(p: P) {
|
||||
const { patches, ...globals } = p;
|
||||
|
||||
for (const patch of patches as Patch[]) {
|
||||
if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement];
|
||||
for (const r of patch.replacement) {
|
||||
if (typeof r.replace === "string") r.replace = r.replace.replaceAll("$self", "VCDP");
|
||||
}
|
||||
|
||||
patch.plugin = "Vesktop";
|
||||
Vencord.Plugins.patches.push(patch);
|
||||
for (const patch of patches) {
|
||||
Vencord.Plugins.addPatch(patch, "Vesktop", "VesktopPatchGlobals");
|
||||
}
|
||||
|
||||
Object.assign(VCDP, globals);
|
||||
Object.assign(VesktopPatchGlobals, globals);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addContextMenuPatch } from "@vencord/types/api/ContextMenu";
|
||||
import { findStoreLazy } from "@vencord/types/webpack";
|
||||
import { ContextMenu, FluxDispatcher, Menu } from "@vencord/types/webpack/common";
|
||||
import { FluxDispatcher, Menu, useMemo, useStateFromStores } from "@vencord/types/webpack/common";
|
||||
import { useSettings } from "renderer/settings";
|
||||
|
||||
import { addPatch } from "./shared";
|
||||
|
||||
@@ -22,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 (.{1,3})\.preventDefault\(\),(.{1,3}\(.{1,3}\))(?<=:(.{1,3})\.enableSpellCheck\).+?)/,
|
||||
// ... else { $self.onSlateContext(() => openMenu(props)) }
|
||||
replace: "else {$self.onSlateContext($1, $3?.enableSpellCheck, () => $2)}"
|
||||
}
|
||||
@@ -46,10 +47,20 @@ addPatch({
|
||||
}
|
||||
});
|
||||
|
||||
addContextMenuPatch("textarea-context", children => () => {
|
||||
addContextMenuPatch("textarea-context", children => {
|
||||
const spellCheckEnabled = useStateFromStores([SpellCheckStore], () => SpellCheckStore.isEnabled());
|
||||
const hasCorrections = Boolean(word && corrections?.length);
|
||||
|
||||
children.push(
|
||||
const availableLanguages = useMemo(VesktopNative.spellcheck.getAvailableLanguages, []);
|
||||
|
||||
const settings = useSettings();
|
||||
const spellCheckLanguages = (settings.spellCheckLanguages ??= [...new Set(navigator.languages)]);
|
||||
|
||||
const pasteSectionIndex = children.findIndex(c => c?.props?.children?.some(c => c?.props?.id === "paste"));
|
||||
|
||||
children.splice(
|
||||
pasteSectionIndex === -1 ? children.length : pasteSectionIndex,
|
||||
0,
|
||||
<Menu.MenuGroup>
|
||||
{hasCorrections && (
|
||||
<>
|
||||
@@ -68,16 +79,39 @@ addContextMenuPatch("textarea-context", children => () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Menu.MenuCheckboxItem
|
||||
id="vcd-spellcheck-enabled"
|
||||
label="Enable Spellcheck"
|
||||
checked={SpellCheckStore.isEnabled()}
|
||||
action={() => {
|
||||
FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
|
||||
// Haven't found a good way to update state, so just close for now 🤷♀️
|
||||
ContextMenu.close();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.MenuItem id="vcd-spellcheck-settings" label="Spellcheck Settings">
|
||||
<Menu.MenuCheckboxItem
|
||||
id="vcd-spellcheck-enabled"
|
||||
label="Enable Spellcheck"
|
||||
checked={spellCheckEnabled}
|
||||
action={() => {
|
||||
FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.MenuItem id="vcd-spellcheck-languages" label="Languages" disabled={!spellCheckEnabled}>
|
||||
{availableLanguages.map(lang => {
|
||||
const isEnabled = spellCheckLanguages.includes(lang);
|
||||
return (
|
||||
<Menu.MenuCheckboxItem
|
||||
id={"vcd-spellcheck-lang-" + lang}
|
||||
label={lang}
|
||||
checked={isEnabled}
|
||||
disabled={!isEnabled && spellCheckLanguages.length >= 5}
|
||||
action={() => {
|
||||
const newSpellCheckLanguages = spellCheckLanguages.filter(l => l !== lang);
|
||||
if (newSpellCheckLanguages.length === spellCheckLanguages.length) {
|
||||
newSpellCheckLanguages.push(lang);
|
||||
}
|
||||
|
||||
settings.spellCheckLanguages = newSpellCheckLanguages;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Menu.MenuItem>
|
||||
</Menu.MenuItem>
|
||||
</Menu.MenuGroup>
|
||||
);
|
||||
});
|
||||
|
||||
21
src/renderer/patches/streamerMode.ts
Normal file
21
src/renderer/patches/streamerMode.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 { addPatch } from "./shared";
|
||||
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
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: ""
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
32
src/renderer/patches/windowMethods.tsx
Normal file
32
src/renderer/patches/windowMethods.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 { addPatch } from "./shared";
|
||||
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
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"
|
||||
},
|
||||
{
|
||||
match: /,getEnableHardwareAcceleration/,
|
||||
replace: "$&:VesktopNative.app.getEnableHardwareAcceleration,_oldGetEnableHardwareAcceleration"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
44
src/renderer/patches/windowsTitleBar.tsx
Normal file
44
src/renderer/patches/windowsTitleBar.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Settings } from "renderer/settings";
|
||||
|
||||
import { addPatch } from "./shared";
|
||||
|
||||
if (Settings.store.customTitleBar)
|
||||
addPatch({
|
||||
patches: [
|
||||
{
|
||||
find: ".wordmarkWindows",
|
||||
replacement: [
|
||||
{
|
||||
// TODO: Fix eslint rule
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
match: /case \i\.\i\.WINDOWS:/,
|
||||
replace: 'case "WEB":'
|
||||
}
|
||||
]
|
||||
},
|
||||
// Visual Refresh
|
||||
{
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -1,12 +1,15 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { useEffect, useReducer } from "@vencord/types/webpack/common";
|
||||
import { SettingsStore } from "shared/utils/SettingsStore";
|
||||
|
||||
import { VesktopLogger } from "./logger";
|
||||
import { localStorage } from "./utils";
|
||||
|
||||
export const Settings = new SettingsStore(VesktopNative.settings.get());
|
||||
Settings.addGlobalChangeListener((o, p) => VesktopNative.settings.set(o, p));
|
||||
|
||||
@@ -28,3 +31,38 @@ export function getValueAndOnChange(key: keyof typeof Settings.store) {
|
||||
onChange: (value: any) => (Settings.store[key] = value)
|
||||
};
|
||||
}
|
||||
|
||||
interface TState {
|
||||
screenshareQuality?: {
|
||||
resolution: string;
|
||||
frameRate: string;
|
||||
};
|
||||
}
|
||||
|
||||
const stateKey = "VesktopState";
|
||||
|
||||
const currentState: TState = (() => {
|
||||
const stored = localStorage.getItem(stateKey);
|
||||
if (!stored) return {};
|
||||
try {
|
||||
return JSON.parse(stored);
|
||||
} catch (e) {
|
||||
VesktopLogger.error("Failed to parse stored state", e);
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
export const State = new SettingsStore<TState>(currentState);
|
||||
State.addGlobalChangeListener((o, p) => localStorage.setItem(stateKey, JSON.stringify(o)));
|
||||
|
||||
export function useVesktopState() {
|
||||
const [, update] = useReducer(x => x + 1, 0);
|
||||
|
||||
useEffect(() => {
|
||||
State.addGlobalChangeListener(update);
|
||||
|
||||
return () => State.removeGlobalChangeListener(update);
|
||||
}, []);
|
||||
|
||||
return State.store;
|
||||
}
|
||||
|
||||
80
src/renderer/themedSplash.ts
Normal file
80
src/renderer/themedSplash.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Settings } from "./settings";
|
||||
|
||||
function isValidColor(color: CSSStyleValue | undefined): color is CSSUnparsedValue & { [0]: string } {
|
||||
return color instanceof CSSUnparsedValue && typeof color[0] === "string" && CSS.supports("color", color[0]);
|
||||
}
|
||||
|
||||
// https://gist.github.com/earthbound19/e7fe15fdf8ca3ef814750a61bc75b5ce
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.max(Math.min(value, max), min);
|
||||
}
|
||||
const linearToGamma = (c: number) => (c >= 0.0031308 ? 1.055 * Math.pow(c, 1 / 2.4) - 0.055 : 12.92 * c);
|
||||
|
||||
function oklabToSRGB({ L, a, b }: { L: number; a: number; b: number }) {
|
||||
let l = L + a * +0.3963377774 + b * +0.2158037573;
|
||||
let m = L + a * -0.1055613458 + b * -0.0638541728;
|
||||
let s = L + a * -0.0894841775 + b * -1.291485548;
|
||||
l **= 3;
|
||||
m **= 3;
|
||||
s **= 3;
|
||||
let R = l * +4.0767416621 + m * -3.3077115913 + s * +0.2309699292;
|
||||
let G = l * -1.2684380046 + m * +2.6097574011 + s * -0.3413193965;
|
||||
let B = l * -0.0041960863 + m * -0.7034186147 + s * +1.707614701;
|
||||
R = 255 * linearToGamma(R);
|
||||
G = 255 * linearToGamma(G);
|
||||
B = 255 * linearToGamma(B);
|
||||
R = Math.round(clamp(R, 0, 255));
|
||||
G = Math.round(clamp(G, 0, 255));
|
||||
B = Math.round(clamp(B, 0, 255));
|
||||
|
||||
return `rgb(${R}, ${G}, ${B})`;
|
||||
}
|
||||
|
||||
function resolveColor(color: string) {
|
||||
const span = document.createElement("span");
|
||||
span.style.color = color;
|
||||
span.style.display = "none";
|
||||
|
||||
document.body.append(span);
|
||||
let rgbColor = getComputedStyle(span).color;
|
||||
span.remove();
|
||||
|
||||
if (rgbColor.startsWith("oklab(")) {
|
||||
// scam
|
||||
const [_, L, a, b] = rgbColor.match(/oklab\((.+?)[, ]+(.+?)[, ]+(.+?)\)/) ?? [];
|
||||
if (L && a && b) {
|
||||
rgbColor = oklabToSRGB({ L: parseFloat(L), a: parseFloat(a), b: parseFloat(b) });
|
||||
}
|
||||
}
|
||||
|
||||
return rgbColor;
|
||||
}
|
||||
|
||||
const updateSplashColors = () => {
|
||||
const bodyStyles = document.body.computedStyleMap();
|
||||
|
||||
const color = bodyStyles.get("--text-normal");
|
||||
const backgroundColor = bodyStyles.get("--background-primary");
|
||||
|
||||
if (isValidColor(color)) {
|
||||
Settings.store.splashColor = resolveColor(color[0]);
|
||||
}
|
||||
|
||||
if (isValidColor(backgroundColor)) {
|
||||
Settings.store.splashBackground = resolveColor(backgroundColor[0]);
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === "complete") {
|
||||
updateSplashColors();
|
||||
} else {
|
||||
window.addEventListener("load", updateSplashColors);
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", updateSplashColors);
|
||||
@@ -1,9 +1,10 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// Discord deletes this from the window so we need to capture it in a variable
|
||||
export const { localStorage } = window;
|
||||
|
||||
export const isFirstRun = (() => {
|
||||
@@ -17,3 +18,27 @@ 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(" ");
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-3.0
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2023 Vendicated and Vencord contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export const enum IpcEvents {
|
||||
@@ -12,22 +12,26 @@ export const enum IpcEvents {
|
||||
|
||||
GET_VERSION = "VCD_GET_VERSION",
|
||||
SUPPORTS_WINDOWS_TRANSPARENCY = "VCD_SUPPORTS_WINDOWS_TRANSPARENCY",
|
||||
GET_ENABLE_HARDWARE_ACCELERATION = "VCD_GET_ENABLE_HARDWARE_ACCELERATION",
|
||||
|
||||
RELAUNCH = "VCD_RELAUNCH",
|
||||
CLOSE = "VCD_CLOSE",
|
||||
FOCUS = "VCD_FOCUS",
|
||||
MINIMIZE = "VCD_MINIMIZE",
|
||||
MAXIMIZE = "VCD_MAXIMIZE",
|
||||
|
||||
SHOW_ITEM_IN_FOLDER = "VCD_SHOW_ITEM_IN_FOLDER",
|
||||
GET_SETTINGS = "VCD_GET_SETTINGS",
|
||||
SET_SETTINGS = "VCD_SET_SETTINGS",
|
||||
|
||||
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",
|
||||
|
||||
SPELLCHECK_SET_LANGUAGES = "VCD_SPELLCHECK_SET_LANGUAGES",
|
||||
SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES",
|
||||
SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT",
|
||||
SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING",
|
||||
SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY",
|
||||
@@ -38,5 +42,31 @@ export const enum IpcEvents {
|
||||
|
||||
AUTOSTART_ENABLED = "VCD_AUTOSTART_ENABLED",
|
||||
ENABLE_AUTOSTART = "VCD_ENABLE_AUTOSTART",
|
||||
DISABLE_AUTOSTART = "VCD_DISABLE_AUTOSTART"
|
||||
DISABLE_AUTOSTART = "VCD_DISABLE_AUTOSTART",
|
||||
|
||||
VIRT_MIC_LIST = "VCD_VIRT_MIC_LIST",
|
||||
VIRT_MIC_START = "VCD_VIRT_MIC_START",
|
||||
VIRT_MIC_START_SYSTEM = "VCD_VIRT_MIC_START_ALL",
|
||||
VIRT_MIC_STOP = "VCD_VIRT_MIC_STOP",
|
||||
|
||||
ARRPC_ACTIVITY = "VCD_ARRPC_ACTIVITY",
|
||||
|
||||
CLIPBOARD_COPY_IMAGE = "VCD_CLIPBOARD_COPY_IMAGE",
|
||||
|
||||
DEBUG_LAUNCH_GPU = "VCD_DEBUG_LAUNCH_GPU",
|
||||
DEBUG_LAUNCH_WEBRTC_INTERNALS = "VCD_DEBUG_LAUNCH_WEBRTC",
|
||||
|
||||
IPC_COMMAND = "VCD_IPC_COMMAND"
|
||||
}
|
||||
|
||||
export const enum IpcCommands {
|
||||
RPC_ACTIVITY = "rpc:activity",
|
||||
RPC_INVITE = "rpc:invite",
|
||||
RPC_DEEP_LINK = "rpc:link",
|
||||
|
||||
NAVIGATE_SETTINGS = "navigate:settings",
|
||||
|
||||
GET_LANGUAGES = "navigator.languages",
|
||||
|
||||
SCREEN_SHARE_PICKER = "screenshare:picker"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user