274 Commits

Author SHA1 Message Date
Vendicated
e9ff3cbb3b add electron fuses config for better security 2025-12-14 17:48:58 +01:00
DemonicSavage
8e91df0376 fix camera resolution being stuck to 640x480 (#1199)
Co-authored-by: V <vendicated@riseup.net>
2025-12-12 22:29:44 +01:00
futamebore
da8e8f52ab feat: add taskbar flashing on new notifications (#1209)
Co-authored-by: V <vendicated@riseup.net>
2025-12-12 22:23:13 +01:00
Vendicated
98b0ba85a3 fix macOS vibrancy support not working 2025-12-12 21:33:47 +01:00
Vendicated
d83aa6675e handle potential filesystem errors when saving settings 2025-12-12 21:11:32 +01:00
Vendicated
b3d595ef98 bump dependencies 2025-12-12 21:01:26 +01:00
Vendicated
24047d7fdd fix splash themeing 2025-12-12 20:56:56 +01:00
Vendicated
5c992d66a6 fix ScreensharePicker ui & modernise all UI 2025-12-12 18:43:30 +01:00
Vendicated
02ab7165aa use \i instead of .{1,3} 2025-11-23 23:57:14 +01:00
Vendicated
0483a1abdc improve eslint config 2025-11-23 23:55:37 +01:00
Vendicated
34c92cfb59 fix screenshare picker ui 2025-11-18 17:24:35 +01:00
Vendicated
28aeab979b Upgrade to electron 39.2.1
Fixes https://github.com/Vencord/Vesktop/issues/1204
2025-11-17 22:24:39 +01:00
Vendicated
8ca3e4f3a1 fix potential edge case in cli argument parsing 2025-11-15 11:33:21 +01:00
Vendicated
ae47c204fa fix --start-minimized not working when splash is disabled 2025-11-15 11:29:44 +01:00
Vendicated
f57245f297 fix views using quirks mode & non utf-8 2025-11-03 20:41:33 +01:00
Vendicated
9193ed58c9 resize splash image to 128x128 2025-10-28 17:32:54 +01:00
khcrysalis
38b7716b2f add new splash animation (#1195) 2025-10-28 16:26:27 +00:00
Vendicated
4b735b9739 bump to v1.6.1 2025-10-28 17:16:27 +01:00
Vendicated
707dbf4f75 upgrade electron to v39 2025-10-28 16:25:38 +01:00
Vendicated
a242d5d694 bound check entire window area instead of only top left corner
Co-Authored-By: MBStarup <mavi.nexu@gmail.com>
2025-10-23 21:17:17 +02:00
Vendicated
b75ed0766d clean up BrowserWindow options logic 2025-10-23 18:56:33 +02:00
Vendicated
03aa30dfc6 do window bound checks without relying on displayId
Fixes Vesktop not remembering its position correctly.
Seems like the display ids aren't consistent on some windows systems...
2025-10-23 18:17:04 +02:00
Vendicated
cd1a40e6c7 oh god no 2025-10-23 02:55:22 +02:00
Vendicated
3aa0bb806e fix some ENOENT errors on first launch 2025-10-23 02:53:13 +02:00
Vendicated
800a97105c fix metainfo generation 2025-10-22 16:30:20 +02:00
Vendicated
b02acd6a7b fix updater printing on --help / --version commands 2025-10-21 21:16:24 +02:00
Vendicated
d293b166fe fix cli argument parsing 2025-10-21 19:56:22 +02:00
Vendicated
28a13be709 add nearest neighbor option to splash customisation 2025-10-20 01:56:04 +02:00
Vendicated
02907d3248 bump to v1.6.0 2025-10-19 20:07:32 +02:00
Vendicated
c82cc7a963 fix metainfo generation & uploading 2025-10-19 19:59:15 +02:00
Vendicated
6a43e135d0 set CSP in updater view 2025-10-19 03:34:50 +02:00
Vendicated
d232797889 clean up vesktop:// protocol handler 2025-10-19 03:14:33 +02:00
Vendicated
fa23c630cb use custom protocol instead of file:// for better security 2025-10-19 02:59:00 +02:00
V
9f0af48355 Updater: show update dialog instead of forcing updates (#1184) 2025-10-19 00:58:48 +00:00
Vendicated
eb3dae897d --help: document chromium & electron flags 2025-10-18 18:25:25 +02:00
Vendicated
d005dd5ebd make cli parser less strict 2025-10-18 18:02:50 +02:00
Vendicated
6aeacaaf21 add cli flags to spoof user agent 2025-10-18 17:54:05 +02:00
Vendicated
40d9cba2f0 error proof settings ui 2025-10-16 13:57:53 +02:00
V
5734a1d33c libvesktop: native dbus module for autostart & app badge (#1180)
Both setAppBadge and autoStart at system boot now use dbus calls. This means that autoStart will work in Flatpak
2025-10-16 10:52:52 +02:00
Vendicated
8cc34e217c bump dependencies, migrate to new Vencord Switch component 2025-10-07 00:37:45 +02:00
V
a55b1f0250 Add splash/tray image customisation & change default splash (#1179) 2025-10-05 18:14:13 +00:00
Vendicated
e79635f15e fix: explicitly set executableName
fixes an upcoming regression caused by https://github.com/electron-userland/electron-builder/pull/9068
2025-10-04 00:38:17 +02:00
Cookie
6e2da1d294 feat: New Vesktop icon (#865)
changes the app icon and tray

Co-authored-by: Wing <44992537+wingio@users.noreply.github.com>
Co-authored-by: khcrysalis <sam4r16@gmail.com>
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-10-02 22:02:43 +02:00
Vendicated
0d9ca2270c fix Vesktop being registered as html mime handler
This is actually a bug in xdg-settings that was already fixed years ago, but Ubuntu
still ships 7 (!!!!!) years out of date xdg-utils. Please just switch to Fedora what
are we doing man
2025-09-18 03:37:27 +02:00
Tiagoquix
497c251d72 Add icon to auto-start script (#1172) 2025-09-05 00:03:12 +00:00
Vendicated
b221882c5b upgrade dependencies 2025-09-04 19:45:04 +02:00
Vendicated
0ee194698d upgrade to electron 38 2025-09-04 19:43:27 +02:00
V
27293d4ae9 Delete meta/dev.vencord.Vesktop.metainfo.xml
this file has been moved to releases: https://github.com/Vencord/Vesktop/releases/latest/download/dev.vencord.Vesktop.metainfo.xml
2025-07-11 02:48:21 +02:00
V
432e54ace5 Update README.md 2025-07-08 17:11:21 +02:00
Vendicated
4a2d12f273 fix lockfile 2025-07-08 01:08:39 +02:00
Vendicated
3498e39cbb bump to v1.5.8 2025-07-08 01:02:17 +02:00
Vendicated
3278b16923 Remove GTK mixed symbols workaround
This bug is fixed in electron 37, so the workaround is no longer needed
2025-07-06 19:16:07 +02:00
Vendicated
fde447bc1d improve about window 2025-07-06 18:57:57 +02:00
Vendicated
4baf6de472 arrpc: fix link handling 2025-07-06 17:54:58 +02:00
Justin
8aa91b4f01 Move arrpc server into a worker thread to reduce stutters (#1053)
Co-authored-by: V <vendicated@riseup.net>
2025-07-06 17:34:32 +02:00
Vendicated
099845deb7 bump electron again surely this time they fixed every bug 2025-07-06 16:27:04 +02:00
Vendicated
3982e122a7 upgrade to electron 37 2025-06-27 23:08:50 +02:00
Cookie
8d3c9390ae update discord css variable name (#1164) 2025-06-27 03:09:33 +02:00
Vendicated
236fc806de fake markdown wtf 2025-06-26 15:09:35 +02:00
Vendicated
2dcebeca79 revise issue template once again 2025-06-26 15:08:28 +02:00
Vendicated
e6589eacfc fix lockfile 2025-06-21 04:12:21 +02:00
Vendicated
29d1c73d81 okay pnpm 2025-06-21 04:10:45 +02:00
Vendicated
26906b9776 patch electron-updater to fix rpm updater 2025-06-21 04:06:13 +02:00
Vendicated
36c67aa54a bump dependencies 2025-06-21 04:06:00 +02:00
Cookie
3cd4e94762 fix spellcheck (#1163) 2025-06-20 19:29:13 +02:00
Vendicated
6950e0b03a add comment explaining the patch 2025-06-10 17:22:30 +02:00
Vendicated
f98309c7b7 fix random logouts (Discord shitcode moment) 2025-06-10 17:16:26 +02:00
Vendicated
a2dade9140 i am very stupid 2025-06-09 01:36:45 +02:00
Vendicated
83ad4970e5 fix Video HWA wrongly being disabled if HWA setting was never changed 2025-06-09 01:36:15 +02:00
Vending Machine
f123f5fc3c Update dev.vencord.Vesktop.metainfo.xml 2025-06-08 22:07:07 +02:00
Vending Machine
0a856b26da Update meta.yml 2025-06-08 22:04:59 +02:00
Vendicated
dad306a4b5 bump to v1.5.7 2025-06-08 21:47:06 +02:00
Vendicated
a52ee1dbb6 bump dependencies 2025-06-08 21:45:17 +02:00
Vendicated
524f8ff277 ipcCommands: check if mainWin is alive 2025-06-08 21:39:56 +02:00
Vendicated
83104e3625 fix discord titlebar buttons missing 2025-06-08 21:34:31 +02:00
Vendicated
fcb61c8f42 improve handling of no hardware acceleration 2025-06-08 21:21:57 +02:00
Vendicated
48e9aea47e add hardware video acceleration switch & improve settings ui 2025-06-08 20:56:28 +02:00
Vendicated
6e7d912b95 make linux screenshare audio stereo 2025-05-20 01:45:24 +02:00
Vendicated
475012cbed bump electron patch 2025-05-16 22:30:11 +02:00
Vendicated
382dac96f7 fix return value 2025-05-16 21:01:19 +02:00
Vendicated
3295a7d344 migrate executeJavaScript calls to new command system 2025-05-16 21:00:32 +02:00
Vendicated
bb3cec0d13 Fix Invalid URL error 2025-05-16 20:48:34 +02:00
Vendicated
1a673f2318 build: automatically glob all patch files 2025-05-16 20:43:27 +02:00
Vendicated
d9a7e81f71 switch to node inbuilt source map support 2025-05-16 19:59:29 +02:00
Vendicated
73848311b1 oh cookie what are u doing 2025-05-16 19:52:16 +02:00
Vendicated
857779e77b remove legacy code 2025-05-16 19:47:10 +02:00
William
fa627b384f add StartupWMClass - fixes AppImage icon & pinning (#1158)
Co-authored-by: Vending Machine <vendicated@riseup.net>
2025-05-16 19:38:02 +02:00
Cookie
43a8781492 Link Discord connections through the browser instead of a popup (#1159)
Co-authored-by: Vending Machine <vendicated@riseup.net>
2025-05-16 19:34:48 +02:00
Vendicated
b17fef93ba make Discord's auto gain control toggle actually work 2025-05-16 03:37:32 +02:00
Vendicated
fc16fc5404 Fix not launching on GNOME
Reference: https://github.com/electron/electron/issues/46538
Also improves the chromium switch code
2025-05-14 05:26:29 +02:00
Vendicated
e760e58ed7 upgrade to electron 36 2025-05-14 05:26:29 +02:00
Cookie
3936a0a41e fix: handle loadURL() failures correctly (#1093) 2025-04-22 22:03:17 +02:00
Glitchtest
c7d830c57c Revert loopbackWithMute to loopback (#1155)
apparently loopbackWithMute does not mute your own app like initially assumed but rather mutes the system audio for the user
2025-04-17 23:23:42 +02:00
Vendicated
7bf05bd907 add back window menu (minimise, close, etc) on mac 2025-04-17 14:39:19 +02:00
Vending Machine
ae20445301 manually update metainfo 2025-04-13 14:49:49 +02:00
Vending Machine
33d1ac43e3 fix meta.yml 2025-04-13 14:47:32 +02:00
Vending Machine
da51ebb0a7 fix(ci): strip v prefix from version in vencord.dev updater 2025-04-13 14:44:27 +02:00
Vendicated
d0399cbde4 bump to v1.5.6 2025-04-13 14:31:02 +02:00
Vendicated
49784bc1aa fix window button patch 2025-04-13 14:29:13 +02:00
Vendicated
43f59cefda use Vencord's addPatch instead of manually adding patches 2025-04-13 14:20:28 +02:00
Sqaaakoi
c42c1b7bbd Fix Visual Refresh titlebar (#1104) 2025-04-04 15:44:47 +02:00
Ryan Cao
765ffc0b57 MacOs: fix DevTools shortcut (#1141) 2025-04-04 15:39:55 +02:00
khcrysalis
3c87c89b3a README: add homebrew as installation method (#1144) 2025-04-02 06:11:52 +02:00
Vendicated
391ad94b85 bump dependencies 2025-04-01 22:02:42 +02:00
rushii
8f94196646 fix: disable Browser Tab shortcuts like Ctrl+W (#1130)
Fixes #934
---------

Co-authored-by: Vending Machine <vendicated@riseup.net>
2025-03-27 01:01:50 +01:00
Vending Machine
878184cab1 improve build instructions 2025-03-27 00:20:29 +01:00
Vending Machine
156ba6ab7b ScreenShare(Windows): loopback -> loopbackWithMute 2025-03-23 22:43:34 +01:00
v
fd91a23188 Update and rename blank.yml to dev-issue.yml 2025-03-06 01:12:17 +01:00
Vendicated
9ca9e78da7 no more issues 2025-03-06 00:14:35 +01:00
Vendicated
a1d5e4dcdc bump dependencies 2025-02-18 15:28:41 +01:00
Vendicated
da5d0f8f19 update blank issue template 2025-02-18 14:18:50 +01:00
Vendicated
d7bc56660b includes is slightly better than every here 2025-02-12 13:18:20 +01:00
Vendicated
d39c54b3ee fix edge case when ~/package.json exists with type: module 2025-02-12 13:16:53 +01:00
v
670c62267e Add workflow to automatically update version in vencord.dev repo 2025-02-12 12:57:15 +01:00
v
8938fe27b2 metaInfo workflow: add to release and push instead of PR 2025-02-08 06:20:29 +01:00
Vendicated
e6c1a03c59 Revert "use Vencord's addPatch instead of manually adding patches"
This was pushed prematurely since it depends on a change to Vencord that hasn't been released yet

This reverts commit 68930a1f50.
2025-02-08 03:53:33 +01:00
Vendicated
68930a1f50 use Vencord's addPatch instead of manually adding patches 2025-02-07 23:13:28 +01:00
Vendicated
3b76c30db2 fix automatic license header inserter still using 2023 (lmao) 2025-02-07 04:16:15 +01:00
Vendicated
523f657b3b hide app download button in server list 2025-02-07 03:54:52 +01:00
github-actions[bot]
d75ab4af1c update Metainfo for v1.5.5 (#1079)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-06 05:51:52 +01:00
Vendicated
7e33780743 Fix Splash colour retrieval & show main window earlier if splash is disabled 2025-02-06 05:36:10 +01:00
Vendicated
9905592b24 add option to disable splash screen 2025-02-06 05:16:32 +01:00
Vendicated
6e1913cafc bump to v1.5.5 2025-02-06 04:56:54 +01:00
Vendicated
b620e07445 remember stream resolution & fps settings 2025-02-06 04:51:20 +01:00
Vendicated
9b0503f49d fix naming of displayid -> displayId 2025-02-06 04:27:52 +01:00
Vendicated
c0b79e6e93 make static title option apply without restart 2025-02-06 04:19:57 +01:00
Vendicated
57cae6f9f1 Settings should be wrapped in ErrorBoundary 2025-02-06 04:13:03 +01:00
Vendicated
2e72fa6589 use proper loggers 2025-02-06 04:00:14 +01:00
Vendicated
4ee57da6f3 remove legacy migration code 2025-02-06 03:47:38 +01:00
Cookie
7560727372 Support discord:// uri scheme (#813)
Co-authored-by: v <vendicated@riseup.net>
2025-02-06 03:45:20 +01:00
BlurOne!
6ba562c663 Enhance debug-logs textarea (#1078)
Update the debug-logs textarea to use the render feature, which automatically makes the content of the textarea into a code block, instead of requiring the default value with backticks.
2025-02-05 23:58:59 +01:00
Vendicated
872b60be1c bump deps so cookie quits yelling at me about arrpc 2025-02-05 20:27:32 +01:00
Cookie
8cd80f4af1 fix: First start ui being blank on windows (#1043) 2025-02-05 20:15:20 +01:00
Cookie
7b5e1ed4da arrpc: support OBS auto streamer mode (#822)
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-02-05 19:12:38 +00:00
Cookie
56442ae1e9 feat(rpc): implement arrpc's link event (#1016)
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-02-05 20:00:40 +01:00
Vendicated
c9be618164 add main to renderer command API 2025-02-02 03:17:45 +01:00
Cookie
eddbe27c4d update hardware accel flags for chromium 131 (#1058)
Co-authored-by: v <vendicated@riseup.net>
2025-02-02 01:51:04 +01:00
Vendicated
00fb658355 clean up and optimise Screenshare UI & CSS 2025-02-01 19:49:56 +01:00
v
67a1847cea please stop opening issues about screenshare stuff 2025-01-31 05:08:02 +01:00
Vendicated
030ffca499 settings: add buttons to open chrome://gpu & webrtc-internals 2025-01-24 01:09:25 +01:00
Vendicated
53913c07bf npm postinstall: update arrpc detectables database 2025-01-22 22:14:27 +01:00
Vendicated
f6e29231f5 update dependencies 2025-01-22 22:14:27 +01:00
RSKYS
7a19c81964 README.md: add slackware link to unofficial repos (#1056) 2025-01-20 12:05:31 +01:00
makin
5a72491ab0 Add 4k resolution to screenshare (#1024) 2025-01-15 17:40:56 +01:00
github-actions[bot]
6c4ecc0d64 Metainfo for v1.5.4 (#990)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-05 23:07:25 +01:00
Vendicated
524fc8bc0a bump to v1.5.4 2024-12-05 20:49:47 +01:00
Cookie
5b6c1c6d81 correctly set splash background (#937)
fixes #858
2024-12-03 04:02:34 +01:00
Vendicated
852410a43b Update Electron ~ fixes blue & light mode devtools 2024-12-03 03:58:54 +01:00
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
171 changed files with 11528 additions and 5300 deletions

View File

@@ -5,4 +5,4 @@
# https://github.com/settings/personal-access-tokens/new
GITHUB_TOKEN=
ELECTRON_LAUNCH_FLAGS="--ozone-platform-hint=auto --enable-webrtc-pipewire-capturer --enable-features=WaylandWindowDecorations"
ELECTRON_LAUNCH_FLAGS="--enable-source-maps --ozone-platform-hint=auto"

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

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: "Need Help? Join our support server and ask in the #vesktop-support channel!"

20
.github/ISSUE_TEMPLATE/dev-issue.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Vesktop Developer Issue
description: Reserved for Vesktop Developers. Join our support server for support.
body:
- type: markdown
attributes:
value: |
# This form is reserved for Vesktop Developers. Do not open an issue.
Instead, use the [#vesktop-support channel](https://discord.com/channels/1015060230222131221/1345457031426871417) on our [Discord server](https://vencord.dev/discord) for help and reporting issues.
Your issue will be closed immediately with no comment and you will be blocked if you ignore this.
This is because 99% of issues are not actually bugs, but rather user or system issues and it adds a lot of noise to our development process.
- type: textarea
id: content
attributes:
label: Content
validations:
required: true

View File

@@ -6,33 +6,33 @@ on:
- published
workflow_dispatch:
permissions:
contents: write
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18.18.2
uses: actions/setup-node@v3
with:
node-version: 18.18.2
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: pnpm i
- name: Install dependencies
run: pnpm i
- name: Update metainfo
run: pnpm updateMeta
- name: Update metainfo
run: pnpm generateMeta
- name: Commit and merge in changes
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b ci/meta-update
git add meta/dev.vencord.Vesktop.metainfo.xml
git commit -m "Insert release changes for ${{ github.event.release.tag_name }}"
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"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and merge in changes
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
gh release upload "${{ github.event.release.tag_name }}" dist/dev.vencord.Vesktop.metainfo.xml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
name: Update vencord.dev Vesktop version
on:
release:
types:
- published
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Update scripts/_latestVesktopVersion.txt file in vencord.dev repo
run: |
git config --global user.name "$USERNAME"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git repo
cd repo
version="${{ github.event.release.tag_name }}"
echo "${version#v}" > scripts/_latestVesktopVersion.txt
git add scripts/_latestVesktopVersion.txt
git commit -m "Update Vesktop version to ${{ github.event.release.tag_name }}"
git push https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
env:
API_TOKEN: ${{ secrets.VENCORD_DEV_GITHUB_TOKEN }}
GH_REPO: Vencord/vencord.dev
USERNAME: GitHub-Actions

View File

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

1
.npmrc
View File

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

View File

@@ -1,51 +1,32 @@
# 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**:
- Global Keybinds
Bug reports, feature requests & contributions are highly appreciated!!
- see the [Roadmap](https://github.com/Vencord/Vesktop/issues/324)
![](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
### Windows
Visit https://vesktop.dev/install
Download and run Vesktop-Setup-VERSION.exe from [releases](https://github.com/Vencord/Vesktop/releases/latest)
## Building from Source
### Mac
You need to have the following dependencies installed:
- [Git](https://git-scm.com/downloads)
- [Node.js](https://nodejs.org/en/download)
- pnpm: `npm install --global pnpm`
Download and run Vesktop-VERSION.dmg from [releases](https://github.com/Vencord/Vesktop/releases/latest)
### Linux
[![](https://dl.flathub.org/assets/badges/flathub-badge-en.svg)](https://flathub.org/apps/dev.vencord.Vesktop)
#### Arch based
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)
#### Ubuntu/Debian based
Download Vesktop-VERSION.deb from [releases](https://github.com/Vencord/Vesktop/releases/latest)
#### Fedora/RHEL based
Download Vesktop-VERSION.rpm from [releases](https://github.com/Vencord/Vesktop/releases/latest)
#### Other
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
Packaging will create builds in the dist/ folder
```sh
git clone https://github.com/Vencord/Vesktop
@@ -57,18 +38,23 @@ pnpm i
# Either run it without packaging
pnpm start
# Or package
# Or package (will build packages for your OS)
pnpm package
# Or only build the pacman target
# Or only build the Linux Pacman package
pnpm package --linux pacman
# Or package to a directory only
pnpm package:dir
```
## Motivation
## Building LibVesktop from Source
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 a small C++ helper library Vesktop uses on Linux to emit D-Bus events. By default, prebuilt binaries for x64 and arm64 are used.
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)
If you want to build it from source:
1. Install build dependencies:
- Debian/Ubuntu: `apt install build-essential python3 curl pkg-config libglib2.0-dev`
- Fedora: `dnf install @c-development @development-tools python3 curl pkgconf-pkg-config glib2-devel`
2. Run `pnpm buildLibVesktop`
3. From now on, building Vesktop will use your own build

BIN
build/Assets.car Normal file

Binary file not shown.

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>

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

1
build/icon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" style="isolation:isolate" viewBox="0 0 1024 1024"><path fill="#eb7396" d="M336.23 886.35c56.07 31.62 150.18 51.4 247.12 37.3C816.1 889.78 977.57 673.33 943.7 440.57 909.85 207.83 693.41 46.36 460.65 80.22 227.9 114.08 66.43 330.53 100.3 563.28c6.86 47.2 21.23 91.46 29.65 108.01 6.67 13.1 9.03 35.28 5.28 49.5l-26.8 101.43c-16.75 63.41 21.03 100.93 84.32 83.73l94.59-25.7c14.18-3.86 36.1-1.12 48.9 6.1z" style="display:inline;isolation:isolate"/><g style="display:inline;isolation:isolate"><g style="isolation:isolate"><path d="M494.586 109.162c-3.861.14-7.743.508-11.621 1.004-72.122 6.505-117.149 63.067-128.176 116.693l-17.812 86.508-35.555-76.785v-.002c-23.455-50.637-77.415-90.65-141.998-81.877-32.285 4.386-60.608 22.22-76.123 48.385-13.722 23.14-16.564 52.439-7.66 78.943v1.608l157.394 336.037c24.863 53.102 81.404 87.116 143.43 78.681 61.475-8.36 109.47-52.362 123.914-110.074l91.158-364.25c7.385-29.508 1.643-63.11-20.129-86.328-19.05-20.315-47.417-29.61-76.822-28.543" style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#1d2021;stop-color:#000;stop-opacity:1"/><path fill="#fff" d="m118.49 274.1 153.35 327.4c16 34.18 58.517 59.883 98.852 54.398s78.958-41.418 88.118-78.018l91.16-364.25c9.16-36.6-14.593-67.398-62.253-60.918-51.41 4.189-83.357 45.818-90.957 82.778l-37.84 183.78c-3.42 16.62-11.98 17.61-19.1 2.22l-77.28-166.9c-15.86-34.24-55.539-63.108-97.349-57.428S102.48 239.92 118.49 274.09Z" style="display:inline;fill:#ededed;fill-opacity:1"/><path d="M701.543 396.742a266.4 266.4 0 0 0-100.73 17.746l-.004.002h-.002a271.8 271.8 0 0 0-81.118 48.942 275 275 0 0 0-59.822 74.15v.002a277 277 0 0 0-31.572 93.527v.006l-.002.006c-1.984 13.48-3.023 27.136-3.024 40.893v.014a275.13 275.13 0 0 0 64.01 176.617l.004.004.002.004a269.5 269.5 0 0 0 72.846 61.008l.02.011.02.012a266.5 266.5 0 0 0 93.054 32.19l.017.002a266.3 266.3 0 0 0 114.906-8.061l.02-.006.02-.006a270.3 270.3 0 0 0 67.706-30.816l.004-.002c34.575-21.867 52.68-62.732 45.645-103.032-7.032-40.29-37.897-72.6-77.82-81.47a101.46 101.46 0 0 0-76.338 13.314l-.024.016-.023.015a67 67 0 0 1-16.83 7.67 63.3 63.3 0 0 1-17.957 2.6h-.081a63.86 63.86 0 0 1-31.726-8.37 67 67 0 0 1-17.951-15.042l-.006-.008a72.44 72.44 0 0 1-16.838-46.524v-.016a76.6 76.6 0 0 1 9.367-36.642 72.5 72.5 0 0 1 15.688-19.395l.012-.012.011-.011a69.3 69.3 0 0 1 20.617-12.455 63.97 63.97 0 0 1 59.061 6.896 68.5 68.5 0 0 1 19.91 21.201h.002v.002c28.529 47.605 91.489 63.35 139.068 34.78l.01-.006.01-.006c47.508-28.562 63.227-91.41 34.76-138.975l-.013-.025-.016-.026a271.8 271.8 0 0 0-79.111-84.105l-.026-.02-.027-.017a266.5 266.5 0 0 0-111.506-43.725 266 266 0 0 0-34.223-2.857" style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#ededed;stop-color:#000;stop-opacity:1"/><path fill="#1d2021" d="M751.65 766.94a59.8 59.8 0 0 1 45.03-7.85c23.607 5.237 41.724 24.197 45.882 48.018 4.157 23.821-6.465 47.797-26.902 60.722a228.7 228.7 0 0 1-57.33 26.1 224.7 224.7 0 0 1-96.97 6.8 224.9 224.9 0 0 1-78.56-27.17 227.9 227.9 0 0 1-61.6-51.59 233.5 233.5 0 0 1-54.33-149.94c0-11.67.88-23.3 2.58-34.85a235.4 235.4 0 0 1 26.83-79.48 233.4 233.4 0 0 1 50.78-62.94 230.2 230.2 0 0 1 68.7-41.45 224.8 224.8 0 0 1 113.91-12.56c33.75 5 65.94 17.62 94.1 36.9a230.15 230.15 0 0 1 67 71.23c16.936 28.299 7.765 64.967-20.5 81.96-28.294 16.99-65.005 7.81-81.97-20.5a110.1 110.1 0 0 0-32.08-34.14 105.7 105.7 0 0 0-97.52-11.4 110.9 110.9 0 0 0-33.05 19.96 114 114 0 0 0-24.79 30.69 118.2 118.2 0 0 0-14.51 56.66 114.07 114.07 0 0 0 26.51 73.24 108.6 108.6 0 0 0 29.24 24.5 105.5 105.5 0 0 0 52.45 13.85c10.08 0 20.12-1.45 29.8-4.32a108.6 108.6 0 0 0 27.3-12.44" style="display:inline"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

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

132
eslint.config.mjs Normal file
View File

@@ -0,0 +1,132 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
//@ts-check
import { defineConfig } from "eslint/config";
import stylistic from "@stylistic/eslint-plugin";
import pathAlias from "eslint-plugin-path-alias";
import react from "eslint-plugin-react";
import simpleHeader from "eslint-plugin-simple-header";
import importSort from "eslint-plugin-simple-import-sort";
import unusedImports from "eslint-plugin-unused-imports";
import tseslint from "typescript-eslint";
import prettier from "eslint-plugin-prettier";
export default defineConfig(
{ ignores: ["dist"] },
{
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}"],
settings: {
react: {
version: "19"
}
},
...react.configs.flat.recommended,
rules: {
...react.configs.flat.recommended.rules,
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/display-name": "off",
"react/no-unescaped-entities": "off"
}
},
{
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}"],
plugins: {
simpleHeader,
stylistic,
importSort,
unusedImports,
// @ts-expect-error Missing types
pathAlias,
prettier,
"@typescript-eslint": tseslint.plugin
},
settings: {
"import/resolver": {
alias: {
map: []
}
}
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname
}
},
rules: {
"simpleHeader/header": [
"error",
{
files: ["scripts/header.txt"],
templates: { author: [".*", "Vendicated and Vesktop contributors"] }
}
],
// ESLint Rules
yoda: "error",
eqeqeq: ["error", "always", { null: "ignore" }],
"prefer-destructuring": [
"error",
{
VariableDeclarator: { array: false, object: true },
AssignmentExpression: { array: false, object: false }
}
],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { defaultAssignment: false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { checkLoops: false }],
"no-duplicate-imports": "error",
"@typescript-eslint/dot-notation": [
"error",
{
allowPrivateClassPropertyAccess: true,
allowProtectedClassPropertyAccess: true
}
],
"no-useless-escape": [
"error",
{
allowRegexCharacters: ["i"]
}
],
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"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", { destructuring: "all" }],
"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

@@ -1,167 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<component type="desktop-application">
<!--Created with jdAppStreamEdit 7.1-->
<id>dev.vencord.Vesktop</id>
<name>Vesktop</name>
<summary>Snappier Discord app with Vencord</summary>
<developer_name>Vencord Contributors</developer_name>
<launchable type="desktop-id">dev.vencord.Vesktop.desktop</launchable>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<project_group>Vencord</project_group>
<description>
<p>Vesktop is a cross platform desktop app aiming to give you a snappier Discord experience with Vencord pre-installed.</p>
<p>Vesktop comes bundled with Venmic, a purpose-built library to provide functioning audio screenshare.</p>
</description>
<screenshots>
<screenshot type="default">
<caption>Vencord settings page and about window open</caption>
<image type="source">https://vencord.dev/assets/screenshots/vesktop-1-appstream.png</image>
</screenshot>
<screenshot>
<caption>A dialog showing screenshare options</caption>
<image type="source">https://vencord.dev/assets/screenshots/vesktop-2-appstream.png</image>
</screenshot>
<screenshot>
<caption>A screenshot of a Discord server</caption>
<image type="source">https://vencord.dev/assets/screenshots/vesktop-3-appstream.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.4.4" date="2023-12-02" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.4</url>
<description>
<p>What's Changed</p>
<ul>
<li>improve venmic system compatibility by @Curve</li>
<li>Update steamdeck controller layout by @AAGaming00</li>
<li>feat: Add option to disable smooth scrolling by @ZirixCZ</li>
<li>unblur shiggy in splash screen by @viacoro</li>
<li>update electron &amp; arrpc @D3SOX</li>
</ul>
</description>
</release>
<release version="0.4.3" date="2023-11-01" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.3</url>
</release>
<release version="0.4.2" date="2023-10-26" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.2</url>
</release>
<release version="0.4.1" date="2023-10-24" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.1</url>
</release>
<release version="0.4.0" date="2023-10-21" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.0</url>
</release>
<release version="0.3.3" date="2023-09-30" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.3</url>
</release>
<release version="0.3.2" date="2023-09-25" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.2</url>
</release>
<release version="0.3.1" date="2023-09-25" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.1</url>
</release>
<release version="0.3.0" date="2023-08-16" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.0</url>
</release>
<release version="0.2.9" date="2023-08-12" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.9</url>
</release>
<release version="0.2.8" date="2023-08-02" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.8</url>
</release>
<release version="0.2.7" date="2023-07-26" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.7</url>
</release>
<release version="0.2.6" date="2023-07-04" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.6</url>
</release>
<release version="0.2.5" date="2023-06-26" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.5</url>
</release>
<release version="0.2.4" date="2023-06-25" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.4</url>
</release>
<release version="0.2.3" date="2023-06-23" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.3</url>
</release>
<release version="0.2.2" date="2023-06-21" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.2</url>
</release>
<release version="0.2.1" date="2023-06-21" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.1</url>
</release>
<release version="0.2.0" date="2023-05-03" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.0</url>
</release>
<release version="0.1.9" date="2023-04-27" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.9</url>
</release>
<release version="0.1.8" date="2023-04-15" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.8</url>
</release>
<release version="0.1.7" date="2023-04-15" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.7</url>
</release>
<release version="0.1.6" date="2023-04-11" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.6</url>
</release>
<release version="0.1.5" date="2023-04-10" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.5</url>
</release>
<release version="0.1.4" date="2023-04-09" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.4</url>
</release>
<release version="0.1.3" date="2023-04-06" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.3</url>
</release>
<release version="0.1.2" date="2023-04-05" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.2</url>
</release>
<release version="0.1.1" date="2023-04-04" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.1</url>
</release>
<release version="0.1.0" date="2023-04-04" type="development">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.0</url>
</release>
</releases>
<url type="homepage">https://vencord.dev/</url>
<url type="bugtracker">https://github.com/Vencord/Vesktop/issues</url>
<url type="faq">https://vencord.dev/faq/</url>
<url type="help">https://github.com/Vencord/Vesktop/issues</url>
<url type="donation">https://github.com/sponsors/Vendicated</url>
<url type="vcs-browser">https://github.com/Vencord/Vesktop</url>
<categories>
<category>InstantMessaging</category>
<category>AudioVideo</category>
</categories>
<requires>
<control>pointing</control>
<control>keyboard</control>
<display_length compare="ge">420</display_length>
<internet>always</internet>
</requires>
<recommends>
<control>voice</control>
<display_length compare="ge">760</display_length>
<display_length compare="le">1200</display_length>
<internet>always</internet>
</recommends>
<supports>
<internet>always</internet>
</supports>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute>
<content_attribute id="social-contacts">intense</content_attribute>
<content_attribute id="social-info">intense</content_attribute>
</content_rating>
<keywords>
<keyword>Discord</keyword>
<keyword>Vencord</keyword>
<keyword>Vesktop</keyword>
<keyword>Privacy</keyword>
<keyword>Mod</keyword>
</keywords>
</component>

View File

@@ -1,19 +1,20 @@
{
"name": "VencordDesktop",
"version": "0.4.4",
"name": "vesktop",
"version": "1.6.1",
"private": true,
"description": "",
"description": "Vesktop is a custom Discord desktop app",
"keywords": [],
"homepage": "https://vencord.dev/",
"license": "GPL-3.0",
"license": "GPL-3.0-or-later",
"author": "Vendicated <vendicated@riseup.net>",
"main": "dist/js/main.js",
"scripts": {
"build": "tsx scripts/build/build.mts",
"build:dev": "pnpm build --dev",
"buildLibVesktop": "pnpm -C packages/libvesktop install && pnpm -C packages/libvesktop run build",
"package": "pnpm build && electron-builder",
"package:dir": "pnpm build && electron-builder --dir",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.mts,.mjs",
"lint": "eslint",
"lint:fix": "pnpm lint --fix",
"start": "pnpm build && electron .",
"start:dev": "pnpm build:dev && electron .",
@@ -21,58 +22,71 @@
"test": "pnpm lint && pnpm testTypes",
"testTypes": "tsc --noEmit",
"watch": "pnpm build --watch",
"updateMeta": "tsx scripts/utils/updateMeta.mts"
"generateMeta": "tsx scripts/utils/generateMeta.mts",
"updateArrpcDB": "node ./node_modules/arrpc/update_db.js",
"postinstall": "pnpm updateArrpcDB"
},
"dependencies": {
"arrpc": "github:OpenAsar/arrpc#3e22fd776273afaa4a80c51deb86077ffdd4d2ae"
"arrpc": "github:OpenAsar/arrpc#2234e9c9111f4c42ebcc3aa6a2215bfd979eef77",
"electron-updater": "^6.6.2"
},
"optionalDependencies": {
"@vencord/venmic": "^2.1.3"
"@vencord/venmic": "^6.1.0"
},
"devDependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@types/node": "^20.10.0",
"@types/react": "^18.2.39",
"@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1",
"@vencord/types": "^0.1.2",
"dotenv": "^16.3.1",
"electron": "^27.1.2",
"electron-builder": "^24.9.1",
"esbuild": "^0.19.8",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"@stylistic/eslint-plugin": "^5.6.1",
"@types/node": "^25.0.1",
"@types/react": "19.2.1",
"@vencord/types": "^1.13.7",
"dotenv": "^17.2.3",
"electron": "^39.2.7",
"electron-builder": "^26.0.12",
"esbuild": "^0.27.1",
"eslint": "^9.39.1",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-license-header": "^0.6.0",
"eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^3.0.0",
"prettier": "^3.1.0",
"eslint-plugin-path-alias": "^2.1.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-header": "^1.2.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.3.0",
"libvesktop": "link:packages/libvesktop",
"prettier": "^3.7.4",
"source-map-support": "^0.5.21",
"tsx": "^4.6.0",
"type-fest": "^4.8.2",
"typescript": "^5.3.2",
"xml-formatter": "^3.6.0"
"tsx": "^4.21.0",
"type-fest": "^5.3.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.49.0",
"xml-formatter": "^3.6.7"
},
"packageManager": "pnpm@8.11.0",
"packageManager": "pnpm@10.7.1",
"engines": {
"node": ">=18",
"pnpm": ">=8"
},
"build": {
"appId": "dev.vencord.desktop",
"appId": "dev.vencord.vesktop",
"productName": "Vesktop",
"executableName": "vesktop",
"files": [
"!*",
"!node_modules",
"dist/js",
"static",
"package.json",
"LICENSE"
],
"beforePack": "scripts/build/sandboxFix.js",
"protocols": {
"name": "Discord",
"schemes": [
"discord"
]
},
"beforePack": "scripts/build/beforePack.mjs",
"afterPack": "scripts/build/afterPack.mjs",
"linux": {
"icon": "build/icon.icns",
"icon": "build/icon.svg",
"category": "Network",
"maintainer": "vendicated+vesktop@riseup.net",
"target": [
@@ -106,12 +120,15 @@
}
],
"desktop": {
"Name": "Vesktop",
"GenericName": "Internet Messenger",
"Type": "Application",
"Categories": "Network;InstantMessaging;Chat;",
"Keywords": "discord;vencord;electron;chat;",
"StartupWMClass": "VencordDesktop"
"entry": {
"Name": "Vesktop",
"GenericName": "Internet Messenger",
"Type": "Application",
"Categories": "Network;InstantMessaging;Chat;",
"Keywords": "discord;vencord;electron;chat;",
"MimeType": "x-scheme-handler/discord",
"StartupWMClass": "vesktop"
}
}
},
"mac": {
@@ -119,31 +136,93 @@
{
"target": "default",
"arch": [
"x64",
"arm64"
"universal"
]
}
],
"category": "Network",
"category": "public.app-category.social-networking",
"darkModeSupport": true,
"extendInfo": {
"NSMicrophoneUsageDescription": "This app needs access to the microphone",
"NSCameraUsageDescription": "This app needs access to the camera",
"com.apple.security.device.audio-input": true,
"com.apple.security.device.camera": true
}
"com.apple.security.device.camera": true,
"CFBundleIconName": "Icon"
},
"notarize": true
},
"dmg": {
"background": "build/background.tiff",
"icon": "build/icon.icns",
"iconSize": 105,
"window": {
"width": 512,
"height": 340
},
"contents": [
{
"x": 140,
"y": 160
},
{
"x": 372,
"y": 160,
"type": "link",
"path": "/Applications"
}
]
},
"nsis": {
"include": "build/installer.nsh",
"oneClick": false
},
"win": {
"icon": "build/icon.ico",
"target": [
"nsis",
"zip"
{
"target": "nsis",
"arch": [
"x64",
"arm64"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
]
},
"publish": {
"provider": "github"
},
"rpm": {
"fpm": [
"--rpm-rpmbuild-define=_build_id_links none"
]
},
"electronFuses": {
"runAsNode": false,
"enableCookieEncryption": false,
"enableNodeOptionsEnvironmentVariable": false,
"enableNodeCliInspectArguments": false,
"enableEmbeddedAsarIntegrityValidation": false,
"onlyLoadAppFromAsar": true,
"loadBrowserProcessSpecificV8Snapshot": false,
"grantFileProtocolExtraPrivileges": false
}
},
"pnpm": {
"patchedDependencies": {
"arrpc@3.5.0": "patches/arrpc@3.5.0.patch",
"electron-updater": "patches/electron-updater.patch"
},
"onlyBuiltDependencies": [
"@vencord/venmic",
"electron",
"esbuild"
]
}
}

1
packages/libvesktop/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build

View File

@@ -0,0 +1,19 @@
# Dockerfile for building both x64 and arm64 on an old distro for maximum compatibility.
# ubuntu20 is dead but debian11 is still supported for now
FROM debian:11
ENV DEBIAN_FRONTEND=noninteractive
RUN dpkg --add-architecture arm64
RUN apt-get update && apt-get install -y \
build-essential python3 curl pkg-config \
g++-aarch64-linux-gnu libglib2.0-dev:amd64 libglib2.0-dev:arm64 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get update && apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src

View File

@@ -0,0 +1,19 @@
{
"targets": [
{
"target_name": "libvesktop",
"sources": [ "src/libvesktop.cc" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"cflags_cc": [
"<!(pkg-config --cflags glib-2.0 gio-2.0)",
"-O3"
],
"libraries": [
"<!@(pkg-config --libs-only-l --libs-only-other glib-2.0 gio-2.0)"
],
"cflags_cc!": ["-fno-exceptions"],
}
]
}

17
packages/libvesktop/build.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/sh
set -e
docker build -t libvesktop-builder -f Dockerfile .
docker run --rm -v "$PWD":/src -w /src libvesktop-builder bash -c "
set -e
echo '=== Building x64 ==='
npx node-gyp rebuild --arch=x64
mv build/Release/vesktop.node prebuilds/vesktop-x64.node
echo '=== Building arm64 ==='
export CXX=aarch64-linux-gnu-g++
npx node-gyp rebuild --arch=arm64
mv build/Release/vesktop.node prebuilds/vesktop-arm64.node
"

3
packages/libvesktop/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export function getAccentColor(): number | null;
export function requestBackground(autoStart: boolean, commandLine: string[]): boolean;
export function updateUnityLauncherCount(count: number): boolean;

View File

@@ -0,0 +1,14 @@
{
"name": "libvesktop",
"main": "build/Release/vesktop.node",
"types": "index.d.ts",
"devDependencies": {
"node-addon-api": "^8.5.0",
"node-gyp": "^11.4.2"
},
"scripts": {
"build": "node-gyp configure build",
"clean": "node-gyp clean",
"test": "npm run build && node test.js"
}
}

730
packages/libvesktop/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,730 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
node-addon-api:
specifier: ^8.5.0
version: 8.5.0
node-gyp:
specifier: ^11.4.2
version: 11.4.2
packages:
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
'@npmcli/agent@3.0.0':
resolution: {integrity: sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==}
engines: {node: ^18.17.0 || >=20.5.0}
'@npmcli/fs@4.0.0':
resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==}
engines: {node: ^18.17.0 || >=20.5.0}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
abbrev@3.0.1:
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
engines: {node: ^18.17.0 || >=20.5.0}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
cacache@19.0.1:
resolution: {integrity: sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==}
engines: {node: ^18.17.0 || >=20.5.0}
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
encoding@0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
err-code@2.0.3:
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
exponential-backoff@3.1.2:
resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
fs-minipass@3.0.3:
resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
ip-address@10.0.1:
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
engines: {node: '>= 12'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
isexe@3.1.1:
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
engines: {node: '>=16'}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
make-fetch-happen@14.0.3:
resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==}
engines: {node: ^18.17.0 || >=20.5.0}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minipass-collect@2.0.1:
resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==}
engines: {node: '>=16 || 14 >=14.17'}
minipass-fetch@4.0.1:
resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==}
engines: {node: ^18.17.0 || >=20.5.0}
minipass-flush@1.0.5:
resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
engines: {node: '>= 8'}
minipass-pipeline@1.2.4:
resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
engines: {node: '>=8'}
minipass-sized@1.0.3:
resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
engines: {node: '>=8'}
minipass@3.3.6:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
minizlib@3.1.0:
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
node-addon-api@8.5.0:
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
engines: {node: ^18 || ^20 || >= 21}
node-gyp@11.4.2:
resolution: {integrity: sha512-3gD+6zsrLQH7DyYOUIutaauuXrcyxeTPyQuZQCQoNPZMHMMS5m4y0xclNpvYzoK3VNzuyxT6eF4mkIL4WSZ1eQ==}
engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true
nopt@8.1.0:
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true
p-map@7.0.3:
resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==}
engines: {node: '>=18'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
proc-log@5.0.0:
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
engines: {node: ^18.17.0 || >=20.5.0}
promise-retry@2.0.1:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
engines: {node: '>=10'}
retry@0.12.0:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
socks-proxy-agent@8.0.5:
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
engines: {node: '>= 14'}
socks@2.8.7:
resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
ssri@12.0.0:
resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==}
engines: {node: ^18.17.0 || >=20.5.0}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.2:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
tar@7.5.1:
resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==}
engines: {node: '>=18'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
unique-filename@4.0.0:
resolution: {integrity: sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==}
engines: {node: ^18.17.0 || >=20.5.0}
unique-slug@5.0.0:
resolution: {integrity: sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==}
engines: {node: ^18.17.0 || >=20.5.0}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
which@5.0.0:
resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==}
engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
snapshots:
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.2
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
'@npmcli/agent@3.0.0':
dependencies:
agent-base: 7.1.4
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
lru-cache: 10.4.3
socks-proxy-agent: 8.0.5
transitivePeerDependencies:
- supports-color
'@npmcli/fs@4.0.0':
dependencies:
semver: 7.7.2
'@pkgjs/parseargs@0.11.0':
optional: true
abbrev@3.0.1: {}
agent-base@7.1.4: {}
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.3: {}
balanced-match@1.0.2: {}
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
cacache@19.0.1:
dependencies:
'@npmcli/fs': 4.0.0
fs-minipass: 3.0.3
glob: 10.4.5
lru-cache: 10.4.3
minipass: 7.1.2
minipass-collect: 2.0.1
minipass-flush: 1.0.5
minipass-pipeline: 1.2.4
p-map: 7.0.3
ssri: 12.0.0
tar: 7.5.1
unique-filename: 4.0.0
chownr@3.0.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
debug@4.4.3:
dependencies:
ms: 2.1.3
eastasianwidth@0.2.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
encoding@0.1.13:
dependencies:
iconv-lite: 0.6.3
optional: true
env-paths@2.2.1: {}
err-code@2.0.3: {}
exponential-backoff@3.1.2: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
fs-minipass@3.0.3:
dependencies:
minipass: 7.1.2
glob@10.4.5:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
graceful-fs@4.2.11: {}
http-cache-semantics@4.2.0: {}
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
optional: true
imurmurhash@0.1.4: {}
ip-address@10.0.1: {}
is-fullwidth-code-point@3.0.0: {}
isexe@2.0.0: {}
isexe@3.1.1: {}
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
lru-cache@10.4.3: {}
make-fetch-happen@14.0.3:
dependencies:
'@npmcli/agent': 3.0.0
cacache: 19.0.1
http-cache-semantics: 4.2.0
minipass: 7.1.2
minipass-fetch: 4.0.1
minipass-flush: 1.0.5
minipass-pipeline: 1.2.4
negotiator: 1.0.0
proc-log: 5.0.0
promise-retry: 2.0.1
ssri: 12.0.0
transitivePeerDependencies:
- supports-color
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
minipass-collect@2.0.1:
dependencies:
minipass: 7.1.2
minipass-fetch@4.0.1:
dependencies:
minipass: 7.1.2
minipass-sized: 1.0.3
minizlib: 3.1.0
optionalDependencies:
encoding: 0.1.13
minipass-flush@1.0.5:
dependencies:
minipass: 3.3.6
minipass-pipeline@1.2.4:
dependencies:
minipass: 3.3.6
minipass-sized@1.0.3:
dependencies:
minipass: 3.3.6
minipass@3.3.6:
dependencies:
yallist: 4.0.0
minipass@7.1.2: {}
minizlib@3.1.0:
dependencies:
minipass: 7.1.2
ms@2.1.3: {}
negotiator@1.0.0: {}
node-addon-api@8.5.0: {}
node-gyp@11.4.2:
dependencies:
env-paths: 2.2.1
exponential-backoff: 3.1.2
graceful-fs: 4.2.11
make-fetch-happen: 14.0.3
nopt: 8.1.0
proc-log: 5.0.0
semver: 7.7.2
tar: 7.5.1
tinyglobby: 0.2.15
which: 5.0.0
transitivePeerDependencies:
- supports-color
nopt@8.1.0:
dependencies:
abbrev: 3.0.1
p-map@7.0.3: {}
package-json-from-dist@1.0.1: {}
path-key@3.1.1: {}
path-scurry@1.11.1:
dependencies:
lru-cache: 10.4.3
minipass: 7.1.2
picomatch@4.0.3: {}
proc-log@5.0.0: {}
promise-retry@2.0.1:
dependencies:
err-code: 2.0.3
retry: 0.12.0
retry@0.12.0: {}
safer-buffer@2.1.2:
optional: true
semver@7.7.2: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
smart-buffer@4.2.0: {}
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
socks: 2.8.7
transitivePeerDependencies:
- supports-color
socks@2.8.7:
dependencies:
ip-address: 10.0.1
smart-buffer: 4.2.0
ssri@12.0.0:
dependencies:
minipass: 7.1.2
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.2
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.2:
dependencies:
ansi-regex: 6.2.2
tar@7.5.1:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
minipass: 7.1.2
minizlib: 3.1.0
yallist: 5.0.0
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
unique-filename@4.0.0:
dependencies:
unique-slug: 5.0.0
unique-slug@5.0.0:
dependencies:
imurmurhash: 0.1.4
which@2.0.2:
dependencies:
isexe: 2.0.0
which@5.0.0:
dependencies:
isexe: 3.1.1
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.3
string-width: 5.1.2
strip-ansi: 7.1.2
yallist@4.0.0: {}
yallist@5.0.0: {}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,269 @@
#include <gio/gio.h>
#include <cstdlib>
#include <cstdint>
#include <iostream>
#include <napi.h>
#include <optional>
#include <cmath>
#include <memory>
template <typename T>
struct GObjectDeleter
{
void operator()(T *obj) const
{
if (obj)
g_object_unref(obj);
}
};
template <typename T>
using GObjectPtr = std::unique_ptr<T, GObjectDeleter<T>>;
struct GVariantDeleter
{
void operator()(GVariant *variant) const
{
if (variant)
g_variant_unref(variant);
}
};
using GVariantPtr = std::unique_ptr<GVariant, GVariantDeleter>;
struct GErrorDeleter
{
void operator()(GError *error) const
{
if (error)
g_error_free(error);
}
};
using GErrorPtr = std::unique_ptr<GError, GErrorDeleter>;
bool update_launcher_count(int count)
{
GError *error = nullptr;
const char *chromeDesktop = std::getenv("CHROME_DESKTOP");
std::string desktop_id = std::string("application://") + (chromeDesktop ? chromeDesktop : "vesktop.desktop");
GObjectPtr<GDBusConnection> bus(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
if (!bus)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::update_launcher_count] Failed to connect to session bus: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return false;
}
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&builder, "{sv}", "count", g_variant_new_int64(count));
g_variant_builder_add(&builder, "{sv}", "count-visible", g_variant_new_boolean(count != 0));
gboolean result = g_dbus_connection_emit_signal(
bus.get(),
nullptr,
"/",
"com.canonical.Unity.LauncherEntry",
"Update",
g_variant_new("(sa{sv})", desktop_id.c_str(), &builder),
&error);
if (!result || error)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::update_launcher_count] Failed to emit Update signal: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return false;
}
return true;
}
std::optional<int32_t> get_accent_color()
{
GError *error = nullptr;
GObjectPtr<GDBusConnection> bus(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
if (!bus)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::get_accent_color] Failed to connect to session bus: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return std::nullopt;
}
GVariantPtr reply(g_dbus_connection_call_sync(
bus.get(),
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Settings",
"Read",
g_variant_new("(ss)", "org.freedesktop.appearance", "accent-color"),
nullptr,
G_DBUS_CALL_FLAGS_NONE,
5000,
nullptr,
&error));
if (!reply)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::get_accent_color] Failed to call Read: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return std::nullopt;
}
GVariant *inner_raw = nullptr;
g_variant_get(reply.get(), "(v)", &inner_raw);
if (!inner_raw)
{
std::cerr << "[libvesktop::get_accent_color] Inner variant is null" << std::endl;
return std::nullopt;
}
GVariantPtr inner(inner_raw);
// Unwrap nested variants
while (g_variant_is_of_type(inner.get(), G_VARIANT_TYPE_VARIANT))
{
GVariant *next = g_variant_get_variant(inner.get());
inner.reset(next);
}
if (!g_variant_is_of_type(inner.get(), G_VARIANT_TYPE_TUPLE) ||
g_variant_n_children(inner.get()) < 3)
{
std::cerr << "[libvesktop::get_accent_color] Inner variant is not a tuple of 3 doubles" << std::endl;
return std::nullopt;
}
double r = 0.0, g = 0.0, b = 0.0;
g_variant_get(inner.get(), "(ddd)", &r, &g, &b);
bool discard = false;
auto toInt = [&discard](double v) -> int
{
if (!std::isfinite(v) || v < 0.0 || v > 1.0)
{
discard = true;
return 0;
}
return static_cast<int>(std::round(v * 255.0));
};
int32_t rgb = (toInt(r) << 16) | (toInt(g) << 8) | toInt(b);
if (discard)
return std::nullopt;
return rgb;
}
bool request_background(bool autostart, const std::vector<std::string> &commandline)
{
GError *error = nullptr;
GObjectPtr<GDBusConnection> bus(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
if (!bus)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::request_background] Failed to connect to session bus: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return false;
}
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&builder, "{sv}", "autostart", g_variant_new_boolean(autostart));
if (!commandline.empty())
{
GVariantBuilder cmd_builder;
g_variant_builder_init(&cmd_builder, G_VARIANT_TYPE("as"));
for (const auto &s : commandline)
g_variant_builder_add(&cmd_builder, "s", s.c_str());
g_variant_builder_add(&builder, "{sv}", "commandline", g_variant_builder_end(&cmd_builder));
}
GVariantPtr reply(g_dbus_connection_call_sync(
bus.get(),
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Background",
"RequestBackground",
g_variant_new("(sa{sv})", "", &builder),
nullptr,
G_DBUS_CALL_FLAGS_NONE,
5000,
nullptr,
&error));
if (!reply)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::request_background] Failed to call RequestBackground: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return false;
}
return true;
}
Napi::Value updateUnityLauncherCount(Napi::CallbackInfo const &info)
{
if (info.Length() < 1 || !info[0].IsNumber())
{
Napi::TypeError::New(info.Env(), "Expected (number)").ThrowAsJavaScriptException();
return info.Env().Undefined();
}
int count = info[0].As<Napi::Number>().Int32Value();
bool success = update_launcher_count(count);
return Napi::Boolean::New(info.Env(), success);
}
Napi::Value getAccentColor(const Napi::CallbackInfo &info)
{
auto color = get_accent_color();
if (color)
return Napi::Number::New(info.Env(), *color);
return info.Env().Null();
}
Napi::Value RequestBackground(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsBoolean() || !info[1].IsArray())
{
Napi::TypeError::New(env, "Expected (boolean, string[])").ThrowAsJavaScriptException();
return env.Null();
}
bool autostart = info[0].As<Napi::Boolean>();
Napi::Array arr = info[1].As<Napi::Array>();
std::vector<std::string> commandline;
for (uint32_t i = 0; i < arr.Length(); i++)
{
Napi::Value v = arr.Get(i);
if (v.IsString())
commandline.push_back(v.As<Napi::String>().Utf8Value());
}
bool ok = request_background(autostart, commandline);
return Napi::Boolean::New(env, ok);
}
Napi::Object Init(Napi::Env env, Napi::Object exports)
{
exports.Set("updateUnityLauncherCount", Napi::Function::New(env, updateUnityLauncherCount));
exports.Set("getAccentColor", Napi::Function::New(env, getAccentColor));
exports.Set("requestBackground", Napi::Function::New(env, RequestBackground));
return exports;
}
NODE_API_MODULE(libvesktop, Init)

View File

@@ -0,0 +1,22 @@
/**
* @type {typeof import(".")}
*/
const libVesktop = require(".");
const test = require("node:test");
const assert = require("node:assert/strict");
test("getAccentColor should return a number", () => {
const color = libVesktop.getAccentColor();
assert.strictEqual(typeof color, "number");
});
test("updateUnityLauncherCount should return true (success)", () => {
assert.strictEqual(libVesktop.updateUnityLauncherCount(5), true);
assert.strictEqual(libVesktop.updateUnityLauncherCount(0), true);
assert.strictEqual(libVesktop.updateUnityLauncherCount(10), true);
});
test("requestBackground should return true (success)", () => {
assert.strictEqual(libVesktop.requestBackground(true, ["bash"]), true);
assert.strictEqual(libVesktop.requestBackground(false, []), true);
});

27
patches/arrpc@3.5.0.patch Normal file
View File

@@ -0,0 +1,27 @@
diff --git a/src/process/index.js b/src/process/index.js
index 389b0845256a34b4536d6da99edb00d17f13a6b4..f17a0ac687e9110ebfd33cb91fd2f6250d318643 100644
--- a/src/process/index.js
+++ b/src/process/index.js
@@ -5,8 +5,20 @@ import fs from 'node:fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8'));
+const DetectableDB = require('./detectable.json');
+DetectableDB.push(
+ {
+ aliases: ["Obs"],
+ executables: [
+ { is_launcher: false, name: "obs", os: "linux" },
+ { is_launcher: false, name: "obs.exe", os: "win32" },
+ { is_launcher: false, name: "obs.app", os: "darwin" }
+ ],
+ hook: true,
+ id: "STREAMERMODE",
+ name: "OBS"
+ }
+);
import * as Natives from './native/index.js';
const Native = Natives[process.platform];

View File

@@ -0,0 +1,16 @@
diff --git a/out/RpmUpdater.js b/out/RpmUpdater.js
index 563187bb18cb0bd154dff6620cb62b8c8f534cd6..d91594026c2bac9cc78ef3b1183df3241d7d9624 100644
--- a/out/RpmUpdater.js
+++ b/out/RpmUpdater.js
@@ -32,7 +32,10 @@ class RpmUpdater extends BaseUpdater_1.BaseUpdater {
const sudo = this.wrapSudo();
// pkexec doesn't want the command to be wrapped in " quotes
const wrapper = /pkexec/i.test(sudo) ? "" : `"`;
- const packageManager = this.spawnSyncLog("which zypper");
+ let packageManager;
+ try {
+ packageManager = this.spawnSyncLog("which zypper");
+ } catch {}
const installerPath = this.installerPath;
if (installerPath == null) {
this.dispatchError(new Error("No valid update available, can't quit and install"));

8571
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
import { copyFile, readdir } from "fs/promises";
/**
* @param {{
* readonly appOutDir: string;
* readonly arch: Arch;
* readonly electronPlatformName: string;
* readonly outDir: string;
* readonly packager: PlatformPackager;
* readonly targets: Target[];
* }} context
*/
export async function addAssetsCar({ appOutDir }) {
if (process.platform !== "darwin") return;
const appName = (await readdir(appOutDir)).find(item => item.endsWith(".app"));
if (!appName) {
console.warn(`Could not find .app directory in ${appOutDir}. Skipping adding assets.car`);
return;
}
await copyFile("build/Assets.car", `${appOutDir}/${appName}/Contents/Resources/Assets.car`);
}

View File

@@ -0,0 +1,5 @@
import { addAssetsCar } from "./addAssetsCar.mjs";
export default async function afterPack(context) {
await addAssetsCar(context);
}

View File

@@ -0,0 +1,5 @@
import { applyAppImageSandboxFix } from "./sandboxFix.mjs";
export default async function beforePack() {
await applyAppImageSandboxFix();
}

View File

@@ -1,13 +1,14 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BuildContext, BuildOptions, context } from "esbuild";
import { copyFile } from "fs/promises";
import vencordDep from "./vencordDep.mjs";
import { includeDirPlugin } from "./includeDirPlugin.mts";
const isDev = process.argv.includes("--dev");
@@ -24,6 +25,9 @@ const NodeCommonOpts: BuildOptions = {
platform: "node",
external: ["electron"],
target: ["esnext"],
loader: {
".node": "file"
},
define: {
IS_DEV: JSON.stringify(isDev)
}
@@ -49,25 +53,58 @@ async function copyVenmic() {
]).catch(() => console.warn("Failed to copy venmic. Building without venmic support"));
}
async function copyLibVesktop() {
if (process.platform !== "linux") return;
try {
await copyFile(
"./packages/libvesktop/build/Release/vesktop.node",
`./static/dist/libvesktop-${process.arch}.node`
);
console.log("Using local libvesktop build");
} catch {
console.log(
"Using prebuilt libvesktop binaries. Run `pnpm buildLibVesktop` and build again to build from source - see README.md for more details"
);
return Promise.all([
copyFile("./packages/libvesktop/prebuilds/vesktop-x64.node", "./static/dist/libvesktop-x64.node"),
copyFile("./packages/libvesktop/prebuilds/vesktop-arm64.node", "./static/dist/libvesktop-arm64.node")
]).catch(() => console.warn("Failed to copy libvesktop. Building without libvesktop support"));
}
}
await Promise.all([
copyVenmic(),
copyLibVesktop(),
createContext({
...NodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/js/main.js",
footer: { js: "//# sourceURL=VCDMain" }
footer: { js: "//# sourceURL=VesktopMain" }
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/main/arrpc/worker.ts"],
outfile: "dist/js/arRpcWorker.js",
footer: { js: "//# sourceURL=VesktopArRpcWorker" }
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/preload/index.ts"],
outfile: "dist/js/preload.js",
footer: { js: "//# sourceURL=VCDPreload" }
footer: { js: "//# sourceURL=VesktopPreload" }
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/updater/preload.ts"],
entryPoints: ["src/preload/splash.ts"],
outfile: "dist/js/splashPreload.js",
footer: { js: "//# sourceURL=VesktopSplashPreload" }
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/preload/updater.ts"],
outfile: "dist/js/updaterPreload.js",
footer: { js: "//# sourceURL=VCDUpdaterPreload" }
footer: { js: "//# sourceURL=VesktopUpdaterPreload" }
}),
createContext({
...CommonOpts,
@@ -79,8 +116,8 @@ await Promise.all([
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",
external: ["@vencord/types/*"],
plugins: [vencordDep],
footer: { js: "//# sourceURL=VCDRenderer" }
plugins: [vencordDep, includeDirPlugin("patches", "src/renderer/patches")],
footer: { js: "//# sourceURL=VesktopRenderer" }
})
]);

View File

@@ -0,0 +1,25 @@
import { Plugin } from "esbuild";
import { readdir } from "fs/promises";
const makeImportAllCode = (files: string[]) =>
files.map(f => `require("./${f.replace(/\.[cm]?[tj]sx?$/, "")}")`).join("\n");
const makeImportDirRecursiveCode = (dir: string) => readdir(dir).then(files => makeImportAllCode(files));
export function includeDirPlugin(namespace: string, path: string): Plugin {
return {
name: `include-dir-plugin:${namespace}`,
setup(build) {
const filter = new RegExp(`^__${namespace}__$`);
build.onResolve({ filter }, args => ({ path: args.path, namespace }));
build.onLoad({ filter, namespace }, async args => {
return {
contents: await makeImportDirRecursiveCode(path),
resolveDir: path
};
});
}
};
}

View File

@@ -1,9 +1,3 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
export let VencordCreateElement = (...args) =>
(VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);

View File

@@ -1,23 +1,26 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js
const fs = require("fs/promises");
const path = require("path");
import fs from "fs/promises";
import path from "path";
import AppImageTarget from "app-builder-lib/out/targets/AppImageTarget.js";
let isApplied = false;
const hook = async () => {
if (isApplied) return;
isApplied = true;
export async function applyAppImageSandboxFix() {
if (process.platform !== "linux") {
// this fix is only required on linux
return;
}
const AppImageTarget = require("app-builder-lib/out/targets/AppImageTarget");
if (isApplied) return;
isApplied = true;
const oldBuildMethod = AppImageTarget.default.prototype.build;
AppImageTarget.default.prototype.build = async function (...args) {
console.log("Running AppImage builder hook", args);
@@ -69,6 +72,4 @@ exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ]
return ret;
};
};
module.exports = hook;
}

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { globalExternalsWithRegExp } from "@fal-works/esbuild-plugin-global-externals";

View File

@@ -1,5 +1,5 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* Copyright (c) {year} {author}
* SPDX-License-Identifier: GPL-3.0-or-later
*/

View File

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

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./start";

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { config } from "dotenv";

View File

@@ -1,11 +1,11 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { promises as fs } from "node:fs";
import { mkdir } from "node:fs/promises";
import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
import xmlFormat from "xml-formatter";
@@ -43,14 +43,25 @@ function generateDescription(description: string, descriptionNode: Element) {
}
}
const latestReleaseInformation = await fetch("https://api.github.com/repos/Vencord/Vesktop/releases/latest", {
const releases = await fetch("https://api.github.com/repos/Vencord/Vesktop/releases", {
headers: {
Accept: "application/vnd.github+json",
"X-Github-Api-Version": "2022-11-28"
}
}).then(res => res.json());
const metaInfo = await fs.readFile("./meta/dev.vencord.Vesktop.metainfo.xml", "utf-8");
const latestReleaseInformation = releases[0];
const metaInfo = await (async () => {
for (const release of releases) {
const metaAsset = release.assets.find((a: any) => a.name === "dev.vencord.Vesktop.metainfo.xml");
if (metaAsset) return fetch(metaAsset.browser_download_url).then(res => res.text());
}
})();
if (!metaInfo) {
throw new Error("Could not find existing meta information from any release");
}
const parser = new DOMParser().parseFromString(metaInfo, "text/xml");
@@ -90,4 +101,7 @@ const output = xmlFormat(new XMLSerializer().serializeToString(parser), {
indentation: " "
});
await fs.writeFile("./meta/dev.vencord.Vesktop.metainfo.xml", output, "utf-8");
await mkdir("./dist", { recursive: true });
await fs.writeFile("./dist/dev.vencord.Vesktop.metainfo.xml", output, "utf-8");
console.log("Updated meta information written to ./dist/dev.vencord.Vesktop.metainfo.xml");

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { spawn as spaaawn, SpawnOptions } from "child_process";

4
src/globals.d.ts vendored
View File

@@ -1,13 +1,13 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
declare global {
export var VesktopNative: typeof import("preload/VesktopNative").VesktopNative;
export var Vesktop: typeof import("renderer/index");
export var VCDP: any;
export var VesktopPatchGlobals: any;
export var IS_DEV: boolean;
}

View File

@@ -1,28 +1,32 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BrowserWindow } from "electron";
import { join } from "path";
import { ICON_PATH, VIEW_DIR } from "shared/paths";
import { app, BrowserWindow } from "electron";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { loadView } from "./vesktopStatic";
export async function createAboutWindow() {
const height = 750;
const width = height * (4 / 3);
export function createAboutWindow() {
const about = new BrowserWindow({
center: true,
autoHideMenuBar: true,
icon: ICON_PATH,
webPreferences: {
preload: join(__dirname, "updaterPreload.js")
}
height,
width
});
makeLinksOpenExternally(about);
about.loadFile(join(VIEW_DIR, "about.html"));
const data = new URLSearchParams({
APP_VERSION: app.getVersion()
});
loadView(about, "about.html", data);
return about;
}

View File

@@ -1,13 +1,17 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, NativeImage, nativeImage } from "electron";
import { join } from "path";
import { BADGE_DIR } from "shared/paths";
import { updateUnityLauncherCount } from "./dbus";
import { AppEvents } from "./events";
import { mainWin } from "./mainWindow";
const imgCache = new Map<number, NativeImage>();
function loadBadge(index: number) {
const cached = imgCache.get(index);
@@ -21,18 +25,24 @@ function loadBadge(index: number) {
let lastIndex: null | number = -1;
/**
* -1 = show unread indicator
* 0 = clear
*/
export function setBadgeCount(count: number) {
AppEvents.emit("setTrayVariant", count !== 0 ? "trayUnread" : "tray");
switch (process.platform) {
case "linux":
if (count === -1) count = 0;
app.setBadgeCount(count);
updateUnityLauncherCount(count);
break;
case "darwin":
if (count === 0) {
app.dock.setBadge("");
app.dock!.setBadge("");
break;
}
app.dock.setBadge(count === -1 ? "•" : count.toString());
app.dock!.setBadge(count === -1 ? "•" : count.toString());
break;
case "win32":
const [index, description] = getBadgeIndexAndDescription(count);
@@ -40,8 +50,6 @@ export function setBadgeCount(count: number) {
lastIndex = index;
// circular import shenanigans
const { mainWin } = require("./mainWindow") as typeof import("./mainWindow");
mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description);
break;
}

View File

@@ -1,38 +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 Server from "arrpc";
import { IpcEvents } from "shared/IpcEvents";
import { mainWin } from "./mainWindow";
import { Settings } from "./settings";
let server: any;
const inviteCodeRegex = /^(\w|-)+$/;
export async function initArRPC() {
if (server || !Settings.store.arRPC) return;
try {
server = await new Server();
server.on("activity", (data: any) => mainWin.webContents.send(IpcEvents.ARRPC_ACTIVITY, JSON.stringify(data)));
server.on("invite", (invite: string, callback: (valid: boolean) => void) => {
invite = String(invite);
if (!inviteCodeRegex.test(invite)) return callback(false);
mainWin.webContents
// Safety: Result of JSON.stringify should always be safe to equal
// Also, just to be super super safe, invite is regex validated above
.executeJavaScript(`Vesktop.openInviteModal(${JSON.stringify(invite)})`)
.then(callback);
});
} catch (e) {
console.error("Failed to start arRPC server", e);
}
}
Settings.addChangeListener("arRPC", initArRPC);

77
src/main/arrpc/index.ts Normal file
View File

@@ -0,0 +1,77 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { resolve } from "path";
import { IpcCommands } from "shared/IpcEvents";
import { MessageChannel, Worker } from "worker_threads";
import { sendRendererCommand } from "../ipcCommands";
import { Settings } from "../settings";
import { ArRpcEvent, ArRpcHostEvent } from "./types";
let worker: Worker;
const inviteCodeRegex = /^(\w|-)+$/;
export async function initArRPC() {
if (worker || !Settings.store.arRPC) return;
try {
const { port1: hostPort, port2: workerPort } = new MessageChannel();
worker = new Worker(resolve(__dirname, "./arRpcWorker.js"), {
workerData: {
workerPort
},
transferList: [workerPort]
});
hostPort.on("message", async ({ type, nonce, data }: ArRpcEvent) => {
switch (type) {
case "activity": {
sendRendererCommand(IpcCommands.RPC_ACTIVITY, data);
break;
}
case "invite": {
const invite = String(data);
const response: ArRpcHostEvent = {
type: "ack-invite",
nonce,
data: false
};
if (!inviteCodeRegex.test(invite)) {
return hostPort.postMessage(response);
}
response.data = await sendRendererCommand(IpcCommands.RPC_INVITE, invite).catch(() => false);
hostPort.postMessage(response);
break;
}
case "link": {
const response: ArRpcHostEvent = {
type: "ack-link",
nonce: nonce,
data: false
};
response.data = await sendRendererCommand(IpcCommands.RPC_DEEP_LINK, data).catch(() => false);
hostPort.postMessage(response);
break;
}
}
});
} catch (e) {
console.error("Failed to start arRPC server", e);
}
}
Settings.addChangeListener("arRPC", initArRPC);

38
src/main/arrpc/types.ts Normal file
View File

@@ -0,0 +1,38 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type ArRpcEvent = ArRpcActivityEvent | ArRpcInviteEvent | ArRpcLinkEvent;
export type ArRpcHostEvent = ArRpcHostAckInviteEvent | ArRpcHostAckLinkEvent;
export interface ArRpcActivityEvent {
type: "activity";
nonce: string;
data: string;
}
export interface ArRpcInviteEvent {
type: "invite";
nonce: string;
data: string;
}
export interface ArRpcLinkEvent {
type: "link";
nonce: string;
data: any;
}
export interface ArRpcHostAckInviteEvent {
type: "ack-invite";
nonce: string;
data: boolean;
}
export interface ArRpcHostAckLinkEvent {
type: "ack-link";
nonce: string;
data: boolean;
}

73
src/main/arrpc/worker.ts Normal file
View File

@@ -0,0 +1,73 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import Server from "arrpc";
import { randomUUID } from "crypto";
import { MessagePort, workerData } from "worker_threads";
import { ArRpcEvent, ArRpcHostEvent } from "./types";
let server: any;
type InviteCallback = (valid: boolean) => void;
type LinkCallback = InviteCallback;
const inviteCallbacks = new Map<string, InviteCallback>();
const linkCallbacks = new Map<string, LinkCallback>();
(async function () {
const { workerPort } = workerData as { workerPort: MessagePort };
server = await new Server();
server.on("activity", (data: any) => {
const event: ArRpcEvent = {
type: "activity",
data: JSON.stringify(data),
nonce: randomUUID()
};
workerPort.postMessage(event);
});
server.on("invite", (invite: string, callback: InviteCallback) => {
const nonce = randomUUID();
inviteCallbacks.set(nonce, callback);
const event: ArRpcEvent = {
type: "invite",
data: invite,
nonce
};
workerPort.postMessage(event);
});
server.on("link", async (data: any, callback: LinkCallback) => {
const nonce = randomUUID();
linkCallbacks.set(nonce, callback);
const event: ArRpcEvent = {
type: "link",
data,
nonce
};
workerPort.postMessage(event);
});
workerPort.on("message", (e: ArRpcHostEvent) => {
switch (e.type) {
case "ack-invite": {
inviteCallbacks.get(e.nonce)?.(e.data);
inviteCallbacks.delete(e.nonce);
break;
}
case "ack-link": {
linkCallbacks.get(e.nonce)?.(e.data);
linkCallbacks.delete(e.nonce);
break;
}
}
});
})();

View File

@@ -1,62 +1,100 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
import { join } from "path";
import { stripIndent } from "shared/utils/text";
import { IS_FLATPAK } from "./constants";
import { requestBackground } from "./dbus";
import { Settings, State } from "./settings";
import { escapeDesktopFileArgument } from "./utils/desktopFileEscape";
interface AutoStart {
isEnabled(): boolean;
wasAutoStarted(): boolean;
enable(): void;
disable(): void;
}
const isFlatpak = process.env.FLATPAK_ID !== undefined;
function getEscapedCommandLine() {
const args = process.argv.map(escapeDesktopFileArgument);
if (Settings.store.autoStartMinimized) args.push("--start-minimized");
return args;
}
function makeAutoStartLinux(): AutoStart {
function makeAutoStartLinuxDesktop(): AutoStart {
const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config");
const dir = join(configDir, "autostart");
const file = join(dir, "vencord.desktop");
const file = join(dir, "vesktop.desktop");
return {
isEnabled: () => existsSync(file), // TODO: flatpak
wasAutoStarted: () => process.argv.includes("--autostart"),
isEnabled: () => existsSync(file),
enable() {
if (isFlatpak) {
} else {
const desktopFile = `
[Desktop Entry]
Type=Application
Version=1.0
Name=Vencord
Comment=Vencord autostart script
Exec=${process.execPath} --autostart
Terminal=false
StartupNotify=false
`.trim();
const desktopFile = stripIndent`
[Desktop Entry]
Type=Application
Name=Vesktop
Comment=Vesktop autostart script
Exec=${getEscapedCommandLine().join(" ")}
StartupNotify=false
Terminal=false
Icon=vesktop
`;
mkdirSync(dir, { recursive: true });
writeFileSync(file, desktopFile);
}
mkdirSync(dir, { recursive: true });
writeFileSync(file, desktopFile);
},
disable: () => {
if (isFlatpak) {
} else {
rmSync(file, { force: true });
disable: () => rmSync(file, { force: true })
};
}
function makeAutoStartLinuxPortal() {
return {
isEnabled: () => State.store.linuxAutoStartEnabled === true,
enable() {
const success = requestBackground(true, getEscapedCommandLine());
if (success) {
State.store.linuxAutoStartEnabled = true;
}
return success;
},
disable() {
const success = requestBackground(false, []);
if (success) {
State.store.linuxAutoStartEnabled = false;
}
return success;
}
};
}
const autoStartWindowsMac: AutoStart = {
isEnabled: () => app.getLoginItemSettings().openAtLogin,
wasAutoStarted: () => app.getLoginItemSettings().wasOpenedAtLogin,
enable: () => app.setLoginItemSettings({ openAtLogin: true }),
enable: () =>
app.setLoginItemSettings({
openAtLogin: true,
args: Settings.store.autoStartMinimized ? ["--start-minimized"] : []
}),
disable: () => app.setLoginItemSettings({ openAtLogin: false })
};
export const autoStart = process.platform === "linux" ? makeAutoStartLinux() : autoStartWindowsMac;
// The portal call uses the app id by default, which is org.chromium.Chromium, even in packaged Vesktop.
// This leads to an autostart entry named "Chromium" instead of "Vesktop".
// Thus, only use the portal inside Flatpak, where the app is actually correct.
// Maybe there is a way to fix it outside of flatpak, but I couldn't figure it out.
export const autoStart =
process.platform !== "linux"
? autoStartWindowsMac
: IS_FLATPAK
? makeAutoStartLinuxPortal()
: makeAutoStartLinuxDesktop();
Settings.addChangeListener("autoStartMinimized", () => {
if (!autoStart.isEnabled()) return;
autoStart.enable();
});

145
src/main/cli.ts Normal file
View File

@@ -0,0 +1,145 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
import { basename } from "path";
import { stripIndent } from "shared/utils/text";
import { parseArgs, ParseArgsOptionDescriptor } from "util";
type Option = ParseArgsOptionDescriptor & {
description: string;
hidden?: boolean;
options?: string[];
argumentName?: string;
};
const options = {
"start-minimized": {
default: false,
type: "boolean",
short: "m",
description: "Start the application minimized to the system tray"
},
version: {
type: "boolean",
short: "v",
description: "Print the application version and exit"
},
help: {
type: "boolean",
short: "h",
description: "Print help information and exit"
},
"user-agent": {
type: "string",
argumentName: "ua",
description: "Set a custom User-Agent. May trigger anti-spam or break voice chat"
},
"user-agent-os": {
type: "string",
description: "Set User-Agent to a specific operating system. May trigger anti-spam or break voice chat",
options: ["windows", "linux", "darwin"]
}
} satisfies Record<string, Option>;
// only for help display
const extraOptions = {
"enable-features": {
type: "string",
description: "Enable specific Chromium features",
argumentName: "feature1,feature2,…"
},
"disable-features": {
type: "string",
description: "Disable specific Chromium features",
argumentName: "feature1,feature2,…"
},
"ozone-platform": {
hidden: process.platform !== "linux",
type: "string",
description: "Whether to run Vesktop in Wayland or X11 (XWayland)",
options: ["x11", "wayland"]
}
} satisfies Record<string, Option>;
const args = basename(process.argv[0]).toLowerCase().startsWith("electron")
? process.argv.slice(2)
: process.argv.slice(1);
export const CommandLine = parseArgs({
args,
options,
strict: false as true, // we manually check later, so cast to true to get better types
allowPositionals: true
});
export function checkCommandLineForHelpOrVersion() {
const { help, version } = CommandLine.values;
if (version) {
console.log(`Vesktop v${app.getVersion()}`);
app.exit(0);
}
if (help) {
const base = stripIndent`
Vesktop v${app.getVersion()}
Usage: ${basename(process.execPath)} [options] [url]
Electron Options:
See <https://www.electronjs.org/docs/latest/api/command-line-switches#electron-cli-flags>
Chromium Options:
See <https://peter.sh/experiments/chromium-command-line-switches> - only some of them work
Vesktop Options:
`;
const optionLines = Object.entries(options)
.sort(([a], [b]) => a.localeCompare(b))
.concat(Object.entries(extraOptions))
.filter(([, opt]) => !("hidden" in opt && opt.hidden))
.map(([name, opt]) => {
const flags = [
"short" in opt && `-${opt.short}`,
`--${name}`,
opt.type !== "boolean" &&
("options" in opt ? `<${opt.options.join(" | ")}>` : `<${opt.argumentName ?? opt.type}>`)
]
.filter(Boolean)
.join(" ");
return [flags, opt.description];
});
const padding = optionLines.reduce((max, [flags]) => Math.max(max, flags.length), 0) + 4;
const optionsHelp = optionLines
.map(([flags, description]) => ` ${flags.padEnd(padding, " ")}${description}`)
.join("\n");
console.log(base + "\n" + optionsHelp);
app.exit(0);
}
for (const [name, def] of Object.entries(options)) {
const value = CommandLine.values[name];
if (value == null) continue;
if (typeof value !== def.type) {
console.error(`Invalid options. Expected ${def.type === "boolean" ? "no" : "an"} argument for --${name}`);
app.exit(1);
}
if ("options" in def && !def.options?.includes(value as string)) {
console.error(`Invalid value for --${name}: ${value}\nExpected one of: ${def.options.join(", ")}`);
app.exit(1);
}
}
}
checkCommandLineForHelpOrVersion();

View File

@@ -1,23 +1,36 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
import { join } from "path";
import { existsSync, mkdirSync } from "fs";
import { dirname, join } from "path";
import { CommandLine } from "./cli";
const vesktopDir = dirname(process.execPath);
export const PORTABLE =
process.platform === "win32" &&
!process.execPath.toLowerCase().endsWith("electron.exe") &&
!existsSync(join(vesktopDir, "Uninstall Vesktop.exe"));
export const DATA_DIR =
process.env.VENCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("userData")));
mkdirSync(DATA_DIR, { recursive: true });
export 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");
mkdirSync(VENCORD_SETTINGS_DIR, { recursive: true });
export const VENCORD_QUICKCSS_FILE = join(VENCORD_SETTINGS_DIR, "quickCss.css");
export const VENCORD_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json");
export const VENCORD_THEMES_DIR = join(DATA_DIR, "themes");
// needs to be inline require because of circular dependency
// as otherwise "DATA_DIR" (which is used by ./settings) will be uninitialised
export const VENCORD_FILES_DIR =
(require("./settings") as typeof import("./settings")).Settings.store.vencordDir || join(DATA_DIR, "vencordDist");
export const USER_AGENT = `Vesktop/${app.getVersion()} (https://github.com/Vencord/Vesktop)`;
// dimensions shamelessly stolen from Discord Desktop :3
@@ -26,16 +39,23 @@ export const MIN_HEIGHT = 500;
export const DEFAULT_WIDTH = 1280;
export const DEFAULT_HEIGHT = 720;
const UserAgents = {
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",
windows:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"];
const VersionString = `AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome.split(".")[0]}.0.0.0 Safari/537.36`;
const BrowserUserAgents = {
darwin: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ${VersionString}`,
linux: `Mozilla/5.0 (X11; Linux x86_64) ${VersionString}`,
windows: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) ${VersionString}`
};
export const UserAgent = UserAgents[process.platform] || UserAgents.windows;
export const BrowserUserAgent =
CommandLine.values["user-agent"] ||
BrowserUserAgents[CommandLine.values["user-agent-os"] || process.platform] ||
BrowserUserAgents.windows;
export const enum MessageBoxChoice {
Default,
Cancel
}
export const IS_FLATPAK = process.env.FLATPAK_ID !== undefined;

40
src/main/dbus.ts Normal file
View File

@@ -0,0 +1,40 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
import { join } from "path";
import { STATIC_DIR } from "shared/paths";
let libVesktop: typeof import("libvesktop") | null = null;
function loadLibVesktop() {
try {
if (!libVesktop) {
libVesktop = require(join(STATIC_DIR, `dist/libvesktop-${process.arch}.node`));
}
} catch (e) {
console.error("Failed to load libvesktop:", e);
}
return libVesktop;
}
export function getAccentColor() {
return loadLibVesktop()?.getAccentColor() ?? null;
}
export function updateUnityLauncherCount(count: number) {
const libVesktop = loadLibVesktop();
if (!libVesktop) {
return app.setBadgeCount(count);
}
return libVesktop.updateUnityLauncherCount(count);
}
export function requestBackground(autoStart: boolean, commandLine: string[]) {
return loadLibVesktop()?.requestBackground(autoStart, commandLine) ?? false;
}

15
src/main/events.ts Normal file
View File

@@ -0,0 +1,15 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EventEmitter } from "events";
import { UserAssetType } from "./userAssets";
export const AppEvents = new EventEmitter<{
appLoaded: [];
userAssetChanged: [UserAssetType];
setTrayVariant: ["tray" | "trayUnread"];
}>();

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
@@ -9,45 +9,45 @@ import { BrowserWindow } from "electron/main";
import { copyFileSync, mkdirSync, readdirSync } from "fs";
import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { ICON_PATH, VIEW_DIR } from "shared/paths";
import { autoStart } from "./autoStart";
import { DATA_DIR } from "./constants";
import { createWindows } from "./mainWindow";
import { Settings } from "./settings";
import { Settings, State } from "./settings";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { loadView } from "./vesktopStatic";
interface Data {
minimizeToTray: boolean;
discordBranch: "stable" | "canary" | "ptb";
autoStart: boolean;
importSettings: boolean;
richPresence: boolean;
minimizeToTray?: "on";
autoStart?: "on";
importSettings?: "on";
richPresence?: "on";
}
export function createFirstLaunchTour() {
const win = new BrowserWindow({
...SplashProps,
transparent: false,
frame: true,
autoHideMenuBar: true,
height: 470,
width: 550,
icon: ICON_PATH
height: 550,
width: 600
});
makeLinksOpenExternally(win);
win.loadFile(join(VIEW_DIR, "first-launch.html"));
loadView(win, "first-launch.html");
win.webContents.addListener("console-message", (_e, _l, msg) => {
if (msg === "cancel") return app.exit();
if (!msg.startsWith("form:")) return;
const data = JSON.parse(msg.slice(5)) as Data;
Settings.store.minimizeToTray = data.minimizeToTray;
State.store.firstLaunch = false;
Settings.store.discordBranch = data.discordBranch;
Settings.store.firstLaunch = false;
Settings.store.arRPC = data.richPresence;
Settings.store.minimizeToTray = !!data.minimizeToTray;
Settings.store.arRPC = !!data.richPresence;
if (data.autoStart) autoStart.enable();
@@ -62,7 +62,11 @@ export function createFirstLaunchTour() {
copyFileSync(join(from, file), join(to, file));
}
} catch (e) {
console.error("Failed to import settings:", e);
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
console.log("No Vencord settings found to import.");
} else {
console.error("Failed to import Vencord settings:", e);
}
}
}

View File

@@ -1,46 +1,105 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./cli";
import "./updater";
import "./ipc";
import "./userAssets";
import "./vesktopProtocol";
import { app, BrowserWindow } from "electron";
import { checkUpdates } from "updater/main";
import { app, BrowserWindow, nativeTheme } from "electron";
import { DATA_DIR } from "./constants";
import { createFirstLaunchTour } from "./firstLaunch";
import { createWindows, mainWin } from "./mainWindow";
import { registerMediaPermissionsHandler } from "./mediaPermissions";
import { registerScreenShareHandler } from "./screenShare";
import { Settings } from "./settings";
import { Settings, State } from "./settings";
import { setAsDefaultProtocolClient } from "./utils/setAsDefaultProtocolClient";
import { isDeckGameMode } from "./utils/steamOS";
if (IS_DEV) {
require("source-map-support").install();
}
console.log("Vesktop v" + app.getVersion());
// Make the Vencord files use our DATA_DIR
process.env.VENCORD_USER_DATA_DIR = DATA_DIR;
const isLinux = process.platform === "linux";
export let enableHardwareAcceleration = true;
function init() {
const { disableSmoothScroll } = Settings.store;
setAsDefaultProtocolClient("discord");
const { disableSmoothScroll, hardwareAcceleration, hardwareVideoAcceleration } = Settings.store;
const enabledFeatures = new Set(app.commandLine.getSwitchValue("enable-features").split(","));
const disabledFeatures = new Set(app.commandLine.getSwitchValue("disable-features").split(","));
app.commandLine.removeSwitch("enable-features");
app.commandLine.removeSwitch("disable-features");
if (hardwareAcceleration === false || process.argv.includes("--disable-gpu")) {
enableHardwareAcceleration = false;
app.disableHardwareAcceleration();
} else {
if (hardwareVideoAcceleration) {
enabledFeatures.add("AcceleratedVideoEncoder");
enabledFeatures.add("AcceleratedVideoDecoder");
if (isLinux) {
enabledFeatures.add("AcceleratedVideoDecodeLinuxGL");
enabledFeatures.add("AcceleratedVideoDecodeLinuxZeroCopyGL");
}
}
}
if (disableSmoothScroll) {
app.commandLine.appendSwitch("disable-smooth-scrolling");
}
// disable renderer backgrounding to prevent the app from unloading when in the background
// https://github.com/electron/electron/issues/2822
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
app.commandLine.appendSwitch("disable-renderer-backgrounding");
app.commandLine.appendSwitch("disable-background-timer-throttling");
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
if (process.platform === "win32") {
disabledFeatures.add("CalculateNativeWinOcclusion");
}
// work around chrome 66 disabling autoplay by default
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
// WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows.
// HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service.
//
// WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790
app.commandLine.appendSwitch(
"disable-features",
"WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService,WidgetLayering"
);
// HardwareMediaKeyHandling, MediaSessionService: Prevent Discord from registering as a media service.
disabledFeatures.add("WinRetrieveSuggestionsOnlyOnDemand");
disabledFeatures.add("HardwareMediaKeyHandling");
disabledFeatures.add("MediaSessionService");
if (isLinux) {
// Support TTS on Linux using https://wiki.archlinux.org/title/Speech_dispatcher
app.commandLine.appendSwitch("enable-speech-dispatcher");
}
disabledFeatures.forEach(feat => enabledFeatures.delete(feat));
const enabledFeaturesArray = enabledFeatures.values().filter(Boolean).toArray();
const disabledFeaturesArray = disabledFeatures.values().filter(Boolean).toArray();
if (enabledFeaturesArray.length) {
app.commandLine.appendSwitch("enable-features", enabledFeaturesArray.join(","));
console.log("Enabled Chromium features:", enabledFeaturesArray.join(", "));
}
if (disabledFeaturesArray.length) {
app.commandLine.appendSwitch("disable-features", disabledFeaturesArray.join(","));
console.log("Disabled Chromium features:", disabledFeaturesArray.join(", "));
}
// In the Flatpak on SteamOS the theme is detected as light, but SteamOS only has a dark mode, so we just override it
if (isDeckGameMode) nativeTheme.themeSource = "dark";
app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => {
if (data.IS_DEV) app.quit();
@@ -52,8 +111,7 @@ function init() {
});
app.whenReady().then(async () => {
checkUpdates();
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.desktop");
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.vesktop");
registerScreenShareHandler();
registerMediaPermissionsHandler();
@@ -79,13 +137,19 @@ if (!app.requestSingleInstanceLock({ IS_DEV })) {
}
async function bootstrap() {
if (!Object.hasOwn(Settings.store, "firstLaunch")) {
if (!Object.hasOwn(State.store, "firstLaunch")) {
createFirstLaunchTour();
} else {
createWindows();
}
}
// MacOS only event
export let darwinURL: string | undefined;
app.on("open-url", (_, url) => {
darwinURL = url;
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});

View File

@@ -1,15 +1,26 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
if (process.platform === "linux") import("./virtmic");
if (process.platform === "linux") import("./venmic");
import { execFile } from "child_process";
import { app, BrowserWindow, dialog, RelaunchOptions, session, shell } from "electron";
import {
app,
BrowserWindow,
clipboard,
dialog,
IpcMainInvokeEvent,
nativeImage,
RelaunchOptions,
session,
shell
} from "electron";
import { mkdirSync, readFileSync, watch } from "fs";
import { open, readFile } from "fs/promises";
import { enableHardwareAcceleration } from "main";
import { release } from "os";
import { join } from "path";
import { debounce } from "shared/utils/debounce";
@@ -17,11 +28,14 @@ import { debounce } from "shared/utils/debounce";
import { IpcEvents } from "../shared/IpcEvents";
import { setBadgeCount } from "./appBadge";
import { autoStart } from "./autoStart";
import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
import { VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
import { mainWin } from "./mainWindow";
import { Settings } from "./settings";
import { Settings, State } from "./settings";
import { handle, handleSync } from "./utils/ipcWrappers";
import { PopoutWindows } from "./utils/popout";
import { isDeckGameMode, showGamePage } from "./utils/steamOS";
import { isValidVencordInstall } from "./utils/vencordLoader";
import { VENCORD_FILES_DIR } from "./vencordFilesDir";
handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload.js"));
handleSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT, () =>
@@ -33,6 +47,7 @@ handleSync(IpcEvents.GET_RENDERER_CSS_FILE, () => join(__dirname, "renderer.css"
handleSync(IpcEvents.GET_SETTINGS, () => Settings.plain);
handleSync(IpcEvents.GET_VERSION, () => app.getVersion());
handleSync(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION, () => enableHardwareAcceleration);
handleSync(
IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY,
@@ -47,11 +62,14 @@ handle(IpcEvents.SET_SETTINGS, (_, settings: typeof Settings.store, path?: strin
Settings.setData(settings, path);
});
handle(IpcEvents.RELAUNCH, () => {
handle(IpcEvents.RELAUNCH, async () => {
const options: RelaunchOptions = {
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);
} else {
app.relaunch(options);
@@ -63,35 +81,34 @@ handle(IpcEvents.SHOW_ITEM_IN_FOLDER, (_, path) => {
shell.showItemInFolder(path);
});
function getWindow(e: IpcMainInvokeEvent, key?: string) {
return key ? PopoutWindows.get(key)! : (BrowserWindow.fromWebContents(e.sender) ?? mainWin);
}
handle(IpcEvents.FOCUS, () => {
if (process.platform === "win32") mainWin.minimize(); // Windows is weird
mainWin.restore();
mainWin.show();
mainWin.setSkipTaskbar(false);
});
handle(IpcEvents.CLOSE, e => {
(BrowserWindow.fromWebContents(e.sender) ?? e.sender).close();
handle(IpcEvents.CLOSE, (e, key?: string) => {
getWindow(e, key).close();
});
handle(IpcEvents.MINIMIZE, e => {
mainWin.minimize();
handle(IpcEvents.MINIMIZE, (e, key?: string) => {
getWindow(e, key).minimize();
});
handle(IpcEvents.MAXIMIZE, e => {
if (mainWin.isMaximized()) {
mainWin.unmaximize();
handle(IpcEvents.MAXIMIZE, (e, key?: string) => {
const win = getWindow(e, key);
if (win.isMaximized()) {
win.unmaximize();
} else {
mainWin.maximize();
win.maximize();
}
});
handle(IpcEvents.SPELLCHECK_SET_LANGUAGES, (_, languages: string[]) => {
const ses = session.defaultSession;
const available = ses.availableSpellCheckerLanguages;
const applicable = languages.filter(l => available.includes(l)).slice(0, 3);
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
handleSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES, e => {
e.returnValue = session.defaultSession.availableSpellCheckerLanguages;
});
handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => {
@@ -102,7 +119,14 @@ handle(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, (e, word: string) => {
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!, {
properties: ["openDirectory"]
});
@@ -111,11 +135,36 @@ handle(IpcEvents.SELECT_VENCORD_DIR, async () => {
const dir = res.filePaths[0];
if (!isValidVencordInstall(dir)) return "invalid";
return dir;
State.store.vencordDir = dir;
return "ok";
});
handle(IpcEvents.SET_BADGE_COUNT, (_, count: number) => setBadgeCount(count));
handle(IpcEvents.FLASH_FRAME, (_, flag: boolean) => {
if (!mainWin || mainWin.isDestroyed() || (flag && mainWin.isFocused())) return;
mainWin.flashFrame(flag);
});
handle(IpcEvents.CLIPBOARD_COPY_IMAGE, async (_, buf: ArrayBuffer, src: string) => {
clipboard.write({
html: `<img src="${src.replaceAll('"', '\\"')}">`,
image: nativeImage.createFromBuffer(Buffer.from(buf))
});
});
function openDebugPage(page: string) {
const win = new BrowserWindow({
autoHideMenuBar: true
});
win.loadURL(page);
}
handle(IpcEvents.DEBUG_LAUNCH_GPU, () => openDebugPage("chrome://gpu"));
handle(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS, () => openDebugPage("chrome://webrtc-internals"));
function readCss() {
return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => "");
}

61
src/main/ipcCommands.ts Normal file
View File

@@ -0,0 +1,61 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { randomUUID } from "crypto";
import { ipcMain } from "electron";
import { IpcEvents } from "shared/IpcEvents";
import { mainWin } from "./mainWindow";
const resolvers = new Map<string, Record<"resolve" | "reject", (data: any) => void>>();
export interface IpcMessage {
nonce: string;
message: string;
data?: any;
}
export interface IpcResponse {
nonce: string;
ok: boolean;
data?: any;
}
/**
* Sends a message to the renderer process and waits for a response.
* `data` must be serializable as it will be sent over IPC.
*
* You must add a handler for the message in the renderer process.
*/
export function sendRendererCommand<T = any>(message: string, data?: any) {
if (mainWin.isDestroyed()) {
console.warn("Main window is destroyed, cannot send IPC command:", message);
return Promise.reject(new Error("Main window is destroyed"));
}
const nonce = randomUUID();
const promise = new Promise<T>((resolve, reject) => {
resolvers.set(nonce, { resolve, reject });
});
mainWin.webContents.send(IpcEvents.IPC_COMMAND, { nonce, message, data });
return promise;
}
ipcMain.on(IpcEvents.IPC_COMMAND, (_event, { nonce, ok, data }: IpcResponse) => {
const resolver = resolvers.get(nonce);
if (!resolver) throw new Error(`Unknown message: ${nonce}`);
if (ok) {
resolver.resolve(data);
} else {
resolver.reject(data);
}
resolvers.delete(nonce);
});

View File

@@ -1,48 +1,43 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import {
app,
BrowserWindow,
BrowserWindowConstructorOptions,
dialog,
Menu,
MenuItemConstructorOptions,
nativeTheme,
Tray
Rectangle,
screen,
session
} from "electron";
import { rm } from "fs/promises";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { IpcCommands, IpcEvents } from "shared/IpcEvents";
import { isTruthy } from "shared/utils/guards";
import { once } from "shared/utils/once";
import type { SettingsStore } from "shared/utils/SettingsStore";
import { ICON_PATH } from "../shared/paths";
import { createAboutWindow } from "./about";
import { initArRPC } from "./arrpc";
import { autoStart } from "./autoStart";
import {
DATA_DIR,
DEFAULT_HEIGHT,
DEFAULT_WIDTH,
MessageBoxChoice,
MIN_HEIGHT,
MIN_WIDTH,
UserAgent,
VENCORD_FILES_DIR
} from "./constants";
import { Settings, VencordSettings } from "./settings";
import { createSplashWindow } from "./splash";
import { CommandLine } from "./cli";
import { BrowserUserAgent, DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH } from "./constants";
import { AppEvents } from "./events";
import { darwinURL } from "./index";
import { sendRendererCommand } from "./ipcCommands";
import { Settings, State, VencordSettings } from "./settings";
import { createSplashWindow, updateSplashMessage } from "./splash";
import { destroyTray, initTray } from "./tray";
import { clearData } from "./utils/clearData";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS";
import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
import { VENCORD_FILES_DIR } from "./vencordFilesDir";
let isQuitting = false;
let tray: Tray;
applyDeckKeyboardFix();
@@ -73,89 +68,6 @@ function makeSettingsListenerHelpers<O extends object>(o: SettingsStore<O>) {
const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpers(Settings);
const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings);
function initTray(win: BrowserWindow) {
const trayMenu = Menu.buildFromTemplate([
{
label: "Open",
click() {
win.show();
},
enabled: false
},
{
label: "About",
click: createAboutWindow
},
{
label: "Update Vencord",
async click() {
await downloadVencordFiles();
app.relaunch();
app.quit();
}
},
{
label: "Reset Vesktop",
async click() {
await clearData(win);
}
},
{
type: "separator"
},
{
label: "Relaunch",
click() {
app.relaunch();
app.quit();
}
},
{
label: "Quit Vesktop",
click() {
isQuitting = true;
app.quit();
}
}
]);
tray = new Tray(ICON_PATH);
tray.setToolTip("Vesktop");
tray.setContextMenu(trayMenu);
tray.on("click", () => win.show());
win.on("show", () => {
trayMenu.items[0].enabled = false;
});
win.on("hide", () => {
trayMenu.items[0].enabled = true;
});
}
async function clearData(win: BrowserWindow) {
const { response } = await dialog.showMessageBox(win, {
message: "Are you sure you want to reset Vesktop?",
detail: "This will log you out, clear caches and reset all your settings!\n\nVesktop will automatically restart after this operation.",
buttons: ["Yes", "No"],
cancelId: MessageBoxChoice.Cancel,
defaultId: MessageBoxChoice.Default,
type: "warning"
});
if (response === MessageBoxChoice.Cancel) return;
win.close();
await win.webContents.session.clearStorageData();
await win.webContents.session.clearCache();
await win.webContents.session.clearCodeCaches({});
await rm(DATA_DIR, { force: true, recursive: true });
app.relaunch();
app.quit();
}
type MenuItemList = Array<MenuItemConstructorOptions | false>;
function initMenuBar(win: BrowserWindow) {
@@ -202,16 +114,13 @@ function initMenuBar(win: BrowserWindow) {
label: "Settings",
accelerator: "CmdOrCtrl+,",
async click() {
mainWin.webContents.executeJavaScript(
"Vencord.Webpack.Common.SettingsRouter.open('My Account')"
);
sendRendererCommand(IpcCommands.NAVIGATE_SETTINGS);
}
},
{
type: "separator"
},
{
label: "Hide Vesktop", // Should probably remove the label, but it says "Hide VencordDesktop" instead of "Hide Vesktop"
role: "hide"
},
{
@@ -250,7 +159,7 @@ function initMenuBar(win: BrowserWindow) {
}
] satisfies MenuItemList;
const menu = Menu.buildFromTemplate([
const menuItems = [
{
label: "Vesktop",
role: "appMenu",
@@ -259,63 +168,18 @@ function initMenuBar(win: BrowserWindow) {
{ role: "fileMenu" },
{ role: "editMenu" },
{ role: "viewMenu" },
{ role: "windowMenu" }
]);
isDarwin && { role: "windowMenu" }
] satisfies MenuItemList;
const menu = Menu.buildFromTemplate(menuItems.filter(isTruthy));
Menu.setApplicationMenu(menu);
}
function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
// We want the default window behaivour to apply in game mode since it expects everything to be fullscreen and maximized.
if (isDeckGameMode) return {};
const { x, y, width, height } = Settings.store.windowBounds ?? {};
const options = {
width: width ?? DEFAULT_WIDTH,
height: height ?? DEFAULT_HEIGHT
} as BrowserWindowConstructorOptions;
if (x != null && y != null) {
options.x = x;
options.y = y;
}
if (!Settings.store.disableMinSize) {
options.minWidth = MIN_WIDTH;
options.minHeight = MIN_HEIGHT;
}
return options;
}
function getDarwinOptions(): BrowserWindowConstructorOptions {
const options = {
titleBarStyle: "hidden",
trafficLightPosition: { x: 10, y: 10 }
} as BrowserWindowConstructorOptions;
const { splashTheming, splashBackground } = Settings.store;
const { macosTranslucency } = VencordSettings.store;
if (macosTranslucency) {
options.vibrancy = "sidebar";
options.backgroundColor = "#ffffff00";
} else {
if (splashTheming) {
options.backgroundColor = splashBackground;
} else {
options.backgroundColor = nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
}
}
return options;
}
function initWindowBoundsListeners(win: BrowserWindow) {
const saveState = () => {
Settings.store.maximized = win.isMaximized();
Settings.store.minimized = win.isMinimized();
State.store.maximized = win.isMaximized();
State.store.minimized = win.isMinimized();
};
win.on("maximize", saveState);
@@ -323,7 +187,7 @@ function initWindowBoundsListeners(win: BrowserWindow) {
win.on("unmaximize", saveState);
const saveBounds = () => {
Settings.store.windowBounds = win.getBounds();
State.store.windowBounds = win.getBounds();
};
win.on("resize", saveBounds);
@@ -332,9 +196,10 @@ function initWindowBoundsListeners(win: BrowserWindow) {
function initSettingsListeners(win: BrowserWindow) {
addSettingsListener("tray", enable => {
if (enable) initTray(win);
else tray?.destroy();
if (enable) initTray(win, q => (isQuitting = q));
else destroyTray();
});
addSettingsListener("disableMinSize", disable => {
if (disable) {
// 0 no work
@@ -363,12 +228,143 @@ function initSettingsListeners(win: BrowserWindow) {
addSettingsListener("enableMenu", enabled => {
win.setAutoHideMenuBar(enabled ?? false);
});
addSettingsListener("spellCheckLanguages", languages => initSpellCheckLanguages(win, languages));
}
async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) {
languages ??= await sendRendererCommand(IpcCommands.GET_LANGUAGES);
if (!languages) return;
const ses = session.defaultSession;
const available = ses.availableSpellCheckerLanguages;
const applicable = languages.filter(l => available.includes(l)).slice(0, 5);
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
}
function initSpellCheck(win: BrowserWindow) {
win.webContents.on("context-menu", (_, data) => {
win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions);
});
initSpellCheckLanguages(win, Settings.store.spellCheckLanguages);
}
function initDevtoolsListeners(win: BrowserWindow) {
win.webContents.on("devtools-opened", () => {
win.webContents.send(IpcEvents.DEVTOOLS_OPENED);
});
win.webContents.on("devtools-closed", () => {
win.webContents.send(IpcEvents.DEVTOOLS_CLOSED);
});
}
function initStaticTitle(win: BrowserWindow) {
const listener = (e: { preventDefault: Function }) => e.preventDefault();
if (Settings.store.staticTitle) win.on("page-title-updated", listener);
addSettingsListener("staticTitle", enabled => {
if (enabled) {
win.setTitle("Vesktop");
win.on("page-title-updated", listener);
} else {
win.off("page-title-updated", listener);
}
});
}
function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
// We want the default window behaviour to apply in game mode since it expects everything to be fullscreen and maximized.
if (isDeckGameMode) return {};
const { x, y, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = State.store.windowBounds ?? {};
const options = { width, height } as BrowserWindowConstructorOptions;
if (x != null && y != null) {
function isInBounds(rect: Rectangle, display: Rectangle) {
return !(
rect.x + rect.width < display.x ||
rect.x > display.x + display.width ||
rect.y + rect.height < display.y ||
rect.y > display.y + display.height
);
}
const inBounds = screen.getAllDisplays().some(d => isInBounds({ x, y, width, height }, d.bounds));
if (inBounds) {
options.x = x;
options.y = y;
}
}
if (!Settings.store.disableMinSize) {
options.minWidth = MIN_WIDTH;
options.minHeight = MIN_HEIGHT;
}
return options;
}
function buildBrowserWindowOptions(): BrowserWindowConstructorOptions {
const { staticTitle, transparencyOption, enableMenu, customTitleBar, splashTheming, splashBackground } =
Settings.store;
const { frameless, transparent, macosVibrancyStyle } = VencordSettings.store;
const noFrame = frameless === true || customTitleBar === true;
const backgroundColor =
splashTheming !== false ? splashBackground : nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
const options: BrowserWindowConstructorOptions = {
show: Settings.store.enableSplashScreen === false && !CommandLine.values["start-minimized"],
backgroundColor,
webPreferences: {
nodeIntegration: false,
sandbox: false, // TODO
contextIsolation: true,
devTools: true,
preload: join(__dirname, "preload.js"),
spellcheck: true,
// disable renderer backgrounding to prevent the app from unloading when in the background
backgroundThrottling: false
},
frame: !noFrame,
autoHideMenuBar: enableMenu,
...getWindowBoundsOptions()
};
if (transparent) {
options.transparent = true;
options.backgroundColor = "#00000000";
}
if (transparencyOption && transparencyOption !== "none") {
options.backgroundColor = "#00000000";
options.backgroundMaterial = transparencyOption;
if (customTitleBar) {
options.transparent = true;
}
}
if (staticTitle) {
options.title = "Vesktop";
}
if (process.platform === "darwin") {
options.titleBarStyle = "hidden";
options.trafficLightPosition = { x: 10, y: 10 };
if (macosVibrancyStyle) {
options.vibrancy = macosVibrancyStyle;
options.backgroundColor = "#00000000";
}
}
return options;
}
function createMainWindow() {
@@ -376,36 +372,10 @@ function createMainWindow() {
removeSettingsListeners();
removeVencordSettingsListeners();
const { staticTitle, transparencyOption, enableMenu, discordWindowsTitleBar } = Settings.store;
const win = (mainWin = new BrowserWindow(buildBrowserWindowOptions()));
const { frameless } = VencordSettings.store;
const noFrame = frameless === true || (process.platform === "win32" && discordWindowsTitleBar === true);
const win = (mainWin = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: false,
sandbox: false,
contextIsolation: true,
devTools: true,
preload: join(__dirname, "preload.js"),
spellcheck: true
},
icon: ICON_PATH,
frame: !noFrame,
...(transparencyOption &&
transparencyOption !== "none" && {
backgroundColor: "#00000000",
backgroundMaterial: transparencyOption,
transparent: true
}),
...(staticTitle && { title: "Vesktop" }),
...(process.platform === "darwin" && getDarwinOptions()),
...getWindowBoundsOptions(),
autoHideMenuBar: enableMenu
}));
win.setMenuBarVisibility(false);
if (process.platform === "darwin" && Settings.store.customTitleBar) win.setWindowButtonVisibility(false);
win.on("close", e => {
const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false;
@@ -419,43 +389,74 @@ function createMainWindow() {
return false;
});
if (Settings.store.staticTitle) win.on("page-title-updated", e => e.preventDefault());
win.on("focus", () => {
win.flashFrame(false);
});
initWindowBoundsListeners(win);
if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win);
if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin")
initTray(win, q => (isQuitting = q));
initMenuBar(win);
makeLinksOpenExternally(win);
initSettingsListeners(win);
initSpellCheck(win);
initDevtoolsListeners(win);
initStaticTitle(win);
win.webContents.setUserAgent(UserAgent);
win.webContents.setUserAgent(BrowserUserAgent);
const subdomain =
Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb"
? `${Settings.store.discordBranch}.`
: "";
win.loadURL(`https://${subdomain}discord.com/app`);
// if the open-url event is fired (in index.ts) while starting up, darwinURL will be set. If not fall back to checking the process args (which Windows and Linux use for URI calling.)
// win.webContents.session.clearCache().then(() => {
loadUrl(darwinURL || process.argv.find(arg => arg.startsWith("discord://")));
// });
return win;
}
const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js")));
export function loadUrl(uri: string | undefined) {
const branch = Settings.store.discordBranch;
const subdomain = branch === "canary" || branch === "ptb" ? `${branch}.` : "";
// we do not rely on 'did-finish-load' because it fires even if loadURL fails which triggers early detruction of the splash
mainWin
.loadURL(`https://${subdomain}discord.com/${uri ? new URL(uri).pathname.slice(1) || "app" : "app"}`)
.then(() => AppEvents.emit("appLoaded"))
.catch(error => retryUrl(error.url, error.code));
}
const retryDelay = 1000;
function retryUrl(url: string, description: string) {
console.log(`retrying in ${retryDelay}ms`);
updateSplashMessage(`Failed to load Discord: ${description}`);
setTimeout(() => loadUrl(url), retryDelay);
}
export async function createWindows() {
const shouldStartMinimized = Settings.store.startMinimized && autoStart.wasAutoStarted();
const splash = createSplashWindow(shouldStartMinimized);
// SteamOS letterboxes and scales it terribly, so just full screen it
if (isDeckGameMode) splash.setFullScreen(true);
const startMinimized = CommandLine.values["start-minimized"];
let splash: BrowserWindow | undefined;
if (Settings.store.enableSplashScreen !== false) {
splash = createSplashWindow(startMinimized);
// SteamOS letterboxes and scales it terribly, so just full screen it
if (isDeckGameMode) splash.setFullScreen(true);
}
await ensureVencordFiles();
runVencordMain();
mainWin = createMainWindow();
mainWin.webContents.on("did-finish-load", () => {
splash.destroy();
AppEvents.on("appLoaded", () => {
splash?.destroy();
if (!shouldStartMinimized || isDeckGameMode) mainWin!.show();
if (!startMinimized) {
if (splash) mainWin!.show();
if (State.store.maximized && !isDeckGameMode) mainWin!.maximize();
}
if (isDeckGameMode) {
// always use entire display
@@ -463,11 +464,21 @@ export async function createWindows() {
askToApplySteamLayout(mainWin);
}
mainWin.once("show", () => {
if (State.store.maximized && !mainWin!.isMaximized() && !isDeckGameMode) {
mainWin!.maximize();
}
});
});
mainWin.once("show", () => {
if (Settings.store.maximized && !mainWin!.isMaximized() && !isDeckGameMode) {
mainWin!.maximize();
mainWin.webContents.on("did-navigate", (_, url: string, responseCode: number) => {
updateSplashMessage(""); // clear the splash message
// check url to ensure app doesn't loop
if (responseCode >= 300 && new URL(url).pathname !== `/app`) {
loadUrl(undefined);
console.warn(`'did-navigate': Caught bad page response: ${responseCode}, redirecting to main app`);
}
});

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { session, systemPreferences } from "electron";
@@ -12,11 +12,13 @@ export function registerMediaPermissionsHandler() {
session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => {
let granted = true;
if (details.mediaTypes?.includes("audio")) {
granted = await systemPreferences.askForMediaAccess("microphone");
}
if (details.mediaTypes?.includes("video")) {
granted &&= await systemPreferences.askForMediaAccess("camera");
if ("mediaTypes" in details) {
if (details.mediaTypes?.includes("audio")) {
granted &&= await systemPreferences.askForMediaAccess("microphone");
}
if (details.mediaTypes?.includes("video")) {
granted &&= await systemPreferences.askForMediaAccess("camera");
}
}
callback(granted);

View File

@@ -1,13 +1,14 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { desktopCapturer, session, Streams } from "electron";
import type { StreamPick } from "renderer/components/ScreenSharePicker";
import { IpcEvents } from "shared/IpcEvents";
import { IpcCommands, IpcEvents } from "shared/IpcEvents";
import { sendRendererCommand } from "./ipcCommands";
import { handle } from "./utils/ipcWrappers";
const isWayland =
@@ -49,11 +50,11 @@ export function registerScreenShareHandler() {
if (isWayland) {
const video = data[0];
if (video) {
const stream = await request.frame
.executeJavaScript(
`Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify([video])},true)`
)
.catch(() => null);
const stream = await sendRendererCommand<StreamPick>(IpcCommands.SCREEN_SHARE_PICKER, {
screens: [video],
skipPicker: true
}).catch(() => null);
if (stream === null) return callback({});
}
@@ -61,13 +62,13 @@ export function registerScreenShareHandler() {
return;
}
const choice = await request.frame
.executeJavaScript(`Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify(data)})`)
.then(e => e as StreamPick)
.catch(e => {
console.error("Error during screenshare picker", e);
return null;
});
const choice = await sendRendererCommand<StreamPick>(IpcCommands.SCREEN_SHARE_PICKER, {
screens: data,
skipPicker: false
}).catch(e => {
console.error("Error during screenshare picker", e);
return null;
});
if (!choice) return callback({});

View File

@@ -1,17 +1,19 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { type Settings as TVencordSettings } from "@vencord/types/Vencord";
import { mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import type { Settings as TSettings } from "shared/settings";
import type { Settings as TSettings, State as TState } from "shared/settings";
import { SettingsStore } from "shared/utils/SettingsStore";
import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants";
const SETTINGS_FILE = join(DATA_DIR, "settings.json");
const STATE_FILE = join(DATA_DIR, "state.json");
function loadSettings<T extends object = any>(file: string, name: string) {
let settings = {} as T;
@@ -20,18 +22,23 @@ function loadSettings<T extends object = any>(file: string, name: string) {
try {
settings = JSON.parse(content);
} catch (err) {
console.error(`Failed to parse ${name} settings.json:`, err);
console.error(`Failed to parse ${name}.json:`, err);
}
} catch {}
const store = new SettingsStore(settings);
store.addGlobalChangeListener(o => {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, JSON.stringify(o, null, 4));
try {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, JSON.stringify(o, null, 4));
} catch (err) {
console.error(`Failed to save settings to ${name}.json:`, err);
}
});
return store;
}
export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop");
export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord");
export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop settings");
export const VencordSettings = loadSettings<TVencordSettings>(VENCORD_SETTINGS_FILE, "Vencord settings");
export const State = loadSettings<TState>(STATE_FILE, "Vesktop state");

View File

@@ -1,28 +1,32 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BrowserWindow } from "electron";
import { join } from "path";
import { SplashProps } from "shared/browserWinProperties";
import { ICON_PATH, VIEW_DIR } from "shared/paths";
import { Settings } from "./settings";
import { loadView } from "./vesktopStatic";
let splash: BrowserWindow | undefined;
export function createSplashWindow(startMinimized = false) {
const splash = new BrowserWindow({
splash = new BrowserWindow({
...SplashProps,
icon: ICON_PATH,
show: !startMinimized
show: !startMinimized,
webPreferences: {
preload: join(__dirname, "splashPreload.js")
}
});
splash.loadFile(join(VIEW_DIR, "splash.html"));
loadView(splash, "splash.html");
const { splashBackground, splashColor, splashTheming } = Settings.store;
const { splashBackground, splashColor, splashTheming, splashPixelated } = Settings.store;
if (splashTheming) {
if (splashTheming !== false) {
if (splashColor) {
const semiTransparentSplashColor = splashColor.replace("rgb(", "rgba(").replace(")", ", 0.2)");
@@ -35,5 +39,13 @@ export function createSplashWindow(startMinimized = false) {
}
}
if (splashPixelated) {
splash.webContents.insertCSS(`img { image-rendering: pixelated; }`);
}
return splash;
}
export function updateSplashMessage(message: string) {
if (splash && !splash.isDestroyed()) splash.webContents.send("update-splash-message", message);
}

92
src/main/tray.ts Normal file
View File

@@ -0,0 +1,92 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, BrowserWindow, Menu, Tray } from "electron";
import { createAboutWindow } from "./about";
import { AppEvents } from "./events";
import { Settings } from "./settings";
import { resolveAssetPath } from "./userAssets";
import { clearData } from "./utils/clearData";
import { downloadVencordFiles } from "./utils/vencordLoader";
let tray: Tray;
let trayVariant: "tray" | "trayUnread" = "tray";
AppEvents.on("userAssetChanged", async asset => {
if (tray && (asset === "tray" || asset === "trayUnread")) {
tray.setImage(await resolveAssetPath(trayVariant));
}
});
AppEvents.on("setTrayVariant", async variant => {
if (trayVariant === variant) return;
trayVariant = variant;
if (!tray) return;
tray.setImage(await resolveAssetPath(trayVariant));
});
export function destroyTray() {
tray?.destroy();
}
export async function initTray(win: BrowserWindow, setIsQuitting: (val: boolean) => void) {
const onTrayClick = () => {
if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide();
else win.show();
};
const trayMenu = Menu.buildFromTemplate([
{
label: "Open",
click() {
win.show();
}
},
{
label: "About",
click: createAboutWindow
},
{
label: "Repair Vencord",
async click() {
await downloadVencordFiles();
app.relaunch();
app.quit();
}
},
{
label: "Reset Vesktop",
async click() {
await clearData(win);
}
},
{
type: "separator"
},
{
label: "Restart",
click() {
app.relaunch();
app.quit();
}
},
{
label: "Quit",
click() {
setIsQuitting(true);
app.quit();
}
}
]);
tray = new Tray(await resolveAssetPath(trayVariant));
tray.setToolTip("Vesktop");
tray.setContextMenu(trayMenu);
tray.on("click", onTrayClick);
}

81
src/main/updater.ts Normal file
View File

@@ -0,0 +1,81 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, BrowserWindow, ipcMain } from "electron";
import { autoUpdater, UpdateInfo } from "electron-updater";
import { join } from "path";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
import { Millis } from "shared/utils/millis";
import { State } from "./settings";
import { handle } from "./utils/ipcWrappers";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { loadView } from "./vesktopStatic";
let updaterWindow: BrowserWindow | null = null;
autoUpdater.on("update-available", update => {
if (State.store.updater?.ignoredVersion === update.version) return;
if ((State.store.updater?.snoozeUntil ?? 0) > Date.now()) return;
openUpdater(update);
});
autoUpdater.on("update-downloaded", () => setTimeout(() => autoUpdater.quitAndInstall(), 100));
autoUpdater.on("download-progress", p =>
updaterWindow?.webContents.send(UpdaterIpcEvents.DOWNLOAD_PROGRESS, p.percent)
);
autoUpdater.on("error", err => updaterWindow?.webContents.send(UpdaterIpcEvents.ERROR, err.message));
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.fullChangelog = true;
const isOutdated = autoUpdater.checkForUpdates().then(res => Boolean(res?.isUpdateAvailable));
handle(IpcEvents.UPDATER_IS_OUTDATED, () => isOutdated);
handle(IpcEvents.UPDATER_OPEN, async () => {
const res = await autoUpdater.checkForUpdates();
if (res?.isUpdateAvailable && res.updateInfo) openUpdater(res.updateInfo);
});
function openUpdater(update: UpdateInfo) {
updaterWindow = new BrowserWindow({
title: "Vesktop Updater",
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, "updaterPreload.js")
},
minHeight: 400,
minWidth: 750
});
makeLinksOpenExternally(updaterWindow);
handle(UpdaterIpcEvents.GET_DATA, () => ({ update, version: app.getVersion() }));
handle(UpdaterIpcEvents.INSTALL, async () => {
await autoUpdater.downloadUpdate();
});
handle(UpdaterIpcEvents.SNOOZE_UPDATE, () => {
State.store.updater ??= {};
State.store.updater.snoozeUntil = Date.now() + 1 * Millis.DAY;
updaterWindow?.close();
});
handle(UpdaterIpcEvents.IGNORE_UPDATE, () => {
State.store.updater ??= {};
State.store.updater.ignoredVersion = update.version;
updaterWindow?.close();
});
updaterWindow.on("closed", () => {
ipcMain.removeHandler(UpdaterIpcEvents.GET_DATA);
ipcMain.removeHandler(UpdaterIpcEvents.INSTALL);
ipcMain.removeHandler(UpdaterIpcEvents.SNOOZE_UPDATE);
ipcMain.removeHandler(UpdaterIpcEvents.IGNORE_UPDATE);
updaterWindow = null;
});
loadView(updaterWindow, "updater/index.html");
}

101
src/main/userAssets.ts Normal file
View File

@@ -0,0 +1,101 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, dialog, net } from "electron";
import { copyFile, mkdir, rm } from "fs/promises";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths";
import { pathToFileURL } from "url";
import { DATA_DIR } from "./constants";
import { AppEvents } from "./events";
import { mainWin } from "./mainWindow";
import { fileExistsAsync } from "./utils/fileExists";
import { handle } from "./utils/ipcWrappers";
const CUSTOMIZABLE_ASSETS = ["splash", "tray", "trayUnread"] as const;
export type UserAssetType = (typeof CUSTOMIZABLE_ASSETS)[number];
const DEFAULT_ASSETS: Record<UserAssetType, string> = {
splash: "splash.webp",
tray: `tray/${process.platform === "darwin" ? "trayTemplate" : "tray"}.png`,
trayUnread: "tray/trayUnread.png"
};
const UserAssetFolder = join(DATA_DIR, "userAssets");
export async function resolveAssetPath(asset: UserAssetType) {
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
throw new Error(`Invalid asset: ${asset}`);
}
const assetPath = join(UserAssetFolder, asset);
if (await fileExistsAsync(assetPath)) {
return assetPath;
}
return join(STATIC_DIR, DEFAULT_ASSETS[asset]);
}
export async function handleVesktopAssetsProtocol(path: string, req: Request) {
const asset = path.slice(1);
// @ts-expect-error dumb types
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
return new Response(null, { status: 404 });
}
try {
const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href);
if (res.ok) return res;
} catch {}
return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href);
}
handle(IpcEvents.CHOOSE_USER_ASSET, async (_event, asset: UserAssetType, value?: null) => {
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
throw `Invalid asset: ${asset}`;
}
const assetPath = join(UserAssetFolder, asset);
if (value === null) {
try {
await rm(assetPath, { force: true });
AppEvents.emit("userAssetChanged", asset);
return "ok";
} catch (e) {
console.error(`Failed to remove user asset ${asset}:`, e);
return "failed";
}
}
const res = await dialog.showOpenDialog(mainWin, {
properties: ["openFile"],
title: `Select an image to use as ${asset}`,
defaultPath: app.getPath("pictures"),
filters: [
{
name: "Images",
extensions: ["png", "jpg", "jpeg", "webp", "gif", "avif", "svg"]
}
]
});
if (res.canceled || !res.filePaths.length) return "cancelled";
try {
await mkdir(UserAssetFolder, { recursive: true });
await copyFile(res.filePaths[0], assetPath);
AppEvents.emit("userAssetChanged", asset);
return "ok";
} catch (e) {
console.error(`Failed to copy user asset ${asset}:`, e);
return "failed";
}
});

View File

@@ -0,0 +1,32 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, BrowserWindow, dialog } from "electron";
import { rm } from "fs/promises";
import { DATA_DIR, MessageBoxChoice } from "main/constants";
export async function clearData(win: BrowserWindow) {
const { response } = await dialog.showMessageBox(win, {
message: "Are you sure you want to reset Vesktop?",
detail: "This will log you out, clear caches and reset all your settings!\n\nVesktop will automatically restart after this operation.",
buttons: ["Yes", "No"],
cancelId: MessageBoxChoice.Cancel,
defaultId: MessageBoxChoice.Default,
type: "warning"
});
if (response === MessageBoxChoice.Cancel) return;
win.close();
await win.webContents.session.clearStorageData();
await win.webContents.session.clearCache();
await win.webContents.session.clearCodeCaches({});
await rm(DATA_DIR, { force: true, recursive: true });
app.relaunch();
app.quit();
}

View File

@@ -0,0 +1,56 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html
// "If an argument contains a reserved character the argument must be quoted."
const desktopFileReservedChars = new Set([
" ",
"\t",
"\n",
'"',
"'",
"\\",
">",
"<",
"~",
"|",
"&",
";",
"$",
"*",
"?",
"#",
"(",
")",
"`"
]);
export function escapeDesktopFileArgument(arg: string) {
let needsQuoting = false;
let out = "";
for (const c of arg) {
if (desktopFileReservedChars.has(c)) {
// "Quoting must be done by enclosing the argument between double quotes"
needsQuoting = true;
// "and escaping the double quote character, backtick character ("`"), dollar sign ("$")
// and backslash character ("\") by preceding it with an additional backslash character"
if (c === '"' || c === "`" || c === "$" || c === "\\") {
out += "\\";
}
}
// "Literal percentage characters must be escaped as %%"
if (c === "%") {
out += "%%";
} else {
out += c;
}
}
return needsQuoting ? `"${out}"` : out;
}

View File

@@ -0,0 +1,13 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { access, constants } from "fs/promises";
export async function fileExistsAsync(path: string) {
return await access(path, constants.F_OK)
.then(() => true)
.catch(() => false);
}

View File

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

View File

@@ -1,36 +1,40 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron";
import { IpcEvents } from "shared/IpcEvents";
import { DISCORD_HOSTNAMES } from "main/constants";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
export function validateSender(frame: WebFrameMain) {
const { hostname, protocol } = new URL(frame.url);
if (protocol === "file:") return;
export function validateSender(frame: WebFrameMain | null, event: string) {
if (!frame) throw new Error(`ipc[${event}]: No sender frame`);
if (!frame.url) return;
switch (hostname) {
case "discord.com":
case "ptb.discord.com":
case "canary.discord.com":
break;
default:
throw new Error("ipc: Disallowed host " + hostname);
try {
var { hostname, protocol } = new URL(frame.url);
} catch (e) {
throw new Error(`ipc[${event}]: Invalid URL ${frame.url}`);
}
if (protocol === "file:" || protocol === "vesktop:") return;
if (!DISCORD_HOSTNAMES.includes(hostname)) {
throw new Error(`ipc[${event}]: Disallowed hostname ${hostname}`);
}
}
export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) {
export function handleSync(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) {
ipcMain.on(event, (e, ...args) => {
validateSender(e.senderFrame);
validateSender(e.senderFrame, event);
e.returnValue = cb(e, ...args);
});
}
export function handle(event: IpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) {
export function handle(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) {
ipcMain.handle(event, (e, ...args) => {
validateSender(e.senderFrame);
validateSender(e.senderFrame, event);
return cb(e, ...args);
});
}

View File

@@ -0,0 +1,16 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { resolve, sep } from "path";
export function isPathInDirectory(filePath: string, directory: string) {
const resolvedPath = resolve(filePath);
const resolvedDirectory = resolve(directory);
const normalizedDirectory = resolvedDirectory.endsWith(sep) ? resolvedDirectory : resolvedDirectory + sep;
return resolvedPath.startsWith(normalizedDirectory) || resolvedPath === resolvedDirectory;
}

View File

@@ -1,40 +1,73 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BrowserWindow, shell } from "electron";
import { DISCORD_HOSTNAMES } from "main/constants";
import { Settings } from "../settings";
import { createOrFocusPopup, setupPopout } from "./popout";
import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS";
export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } {
if (protocol == null) {
try {
protocol = new URL(url).protocol;
} catch {
return { action: "deny" };
}
}
switch (protocol) {
case "http:":
case "https:":
if (Settings.store.openLinksWithElectron) {
return { action: "allow" };
}
// eslint-disable-next-line no-fallthrough
case "mailto:":
case "spotify:":
if (isDeckGameMode) {
steamOpenURL(url);
} else {
shell.openExternal(url);
}
break;
case "steam:":
if (isDeckGameMode) {
execSteamURL(url);
} else {
shell.openExternal(url);
}
break;
}
return { action: "deny" };
}
export function makeLinksOpenExternally(win: BrowserWindow) {
win.webContents.setWindowOpenHandler(({ url }) => {
switch (url) {
case "about:blank":
case "https://discord.com/popout":
return { action: "allow" };
}
win.webContents.setWindowOpenHandler(({ url, frameName, features }) => {
try {
var { protocol } = new URL(url);
var { protocol, hostname, pathname, searchParams } = new URL(url);
} catch {
return { action: "deny" };
}
switch (protocol) {
case "http:":
case "https:":
if (Settings.store.openLinksWithElectron) {
return { action: "allow" };
}
// eslint-disable-next-line no-fallthrough
case "mailto:":
case "steam:":
case "spotify:":
shell.openExternal(url);
if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) {
return createOrFocusPopup(frameName, features);
}
return { action: "deny" };
if (url === "about:blank") return { action: "allow" };
// Drop the static temp page Discord web loads for the connections popout
if (frameName === "authorize" && searchParams.get("loading") === "true") return { action: "deny" };
return handleExternalUrl(url, protocol);
});
win.webContents.on("did-create-window", (win, { frameName }) => {
if (frameName.startsWith("DISCORD_")) setupPopout(win, frameName);
});
}

116
src/main/utils/popout.ts Normal file
View File

@@ -0,0 +1,116 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
import { Settings } from "main/settings";
import { handleExternalUrl } from "./makeLinksOpenExternally";
const ALLOWED_FEATURES = new Set([
"width",
"height",
"left",
"top",
"resizable",
"movable",
"alwaysOnTop",
"frame",
"transparent",
"hasShadow",
"closable",
"skipTaskbar",
"backgroundColor",
"menubar",
"toolbar",
"location",
"directories",
"titleBarStyle"
]);
const MIN_POPOUT_WIDTH = 320;
const MIN_POPOUT_HEIGHT = 180;
const DEFAULT_POPOUT_OPTIONS: BrowserWindowConstructorOptions = {
title: "Discord Popout",
backgroundColor: "#2f3136",
minWidth: MIN_POPOUT_WIDTH,
minHeight: MIN_POPOUT_HEIGHT,
frame: Settings.store.customTitleBar !== true,
titleBarStyle: process.platform === "darwin" ? "hidden" : undefined,
trafficLightPosition:
process.platform === "darwin"
? {
x: 10,
y: 3
}
: undefined,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
},
autoHideMenuBar: Settings.store.enableMenu
};
export const PopoutWindows = new Map<string, BrowserWindow>();
function focusWindow(window: BrowserWindow) {
window.setAlwaysOnTop(true);
window.focus();
window.setAlwaysOnTop(false);
}
function parseFeatureValue(feature: string) {
if (feature === "yes") return true;
if (feature === "no") return false;
const n = Number(feature);
if (!isNaN(n)) return n;
return feature;
}
function parseWindowFeatures(features: string) {
const keyValuesParsed = features.split(",");
return keyValuesParsed.reduce((features, feature) => {
const [key, value] = feature.split("=");
if (ALLOWED_FEATURES.has(key)) features[key] = parseFeatureValue(value);
return features;
}, {});
}
export function createOrFocusPopup(key: string, features: string) {
const existingWindow = PopoutWindows.get(key);
if (existingWindow) {
focusWindow(existingWindow);
return <const>{ action: "deny" };
}
return <const>{
action: "allow",
overrideBrowserWindowOptions: {
...DEFAULT_POPOUT_OPTIONS,
...parseWindowFeatures(features)
}
};
}
export function setupPopout(win: BrowserWindow, key: string) {
win.setMenuBarVisibility(false);
PopoutWindows.set(key, win);
/* win.webContents.on("will-navigate", (evt, url) => {
// maybe prevent if not origin match
})*/
win.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url));
win.once("closed", () => {
win.removeAllListeners();
PopoutWindows.delete(key);
});
}

View File

@@ -0,0 +1,29 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { execFile } from "child_process";
import { app } from "electron";
export async function setAsDefaultProtocolClient(protocol: string) {
if (process.platform !== "linux") {
return app.setAsDefaultProtocolClient(protocol);
}
// electron setAsDefaultProtocolClient uses xdg-settings instead of xdg-mime.
// xdg-settings had a bug where it would also register the app as a handler for text/html,
// aka become your browser. This bug was fixed years ago (xdg-utils 1.2.0) but Ubuntu ships
// 7 (YES, SEVEN) years out of date xdg-utils which STILL has the bug.
// FIXME: remove this workaround when Ubuntu updates their xdg-utils or electron switches to xdg-mime.
const { CHROME_DESKTOP } = process.env;
if (!CHROME_DESKTOP) return false;
return new Promise<boolean>(resolve => {
execFile("xdg-mime", ["default", CHROME_DESKTOP, `x-scheme-handler/${protocol}`], err => {
resolve(err == null);
});
});
}

View File

@@ -1,18 +1,15 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { exec as callbackExec } from "child_process";
import { BrowserWindow, dialog } from "electron";
import { sleep } from "shared/utils/sleep";
import { promisify } from "util";
import { writeFile } from "fs/promises";
import { join } from "path";
import { MessageBoxChoice } from "../constants";
import { Settings } from "../settings";
const exec = promisify(callbackExec);
import { State } from "../settings";
// Bump this to re-show the prompt
const layoutVersion = 2;
@@ -20,6 +17,8 @@ const layoutVersion = 2;
const layoutId = "3080264545"; // Vesktop Layout v2
const numberRegex = /^[0-9]*$/;
let steamPipeQueue = Promise.resolve();
export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1";
export function applyDeckKeyboardFix() {
@@ -42,23 +41,37 @@ function getAppId(): string | null {
return null;
}
async function execSteamURL(url: string): Promise<void> {
await exec(`steam -ifrunning ${url}`);
export function execSteamURL(url: string) {
// This doesn't allow arbitrary execution despite the weird syntax.
steamPipeQueue = steamPipeQueue.then(() =>
writeFile(
join(process.env.HOME || "/home/deck", ".steam", "steam.pipe"),
// replace ' to prevent argument injection
`'${process.env.HOME}/.local/share/Steam/ubuntu12_32/steam' '-ifrunning' '${url.replaceAll("'", "%27")}'\n`,
"utf-8"
)
);
}
export function steamOpenURL(url: string) {
execSteamURL(`steam://openurl/${url}`);
}
export async function showGamePage() {
const appId = getAppId();
if (!appId) return;
await execSteamURL(`steam://nav/games/details/${appId}`);
}
async function showLayout(appId: string) {
await 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}`);
execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`);
}
export async function askToApplySteamLayout(win: BrowserWindow) {
const appId = getAppId();
if (!appId) return;
if (Settings.store.steamOSLayoutVersion === layoutVersion) return;
const update = Boolean(Settings.store.steamOSLayoutVersion);
if (State.store.steamOSLayoutVersion === layoutVersion) return;
const update = Boolean(State.store.steamOSLayoutVersion);
// Touch screen breaks in some menus when native touch mode is enabled on latest SteamOS beta, remove most of the update specific text once that's fixed.
const { response } = await dialog.showMessageBox(win, {
@@ -74,8 +87,8 @@ ${update ? "Click" : "Tap"} no to keep your current layout.`,
type: "question"
});
if (Settings.store.steamOSLayoutVersion !== layoutVersion) {
Settings.store.steamOSLayoutVersion = layoutVersion;
if (State.store.steamOSLayoutVersion !== layoutVersion) {
State.store.steamOSLayoutVersion = layoutVersion;
}
if (response === MessageBoxChoice.Cancel) return;

View File

@@ -1,15 +1,16 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { existsSync, mkdirSync } from "fs";
import type { RequestOptions } from "https";
import { mkdirSync } from "fs";
import { access, constants as FsConstants, writeFile } from "fs/promises";
import { VENCORD_FILES_DIR } from "main/vencordFilesDir";
import { join } from "path";
import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
import { downloadFile, simpleGet } from "./http";
import { USER_AGENT } from "../constants";
import { downloadFile, fetchie } from "./http";
const API_BASE = "https://api.github.com";
@@ -31,37 +32,46 @@ export interface ReleaseData {
}
export async function githubGet(endpoint: string) {
const opts: RequestOptions = {
const opts: RequestInit = {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": USER_AGENT
}
};
if (process.env.GITHUB_TOKEN) opts.headers!.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
if (process.env.GITHUB_TOKEN) (opts.headers! as any).Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
return simpleGet(API_BASE + endpoint, opts);
return fetchie(API_BASE + endpoint, opts, { retryOnNetworkError: true });
}
export async function downloadVencordFiles() {
const release = await githubGet("/repos/Vendicated/Vencord/releases/latest");
const { assets } = JSON.parse(release.toString("utf-8")) as ReleaseData;
const { assets }: ReleaseData = await release.json();
await Promise.all(
assets
.filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f)))
.map(({ name, browser_download_url }) => downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name)))
.map(({ name, browser_download_url }) =>
downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name), {}, { retryOnNetworkError: true })
)
);
}
export function isValidVencordInstall(dir: string) {
return FILES_TO_DOWNLOAD.every(f => existsSync(join(dir, f)));
const existsAsync = (path: string) =>
access(path, FsConstants.F_OK)
.then(() => true)
.catch(() => false);
export async function isValidVencordInstall(dir: string) {
const results = await Promise.all(["package.json", ...FILES_TO_DOWNLOAD].map(f => existsAsync(join(dir, f))));
return !results.includes(false);
}
export async function ensureVencordFiles() {
if (isValidVencordInstall(VENCORD_FILES_DIR)) return;
if (await isValidVencordInstall(VENCORD_FILES_DIR)) return;
mkdirSync(VENCORD_FILES_DIR, { recursive: true });
await downloadVencordFiles();
await Promise.all([downloadVencordFiles(), writeFile(join(VENCORD_FILES_DIR, "package.json"), "{}")]);
}

View File

@@ -0,0 +1,13 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { join } from "path";
import { SESSION_DATA_DIR } from "./constants";
import { State } from "./settings";
// this is in a separate file to avoid circular dependencies
export const VENCORD_FILES_DIR = State.store.vencordDir || join(SESSION_DATA_DIR, "vencordFiles");

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

@@ -0,0 +1,135 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic";
import { app, ipcMain } from "electron";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths";
import { Settings } from "./settings";
let PatchBay: typeof PatchBayType | undefined;
let patchBayInstance: PatchBayType | undefined;
let imported = false;
let initialized = false;
let hasPipewirePulse = false;
let isGlibCxxOutdated = false;
function importVenmic() {
if (imported) {
return;
}
imported = true;
try {
PatchBay = (require(join(STATIC_DIR, `dist/venmic-${process.arch}.node`)) as typeof import("@vencord/venmic"))
.PatchBay;
hasPipewirePulse = PatchBay.hasPipeWire();
} catch (e: any) {
console.error("Failed to import venmic", e);
isGlibCxxOutdated = (e?.stack || e?.message || "").toLowerCase().includes("glibc");
}
}
function obtainVenmic() {
if (!imported) {
importVenmic();
}
if (PatchBay && !initialized) {
initialized = true;
try {
patchBayInstance = new PatchBay();
} catch (e: any) {
console.error("Failed to instantiate venmic", e);
}
}
return patchBayInstance;
}
function getRendererAudioServicePid() {
return (
app
.getAppMetrics()
.find(proc => proc.name === "Audio Service")
?.pid?.toString() ?? "owo"
);
}
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
const audioPid = getRendererAudioServicePid();
const { granularSelect } = Settings.store.audio ?? {};
const targets = obtainVenmic()
?.list(granularSelect ? ["node.name"] : undefined)
.filter(s => s["application.process.id"] !== audioPid);
return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated };
});
ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => {
const pid = getRendererAudioServicePid();
const { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {};
const data: LinkData = {
include,
exclude: [{ "application.process.id": pid }],
ignore_devices: ignoreDevices
};
if (ignoreInputMedia ?? true) {
data.exclude.push({ "media.class": "Stream/Input/Audio" });
}
if (ignoreVirtual) {
data.exclude.push({ "node.virtual": "true" });
}
if (workaround) {
data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
}
return obtainVenmic()?.link(data);
});
ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, exclude: Node[]) => {
const pid = getRendererAudioServicePid();
const { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } =
Settings.store.audio ?? {};
const data: LinkData = {
include: [],
exclude: [{ "application.process.id": pid }, ...exclude],
only_speakers: onlySpeakers,
ignore_devices: ignoreDevices,
only_default_speakers: onlyDefaultSpeakers
};
if (ignoreInputMedia ?? true) {
data.exclude.push({ "media.class": "Stream/Input/Audio" });
}
if (ignoreVirtual) {
data.exclude.push({ "node.virtual": "true" });
}
if (workaround) {
data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
}
return obtainVenmic()?.link(data);
});
ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink());

View File

@@ -0,0 +1,25 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, protocol } from "electron";
import { handleVesktopAssetsProtocol } from "./userAssets";
import { handleVesktopStaticProtocol } from "./vesktopStatic";
app.whenReady().then(() => {
protocol.handle("vesktop", async req => {
const url = new URL(req.url);
switch (url.hostname) {
case "assets":
return handleVesktopAssetsProtocol(url.pathname, req);
case "static":
return handleVesktopStaticProtocol(url.pathname, req);
default:
return new Response(null, { status: 404 });
}
});
});

31
src/main/vesktopStatic.ts Normal file
View File

@@ -0,0 +1,31 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BrowserWindow, net } from "electron";
import { join } from "path";
import { pathToFileURL } from "url";
import { isPathInDirectory } from "./utils/isPathInDirectory";
const STATIC_DIR = join(__dirname, "..", "..", "static");
export async function handleVesktopStaticProtocol(path: string, req: Request) {
const fullPath = join(STATIC_DIR, path);
if (!isPathInDirectory(fullPath, STATIC_DIR)) {
return new Response(null, { status: 404 });
}
return net.fetch(pathToFileURL(fullPath).href);
}
export function loadView(browserWindow: BrowserWindow, view: string, params?: URLSearchParams) {
const url = new URL(`vesktop://static/views/${view}`);
if (params) {
url.search = params.toString();
}
return browserWindow.loadURL(url.toString());
}

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());

10
src/module.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
declare module "__patches__" {
const never: never;
export default never;
}

View File

@@ -1,12 +1,13 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Node } from "@vencord/venmic";
import { ipcRenderer } from "electron";
import { IpcMessage, IpcResponse } from "main/ipcCommands";
import type { Settings } from "shared/settings";
import type { LiteralUnion } from "type-fest";
import { IpcEvents } from "../shared/IpcEvents";
import { invoke, sendSync } from "./typedIpc";
@@ -19,12 +20,21 @@ ipcRenderer.on(IpcEvents.SPELLCHECK_RESULT, (_, w: string, s: string[]) => {
spellCheckCallbacks.forEach(cb => cb(w, s));
});
let onDevtoolsOpen = () => {};
let onDevtoolsClose = () => {};
ipcRenderer.on(IpcEvents.DEVTOOLS_OPENED, () => onDevtoolsOpen());
ipcRenderer.on(IpcEvents.DEVTOOLS_CLOSED, () => onDevtoolsClose());
export const VesktopNative = {
app: {
relaunch: () => invoke<void>(IpcEvents.RELAUNCH),
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION),
setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY)
supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY),
getEnableHardwareAcceleration: () => sendSync<boolean>(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION),
isOutdated: () => invoke<boolean>(IpcEvents.UPDATER_IS_OUTDATED),
openUpdater: () => invoke<void>(IpcEvents.UPDATER_OPEN)
},
autostart: {
isEnabled: () => sendSync<boolean>(IpcEvents.AUTOSTART_ENABLED),
@@ -33,14 +43,17 @@ export const VesktopNative = {
},
fileManager: {
showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path),
selectVencordDir: () => invoke<LiteralUnion<"cancelled" | "invalid", string>>(IpcEvents.SELECT_VENCORD_DIR)
getVencordDir: () => sendSync<string | undefined>(IpcEvents.GET_VENCORD_DIR),
selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value),
chooseUserAsset: (asset: string, value?: null) =>
invoke<"cancelled" | "invalid" | "ok" | "failed">(IpcEvents.CHOOSE_USER_ASSET, asset, value)
},
settings: {
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path)
},
spellcheck: {
setLanguages: (languages: readonly string[]) => invoke<void>(IpcEvents.SPELLCHECK_SET_LANGUAGES, languages),
getAvailableLanguages: () => sendSync<string[]>(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES),
onSpellcheckResult(cb: SpellCheckerResultCallback) {
spellCheckCallbacks.add(cb);
},
@@ -52,9 +65,14 @@ export const VesktopNative = {
},
win: {
focus: () => invoke<void>(IpcEvents.FOCUS),
close: () => invoke<void>(IpcEvents.CLOSE),
minimize: () => invoke<void>(IpcEvents.MINIMIZE),
maximize: () => invoke<void>(IpcEvents.MAXIMIZE)
close: (key?: string) => invoke<void>(IpcEvents.CLOSE, key),
minimize: (key?: string) => invoke<void>(IpcEvents.MINIMIZE, key),
maximize: (key?: string) => invoke<void>(IpcEvents.MAXIMIZE, key),
flashFrame: (flag: boolean) => invoke<void>(IpcEvents.FLASH_FRAME, flag),
setDevtoolsCallbacks: (onOpen: () => void, onClose: () => void) => {
onDevtoolsOpen = onOpen;
onDevtoolsClose = onClose;
}
},
capturer: {
getLargeThumbnail: (id: string) => invoke<string>(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id)
@@ -62,14 +80,25 @@ export const VesktopNative = {
/** only available on Linux. */
virtmic: {
list: () =>
invoke<{ ok: false; isGlibcxxToOld: boolean } | { ok: true; targets: string[] }>(IpcEvents.VIRT_MIC_LIST),
start: (targets: string[]) => invoke<void>(IpcEvents.VIRT_MIC_START, targets),
startSystem: () => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM),
invoke<
{ ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean }
>(IpcEvents.VIRT_MIC_LIST),
start: (include: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START, include),
startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude),
stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP)
},
arrpc: {
onActivity(cb: (data: string) => void) {
ipcRenderer.on(IpcEvents.ARRPC_ACTIVITY, (_, data: string) => cb(data));
}
clipboard: {
copyImage: (imageBuffer: Uint8Array, imageSrc: string) =>
invoke<void>(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc)
},
debug: {
launchGpu: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_GPU),
launchWebrtcInternals: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS)
},
commands: {
onCommand(cb: (message: IpcMessage) => void) {
ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message));
},
respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response)
}
};

View File

@@ -1,7 +1,7 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { contextBridge, ipcRenderer, webFrame } from "electron";
@@ -40,5 +40,3 @@ if (IS_DEV) {
});
}
// #endregion
VesktopNative.spellcheck.setLanguages(window.navigator.languages);

13
src/preload/splash.ts Normal file
View File

@@ -0,0 +1,13 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("VesktopSplashNative", {
onUpdateMessage(callback: (message: string) => void) {
ipcRenderer.on("update-splash-message", (_, message: string) => callback(message));
}
});

View File

@@ -1,16 +1,16 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ipcRenderer } from "electron";
import { IpcEvents } from "shared/IpcEvents";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
export function invoke<T = any>(event: IpcEvents, ...args: any[]) {
export function invoke<T = any>(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>;
}
export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
export function sendSync<T = any>(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) {
return ipcRenderer.sendSync(event, ...args) as T;
}

24
src/preload/updater.ts Normal file
View File

@@ -0,0 +1,24 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { contextBridge, ipcRenderer } from "electron";
import type { UpdateInfo } from "electron-updater";
import { UpdaterIpcEvents } from "shared/IpcEvents";
import { invoke } from "./typedIpc";
contextBridge.exposeInMainWorld("VesktopUpdaterNative", {
getData: () => invoke<UpdateInfo>(UpdaterIpcEvents.GET_DATA),
installUpdate: () => invoke(UpdaterIpcEvents.INSTALL),
onProgress: (cb: (percent: number) => void) => {
ipcRenderer.on(UpdaterIpcEvents.DOWNLOAD_PROGRESS, (_, percent: number) => cb(percent));
},
onError: (cb: (message: string) => void) => {
ipcRenderer.on(UpdaterIpcEvents.ERROR, (_, message: string) => cb(message));
},
snoozeUpdate: () => invoke(UpdaterIpcEvents.SNOOZE_UPDATE),
ignoreUpdate: () => invoke(UpdaterIpcEvents.IGNORE_UPDATE)
});

View File

@@ -1,12 +1,13 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { filters, waitFor } from "@vencord/types/webpack";
import { RelationshipStore } from "@vencord/types/webpack/common";
import { VesktopLogger } from "./logger";
import { Settings } from "./settings";
let GuildReadStateStore: any;
@@ -26,7 +27,7 @@ export function setBadge() {
VesktopNative.app.setBadgeCount(totalCount);
} catch (e) {
console.error(e);
VesktopLogger.error("Failed to update badge count", e);
}
}

67
src/renderer/arrpc.ts Normal file
View File

@@ -0,0 +1,67 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type arRpcPlugin from "@vencord/types/plugins/arRPC.web";
import { Logger } from "@vencord/types/utils";
import { findLazy, findStoreLazy, onceReady } from "@vencord/types/webpack";
import { FluxDispatcher, InviteActions } from "@vencord/types/webpack/common";
import { IpcCommands } from "shared/IpcEvents";
import { onIpcCommand } from "./ipcCommands";
import { Settings } from "./settings";
const logger = new Logger("VesktopRPC", "#5865f2");
const StreamerModeStore = findStoreLazy("StreamerModeStore");
const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as typeof arRpcPlugin;
onIpcCommand(IpcCommands.RPC_ACTIVITY, async jsonData => {
if (!Settings.store.arRPC) return;
await onceReady;
const data = JSON.parse(jsonData);
if (data.socketId === "STREAMERMODE" && StreamerModeStore.autoToggle) {
FluxDispatcher.dispatch({
type: "STREAMER_MODE_UPDATE",
key: "enabled",
value: data.activity?.application_id === "STREAMERMODE"
});
return;
}
arRPC.handleEvent(new MessageEvent("message", { data: jsonData }));
});
onIpcCommand(IpcCommands.RPC_INVITE, async code => {
const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal");
if (!invite) return false;
VesktopNative.win.focus();
FluxDispatcher.dispatch({
type: "INVITE_MODAL_OPEN",
invite,
code,
context: "APP"
});
return true;
});
const { DEEP_LINK } = findLazy(m => m.DEEP_LINK?.handler);
onIpcCommand(IpcCommands.RPC_DEEP_LINK, async data => {
logger.debug("Opening deep link:", data);
try {
DEEP_LINK.handler({ args: data });
return true;
} catch (err) {
logger.error("Failed to open deep link:", err);
return false;
}
});

9
src/renderer/common.ts Normal file
View File

@@ -0,0 +1,9 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findStoreLazy } from "@vencord/types/webpack";
export const MediaEngineStore = findStoreLazy("MediaEngineStore");

View File

@@ -1,41 +1,69 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./screenSharePicker.css";
import { closeModal, Modals, openModal, useAwaiter } from "@vencord/types/utils";
import { findStoreLazy, onceReady } from "@vencord/types/webpack";
import { classNameFactory } from "@vencord/types/api/Styles";
import {
BaseText,
Button,
Card,
FluxDispatcher,
Forms,
Select,
Switch,
Text,
UserStore,
useState
} from "@vencord/types/webpack/common";
CogWheel,
FormSwitch,
Heading,
HeadingTertiary,
Margins,
Paragraph,
RestartIcon,
Span
} from "@vencord/types/components";
import {
closeModal,
Logger,
ModalCloseButton,
Modals,
ModalSize,
openModal,
useAwaiter,
useForceUpdater
} from "@vencord/types/utils";
import { onceReady } from "@vencord/types/webpack";
import { FluxDispatcher, Select, UserStore, useState } from "@vencord/types/webpack/common";
import { Node } from "@vencord/venmic";
import type { Dispatch, SetStateAction } from "react";
import { MediaEngineStore } from "renderer/common";
import { addPatch } from "renderer/patches/shared";
import { State, useSettings, useVesktopState } from "renderer/settings";
import { isLinux, isWindows } from "renderer/utils";
const StreamResolutions = ["480", "720", "1080", "1440"] as const;
import { SimpleErrorBoundary } from "./SimpleErrorBoundary";
const StreamResolutions = ["480", "720", "1080", "1440", "2160"] as const;
const StreamFps = ["15", "30", "60"] as const;
const MediaEngineStore = findStoreLazy("MediaEngineStore");
const cl = classNameFactory("vcd-screen-picker-");
export type StreamResolution = (typeof StreamResolutions)[number];
export type StreamFps = (typeof StreamFps)[number];
type SpecialSource = "None" | "Entire System";
type AudioSource = SpecialSource | Node;
type AudioSources = SpecialSource | Node[];
interface AudioItem {
name: string;
value: AudioSource;
}
interface StreamSettings {
resolution: StreamResolution;
fps: StreamFps;
audio: boolean;
audioSource?: string;
contentHint?: string;
includeSources?: AudioSources;
excludeSources?: AudioSources;
}
export interface StreamPick extends StreamSettings {
@@ -48,23 +76,26 @@ interface Source {
url: string;
}
let currentSettings: StreamSettings | null = null;
export let currentSettings: StreamSettings | null = null;
const logger = new Logger("VesktopScreenShare");
addPatch({
patches: [
{
find: "this.localWant=",
find: "this.getDefaultGoliveQuality()",
replacement: {
match: /this.localWant=/,
replace: "$self.patchStreamQuality(this);$&"
match: /this\.getDefaultGoliveQuality\(\)/,
replace: "$self.patchStreamQuality($&)"
}
}
],
patchStreamQuality(opts: any) {
if (!currentSettings) return;
const { screenshareQuality } = State.store;
if (!screenshareQuality) return opts;
const framerate = Number(currentSettings.fps);
const height = Number(currentSettings.resolution);
const framerate = Number(screenshareQuality.frameRate);
const height = Number(screenshareQuality.resolution);
const width = Math.round(height * (16 / 9));
Object.assign(opts, {
@@ -72,22 +103,34 @@ addPatch({
bitrateMax: 8000000,
bitrateTarget: 600000
});
if (opts?.encode) {
Object.assign(opts.encode, {
framerate,
width,
height,
pixelCount: height * width
});
}
Object.assign(opts.capture, {
framerate,
width,
height,
pixelCount: height * width
});
return opts;
}
});
if (isLinux) {
onceReady.then(() => {
FluxDispatcher.subscribe("VOICE_STATE_UPDATES", e => {
for (const state of e.voiceStates) {
if (state.userId === UserStore.getCurrentUser().id && state.oldChannelId && !state.channelId)
VesktopNative.virtmic.stop();
FluxDispatcher.subscribe("STREAM_CLOSE", ({ streamKey }: { streamKey: string }) => {
const owner = streamKey.split(":").at(-1);
if (owner !== UserStore.getCurrentUser().id) {
return;
}
VesktopNative.virtmic.stop();
});
});
}
@@ -102,13 +145,17 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
modalProps={props}
submit={async v => {
didSubmit = true;
if (v.audioSource && v.audioSource !== "None") {
if (v.audioSource === "Entire System") {
await VesktopNative.virtmic.startSystem();
if (v.includeSources && v.includeSources !== "None") {
if (v.includeSources === "Entire System") {
await VesktopNative.virtmic.startSystem(
!v.excludeSources || isSpecialSource(v.excludeSources) ? [] : v.excludeSources
);
} else {
await VesktopNative.virtmic.start([v.audioSource]);
await VesktopNative.virtmic.start(v.includeSources);
}
}
resolve(v);
}}
close={() => {
@@ -122,6 +169,9 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
onCloseRequest() {
closeModal(key);
reject("Aborted");
},
onCloseCallback() {
reject("Aborted");
}
}
);
@@ -130,20 +180,168 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) {
return (
<div className="vcd-screen-picker-grid">
<div className={cl("screen-grid")}>
{screens.map(({ id, name, url }) => (
<label key={id}>
<input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} />
<label key={id} className={cl("screen-label")}>
<input
type="radio"
className={cl("screen-radio")}
name="screen"
value={id}
onChange={() => chooseScreen(id)}
/>
<img src={url} alt="" />
<Text variant="text-sm/normal">{name}</Text>
<Paragraph className={cl("screen-name")}>{name}</Paragraph>
</label>
))}
</div>
);
}
function StreamSettings({
function AudioSettingsModal({
modalProps,
close,
setAudioSources
}: {
modalProps: any;
close: () => void;
setAudioSources: (s: AudioSources) => void;
}) {
const Settings = useSettings();
return (
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Modals.ModalHeader className={cl("header")}>
<BaseText size="lg" weight="semibold" tag="h3" style={{ flexGrow: 1 }}>
Audio Settings
</BaseText>
<ModalCloseButton onClick={close} />
</Modals.ModalHeader>
<Modals.ModalContent className={cl("modal", "venmic-settings")}>
<FormSwitch
title="Microphone Workaround"
description="Work around an issue that causes the microphone to be shared instead of the correct audio. Only enable if you're experiencing this issue."
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, workaround: v })}
value={Settings.audio?.workaround ?? false}
/>
<FormSwitch
title="Only Speakers"
description={
'When sharing entire desktop audio, only share apps that play to a speaker. You may want to disable this when using "mix bussing".'
}
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, onlySpeakers: v })}
value={Settings.audio?.onlySpeakers ?? true}
/>
<FormSwitch
title="Only Default Speakers"
description={
<>
When sharing entire desktop audio, only share apps that play to the <b>default</b> speakers.
You may want to disable this when using "mix bussing".
</>
}
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })}
value={Settings.audio?.onlyDefaultSpeakers ?? true}
/>
<FormSwitch
title="Ignore Inputs"
description="Exclude nodes that are intended to capture audio."
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })}
value={Settings.audio?.ignoreInputMedia ?? true}
/>
<FormSwitch
title="Ignore Virtual"
description={
'Exclude virtual nodes, such as nodes belonging to loopbacks. This might be useful when using "mix bussing".'
}
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, ignoreVirtual: v })}
value={Settings.audio?.ignoreVirtual ?? false}
/>
<FormSwitch
title="Ignore Devices"
description="Exclude device nodes, such as nodes belonging to microphones or speakers."
hideBorder
onChange={v =>
(Settings.audio = {
...Settings.audio,
ignoreDevices: v,
deviceSelect: v ? false : Settings.audio?.deviceSelect
})
}
value={Settings.audio?.ignoreDevices ?? true}
/>
<FormSwitch
title="Granular Selection"
description="Allow to select applications more granularly."
hideBorder
onChange={value => {
Settings.audio = { ...Settings.audio, granularSelect: value };
setAudioSources("None");
}}
value={Settings.audio?.granularSelect ?? false}
/>
<FormSwitch
title="Device Selection"
description={
<>
Allow to select devices such as microphones. Requires <b>Ignore Devices</b> to be turned
off.
</>
}
hideBorder
onChange={value => {
Settings.audio = { ...Settings.audio, deviceSelect: value };
setAudioSources("None");
}}
value={Settings.audio?.deviceSelect ?? false}
disabled={Settings.audio?.ignoreDevices}
/>
</Modals.ModalContent>
<Modals.ModalFooter className={cl("footer")}>
<Button variant="secondary" onClick={close}>
Back
</Button>
</Modals.ModalFooter>
</Modals.ModalRoot>
);
}
function OptionRadio<Settings extends object, Key extends keyof Settings>(props: {
options: Array<string> | ReadonlyArray<string>;
labels?: Array<string>;
settings: Settings;
settingsKey: Key;
onChange: (option: string) => void;
}) {
const { options, settings, settingsKey, labels, onChange } = props;
return (
<div className={cl("option-radios")}>
{(options as string[]).map((option, idx) => (
<label className={cl("option-radio")} data-checked={settings[settingsKey] === option} key={option}>
<Span weight="bold">{labels?.[idx] ?? option}</Span>
<input
className={cl("option-input")}
type="radio"
name="fps"
value={option}
checked={settings[settingsKey] === option}
onChange={() => onChange(option)}
/>
</label>
))}
</div>
);
}
function StreamSettingsUi({
source,
settings,
setSettings,
@@ -154,6 +352,9 @@ function StreamSettings({
setSettings: Dispatch<SetStateAction<StreamSettings>>;
skipPicker: boolean;
}) {
const Settings = useSettings();
const qualitySettings = State.store.screenshareQuality!;
const [thumb] = useAwaiter(
() => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)),
{
@@ -162,70 +363,88 @@ function StreamSettings({
}
);
const openSettings = () => {
openModal(props => (
<AudioSettingsModal
modalProps={props}
close={() => props.onClose()}
setAudioSources={sources =>
setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources }))
}
/>
));
};
return (
<div>
<Forms.FormTitle>What you're streaming</Forms.FormTitle>
<Card className="vcd-screen-picker-card vcd-screen-picker-preview">
<img src={thumb} alt="" />
<Text variant="text-sm/normal">{source.name}</Text>
<HeadingTertiary className={Margins.bottom8}>What you're streaming</HeadingTertiary>
<Card className={cl("card", "preview")}>
<img src={thumb} alt="" className={cl(isLinux ? "preview-img-linux" : "preview-img")} />
<Paragraph>{source.name}</Paragraph>
</Card>
<Forms.FormTitle>Stream Settings</Forms.FormTitle>
<HeadingTertiary className={Margins.bottom8}>Stream Settings</HeadingTertiary>
<Card className="vcd-screen-picker-card">
<div className="vcd-screen-picker-quality">
<section>
<Forms.FormTitle>Resolution</Forms.FormTitle>
<div className="vcd-screen-picker-radios">
{StreamResolutions.map(res => (
<label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}>
<Text variant="text-sm/bold">{res}</Text>
<input
type="radio"
name="resolution"
value={res}
checked={settings.resolution === res}
onChange={() => setSettings(s => ({ ...s, resolution: res }))}
/>
</label>
))}
</div>
<Card className={cl("card")}>
<div className={cl("quality")}>
<section className={cl("quality-section")}>
<Heading tag="h5">Resolution</Heading>
<OptionRadio
options={StreamResolutions}
settings={qualitySettings}
settingsKey="resolution"
onChange={value => (qualitySettings.resolution = value)}
/>
</section>
<section>
<Forms.FormTitle>Frame Rate</Forms.FormTitle>
<div className="vcd-screen-picker-radios">
{StreamFps.map(fps => (
<label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
<Text variant="text-sm/bold">{fps}</Text>
<input
type="radio"
name="fps"
value={fps}
checked={settings.fps === fps}
onChange={() => setSettings(s => ({ ...s, fps }))}
/>
</label>
))}
<section className={cl("quality-section")}>
<Heading tag="h5">Frame Rate</Heading>
<OptionRadio
options={StreamFps}
settings={qualitySettings}
settingsKey="frameRate"
onChange={value => (qualitySettings.frameRate = value)}
/>
</section>
</div>
<div className={cl("quality")}>
<section className={cl("quality-section")}>
<Heading tag="h5">Content Type</Heading>
<div>
<OptionRadio
options={["motion", "detail"]}
labels={["Prefer Smoothness", "Prefer Clarity"]}
settings={settings}
settingsKey="contentHint"
onChange={option => setSettings(s => ({ ...s, contentHint: option }))}
/>
<Paragraph className={Margins.top8}>
Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange for
a much sharper and clearer image.
</Paragraph>
</div>
{isWindows && (
<FormSwitch
title="Stream With Audio"
hideBorder
value={settings.audio}
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
className={cl("audio")}
/>
)}
</section>
</div>
{isWindows && (
<Switch
value={settings.audio}
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
hideBorder
className="vcd-screen-picker-audio"
>
Stream With Audio
</Switch>
)}
{isLinux && (
<AudioSourcePickerLinux
audioSource={settings.audioSource}
setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))}
openSettings={openSettings}
includeSources={settings.includeSources}
excludeSources={settings.excludeSources}
deviceSelect={Settings.audio?.deviceSelect}
granularSelect={Settings.audio?.granularSelect}
setIncludeSources={sources => setSettings(s => ({ ...s, includeSources: sources }))}
setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))}
/>
)}
</Card>
@@ -233,47 +452,249 @@ 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({
audioSource,
setAudioSource
includeSources,
excludeSources,
deviceSelect,
granularSelect,
openSettings,
setIncludeSources,
setExcludeSources
}: {
audioSource?: string;
setAudioSource(s: string): void;
includeSources?: AudioSources;
excludeSources?: AudioSources;
deviceSelect?: boolean;
granularSelect?: boolean;
openSettings: () => void;
setIncludeSources: (s: AudioSources) => void;
setExcludeSources: (s: AudioSources) => void;
}) {
const [audioSourcesSignal, refreshAudioSources] = useForceUpdater(true);
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
fallbackValue: { ok: true, targets: [] }
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true },
deps: [audioSourcesSignal]
});
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 (
<Paragraph>
Failed to retrieve Audio Sources because your C++ library is too old to run
<a href="https://github.com/Vencord/venmic" target="_blank" rel="noreferrer">
venmic
</a>
. See{" "}
<a
href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a"
target="_blank"
rel="noreferrer"
>
this guide
</a>{" "}
for possible solutions.
</Paragraph>
);
}
if (!hasPipewirePulse && !ignorePulseWarning) {
return (
<Paragraph>
Could not find pipewire-pulse. See{" "}
<a
href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install"
target="_blank"
rel="noreferrer"
>
this guide
</a>{" "}
on how to switch to pipewire. <br />
You can still continue, however, please{" "}
<b>beware that you can only share audio of apps that are running under pipewire</b>.{" "}
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing!</a>
</Paragraph>
);
}
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 (
<section>
<Forms.FormTitle>Audio</Forms.FormTitle>
{loading && <Forms.FormTitle>Loading Audio sources...</Forms.FormTitle>}
{!sources.ok &&
(sources.isGlibcxxToOld ? (
<Forms.FormText>
Failed to retrieve Audio Sources because your c++ library is too old to run venmic. If you would
like to stream with Audio, see{" "}
<a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
this guide
</a>
</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 && (
<Select
options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))}
isSelected={s => s === audioSource}
select={setAudioSource}
serialize={String}
/>
)}
</section>
<>
<div className={cl("audio-sources")}>
<section>
<Heading tag="h5">{loading ? "Loading Sources..." : "Audio Sources"}</Heading>
<SimpleErrorBoundary>
<Select
options={allSources.map(({ name, value }) => ({
label: name,
value: value,
default: name === "None"
}))}
isSelected={isItemSelected(includeSources)}
select={updateItems(setIncludeSources, includeSources)}
serialize={String}
popoutPosition="top"
closeOnSelect={false}
/>
</SimpleErrorBoundary>
</section>
{includeSources === "Entire System" && (
<section>
<Heading tag="h5">Exclude Sources</Heading>
<SimpleErrorBoundary>
<Select
options={allSources
.filter(x => x.name !== "Entire System")
.map(({ name, value }) => ({
label: name,
value: value,
default: name === "None"
}))}
isSelected={isItemSelected(excludeSources)}
select={updateItems(setExcludeSources, excludeSources)}
serialize={String}
popoutPosition="top"
closeOnSelect={false}
/>
</SimpleErrorBoundary>
</section>
)}
</div>
<div className={cl("settings-buttons")}>
<Button variant="secondary" onClick={refreshAudioSources} className={cl("settings-button")}>
<RestartIcon className={cl("settings-button-icon")} />
Refresh Audio Sources
</Button>
<Button variant="secondary" onClick={openSettings} className={cl("settings-button")}>
<CogWheel className={cl("settings-button-icon")} />
Open Audio Settings
</Button>
</div>
</>
);
}
@@ -292,23 +713,28 @@ function ModalComponent({
}) {
const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0);
const [settings, setSettings] = useState<StreamSettings>({
resolution: "1080",
fps: "60",
audio: true
contentHint: "motion",
audio: true,
includeSources: "None"
});
const qualitySettings = (useVesktopState().screenshareQuality ??= {
resolution: "720",
frameRate: "30"
});
return (
<Modals.ModalRoot {...modalProps}>
<Modals.ModalHeader className="vcd-screen-picker-header">
<Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle>
<Modals.ModalCloseButton onClick={close} />
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Modals.ModalHeader className={cl("header")}>
<BaseText size="lg" weight="semibold" tag="h3" style={{ flexGrow: 1 }}>
Screen Share Picker
</BaseText>
<ModalCloseButton onClick={close} />
</Modals.ModalHeader>
<Modals.ModalContent className="vcd-screen-picker-modal">
<Modals.ModalContent className={cl("modal")}>
{!selected ? (
<ScreenPicker screens={screens} chooseScreen={setSelected} />
) : (
<StreamSettings
<StreamSettingsUi
source={screens.find(s => s.id === selected)!}
settings={settings}
setSettings={setSettings}
@@ -316,35 +742,62 @@ function ModalComponent({
/>
)}
</Modals.ModalContent>
<Modals.ModalFooter className="vcd-screen-picker-footer">
<Modals.ModalFooter className={cl("footer")}>
<Button
disabled={!selected}
onClick={() => {
currentSettings = settings;
// If there are 2 connections, the second one is the existing stream.
// In that case, we patch its quality
const conn = [...MediaEngineStore.getMediaEngine().connections][1];
if (conn && conn.videoStreamParameters.length > 0) {
const height = Number(settings.resolution);
try {
const frameRate = Number(qualitySettings.frameRate);
const height = Number(qualitySettings.resolution);
const width = Math.round(height * (16 / 9));
Object.assign(conn.videoStreamParameters[0], {
maxFrameRate: Number(settings.fps),
maxPixelCount: width * height,
maxBitrate: 8000000,
maxResolution: {
type: "fixed",
width,
height
}
});
}
submit({
id: selected!,
...settings
});
const conn = [...MediaEngineStore.getMediaEngine().connections].find(
connection => connection.streamUserId === UserStore.getCurrentUser().id
);
if (conn) {
conn.videoStreamParameters[0].maxFrameRate = frameRate;
conn.videoStreamParameters[0].maxResolution.height = height;
conn.videoStreamParameters[0].maxResolution.width = width;
}
submit({
id: selected!,
...settings
});
setTimeout(async () => {
const conn = [...MediaEngineStore.getMediaEngine().connections].find(
connection => connection.streamUserId === UserStore.getCurrentUser().id
);
if (!conn) return;
const track = conn.input.stream.getVideoTracks()[0];
const constraints = {
...track.getConstraints(),
frameRate: { min: frameRate, ideal: frameRate },
width: { min: 640, ideal: width, max: width },
height: { min: 480, ideal: height, max: height },
advanced: [{ width: width, height: height }],
resizeMode: "none"
};
try {
await track.applyConstraints(constraints);
logger.info(
"Applied constraints successfully. New constraints:",
track.getConstraints()
);
} catch (e) {
logger.error("Failed to apply constraints.", e);
}
}, 100);
} catch (error) {
logger.error("Error while submitting stream.", error);
}
close();
}}
@@ -353,11 +806,11 @@ function ModalComponent({
</Button>
{selected && !skipPicker ? (
<Button color={Button.Colors.TRANSPARENT} onClick={() => setSelected(void 0)}>
<Button variant="secondary" onClick={() => setSelected(void 0)}>
Back
</Button>
) : (
<Button color={Button.Colors.TRANSPARENT} onClick={close}>
<Button variant="secondary" onClick={close}>
Cancel
</Button>
)}

Some files were not shown because too many files have changed in this diff Show More