Compare commits

...

126 commits

Author SHA1 Message Date
Vendicated
5d675efb64
upgrade to electron33 it probably goes crazy idk what they changed 2024-10-26 13:56:28 +02:00
lewisakura
03c7ad4cc0
ci: remove mentions from meta workflow [skip ci] 2024-10-22 20:22:00 +01:00
δDD
135a369848
fix typo in Settings ui (#924)
Fixed spelling mistake (Miscelleanous → Miscellaneous)
2024-10-22 13:44:49 +00:00
ryanamay
8993b0d520
fix screenshare picker window subtitle alignment (#875)
Co-authored-by: v <vendicated@riseup.net>
2024-10-14 17:40:47 +02:00
adryd
ccff1ac3ef
Enable speech-dispatcher support for TTS on linux (#874) 2024-10-04 18:51:55 +00:00
Tiagoquix
062b536617
Update text in bug report (#873) 2024-09-24 23:55:04 +02:00
Vendicated
d008f90399
update arrpc to 5aadc307cb 2024-09-20 20:27:26 +02:00
Vendicated
4fdf43ea6a
https://tenor.com/view/ah-eto-bleh-27178660 2024-09-20 20:23:57 +02:00
Vendicated
b94379f5bd
workaround electron/electron#43367 2024-09-20 20:16:11 +02:00
Vendicated
37db07807a
upgrade electron to v32 2024-09-20 20:07:17 +02:00
Aiden
4274647c81
update ESLint to v9 (#859) 2024-09-20 20:02:15 +02:00
v
24fbf35542
https://x.com/ArmCordClient/status/1834617546445672703 2024-09-13 23:16:42 +02:00
lewisakura
c8eccc7e9d
ci: add signing certificate password for mac [skip ci] 2024-09-13 22:02:34 +01:00
v
a318f6b407
Update feature-request.yml 2024-08-27 12:59:57 +02:00
donCESAR12345
75354ad8e6
fix(rpm): don't generate build_id links to avoid conflicts (#826)
...with other packages
2024-08-24 00:17:27 +02:00
lewisakura
af9ed58eef
ci: update winget releaser [skip ci] 2024-08-10 10:21:31 +01:00
v
e0453418bd
Update bug_report.yml 2024-08-05 19:18:22 +02:00
lewisakura
22344512ad
feat: notarization (#776) 2024-07-23 15:10:50 +01:00
Noah
9acc6652ff
AudioShare: Even more granular selection, Allow device sharing (#750) 2024-07-12 16:14:18 +02:00
vee
61bbd7f6aa
Update bug_report.yml 2024-07-11 05:31:07 +02:00
github-actions[bot]
f31f06f5c4
meta: Insert release changes for v1.5.3 (#724) 2024-07-04 20:21:38 +02:00
vee
786fe131b8
Update install instructions 2024-07-04 20:21:14 +02:00
Vendicated
35d6f19e80
bump to v1.5.3 2024-07-04 19:49:52 +02:00
Vendicated
8798bbce44
only check updates in production 2024-07-04 19:47:23 +02:00
Vendicated
485eb8eaef
update user agents 2024-07-04 19:43:27 +02:00
HAHALOSAH
1e39ec7d39
macOS: Workaround for making things in draggable area clickable (#582) 2024-07-04 19:38:56 +02:00
Vendicated
5fa4ae9824
fix first launch switches 2024-07-04 19:30:03 +02:00
Vendicated
281885a87e
add auto updater 2024-07-04 19:22:26 +02:00
Vendicated
021aca4089
improve tray strings 2024-07-04 18:51:18 +02:00
Vendicated
f341c5e5bb
specify that it's for security so users don't get annoyed 2024-07-04 18:47:33 +02:00
Vendicated
0d203ac21d
remove silly console.log 2024-07-04 18:41:03 +02:00
Vendicated
1f12d270ec
fix potential sandbox escape via custom vencordDir 2024-07-04 18:40:24 +02:00
kittykel
ec3d83f7ca
fix: specify framerate in constraints (#703) 2024-07-04 18:04:57 +02:00
Vendicated
05014f747a
fix incorrectly packaging node_modules 2024-07-04 17:54:38 +02:00
Vendicated
9c44da0cae
update workflow actions 2024-07-04 17:46:49 +02:00
Lionir
5d5b38b259
meta: Use Network category instead of AudioVideo (#721) 2024-07-04 03:34:17 +02:00
Vendicated
9e3d83a2ee
build: arm64 on windows, universal on mac 2024-06-29 12:36:49 +02:00
Nico
0e49e5e8f6
fix: update switch device and notifications patch (#688) 2024-06-21 16:16:01 +02:00
vee
456d5a61cd
FOR THE LOVE OF THE GODS PLEASE STOP 2024-06-21 03:43:43 +02:00
Tiagoquix
ebd4e6b28e
Make feature request issue template apply appropriate tag (#671) 2024-06-19 19:09:53 +02:00
Tiagoquix
3fe2094814
Change default screen-sharing quality to 720p 30 FPS (#665) 2024-06-18 00:21:55 +02:00
Vendicated
9554902704
update arrpc 2024-06-16 18:42:48 +02:00
Noah
da7f13288f
linux audioshare: add granular selection, more options, better ui (#621) 2024-06-16 18:10:03 +02:00
Vendicated
1a4d173bb4
Upgrade to electron 31 2024-06-16 17:59:46 +02:00
lewisakura
abd1e8c6da
ci: update winget releaser 2024-06-04 16:51:29 +01:00
Vendicated
7fe79b629e
fix(css): brand-experiment is now brand-500 2024-06-01 19:12:29 +02:00
MrGarlic1
d3e8dfa724
hide macos traffic light with custom titlebar (#636)
Co-authored-by: MrGarlic <bsamans@samans.com>
2024-06-01 17:25:42 +02:00
MrGarlic1
3772db9eea
Windows Portable: store settings in portable folder (#592)
Co-authored-by: MrGarlic <bsamans@samans.com>
Co-authored-by: vee <vendicated@riseup.net>
2024-05-29 02:03:01 +02:00
Vendicated
b6c3c8024b
disable background throttling; hopefully fix unloads when in background 2024-05-28 18:11:55 +02:00
Vendicated
6b948668b9
make package manager check warn instead of error on mismatched version 2024-05-23 17:40:57 +02:00
Vendicated
f232cfc3c8
Spellcheck: add language changer & swap position with paste/cut/copy 2024-05-23 17:30:40 +02:00
Vendicated
bca8872db1
bump venmic 2024-05-22 22:54:35 +02:00
Vendicated
463c42330e
bump vencord types 2024-05-17 23:00:06 +02:00
Vendicated
d3b94fc4df
fix react types version 2024-05-17 22:52:05 +02:00
MrGarlic1
a8d72fa665
fix: do not re-position on screen that was disconnected (#598)
Co-authored-by: MrGarlic <bsamans@samans.com>
2024-05-10 21:04:16 +02:00
Jörg Thalheim
69f14ee611
README: update link to new nixos wiki (#594)
This commit updates the the link from the former, unofficial nixos wiki
page to the new https://wiki.nixos.org
ref: NixOS/foundation#113
2024-05-09 13:37:36 +02:00
kaitlynkitty
944a699e83
remove obsolete patch (moved to vencord) (#563) 2024-05-08 15:14:55 +00:00
Nick
d11a9a04ff
chore: update pnpm to latest version (#590) 2024-05-08 03:47:30 +02:00
splatterxl
56b96bded9
simplify mac links in readme (#585) 2024-05-06 20:27:32 +02:00
Oleh Polisan
ac35f81476
fix: Screenshare UI for non-linux systems (#568) 2024-05-05 04:14:08 +02:00
Vendicated
0623a71271
improve issue templates 2024-05-03 14:49:49 +02:00
samara
a66af353f7
macOS: Add customized dmg background (#565) 2024-05-02 23:41:09 +02:00
github-actions[bot]
cb55cf4b74
add Metainfo for v1.5.2 (#557)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-05-01 16:57:29 +02:00
Vendicated
e9da30e420
bump to v1.5.2 2024-05-01 16:45:15 +02:00
☆ sam
c52abdc1ee
mac: add entitlements needed for camera/microphone access (#533) 2024-04-26 22:02:19 +02:00
V
55ca2f3091
allow triggering release ci manually 2024-04-26 21:36:47 +02:00
Takase
0beb74fad0
Allow users to pass --enable/disable-features flag (#527)
Previously they would be overwritten by vesktop
2024-04-26 21:22:17 +02:00
Noah
2733727a40
fix regression breaking venmic (#531) 2024-04-21 19:31:14 +02:00
Vendicated
ab9e8579ee
add scrollbar workaround because discord dum 2024-04-20 17:16:29 +02:00
Vendicated
617ef0fa19
bump arrpc 2024-04-20 17:02:32 +02:00
Noah
2649598361
Improve Venmic Usability (#504) 2024-04-20 16:09:40 +02:00
kaitlynkitty
8eaa5206b9
overhaul & improve Linux screenshare (#489)
Fixes fps/resolution not properly being applied
Enables hardware encoding via vaapi
Redesigns stream picker modal 

Co-authored-by: kaitlynkittyy <kaitlynyaadev@kaitlynyaa.dev>
Co-authored-by: Oleh Polisan <polisanoleg@gmail.com>
Co-authored-by: Vendicated <vendicated@riseup.net>
2024-04-18 01:40:03 +02:00
Noah
df05d12fb2
Venmic v3.4 (#499) 2024-04-14 02:24:19 +02:00
Diego Parra
8c6941b8e9
Tray: Add left click hide/show feature (#404)
Co-authored-by: V <vendicated@riseup.net>
2024-04-09 02:23:34 +00:00
Noah
6ed59332d1
Bump venmic to 3.3.3 (#483) 2024-04-09 04:22:20 +02:00
github-actions[bot]
4abae9c708
Metainfo for v1.5.1 (#436)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-03-12 03:51:13 +01:00
Vendicated
b09f035ea9
Bump to v1.5.1 2024-03-12 03:39:57 +01:00
Vendicated
44627b4cd4
autostart(linux): correctly preserve command line arguments 2024-03-12 03:38:37 +01:00
Vendicated
97267ef89a
Rewrite http utils; properly handle (& retry on) network errors 2024-03-12 02:31:53 +01:00
Vendicated
132adcb733
Bump dependencies; electron v29 2024-03-12 01:33:50 +01:00
Vendicated
738fa588a6
Adapt spellcheck to new context menu api 2024-03-12 01:28:37 +01:00
Lewis Crichton
e63cff7a52
chore: remove unnecessary relations [skip ci] 2024-03-08 21:32:31 +00:00
V
777a0e83fe
Update bug_report.md 2024-03-07 03:21:58 +01:00
Justin Chung
612d35c96f
Add categories to Vesktop settings to reduce visual clutter (#379)
Co-authored-by: Vendicated <vendicated@riseup.net>
2024-02-18 15:49:42 +00:00
Filip Komárek
0acd3e160a
README: add Scoop to unofficial sources (#386) 2024-02-10 23:12:24 +01:00
V
6993b2a7d4
Update bug_report.md 2024-01-30 22:07:05 +01:00
Noah
2bd8ca96df
Workaround screenshare audio using microphone on debian bug (#360)
* feat(screenshare): add workaround

* refactor: review suggestions

* chore(deps): bump venmic

* chore(deps): bump venmic
2024-01-28 16:55:46 +01:00
Noah
4d82a6f41d
refactor: stop venmic on stream stop (#353) 2024-01-23 17:37:57 +01:00
V
cb33f1834b
Update release.yml 2024-01-19 21:55:30 +01:00
V
fb40f4b42d
Update README.md 2024-01-19 03:39:27 +01:00
V
808eb56327
Update README.md 2024-01-19 03:38:15 +01:00
Vendicated
b636b65e55
implement vencord 'transparency' option 2024-01-19 01:07:27 +01:00
V
7d30dcdb47
Make popouts respect the menu bar visibility setting 2024-01-19 00:01:30 +01:00
V
4f1615ecb3
fix window flash when clicking notification 2024-01-18 23:49:26 +01:00
V
463cd6dc46
popout: fix titlebar close button 2024-01-18 21:33:17 +01:00
V
8c007476c3
popout: fix titlebar on windows when using native titlebar 2024-01-18 21:13:52 +01:00
github-actions[bot]
b20c77734c
Metainfo for v1.5.0 (#335)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-16 02:15:56 +01:00
V
79cd2791f6
bump to v1.5.0 2024-01-16 02:02:48 +01:00
V
73de0fa535
bump venmic 2024-01-16 01:34:38 +01:00
Vendicated
83e74b98d5
don't disable tray 'Open' item when window is shown (doesnt work correctly) 2024-01-16 01:22:10 +01:00
Vendicated
2e4c834a90
also allow using discord titlebar on non-windows 2024-01-16 01:08:06 +01:00
Vendicated
2aa0b0fa20
prettier update borked lint somehow woah 2024-01-15 19:10:42 +01:00
Vendicated
3ac0ed3d78
config migration: also migrate indexeddb 2024-01-15 19:09:09 +01:00
Vendicated
eddc1de784
bump deps 2024-01-15 19:05:29 +01:00
Vendicated
6483b3a3d9
port popout logic from discord desktop 2024-01-15 18:52:46 +01:00
Vendicated
38f0330eb2
Fix adding connections & popout on ptb/canary 2024-01-15 17:56:33 +01:00
Vendicated
15a49a31e1
add clipboard.copyImage ipc
the web clipboard api only works when focused. so when you try to copy an image but immediately unfocus while its still fetching the image, it will fail. this ipc ensures it will always work even without focus
2024-01-13 19:04:32 +01:00
Lewis Crichton
33eb1af2be
ci: mac code signing 2024-01-11 17:50:30 +00:00
V
563ba1eebe
Add basic issue templates 2024-01-11 17:23:14 +01:00
Vendicated
62cf02e7b1
make splash window draggable 2024-01-08 02:38:38 +01:00
Redeven
0f0bddbef9
feat: Add start minimized as a launch argument (#316)
Co-authored-by: V <vendicated@riseup.net>
2024-01-07 03:11:00 +01:00
Vendicated
0881143d57
update appId 2024-01-07 02:52:39 +01:00
Vendicated
effd950b2d
fully rename app to Vesktop 2024-01-07 02:44:14 +01:00
Vendicated
4074e8d6ac
move internal state from settings.json to state.json 2024-01-07 02:26:18 +01:00
Marocco2
779c8fa516
Remove transparent: true for transparencyOption (#266)
Co-authored-by: V <vendicated@riseup.net>
2024-01-07 00:59:29 +00:00
Vendicated
b059516707
SteamOS: always use dark theme
Co-authored-by: AAGaming <aagaming@riseup.net>
2024-01-06 03:20:13 +01:00
Vendicated
dfa9d248d3
bump venmic 2024-01-06 03:18:30 +01:00
Vendicated
18925ad583
bump deps 2024-01-06 02:05:09 +01:00
Vendicated
0b01732293
make hardwareAcceleration setting more intuitive 2024-01-06 02:02:54 +01:00
Nick
a89cd9d2ba
add setting to disable hardware acceleration (#315) 2024-01-02 05:36:50 +01:00
V
2f35128acf
Revert "feat: Add start as minimized toggle to settings (#248)" (#310)
the current implementation was prematurely merged and is broken. it will be back with a fixed implementaton eventually
2023-12-28 04:25:54 +01:00
V
7565eb39a4
improve method for enabling notifications by default 2023-12-28 04:19:56 +01:00
V
d71d0c1cc4
bump electron to v28 2023-12-28 03:27:38 +01:00
Lewis Crichton
dfc6970756
fix(security): use promise queue for steam pipe (#300)
this prevents an (unlikely) race condition where writing multiple large payloads to the pipe simultaneously could lead to jambled data => argument injection
2023-12-28 01:38:31 +01:00
V
1429815fd1
README: remove outdated section (#303) 2023-12-24 00:15:26 +01:00
AAGaming
aa397d003c
SteamOS: add flatpak support using steam.pipe (#283) 2023-12-22 13:56:11 +00:00
68 changed files with 6830 additions and 4228 deletions

View file

@ -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"
}
}

18
.github/ISSUE_TEMPLATE/blank.yml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Blank Issue
description: Reserved for developers. Use the bug report or feature request templates instead.
body:
- type: markdown
attributes:
value: |
# READ THIS BEFORE OPENING AN ISSUE
This form is only meant for Vesktop developers. If you don't know what you're doing,
please use the bug report or feature request templates instead.
- type: textarea
id: content
attributes:
label: Content
validations:
required: true

130
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,130 @@
name: 🐛 Bug / Crash Report
description: Create a bug or crash report for Vesktop
labels: [bug]
title: "[Bug] <title>"
body:
- type: markdown
attributes:
value: |
**Thanks 🩷 for taking the time to fill out this bug report! Before proceeding, please read the following**
Make sure a similar issue doesn't already exist by [searching the existing issues](https://github.com/Vencord/Vesktop/issues?q=is%3Aissue) for keywords!
Make sure both Vesktop and Vencord are fully up to date. You can update Vencord by right-clicking the Vesktop tray icon and pressing "Repair Vencord"
**DO NOT REPORT** any of the following issues:
- Purely graphical glitches like flickering, scaling issues[^1]
- App crashing / not showing window with mentions of the gpu process in the stacktrace[^1]
- Screenshare not starting, black screening or crashing[^2]
- Vencord related issues: This is the Vesktop repo, not Vencord
- Captchas[^3]
- Issues with opening URLs[^4]
- Issues with Notifications[^4]
- Issues with Input Methods[^4]
- Issues with File Drag and Drop[^5]
- Network Errors[^6]
Linux users: Please only report issues with supported packages (flatpak and any builds from the README / releases).
We do not support other packages, like the AUR or Nix packages, so please first make sure your issue is reproducible with official releases,
like [our Flatpak](https://flathub.org/apps/dev.vencord.Vesktop) or [AppImage](https://vencord.dev/download/vesktop/amd64/appimage)
[^1]: GPU issue. Disable hardware acceleration in Vesktop Settings or run with `--disable-gpu`
[^2]: System issue. You will have to fix it
[^3]: If you are receiving a lot of captchas, it means Discord thinks you might be a bot. Make sure you're not using a VPN/Proxy
[^4]: These things are handled by Chromium / Electron, not us. If they don't work, it's either an issue with your system or a bug with Chromium.
[^5]: You are likely using the Vesktop flatpak and trying to drop a file the flatpak can't access. You can fix this by installing Flatseal and using it to grant Vesktop full access to your files
[^6]: Issue on your end, you have to fix it. Try changing your DNS to [1.1.1.1 (Cloudflare DNS)](https://developers.cloudflare.com/1.1.1.1/setup/)
- type: input
id: discord
attributes:
label: Discord Account
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
placeholder: username#0000
validations:
required: false
- type: input
id: os
attributes:
label: Operating System
description: What operating system are you using (eg Windows 10, macOS Big Sur, Ubuntu 20.04)?
placeholder: Windows 10
validations:
required: true
- type: input
id: linux-de
attributes:
label: Linux Only ~ Desktop Environment
description: If you are on Linux, what Desktop environment are you using (eg GNOME, KDE, XFCE)? Are you using Wayland or Xorg?
placeholder: Gnome on Wayland
validations:
required: false
- type: input
id: install-type
attributes:
label: Package Type
description: What kind of Vesktop package are you using? (Setup exe, Portable, Flatpak, AppImage, Deb, etc)
placeholder: Flatpak
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: What happens when the bug or crash occurs?
description: Where does this bug or crash occur, when does it occur, etc.
placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...
validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: What is the expected behaviour?
description: Simply detail what the expected behaviour is.
placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...
validations:
required: true
- type: textarea
id: steps-to-take
attributes:
label: How do you recreate this bug or crash?
description: Give us a list of steps in order to recreate the bug or crash.
placeholder: |
1. Do ...
2. Then ...
3. Do this ..., ... and then ...
4. Observe "the bug" or "the crash"
validations:
required: true
- type: textarea
id: debug-logs
attributes:
label: Debug Logs
description: Run vesktop from the command line. Include the relevant command line output here. If there are any lines that seem relevant, try googling them or searching existing issues
value: |
```
Replace this text with your crash-log. Do not remove the backticks
```
validations:
required: true
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
description: We only accept reports for bugs that happen on supported and up to date Vesktop releases
options:
- label: I have searched the existing issues and found no similar issue
required: true
- label: I am using the latest Vesktop and Vencord versions
required: true
- label: This issue occurs on an official release (not just the AUR or Nix packages)
required: true

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Vencord Support Server
url: https://discord.gg/D9uwnFnqmd
about: If you need help regarding Vesktop or Vencord, please join our support server!

View file

@ -0,0 +1,74 @@
name: 🛠️ Feature Request
description: Request a feature for Vesktop
labels: [enhancement]
title: "[Feature Request] <title>"
body:
- type: markdown
attributes:
value: |
**Thanks 🩷 for taking the time to fill out this request! Before proceeding, please read the following**
Make sure a similar request doesn't already exist by [searching the existing issues](https://github.com/Vencord/Vesktop/issues?q=is%3Aissue) for keywords!
This form is only meant for **Vesktop feature requests**.
For plugin requests or Vencord feature requests, go [here](https://github.com/Vencord/plugin-requests/issues/new?template=request.yml) instead!
**DO NOT** make any icon related requests or you will be blocked.
- type: input
id: discord
attributes:
label: Discord Account
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
placeholder: username#0000
validations:
required: false
- type: textarea
id: motivation
attributes:
label: Motivation
description: If your feature request related to a problem? Please describe
placeholder: I'm always frustrated when ..., I think it would be better if ...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution
description: Describe the solution you'd like
placeholder: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives
description: Describe alternatives you've considered
placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context here. Screenshots or mockups could help greatly
validations:
required: false
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
description: This form is only for Vesktop feature requests. If the following don't apply, re-read the introduction text
options:
- label: I have searched the existing issues and found no similar issue
required: true
- label: This is not a plugin request
required: true
- label: This is not a Vencord feature request
required: true

View file

@ -11,13 +11,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18.18.2 - name: Use Node.js 20
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18.18.2 node-version: 20
- name: Install dependencies - name: Install dependencies
run: pnpm i run: pnpm i
@ -33,6 +33,6 @@ jobs:
git add meta/dev.vencord.Vesktop.metainfo.xml git add meta/dev.vencord.Vesktop.metainfo.xml
git commit -m "Insert release changes for ${{ github.event.release.tag_name }}" git commit -m "Insert release changes for ${{ github.event.release.tag_name }}"
git push origin ci/meta-update git push origin ci/meta-update
gh pr create -B main -H ci/meta-update -t "Metainfo for ${{ github.event.release.tag_name }}" -b "This PR updates the metainfo for release ${{ github.event.release.tag_name }}. @lewisakura @Vendicated" gh pr create -B main -H ci/meta-update -t "Metainfo for ${{ github.event.release.tag_name }}" -b "This PR updates the metainfo for release ${{ github.event.release.tag_name }}."
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -4,6 +4,7 @@ on:
push: push:
tags: tags:
- v* - v*
workflow_dispatch:
jobs: jobs:
release: release:
@ -21,13 +22,13 @@ jobs:
platform: windows platform: windows
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18.18.2 - name: Use Node.js 20
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18.18.2 node-version: 20
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -37,7 +38,22 @@ jobs:
run: pnpm build run: pnpm build
- name: Run Electron Builder - name: Run Electron Builder
if: ${{ matrix.platform != 'mac' }}
run: | run: |
pnpm electron-builder --${{ matrix.platform }} --publish always pnpm electron-builder --${{ matrix.platform }} --publish always
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 }}

View file

@ -11,13 +11,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18.18.2 - name: Use Node.js 20
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18.18.2 node-version: 20
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies

View file

@ -13,10 +13,10 @@ on:
jobs: jobs:
winget: winget:
name: Publish winget package name: Publish winget package
runs-on: ubuntu-latest runs-on: windows-latest
steps: steps:
- name: Submit package to Winget Community Repo - name: Submit package to Winget Community Repo
uses: vedantmgoyal2009/winget-releaser@e68d386d5d6a1cef8cb0fb5e62b77ebcb83e7d58 # v2 uses: vedantmgoyal2009/winget-releaser@0db4f0a478166abd0fa438c631849f0b8dcfb99f
with: with:
identifier: Vencord.Vesktop identifier: Vencord.Vesktop
token: ${{ secrets.WINGET_PAT }} token: ${{ secrets.WINGET_PAT }}

1
.npmrc
View file

@ -1 +1,2 @@
node-linker=hoisted node-linker=hoisted
package-manager-strict=false

View file

@ -1,51 +1,63 @@
# Vesktop # Vesktop
Vesktop is a cross platform desktop app aiming to give you a snappier Discord experience with [Vencord](https://github.com/Vendicated/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**: **Not yet supported**:
- Global Keybinds - Global Keybinds
- see the [Roadmap](https://github.com/Vencord/Vesktop/issues/324)
Bug reports, feature requests & contributions are highly appreciated!!
![](https://github.com/Vencord/Vesktop/assets/45497981/8608a899-96a9-4027-9725-2cb02ba189fd) ![](https://github.com/Vencord/Vesktop/assets/45497981/8608a899-96a9-4027-9725-2cb02ba189fd)
![grafik](https://github.com/Vencord/Vesktop/assets/45497981/8701e5de-52c4-4346-a990-719cb971642e) ![](https://github.com/Vencord/Vesktop/assets/45497981/8701e5de-52c4-4346-a990-719cb971642e)
## Installing ## Installing
### Windows ### 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 ### Mac
Download and run Vesktop-VERSION.dmg from [releases](https://github.com/Vencord/Vesktop/releases/latest) [Vesktop.dmg](https://vencord.dev/download/vesktop/universal/dmg)
### Linux ### Linux
[![](https://dl.flathub.org/assets/badges/flathub-badge-en.svg)](https://flathub.org/apps/dev.vencord.Vesktop) [![Download on Flathub](https://dl.flathub.org/assets/badges/flathub-badge-en.svg)](https://flathub.org/apps/dev.vencord.Vesktop)
#### Arch based If you don't know the difference, pick amd64.
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) - 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)
#### Ubuntu/Debian based #### Community packages
Download Vesktop-VERSION.deb from [releases](https://github.com/Vencord/Vesktop/releases/latest) 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!
#### Fedora/RHEL based - Arch Linux: [Vesktop on the Arch user repository](https://aur.archlinux.org/packages?K=vesktop)
- NixOS: https://wiki.nixos.org/wiki/Discord#Vesktop
- Windows - Scoop: https://scoop.sh/#/apps?q=Vesktop
Download Vesktop-VERSION.rpm from [releases](https://github.com/Vencord/Vesktop/releases/latest) ## Building from Source
#### Other Packaging will create builds in the dist/ folder
Either download Vesktop-VERSION.AppImage and just run it directly or grab Vesktop-VERSION.tar.gz, extract it somewhere and run `vencorddesktop`.
If other packages are created, feel free to open an issue and we'll link them here.
## Building
Packaging will create builds in the dist/ folder. You can then install them like mentioned above or distribute them
```sh ```sh
git clone https://github.com/Vencord/Vesktop git clone https://github.com/Vencord/Vesktop
@ -64,11 +76,3 @@ pnpm package --linux pacman
# Or package to a directory only # Or package to a directory only
pnpm package:dir 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

Binary file not shown.

View 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>

View file

@ -1,8 +1,8 @@
!macro preInit !macro preInit
SetRegView 64 SetRegView 64
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop" WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop" WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop"
SetRegView 32 SetRegView 32
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop" WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop" WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop"
!macroend !macroend

102
eslint.config.mjs Normal file
View file

@ -0,0 +1,102 @@
/*
* 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
*/
//@ts-check
import stylistic from "@stylistic/eslint-plugin";
import pathAlias from "eslint-plugin-path-alias";
import header 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: {
header,
stylistic,
importSort,
unusedImports,
pathAlias,
prettier
},
settings: {
"import/resolver": {
alias: {
map: []
}
}
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname
}
},
rules: {
"header/header": [
"error",
{
files: ["scripts/header.txt"]
}
],
// 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"
}
}
);

View file

@ -28,6 +28,82 @@
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<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 &amp; 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 &amp; 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 &amp; 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"> <release version="0.4.4" date="2023-12-02" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.4</url> <url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.4</url>
<description> <description>
@ -134,7 +210,7 @@
<url type="vcs-browser">https://github.com/Vencord/Vesktop</url> <url type="vcs-browser">https://github.com/Vencord/Vesktop</url>
<categories> <categories>
<category>InstantMessaging</category> <category>InstantMessaging</category>
<category>AudioVideo</category> <category>Network</category>
</categories> </categories>
<requires> <requires>
<control>pointing</control> <control>pointing</control>
@ -146,11 +222,7 @@
<control>voice</control> <control>voice</control>
<display_length compare="ge">760</display_length> <display_length compare="ge">760</display_length>
<display_length compare="le">1200</display_length> <display_length compare="le">1200</display_length>
<internet>always</internet>
</recommends> </recommends>
<supports>
<internet>always</internet>
</supports>
<content_rating type="oars-1.1"> <content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute> <content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute> <content_attribute id="social-audio">intense</content_attribute>

View file

@ -1,8 +1,8 @@
{ {
"name": "VencordDesktop", "name": "vesktop",
"version": "0.4.4", "version": "1.5.3",
"private": true, "private": true,
"description": "", "description": "Vesktop is a custom Discord desktop app",
"keywords": [], "keywords": [],
"homepage": "https://vencord.dev/", "homepage": "https://vencord.dev/",
"license": "GPL-3.0", "license": "GPL-3.0",
@ -13,7 +13,7 @@
"build:dev": "pnpm build --dev", "build:dev": "pnpm build --dev",
"package": "pnpm build && electron-builder", "package": "pnpm build && electron-builder",
"package:dir": "pnpm build && electron-builder --dir", "package:dir": "pnpm build && electron-builder --dir",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.mts,.mjs", "lint": "eslint",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
"start": "pnpm build && electron .", "start": "pnpm build && electron .",
"start:dev": "pnpm build:dev && electron .", "start:dev": "pnpm build:dev && electron .",
@ -24,47 +24,48 @@
"updateMeta": "tsx scripts/utils/updateMeta.mts" "updateMeta": "tsx scripts/utils/updateMeta.mts"
}, },
"dependencies": { "dependencies": {
"arrpc": "github:OpenAsar/arrpc#3e22fd776273afaa4a80c51deb86077ffdd4d2ae" "arrpc": "github:OpenAsar/arrpc#5aadc307cb9bf4479f0a12364a253b07a77ace22",
"electron-updater": "^6.3.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"@vencord/venmic": "^2.1.3" "@vencord/venmic": "^6.1.0"
}, },
"devDependencies": { "devDependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@types/node": "^20.10.0", "@stylistic/eslint-plugin": "^2.8.0",
"@types/react": "^18.2.39", "@types/node": "^22.5.5",
"@typescript-eslint/eslint-plugin": "^6.13.1", "@types/react": "^18.3.8",
"@typescript-eslint/parser": "^6.13.1", "@vencord/types": "^1.8.4",
"@vencord/types": "^0.1.2", "dotenv": "^16.4.5",
"dotenv": "^16.3.1", "electron": "^33.0.2",
"electron": "^27.1.2", "electron-builder": "^25.0.5",
"electron-builder": "^24.9.1", "esbuild": "^0.23.1",
"esbuild": "^0.19.8", "eslint": "^9.11.0",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-license-header": "^0.6.0", "eslint-plugin-path-alias": "^2.1.0",
"eslint-plugin-path-alias": "^1.0.0", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-simple-header": "^1.2.1",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^3.0.0", "eslint-plugin-unused-imports": "^4.1.4",
"prettier": "^3.1.0", "prettier": "^3.3.3",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"tsx": "^4.6.0", "tsx": "^4.19.1",
"type-fest": "^4.8.2", "type-fest": "^4.26.1",
"typescript": "^5.3.2", "typescript": "^5.6.2",
"xml-formatter": "^3.6.0" "typescript-eslint": "^8.6.0",
"xml-formatter": "^3.6.3"
}, },
"packageManager": "pnpm@8.11.0", "packageManager": "pnpm@9.1.0",
"engines": { "engines": {
"node": ">=18", "node": ">=18",
"pnpm": ">=8" "pnpm": ">=8"
}, },
"build": { "build": {
"appId": "dev.vencord.desktop", "appId": "dev.vencord.vesktop",
"productName": "Vesktop", "productName": "Vesktop",
"files": [ "files": [
"!*", "!*",
"!node_modules",
"dist/js", "dist/js",
"static", "static",
"package.json", "package.json",
@ -110,8 +111,7 @@
"GenericName": "Internet Messenger", "GenericName": "Internet Messenger",
"Type": "Application", "Type": "Application",
"Categories": "Network;InstantMessaging;Chat;", "Categories": "Network;InstantMessaging;Chat;",
"Keywords": "discord;vencord;electron;chat;", "Keywords": "discord;vencord;electron;chat;"
"StartupWMClass": "VencordDesktop"
} }
}, },
"mac": { "mac": {
@ -119,18 +119,40 @@
{ {
"target": "default", "target": "default",
"arch": [ "arch": [
"x64", "universal"
"arm64"
] ]
} }
], ],
"category": "Network", "category": "public.app-category.social-networking",
"darkModeSupport": true,
"extendInfo": { "extendInfo": {
"NSMicrophoneUsageDescription": "This app needs access to the microphone", "NSMicrophoneUsageDescription": "This app needs access to the microphone",
"NSCameraUsageDescription": "This app needs access to the camera", "NSCameraUsageDescription": "This app needs access to the camera",
"com.apple.security.device.audio-input": true, "com.apple.security.device.audio-input": true,
"com.apple.security.device.camera": 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": { "nsis": {
"include": "build/installer.nsh", "include": "build/installer.nsh",
@ -138,12 +160,34 @@
}, },
"win": { "win": {
"target": [ "target": [
"nsis", {
"zip" "target": "nsis",
"arch": [
"x64",
"arm64"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
] ]
}, },
"publish": { "publish": {
"provider": "github" "provider": "github"
},
"rpm": {
"fpm": [
"--rpm-rpmbuild-define=_build_id_links none"
]
}
},
"pnpm": {
"patchedDependencies": {
"arrpc@3.5.0": "patches/arrpc@3.5.0.patch"
} }
} }
} }

14
patches/arrpc@3.5.0.patch Normal file
View file

@ -0,0 +1,14 @@
diff --git a/src/process/index.js b/src/process/index.js
index 97ea6514b54dd9c5df588c78f0397d31ab5f882a..c2bdbd6aaa5611bc6ff1d993beeb380b1f5ec575 100644
--- a/src/process/index.js
+++ b/src/process/index.js
@@ -5,8 +5,7 @@ import fs from 'node:fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8'));
+const DetectableDB = require('./detectable.json');
import * as Natives from './native/index.js';
const Native = Natives[process.platform];

File diff suppressed because it is too large Load diff

View file

@ -63,12 +63,6 @@ await Promise.all([
outfile: "dist/js/preload.js", outfile: "dist/js/preload.js",
footer: { js: "//# sourceURL=VCDPreload" } footer: { js: "//# sourceURL=VCDPreload" }
}), }),
createContext({
...NodeCommonOpts,
entryPoints: ["src/updater/preload.ts"],
outfile: "dist/js/updaterPreload.js",
footer: { js: "//# sourceURL=VCDUpdaterPreload" }
}),
createContext({ createContext({
...CommonOpts, ...CommonOpts,
globalName: "Vesktop", globalName: "Vesktop",

View file

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

View file

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

View file

@ -5,9 +5,41 @@
*/ */
import { app } from "electron"; import { app } from "electron";
import { join } from "path"; import { existsSync, mkdirSync, readdirSync, renameSync, rmdirSync } 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"));
const LEGACY_DATA_DIR = join(app.getPath("appData"), "VencordDesktop", "VencordDesktop");
export const DATA_DIR =
process.env.VENCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("userData")));
mkdirSync(DATA_DIR, { recursive: true });
// TODO: remove eventually
if (existsSync(LEGACY_DATA_DIR)) {
try {
console.warn("Detected legacy settings dir", LEGACY_DATA_DIR + ".", "migrating to", DATA_DIR);
for (const file of readdirSync(LEGACY_DATA_DIR)) {
renameSync(join(LEGACY_DATA_DIR, file), join(DATA_DIR, file));
}
rmdirSync(LEGACY_DATA_DIR);
renameSync(
join(app.getPath("appData"), "VencordDesktop", "IndexedDB"),
join(DATA_DIR, "sessionData", "IndexedDB")
);
} catch (e) {
console.error("Migration failed", e);
}
}
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_SETTINGS_DIR = join(DATA_DIR, "settings");
export const VENCORD_QUICKCSS_FILE = join(VENCORD_SETTINGS_DIR, "quickCss.css"); 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_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json");
@ -16,7 +48,8 @@ export const VENCORD_THEMES_DIR = join(DATA_DIR, "themes");
// needs to be inline require because of circular dependency // needs to be inline require because of circular dependency
// as otherwise "DATA_DIR" (which is used by ./settings) will be uninitialised // as otherwise "DATA_DIR" (which is used by ./settings) will be uninitialised
export const VENCORD_FILES_DIR = 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)`; export const USER_AGENT = `Vesktop/${app.getVersion()} (https://github.com/Vencord/Vesktop)`;
@ -26,14 +59,16 @@ export const MIN_HEIGHT = 500;
export const DEFAULT_WIDTH = 1280; export const DEFAULT_WIDTH = 1280;
export const DEFAULT_HEIGHT = 720; export const DEFAULT_HEIGHT = 720;
const UserAgents = { export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"];
darwin: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", const VersionString = `AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome.split(".")[0]}.0.0.0 Safari/537.36`;
windows: const BrowserUserAgents = {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" 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 UserAgent = UserAgents[process.platform] || UserAgents.windows; export const BrowserUserAgent = BrowserUserAgents[process.platform] || BrowserUserAgents.windows;
export const enum MessageBoxChoice { export const enum MessageBoxChoice {
Default, Default,

View file

@ -14,15 +14,15 @@ import { ICON_PATH, VIEW_DIR } from "shared/paths";
import { autoStart } from "./autoStart"; import { autoStart } from "./autoStart";
import { DATA_DIR } from "./constants"; import { DATA_DIR } from "./constants";
import { createWindows } from "./mainWindow"; import { createWindows } from "./mainWindow";
import { Settings } from "./settings"; import { Settings, State } from "./settings";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
interface Data { interface Data {
minimizeToTray: boolean;
discordBranch: "stable" | "canary" | "ptb"; discordBranch: "stable" | "canary" | "ptb";
autoStart: boolean; minimizeToTray?: "on";
importSettings: boolean; autoStart?: "on";
richPresence: boolean; importSettings?: "on";
richPresence?: "on";
} }
export function createFirstLaunchTour() { export function createFirstLaunchTour() {
@ -44,10 +44,11 @@ export function createFirstLaunchTour() {
if (!msg.startsWith("form:")) return; if (!msg.startsWith("form:")) return;
const data = JSON.parse(msg.slice(5)) as Data; 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.discordBranch = data.discordBranch;
Settings.store.firstLaunch = false; Settings.store.minimizeToTray = !!data.minimizeToTray;
Settings.store.arRPC = data.richPresence; Settings.store.arRPC = !!data.richPresence;
if (data.autoStart) autoStart.enable(); if (data.autoStart) autoStart.enable();

View file

@ -6,41 +6,68 @@
import "./ipc"; import "./ipc";
import { app, BrowserWindow } from "electron"; import { app, BrowserWindow, nativeTheme } from "electron";
import { checkUpdates } from "updater/main"; import { autoUpdater } from "electron-updater";
import { DATA_DIR } from "./constants"; import { DATA_DIR } from "./constants";
import { createFirstLaunchTour } from "./firstLaunch"; import { createFirstLaunchTour } from "./firstLaunch";
import { createWindows, mainWin } from "./mainWindow"; import { createWindows, mainWin } from "./mainWindow";
import { registerMediaPermissionsHandler } from "./mediaPermissions"; import { registerMediaPermissionsHandler } from "./mediaPermissions";
import { registerScreenShareHandler } from "./screenShare"; import { registerScreenShareHandler } from "./screenShare";
import { Settings } from "./settings"; import { Settings, State } from "./settings";
import { isDeckGameMode } from "./utils/steamOS";
if (IS_DEV) { if (IS_DEV) {
require("source-map-support").install(); require("source-map-support").install();
} else {
autoUpdater.checkForUpdatesAndNotify();
} }
// Make the Vencord files use our DATA_DIR // Make the Vencord files use our DATA_DIR
process.env.VENCORD_USER_DATA_DIR = DATA_DIR; process.env.VENCORD_USER_DATA_DIR = DATA_DIR;
function init() { function init() {
const { disableSmoothScroll } = Settings.store; const { disableSmoothScroll, hardwareAcceleration } = Settings.store;
const enabledFeatures = app.commandLine.getSwitchValue("enable-features").split(",");
const disabledFeatures = app.commandLine.getSwitchValue("disable-features").split(",");
if (hardwareAcceleration === false) {
app.disableHardwareAcceleration();
} else {
enabledFeatures.push("VaapiVideoDecodeLinuxGL", "VaapiVideoEncoder", "VaapiVideoDecoder");
}
if (disableSmoothScroll) { if (disableSmoothScroll) {
app.commandLine.appendSwitch("disable-smooth-scrolling"); app.commandLine.appendSwitch("disable-smooth-scrolling");
} }
// disable renderer backgrounding to prevent the app from unloading when in the background
// https://github.com/electron/electron/issues/2822
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
app.commandLine.appendSwitch("disable-renderer-backgrounding");
app.commandLine.appendSwitch("disable-background-timer-throttling");
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
if (process.platform === "win32") {
disabledFeatures.push("CalculateNativeWinOcclusion");
}
// work around chrome 66 disabling autoplay by default // work around chrome 66 disabling autoplay by default
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
// WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows. // WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows.
// HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service. // HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service.
// //
// WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790 // WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790
app.commandLine.appendSwitch( disabledFeatures.push("WinRetrieveSuggestionsOnlyOnDemand", "HardwareMediaKeyHandling", "MediaSessionService");
"disable-features",
"WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService,WidgetLayering" // Support TTS on Linux using speech-dispatcher
); app.commandLine.appendSwitch("enable-speech-dispatcher");
app.commandLine.appendSwitch("enable-features", [...new Set(enabledFeatures)].filter(Boolean).join(","));
app.commandLine.appendSwitch("disable-features", [...new Set(disabledFeatures)].filter(Boolean).join(","));
// In the Flatpak on SteamOS the theme is detected as light, but SteamOS only has a dark mode, so we just override it
if (isDeckGameMode) nativeTheme.themeSource = "dark";
app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => { app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => {
if (data.IS_DEV) app.quit(); if (data.IS_DEV) app.quit();
@ -52,8 +79,7 @@ function init() {
}); });
app.whenReady().then(async () => { app.whenReady().then(async () => {
checkUpdates(); if (process.platform === "win32") app.setAppUserModelId("dev.vencord.vesktop");
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.desktop");
registerScreenShareHandler(); registerScreenShareHandler();
registerMediaPermissionsHandler(); registerMediaPermissionsHandler();
@ -79,7 +105,7 @@ if (!app.requestSingleInstanceLock({ IS_DEV })) {
} }
async function bootstrap() { async function bootstrap() {
if (!Object.hasOwn(Settings.store, "firstLaunch")) { if (!Object.hasOwn(State.store, "firstLaunch")) {
createFirstLaunchTour(); createFirstLaunchTour();
} else { } else {
createWindows(); createWindows();

View file

@ -4,10 +4,10 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
if (process.platform === "linux") import("./virtmic"); if (process.platform === "linux") import("./venmic");
import { execFile } from "child_process"; import { execFile } from "child_process";
import { app, BrowserWindow, dialog, RelaunchOptions, session, shell } from "electron"; import { app, BrowserWindow, clipboard, dialog, nativeImage, RelaunchOptions, session, shell } from "electron";
import { mkdirSync, readFileSync, watch } from "fs"; import { mkdirSync, readFileSync, watch } from "fs";
import { open, readFile } from "fs/promises"; import { open, readFile } from "fs/promises";
import { release } from "os"; import { release } from "os";
@ -19,8 +19,10 @@ import { setBadgeCount } from "./appBadge";
import { autoStart } from "./autoStart"; import { autoStart } from "./autoStart";
import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants"; import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
import { mainWin } from "./mainWindow"; import { mainWin } from "./mainWindow";
import { Settings } from "./settings"; import { Settings, State } from "./settings";
import { handle, handleSync } from "./utils/ipcWrappers"; import { handle, handleSync } from "./utils/ipcWrappers";
import { PopoutWindows } from "./utils/popout";
import { isDeckGameMode, showGamePage } from "./utils/steamOS";
import { isValidVencordInstall } from "./utils/vencordLoader"; import { isValidVencordInstall } from "./utils/vencordLoader";
handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload.js")); handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload.js"));
@ -47,11 +49,14 @@ handle(IpcEvents.SET_SETTINGS, (_, settings: typeof Settings.store, path?: strin
Settings.setData(settings, path); Settings.setData(settings, path);
}); });
handle(IpcEvents.RELAUNCH, () => { handle(IpcEvents.RELAUNCH, async () => {
const options: RelaunchOptions = { const options: RelaunchOptions = {
args: process.argv.slice(1).concat(["--relaunch"]) args: process.argv.slice(1).concat(["--relaunch"])
}; };
if (app.isPackaged && process.env.APPIMAGE) { 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); execFile(process.env.APPIMAGE, options.args);
} else { } else {
app.relaunch(options); app.relaunch(options);
@ -64,14 +69,16 @@ handle(IpcEvents.SHOW_ITEM_IN_FOLDER, (_, path) => {
}); });
handle(IpcEvents.FOCUS, () => { handle(IpcEvents.FOCUS, () => {
if (process.platform === "win32") mainWin.minimize(); // Windows is weird
mainWin.restore();
mainWin.show(); mainWin.show();
mainWin.setSkipTaskbar(false);
}); });
handle(IpcEvents.CLOSE, e => { handle(IpcEvents.CLOSE, (e, key?: string) => {
(BrowserWindow.fromWebContents(e.sender) ?? e.sender).close(); const popout = PopoutWindows.get(key!);
if (popout) return popout.close();
const win = BrowserWindow.fromWebContents(e.sender) ?? e.sender;
win.close();
}); });
handle(IpcEvents.MINIMIZE, e => { handle(IpcEvents.MINIMIZE, e => {
@ -86,12 +93,8 @@ handle(IpcEvents.MAXIMIZE, e => {
} }
}); });
handle(IpcEvents.SPELLCHECK_SET_LANGUAGES, (_, languages: string[]) => { handleSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES, e => {
const ses = session.defaultSession; e.returnValue = session.defaultSession.availableSpellCheckerLanguages;
const available = ses.availableSpellCheckerLanguages;
const applicable = languages.filter(l => available.includes(l)).slice(0, 3);
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
}); });
handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => { handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => {
@ -102,7 +105,14 @@ handle(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, (e, word: string) => {
e.sender.session.addWordToSpellCheckerDictionary(word); e.sender.session.addWordToSpellCheckerDictionary(word);
}); });
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!, { const res = await dialog.showOpenDialog(mainWin!, {
properties: ["openDirectory"] properties: ["openDirectory"]
}); });
@ -111,11 +121,20 @@ handle(IpcEvents.SELECT_VENCORD_DIR, async () => {
const dir = res.filePaths[0]; const dir = res.filePaths[0];
if (!isValidVencordInstall(dir)) return "invalid"; if (!isValidVencordInstall(dir)) return "invalid";
return dir; State.store.vencordDir = dir;
return "ok";
}); });
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 readCss() { function readCss() {
return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => ""); return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => "");
} }

View file

@ -12,6 +12,8 @@ import {
Menu, Menu,
MenuItemConstructorOptions, MenuItemConstructorOptions,
nativeTheme, nativeTheme,
screen,
session,
Tray Tray
} from "electron"; } from "electron";
import { rm } from "fs/promises"; import { rm } from "fs/promises";
@ -25,16 +27,16 @@ import { ICON_PATH } from "../shared/paths";
import { createAboutWindow } from "./about"; import { createAboutWindow } from "./about";
import { initArRPC } from "./arrpc"; import { initArRPC } from "./arrpc";
import { import {
BrowserUserAgent,
DATA_DIR, DATA_DIR,
DEFAULT_HEIGHT, DEFAULT_HEIGHT,
DEFAULT_WIDTH, DEFAULT_WIDTH,
MessageBoxChoice, MessageBoxChoice,
MIN_HEIGHT, MIN_HEIGHT,
MIN_WIDTH, MIN_WIDTH,
UserAgent,
VENCORD_FILES_DIR VENCORD_FILES_DIR
} from "./constants"; } from "./constants";
import { Settings, VencordSettings } from "./settings"; import { Settings, State, VencordSettings } from "./settings";
import { createSplashWindow } from "./splash"; import { createSplashWindow } from "./splash";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS"; import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS";
@ -73,20 +75,23 @@ const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpe
const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings); const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings);
function initTray(win: BrowserWindow) { function initTray(win: BrowserWindow) {
const onTrayClick = () => {
if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide();
else win.show();
};
const trayMenu = Menu.buildFromTemplate([ const trayMenu = Menu.buildFromTemplate([
{ {
label: "Open", label: "Open",
click() { click() {
win.show(); win.show();
}, }
enabled: false
}, },
{ {
label: "About", label: "About",
click: createAboutWindow click: createAboutWindow
}, },
{ {
label: "Update Vencord", label: "Repair Vencord",
async click() { async click() {
await downloadVencordFiles(); await downloadVencordFiles();
app.relaunch(); app.relaunch();
@ -103,14 +108,14 @@ function initTray(win: BrowserWindow) {
type: "separator" type: "separator"
}, },
{ {
label: "Relaunch", label: "Restart",
click() { click() {
app.relaunch(); app.relaunch();
app.quit(); app.quit();
} }
}, },
{ {
label: "Quit Vesktop", label: "Quit",
click() { click() {
isQuitting = true; isQuitting = true;
app.quit(); app.quit();
@ -121,15 +126,7 @@ function initTray(win: BrowserWindow) {
tray = new Tray(ICON_PATH); tray = new Tray(ICON_PATH);
tray.setToolTip("Vesktop"); tray.setToolTip("Vesktop");
tray.setContextMenu(trayMenu); tray.setContextMenu(trayMenu);
tray.on("click", () => win.show()); tray.on("click", onTrayClick);
win.on("show", () => {
trayMenu.items[0].enabled = false;
});
win.on("hide", () => {
trayMenu.items[0].enabled = true;
});
} }
async function clearData(win: BrowserWindow) { async function clearData(win: BrowserWindow) {
@ -210,7 +207,6 @@ function initMenuBar(win: BrowserWindow) {
type: "separator" type: "separator"
}, },
{ {
label: "Hide Vesktop", // Should probably remove the label, but it says "Hide VencordDesktop" instead of "Hide Vesktop"
role: "hide" role: "hide"
}, },
{ {
@ -268,14 +264,16 @@ function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
// We want the default window behaivour to apply in game mode since it expects everything to be fullscreen and maximized. // We want the default window behaivour to apply in game mode since it expects everything to be fullscreen and maximized.
if (isDeckGameMode) return {}; if (isDeckGameMode) return {};
const { x, y, width, height } = Settings.store.windowBounds ?? {}; const { x, y, width, height } = State.store.windowBounds ?? {};
const options = { const options = {
width: width ?? DEFAULT_WIDTH, width: width ?? DEFAULT_WIDTH,
height: height ?? DEFAULT_HEIGHT height: height ?? DEFAULT_HEIGHT
} as BrowserWindowConstructorOptions; } as BrowserWindowConstructorOptions;
if (x != null && y != null) { const storedDisplay = screen.getAllDisplays().find(display => display.id === State.store.displayid);
if (x != null && y != null && storedDisplay) {
options.x = x; options.x = x;
options.y = y; options.y = y;
} }
@ -313,8 +311,8 @@ function getDarwinOptions(): BrowserWindowConstructorOptions {
function initWindowBoundsListeners(win: BrowserWindow) { function initWindowBoundsListeners(win: BrowserWindow) {
const saveState = () => { const saveState = () => {
Settings.store.maximized = win.isMaximized(); State.store.maximized = win.isMaximized();
Settings.store.minimized = win.isMinimized(); State.store.minimized = win.isMinimized();
}; };
win.on("maximize", saveState); win.on("maximize", saveState);
@ -322,7 +320,8 @@ function initWindowBoundsListeners(win: BrowserWindow) {
win.on("unmaximize", saveState); win.on("unmaximize", saveState);
const saveBounds = () => { 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); win.on("resize", saveBounds);
@ -362,12 +361,27 @@ function initSettingsListeners(win: BrowserWindow) {
addSettingsListener("enableMenu", enabled => { addSettingsListener("enableMenu", enabled => {
win.setAutoHideMenuBar(enabled ?? false); win.setAutoHideMenuBar(enabled ?? false);
}); });
addSettingsListener("spellCheckLanguages", languages => initSpellCheckLanguages(win, languages));
}
async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) {
languages ??= await win.webContents.executeJavaScript("[...new Set(navigator.languages)]").catch(() => []);
if (!languages) return;
const ses = session.defaultSession;
const available = ses.availableSpellCheckerLanguages;
const applicable = languages.filter(l => available.includes(l)).slice(0, 5);
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
} }
function initSpellCheck(win: BrowserWindow) { function initSpellCheck(win: BrowserWindow) {
win.webContents.on("context-menu", (_, data) => { win.webContents.on("context-menu", (_, data) => {
win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions); win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions);
}); });
initSpellCheckLanguages(win, Settings.store.spellCheckLanguages);
} }
function createMainWindow() { function createMainWindow() {
@ -375,11 +389,11 @@ function createMainWindow() {
removeSettingsListeners(); removeSettingsListeners();
removeVencordSettingsListeners(); removeVencordSettingsListeners();
const { staticTitle, transparencyOption, enableMenu, discordWindowsTitleBar } = Settings.store; const { staticTitle, transparencyOption, enableMenu, customTitleBar } = Settings.store;
const { frameless } = VencordSettings.store; const { frameless, transparent } = VencordSettings.store;
const noFrame = frameless === true || (process.platform === "win32" && discordWindowsTitleBar === true); const noFrame = frameless === true || customTitleBar === true;
const win = (mainWin = new BrowserWindow({ const win = (mainWin = new BrowserWindow({
show: false, show: false,
@ -389,14 +403,25 @@ function createMainWindow() {
contextIsolation: true, contextIsolation: true,
devTools: true, devTools: true,
preload: join(__dirname, "preload.js"), preload: join(__dirname, "preload.js"),
spellcheck: true spellcheck: true,
// disable renderer backgrounding to prevent the app from unloading when in the background
backgroundThrottling: false
}, },
icon: ICON_PATH, icon: ICON_PATH,
frame: !noFrame, frame: !noFrame,
...(transparent && {
transparent: true,
backgroundColor: "#00000000"
}),
...(transparencyOption && ...(transparencyOption &&
transparencyOption !== "none" && { transparencyOption !== "none" && {
backgroundColor: "#00000000", backgroundColor: "#00000000",
backgroundMaterial: transparencyOption, backgroundMaterial: transparencyOption
}),
// Fix transparencyOption for custom discord titlebar
...(customTitleBar &&
transparencyOption &&
transparencyOption !== "none" && {
transparent: true transparent: true
}), }),
...(staticTitle && { title: "Vesktop" }), ...(staticTitle && { title: "Vesktop" }),
@ -405,6 +430,7 @@ function createMainWindow() {
autoHideMenuBar: enableMenu autoHideMenuBar: enableMenu
})); }));
win.setMenuBarVisibility(false); win.setMenuBarVisibility(false);
if (process.platform === "darwin" && customTitleBar) win.setWindowButtonVisibility(false);
win.on("close", e => { win.on("close", e => {
const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false; const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false;
@ -427,7 +453,7 @@ function createMainWindow() {
initSettingsListeners(win); initSettingsListeners(win);
initSpellCheck(win); initSpellCheck(win);
win.webContents.setUserAgent(UserAgent); win.webContents.setUserAgent(BrowserUserAgent);
const subdomain = const subdomain =
Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb" Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb"
@ -442,7 +468,7 @@ function createMainWindow() {
const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js"))); const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js")));
export async function createWindows() { export async function createWindows() {
const { startMinimized } = Settings.store; const startMinimized = process.argv.includes("--start-minimized");
const splash = createSplashWindow(startMinimized); const splash = createSplashWindow(startMinimized);
// SteamOS letterboxes and scales it terribly, so just full screen it // SteamOS letterboxes and scales it terribly, so just full screen it
if (isDeckGameMode) splash.setFullScreen(true); if (isDeckGameMode) splash.setFullScreen(true);
@ -454,7 +480,10 @@ export async function createWindows() {
mainWin.webContents.on("did-finish-load", () => { mainWin.webContents.on("did-finish-load", () => {
splash.destroy(); splash.destroy();
if (!startMinimized || isDeckGameMode) mainWin!.show(); if (!startMinimized) {
mainWin!.show();
if (State.store.maximized && !isDeckGameMode) mainWin!.maximize();
}
if (isDeckGameMode) { if (isDeckGameMode) {
// always use entire display // always use entire display
@ -462,13 +491,25 @@ export async function createWindows() {
askToApplySteamLayout(mainWin); askToApplySteamLayout(mainWin);
} }
});
mainWin.once("show", () => { mainWin.once("show", () => {
if (Settings.store.maximized && !mainWin!.isMaximized() && !isDeckGameMode) { if (State.store.maximized && !mainWin!.isMaximized() && !isDeckGameMode) {
mainWin!.maximize(); mainWin!.maximize();
} }
}); });
});
// evil hack to fix electron 32 regression that makes devtools always light theme
// https://github.com/electron/electron/issues/43367
// TODO: remove once fixed
mainWin.webContents.on("devtools-opened", () => {
if (!nativeTheme.shouldUseDarkColors) return;
nativeTheme.themeSource = "light";
setTimeout(() => {
nativeTheme.themeSource = "dark";
}, 100);
});
initArRPC(); initArRPC();
} }

View file

@ -12,12 +12,14 @@ export function registerMediaPermissionsHandler() {
session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => { session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => {
let granted = true; let granted = true;
if ("mediaTypes" in details) {
if (details.mediaTypes?.includes("audio")) { if (details.mediaTypes?.includes("audio")) {
granted = await systemPreferences.askForMediaAccess("microphone"); granted &&= await systemPreferences.askForMediaAccess("microphone");
} }
if (details.mediaTypes?.includes("video")) { if (details.mediaTypes?.includes("video")) {
granted &&= await systemPreferences.askForMediaAccess("camera"); granted &&= await systemPreferences.askForMediaAccess("camera");
} }
}
callback(granted); callback(granted);
}); });

View file

@ -4,14 +4,15 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import { mkdirSync, readFileSync, writeFileSync } from "fs"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path"; 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 { SettingsStore } from "shared/utils/SettingsStore";
import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants"; import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants";
const SETTINGS_FILE = join(DATA_DIR, "settings.json"); 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) { function loadSettings<T extends object = any>(file: string, name: string) {
let settings = {} as T; let settings = {} as T;
@ -20,7 +21,7 @@ function loadSettings<T extends object = any>(file: string, name: string) {
try { try {
settings = JSON.parse(content); settings = JSON.parse(content);
} catch (err) { } catch (err) {
console.error(`Failed to parse ${name} settings.json:`, err); console.error(`Failed to parse ${name}.json:`, err);
} }
} catch {} } catch {}
@ -33,5 +34,19 @@ function loadSettings<T extends object = any>(file: string, name: string) {
return store; return store;
} }
export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop"); export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop settings");
export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord");
export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord settings");
if (Object.hasOwn(Settings.plain, "firstLaunch") && !existsSync(STATE_FILE)) {
console.warn("legacy state in settings.json detected. migrating to state.json");
const state = {} as TState;
for (const prop of ["firstLaunch", "maximized", "minimized", "steamOSLayoutVersion", "windowBounds"] as const) {
state[prop] = Settings.plain[prop];
delete Settings.plain[prop];
}
Settings.markAsChanged();
writeFileSync(STATE_FILE, JSON.stringify(state, null, 4));
}
export const State = loadSettings<TState>(STATE_FILE, "Vesktop state");

View file

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

View file

@ -5,20 +5,14 @@
*/ */
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron"; import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron";
import { DISCORD_HOSTNAMES } from "main/constants";
import { IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
export function validateSender(frame: WebFrameMain) { export function validateSender(frame: WebFrameMain) {
const { hostname, protocol } = new URL(frame.url); const { hostname, protocol } = new URL(frame.url);
if (protocol === "file:") return; if (protocol === "file:") return;
switch (hostname) { if (!DISCORD_HOSTNAMES.includes(hostname)) throw new Error("ipc: Disallowed host " + hostname);
case "discord.com":
case "ptb.discord.com":
case "canary.discord.com":
break;
default:
throw new Error("ipc: Disallowed host " + hostname);
}
} }
export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) { export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) {

View file

@ -5,22 +5,20 @@
*/ */
import { BrowserWindow, shell } from "electron"; import { BrowserWindow, shell } from "electron";
import { DISCORD_HOSTNAMES } from "main/constants";
import { Settings } from "../settings"; import { Settings } from "../settings";
import { createOrFocusPopup, setupPopout } from "./popout";
import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS";
export function makeLinksOpenExternally(win: BrowserWindow) { export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } {
win.webContents.setWindowOpenHandler(({ url }) => { if (protocol == null) {
switch (url) {
case "about:blank":
case "https://discord.com/popout":
return { action: "allow" };
}
try { try {
var { protocol } = new URL(url); protocol = new URL(url).protocol;
} catch { } catch {
return { action: "deny" }; return { action: "deny" };
} }
}
switch (protocol) { switch (protocol) {
case "http:": case "http:":
@ -30,11 +28,44 @@ export function makeLinksOpenExternally(win: BrowserWindow) {
} }
// eslint-disable-next-line no-fallthrough // eslint-disable-next-line no-fallthrough
case "mailto:": case "mailto:":
case "steam:":
case "spotify:": case "spotify:":
if (isDeckGameMode) {
steamOpenURL(url);
} else {
shell.openExternal(url); shell.openExternal(url);
} }
break;
case "steam:":
if (isDeckGameMode) {
execSteamURL(url);
} else {
shell.openExternal(url);
}
break;
}
return { action: "deny" }; return { action: "deny" };
}
export function makeLinksOpenExternally(win: BrowserWindow) {
win.webContents.setWindowOpenHandler(({ url, frameName, features }) => {
try {
var { protocol, hostname, pathname } = new URL(url);
} catch {
return { action: "deny" };
}
if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) {
return createOrFocusPopup(frameName, features);
}
if (url === "about:blank" || (frameName === "authorize" && DISCORD_HOSTNAMES.includes(hostname)))
return { action: "allow" };
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
View file

@ -0,0 +1,116 @@
/*
* 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 { 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);
});
}

View file

@ -4,15 +4,12 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import { exec as callbackExec } from "child_process";
import { BrowserWindow, dialog } from "electron"; import { BrowserWindow, dialog } from "electron";
import { sleep } from "shared/utils/sleep"; import { writeFile } from "fs/promises";
import { promisify } from "util"; import { join } from "path";
import { MessageBoxChoice } from "../constants"; import { MessageBoxChoice } from "../constants";
import { Settings } from "../settings"; import { State } from "../settings";
const exec = promisify(callbackExec);
// Bump this to re-show the prompt // Bump this to re-show the prompt
const layoutVersion = 2; const layoutVersion = 2;
@ -20,6 +17,8 @@ const layoutVersion = 2;
const layoutId = "3080264545"; // Vesktop Layout v2 const layoutId = "3080264545"; // Vesktop Layout v2
const numberRegex = /^[0-9]*$/; const numberRegex = /^[0-9]*$/;
let steamPipeQueue = Promise.resolve();
export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1"; export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1";
export function applyDeckKeyboardFix() { export function applyDeckKeyboardFix() {
@ -42,23 +41,37 @@ function getAppId(): string | null {
return null; return null;
} }
async function execSteamURL(url: string): Promise<void> { export function execSteamURL(url: string) {
await exec(`steam -ifrunning ${url}`); // 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) { async function showLayout(appId: string) {
await execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`); execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`);
// because the UI doesn't consistently reload after the data for the config has loaded...
// HOW HAS NOBODY AT VALVE RUN INTO THIS YET
await sleep(100);
await execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`);
} }
export async function askToApplySteamLayout(win: BrowserWindow) { export async function askToApplySteamLayout(win: BrowserWindow) {
const appId = getAppId(); const appId = getAppId();
if (!appId) return; if (!appId) return;
if (Settings.store.steamOSLayoutVersion === layoutVersion) return; if (State.store.steamOSLayoutVersion === layoutVersion) return;
const update = Boolean(Settings.store.steamOSLayoutVersion); 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. // 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, { const { response } = await dialog.showMessageBox(win, {
@ -74,8 +87,8 @@ ${update ? "Click" : "Tap"} no to keep your current layout.`,
type: "question" type: "question"
}); });
if (Settings.store.steamOSLayoutVersion !== layoutVersion) { if (State.store.steamOSLayoutVersion !== layoutVersion) {
Settings.store.steamOSLayoutVersion = layoutVersion; State.store.steamOSLayoutVersion = layoutVersion;
} }
if (response === MessageBoxChoice.Cancel) return; if (response === MessageBoxChoice.Cancel) return;

View file

@ -4,12 +4,12 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import { existsSync, mkdirSync } from "fs"; import { mkdirSync } from "fs";
import type { RequestOptions } from "https"; import { access, constants as FsConstants } from "fs/promises";
import { join } from "path"; import { join } from "path";
import { USER_AGENT, VENCORD_FILES_DIR } from "../constants"; import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
import { downloadFile, simpleGet } from "./http"; import { downloadFile, fetchie } from "./http";
const API_BASE = "https://api.github.com"; const API_BASE = "https://api.github.com";
@ -31,36 +31,44 @@ export interface ReleaseData {
} }
export async function githubGet(endpoint: string) { export async function githubGet(endpoint: string) {
const opts: RequestOptions = { const opts: RequestInit = {
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
"User-Agent": USER_AGENT "User-Agent": USER_AGENT
} }
}; };
if (process.env.GITHUB_TOKEN) opts.headers!.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; if (process.env.GITHUB_TOKEN) (opts.headers! as any).Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
return simpleGet(API_BASE + endpoint, opts); return fetchie(API_BASE + endpoint, opts, { retryOnNetworkError: true });
} }
export async function downloadVencordFiles() { export async function downloadVencordFiles() {
const release = await githubGet("/repos/Vendicated/Vencord/releases/latest"); const release = await githubGet("/repos/Vendicated/Vencord/releases/latest");
const { assets } = JSON.parse(release.toString("utf-8")) as ReleaseData; const { assets }: ReleaseData = await release.json();
await Promise.all( await Promise.all(
assets assets
.filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f))) .filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f)))
.map(({ name, browser_download_url }) => downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name))) .map(({ name, browser_download_url }) =>
downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name), {}, { retryOnNetworkError: true })
)
); );
} }
export function isValidVencordInstall(dir: string) { const existsAsync = (path: string) =>
return FILES_TO_DOWNLOAD.every(f => existsSync(join(dir, f))); access(path, FsConstants.F_OK)
.then(() => true)
.catch(() => false);
export async function isValidVencordInstall(dir: string) {
return Promise.all(FILES_TO_DOWNLOAD.map(f => existsAsync(join(dir, f)))).then(arr => !arr.includes(false));
} }
export async function ensureVencordFiles() { export async function ensureVencordFiles() {
if (isValidVencordInstall(VENCORD_FILES_DIR)) return; if (await isValidVencordInstall(VENCORD_FILES_DIR)) return;
mkdirSync(VENCORD_FILES_DIR, { recursive: true }); mkdirSync(VENCORD_FILES_DIR, { recursive: true });
await downloadVencordFiles(); await downloadVencordFiles();

135
src/main/venmic.ts Normal file
View file

@ -0,0 +1,135 @@
/*
* 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 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());

View file

@ -1,77 +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 { app, ipcMain } from "electron";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths";
let initialized = false;
let patchBay: import("@vencord/venmic").PatchBay | undefined;
let isGlibcxxToOld = false;
function getRendererAudioServicePid() {
return (
app
.getAppMetrics()
.find(proc => proc.name === "Audio Service")
?.pid?.toString() ?? "owo"
);
}
function obtainVenmic() {
if (!initialized) {
initialized = true;
try {
const { PatchBay } = require(
join(STATIC_DIR, `dist/venmic-${process.arch}.node`)
) as typeof import("@vencord/venmic");
patchBay = new PatchBay();
} catch (e: any) {
console.error("Failed to initialise venmic. Make sure you're using pipewire", e);
isGlibcxxToOld = (e?.stack || e?.message || "").toLowerCase().includes("glibc");
}
}
return patchBay;
}
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
const audioPid = getRendererAudioServicePid();
const list = obtainVenmic()
?.list()
.filter(s => s["application.process.id"] !== audioPid)
.map(s => s["application.name"]);
return list
? { ok: true, targets: [...new Set(list)] } // Remove duplicates
: { ok: false, isGlibcxxToOld };
});
ipcMain.handle(
IpcEvents.VIRT_MIC_START,
(_, targets: string[]) =>
obtainVenmic()?.link({
props: targets.map(target => ({ key: "application.name", value: target })),
mode: "include"
})
);
ipcMain.handle(
IpcEvents.VIRT_MIC_START_SYSTEM,
() =>
obtainVenmic()?.link({
props: [
{
key: "application.process.id",
value: getRendererAudioServicePid()
}
],
mode: "exclude"
})
);
ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink());

View file

@ -4,9 +4,9 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import { Node } from "@vencord/venmic";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import type { Settings } from "shared/settings"; import type { Settings } from "shared/settings";
import type { LiteralUnion } from "type-fest";
import { IpcEvents } from "../shared/IpcEvents"; import { IpcEvents } from "../shared/IpcEvents";
import { invoke, sendSync } from "./typedIpc"; import { invoke, sendSync } from "./typedIpc";
@ -33,14 +33,15 @@ export const VesktopNative = {
}, },
fileManager: { fileManager: {
showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path), 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: { settings: {
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS), get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path) set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path)
}, },
spellcheck: { spellcheck: {
setLanguages: (languages: readonly string[]) => invoke<void>(IpcEvents.SPELLCHECK_SET_LANGUAGES, languages), getAvailableLanguages: () => sendSync<string[]>(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES),
onSpellcheckResult(cb: SpellCheckerResultCallback) { onSpellcheckResult(cb: SpellCheckerResultCallback) {
spellCheckCallbacks.add(cb); spellCheckCallbacks.add(cb);
}, },
@ -52,7 +53,7 @@ export const VesktopNative = {
}, },
win: { win: {
focus: () => invoke<void>(IpcEvents.FOCUS), focus: () => invoke<void>(IpcEvents.FOCUS),
close: () => invoke<void>(IpcEvents.CLOSE), close: (key?: string) => invoke<void>(IpcEvents.CLOSE, key),
minimize: () => invoke<void>(IpcEvents.MINIMIZE), minimize: () => invoke<void>(IpcEvents.MINIMIZE),
maximize: () => invoke<void>(IpcEvents.MAXIMIZE) maximize: () => invoke<void>(IpcEvents.MAXIMIZE)
}, },
@ -62,14 +63,20 @@ export const VesktopNative = {
/** only available on Linux. */ /** only available on Linux. */
virtmic: { virtmic: {
list: () => list: () =>
invoke<{ ok: false; isGlibcxxToOld: boolean } | { ok: true; targets: string[] }>(IpcEvents.VIRT_MIC_LIST), invoke<
start: (targets: string[]) => invoke<void>(IpcEvents.VIRT_MIC_START, targets), { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean }
startSystem: () => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM), >(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) stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP)
}, },
arrpc: { arrpc: {
onActivity(cb: (data: string) => void) { onActivity(cb: (data: string) => void) {
ipcRenderer.on(IpcEvents.ARRPC_ACTIVITY, (_, data: string) => cb(data)); ipcRenderer.on(IpcEvents.ARRPC_ACTIVITY, (_, data: string) => cb(data));
} }
},
clipboard: {
copyImage: (imageBuffer: Uint8Array, imageSrc: string) =>
invoke<void>(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc)
} }
}; };

View file

@ -40,5 +40,3 @@ if (IS_DEV) {
}); });
} }
// #endregion // #endregion
VesktopNative.spellcheck.setLanguages(window.navigator.languages);

View file

@ -6,7 +6,7 @@
import "./screenSharePicker.css"; import "./screenSharePicker.css";
import { closeModal, Modals, openModal, useAwaiter } from "@vencord/types/utils"; import { closeModal, Logger, Modals, ModalSize, openModal, useAwaiter } from "@vencord/types/utils";
import { findStoreLazy, onceReady } from "@vencord/types/webpack"; import { findStoreLazy, onceReady } from "@vencord/types/webpack";
import { import {
Button, Button,
@ -19,8 +19,10 @@ import {
UserStore, UserStore,
useState useState
} from "@vencord/types/webpack/common"; } from "@vencord/types/webpack/common";
import { Node } from "@vencord/venmic";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { addPatch } from "renderer/patches/shared"; import { addPatch } from "renderer/patches/shared";
import { useSettings } from "renderer/settings";
import { isLinux, isWindows } from "renderer/utils"; import { isLinux, isWindows } from "renderer/utils";
const StreamResolutions = ["480", "720", "1080", "1440"] as const; const StreamResolutions = ["480", "720", "1080", "1440"] as const;
@ -31,11 +33,23 @@ const MediaEngineStore = findStoreLazy("MediaEngineStore");
export type StreamResolution = (typeof StreamResolutions)[number]; export type StreamResolution = (typeof StreamResolutions)[number];
export type StreamFps = (typeof StreamFps)[number]; export type StreamFps = (typeof StreamFps)[number];
type SpecialSource = "None" | "Entire System";
type AudioSource = SpecialSource | Node;
type AudioSources = SpecialSource | Node[];
interface AudioItem {
name: string;
value: AudioSource;
}
interface StreamSettings { interface StreamSettings {
resolution: StreamResolution; resolution: StreamResolution;
fps: StreamFps; fps: StreamFps;
audio: boolean; audio: boolean;
audioSource?: string; contentHint?: string;
includeSources?: AudioSources;
excludeSources?: AudioSources;
} }
export interface StreamPick extends StreamSettings { export interface StreamPick extends StreamSettings {
@ -48,7 +62,9 @@ interface Source {
url: string; url: string;
} }
let currentSettings: StreamSettings | null = null; export let currentSettings: StreamSettings | null = null;
const logger = new Logger("VesktopScreenShare");
addPatch({ addPatch({
patches: [ patches: [
@ -72,6 +88,14 @@ addPatch({
bitrateMax: 8000000, bitrateMax: 8000000,
bitrateTarget: 600000 bitrateTarget: 600000
}); });
if (opts?.encode) {
Object.assign(opts.encode, {
framerate,
width,
height,
pixelCount: height * width
});
}
Object.assign(opts.capture, { Object.assign(opts.capture, {
framerate, framerate,
width, width,
@ -83,11 +107,14 @@ addPatch({
if (isLinux) { if (isLinux) {
onceReady.then(() => { onceReady.then(() => {
FluxDispatcher.subscribe("VOICE_STATE_UPDATES", e => { FluxDispatcher.subscribe("STREAM_CLOSE", ({ streamKey }: { streamKey: string }) => {
for (const state of e.voiceStates) { const owner = streamKey.split(":").at(-1);
if (state.userId === UserStore.getCurrentUser().id && state.oldChannelId && !state.channelId)
VesktopNative.virtmic.stop(); if (owner !== UserStore.getCurrentUser().id) {
return;
} }
VesktopNative.virtmic.stop();
}); });
}); });
} }
@ -102,13 +129,17 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
modalProps={props} modalProps={props}
submit={async v => { submit={async v => {
didSubmit = true; didSubmit = true;
if (v.audioSource && v.audioSource !== "None") {
if (v.audioSource === "Entire System") { if (v.includeSources && v.includeSources !== "None") {
await VesktopNative.virtmic.startSystem(); if (v.includeSources === "Entire System") {
await VesktopNative.virtmic.startSystem(
!v.excludeSources || isSpecialSource(v.excludeSources) ? [] : v.excludeSources
);
} else { } else {
await VesktopNative.virtmic.start([v.audioSource]); await VesktopNative.virtmic.start(v.includeSources);
} }
} }
resolve(v); resolve(v);
}} }}
close={() => { close={() => {
@ -143,6 +174,136 @@ function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScre
); );
} }
function AudioSettingsModal({
modalProps,
close,
setAudioSources
}: {
modalProps: any;
close: () => void;
setAudioSources: (s: AudioSources) => void;
}) {
const Settings = useSettings();
return (
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Modals.ModalHeader className="vcd-screen-picker-header">
<Forms.FormTitle tag="h2">Venmic Settings</Forms.FormTitle>
<Modals.ModalCloseButton onClick={close} />
</Modals.ModalHeader>
<Modals.ModalContent className="vcd-screen-picker-modal">
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, workaround: v })}
value={Settings.audio?.workaround ?? false}
note={
<>
Work around an issue that causes the microphone to be shared instead of the correct audio.
Only enable if you're experiencing this issue.
</>
}
>
Microphone Workaround
</Switch>
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, onlySpeakers: v })}
value={Settings.audio?.onlySpeakers ?? true}
note={
<>
When sharing entire desktop audio, only share apps that play to a speaker. You may want to
disable this when using "mix bussing".
</>
}
>
Only Speakers
</Switch>
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })}
value={Settings.audio?.onlyDefaultSpeakers ?? true}
note={
<>
When sharing entire desktop audio, only share apps that play to the <b>default</b> speakers.
You may want to disable this when using "mix bussing".
</>
}
>
Only Default Speakers
</Switch>
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })}
value={Settings.audio?.ignoreInputMedia ?? true}
note={<>Exclude nodes that are intended to capture audio.</>}
>
Ignore Inputs
</Switch>
<Switch
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, ignoreVirtual: v })}
value={Settings.audio?.ignoreVirtual ?? false}
note={
<>
Exclude virtual nodes, such as nodes belonging to loopbacks. This might be useful when using
"mix bussing".
</>
}
>
Ignore Virtual
</Switch>
<Switch
hideBorder
onChange={v =>
(Settings.audio = {
...Settings.audio,
ignoreDevices: v,
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="vcd-screen-picker-footer">
<Button color={Button.Colors.TRANSPARENT} onClick={close}>
Back
</Button>
</Modals.ModalFooter>
</Modals.ModalRoot>
);
}
function StreamSettings({ function StreamSettings({
source, source,
settings, settings,
@ -154,6 +315,8 @@ function StreamSettings({
setSettings: Dispatch<SetStateAction<StreamSettings>>; setSettings: Dispatch<SetStateAction<StreamSettings>>;
skipPicker: boolean; skipPicker: boolean;
}) { }) {
const Settings = useSettings();
const [thumb] = useAwaiter( const [thumb] = useAwaiter(
() => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)), () => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)),
{ {
@ -162,11 +325,27 @@ function StreamSettings({
} }
); );
const openSettings = () => {
const key = openModal(props => (
<AudioSettingsModal
modalProps={props}
close={() => props.onClose()}
setAudioSources={sources =>
setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources }))
}
/>
));
};
return ( return (
<div> <div>
<Forms.FormTitle>What you're streaming</Forms.FormTitle> <Forms.FormTitle>What you're streaming</Forms.FormTitle>
<Card className="vcd-screen-picker-card vcd-screen-picker-preview"> <Card className="vcd-screen-picker-card vcd-screen-picker-preview">
<img src={thumb} alt="" /> <img
src={thumb}
alt=""
className={isLinux ? "vcd-screen-picker-preview-img-linux" : "vcd-screen-picker-preview-img"}
/>
<Text variant="text-sm/normal">{source.name}</Text> <Text variant="text-sm/normal">{source.name}</Text>
</Card> </Card>
@ -210,7 +389,45 @@ function StreamSettings({
</div> </div>
</section> </section>
</div> </div>
<div className="vcd-screen-picker-quality">
<section>
<Forms.FormTitle>Content Type</Forms.FormTitle>
<div>
<div className="vcd-screen-picker-radios">
<label
className="vcd-screen-picker-radio"
data-checked={settings.contentHint === "motion"}
>
<Text variant="text-sm/bold">Prefer Smoothness</Text>
<input
type="radio"
name="contenthint"
value="motion"
checked={settings.contentHint === "motion"}
onChange={() => setSettings(s => ({ ...s, contentHint: "motion" }))}
/>
</label>
<label
className="vcd-screen-picker-radio"
data-checked={settings.contentHint === "detail"}
>
<Text variant="text-sm/bold">Prefer Clarity</Text>
<input
type="radio"
name="contenthint"
value="detail"
checked={settings.contentHint === "detail"}
onChange={() => setSettings(s => ({ ...s, contentHint: "detail" }))}
/>
</label>
</div>
<div className="vcd-screen-picker-hint-description">
<p>
Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange
for a much sharper and clearer image.
</p>
</div>
</div>
{isWindows && ( {isWindows && (
<Switch <Switch
value={settings.audio} value={settings.audio}
@ -221,11 +438,18 @@ function StreamSettings({
Stream With Audio Stream With Audio
</Switch> </Switch>
)} )}
</section>
</div>
{isLinux && ( {isLinux && (
<AudioSourcePickerLinux <AudioSourcePickerLinux
audioSource={settings.audioSource} openSettings={openSettings}
setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))} 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> </Card>
@ -233,47 +457,232 @@ function StreamSettings({
); );
} }
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({ function AudioSourcePickerLinux({
audioSource, includeSources,
setAudioSource excludeSources,
deviceSelect,
granularSelect,
openSettings,
setIncludeSources,
setExcludeSources
}: { }: {
audioSource?: string; includeSources?: AudioSources;
setAudioSource(s: string): void; excludeSources?: AudioSources;
deviceSelect?: boolean;
granularSelect?: boolean;
openSettings: () => void;
setIncludeSources: (s: AudioSources) => void;
setExcludeSources: (s: AudioSources) => void;
}) { }) {
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
fallbackValue: { ok: true, targets: [] } fallbackValue: { ok: true, targets: [], hasPipewirePulse: true }
}); });
const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null;
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false);
if (!sources.ok && sources.isGlibCxxOutdated) {
return ( return (
<section>
<Forms.FormTitle>Audio</Forms.FormTitle>
{loading && <Forms.FormTitle>Loading Audio sources...</Forms.FormTitle>}
{!sources.ok &&
(sources.isGlibcxxToOld ? (
<Forms.FormText> <Forms.FormText>
Failed to retrieve Audio Sources because your c++ library is too old to run venmic. If you would Failed to retrieve Audio Sources because your C++ library is too old to run
like to stream with Audio, see{" "} <a href="https://github.com/Vencord/venmic" target="_blank">
venmic
</a>
. See{" "}
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank"> <a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
this guide this guide
</a> </a>{" "}
for possible solutions.
</Forms.FormText> </Forms.FormText>
) : ( );
<Forms.FormText> }
Failed to retrieve Audio Sources. If you would like to stream with Audio, make sure you're using
Pipewire, not Pulseaudio
</Forms.FormText>
))}
{allSources && ( 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={includeSources === "Entire System" ? "vcd-screen-picker-quality" : undefined}>
<section>
<Forms.FormTitle>{loading ? "Loading Sources..." : "Audio Sources"}</Forms.FormTitle>
<Select <Select
options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))} options={allSources.map(({ name, value }) => ({
isSelected={s => s === audioSource} label: name,
select={setAudioSource} value: value,
default: name === "None"
}))}
isSelected={isItemSelected(includeSources)}
select={updateItems(setIncludeSources, includeSources)}
serialize={String} serialize={String}
popoutPosition="top"
closeOnSelect={false}
/> />
)}
</section> </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="vcd-screen-picker-settings-button"
>
Open Audio Settings
</Button>
</>
); );
} }
@ -292,18 +701,19 @@ function ModalComponent({
}) { }) {
const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0); const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0);
const [settings, setSettings] = useState<StreamSettings>({ const [settings, setSettings] = useState<StreamSettings>({
resolution: "1080", resolution: "720",
fps: "60", fps: "30",
audio: true contentHint: "motion",
audio: true,
includeSources: "None"
}); });
return ( return (
<Modals.ModalRoot {...modalProps}> <Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Modals.ModalHeader className="vcd-screen-picker-header"> <Modals.ModalHeader className="vcd-screen-picker-header">
<Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle> <Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle>
<Modals.ModalCloseButton onClick={close} /> <Modals.ModalCloseButton onClick={close} />
</Modals.ModalHeader> </Modals.ModalHeader>
<Modals.ModalContent className="vcd-screen-picker-modal"> <Modals.ModalContent className="vcd-screen-picker-modal">
{!selected ? ( {!selected ? (
<ScreenPicker screens={screens} chooseScreen={setSelected} /> <ScreenPicker screens={screens} chooseScreen={setSelected} />
@ -316,29 +726,24 @@ function ModalComponent({
/> />
)} )}
</Modals.ModalContent> </Modals.ModalContent>
<Modals.ModalFooter className="vcd-screen-picker-footer"> <Modals.ModalFooter className="vcd-screen-picker-footer">
<Button <Button
disabled={!selected} disabled={!selected}
onClick={() => { onClick={() => {
currentSettings = settings; currentSettings = settings;
try {
// If there are 2 connections, the second one is the existing stream. const frameRate = Number(settings.fps);
// In that case, we patch its quality
const conn = [...MediaEngineStore.getMediaEngine().connections][1];
if (conn && conn.videoStreamParameters.length > 0) {
const height = Number(settings.resolution); const height = Number(settings.resolution);
const width = Math.round(height * (16 / 9)); const width = Math.round(height * (16 / 9));
Object.assign(conn.videoStreamParameters[0], {
maxFrameRate: Number(settings.fps), const conn = [...MediaEngineStore.getMediaEngine().connections].find(
maxPixelCount: width * height, connection => connection.streamUserId === UserStore.getCurrentUser().id
maxBitrate: 8000000, );
maxResolution: {
type: "fixed", if (conn) {
width, conn.videoStreamParameters[0].maxFrameRate = frameRate;
height conn.videoStreamParameters[0].maxResolution.height = height;
} conn.videoStreamParameters[0].maxResolution.width = width;
});
} }
submit({ submit({
@ -346,6 +751,38 @@ function ModalComponent({
...settings ...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(); close();
}} }}
> >

View file

@ -1,201 +0,0 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import "./settings.css";
import { Margins } from "@vencord/types/utils";
import { Button, Forms, Select, Switch, Text, Toasts, useState } from "@vencord/types/webpack/common";
import { setBadge } from "renderer/appBadge";
import { useSettings } from "renderer/settings";
import { isMac, isWindows } 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)?]> = [
isWindows && [
"discordWindowsTitleBar",
"Discord Titlebar",
"Use Discord's custom title bar instead of the Windows one. Requires a full restart."
],
!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"
],
["staticTitle", "Static Title", 'Makes the window title "Vesktop" instead of changing to the current page'],
["enableMenu", "Enable Menu Bar", "Enables the application menu bar. Press ALT to toggle visibility."],
["disableSmoothScroll", "Disable smooth scrolling", "Disables smooth scrolling in Vesktop", false],
["splashTheming", "Splash theming", "Adapt the splash window colors to your custom theme", false],
[
"openLinksWithElectron",
"Open Links in app (experimental)",
"Opens links in a new Vesktop window instead of your web browser"
],
["checkUpdates", "Check for updates", "Automatically check for Vesktop updates", true],
["startMinimized", "Start minimized", "Vesktop remains in minimized mode on start", false]
];
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":
return;
case "invalid":
Toasts.show({
message:
"You did not choose a valid Vencord install. Make sure you're selecting the dist dir!",
id: Toasts.genId(),
type: Toasts.Type.FAILURE
});
return;
}
Settings.vencordDir = choice;
}}
>
Change
</Button>
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
onClick={() => (Settings.vencordDir = void 0)}
>
Reset
</Button>
</div>
</Forms.FormSection>
);
}

View file

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

View file

@ -11,6 +11,10 @@
gap: 1em; gap: 1em;
} }
.vcd-screen-picker-card {
flex-grow: 1;
}
.vcd-screen-picker-grid { .vcd-screen-picker-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@ -23,21 +27,22 @@
} }
.vcd-screen-picker-selected img { .vcd-screen-picker-selected img {
border: 2px solid var(--brand-experiment); border: 2px solid var(--brand-500);
border-radius: 3px; border-radius: 3px;
} }
.vcd-screen-picker-grid label { .vcd-screen-picker-grid label {
overflow: hidden; overflow: hidden;
padding: 4px 0px; padding: 8px;
cursor: pointer; cursor: pointer;
display: grid;
justify-items: center;
} }
.vcd-screen-picker-grid label:hover { .vcd-screen-picker-grid label:hover {
outline: 2px solid var(--brand-experiment); outline: 2px solid var(--brand-500);
} }
.vcd-screen-picker-grid div { .vcd-screen-picker-grid div {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -52,8 +57,13 @@
box-sizing: border-box; box-sizing: border-box;
} }
.vcd-screen-picker-preview img { .vcd-screen-picker-preview-img-linux {
width: 100%; width: 60%;
margin-bottom: 0.5em;
}
.vcd-screen-picker-preview-img {
width: 90%;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@ -81,8 +91,8 @@
} }
.vcd-screen-picker-radio[data-checked="true"] { .vcd-screen-picker-radio[data-checked="true"] {
background-color: var(--brand-experiment); background-color: var(--brand-500);
border-color: var(--brand-experiment); border-color: var(--brand-500);
} }
.vcd-screen-picker-radio[data-checked="true"] h2 { .vcd-screen-picker-radio[data-checked="true"] h2 {
@ -92,7 +102,6 @@
.vcd-screen-picker-quality { .vcd-screen-picker-quality {
display: flex; display: flex;
gap: 1em; gap: 1em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@ -100,6 +109,11 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.vcd-screen-picker-settings-button {
margin-left: auto;
margin-top: 0.3rem;
}
.vcd-screen-picker-radios { .vcd-screen-picker-radios {
display: flex; display: flex;
width: 100%; width: 100%;
@ -122,3 +136,10 @@
.vcd-screen-picker-audio { .vcd-screen-picker-audio {
margin-bottom: 0; margin-bottom: 0;
} }
.vcd-screen-picker-hint-description {
color: var(--header-secondary);
font-size: 14px;
line-height: 20px;
font-weight: 400;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,76 @@
/*
* 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 { useForceUpdater } from "@vencord/types/utils";
import { Button, Forms, Toasts } from "@vencord/types/webpack/common";
import { SettingsComponent } from "./Settings";
export 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-location-btns">
<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>
</>
);
};

View file

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

View file

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

16
src/renderer/fixes.css Normal file
View file

@ -0,0 +1,16 @@
/* Download Desktop button in guilds list */
[class^=listItem_]:has([data-list-item-id=guildsnav___app-download-button]),
[class^=listItem_]:has(+ [class^=listItem_] [data-list-item-id=guildsnav___app-download-button]) {
display: none;
}
/* FIXME: remove this once Discord fixes their css to not explode scrollbars on chromium >=121 */
* {
scrollbar-width: unset !important;
scrollbar-color: unset !important;
}
/* Workaround for making things in the draggable area clickable again on macOS */
.platform-osx [class*=topic_], .platform-osx [class*=notice_] button {
-webkit-app-region: no-drag;
}

View file

@ -4,11 +4,9 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import "./hideGarbage.css"; import "./fixes.css";
import { waitFor } from "@vencord/types/webpack"; import { isWindows, localStorage } from "./utils";
import { isFirstRun, isWindows, localStorage } from "./utils";
// Make clicking Notifications focus the window // Make clicking Notifications focus the window
const originalSetOnClick = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick")!.set!; const originalSetOnClick = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick")!.set!;
@ -22,15 +20,8 @@ Object.defineProperty(Notification.prototype, "onclick", {
configurable: true configurable: true
}); });
if (isFirstRun) { // Hide "Download Discord Desktop now!!!!" banner
// Hide "Download Discord Desktop now!!!!" banner localStorage.setItem("hideNag", "true");
localStorage.setItem("hideNag", "true");
// Enable Desktop Notifications by default
waitFor("setDesktopType", m => {
m.setDesktopType("all");
});
}
// FIXME: Remove eventually. // FIXME: Remove eventually.
// Originally, Vencord always used a Windows user agent. This seems to cause captchas // Originally, Vencord always used a Windows user agent. This seems to cause captchas

View file

@ -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;
}

View file

@ -12,10 +12,10 @@ import "./themedSplash";
console.log("read if cute :3"); console.log("read if cute :3");
export * as Components from "./components"; export * as Components from "./components";
import { findByPropsLazy } from "@vencord/types/webpack"; import { findByPropsLazy, onceReady } from "@vencord/types/webpack";
import { FluxDispatcher } from "@vencord/types/webpack/common"; import { Alerts, FluxDispatcher } from "@vencord/types/webpack/common";
import SettingsUi from "./components/Settings"; import SettingsUi from "./components/settings/Settings";
import { Settings } from "./settings"; import { Settings } from "./settings";
export { Settings }; export { Settings };
@ -52,8 +52,26 @@ const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as any as {
handleEvent(e: MessageEvent): void; handleEvent(e: MessageEvent): void;
}; };
VesktopNative.arrpc.onActivity(data => { VesktopNative.arrpc.onActivity(async data => {
if (!Settings.store.arRPC) return; if (!Settings.store.arRPC) return;
await onceReady;
arRPC.handleEvent(new MessageEvent("message", { data })); arRPC.handleEvent(new MessageEvent("message", { data }));
}); });
// TODO: remove soon
const vencordDir = "vencordDir" as keyof typeof Settings.store;
if (Settings.store[vencordDir]) {
onceReady.then(() =>
setTimeout(
() =>
Alerts.show({
title: "Custom Vencord Location",
body: "Due to security hardening changes in Vesktop, your custom Vencord location had to be reset. Please configure it again in the settings.",
onConfirm: () => delete Settings.store[vencordDir]
}),
5000
)
);
}

View file

@ -0,0 +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
*/
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"
}
}
]
});

View file

@ -0,0 +1,24 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { addPatch } from "./shared";
addPatch({
patches: [
{
find: "lastOutputSystemDevice.justChanged",
replacement: {
// eslint-disable-next-line no-useless-escape
match: /(\i)\.\i\.getState\(\).neverShowModal/,
replace: "$& || $self.shouldIgnore($1)"
}
}
],
shouldIgnore(state: any) {
return Object.keys(state?.default?.lastDeviceConnected ?? {})?.[0] === "vencord-screen-share";
}
});

View file

@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { addPatch } from "./shared";
addPatch({
patches: [
{
find: 'setSinkId"in',
replacement: {
// eslint-disable-next-line no-useless-escape
match: /return (\i)\?navigator\.mediaDevices\.enumerateDevices/,
replace: "return $1 ? $self.filteredDevices"
}
}
],
async filteredDevices() {
const original = await navigator.mediaDevices.enumerateDevices();
return original.filter(x => x.label !== "vencord-screen-share");
}
});

View file

@ -5,7 +5,10 @@
*/ */
// TODO: Possibly auto generate glob if we have more patches in the future // TODO: Possibly auto generate glob if we have more patches in the future
import "./spellCheck"; import "./enableNotificationsByDefault";
import "./platformClass"; import "./platformClass";
import "./hideSwitchDevice";
import "./hideVenmicInput";
import "./screenShareFixes";
import "./spellCheck";
import "./windowsTitleBar"; import "./windowsTitleBar";
import "./screenShareAudio";

View file

@ -5,7 +5,7 @@
*/ */
import { Settings } from "renderer/settings"; import { Settings } from "renderer/settings";
import { isMac, isWindows } from "renderer/utils"; import { isMac } from "renderer/utils";
import { addPatch } from "./shared"; import { addPatch } from "./shared";
@ -22,8 +22,8 @@ addPatch({
], ],
getPlatformClass() { getPlatformClass() {
if (Settings.store.customTitleBar) return "platform-win";
if (isMac) return "platform-osx"; if (isMac) return "platform-osx";
if (isWindows && Settings.store.discordWindowsTitleBar) return "platform-win";
return "platform-web"; return "platform-web";
} }
}); });

View file

@ -4,8 +4,12 @@
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
*/ */
import { Logger } from "@vencord/types/utils";
import { currentSettings } from "renderer/components/ScreenSharePicker";
import { isLinux } from "renderer/utils"; import { isLinux } from "renderer/utils";
const logger = new Logger("VesktopStreamFixes");
if (isLinux) { if (isLinux) {
const original = navigator.mediaDevices.getDisplayMedia; const original = navigator.mediaDevices.getDisplayMedia;
@ -23,6 +27,29 @@ if (isLinux) {
const stream = await original.call(this, opts); const stream = await original.call(this, opts);
const id = await getVirtmic(); const id = await getVirtmic();
const frameRate = Number(currentSettings?.fps);
const height = Number(currentSettings?.resolution);
const width = Math.round(height * (16 / 9));
const track = stream.getVideoTracks()[0];
track.contentHint = String(currentSettings?.contentHint);
const constraints = {
...track.getConstraints(),
frameRate: { 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) { if (id) {
const audio = await navigator.mediaDevices.getUserMedia({ const audio = await navigator.mediaDevices.getUserMedia({
audio: { audio: {

View file

@ -6,7 +6,8 @@
import { addContextMenuPatch } from "@vencord/types/api/ContextMenu"; import { addContextMenuPatch } from "@vencord/types/api/ContextMenu";
import { findStoreLazy } from "@vencord/types/webpack"; import { findStoreLazy } from "@vencord/types/webpack";
import { 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"; import { addPatch } from "./shared";
@ -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); const hasCorrections = Boolean(word && corrections?.length);
children.push( const availableLanguages = useMemo(VesktopNative.spellcheck.getAvailableLanguages, []);
const settings = useSettings();
const spellCheckLanguages = (settings.spellCheckLanguages ??= [...new Set(navigator.languages)]);
const pasteSectionIndex = children.findIndex(c => c?.props?.children?.some(c => c?.props?.id === "paste"));
children.splice(
pasteSectionIndex === -1 ? children.length : pasteSectionIndex,
0,
<Menu.MenuGroup> <Menu.MenuGroup>
{hasCorrections && ( {hasCorrections && (
<> <>
@ -68,16 +79,39 @@ addContextMenuPatch("textarea-context", children => () => {
/> />
</> </>
)} )}
<Menu.MenuItem id="vcd-spellcheck-settings" label="Spellcheck Settings">
<Menu.MenuCheckboxItem <Menu.MenuCheckboxItem
id="vcd-spellcheck-enabled" id="vcd-spellcheck-enabled"
label="Enable Spellcheck" label="Enable Spellcheck"
checked={SpellCheckStore.isEnabled()} checked={spellCheckEnabled}
action={() => { action={() => {
FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" }); FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" });
// Haven't found a good way to update state, so just close for now 🤷‍♀️
ContextMenu.close();
}} }}
/> />
<Menu.MenuItem id="vcd-spellcheck-languages" label="Languages" disabled={!spellCheckEnabled}>
{availableLanguages.map(lang => {
const isEnabled = spellCheckLanguages.includes(lang);
return (
<Menu.MenuCheckboxItem
id={"vcd-spellcheck-lang-" + lang}
label={lang}
checked={isEnabled}
disabled={!isEnabled && spellCheckLanguages.length >= 5}
action={() => {
const newSpellCheckLanguages = spellCheckLanguages.filter(l => l !== lang);
if (newSpellCheckLanguages.length === spellCheckLanguages.length) {
newSpellCheckLanguages.push(lang);
}
settings.spellCheckLanguages = newSpellCheckLanguages;
}}
/>
);
})}
</Menu.MenuItem>
</Menu.MenuItem>
</Menu.MenuGroup> </Menu.MenuGroup>
); );
}); });

View file

@ -8,7 +8,7 @@ import { Settings } from "renderer/settings";
import { addPatch } from "./shared"; import { addPatch } from "./shared";
if (Settings.store.discordWindowsTitleBar) if (Settings.store.customTitleBar)
addPatch({ addPatch({
patches: [ patches: [
{ {

View file

@ -23,13 +23,14 @@ export const enum IpcEvents {
GET_SETTINGS = "VCD_GET_SETTINGS", GET_SETTINGS = "VCD_GET_SETTINGS",
SET_SETTINGS = "VCD_SET_SETTINGS", SET_SETTINGS = "VCD_SET_SETTINGS",
GET_VENCORD_DIR = "VCD_GET_VENCORD_DIR",
SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR", SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR",
UPDATER_GET_DATA = "VCD_UPDATER_GET_DATA", UPDATER_GET_DATA = "VCD_UPDATER_GET_DATA",
UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD", UPDATER_DOWNLOAD = "VCD_UPDATER_DOWNLOAD",
UPDATE_IGNORE = "VCD_UPDATE_IGNORE", UPDATE_IGNORE = "VCD_UPDATE_IGNORE",
SPELLCHECK_SET_LANGUAGES = "VCD_SPELLCHECK_SET_LANGUAGES", SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES",
SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT", SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT",
SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING", SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING",
SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY", SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY",
@ -47,5 +48,7 @@ export const enum IpcEvents {
VIRT_MIC_START_SYSTEM = "VCD_VIRT_MIC_START_ALL", VIRT_MIC_START_SYSTEM = "VCD_VIRT_MIC_START_ALL",
VIRT_MIC_STOP = "VCD_VIRT_MIC_STOP", VIRT_MIC_STOP = "VCD_VIRT_MIC_STOP",
ARRPC_ACTIVITY = "VCD_ARRPC_ACTIVITY" ARRPC_ACTIVITY = "VCD_ARRPC_ACTIVITY",
CLIPBOARD_COPY_IMAGE = "VCD_CLIPBOARD_COPY_IMAGE"
} }

View file

@ -8,7 +8,6 @@ import type { Rectangle } from "electron";
export interface Settings { export interface Settings {
discordBranch?: "stable" | "canary" | "ptb"; discordBranch?: "stable" | "canary" | "ptb";
vencordDir?: string;
transparencyOption?: "none" | "mica" | "tabbed" | "acrylic"; transparencyOption?: "none" | "mica" | "tabbed" | "acrylic";
tray?: boolean; tray?: boolean;
minimizeToTray?: boolean; minimizeToTray?: boolean;
@ -16,23 +15,43 @@ export interface Settings {
staticTitle?: boolean; staticTitle?: boolean;
enableMenu?: boolean; enableMenu?: boolean;
disableSmoothScroll?: boolean; disableSmoothScroll?: boolean;
hardwareAcceleration?: boolean;
arRPC?: boolean; arRPC?: boolean;
appBadge?: boolean; appBadge?: boolean;
discordWindowsTitleBar?: boolean;
startMinimized?: boolean;
maximized?: boolean;
minimized?: boolean;
windowBounds?: Rectangle;
disableMinSize?: boolean; disableMinSize?: boolean;
clickTrayToShowHide?: boolean;
checkUpdates?: boolean; customTitleBar?: boolean;
skippedUpdate?: string;
firstLaunch?: boolean;
splashTheming?: boolean; splashTheming?: boolean;
splashColor?: string; splashColor?: string;
splashBackground?: string; splashBackground?: string;
steamOSLayoutVersion?: number; spellCheckLanguages?: string[];
audio?: {
workaround?: boolean;
deviceSelect?: boolean;
granularSelect?: boolean;
ignoreVirtual?: boolean;
ignoreDevices?: boolean;
ignoreInputMedia?: boolean;
onlySpeakers?: boolean;
onlyDefaultSpeakers?: boolean;
};
}
export interface State {
maximized?: boolean;
minimized?: boolean;
windowBounds?: Rectangle;
displayid: int;
firstLaunch?: boolean;
steamOSLayoutVersion?: number;
vencordDir?: string;
} }

View file

@ -59,6 +59,19 @@ export class SettingsStore<T extends object> {
self.pathListeners.get(setPath)?.forEach(cb => cb(value)); self.pathListeners.get(setPath)?.forEach(cb => cb(value));
return true; return true;
},
deleteProperty(target, key: string) {
if (!(key in target)) return true;
const res = Reflect.deleteProperty(target, key);
if (!res) return false;
const setPath = `${path}${path && "."}${key}`;
self.globalListeners.forEach(cb => cb(root, setPath));
self.pathListeners.get(setPath)?.forEach(cb => cb(undefined));
return res;
} }
}); });
} }
@ -144,4 +157,11 @@ export class SettingsStore<T extends object> {
listeners.delete(cb); listeners.delete(cb);
if (!listeners.size) this.pathListeners.delete(path as string); if (!listeners.size) this.pathListeners.delete(path as string);
} }
/**
* Call all global change listeners
*/
public markAsChanged() {
this.globalListeners.forEach(cb => cb(this.plain, ""));
}
} }

View file

@ -1,119 +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 { app, BrowserWindow, shell } from "electron";
import { Settings } from "main/settings";
import { handle } from "main/utils/ipcWrappers";
import { makeLinksOpenExternally } from "main/utils/makeLinksOpenExternally";
import { githubGet, ReleaseData } from "main/utils/vencordLoader";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { ICON_PATH, VIEW_DIR } from "shared/paths";
export interface UpdateData {
currentVersion: string;
latestVersion: string;
release: ReleaseData;
}
let updateData: UpdateData;
handle(IpcEvents.UPDATER_GET_DATA, () => updateData);
handle(IpcEvents.UPDATER_DOWNLOAD, () => {
const portable = !!process.env.PORTABLE_EXECUTABLE_FILE;
const { assets } = updateData.release;
const url = (() => {
switch (process.platform) {
case "win32":
return assets.find(a => {
if (!a.name.endsWith(".exe")) return false;
const isSetup = a.name.includes("Setup");
return portable ? !isSetup : isSetup;
})!.browser_download_url;
case "darwin":
return assets.find(a =>
process.arch === "arm64"
? a.name.endsWith("-arm64-mac.zip")
: a.name.endsWith("-mac.zip") && !a.name.includes("arm64")
)!.browser_download_url;
case "linux":
return updateData.release.html_url;
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
})();
shell.openExternal(url);
});
handle(IpcEvents.UPDATE_IGNORE, () => {
Settings.store.skippedUpdate = updateData.latestVersion;
});
function isOutdated(oldVersion: string, newVersion: string) {
const oldParts = oldVersion.split(".");
const newParts = newVersion.split(".");
if (oldParts.length !== newParts.length)
throw new Error(`Incompatible version strings (old: ${oldVersion}, new: ${newVersion})`);
for (let i = 0; i < oldParts.length; i++) {
const oldPart = Number(oldParts[i]);
const newPart = Number(newParts[i]);
if (isNaN(oldPart) || isNaN(newPart))
throw new Error(`Invalid version string (old: ${oldVersion}, new: ${newVersion})`);
if (oldPart < newPart) return true;
if (oldPart > newPart) return false;
}
return false;
}
export async function checkUpdates() {
if (Settings.store.checkUpdates === false) return;
try {
const raw = await githubGet("/repos/Vencord/Vesktop/releases/latest");
const data = JSON.parse(raw.toString("utf-8")) as ReleaseData;
const oldVersion = app.getVersion();
const newVersion = data.tag_name.replace(/^v/, "");
updateData = {
currentVersion: oldVersion,
latestVersion: newVersion,
release: data
};
if (Settings.store.skippedUpdate !== newVersion && isOutdated(oldVersion, newVersion)) {
openNewUpdateWindow();
}
} catch (e) {
console.error("AppUpdater: Failed to check for updates\n", e);
}
}
function openNewUpdateWindow() {
const win = new BrowserWindow({
width: 500,
autoHideMenuBar: true,
alwaysOnTop: true,
webPreferences: {
preload: join(__dirname, "updaterPreload.js"),
nodeIntegration: false,
contextIsolation: true,
sandbox: true
},
icon: ICON_PATH
});
makeLinksOpenExternally(win);
win.loadFile(join(VIEW_DIR, "updater.html"));
}

View file

@ -1,21 +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 { contextBridge } from "electron";
import { invoke } from "preload/typedIpc";
import { IpcEvents } from "shared/IpcEvents";
import type { UpdateData } from "./main";
contextBridge.exposeInMainWorld("Updater", {
getData: () => invoke<UpdateData>(IpcEvents.UPDATER_GET_DATA),
download: () => {
invoke<void>(IpcEvents.UPDATER_DOWNLOAD);
invoke<void>(IpcEvents.CLOSE);
},
ignore: () => invoke<void>(IpcEvents.UPDATE_IGNORE),
close: () => invoke<void>(IpcEvents.CLOSE)
});

View file

@ -2,8 +2,9 @@
<link rel="stylesheet" href="./style.css" type="text/css" /> <link rel="stylesheet" href="./style.css" type="text/css" />
<style> <style>
* { body {
user-select: none; user-select: none;
-webkit-app-region: drag;
} }
.wrapper { .wrapper {