Compare commits

..

166 Commits

Author SHA1 Message Date
Rie Takahashi
0e06b8d34c grammar lol 2023-02-22 03:45:56 +00:00
Rie Takahashi
b972aa1663 fix some labels in settings 2023-02-22 03:44:47 +00:00
Rie Takahashi
3bf81ee0fa make each notification type toggleable 2023-02-22 03:42:19 +00:00
Rie Takahashi
486230a335 feat(plugins): add relationship notifier plugin 2023-02-22 03:13:39 +00:00
nick
77c691651e ReviewDB: Show edit instead of create review where applicable (#466)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-18 03:35:51 +01:00
Nuckyz
e14ec96e21 feat(FakeNitro): Bypass client themes and fixes (#504)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-18 03:32:02 +01:00
Vendicated
ff1f337699 Fix QuickCSS on electron 20+ 2023-02-17 15:37:38 +01:00
Nuckyz
3ca87848e5 TypingIndicator: Fix a dumb (#503)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-17 01:31:55 +01:00
Vendicated
9420735bc7 Version 1.0.6 2023-02-16 23:40:38 +01:00
Vendicated
6807820f6c Badges should use ErrorBoundaries 2023-02-16 22:46:51 +01:00
Vendicated
3cad0d60b4 Silly Discord changed a bunch of css vars 2023-02-16 22:40:19 +01:00
Vendicated
fbbc198b1b Fix PlatformIndicator 2023-02-16 22:31:13 +01:00
Nuckyz
224ae979f2 feat(plugins): Typing Indicator (#502) 2023-02-16 03:57:57 +01:00
Lewis Crichton
27fc20118b feat(plugin): RoleColorEverywhere (#482)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-16 02:50:42 +01:00
Nuckyz
60ccd8cc25 Various plugin fixes (#492)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-16 02:00:09 +01:00
Lewis Crichton
5c1519156b feat(plugin): ColorSighted (#501) 2023-02-16 01:46:14 +01:00
Vendicated
58270ef925 bump to v1.0.5 2023-02-14 19:22:01 +01:00
Vendicated
68055977d2 NotificationAPI: Correctly request browser permissions 2023-02-14 19:20:10 +01:00
Sammy
2b0c25b45c Feat(InvisibleChat): Add Autodecryption (#490)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-12 22:10:03 +01:00
Vendicated
c154965d70 TypingTweaks: Fix crash after changing language 2023-02-12 21:07:05 +01:00
Ven
614234ad20 MessageLinkEmbeds: Prevent infinite cycles (#488) 2023-02-12 19:43:57 +01:00
Nuckyz
2489bc6831 Fix WhoReacted (#487)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-12 18:58:44 +01:00
fawn
d95be1acba refactor: update plugins to use $self (#478)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-10 22:41:49 +01:00
Ven
1d995e58f5 Notification API (#467)
Co-authored-by: Ven <vendicated@riseup.net>
Co-authored-by: afn <hey@afn.lol>
Co-authored-by: afn <afnzmn@gmail.com>
2023-02-10 22:33:34 +01:00
Justice Almanzar
6114bc6b16 make proxies enumerable (#476)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-09 21:21:14 +01:00
Vendicated
ae98401bd3 Fix lag when alt tabbing to Discord 2023-02-09 19:36:30 +01:00
Nuckyz
992a77e76c ShowHiddenChannels: Stage and voice channels support (#469)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-08 21:54:11 +01:00
Nuckyz
291f38115c New webpack filter: byDisplayName (#474) 2023-02-08 21:48:26 +01:00
cryptofyre
8a52189378 feat(plugin): richerCider (#471)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-08 21:48:12 +01:00
Vendicated
70278f64a9 Fix broken patches 2023-02-01 18:00:25 +01:00
Vendicated
7b1d03699d ci(reporter): Ignore 404/429 errors 2023-02-01 14:13:55 +01:00
Nico
8b40760187 fix(showHiddenChannels): revert lock icon to correct path (#465) 2023-02-01 13:59:58 +01:00
whqwert
de0990434e feat(plugin): RevealAllSpoilers (#381)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-01 13:38:02 +01:00
Nuckyz
369d179bbf ShowHiddenChannels: New screen for showing hidden channels (#460)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-01 12:11:05 +01:00
Nick
8f4e8d0a9b TypingTweaks: fix crash on non en-US locales (#463) 2023-01-31 06:35:52 +01:00
Vendicated
62f7e4d45c Add stylelint 2023-01-30 05:04:06 +01:00
Vendicated
fce7d6b681 Make webpack types importable from @webpack/types 2023-01-30 04:53:28 +01:00
Vendicated
69715070b9 browser ext: change applications to browser_specific_settings 2023-01-29 00:22:11 +01:00
Vendicated
d9fb7f45b5 ci: fix extension publishing 2023-01-29 00:19:39 +01:00
Vendicated
e32388e3ac ci: fix version check 2023-01-29 00:12:27 +01:00
Vendicated
823fa2d0c3 Bump to v1.0.4 2023-01-29 00:10:17 +01:00
Vendicated
3cdffe444e chore: Fix inconsistent file name casing 2023-01-29 00:09:17 +01:00
Nick
429ab9d363 feat(plugin): TypingTweaks (#432)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-29 00:06:33 +01:00
Kaydax
072ad3d7e6 feat(settings): Add the ability to make the window transparent (#457) 2023-01-28 23:54:38 +01:00
Sofia
6e22a96d9e feat(ShowHiddenChannels): fix channel switch keybinds jumping to hiddens (#459)
Co-authored-by: Nuckyz
2023-01-28 01:40:10 +01:00
Ven
bc4c7473e8 ci: fix typo 2023-01-26 22:51:30 +01:00
Ven
399305fd8a Automatic extension publishing (#453) 2023-01-26 22:38:02 +01:00
Vendicated
0c030a3a27 VcNarrator: Show all voices, better defaults 2023-01-25 21:08:45 +01:00
Vendicated
49aacccc19 shc: Make topic not inline 2023-01-25 21:06:22 +01:00
Vendicated
6ab4b48b47 chore: bump deps 2023-01-25 21:05:35 +01:00
Vendicated
103cd14361 Fix Themes Tab 2023-01-25 17:49:19 +01:00
Vendicated
41226f0358 Fix ShowHiddenChannels 2023-01-25 04:08:37 +01:00
Ven
5d3148cf50 New plugin: VcNarrator (#402)
Co-authored-by: Nico <nico@d3sox.me>
2023-01-25 03:42:01 +01:00
Nuckyz
d628924b59 ShowHiddenChannels: More improvements (#454)
- Remove buttons like the invite button when hovering hidden channels (as they do not work correctly)
- Make hideUnreads false work with HiddenIconWithMutedStyle
- migrate to definePluginSettings
- Change hardcoded constants to webpack gathering
- Clean up some patches
- Other minor things
- Make all patches use lookbehind for cleaner replacements (and better performance too lmao)
- Handle trying to connect to hidden channels
2023-01-25 03:35:34 +01:00
Ven
f19504f828 split up webpack commons into categories & type everything (#455) 2023-01-25 03:25:29 +01:00
Vendicated
a38ac956df chore: Remove legacy workarounds 2023-01-24 13:50:02 +01:00
Vendicated
34276301c3 Fix Settings UI (Discord removed default margins 2023-01-24 13:35:57 +01:00
Ven
b2ecb02335 Make Windows Ctrl+Q feature optional; add opt-in auto update (#451) 2023-01-24 01:42:57 +01:00
Vendicated
25d32ce292 Settings: Fix plugin switch state not updating (fixes #209) 2023-01-23 22:43:25 +01:00
Dominik
cb4c50842f [SpotifyControls] Add option to show Controls on hover (#352)
Co-authored-by: Dominik <domi@bambus.me>
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-23 22:25:00 +01:00
megumin
83757b19be fix: emojis with duplicate names failing to clone (#449) 2023-01-23 21:11:28 +00:00
Nuckyz
75050e74ca ShowHiddenChannels: better ui, alternative display mode (#446)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-23 22:04:50 +01:00
Vendicated
8a43e9b25f dev: Better errors when using Node < v18 2023-01-23 04:02:09 +01:00
hunter
84cfe531af Ignore dotfiles in plugin dirs (#447) 2023-01-23 00:27:55 +01:00
Vendicated
68e80c4d4c Fix small QuickCss bug 2023-01-22 04:29:58 +01:00
Vendicated
b4f98e5066 Fix Settings ContextMenu Shortcuts & Settings on canary 2023-01-22 04:26:33 +01:00
Nuckyz
9602f527d8 Future proof Volume Booster to work with volume settings syncing (#439) 2023-01-21 14:41:10 +01:00
Nuckyz
64180362fd ViewIcons: Fix finding ImageModal and props passing to MaskedLink (#442)
* Fix finding ImageModal and props passing to MaskedLink

* gonna stick this here
2023-01-21 14:37:36 +01:00
Nuckyz
6e44b8c47e Fix Message Accessories API (#441) 2023-01-21 01:47:24 +01:00
Vendicated
2641adb29b canary bad 2023-01-19 19:39:33 +01:00
Vendicated
ef5b3e1818 add issue title templates 2023-01-19 19:35:53 +01:00
Vendicated
7fe3a2c805 Add issue templates 2023-01-19 19:31:14 +01:00
Nuckyz
c4d2b4a8cd Fix message logger patch (#431) 2023-01-19 00:36:31 +01:00
The Captain
08a2030bbc feat(Plugin): customRPC (#406)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-18 23:47:40 +01:00
Nuckyz
5fe0600d6c Fix Message Popover API (#425) 2023-01-17 03:33:33 +01:00
Ven
ebdcbcaf0c Update README.md 2023-01-16 05:19:04 +01:00
Ven
1d287357ca Reimplement Discord's Switch to fix performance (#413) 2023-01-15 21:26:02 +00:00
megumin
e49151ff33 remove power user instructions from readme (#417)
normal users will stop asking dumb questions in support!! pog
2023-01-15 04:59:20 +00:00
Vendicated
7478e880a8 ShowHiddenChannels: Use Lock as ChannelIcon 2023-01-14 23:01:19 +01:00
Nico
be7fa0cb3f fix(showHiddenChannels): remove obsolete icons patch (#416)
resolves https://github.com/Vendicated/Vencord/issues/415
2023-01-14 20:32:33 +01:00
Sofia
9338b92b1a feat(SilentTyping): add toggle command and icon (#398)
Co-authored-by: Ven <vendicated@riseup.net>
Co-authored-by: Justice Almanzar <superdash993@gmail.com>
2023-01-14 18:47:12 +01:00
Ven
efb0ef8b9c Dev inject: add Mac support (#414) 2023-01-14 18:44:02 +01:00
Vendicated
fd766bc98f Dev: Hot reload core css 2023-01-14 02:15:17 +01:00
Swishilicous
0e5b8b07c9 make plugin cards prettier (#389)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-14 00:25:24 +01:00
Jeroen Claassens
7582feb603 feat(messageActions): make features toggleable (#373)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-13 23:59:31 +01:00
Dominik
6329499b1d git updater: Fix macOS (#391)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-13 23:24:51 +01:00
Nico
32cdb63885 fix(vcDoubleClick): fix functionality (#410)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-13 23:18:12 +01:00
Justice Almanzar
ea748dfb60 feat: Typesafe Settings Definitions (#403)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-13 23:15:45 +01:00
Ven
6c5fcc4119 Use GUI installer for pnpm inject/uninject (#407)
* Use GUI installer for pnpm inject/uninject

* Run Installer in DevMode
2023-01-13 17:52:28 +01:00
Vendicated
26f2b51eb9 Fix Frameless 2023-01-13 01:50:58 +01:00
Ven
075b0e0970 Update patcher.ts 2023-01-13 01:34:06 +01:00
Nick
10fd51071e feat: Add option to disable the window frame (#400)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-12 23:48:37 +01:00
Ven
e70abc57b6 Update Windows Update patcher (#404) 2023-01-12 23:15:38 +01:00
Vendicated
a8678db78c Fix React DevTools 2023-01-12 04:44:00 +01:00
Nick
bedb7b212b feat(Plugin): Add AlwaysTrust (#401)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-12 02:55:02 +01:00
Nuckyz
b39cbcd934 fix(IgnoreActivities): Fix for upcoming change (#399) 2023-01-12 02:50:31 +01:00
Vendicated
19c9a13273 Fix InvisibleChat button getting hidden by themes 2023-01-11 01:50:00 +01:00
Vendicated
c525672777 Fix BetterRoleDot crash 2023-01-11 01:24:55 +01:00
Vendicated
a772aa62f5 Fix PetPet & CorruptMp4s 2023-01-09 23:19:00 +01:00
ZerXDE "Till O
23a461c36d oneko: Disable pointer events to not block below buttons (#395)
Updated version of oneko which disables pointer Events.

Co-authored-by: Ven <vendicated@riseup.net>
2023-01-09 16:53:33 +01:00
Vendicated
da2d317555 Unhardcode PluginSettings styles 2023-01-09 16:23:40 +01:00
Vendicated
95df164e44 PluginSettings: Try to improve performance 2023-01-09 15:57:02 +01:00
Vendicated
ae9fe7fcfd Fix Ctrl+Q shortcut working when Discord minimised 2023-01-08 19:13:57 +01:00
Nuckyz
f0240ec345 chore(plugins): Fix IgnoreActivities & clean up other plugins (#384) 2023-01-08 02:15:22 +01:00
Vendicated
15aa2299c3 Add Ctrl+Q Exit shortcut on Windows 2023-01-07 22:53:41 +01:00
Sammy
06aa72c636 feat(Plugin): InvisibleChat (#349)
Co-authored-by: Ven <vendicated@riseup.net>
2023-01-07 22:52:55 +01:00
Dominik
1713450540 [PlatformIndicators] Fix Chat Badges in DMs (#367) 2023-01-07 21:31:45 +01:00
ActuallyTheSun
eecc555dac Fix Badges & MessageLinkEmbeds (#383) 2023-01-07 17:17:18 +01:00
Vendicated
5a3fbbfb30 unscuff profiles 2023-01-07 03:49:43 +01:00
Vendicated
cc51f6e2d2 Fix blurNSFW on canary 2023-01-07 03:28:40 +01:00
Vendicated
8113ed3c8c Fix canary 2023-01-07 03:19:37 +01:00
ActuallyTheSun
b8ed72286b fix(ViewIcons): module not found (#382) 2023-01-05 16:03:44 +01:00
Ven
9c5a149fb1 Move Installer to Vencord org 2023-01-04 20:56:14 +01:00
Ven
cf2bf2b43a oop 2023-01-04 01:17:08 +01:00
Elliott Tallis
e6f759eecd Add ImgOps to ReverseImageSearch (#379) 2023-01-03 19:42:06 +01:00
Vendicated
933216fcd5 QoL(PluginSettings): auto focus search bar 2023-01-03 02:37:22 +01:00
Vendicated
bcbbc79365 Happy new year 2023-01-03 02:32:08 +01:00
A user
374531d10e Extract inline styles to css (#370) 2023-01-03 02:30:54 +01:00
Ven
2e5d27b6b6 feat: Proper CSS api & css bundle (#269)
Co-authored-by: Vap0r1ze <superdash993@gmail.com>
2022-12-25 20:47:35 +01:00
Dominik
2172cae779 [PlatformIndicators] Add own Status (#350)
Co-authored-by: Dominik <domi@bambus.me>
Co-authored-by: HypedDomi <HypedDomi@users.noreply.github.com>
Co-authored-by: Ven <vendicated@riseup.net>
2022-12-23 04:16:17 +01:00
Ven
e740f55450 Fix Vencord 2022-12-23 03:46:39 +01:00
Nickyux
aff1b68d6b Add a "NEW" Badge for New Plugins (V2)! (#234)
Co-authored-by: Ven <vendicated@riseup.net>
Co-authored-by: Justice Almanzar <superdash993@gmail.com>
Co-authored-by: ArjixWasTaken <53124886+ArjixWasTaken@users.noreply.github.com>
2022-12-23 03:17:19 +01:00
Nuckyz
074542f0b3 feat(plugins): NoScreensharePreview plugin (#358) 2022-12-23 03:00:59 +01:00
Vendicated
b0c41d556a Improve treeshaking 2022-12-22 18:05:04 +01:00
Elliott Tallis
af0d34b155 pointy is a "contributor" (#359) 2022-12-22 17:42:54 +01:00
Ven
6dd705f951 Update build.yml 2022-12-22 17:25:57 +01:00
Ven
259f0284f0 Update build.yml 2022-12-21 22:43:54 +01:00
Ven
cb9eb1f772 i hate ci i hate ci 2022-12-21 21:12:28 +01:00
Ven
42b4eebca1 Update build.yml 2022-12-21 21:05:52 +01:00
Ven
a9ee0c7e50 Delete obsolete FUNDING.yml 2022-12-21 20:59:06 +01:00
Elliott Tallis
73b7f11d7a Also push builds to https://github.com/Vencord/builds (#344)
Co-authored-by: Ven <vendicated@riseup.net>
2022-12-21 20:58:07 +01:00
ActuallyTheSun
d806be1346 feat(PlatformIndicators): add indicator to messages (#343) 2022-12-21 20:16:32 +01:00
V3L0C1T13S
1f73cfa91a EmoteCloner: Use CDN_HOST variable to support unofficial backends (#356) 2022-12-21 16:12:05 +01:00
Nuckyz
7e6077367a feat(plugins): DisableDMCallIdle (#355) 2022-12-20 23:54:47 +01:00
Vendicated
103c499310 Monaco Popup: Add metadata, store window instance 2022-12-20 18:04:33 +01:00
Vendicated
9dcafbf468 Fix Notices
Have I ever mentioned how terrible Discord's Notices code is?
2022-12-20 18:03:58 +01:00
Nuckyz
1742bb6020 Fix StartupTimings (#353) 2022-12-20 16:18:15 +01:00
Vendicated
0743c1215e Add canary test 2022-12-20 02:59:16 +01:00
Vendicated
94ad8e8f61 Add useEffect/useState/useMemo to webpack commons 2022-12-20 00:34:26 +01:00
Justice Almanzar
989bd36eeb refactor: identifier escapes + "self" group (#339)
Co-authored-by: Ven <vendicated@riseup.net>
2022-12-19 22:59:54 +00:00
Nuckyz
4974c53f9c Improve PronounDB patch (#348) 2022-12-18 05:13:34 +01:00
Manti
47de9fab2e Make some changes to reviewdb ui and add badges to it (#245) 2022-12-17 23:30:29 +01:00
Nico
3efc79224f [ShowHiddenChannels] Fix last message date (#342) 2022-12-16 15:51:23 +01:00
Nuckyz
456164253d fix(MessageLogger): Fix module not being found (#338) 2022-12-16 14:16:47 +01:00
Vendicated
c257f86576 Fix experiments 2022-12-15 17:53:12 +01:00
Pedro
f6122a00ca feat(NoReplyMention): exempt list support (#337) 2022-12-15 14:05:44 +00:00
Ven
f1bdfdd6b9 Update nsfwGateBypass.ts 2022-12-14 23:50:00 +01:00
ActuallyTheSun
c8f2141114 feat(plugin): add MessageLinkEmbeds (#264)
Co-authored-by: Ven <vendicated@riseup.net>
2022-12-14 23:44:58 +01:00
megumin
fea8c60a40 hotfix injector for ptb/canary/dev (#330) 2022-12-14 23:35:02 +01:00
A user
a67db11dc2 Improve Settings UI & View Raw Modal (#332)
very cool
2022-12-14 00:44:57 +01:00
Box_
9a088b7a31 MoreKaomoji: Add more kaomoji (#299) 2022-12-09 22:54:46 +01:00
Justice Almanzar
ebb8da0f23 fix(FakeNitro): more reliable patches (#304) 2022-12-09 04:32:16 +01:00
Commandtechno
f2e0542614 New Plugin: NSFWGateBypass (#300) 2022-12-09 00:35:09 +01:00
megumin
ee24439795 feat(plugin): sort friend requests by date received (#280) 2022-12-08 23:53:12 +01:00
David Ralph
022bf17140 fix inconsistent margins & capitalisation (#281) 2022-12-08 23:51:18 +01:00
Justice Almanzar
2de461985d fix(ShikiCodeblocks): spoilers (#298)
* fix(ShikiCodeblocks): spoilers

* fix a settings bug i thikn
2022-12-08 15:54:19 +01:00
Justice Almanzar
2d08dd8a9c Shiki settings preview (#297) 2022-12-07 15:33:40 +01:00
Commandtechno
49b45d8262 google changed their shit (#294) 2022-12-05 23:14:48 +00:00
Cloudburst
8a5a5c7d1e UserScript: add csp bypassing fetch (#284) 2022-12-04 13:58:29 +01:00
Nuckyz
53d0a55561 refactor(IgnoreActivities): Use React Components and support Embedded Activities (#282) 2022-12-04 02:16:47 +01:00
Commandtechno
25ef5d60b4 add me to contributors (#287) 2022-12-03 22:42:18 +00:00
Commandtechno
c74241fde6 add commas in member count (#286)
l
2022-12-03 23:11:08 +01:00
Vendicated
4d8145f12c Fix arrpc 2022-12-03 14:58:00 +01:00
Ven
d4f70218ba ci: Do not release extension-v2.zip 2022-12-03 13:42:46 +01:00
Ven
6b4b4772bb Update README.md 2022-12-03 13:41:31 +01:00
Justice Almanzar
54010aab94 fix: hljs fallback (#283) 2022-12-03 11:32:14 +01:00
205 changed files with 9019 additions and 2768 deletions

View File

@ -37,7 +37,7 @@
" * Vencord, a modification for Discord's desktop app", " * Vencord, a modification for Discord's desktop app",
{ {
"pattern": " \\* Copyright \\(c\\) \\d{4}", "pattern": " \\* Copyright \\(c\\) \\d{4}",
"template": " * Copyright (c) 2022 Vendicated and contributors" "template": " * Copyright (c) 2023 Vendicated and contributors"
}, },
" *", " *",
" * This program is free software: you can redistribute it and/or modify", " * This program is free software: you can redistribute it and/or modify",
@ -82,9 +82,13 @@
"no-constant-condition": ["error", { "checkLoops": false }], "no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error", "no-duplicate-imports": "error",
"no-extra-semi": "error", "no-extra-semi": "error",
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
"dot-notation": "error", "dot-notation": "error",
"no-useless-escape": "error", "no-useless-escape": [
"error",
{
"extra": "i"
}
],
"no-fallthrough": "error", "no-fallthrough": "error",
"for-direction": "error", "for-direction": "error",
"no-async-promise-executor": "error", "no-async-promise-executor": "error",

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

13
.github/FUNDING.yml vendored
View File

@ -1,13 +0,0 @@
# These are supported funding model platforms
github: Vendicated
patreon: Aliucord
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

22
.github/ISSUE_TEMPLATE/blank.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Blank Template
description: Use this only if your issue does not fit into another template. **DO NOT ASK FOR SUPPORT OR REQUEST PLUGINS**
labels: []
body:
- type: textarea
id: info-sec
attributes:
label: Tell us all about it.
description: Go nuts, let us know what you're wanting to bring attention to.
placeholder: ...
validations:
required: true
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
description: DO NOT USE THIS TEMPLATE FOR SUPPORT OR PLUGIN REQUESTS!!! For Support, **join our Discord**. For plugin requests, **use discussions**
options:
- label: This is not a support or plugin request
required: true

66
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,66 @@
name: Bug/Crash Report
description: Create a bug or crash report for Vencord
labels: [bug]
title: "[Bug] <title>"
body:
- type: input
id: discord
attributes:
label: Discord Account
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
placeholder: username#0000
validations:
required: false
- type: textarea
id: bug-description
attributes:
label: What happens when the bug or crash occurs?
description: Where does this bug or crash occur, when does it occur, etc.
placeholder: The bug/crash happens sometimes when I do ..., causing this to not work/the app to crash. I think it happens because of ...
validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: What is the expected behaviour?
description: Simply detail what the expected behaviour is.
placeholder: I expect Vencord/Discord to open the ... page instead of ..., it prevents me from doing ...
validations:
required: true
- type: textarea
id: steps-to-take
attributes:
label: How do you recreate this bug or crash?
description: Give us a list of steps in order to recreate the bug or crash.
placeholder: |
1. Do ...
2. Then ...
3. Do this ..., ... and then ...
4. Observe "the bug" or "the crash"
validations:
required: true
- type: textarea
id: crash-log
attributes:
label: Errors
description: Open the Developer Console with Ctrl/Cmd + Shift + i. Then look for any red errors (Ignore network errors like Failed to load resource) and paste them between the "```".
value: |
```
Replace this text with your crash-log.
```
validations:
required: false
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
description: We only accept reports for bugs that happen on Discord Stable. Canary and PTB are Development branches and may be unstable
options:
- label: I am using Discord Stable or tried on Stable and this bug happens there as well
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Vencord Support Server
url: https://discord.gg/D9uwnFnqmd
about: If you need help regarding Vencord, please join our support server!
- name: Vencord Installer
url: https://github.com/Vencord/Installer
about: You can find the Vencord Installer here

View File

@ -0,0 +1,32 @@
name: Feature Request
description: Create a feature request for Vencord. To request new plugins, please use the Discussions tab
labels: [enhancement]
title: "[Feature Request] <title>"
body:
- type: input
id: discord
attributes:
label: Discord Account
description: Who on Discord is making this request? Not required but encouraged for easier follow-up
placeholder: username#0000
validations:
required: false
- type: textarea
id: feature-basic-description
attributes:
label: What is it that you'd like to see?
description: Describe the feature you want added as detailed as possible
placeholder: I think ... would be a cool feature to add. This would be awesome, thanks!
validations:
required: true
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
description: DO NOT USE THIS TEMPLATE FOR PLUGIN REQUESTS!!! For plugin requests, **use discussions**
options:
- label: This is not a plugin request
required: true

View File

@ -34,31 +34,42 @@ jobs:
- name: Build web - name: Build web
run: pnpm buildWeb --standalone run: pnpm buildWeb --standalone
- name: Sign firefox extension
run: |
pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=unlisted
env:
WEBEXT_USER: ${{ secrets.WEBEXT_USER }}
WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }}
- name: Build - name: Build
run: pnpm build --standalone run: pnpm build --standalone
- name: Rename extensions for more user friendliness - name: Clean up obsolete files
run: | run: |
mv dist/*.xpi dist/Vencord-for-Firefox.xpi rm -rf dist/extension* Vencord.user.css
mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip
rm -rf dist/extension-v2-unpacked
- name: Get some values needed for the release - name: Get some values needed for the release
id: release_values id: release_values
run: | run: |
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Upload Devbuild - name: Upload DevBuild as release
run: | run: |
gh release upload devbuild --clobber dist/* gh release upload devbuild --clobber dist/*
gh release edit devbuild --title "DevBuild $RELEASE_TAG" gh release edit devbuild --title "DevBuild $RELEASE_TAG"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ env.release_tag }} RELEASE_TAG: ${{ env.release_tag }}
- name: Upload DevBuild to builds repo
run: |
git config --global user.name "$USERNAME"
git config --global user.email actions@github.com
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git upload
cd upload
GLOBIGNORE=.git:.gitignore:README.md:LICENSE
rm -rf *
cp -r ../dist/* .
git add -A
git commit -m "Builds for https://github.com/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
git push --force https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
env:
API_TOKEN: ${{ secrets.BUILDS_TOKEN }}
GH_REPO: Vencord/builds
USERNAME: GitHub-Actions

61
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,61 @@
name: Release Browser Extension
on:
push:
tags:
- v*
jobs:
Publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: check that tag matches package.json version
run: |
pkg_version="v$(jq -r .version < package.json)"
if [[ "${{ github.ref_name }}" != "$pkg_version" ]]; then
echo "Tag ${{ github.ref_name }} does not match package.json version $pkg_version" >&2
exit 1
fi
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 19
uses: actions/setup-node@v3
with:
node-version: 19
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build web
run: pnpm buildWeb --standalone
- name: Publish extension
run: |
cd dist/extension-unpacked
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
EXIT_CODE=0
# Chrome
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
# Firefox
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
web-ext-submit || EXIT_CODE=$?
exit $EXIT_CODE
env:
# Chrome
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
# Firefox
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}

View File

@ -41,3 +41,17 @@ jobs:
env: env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- name: Create Report (Canary)
timeout-minutes: 10
if: success() || failure() # even run if previous one failed
run: |
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser)
export USE_CANARY=true
esbuild test/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ node_modules
vencord_installer vencord_installer
.idea .idea
.DS_Store
yarn.lock yarn.lock
package-lock.json package-lock.json

6
.stylelintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "stylelint-config-standard",
"rules": {
"indentation": 4
}
}

View File

@ -1,11 +1,11 @@
{ {
"recommendations": [ "recommendations": [
"EditorConfig.EditorConfig",
"pmneo.tsimporter",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"eamodio.gitlens",
"EditorConfig.EditorConfig",
"ExodiusStudios.comment-anchors",
"formulahendry.auto-rename-tag", "formulahendry.auto-rename-tag",
"GregorBiswanger.json2ts", "GregorBiswanger.json2ts",
"eamodio.gitlens", "stylelint.vscode-stylelint"
"kamikillerto.vscode-colorize"
] ]
} }

View File

@ -1,47 +1,30 @@
# Vencord # Vencord
A Discord client mod that does things differently The cutest Discord client mod
## Features ## Features
- Super easy to install, no git or node or anything else required - Super easy to install (one click installer)
- Many plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033) - 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, custom slash commands, ShowHiddenChannels - Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- Browser Support: Run Vencord in your Browser via extension or UserScript - Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes) - Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
- Works in all Electron versions (Confirmed working on versions 13-23) - Works in all Electron versions (Confirmed working on versions 13-23)
- Maintained very actively, broken plugins are usually fixed within 12 hours
## Installing / Uninstalling ## Installing / Uninstalling
If you're just a normal user, use [our simple gui installer!](https://github.com/Vendicated/VencordInstaller#usage) [![Download and run the Installer ](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#usage)
If you're a power user who wants to contribute and make plugins or just want to build from source and install manually, read [Megu's Installation Guide!](docs/1_INSTALLING.md)
## Installing on Browser ## Installing on Browser
Install the browser extension for [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [![Firefox](https://img.shields.io/badge/firefox-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Firefox.xpi) or [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js). Please note that they aren't automatically updated for now, so you will regularely have to reinstall it. [![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS and plugins making use of external resources will not work with the UserScript.
You may also build them from source, to do that do the same steps as in the manual regular install method, ## Building from Source
except run `pnpm buildWeb` instead of `pnpm build`, and your outputs will be in the dist folder
```sh See the docs folder
pnpm buildWeb
```
You will find the built extension at dist/extension.zip. Now just install this extension in your Browser
## Installing Plugins
> **Note**
> You can only use 3rd party plugins in the manual Vencord install for now.
Vencord comes with a bunch of plugins out of the box!
However, if you want to install your own ones, create a `userplugins` folder in the `src` directory and create or clone your plugins in there.
Don't forget to rebuild!
Want to learn how to create your own plugin, and maybe PR it into Vencord? See the [Contributing](#contributing) section below!
## Contributing ## Contributing

107
browser/GMPolyfill.js Normal file
View File

@ -0,0 +1,107 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function fetchOptions(url) {
return new Promise((resolve, reject) => {
const opt = {
method: "OPTIONS",
url: url,
};
opt.onload = resp => resolve(resp.responseHeaders);
opt.ontimeout = () => reject("fetch timeout");
opt.onerror = () => reject("fetch error");
opt.onabort = () => reject("fetch abort");
GM_xmlhttpRequest(opt);
});
}
function parseHeaders(headers) {
if (!headers)
return {};
const result = {};
const headersArr = headers.trim().split("\n");
for (var i = 0; i < headersArr.length; i++) {
var row = headersArr[i];
var index = row.indexOf(":")
, key = row.slice(0, index).trim().toLowerCase()
, value = row.slice(index + 1).trim();
if (result[key] === undefined) {
result[key] = value;
} else if (Array.isArray(result[key])) {
result[key].push(value);
} else {
result[key] = [result[key], value];
}
}
return result;
}
// returns true if CORS permits request
async function checkCors(url, method) {
const headers = parseHeaders(await fetchOptions(url));
const origin = headers["access-control-allow-origin"];
if (origin !== "*" && origin !== window.location.origin) return false;
const methods = headers["access-control-allow-methods"]?.split(/,\s/g);
if (methods && !methods.includes(method)) return false;
return true;
}
function blobTo(to, blob) {
if (to === "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer();
return new Promise((resolve, reject) => {
var fileReader = new FileReader();
fileReader.onload = event => resolve(event.target.result);
if (to === "arrayBuffer") fileReader.readAsArrayBuffer(blob);
else if (to === "text") fileReader.readAsText(blob, "utf-8");
else reject("unknown to");
});
}
function GM_fetch(url, opt) {
return new Promise((resolve, reject) => {
checkCors(url, opt?.method || "GET")
.then(can => {
if (can) {
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
const options = opt || {};
options.url = url;
options.data = options.body;
options.responseType = "blob";
options.onload = resp => {
var blob = resp.response;
resp.blob = () => Promise.resolve(blob);
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
resp.text = () => blobTo("text", blob);
resp.json = async () => JSON.parse(await blobTo("text", blob));
resolve(resp);
};
options.ontimeout = () => reject("fetch timeout");
options.onerror = () => reject("fetch error");
options.onabort = () => reject("fetch abort");
GM_xmlhttpRequest(options);
} else {
reject("CORS issue");
}
});
});
}
export const fetch = GM_fetch;

View File

@ -1,48 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Linnea Gräf
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function setContentTypeOnStylesheets(details) {
if (details.type === "stylesheet") {
details.responseHeaders = details.responseHeaders.filter(it => it.name.toLowerCase() !== 'content-type');
details.responseHeaders.push({ name: "Content-Type", value: "text/css" });
}
return { responseHeaders: details.responseHeaders };
}
var cspHeaders = [
"content-security-policy",
"content-security-policy-report-only",
];
function removeCSPHeaders(details) {
return {
responseHeaders: details.responseHeaders.filter(header =>
!cspHeaders.includes(header.name.toLowerCase()))
};
}
browser.webRequest.onHeadersReceived.addListener(
setContentTypeOnStylesheets, { urls: ["https://raw.githubusercontent.com/*"] }, ["blocking", "responseHeaders"]
);
browser.webRequest.onHeadersReceived.addListener(
removeCSPHeaders, { urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"] }, ["blocking", "responseHeaders"]
);

View File

@ -2,7 +2,18 @@ if (typeof browser === "undefined") {
var browser = chrome; var browser = chrome;
} }
var script = document.createElement("script"); const script = document.createElement("script");
script.src = browser.runtime.getURL("dist/Vencord.js"); script.src = browser.runtime.getURL("dist/Vencord.js");
// documentElement because we load before body/head are ready
document.documentElement.appendChild(script); const style = document.createElement("link");
style.type = "text/css";
style.rel = "stylesheet";
style.href = browser.runtime.getURL("dist/Vencord.css");
document.documentElement.append(script);
document.addEventListener(
"DOMContentLoaded",
() => document.documentElement.append(style),
{ once: true }
);

BIN
browser/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,10 +1,14 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"minimum_chrome_version": "91",
"name": "Vencord Web", "name": "Vencord Web",
"description": "Yeee", "description": "The cutest Discord mod now in your browser",
"version": "1.0.0",
"author": "Vendicated", "author": "Vendicated",
"homepage_url": "https://github.com/Vendicated/Vencord", "homepage_url": "https://github.com/Vendicated/Vencord",
"icons": {
"128": "icon.png"
},
"host_permissions": [ "host_permissions": [
"*://*.discord.com/*", "*://*.discord.com/*",
@ -23,7 +27,7 @@
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": ["dist/Vencord.js"], "resources": ["dist/Vencord.js", "dist/Vencord.css"],
"matches": ["*://*.discord.com/*"] "matches": ["*://*.discord.com/*"]
} }
], ],
@ -36,5 +40,12 @@
"path": "modifyResponseHeaders.json" "path": "modifyResponseHeaders.json"
} }
] ]
},
"browser_specific_settings": {
"gecko": {
"id": "vencord-firefox@vendicated.dev",
"strict_min_version": "109.0"
}
} }
} }

View File

@ -1,25 +0,0 @@
{
"manifest_version": 2,
"name": "Vencord Web",
"description": "The Vencord Client Mod for Discord Web.",
"version": "1.0.0",
"author": "Vendicated",
"homepage_url": "https://github.com/Vendicated/Vencord",
"permissions": [
"webRequest",
"webRequestBlocking",
"*://*.discord.com/*",
"https://raw.githubusercontent.com/*"
],
"content_scripts": [
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["content.js"]
}
],
"web_accessible_resources": ["dist/Vencord.js"],
"background": {
"scripts": ["background.js"]
}
}

View File

@ -7,7 +7,7 @@
// @supportURL https://github.com/Vendicated/Vencord // @supportURL https://github.com/Vendicated/Vencord
// @license GPL-3.0 // @license GPL-3.0
// @match *://*.discord.com/* // @match *://*.discord.com/*
// @grant none // @grant GM_xmlhttpRequest
// @run-at document-start // @run-at document-start
// @compatible chrome Chrome + Tampermonkey or Violentmonkey // @compatible chrome Chrome + Tampermonkey or Violentmonkey
// @compatible firefox Firefox Tampermonkey // @compatible firefox Firefox Tampermonkey

View File

@ -1,3 +0,0 @@
// FIXME: Delete this soon, for now it is needed so people can update
import("./scripts/build/build.mjs");

View File

@ -183,7 +183,6 @@ In `index.js`:
```js ```js
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js"); require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
require("../app.asar");
``` ```
And in `package.json`: And in `package.json`:

View File

@ -1,8 +1,8 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.0.1", "version": "1.0.6",
"description": "A Discord client mod that does things differently", "description": "The cutest Discord client mod",
"keywords": [], "keywords": [],
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": { "bugs": {
@ -20,32 +20,33 @@
"scripts": { "scripts": {
"build": "node scripts/build/build.mjs", "build": "node scripts/build/build.mjs",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"inject": "node scripts/patcher/install.js", "inject": "node scripts/runInstaller.mjs",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint-styles": "stylelint \"src/**/*.css\"",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
"test": "pnpm lint && pnpm build && pnpm testTsc", "test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc", "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit", "testTsc": "tsc --noEmit",
"uninject": "node scripts/patcher/uninstall.js", "uninject": "node scripts/runInstaller.mjs",
"watch": "node scripts/build/build.mjs --watch" "watch": "node scripts/build/build.mjs --watch"
}, },
"dependencies": { "dependencies": {
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.3",
"fflate": "^0.7.4" "fflate": "^0.7.4"
}, },
"devDependencies": { "devDependencies": {
"@types/diff": "^5.0.2", "@types/diff": "^5.0.2",
"@types/node": "^18.11.9", "@types/lodash": "^4.14.191",
"@types/react": "^18.0.25", "@types/node": "^18.11.18",
"@types/react-dom": "^18.0.9", "@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/yazl": "^2.4.2", "@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.44.0", "@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.44.0", "@typescript-eslint/parser": "^5.49.0",
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.3",
"console-menu": "^0.1.0",
"diff": "^5.1.0", "diff": "^5.1.0",
"discord-types": "^1.3.26", "discord-types": "^1.3.26",
"esbuild": "^0.15.16", "esbuild": "^0.15.18",
"eslint": "^8.28.0", "eslint": "^8.28.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-header": "^3.1.1", "eslint-plugin-header": "^3.1.1",
@ -54,15 +55,30 @@
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0", "highlight.js": "10.6.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"puppeteer-core": "^19.3.0", "puppeteer-core": "^19.6.0",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",
"type-fest": "^3.3.0", "stylelint": "^14.16.1",
"typescript": "^4.9.3" "stylelint-config-standard": "^29.0.0",
"type-fest": "^3.5.3",
"typescript": "^4.9.4"
}, },
"packageManager": "pnpm@7.13.4", "packageManager": "pnpm@7.13.4",
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch" "eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch",
"eslint@8.28.0": "patches/eslint@8.28.0.patch"
},
"peerDependencyRules": {
"ignoreMissing": [
"eslint-plugin-import",
"eslint"
]
},
"allowedDeprecatedVersions": {
"source-map-resolve": "*",
"resolve-url": "*",
"source-map-url": "*",
"urix": "*"
} }
}, },
"webExt": { "webExt": {
@ -71,5 +87,8 @@
"overwriteDest": true "overwriteDest": true
}, },
"sourceDir": "./dist/extension-v2-unpacked" "sourceDir": "./dist/extension-v2-unpacked"
},
"engines": {
"node": ">=18"
} }
} }

View File

@ -0,0 +1,45 @@
diff --git a/lib/rules/no-useless-escape.js b/lib/rules/no-useless-escape.js
index 2046a148a17fd1d5f3a4bbc9f45f7700259d11fa..f4898c6b57355a4fd72c43a9f32bf1a36a6ccf4a 100644
--- a/lib/rules/no-useless-escape.js
+++ b/lib/rules/no-useless-escape.js
@@ -97,12 +97,30 @@ module.exports = {
escapeBackslash: "Replace the `\\` with `\\\\` to include the actual backslash character."
},
- schema: []
+ schema: [{
+ type: "object",
+ properties: {
+ extra: {
+ type: "string",
+ default: ""
+ },
+ extraCharClass: {
+ type: "string",
+ default: ""
+ },
+ },
+ additionalProperties: false
+ }]
},
create(context) {
+ const options = context.options[0] || {};
+ const { extra, extraCharClass } = options || ''
const sourceCode = context.getSourceCode();
+ const NON_CHARCLASS_ESCAPES = union(REGEX_NON_CHARCLASS_ESCAPES, new Set(extra))
+ const CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set(extraCharClass))
+
/**
* Reports a node
* @param {ASTNode} node The node to report
@@ -238,7 +256,7 @@ module.exports = {
.filter(charInfo => charInfo.escaped)
// Filter out characters that are valid to escape, based on their position in the regular expression.
- .filter(charInfo => !(charInfo.inCharClass ? REGEX_GENERAL_ESCAPES : REGEX_NON_CHARCLASS_ESCAPES).has(charInfo.text))
+ .filter(charInfo => !(charInfo.inCharClass ? CHARCLASS_ESCAPES : NON_CHARCLASS_ESCAPES).has(charInfo.text))
// Report all the remaining characters.
.forEach(charInfo => report(node, charInfo.index, charInfo.text));

1149
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

91
scripts/build/buildWeb.mjs Executable file → Normal file
View File

@ -20,13 +20,13 @@
import esbuild from "esbuild"; import esbuild from "esbuild";
import { zip } from "fflate"; import { zip } from "fflate";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { readFileSync } from "fs";
import { readFile } from "fs/promises"; import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
import { join, resolve } from "path"; import { join } from "path";
// wtf is this assert syntax // wtf is this assert syntax
import PackageJSON from "../../package.json" assert { type: "json" }; import PackageJSON from "../../package.json" assert { type: "json" };
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins, watch } from "./common.mjs"; import { commonOpts, globPlugins, watch } from "./common.mjs";
/** /**
* @type {esbuild.BuildOptions} * @type {esbuild.BuildOptions}
@ -39,9 +39,7 @@ const commonOptions = {
external: ["plugins", "git-hash"], external: ["plugins", "git-hash"],
plugins: [ plugins: [
globPlugins, globPlugins,
gitHashPlugin, ...commonOpts.plugins,
gitRemotePlugin,
fileIncludePlugin
], ],
target: ["esnext"], target: ["esnext"],
define: { define: {
@ -60,51 +58,88 @@ await Promise.all(
}), }),
esbuild.build({ esbuild.build({
...commonOptions, ...commonOptions,
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
"window": "unsafeWindow",
...(commonOptions?.define)
},
outfile: "dist/Vencord.user.js", outfile: "dist/Vencord.user.js",
banner: { banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`) js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`)
}, },
footer: { footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local // UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(window,'Vencord',{get:()=>Vencord});" js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
}, },
}) })
] ]
); );
/**
* @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>}
*/
async function buildPluginZip(target, files, shouldZip) { async function buildPluginZip(target, files, shouldZip) {
const entries = { const entries = {
"dist/Vencord.js": readFileSync("dist/browser.js"), "dist/Vencord.js": await readFile("dist/browser.js"),
...Object.fromEntries(await Promise.all(files.map(async f => [ "dist/Vencord.css": await readFile("dist/browser.css"),
(f.startsWith("manifest") ? "manifest.json" : f), ...Object.fromEntries(await Promise.all(files.map(async f => {
await readFile(join("browser", f)) let content = await readFile(join("browser", f));
]))), if (f.startsWith("manifest")) {
const json = JSON.parse(content.toString("utf-8"));
json.version = PackageJSON.version;
content = new TextEncoder().encode(JSON.stringify(json));
}
return [
f.startsWith("manifest") ? "manifest.json" : f,
content
];
}))),
}; };
if (shouldZip) { if (shouldZip) {
return new Promise((resolve, reject) => {
zip(entries, {}, (err, data) => { zip(entries, {}, (err, data) => {
if (err) { if (err) {
console.error(err); reject(err);
process.exitCode = 1;
} else { } else {
writeFileSync("dist/" + target, data); const out = join("dist", target);
console.info("Extension written to dist/" + target); writeFile(out, data).then(() => {
console.info("Extension written to " + out);
resolve();
}).catch(reject);
} }
}); });
});
} else { } else {
if (existsSync(target)) await rm(target, { recursive: true, force: true });
rmSync(target, { recursive: true }); await Promise.all(Object.entries(entries).map(async ([file, content]) => {
for (const entry in entries) { const dest = join("dist", target, file);
const destination = "dist/" + target + "/" + entry; const parentDirectory = join(dest, "..");
const parentDirectory = resolve(destination, ".."); await mkdir(parentDirectory, { recursive: true });
mkdirSync(parentDirectory, { recursive: true }); await writeFile(dest, content);
writeFileSync(destination, entries[entry]); }));
}
console.info("Unpacked Extension written to dist/" + target); console.info("Unpacked Extension written to dist/" + target);
} }
} }
await buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true); const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
await buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true); const cssRuntime = `
await buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false); ;document.addEventListener("DOMContentLoaded", () => document.documentElement.appendChild(
Object.assign(document.createElement("style"), {
textContent: \`${content.replaceAll("`", "\\`")}\`,
id: "vencord-css-core"
})
), { once: true });
`;
return appendFile("dist/Vencord.user.js", cssRuntime);
});
await Promise.all([
appendCssRuntime,
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
buildPluginZip("extension-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
]);

View File

@ -17,9 +17,9 @@
*/ */
import { exec, execSync } from "child_process"; import { exec, execSync } from "child_process";
import { existsSync } from "fs"; import { existsSync, readFileSync } from "fs";
import { readdir, readFile } from "fs/promises"; import { readdir, readFile } from "fs/promises";
import { join } from "path"; import { join, relative } from "path";
import { promisify } from "util"; import { promisify } from "util";
export const watch = process.argv.includes("--watch"); export const watch = process.argv.includes("--watch");
@ -35,7 +35,7 @@ export const banner = {
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294 // https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const makeAllPackagesExternalPlugin = { export const makeAllPackagesExternalPlugin = {
name: "make-all-packages-external", name: "make-all-packages-external",
@ -46,7 +46,7 @@ export const makeAllPackagesExternalPlugin = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const globPlugins = { export const globPlugins = {
name: "glob-plugins", name: "glob-plugins",
@ -68,11 +68,12 @@ export const globPlugins = {
if (!existsSync(`./src/${dir}`)) continue; if (!existsSync(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`); const files = await readdir(`./src/${dir}`);
for (const file of files) { for (const file of files) {
if (file.startsWith(".")) continue;
if (file === "index.ts") { if (file === "index.ts") {
continue; continue;
} }
const mod = `p${i}`; const mod = `p${i}`;
code += `import ${mod} from "./${dir}/${file.replace(/.tsx?$/, "")}";\n`; code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`;
plugins += `[${mod}.name]:${mod},\n`; plugins += `[${mod}.name]:${mod},\n`;
i++; i++;
} }
@ -87,7 +88,7 @@ export const globPlugins = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const gitHashPlugin = { export const gitHashPlugin = {
name: "git-hash-plugin", name: "git-hash-plugin",
@ -103,7 +104,7 @@ export const gitHashPlugin = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const gitRemotePlugin = { export const gitRemotePlugin = {
name: "git-remote-plugin", name: "git-remote-plugin",
@ -125,7 +126,7 @@ export const gitRemotePlugin = {
}; };
/** /**
* @type {esbuild.Plugin} * @type {import("esbuild").Plugin}
*/ */
export const fileIncludePlugin = { export const fileIncludePlugin = {
name: "file-include-plugin", name: "file-include-plugin",
@ -147,6 +148,31 @@ export const fileIncludePlugin = {
} }
}; };
const styleModule = readFileSync("./scripts/build/module/style.js", "utf-8");
/**
* @type {import("esbuild").Plugin}
*/
export const stylePlugin = {
name: "style-plugin",
setup: ({ onResolve, onLoad }) => {
onResolve({ filter: /\.css\?managed$/, namespace: "file" }, ({ path, resolveDir }) => ({
path: relative(process.cwd(), join(resolveDir, path.replace("?managed", ""))),
namespace: "managed-style",
}));
onLoad({ filter: /\.css$/, namespace: "managed-style" }, async ({ path }) => {
const css = await readFile(path, "utf-8");
const name = relative(process.cwd(), path).replaceAll("\\", "/");
return {
loader: "js",
contents: styleModule
.replaceAll("STYLE_SOURCE", JSON.stringify(css))
.replaceAll("STYLE_NAME", JSON.stringify(name))
};
});
}
};
/** /**
* @type {import("esbuild").BuildOptions} * @type {import("esbuild").BuildOptions}
*/ */
@ -158,7 +184,7 @@ export const commonOpts = {
sourcemap: watch ? "inline" : "", sourcemap: watch ? "inline" : "",
legalComments: "linked", legalComments: "linked",
banner, banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin], plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote"], external: ["~plugins", "~git-hash", "~git-remote"],
inject: ["./scripts/build/inject/react.mjs"], inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement", jsxFactory: "VencordCreateElement",

View File

@ -16,6 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export const VencordFragment = Symbol.for("react.fragment"); export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
export let VencordCreateElement = export let VencordCreateElement =
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args); (...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);

View File

@ -0,0 +1,26 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
(window.VencordStyles ??= new Map()).set(STYLE_NAME, {
name: STYLE_NAME,
source: STYLE_SOURCE,
classNames: {},
dom: null,
});
export default STYLE_NAME;

View File

@ -0,0 +1,20 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (Number(process.versions.node.split(".")[0]) < 18)
throw `Your node version (${process.version}) is too old, please update to v18 or higher https://nodejs.org/en/download/`;

View File

@ -1,342 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const path = require("path");
const readline = require("readline");
const fs = require("fs");
const menu = require("console-menu");
const BRANCH_NAMES = [
"Discord",
"DiscordPTB",
"DiscordCanary",
"DiscordDevelopment",
"discord",
"discordptb",
"discordcanary",
"discorddevelopment",
"discord-ptb",
"discord-canary",
"discord-development",
// Flatpak
"com.discordapp.Discord",
"com.discordapp.DiscordPTB",
"com.discordapp.DiscordCanary",
"com.discordapp.DiscordDevelopment",
];
const MACOS_DISCORD_DIRS = [
"Discord.app",
"Discord PTB.app",
"Discord Canary.app",
"Discord Development.app",
];
if (process.platform === "linux" && process.env.SUDO_USER) {
process.env.HOME = fs
.readFileSync("/etc/passwd", "utf-8")
.match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0]
.split(":")[5];
}
const LINUX_DISCORD_DIRS = [
"/usr/share",
"/usr/lib64",
"/opt",
`${process.env.HOME}/.local/share`,
`${process.env.HOME}/.dvm`,
"/var/lib/flatpak/app",
`${process.env.HOME}/.local/share/flatpak/app`,
];
const FLATPAK_NAME_MAPPING = {
DiscordCanary: "discord-canary",
DiscordPTB: "discord-ptb",
DiscordDevelopment: "discord-development",
Discord: "discord",
};
const ENTRYPOINT = path
.join(process.cwd(), "dist", "patcher.js")
.replace(/\\/g, "/");
function question(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer);
});
});
}
async function getMenuItem(installations) {
const menuItems = installations.map(info => ({
title: info.patched ? "[MODIFIED] " + info.location : info.location,
info,
}));
const result = await menu(
[
...menuItems,
{ title: "Specify custom path", info: "custom" },
{ title: "Exit without patching", exit: true }
],
{
header: "Select a Discord installation to patch:",
border: true,
helpMessage:
"Use the up/down arrow keys to select an option. " +
"Press ENTER to confirm.",
}
);
if (!result || !result.info || result.exit) {
console.log("No installation selected.");
process.exit(0);
}
if (result.info === "custom") {
const customPath = await question("Please enter the path: ");
if (!customPath || !fs.existsSync(customPath)) {
console.log("No such Path or not specifed.");
process.exit();
}
const resourceDir = path.join(customPath, "resources");
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
console.log("Unsupported Install. resources/app.asar not found");
process.exit();
}
const appDir = path.join(resourceDir, "app");
result.info = {
branch: "unknown",
patched: fs.existsSync(appDir),
location: customPath,
versions: [{
path: appDir,
name: null
}],
arch: process.platform === "linux" ? "linux" : "win32",
isFlatpak: false,
};
}
if (result.info.patched) {
const answer = await question(
"This installation has already been modified. Overwrite? [Y/n]: "
);
if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) {
console.log("Not patching.");
process.exit(0);
}
}
return result.info;
}
function getWindowsDirs() {
const dirs = [];
for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) {
if (!BRANCH_NAMES.includes(dir)) continue;
const location = path.join(process.env.LOCALAPPDATA, dir);
if (!fs.statSync(location).isDirectory()) continue;
const appDirs = fs
.readdirSync(location, { withFileTypes: true })
.filter(file => file.isDirectory())
.filter(file => file.name.startsWith("app-"))
.map(file => path.join(location, file.name));
const versions = [];
let patched = false;
for (const fqAppDir of appDirs) {
const resourceDir = path.join(fqAppDir, "resources");
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
continue;
}
const appDir = path.join(resourceDir, "app");
if (fs.existsSync(appDir)) {
patched = true;
}
versions.push({
path: appDir,
name: /app-([0-9.]+)/.exec(fqAppDir)[1],
});
}
if (appDirs.length) {
dirs.push({
branch: dir,
patched,
location,
versions,
arch: "win32",
flatpak: false,
});
}
}
return dirs;
}
function getDarwinDirs() {
const dirs = [];
for (const dir of fs.readdirSync("/Applications")) {
if (!MACOS_DISCORD_DIRS.includes(dir)) continue;
const location = path.join("/Applications", dir, "Contents");
if (!fs.existsSync(location)) continue;
if (!fs.statSync(location).isDirectory()) continue;
const appDirs = fs
.readdirSync(location, { withFileTypes: true })
.filter(file => file.isDirectory())
.filter(file => file.name.startsWith("Resources"))
.map(file => path.join(location, file.name));
const versions = [];
let patched = false;
for (const resourceDir of appDirs) {
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
continue;
}
const appDir = path.join(resourceDir, "app");
if (fs.existsSync(appDir)) {
patched = true;
}
versions.push({
path: appDir,
name: null, // MacOS installs have no version number
});
}
if (appDirs.length) {
dirs.push({
branch: dir,
patched,
location,
versions,
arch: "win32",
});
}
}
return dirs;
}
function getLinuxDirs() {
const dirs = [];
for (const dir of LINUX_DISCORD_DIRS) {
if (!fs.existsSync(dir)) continue;
for (const branch of fs.readdirSync(dir)) {
if (!BRANCH_NAMES.includes(branch)) continue;
const location = path.join(dir, branch);
if (!fs.statSync(location).isDirectory()) continue;
const isFlatpak = location.includes("/flatpak/");
let appDirs = [];
if (isFlatpak) {
const fqDir = path.join(location, "current", "active", "files");
if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue;
const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1];
if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) {
continue;
}
const appDir = path.join(
fqDir,
FLATPAK_NAME_MAPPING[branchName]
);
if (!fs.existsSync(appDir)) continue;
if (!fs.statSync(appDir).isDirectory()) continue;
const resourceDir = path.join(appDir, "resources");
appDirs.push(resourceDir);
} else {
appDirs = fs
.readdirSync(location, { withFileTypes: true })
.filter(file => file.isDirectory())
.filter(
file =>
file.name.startsWith("app-") ||
file.name === "resources"
)
.map(file => path.join(location, file.name));
}
const versions = [];
let patched = false;
for (const resourceDir of appDirs) {
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
continue;
}
const appDir = path.join(resourceDir, "app");
if (fs.existsSync(appDir)) {
patched = true;
}
const version = /app-([0-9.]+)/.exec(resourceDir);
versions.push({
path: appDir,
name: version && version.length > 1 ? version[1] : null,
});
}
if (appDirs.length) {
dirs.push({
branch,
patched,
location,
versions,
arch: "linux",
isFlatpak,
});
}
}
}
return dirs;
}
module.exports = {
BRANCH_NAMES,
MACOS_DISCORD_DIRS,
LINUX_DISCORD_DIRS,
FLATPAK_NAME_MAPPING,
ENTRYPOINT,
question,
getMenuItem,
getWindowsDirs,
getDarwinDirs,
getLinuxDirs,
};

View File

@ -1,153 +0,0 @@
#!/usr/bin/node
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const path = require("path");
const fs = require("fs");
const { execSync } = require("child_process");
console.log("\nVencord Installer\n");
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
process.exit(1);
}
if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) {
console.log("You need to build the project first. Run:", "pnpm build");
process.exit(1);
}
const {
getMenuItem,
getWindowsDirs,
getDarwinDirs,
getLinuxDirs,
ENTRYPOINT,
question
} = require("./common");
switch (process.platform) {
case "win32":
install(getWindowsDirs());
break;
case "darwin":
install(getDarwinDirs());
break;
case "linux":
install(getLinuxDirs());
break;
default:
console.log("Unknown OS");
break;
}
async function install(installations) {
const selected = await getMenuItem(installations);
// Attempt to give flatpak perms
if (selected.isFlatpak) {
try {
const cwd = process.cwd();
const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`;
const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`;
const cmd = selected.location.startsWith("/home")
? userCmd
: globalCmd;
execSync(cmd);
console.log("Gave write perms to Discord Flatpak.");
} catch (e) {
console.log("Failed to give write perms to Discord Flatpak.");
console.log(
"Try running this script as an administrator:",
"sudo pnpm inject"
);
process.exit(1);
}
const answer = await question(
`Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` +
"This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" +
"Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" +
"[y/N]: "
);
if (["y", "yes", "yeah"].includes(answer.toLowerCase())) {
try {
const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
const cmd = selected.location.startsWith("/home")
? userCmd
: globalCmd;
execSync(cmd);
console.log("Sucessfully gave talk permission");
} catch (err) {
console.error("Failed to give talk permission\n", err);
}
} else {
console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`);
}
}
for (const version of selected.versions) {
const dir = version.path;
// Check if we have write perms to the install directory...
try {
fs.accessSync(selected.location, fs.constants.W_OK);
} catch (e) {
console.error("No write access to", selected.location);
console.error(
"Try running this script as an administrator:",
"sudo pnpm inject"
);
process.exit(1);
}
if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) {
fs.rmSync(dir, { recursive: true });
}
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(
path.join(dir, "index.js"),
`require("${ENTRYPOINT}");`
);
fs.writeFileSync(
path.join(dir, "package.json"),
JSON.stringify({
name: "discord",
main: "index.js",
})
);
const requiredFiles = ["index.js", "package.json"];
if (requiredFiles.every(f => fs.existsSync(path.join(dir, f)))) {
console.log(
"Successfully patched",
version.name
? `${selected.branch} ${version.name}`
: selected.branch
);
} else {
console.log("Failed to patch", dir);
console.log("Files in directory:", fs.readdirSync(dir));
}
}
}

View File

@ -1,78 +0,0 @@
#!/usr/bin/node
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const path = require("path");
const fs = require("fs");
console.log("\nVencord Uninstaller\n");
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
process.exit(1);
}
const {
getMenuItem,
getWindowsDirs,
getDarwinDirs,
getLinuxDirs,
} = require("./common");
switch (process.platform) {
case "win32":
uninstall(getWindowsDirs());
break;
case "darwin":
uninstall(getDarwinDirs());
break;
case "linux":
uninstall(getLinuxDirs());
break;
default:
console.log("Unknown OS");
break;
}
async function uninstall(installations) {
const selected = await getMenuItem(installations);
for (const version of selected.versions) {
const dir = version.path;
// Check if we have write perms to the install directory...
try {
fs.accessSync(selected.location, fs.constants.W_OK);
} catch (e) {
console.error("No write access to", selected.location);
console.error(
"Try running this script as an administrator:",
"sudo pnpm uninject"
);
process.exit(1);
}
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true });
}
console.log(
"Successfully unpatched",
version.name
? `${selected.branch} ${version.name}`
: selected.branch
);
}
}

128
scripts/runInstaller.mjs Normal file
View File

@ -0,0 +1,128 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./checkNodeVersion.js";
import { execFileSync, execSync } from "child_process";
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import { Readable } from "stream";
import { finished } from "stream/promises";
import { fileURLToPath } from "url";
const BASE_URL = "https://github.com/Vencord/Installer/releases/latest/download/";
const INSTALLER_PATH_DARWIN = "VencordInstaller.app/Contents/MacOS/VencordInstaller";
const BASE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
const FILE_DIR = join(BASE_DIR, "dist", "Installer");
const ETAG_FILE = join(FILE_DIR, "etag.txt");
function getFilename() {
switch (process.platform) {
case "win32":
return "VencordInstaller.exe";
case "darwin":
return "VencordInstaller.MacOS.zip";
case "linux":
return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11");
default:
throw new Error("Unsupported platform: " + process.platform);
}
}
async function ensureBinary() {
const filename = getFilename();
console.log("Downloading " + filename);
mkdirSync(FILE_DIR, { recursive: true });
const downloadName = join(FILE_DIR, filename);
const outputFile = process.platform === "darwin"
? join(FILE_DIR, "VencordInstaller")
: downloadName;
const etag = existsSync(outputFile) && existsSync(ETAG_FILE)
? readFileSync(ETAG_FILE, "utf-8")
: null;
const res = await fetch(BASE_URL + filename, {
headers: {
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)",
"If-None-Match": etag
}
});
if (res.status === 304) {
console.log("Up to date, not redownloading!");
return outputFile;
}
if (!res.ok)
throw new Error(`Failed to download installer: ${res.status} ${res.statusText}`);
writeFileSync(ETAG_FILE, res.headers.get("etag"));
if (process.platform === "darwin") {
console.log("Unzipping...");
const zip = new Uint8Array(await res.arrayBuffer());
const ff = await import("fflate");
const bytes = ff.unzipSync(zip, {
filter: f => f.name === INSTALLER_PATH_DARWIN
})[INSTALLER_PATH_DARWIN];
writeFileSync(outputFile, bytes, { mode: 0o755 });
console.log("Overriding security policy for installer binary (this is required to run it)");
console.log("xattr might error, that's okay");
const logAndRun = cmd => {
console.log("Running", cmd);
try {
execSync(cmd);
} catch { }
};
logAndRun(`sudo spctl --add '${outputFile}' --label "Vencord Installer"`);
logAndRun(`sudo xattr -d com.apple.quarantine '${outputFile}'`);
} else {
// WHY DOES NODE FETCH RETURN A WEB STREAM OH MY GOD
const body = Readable.fromWeb(res.body);
await finished(body.pipe(createWriteStream(outputFile, {
mode: 0o755,
autoClose: true
})));
}
console.log("Finished downloading!");
return outputFile;
}
const installerBin = await ensureBinary();
console.log("Now running Installer...");
execFileSync(installerBin, {
stdio: "inherit",
env: {
...process.env,
VENCORD_USER_DATA_DIR: BASE_DIR,
VENCORD_DEV_INSTALL: "1"
}
});

View File

@ -18,7 +18,6 @@
export * as Api from "./api"; export * as Api from "./api";
export * as Plugins from "./plugins"; export * as Plugins from "./plugins";
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
export * as Util from "./utils"; export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss"; export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater"; export * as Updater from "./utils/updater";
@ -31,9 +30,9 @@ import "./webpack/patchWebpack";
import { popNotice, showNotice } from "./api/Notices"; import { popNotice, showNotice } from "./api/Notices";
import { PlainSettings, Settings } from "./api/settings"; import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins"; import { patches, PMLogger, startAllPlugins } from "./plugins";
import { checkForUpdates, UpdateLogger } from "./utils/updater"; import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack"; import { onceReady } from "./webpack";
import { Router } from "./webpack/common"; import { SettingsRouter } from "./webpack/common";
export let Components: any; export let Components: any;
@ -45,17 +44,37 @@ async function init() {
if (!IS_WEB) { if (!IS_WEB) {
try { try {
const isOutdated = await checkForUpdates(); const isOutdated = await checkForUpdates();
if (isOutdated && Settings.notifyAboutUpdates) if (!isOutdated) return;
if (Settings.autoUpdate) {
await update();
const needsFullRestart = await rebuild();
setTimeout(() => {
showNotice(
"Vencord has been updated!",
"Restart",
() => {
if (needsFullRestart)
window.DiscordNative.app.relaunch();
else
location.reload();
}
);
}, 10_000);
return;
}
if (Settings.notifyAboutUpdates)
setTimeout(() => { setTimeout(() => {
showNotice( showNotice(
"A Vencord update is available!", "A Vencord update is available!",
"View Update", "View Update",
() => { () => {
popNotice(); popNotice();
Router.open("VencordUpdater"); SettingsRouter.open("VencordUpdater");
} }
); );
}, 10000); }, 10_000);
} catch (err) { } catch (err) {
UpdateLogger.error("Failed to check for updates", err); UpdateLogger.error("Failed to check for updates", err);
} }

View File

@ -16,8 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ErrorBoundary from "@components/ErrorBoundary";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import { HTMLProps } from "react"; import { ComponentType, HTMLProps } from "react";
import Plugins from "~plugins"; import Plugins from "~plugins";
@ -27,20 +28,21 @@ export enum BadgePosition {
} }
export interface ProfileBadge { export interface ProfileBadge {
/** The tooltip to show on hover */ /** The tooltip to show on hover. Required for image badges */
tooltip: string; tooltip?: string;
/** Custom component for the badge (tooltip not included) */
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
/** The custom image to use */ /** The custom image to use */
image?: string; image?: string;
/** Action to perform when you click the badge */ /** Action to perform when you click the badge */
onClick?(): void; onClick?(): void;
/** Should the user display this badge? */ /** Should the user display this badge? */
shouldShow?(userInfo: BadgeUserArgs): boolean; shouldShow?(userInfo: BadgeUserArgs): boolean;
/** Optional props (e.g. style) for the badge */ /** Optional props (e.g. style) for the badge, ignored for component badges */
props?: HTMLProps<HTMLImageElement>; props?: HTMLProps<HTMLImageElement>;
/** Insert at start or end? */ /** Insert at start or end? */
position?: BadgePosition; position?: BadgePosition;
/** The badge name to display, Discord uses this. Required for component badges */
/** The badge name to display. Discord uses this, but we don't. */
key?: string; key?: string;
} }
@ -51,6 +53,7 @@ const Badges = new Set<ProfileBadge>();
* @param badge The badge to register * @param badge The badge to register
*/ */
export function addBadge(badge: ProfileBadge) { export function addBadge(badge: ProfileBadge) {
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
Badges.add(badge); Badges.add(badge);
} }
@ -70,8 +73,8 @@ export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
for (const badge of Badges) { for (const badge of Badges) {
if (!badge.shouldShow || badge.shouldShow(args)) { if (!badge.shouldShow || badge.shouldShow(args)) {
badge.position === BadgePosition.START badge.position === BadgePosition.START
? badgeArray.unshift(badge) ? badgeArray.unshift({ ...badge, ...args })
: badgeArray.push(badge); : badgeArray.push({ ...badge, ...args });
} }
} }
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id); (Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);

View File

@ -17,7 +17,8 @@
*/ */
import { mergeDefaults } from "@utils/misc"; import { mergeDefaults } from "@utils/misc";
import { findByCodeLazy, findByPropsLazy, waitFor } from "@webpack"; import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { SnowflakeUtils } from "@webpack/common";
import { Message } from "discord-types/general"; import { Message } from "discord-types/general";
import type { PartialDeep } from "type-fest"; import type { PartialDeep } from "type-fest";
@ -26,9 +27,6 @@ import { Argument } from "./types";
const createBotMessage = findByCodeLazy('username:"Clyde"'); const createBotMessage = findByCodeLazy('username:"Clyde"');
const MessageSender = findByPropsLazy("receiveMessage"); const MessageSender = findByPropsLazy("receiveMessage");
let SnowflakeUtils: any;
waitFor("fromTimestamp", m => SnowflakeUtils = m);
export function generateId() { export function generateId() {
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`; return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
} }

View File

@ -0,0 +1,65 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Channel, User } from "discord-types/general/index.js";
interface DecoratorProps {
activities: any[];
canUseAvatarDecorations: boolean;
channel: Channel;
/**
* Only for DM members
*/
channelName?: string;
/**
* Only for server members
*/
currentUser?: User;
guildId?: string;
isMobile: boolean;
isOwner?: boolean;
isTyping: boolean;
selected: boolean;
status: string;
user: User;
[key: string]: any;
}
export type Decorator = (props: DecoratorProps) => JSX.Element | null;
type OnlyIn = "guilds" | "dms";
export const decorators = new Map<string, { decorator: Decorator, onlyIn?: OnlyIn; }>();
export function addDecorator(identifier: string, decorator: Decorator, onlyIn?: OnlyIn) {
decorators.set(identifier, { decorator, onlyIn });
}
export function removeDecorator(identifier: string) {
decorators.delete(identifier);
}
export function __addDecoratorsToList(props: DecoratorProps): (JSX.Element | null)[] {
const isInGuild = !!(props.guildId);
return [...decorators.values()].map(decoratorObj => {
const { decorator, onlyIn } = decoratorObj;
// this can most likely be done cleaner
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) {
return decorator(props);
}
return null;
});
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element; export type AccessoryCallback = (props: Record<string, any>) => JSX.Element | null | Array<JSX.Element | null>;
export type Accessory = { export type Accessory = {
callback: AccessoryCallback; callback: AccessoryCallback;
position?: number; position?: number;
@ -44,6 +44,15 @@ export function _modifyAccessories(
props: Record<string, any> props: Record<string, any>
) { ) {
for (const accessory of accessories.values()) { for (const accessory of accessories.values()) {
let accessories = accessory.callback(props);
if (accessories == null)
continue;
if (!Array.isArray(accessories))
accessories = [accessories];
else if (accessories.length === 0)
continue;
elements.splice( elements.splice(
accessory.position != null accessory.position != null
? accessory.position < 0 ? accessory.position < 0
@ -51,7 +60,7 @@ export function _modifyAccessories(
: accessory.position : accessory.position
: elements.length, : elements.length,
0, 0,
accessory.callback(props) ...accessories.filter(e => e != null) as JSX.Element[]
); );
} }

View File

@ -0,0 +1,63 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Channel, Message } from "discord-types/general/index.js";
interface DecorationProps {
author: {
/**
* Will be username if the user has no nickname
*/
nick: string;
iconRoleId: string;
guildMemberAvatar: string;
colorRoleName: string;
colorString: string;
};
channel: Channel;
compact: boolean;
decorations: {
/**
* Element for the [BOT] tag if there is one
*/
0: JSX.Element | null;
/**
* Other decorations (including ones added with this api)
*/
1: JSX.Element[];
};
message: Message;
[key: string]: any;
}
export type Decoration = (props: DecorationProps) => JSX.Element | null;
export const decorations = new Map<string, Decoration>();
export function addDecoration(identifier: string, decoration: Decoration) {
decorations.set(identifier, decoration);
}
export function removeDecoration(identifier: string) {
decorations.delete(identifier);
}
export function __addDecorationsToMessage(props: DecorationProps): (JSX.Element | null)[] {
return [...decorations.values()].map(decoration => {
return decoration(props);
});
}

View File

@ -0,0 +1,92 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./styles.css";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
import { NotificationData } from "./Notifications";
export default ErrorBoundary.wrap(function NotificationComponent({
title,
body,
richBody,
color,
icon,
onClick,
onClose,
image
}: NotificationData) {
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
const [isHover, setIsHover] = useState(false);
const [elapsed, setElapsed] = useState(0);
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
useEffect(() => {
if (isHover || !hasFocus || timeout === 0) return void setElapsed(0);
const intervalId = setInterval(() => {
const elapsed = Date.now() - start;
if (elapsed >= timeout)
onClose!();
else
setElapsed(elapsed);
}, 10);
return () => clearInterval(intervalId);
}, [timeout, isHover, hasFocus]);
const timeoutProgress = elapsed / timeout;
return (
<button
className="vc-notification-root"
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
onClick={onClick}
onContextMenu={e => {
e.preventDefault();
e.stopPropagation();
onClose!();
}}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
>
<div className="vc-notification">
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
<div className="vc-notification-content">
<Forms.FormTitle tag="h2">{title}</Forms.FormTitle>
<div>
{richBody ?? <p className="vc-notification-p">{body}</p>}
</div>
</div>
</div>
{image && <img className="vc-notification-img" src={image} alt="" />}
{timeout !== 0 && (
<div
className="vc-notification-progressbar"
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
/>
)}
</button>
);
});

View File

@ -0,0 +1,99 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/settings";
import { Queue } from "@utils/Queue";
import { ReactDOM } from "@webpack/common";
import type { ReactNode } from "react";
import type { Root } from "react-dom/client";
import NotificationComponent from "./NotificationComponent";
const NotificationQueue = new Queue();
let reactRoot: Root;
let id = 42;
function getRoot() {
if (!reactRoot) {
const container = document.createElement("div");
container.id = "vc-notification-container";
document.body.append(container);
reactRoot = ReactDOM.createRoot(container);
}
return reactRoot;
}
export interface NotificationData {
title: string;
body: string;
/**
* Same as body but can be a custom component.
* Will be used over body if present.
* Not supported on desktop notifications, those will fall back to body */
richBody?: ReactNode;
/** Small icon. This is for things like profile pictures and should be square */
icon?: string;
/** Large image. Optimally, this should be around 16x9 but it doesn't matter much. Desktop Notifications might not support this */
image?: string;
onClick?(): void;
onClose?(): void;
color?: string;
}
function _showNotification(notification: NotificationData, id: number) {
const root = getRoot();
return new Promise<void>(resolve => {
root.render(
<NotificationComponent key={id} {...notification} onClose={() => {
notification.onClose?.();
root.render(null);
resolve();
}} />,
);
});
}
function shouldBeNative() {
const { useNative } = Settings.notifications;
if (useNative === "always") return true;
if (useNative === "not-focused") return !document.hasFocus();
return false;
}
export async function requestPermission() {
return (
Notification.permission === "granted" ||
(Notification.permission !== "denied" && (await Notification.requestPermission()) === "granted")
);
}
export async function showNotification(data: NotificationData) {
if (shouldBeNative() && await requestPermission()) {
const { title, body, icon, image, onClick = null, onClose = null } = data;
const n = new Notification(title, {
body,
icon,
image
});
n.onclick = onClick;
n.onclose = onClose;
} else {
NotificationQueue.push(() => _showNotification(data, id++));
}
}

View File

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * from "./Notifications";

View File

@ -0,0 +1,49 @@
.vc-notification-root {
/* clear default button styles */
all: unset;
display: flex;
flex-direction: column;
width: 25vw;
min-height: 10vh;
color: var(--text-normal);
background-color: var(--background-secondary-alt);
position: absolute;
z-index: 2147483647;
right: 1rem;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
}
.vc-notification {
display: flex;
flex-direction: row;
padding: 1.25rem;
gap: 1.25rem;
}
.vc-notification-icon {
height: 4rem;
width: 4rem;
border-radius: 6px;
}
/* Discord adding 3km margin to generic tags */
.vc-notification h2 {
margin: unset;
}
.vc-notification-progressbar {
height: 0.25rem;
border-radius: 5px;
margin-top: auto;
}
.vc-notification-p {
margin: 0.5rem 0 0;
line-height: 140%;
}
.vc-notification-img {
width: 100%;
}

162
src/api/Styles.ts Normal file
View File

@ -0,0 +1,162 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { MapValue } from "type-fest/source/entry";
export type Style = MapValue<typeof VencordStyles>;
export const styleMap = window.VencordStyles ??= new Map();
export function requireStyle(name: string) {
const style = styleMap.get(name);
if (!style) throw new Error(`Style "${name}" does not exist`);
return style;
}
/**
* A style's name can be obtained from importing a stylesheet with `?managed` at the end of the import
* @param name The name of the style
* @returns `false` if the style was already enabled, `true` otherwise
* @example
* import pluginStyle from "./plugin.css?managed";
*
* // Inside some plugin method like "start()" or "[option].onChange()"
* enableStyle(pluginStyle);
*/
export function enableStyle(name: string) {
const style = requireStyle(name);
if (style.dom?.isConnected)
return false;
if (!style.dom) {
style.dom = document.createElement("style");
style.dom.dataset.vencordName = style.name;
}
compileStyle(style);
document.head.appendChild(style.dom);
return true;
}
/**
* @param name The name of the style
* @returns `false` if the style was already disabled, `true` otherwise
* @see {@link enableStyle} for info on getting the name of an imported style
*/
export function disableStyle(name: string) {
const style = requireStyle(name);
if (!style.dom?.isConnected)
return false;
style.dom.remove();
style.dom = null;
return true;
}
/**
* @param name The name of the style
* @returns `true` in most cases, may return `false` in some edge cases
* @see {@link enableStyle} for info on getting the name of an imported style
*/
export const toggleStyle = (name: string) => isStyleEnabled(name) ? disableStyle(name) : enableStyle(name);
/**
* @param name The name of the style
* @returns Whether the style is enabled
* @see {@link enableStyle} for info on getting the name of an imported style
*/
export const isStyleEnabled = (name: string) => requireStyle(name).dom?.isConnected ?? false;
/**
* Sets the variables of a style
* ```ts
* // -- plugin.ts --
* import pluginStyle from "./plugin.css?managed";
* import { setStyleVars } from "@api/Styles";
* import { findByPropsLazy } from "@webpack";
* const classNames = findByPropsLazy("thin", "scrollerBase"); // { thin: "thin-31rlnD scrollerBase-_bVAAt", ... }
*
* // Inside some plugin method like "start()"
* setStyleClassNames(pluginStyle, classNames);
* enableStyle(pluginStyle);
* ```
* ```scss
* // -- plugin.css --
* .plugin-root [--thin]::-webkit-scrollbar { ... }
* ```
* ```scss
* // -- final stylesheet --
* .plugin-root .thin-31rlnD.scrollerBase-_bVAAt::-webkit-scrollbar { ... }
* ```
* @param name The name of the style
* @param classNames An object where the keys are the variable names and the values are the variable values
* @param recompile Whether to recompile the style after setting the variables, defaults to `true`
* @see {@link enableStyle} for info on getting the name of an imported style
*/
export const setStyleClassNames = (name: string, classNames: Record<string, string>, recompile = true) => {
const style = requireStyle(name);
style.classNames = classNames;
if (recompile && isStyleEnabled(style.name))
compileStyle(style);
};
/**
* Updates the stylesheet after doing the following to the sourcecode:
* - Interpolate style classnames
* @param style **_Must_ be a style with a DOM element**
* @see {@link setStyleClassNames} for more info on style classnames
*/
export const compileStyle = (style: Style) => {
if (!style.dom) throw new Error("Style has no DOM element");
style.dom.textContent = style.source
.replace(/\[--(\w+)\]/g, (match, name) => {
const className = style.classNames[name];
return className ? classNameToSelector(className) : match;
});
};
/**
* @param name The classname
* @param prefix A prefix to add each class, defaults to `""`
* @return A css selector for the classname
* @example
* classNameToSelector("foo bar") // => ".foo.bar"
*/
export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join("");
type ClassNameFactoryArg = string | string[] | Record<string, unknown>;
/**
* @param prefix The prefix to add to each class, defaults to `""`
* @returns A classname generator function
* @example
* const cl = classNameFactory("plugin-");
*
* cl("base", ["item", "editable"], { selected: null, disabled: true })
* // => "plugin-base plugin-item plugin-editable plugin-disabled"
*/
export const classNameFactory = (prefix: string = "") => (...args: ClassNameFactoryArg[]) => {
const classNames = new Set<string>();
for (const arg of args) {
if (typeof arg === "string") classNames.add(arg);
else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name));
else if (typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));
}
return Array.from(classNames, name => prefix + name).join(" ");
};

View File

@ -19,11 +19,15 @@
import * as $Badges from "./Badges"; import * as $Badges from "./Badges";
import * as $Commands from "./Commands"; import * as $Commands from "./Commands";
import * as $DataStore from "./DataStore"; import * as $DataStore from "./DataStore";
import * as $MemberListDecorators from "./MemberListDecorators";
import * as $MessageAccessories from "./MessageAccessories"; import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageDecorations from "./MessageDecorations";
import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover"; import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices"; import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList"; import * as $ServerList from "./ServerList";
import * as $Styles from "./Styles";
/** /**
* An API allowing you to listen to Message Clicks or run your own logic * An API allowing you to listen to Message Clicks or run your own logic
@ -31,16 +35,16 @@ import * as $ServerList from "./ServerList";
* *
* If your plugin uses this, you must add MessageEventsAPI to its dependencies * If your plugin uses this, you must add MessageEventsAPI to its dependencies
*/ */
const MessageEvents = $MessageEventsAPI; export const MessageEvents = $MessageEventsAPI;
/** /**
* An API allowing you to create custom notices * An API allowing you to create custom notices
* (snackbars on the top, like the Update prompt) * (snackbars on the top, like the Update prompt)
*/ */
const Notices = $Notices; export const Notices = $Notices;
/** /**
* An API allowing you to register custom commands * An API allowing you to register custom commands
*/ */
const Commands = $Commands; export const Commands = $Commands;
/** /**
* A wrapper around IndexedDB. This can store arbitrarily * A wrapper around IndexedDB. This can store arbitrarily
* large data and supports a lot of datatypes (Blob, Map, ...). * large data and supports a lot of datatypes (Blob, Map, ...).
@ -55,22 +59,37 @@ const Commands = $Commands;
* This is actually just idb-keyval, so if you're familiar with that, you're golden! * This is actually just idb-keyval, so if you're familiar with that, you're golden!
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types} * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
*/ */
const DataStore = $DataStore; export const DataStore = $DataStore;
/** /**
* An API allowing you to add custom components as message accessories * An API allowing you to add custom components as message accessories
*/ */
const MessageAccessories = $MessageAccessories; export const MessageAccessories = $MessageAccessories;
/** /**
* An API allowing you to add custom buttons in the message popover * An API allowing you to add custom buttons in the message popover
*/ */
const MessagePopover = $MessagePopover; export const MessagePopover = $MessagePopover;
/** /**
* An API allowing you to add badges to user profiles * An API allowing you to add badges to user profiles
*/ */
const Badges = $Badges; export const Badges = $Badges;
/** /**
* An API allowing you to add custom elements to the server list * An API allowing you to add custom elements to the server list
*/ */
const ServerList = $ServerList; export const ServerList = $ServerList;
/**
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList }; * An API allowing you to add components as message accessories
*/
export const MessageDecorations = $MessageDecorations;
/**
* An API allowing you to add components to member list users, in both DM's and servers
*/
export const MemberListDecorators = $MemberListDecorators;
/**
* An API allowing you to dynamically load styles
* a
*/
export const Styles = $Styles;
/**
* An API allowing you to display notifications
*/
export const Notifications = $Notifications;

View File

@ -19,7 +19,7 @@
import IpcEvents from "@utils/IpcEvents"; import IpcEvents from "@utils/IpcEvents";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { mergeDefaults } from "@utils/misc"; import { mergeDefaults } from "@utils/misc";
import { OptionType } from "@utils/types"; import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common"; import { React } from "@webpack/common";
import plugins from "~plugins"; import plugins from "~plugins";
@ -27,23 +27,43 @@ import plugins from "~plugins";
const logger = new Logger("Settings"); const logger = new Logger("Settings");
export interface Settings { export interface Settings {
notifyAboutUpdates: boolean; notifyAboutUpdates: boolean;
autoUpdate: boolean;
useQuickCss: boolean; useQuickCss: boolean;
enableReactDevtools: boolean; enableReactDevtools: boolean;
themeLinks: string[]; themeLinks: string[];
frameless: boolean;
transparent: boolean;
winCtrlQ: boolean;
plugins: { plugins: {
[plugin: string]: { [plugin: string]: {
enabled: boolean; enabled: boolean;
[setting: string]: any; [setting: string]: any;
}; };
}; };
notifications: {
timeout: number;
position: "top-right" | "bottom-right";
useNative: "always" | "never" | "not-focused";
};
} }
const DefaultSettings: Settings = { const DefaultSettings: Settings = {
notifyAboutUpdates: true, notifyAboutUpdates: true,
autoUpdate: false,
useQuickCss: true, useQuickCss: true,
themeLinks: [], themeLinks: [],
enableReactDevtools: false, enableReactDevtools: false,
plugins: {} frameless: false,
transparent: false,
winCtrlQ: false,
plugins: {},
notifications: {
timeout: 5000,
position: "bottom-right",
useNative: "not-focused"
}
}; };
try { try {
@ -144,6 +164,7 @@ export const Settings = makeProxy(settings);
* @param paths An optional list of paths to whitelist for rerenders * @param paths An optional list of paths to whitelist for rerenders
* @returns Settings * @returns Settings
*/ */
// TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
export function useSettings(paths?: string[]) { export function useSettings(paths?: string[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {}); const [, forceUpdate] = React.useReducer(() => ({}), {});
@ -198,3 +219,19 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
} }
} }
} }
export function definePluginSettings<D extends SettingsDefinition, C extends SettingsChecks<D>>(def: D, checks?: C) {
const definedSettings: DefinedSettings<D> = {
get store() {
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return Settings.plugins[definedSettings.pluginName] as any;
},
use: settings => useSettings(
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`)
).plugins[definedSettings.pluginName] as any,
def,
checks: checks ?? {},
pluginName: "",
};
return definedSettings;
}

29
src/components/Badge.tsx Normal file
View File

@ -0,0 +1,29 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function Badge({ text, color }): JSX.Element {
return (
<div className="vc-plugins-badge" style={{
backgroundColor: color,
justifySelf: "flex-end",
marginLeft: "auto"
}}>
{text}
</div>
);
}

View File

@ -103,7 +103,7 @@ const ErrorBoundary = LazyComponent(() => {
}; };
}) as }) as
React.ComponentType<React.PropsWithChildren<Props>> & { React.ComponentType<React.PropsWithChildren<Props>> & {
wrap<T extends JSX.IntrinsicAttributes = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>; wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
}; };
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => ( ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (

View File

@ -29,7 +29,12 @@ const setCss = debounce((css: string) => {
}); });
export async function launchMonacoEditor() { export async function launchMonacoEditor() {
const win = open("about:blank", void 0, "popup,width=1000,height=1000")!; const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
const win = open("about:blank", "VencordQuickCss", features);
if (!win) {
alert("Failed to open QuickCSS popup. Make sure to allow popups!");
return;
}
win.setCss = setCss; win.setCss = setCss;
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS); win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
@ -41,4 +46,6 @@ export async function launchMonacoEditor() {
: "vs-dark"; : "vs-dark";
win.document.write(monacoHtml); win.document.write(monacoHtml);
window.__VENCORD_MONACO_WIN__ = new WeakRef(win);
} }

View File

@ -18,6 +18,7 @@
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import { makeCodeblock } from "@utils/misc"; import { makeCodeblock } from "@utils/misc";
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
import { search } from "@webpack"; import { search } from "@webpack";
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common"; import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
@ -41,20 +42,29 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
setModule([keys[0], candidates[keys[0]]]); setModule([keys[0], candidates[keys[0]]]);
}); });
function ReplacementComponent({ module, match, replacement, setReplacementError }) { interface ReplacementComponentProps {
module: [id: number, factory: Function];
match: string | RegExp;
replacement: string | ReplaceFn;
setReplacementError(error: any): void;
}
function ReplacementComponent({ module, match, replacement, setReplacementError }: ReplacementComponentProps) {
const [id, fact] = module; const [id, fact] = module;
const [compileResult, setCompileResult] = React.useState<[boolean, string]>(); const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
const [patchedCode, matchResult, diff] = React.useMemo(() => { const [patchedCode, matchResult, diff] = React.useMemo(() => {
const src: string = fact.toString().replaceAll("\n", ""); const src: string = fact.toString().replaceAll("\n", "");
const canonicalMatch = canonicalizeMatch(match);
try { try {
var patched = src.replace(match, replacement); const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
var patched = src.replace(canonicalMatch, canonicalReplace as string);
setReplacementError(void 0); setReplacementError(void 0);
} catch (e) { } catch (e) {
setReplacementError((e as Error).message); setReplacementError((e as Error).message);
return ["", [], []]; return ["", [], []];
} }
const m = src.match(match); const m = src.match(canonicalMatch);
return [patched, m, makeDiff(src, patched, m)]; return [patched, m, makeDiff(src, patched, m)];
}, [id, match, replacement]); }, [id, match, replacement]);
@ -179,9 +189,10 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
{Object.entries({ {Object.entries({
"$$": "Insert a $", "$$": "Insert a $",
"$&": "Insert the entire match", "$&": "Insert the entire match",
"$`": "Insert the substring before the match", "$`\u200b": "Insert the substring before the match",
"$'": "Insert the substring after the match", "$'": "Insert the substring after the match",
"$n": "Insert the nth capturing group ($1, $2...)" "$n": "Insert the nth capturing group ($1, $2...)",
"$self": "Insert the plugin instance",
}).map(([placeholder, desc]) => ( }).map(([placeholder, desc]) => (
<Forms.FormText key={placeholder}> <Forms.FormText key={placeholder}>
{Parser.parse("`" + placeholder + "`")}: {desc} {Parser.parse("`" + placeholder + "`")}: {desc}
@ -206,7 +217,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
function PatchHelper() { function PatchHelper() {
const [find, setFind] = React.useState<string>(""); const [find, setFind] = React.useState<string>("");
const [match, setMatch] = React.useState<string>(""); const [match, setMatch] = React.useState<string>("");
const [replacement, setReplacement] = React.useState<string | Function>(""); const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
const [replacementError, setReplacementError] = React.useState<string>(); const [replacementError, setReplacementError] = React.useState<string>();

View File

@ -21,7 +21,7 @@ import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { LazyComponent } from "@utils/misc"; import { LazyComponent } from "@utils/misc";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { proxyLazy } from "@utils/proxyLazy"; import { proxyLazy } from "@utils/proxyLazy";
import { OptionType, Plugin } from "@utils/types"; import { OptionType, Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack"; import { findByCode, findByPropsLazy } from "@webpack";
@ -84,6 +84,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const canSubmit = () => Object.values(errors).every(e => !e); const canSubmit = () => Object.values(errors).every(e => !e);
const hasSettings = Boolean(pluginSettings && plugin.options);
React.useEffect(() => { React.useEffect(() => {
(async () => { (async () => {
for (const user of plugin.authors.slice(0, 6)) { for (const user of plugin.authors.slice(0, 6)) {
@ -121,10 +123,9 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
} }
function renderSettings() { function renderSettings() {
if (!pluginSettings || !plugin.options) { if (!hasSettings || !plugin.options) {
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>; return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
} } else {
const options = Object.entries(plugin.options).map(([key, setting]) => { const options = Object.entries(plugin.options).map(([key, setting]) => {
function onChange(newValue: any) { function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue })); setTempSettings(s => ({ ...s, [key]: newValue }));
@ -143,12 +144,14 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
onChange={onChange} onChange={onChange}
onError={onError} onError={onError}
pluginSettings={pluginSettings} pluginSettings={pluginSettings}
definedSettings={plugin.settings}
/> />
); );
}); });
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>; return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
} }
}
function renderMoreUsers(_label: string, count: number) { function renderMoreUsers(_label: string, count: number) {
const sliceCount = plugin.authors.length - count; const sliceCount = plugin.authors.length - count;
@ -172,14 +175,16 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
return ( return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}> <ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
<ModalHeader> <ModalHeader separator={false}>
<Text variant="heading-md/bold">{plugin.name}</Text> <Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
<ModalCloseButton onClick={onClose} />
</ModalHeader> </ModalHeader>
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}> <ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle> <Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
<Forms.FormText>{plugin.description}</Forms.FormText> <Forms.FormText>{plugin.description}</Forms.FormText>
<div style={{ marginTop: 8, marginBottom: 8, width: "fit-content" }}> <Forms.FormTitle tag="h3" style={{ marginTop: 8, marginBottom: 0 }}>Authors</Forms.FormTitle>
<div style={{ width: "fit-content", marginBottom: 8 }}>
<UserSummaryItem <UserSummaryItem
users={authors} users={authors}
count={plugin.authors.length} count={plugin.authors.length}
@ -196,7 +201,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Forms.FormSection> <Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent"> <ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
<plugin.settingsAboutComponent /> <plugin.settingsAboutComponent tempSettings={tempSettings} />
</ErrorBoundary> </ErrorBoundary>
</Forms.FormSection> </Forms.FormSection>
</div> </div>
@ -206,13 +211,14 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
{renderSettings()} {renderSettings()}
</Forms.FormSection> </Forms.FormSection>
</ModalContent> </ModalContent>
<ModalFooter> {hasSettings && <ModalFooter>
<Flex flexDirection="column" style={{ width: "100%" }}> <Flex flexDirection="column" style={{ width: "100%" }}>
<Flex style={{ marginLeft: "auto" }}> <Flex style={{ marginLeft: "auto" }}>
<Button <Button
onClick={onClose} onClick={onClose}
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
color={Button.Colors.RED} color={Button.Colors.WHITE}
look={Button.Looks.LINK}
> >
Cancel Cancel
</Button> </Button>
@ -233,7 +239,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</Flex> </Flex>
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>} {saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
</Flex> </Flex>
</ModalFooter> </ModalFooter>}
</ModalRoot> </ModalRoot>
); );
} }

View File

@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common";
import { ISettingElementProps } from "."; import { ISettingElementProps } from ".";
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) { export function SettingBooleanComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
const def = pluginSettings[id] ?? option.default; const def = pluginSettings[id] ?? option.default;
const [state, setState] = React.useState(def ?? false); const [state, setState] = React.useState(def ?? false);
@ -37,7 +37,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
]; ];
function handleChange(newValue: boolean): void { function handleChange(newValue: boolean): void {
const isValid = (option.isValid && option.isValid(newValue)) ?? true; const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid); if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided."); else if (!isValid) setError("Invalid input provided.");
else { else {
@ -51,7 +51,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle> <Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select <Select
isDisabled={option.disabled?.() ?? false} isDisabled={option.disabled?.call(definedSettings) ?? false}
options={options} options={options}
placeholder={option.placeholder ?? "Select an option"} placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5} maxVisibleItems={5}

View File

@ -23,7 +23,7 @@ import { ISettingElementProps } from ".";
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER); const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
export function SettingNumericComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) { export function SettingNumericComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
function serialize(value: any) { function serialize(value: any) {
if (option.type === OptionType.BIGINT) return BigInt(value); if (option.type === OptionType.BIGINT) return BigInt(value);
return Number(value); return Number(value);
@ -37,7 +37,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
}, [error]); }, [error]);
function handleChange(newValue) { function handleChange(newValue) {
const isValid = (option.isValid && option.isValid(newValue)) ?? true; const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid); if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided."); else if (!isValid) setError("Invalid input provided.");
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) { else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
@ -58,7 +58,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
value={state} value={state}
onChange={handleChange} onChange={handleChange}
placeholder={option.placeholder ?? "Enter a number"} placeholder={option.placeholder ?? "Enter a number"}
disabled={option.disabled?.() ?? false} disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps} {...option.componentProps}
/> />
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>} {error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}

View File

@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common";
import { ISettingElementProps } from "."; import { ISettingElementProps } from ".";
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) { export function SettingSelectComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value; const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
const [state, setState] = React.useState<any>(def ?? null); const [state, setState] = React.useState<any>(def ?? null);
@ -32,7 +32,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
}, [error]); }, [error]);
function handleChange(newValue) { function handleChange(newValue) {
const isValid = (option.isValid && option.isValid(newValue)) ?? true; const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid); if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided."); else if (!isValid) setError("Invalid input provided.");
else { else {
@ -45,7 +45,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle> <Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select <Select
isDisabled={option.disabled?.() ?? false} isDisabled={option.disabled?.call(definedSettings) ?? false}
options={option.options} options={option.options}
placeholder={option.placeholder ?? "Select an option"} placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5} maxVisibleItems={5}

View File

@ -29,7 +29,7 @@ export function makeRange(start: number, end: number, step = 1) {
return ranges; return ranges;
} }
export function SettingSliderComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) { export function SettingSliderComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
const def = pluginSettings[id] ?? option.default; const def = pluginSettings[id] ?? option.default;
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
@ -39,7 +39,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
}, [error]); }, [error]);
function handleChange(newValue: number): void { function handleChange(newValue: number): void {
const isValid = (option.isValid && option.isValid(newValue)) ?? true; const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid); if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided."); else if (!isValid) setError("Invalid input provided.");
else { else {
@ -52,7 +52,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
<Forms.FormSection> <Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle> <Forms.FormTitle>{option.description}</Forms.FormTitle>
<Slider <Slider
disabled={option.disabled?.() ?? false} disabled={option.disabled?.call(definedSettings) ?? false}
markers={option.markers} markers={option.markers}
minValue={option.markers[0]} minValue={option.markers[0]}
maxValue={option.markers[option.markers.length - 1]} maxValue={option.markers[option.markers.length - 1]}

View File

@ -21,7 +21,7 @@ import { Forms, React, TextInput } from "@webpack/common";
import { ISettingElementProps } from "."; import { ISettingElementProps } from ".";
export function SettingTextComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) { export function SettingTextComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null); const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
@ -30,7 +30,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
}, [error]); }, [error]);
function handleChange(newValue) { function handleChange(newValue) {
const isValid = (option.isValid && option.isValid(newValue)) ?? true; const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid); if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided."); else if (!isValid) setError("Invalid input provided.");
else { else {
@ -47,7 +47,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
value={state} value={state}
onChange={handleChange} onChange={handleChange}
placeholder={option.placeholder ?? "Enter a value"} placeholder={option.placeholder ?? "Enter a value"}
disabled={option.disabled?.() ?? false} disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps} {...option.componentProps}
/> />
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>} {error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { PluginOptionBase } from "@utils/types"; import { DefinedSettings, PluginOptionBase } from "@utils/types";
export interface ISettingElementProps<T extends PluginOptionBase> { export interface ISettingElementProps<T extends PluginOptionBase> {
option: T; option: T;
@ -27,8 +27,10 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
}; };
id: string; id: string;
onError(hasError: boolean): void; onError(hasError: boolean): void;
definedSettings?: DefinedSettings;
} }
export * from "../../Badge";
export * from "./SettingBooleanComponent"; export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent"; export * from "./SettingCustomComponent";
export * from "./SettingNumericComponent"; export * from "./SettingNumericComponent";

View File

@ -16,26 +16,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./styles.css";
import * as DataStore from "@api/DataStore";
import { showNotice } from "@api/Notices"; import { showNotice } from "@api/Notices";
import { Settings, useSettings } from "@api/settings"; import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed"; import { handleComponentFailed } from "@components/handleComponentFailed";
import { Badge } from "@components/PluginSettings/components";
import PluginModal from "@components/PluginSettings/PluginModal";
import { Switch } from "@components/Switch";
import { ChangeList } from "@utils/ChangeList"; import { ChangeList } from "@utils/ChangeList";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { classes, LazyComponent } from "@utils/misc"; import { classes, LazyComponent, useAwaiter } from "@utils/misc";
import { openModalLazy } from "@utils/modal"; import { openModalLazy } from "@utils/modal";
import { Plugin } from "@utils/types"; import { Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack"; import { findByCode, findByPropsLazy } from "@webpack";
import { Alerts, Button, Forms, Margins, Parser, React, Select, Switch, Text, TextInput, Toasts, Tooltip } from "@webpack/common"; import { Alerts, Button, Card, Forms, Margins, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
import Plugins from "~plugins"; import Plugins from "~plugins";
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins"; import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
import PluginModal from "./PluginModal";
import * as styles from "./styles";
const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189"); const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper"); const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
@ -54,23 +60,27 @@ function showErrorToast(message: string) {
}); });
} }
interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> { function ReloadRequiredCard({ required }: { required: boolean; }) {
plugins: string[];
}
function ReloadRequiredCard({ plugins, ...props }: ReloadRequiredCardProps) {
if (plugins.length === 0) return null;
const pluginPrefix = plugins.length === 1 ? "The plugin" : "The following plugins require a reload to apply changes:";
const pluginSuffix = plugins.length === 1 ? " requires a reload to apply changes." : ".";
return ( return (
<ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}> <Card className={cl("info-card", { "restart-card": required })}>
<span style={{ margin: "auto 0" }}> {required ? (
{pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix} <>
</span> <Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
<Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button> <Forms.FormText className={cl("dep-text")}>
</ErrorCard> Restart now to apply new plugins and their settings
</Forms.FormText>
<Button color={Button.Colors.YELLOW} onClick={() => location.reload()}>
Restart
</Button>
</>
) : (
<>
<Forms.FormTitle tag="h5">Plugin Management</Forms.FormTitle>
<Forms.FormText>Press the cog wheel or info icon to get more info on a plugin</Forms.FormText>
<Forms.FormText>Plugins with a cog wheel have settings you can modify!</Forms.FormText>
</>
)}
</Card>
); );
} }
@ -78,17 +88,13 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
plugin: Plugin; plugin: Plugin;
disabled: boolean; disabled: boolean;
onRestartNeeded(name: string): void; onRestartNeeded(name: string): void;
isNew?: boolean;
} }
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave }: PluginCardProps) { function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = useSettings(); const settings = useSettings([`plugins.${plugin.name}`]).plugins[plugin.name];
const pluginSettings = settings.plugins[plugin.name];
const [iconHover, setIconHover] = React.useState(false); const isEnabled = () => settings.enabled ?? false;
function isEnabled() {
return pluginSettings?.enabled || plugin.started;
}
function openModal() { function openModal() {
openModalLazy(async () => { openModalLazy(async () => {
@ -110,7 +116,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
return; return;
} else if (restartNeeded) { } else if (restartNeeded) {
// If any dependencies have patches, don't start the plugin yet. // If any dependencies have patches, don't start the plugin yet.
pluginSettings.enabled = true; settings.enabled = true;
onRestartNeeded(plugin.name); onRestartNeeded(plugin.name);
return; return;
} }
@ -118,14 +124,14 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes. // if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
if (plugin.patches) { if (plugin.patches) {
pluginSettings.enabled = !wasEnabled; settings.enabled = !wasEnabled;
onRestartNeeded(plugin.name); onRestartNeeded(plugin.name);
return; return;
} }
// If the plugin is enabled, but hasn't been started, then we can just toggle it off. // If the plugin is enabled, but hasn't been started, then we can just toggle it off.
if (wasEnabled && !plugin.started) { if (wasEnabled && !plugin.started) {
pluginSettings.enabled = !wasEnabled; settings.enabled = !wasEnabled;
return; return;
} }
@ -138,53 +144,38 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
return; return;
} }
pluginSettings.enabled = !wasEnabled; settings.enabled = !wasEnabled;
} }
return ( return (
<Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> <Flex className={cl("card", { "card-disabled": disabled })} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<div className={cl("card-header")}>
<Text variant="text-md/bold" className={cl("name")}>
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
</Text>
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
{plugin.options
? <CogWheel />
: <InfoIcon width="24" height="24" />}
</button>
<Switch <Switch
checked={isEnabled()}
onChange={toggleEnabled} onChange={toggleEnabled}
disabled={disabled} disabled={disabled}
value={isEnabled()}
note={<Text variant="text-md/normal" style={{
height: 40,
overflow: "hidden",
// mfw css is so bad you need whatever this is to get multi line overflow ellipsis to work
textOverflow: "ellipsis",
display: "-webkit-box", // firefox users will cope (it doesn't support it)
WebkitLineClamp: 2,
lineClamp: 2,
WebkitBoxOrient: "vertical",
boxOrient: "vertical"
}}>
{plugin.description}
</Text>}
hideBorder={true}
>
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}>
<Text variant="text-md/bold" style={{ flexGrow: "1" }}>{plugin.name}</Text>
<button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur">
{plugin.options
? <CogWheel
style={{ color: iconHover ? "" : "var(--text-muted)" }}
onMouseEnter={() => setIconHover(true)}
onMouseLeave={() => setIconHover(false)}
/> />
: <InfoIcon </div>
width="24" height="24" <Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text>
style={{ color: iconHover ? "" : "var(--text-muted)" }}
onMouseEnter={() => setIconHover(true)}
onMouseLeave={() => setIconHover(false)}
/>}
</button>
</Flex>
</Switch>
</Flex > </Flex >
); );
} }
export default ErrorBoundary.wrap(function Settings() { enum SearchStatus {
ALL,
ENABLED,
DISABLED
}
export default ErrorBoundary.wrap(function PluginSettings() {
const settings = useSettings(); const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []); const changes = React.useMemo(() => new ChangeList<string>(), []);
@ -225,41 +216,102 @@ export default ErrorBoundary.wrap(function Settings() {
const sortedPlugins = React.useMemo(() => Object.values(Plugins) const sortedPlugins = React.useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []); .sort((a, b) => a.name.localeCompare(b.name)), []);
const [searchValue, setSearchValue] = React.useState({ value: "", status: "all" }); const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL });
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query })); const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
const onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status })); const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => { const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
const showEnabled = searchValue.status === "enabled" || searchValue.status === "all"; const enabled = settings.plugins[plugin.name]?.enabled;
const showDisabled = searchValue.status === "disabled" || searchValue.status === "all"; if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started; if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
if (!searchValue.value.length) return true;
return ( return (
((showEnabled && enabled) || (showDisabled && !enabled)) &&
(
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) || plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase()) plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
)
); );
}; };
const [newPlugins] = useAwaiter(() => DataStore.get("Vencord_existingPlugins").then((cachedPlugins: Record<string, number> | undefined) => {
const now = Date.now() / 1000;
const existingTimestamps: Record<string, number> = {};
const sortedPluginNames = Object.values(sortedPlugins).map(plugin => plugin.name);
const newPlugins: string[] = [];
for (const { name: p } of sortedPlugins) {
const time = existingTimestamps[p] = cachedPlugins?.[p] ?? now;
if ((time + 60 * 60 * 24 * 2) > now) {
newPlugins.push(p);
}
}
DataStore.set("Vencord_existingPlugins", existingTimestamps);
return window._.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins;
}));
type P = JSX.Element | JSX.Element[];
let plugins: P, requiredPlugins: P;
if (sortedPlugins?.length) {
plugins = [];
requiredPlugins = [];
for (const p of sortedPlugins) {
if (!pluginFilter(p)) continue;
const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled);
if (isRequired) {
const tooltipText = p.required
? "This plugin is required for Vencord to function."
: makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled));
requiredPlugins.push(
<Tooltip text={tooltipText} key={p.name}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => changes.handleChange(name)}
disabled={true}
plugin={p}
/>
)}
</Tooltip>
);
} else {
plugins.push(
<PluginCard
onRestartNeeded={name => changes.handleChange(name)}
disabled={false}
plugin={p}
isNew={newPlugins?.includes(p.name)}
key={p.name}
/>
);
}
}
} else {
plugins = requiredPlugins = <Text variant="text-md/normal">No plugins meet search criteria.</Text>;
}
return ( return (
<Forms.FormSection> <Forms.FormSection className={Margins.marginTop16}>
<ReloadRequiredCard required={changes.hasChanges} />
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}> <Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Filters Filters
</Forms.FormTitle> </Forms.FormTitle>
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} /> <div className={cl("filter-controls")}>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} />
<div style={styles.FiltersBar}>
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
<div className={InputStyles.inputWrapper}> <div className={InputStyles.inputWrapper}>
<Select <Select
className={InputStyles.inputDefault} className={InputStyles.inputDefault}
options={[ options={[
{ label: "Show All", value: "all", default: true }, { label: "Show All", value: SearchStatus.ALL, default: true },
{ label: "Show Enabled", value: "enabled" }, { label: "Show Enabled", value: SearchStatus.ENABLED },
{ label: "Show Disabled", value: "disabled" } { label: "Show Disabled", value: SearchStatus.DISABLED }
]} ]}
serialize={String} serialize={String}
select={onStatusChange} select={onStatusChange}
@ -271,49 +323,17 @@ export default ErrorBoundary.wrap(function Settings() {
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle> <Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
<div style={styles.PluginsGrid}> <div className={cl("grid")}>
{sortedPlugins?.length ? sortedPlugins {plugins}
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
.map(plugin => {
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length;
return <PluginCard
onRestartNeeded={name => changes.add(name)}
disabled={plugin.required || !!dependency}
plugin={plugin}
key={plugin.name}
/>;
})
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
}
</div> </div>
<Forms.FormDivider />
<Forms.FormDivider className={Margins.marginTop20} />
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}> <Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Required Plugins Required Plugins
</Forms.FormTitle> </Forms.FormTitle>
<div style={styles.PluginsGrid}> <div className={cl("grid")}>
{sortedPlugins?.length ? sortedPlugins {requiredPlugins}
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
.map(plugin => {
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length;
const tooltipText = plugin.required
? "This plugin is required for Vencord to function."
: makeDependencyList(dependencyCheck(plugin.name, depMap));
return <Tooltip text={tooltipText} key={plugin.name}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => changes.handleChange(name)}
disabled={plugin.required || !!dependency}
plugin={plugin}
/>
)}
</Tooltip>;
})
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
}
</div> </div>
</Forms.FormSection > </Forms.FormSection >
); );
@ -326,11 +346,7 @@ function makeDependencyList(deps: string[]) {
return ( return (
<React.Fragment> <React.Fragment>
<Forms.FormText>This plugin is required by:</Forms.FormText> <Forms.FormText>This plugin is required by:</Forms.FormText>
{deps.map((dep: string) => <Forms.FormText style={{ margin: "0 auto" }}>{dep}</Forms.FormText>)} {deps.map((dep: string) => <Forms.FormText className={cl("dep-text")}>{dep}</Forms.FormText>)}
</React.Fragment> </React.Fragment>
); );
} }
function dependencyCheck(pluginName: string, depMap: Record<string, string[]>): string[] {
return depMap[pluginName]?.filter(d => Settings.plugins[d].enabled) || [];
}

View File

@ -0,0 +1,138 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.vc-plugins-grid {
margin-top: 16px;
display: grid;
grid-gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.vc-plugins-card {
background-color: var(--background-secondary-alt);
color: var(--interactive-active);
border-radius: 8px;
display: block;
height: 100%;
padding: 12px;
width: 100%;
transition: 0.1s ease-out;
transition-property: box-shadow, transform, background, opacity;
}
.vc-plugins-card-disabled {
opacity: 0.6;
}
.vc-plugins-card:hover {
background-color: var(--background-tertiary);
transform: translateY(-1px);
box-shadow: var(--elevation-high);
}
.vc-plugins-card-header {
margin-top: auto;
display: flex;
width: 100%;
justify-content: flex-end;
height: 1.5rem;
align-items: center;
gap: 8px;
}
.vc-plugins-info-button {
height: 24px;
width: 24px;
padding: 0;
background: transparent;
margin-right: 8px;
}
.vc-plugins-settings-button:hover {
color: var(--interactive-hover);
}
.vc-plugins-filter-controls {
display: grid;
height: 40px;
gap: 10px;
grid-template-columns: 1fr 150px;
}
.vc-plugins-badge {
padding: 0 6px;
font-family: var(--font-display);
font-weight: 500;
border-radius: 8px;
height: 16px;
font-size: 12px;
line-height: 16px;
color: var(--white-500);
text-align: center;
}
.vc-plugins-note {
height: 36px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
/* stylelint-disable-next-line property-no-unknown */
box-orient: vertical;
}
.vc-plugins-name {
display: flex;
width: 100%;
align-items: center;
flex-grow: 1;
gap: 8px;
cursor: "default";
}
.vc-plugins-dep-name {
margin: 0 auto;
}
.vc-plugins-info-card {
padding: 1em;
height: 8em;
display: flex;
flex-direction: column;
}
.vc-plugins-info-card div {
line-height: 32px;
}
.vc-plugins-restart-card {
padding: 1em;
background: var(--info-warning-background);
border: 1px solid var(--info-warning-foreground);
color: var(--info-warning-text);
}
.vc-plugins-restart-card button {
margin-top: 0.5em;
}
.vc-plugins-info-button svg:not(:hover, :focus) {
color: var(--text-muted);
}

View File

@ -1,50 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const PluginsGrid: React.CSSProperties = {
marginTop: 16,
display: "grid",
gridGap: 16,
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
};
export const PluginsGridItem: React.CSSProperties = {
backgroundColor: "var(--background-modifier-selected)",
color: "var(--interactive-active)",
borderRadius: 3,
cursor: "pointer",
display: "block",
height: "min-content",
padding: 10,
width: "100%",
};
export const FiltersBar: React.CSSProperties = {
gap: 10,
height: 40,
gridTemplateColumns: "1fr 150px",
display: "grid"
};
export const SettingsIcon: React.CSSProperties = {
height: "24px",
width: "24px",
padding: "0",
background: "transparent",
marginRight: 8
};

View File

@ -0,0 +1,3 @@
.vc-switch-slider {
transition: 100ms transform ease-in-out;
}

76
src/components/Switch.tsx Normal file
View File

@ -0,0 +1,76 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./Switch.css";
import { findByPropsLazy } from "@webpack";
interface SwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
}
const SWITCH_ON = "var(--green-360)";
const SWITCH_OFF = "var(--primary-400)";
const SwitchClasses = findByPropsLazy("slider", "input", "container");
export function Switch({ checked, onChange, disabled }: SwitchProps) {
return (
<div>
<div className={`${SwitchClasses.container} default-colors`} style={{
backgroundColor: checked ? SWITCH_ON : SWITCH_OFF,
opacity: disabled ? 0.3 : 1
}}>
<svg
className={SwitchClasses.slider + " vc-switch-slider"}
viewBox="0 0 28 20"
preserveAspectRatio="xMinYMid meet"
aria-hidden="true"
style={{
transform: checked ? "translateX(12px)" : "translateX(-3px)",
}}
>
<rect fill="white" x="4" y="0" height="20" width="20" rx="10" />
<svg viewBox="0 0 20 20" fill="none">
{checked ? (
<>
<path fill={SWITCH_ON} d="M7.89561 14.8538L6.30462 13.2629L14.3099 5.25755L15.9009 6.84854L7.89561 14.8538Z" />
<path fill={SWITCH_ON} d="M4.08643 11.0903L5.67742 9.49929L9.4485 13.2704L7.85751 14.8614L4.08643 11.0903Z" />
</>
) : (
<>
<path fill={SWITCH_OFF} d="M5.13231 6.72963L6.7233 5.13864L14.855 13.2704L13.264 14.8614L5.13231 6.72963Z" />
<path fill={SWITCH_OFF} d="M13.2704 5.13864L14.8614 6.72963L6.72963 14.8614L5.13864 13.2704L13.2704 5.13864Z" />
</>
)}
</svg>
</svg>
<input
disabled={disabled}
type="checkbox"
className={SwitchClasses.input}
tabIndex={0}
checked={checked}
onChange={e => onChange(e.currentTarget.checked)}
/>
</div>
</div>
);
}

View File

@ -18,19 +18,14 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { classes } from "@utils/misc";
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync"; import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
import { Button, Card, Forms, Margins, Text } from "@webpack/common"; import { Button, Card, Forms, Margins, Text } from "@webpack/common";
function BackupRestoreTab() { function BackupRestoreTab() {
return ( return (
<Forms.FormSection title="Settings Sync"> <Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
<Card style={{ <Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
backgroundColor: "var(--info-warning-background)",
borderColor: "var(--info-warning-foreground)",
color: "var(--info-warning-text)",
padding: "1em",
marginBottom: "0.5em",
}}>
<Flex flexDirection="column"> <Flex flexDirection="column">
<strong>Warning</strong> <strong>Warning</strong>
<span>Importing a settings file will overwrite your current settings.</span> <span>Importing a settings file will overwrite your current settings.</span>
@ -50,7 +45,7 @@ function BackupRestoreTab() {
</Text> </Text>
<Flex> <Flex>
<Button <Button
onClick={uploadSettingsBackup} onClick={() => uploadSettingsBackup()}
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
> >
Import Settings Import Settings

View File

@ -57,7 +57,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
{themeLinks.map(link => ( {themeLinks.map(link => (
<Card style={{ <Card style={{
padding: ".5em", padding: ".5em",
marginBottom: ".5em" marginBottom: ".5em",
marginTop: ".5em"
}} key={link}> }} key={link}>
<Forms.FormTitle tag="h5" style={{ <Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word" overflowWrap: "break-word"
@ -74,11 +75,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
export default ErrorBoundary.wrap(function () { export default ErrorBoundary.wrap(function () {
const settings = useSettings(); const settings = useSettings();
const ref = React.useRef<HTMLTextAreaElement>(); const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
function onBlur() { function onBlur() {
settings.themeLinks = [...new Set( settings.themeLinks = [...new Set(
ref.current!.value themeText
.trim() .trim()
.split(/\n+/) .split(/\n+/)
.map(s => s.trim()) .map(s => s.trim())
@ -88,28 +89,24 @@ export default ErrorBoundary.wrap(function () {
return ( return (
<> <>
<Card style={{ <Card className="vc-settings-card">
padding: "1em",
marginBottom: "1em",
marginTop: "1em"
}}>
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle> <Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>Be careful to use the raw links or github.io links!</Forms.FormText> <Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
<Forms.FormDivider /> <Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle> <Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
<div> <div style={{ marginBottom: ".5em" }}>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes"> <Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
BetterDiscord Themes BetterDiscord Themes
</Link> </Link>
<Link href="https://github.com/search?q=discord+theme">Github</Link> <Link href="https://github.com/search?q=discord+theme">GitHub</Link>
</div> </div>
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText> <Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText> <Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText>
<Forms.FormText> <Forms.FormText>
If the theme has configuration that requires you to edit the file: If the theme has configuration that requires you to edit the file:
<ul> <ul>
<li> Make a github account</li> <li> Make a <Link href="https://github.com/signup">GitHub</Link> account</li>
<li> Click the fork button on the top right</li> <li> Click the fork button on the top right</li>
<li> Edit the file</li> <li> Edit the file</li>
<li> Use the link to your own repository instead</li> <li> Use the link to your own repository instead</li>
@ -122,8 +119,8 @@ export default ErrorBoundary.wrap(function () {
padding: ".5em", padding: ".5em",
border: "1px solid var(--background-modifier-accent)" border: "1px solid var(--background-modifier-accent)"
}} }}
ref={ref} value={themeText}
defaultValue={settings.themeLinks.join("\n")} onChange={e => setThemeText(e.currentTarget.value)}
className={TextAreaProps.textarea} className={TextAreaProps.textarea}
placeholder="Theme Links" placeholder="Theme Links"
spellCheck={false} spellCheck={false}

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
@ -23,7 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { classes, useAwaiter } from "@utils/misc"; import { classes, useAwaiter } from "@utils/misc";
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater"; import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
import { Alerts, Button, Card, Forms, Margins, Parser, React, Toasts } from "@webpack/common"; import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch, Toasts } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
@ -69,14 +70,18 @@ interface CommonProps {
repoPending: boolean; repoPending: boolean;
} }
function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) {
return <Link href={`${repo}/commit/${hash}`} disabled={disabled}>
{hash}
</Link>;
}
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) { function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
return ( return (
<Card style={{ padding: ".5em" }}> <Card style={{ padding: ".5em" }}>
{updates.map(({ hash, author, message }) => ( {updates.map(({ hash, author, message }) => (
<div> <div>
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}> <code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
<code>{hash}</code>
</Link>
<span style={{ <span style={{
marginLeft: "0.5em", marginLeft: "0.5em",
color: "var(--text-normal)" color: "var(--text-normal)"
@ -179,6 +184,8 @@ function Newer(props: CommonProps) {
} }
function Updater() { function Updater() {
const settings = useSettings(["notifyAboutUpdates", "autoUpdate"]);
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." }); const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => { React.useEffect(() => {
@ -192,16 +199,33 @@ function Updater() {
}; };
return ( return (
<Forms.FormSection> <Forms.FormSection className={Margins.marginTop16}>
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
<Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a toast on startup"
disabled={settings.autoUpdate}
>
Get notified about new updates
</Switch>
<Switch
value={settings.autoUpdate}
onChange={(v: boolean) => settings.autoUpdate = v}
note="Automatically update Vencord without confirmation prompt"
>
Automatically update
</Switch>
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle> <Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : ( <Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
<Link href={repo}> <Link href={repo}>
{repo.split("/").slice(-2).join("/")} {repo.split("/").slice(-2).join("/")}
</Link> </Link>
)} ({gitHash})</Forms.FormText> )} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
<Forms.FormDivider /> <Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle> <Forms.FormTitle tag="h5">Updates</Forms.FormTitle>

View File

@ -18,31 +18,73 @@
import { useSettings } from "@api/settings"; import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import IpcEvents from "@utils/IpcEvents"; import IpcEvents from "@utils/IpcEvents";
import { useAwaiter } from "@utils/misc"; import { Margins } from "@utils/margins";
import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common"; import { identity, useAwaiter } from "@utils/misc";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
const st = (style: string) => `vcSettings${style}`; const cl = classNameFactory("vc-settings-");
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
type KeysOfType<Object, Type> = {
[K in keyof Object]: Object[K] extends Type ? K : never;
}[keyof Object];
function VencordSettings() { function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), { const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
fallbackValue: "Loading..." fallbackValue: "Loading..."
}); });
const settings = useSettings(); const settings = useSettings();
const notifSettings = settings.notifications;
const [donateImage] = React.useState( const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
Math.random() > 0.5
? "https://cdn.discordapp.com/emojis/1026533090627174460.png" const isWindows = navigator.platform.toLowerCase().startsWith("win");
: "https://media.discordapp.net/stickers/1039992459209490513.png"
); const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>;
title: string;
note: string;
}> =
[
{
key: "useQuickCss",
title: "Enable Custom CSS",
note: "Loads your Custom CSS"
},
!IS_WEB && {
key: "enableReactDevtools",
title: "Enable React Developer Tools",
note: "Requires a full restart"
},
!IS_WEB && !isWindows && {
key: "frameless",
title: "Disable the window frame",
note: "Requires a full restart"
},
!IS_WEB && {
key: "transparent",
title: "Enable window transparency",
note: "Requires a full restart"
},
!IS_WEB && isWindows && {
key: "winCtrlQ",
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
note: "Requires a full restart"
}
];
return ( return (
<React.Fragment> <React.Fragment>
<DonateCard image={donateImage} /> <DonateCard image={donateImage} />
<Forms.FormSection title="Quick Actions"> <Forms.FormSection title="Quick Actions">
<Card className={st("QuickActionCard")}> <Card className={cl("quick-actions-card")}>
{IS_WEB ? ( {IS_WEB ? (
<Button <Button
onClick={() => require("../Monaco").launchMonacoEditor()} onClick={() => require("../Monaco").launchMonacoEditor()}
@ -82,34 +124,76 @@ function VencordSettings() {
<Forms.FormDivider /> <Forms.FormDivider />
<Forms.FormSection title="Settings"> <Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
<Forms.FormText className={Margins.marginBottom20}> <Forms.FormText className={Margins.bottom20}>
Hint: You can change the position of this settings section in the settings of the "Settings" plugin! Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
</Forms.FormText> </Forms.FormText>
{Switches.map(s => s && (
<Switch <Switch
value={settings.useQuickCss} key={s.key}
onChange={(v: boolean) => settings.useQuickCss = v} value={settings[s.key]}
note="Loads styles from your QuickCss file"> onChange={v => settings[s.key] = v}
Use QuickCss note={s.note}
>
{s.title}
</Switch> </Switch>
{!IS_WEB && ( ))}
<React.Fragment>
<Switch
value={settings.enableReactDevtools}
onChange={(v: boolean) => settings.enableReactDevtools = v}
note="Requires a full restart">
Enable React Developer Tools
</Switch>
<Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a Toast on StartUp">
Get notified about new Updates
</Switch>
</React.Fragment>
)}
</Forms.FormSection> </Forms.FormSection>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{notifSettings.useNative !== "never" && Notification.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
</ErrorCard>
)}
<Forms.FormText className={Margins.bottom8}>
Some plugins may show you notifications. These come in two styles:
<ul>
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
</ul>
</Forms.FormText>
<Select
placeholder="Notification Style"
options={[
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => notifSettings.useNative = v}
isSelected={v => v === notifSettings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={notifSettings.useNative === "always"}
placeholder="Notification Position"
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
select={v => notifSettings.position = v}
isSelected={v => v === notifSettings.position}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider
disabled={notifSettings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={notifSettings.timeout}
onValueChange={v => notifSettings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
</React.Fragment> </React.Fragment>
); );
} }
@ -121,18 +205,10 @@ interface DonateCardProps {
function DonateCard({ image }: DonateCardProps) { function DonateCard({ image }: DonateCardProps) {
return ( return (
<Card style={{ <Card className={cl("card", "donate")}>
padding: "1em",
display: "flex",
flexDirection: "row",
marginBottom: "1em",
marginTop: "1em"
}}>
<div> <div>
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle> <Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
<Forms.FormText> <Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText>
Please consider supporting the Development of Vencord by donating!
</Forms.FormText>
<DonateButton style={{ transform: "translateX(-1em)" }} /> <DonateButton style={{ transform: "translateX(-1em)" }} />
</div> </div>
<img <img
@ -140,7 +216,7 @@ function DonateCard({ image }: DonateCardProps) {
src={image} src={image}
alt="" alt=""
height={128} height={128}
style={{ marginLeft: "auto", transform: "rotate(10deg)" }} style={{ marginLeft: "auto", transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : "" }}
/> />
</Card> </Card>
); );

View File

@ -16,11 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./settingsStyles.css";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy } from "@webpack";
import { Forms, Router, Text } from "@webpack/common"; import { Forms, SettingsRouter, Text } from "@webpack/common";
import cssText from "~fileContent/settingsStyles.css";
import BackupRestoreTab from "./BackupRestoreTab"; import BackupRestoreTab from "./BackupRestoreTab";
import PluginsTab from "./PluginsTab"; import PluginsTab from "./PluginsTab";
@ -28,11 +29,7 @@ import ThemesTab from "./ThemesTab";
import Updater from "./Updater"; import Updater from "./Updater";
import VencordSettings from "./VencordTab"; import VencordSettings from "./VencordTab";
const style = document.createElement("style"); const cl = classNameFactory("vc-settings-");
style.textContent = cssText;
document.head.appendChild(style);
const st = (style: string) => `vcSettings${style}`;
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]'); const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
@ -66,15 +63,15 @@ function Settings(props: SettingsProps) {
<TabBar <TabBar
type={TabBar.Types.TOP} type={TabBar.Types.TOP}
look={TabBar.Looks.BRAND} look={TabBar.Looks.BRAND}
className={st("TabBar")} className={cl("tab-bar")}
selectedItem={tab} selectedItem={tab}
onItemSelect={Router.open} onItemSelect={SettingsRouter.open}
> >
{Object.entries(SettingsTabs).map(([key, { name, component }]) => { {Object.entries(SettingsTabs).map(([key, { name, component }]) => {
if (!component) return null; if (!component) return null;
return <TabBar.Item return <TabBar.Item
id={key} id={key}
className={st("TabBarItem")} className={cl("tab-bar-item")}
key={key}> key={key}>
{name} {name}
</TabBar.Item>; </TabBar.Item>;

View File

@ -1,23 +1,40 @@
.vcSettingsTabBar { .vc-settings-tab-bar {
margin-top: 20px; margin-top: 20px;
margin-bottom: -2px; margin-bottom: -2px;
border-bottom: 2px solid var(--background-modifier-accent); border-bottom: 2px solid var(--background-modifier-accent);
} }
.vcSettingsTabBarItem { .vc-settings-tab-bar-item {
margin-right: 32px; margin-right: 32px;
padding-bottom: 16px; padding-bottom: 16px;
margin-bottom: -2px; margin-bottom: -2px;
} }
.vcSettingsQuickActionCard { .vc-settings-quick-actions-card {
padding: 1em; padding: 1em;
display: flex; display: flex;
gap: 1em; gap: 1em;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
flex-grow: 1; flex-grow: 1;
flex-direction: row; flex-flow: row wrap;
margin-bottom: 1em; margin-bottom: 1em;
} }
.vc-settings-donate {
display: flex;
flex-direction: row;
}
.vc-settings-card {
padding: 1em;
margin-bottom: 1em;
margin-top: 1em;
}
.vc-backup-restore-card {
background-color: var(--info-warning-background);
border-color: var(--info-warning-foreground);
color: var(--info-warning-text);
margin-top: 0;
}

8
src/globals.d.ts vendored
View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { LoDashStatic } from "lodash";
declare global { declare global {
/** /**
@ -37,6 +38,12 @@ declare global {
export var VencordNative: typeof import("./VencordNative").default; export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord"); export var Vencord: typeof import("./Vencord");
export var VencordStyles: Map<string, {
name: string;
source: string;
classNames: Record<string, string>;
dom: HTMLStyleElement | null;
}>;
export var appSettings: { export var appSettings: {
set(setting: string, v: any): void; set(setting: string, v: any): void;
}; };
@ -54,6 +61,7 @@ declare global {
push(chunk: any): any; push(chunk: any): any;
pop(): any; pop(): any;
}; };
_: LoDashStatic;
[k: string]: any; [k: string]: any;
} }
} }

View File

@ -67,9 +67,18 @@ export async function installExt(id: string) {
try { try {
await access(extDir, fsConstants.F_OK); await access(extDir, fsConstants.F_OK);
} catch (err) { } catch (err) {
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`; const url = id === "fmkadmapgofadopljbjfkapdkoienihi"
const buf = await get(url); // React Devtools v4.25
await extract(crxToZip(buf), extDir); // v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843
// Unfortunately, Google does not serve old versions, so this is the only way
? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip"
: `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
const buf = await get(url, {
headers: {
"User-Agent": "Vencord (https://github.com/Vendicated/Vencord)"
}
});
await extract(crxToZip(buf), extDir).catch(console.error);
} }
session.defaultSession.loadExtension(extDir); session.defaultSession.loadExtension(extDir);

View File

@ -21,7 +21,7 @@ import "./updater";
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import IpcEvents from "@utils/IpcEvents"; import IpcEvents from "@utils/IpcEvents";
import { Queue } from "@utils/Queue"; import { Queue } from "@utils/Queue";
import { BrowserWindow, desktopCapturer, ipcMain, shell } from "electron"; import { BrowserWindow, ipcMain, shell } from "electron";
import { mkdirSync, readFileSync, watch } from "fs"; import { mkdirSync, readFileSync, watch } from "fs";
import { open, readFile, writeFile } from "fs/promises"; import { open, readFile, writeFile } from "fs/promises";
import { join } from "path"; import { join } from "path";
@ -44,9 +44,6 @@ export function readSettings() {
} }
} }
// Fix for screensharing in Electron >= 17
ipcMain.handle(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, (_, opts) => desktopCapturer.getSources(opts));
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH)); ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
@ -80,7 +77,7 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
export function initIpc(mainWindow: BrowserWindow) { export function initIpc(mainWindow: BrowserWindow) {
open(QUICKCSS_PATH, "a+").then(fd => { open(QUICKCSS_PATH, "a+").then(fd => {
fd.close(); fd.close();
watch(QUICKCSS_PATH, debounce(async () => { watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
}, 50)); }, 50));
}); });
@ -94,7 +91,8 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
webPreferences: { webPreferences: {
preload: join(__dirname, "preload.js"), preload: join(__dirname, "preload.js"),
contextIsolation: true, contextIsolation: true,
nodeIntegration: false nodeIntegration: false,
sandbox: false
} }
}); });
await win.loadURL(`data:text/html;base64,${monacoHtml}`); await win.loadURL(`data:text/html;base64,${monacoHtml}`);

View File

@ -24,7 +24,7 @@ export async function calculateHashes() {
const hashes = {} as Record<string, string>; const hashes = {} as Record<string, string>;
await Promise.all( await Promise.all(
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => { ["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => {
const fis = createReadStream(join(__dirname, file)); const fis = createReadStream(join(__dirname, file));
const hash = createHash("sha1", { encoding: "hex" }); const hash = createHash("sha1", { encoding: "hex" });
fis.once("end", () => { fis.once("end", () => {

View File

@ -28,7 +28,9 @@ const VENCORD_SRC_DIR = join(__dirname, "..");
const execFile = promisify(cpExecFile); const execFile = promisify(cpExecFile);
const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord")); const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`;
function git(...args: string[]) { function git(...args: string[]) {
const opts = { cwd: VENCORD_SRC_DIR }; const opts = { cwd: VENCORD_SRC_DIR };
@ -66,10 +68,10 @@ async function pull() {
async function build() { async function build() {
const opts = { cwd: VENCORD_SRC_DIR }; const opts = { cwd: VENCORD_SRC_DIR };
let res; const command = isFlatpak ? "flatpak-spawn" : "node";
const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"];
if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts); const res = await execFile(command, args, opts);
else res = await execFile("node", ["scripts/build/build.mjs"], opts);
return !res.stderr.includes("Build failed"); return !res.stderr.includes("Build failed");
} }

View File

@ -37,10 +37,7 @@ async function githubGet(endpoint: string) {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
// "All API requests MUST include a valid User-Agent header. // "All API requests MUST include a valid User-Agent header.
// Requests with no User-Agent header will be rejected." // Requests with no User-Agent header will be rejected."
"User-Agent": VENCORD_USER_AGENT, "User-Agent": VENCORD_USER_AGENT
// todo: perhaps add support for (optional) api token?
// unauthorised rate limit is 60 reqs/h
// https://github.com/settings/tokens/new?description=Vencord%20Updater
} }
}); });
} }
@ -52,7 +49,7 @@ async function calculateGitChanges() {
const res = await githubGet(`/compare/${gitHash}...HEAD`); const res = await githubGet(`/compare/${gitHash}...HEAD`);
const data = JSON.parse(res.toString("utf-8")); const data = JSON.parse(res.toString("utf-8"));
return data.commits.map(c => ({ return data.commits.map((c: any) => ({
// github api only sends the long sha // github api only sends the long sha
hash: c.sha.slice(0, 7), hash: c.sha.slice(0, 7),
author: c.author.login, author: c.author.login,
@ -69,7 +66,7 @@ async function fetchUpdates() {
return false; return false;
data.assets.forEach(({ name, browser_download_url }) => { data.assets.forEach(({ name, browser_download_url }) => {
if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) { if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) {
PendingUpdates.push([name, browser_download_url]); PendingUpdates.push([name, browser_download_url]);
} }
}); });

7
src/modules.d.ts vendored
View File

@ -37,3 +37,10 @@ declare module "~fileContent/*" {
const content: string; const content: string;
export default content; export default content;
} }
declare module "*.css";
declare module "*.css?managed" {
const name: string;
export default name;
}

View File

@ -17,7 +17,7 @@
*/ */
import { app, autoUpdater } from "electron"; import { app, autoUpdater } from "electron";
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs"; import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
import { basename, dirname, join } from "path"; import { basename, dirname, join } from "path";
const { setAppUserModelId } = app; const { setAppUserModelId } = app;
@ -44,6 +44,7 @@ function isNewer($new: string, old: string) {
} }
function patchLatest() { function patchLatest() {
try {
const currentAppPath = dirname(process.execPath); const currentAppPath = dirname(process.execPath);
const currentVersion = basename(currentAppPath); const currentVersion = basename(currentAppPath);
const discordPath = join(currentAppPath, ".."); const discordPath = join(currentAppPath, "..");
@ -56,46 +57,37 @@ function patchLatest() {
if (latestVersion === currentVersion) return; if (latestVersion === currentVersion) return;
const app = join(discordPath, latestVersion, "resources", "app"); const resources = join(discordPath, latestVersion, "resources");
if (existsSync(app)) return; const app = join(resources, "app.asar");
const _app = join(resources, "_app.asar");
if (!existsSync(app) || statSync(app).isDirectory()) return;
console.info("[Vencord] Detected Host Update. Repatching..."); console.info("[Vencord] Detected Host Update. Repatching...");
const patcherPath = join(__dirname, "patcher.js"); renameSync(app, _app);
mkdirSync(app); mkdirSync(app);
writeFileSync(join(app, "package.json"), JSON.stringify({ writeFileSync(join(app, "package.json"), JSON.stringify({
name: "discord", name: "discord",
main: "index.js" main: "index.js"
})); }));
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(patcherPath)});`); writeFileSync(join(app, "index.js"), `require(${JSON.stringify(join(__dirname, "patcher.js"))});`);
} catch (err) {
console.error("[Vencord] Failed to repatch latest host update", err);
}
} }
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we // Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
// need to reinject // need to reinject
function patchUpdater() { function patchUpdater() {
const main = require.main!;
const buildInfo = require(join(process.resourcesPath, "build_info.json"));
try { try {
if (buildInfo?.newUpdater) { const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js");
const autoStartScript = join(main.filename, "..", "autoStart", "win32.js");
const { update } = require(autoStartScript); const { update } = require(autoStartScript);
// New Updater Injection
require.cache[autoStartScript]!.exports.update = function () { require.cache[autoStartScript]!.exports.update = function () {
patchLatest();
update.apply(this, arguments); update.apply(this, arguments);
};
} else {
const hostUpdaterScript = join(main.filename, "..", "hostUpdater.js");
const { quitAndInstall } = require(hostUpdaterScript);
// Old Updater Injection
require.cache[hostUpdaterScript]!.exports.quitAndInstall = function () {
patchLatest(); patchLatest();
quitAndInstall.apply(this, arguments);
}; };
}
} catch { } catch {
// OpenAsar uses electrons autoUpdater on Windows // OpenAsar uses electrons autoUpdater on Windows
const { quitAndInstall } = autoUpdater; const { quitAndInstall } = autoUpdater;

View File

@ -17,8 +17,7 @@
*/ */
import { onceDefined } from "@utils/onceDefined"; import { onceDefined } from "@utils/onceDefined";
import electron, { app, BrowserWindowConstructorOptions } from "electron"; import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import { readFileSync } from "fs";
import { dirname, join } from "path"; import { dirname, join } from "path";
import { initIpc } from "./ipcMain"; import { initIpc } from "./ipcMain";
@ -43,16 +42,48 @@ require.main!.filename = join(asarPath, discordPkg.main);
app.setAppPath(asarPath); app.setAppPath(asarPath);
if (!process.argv.includes("--vanilla")) { if (!process.argv.includes("--vanilla")) {
let settings: typeof import("@api/settings").Settings = {} as any;
try {
settings = JSON.parse(readSettings());
} catch { }
// Repatch after host updates on Windows // Repatch after host updates on Windows
if (process.platform === "win32") if (process.platform === "win32") {
require("./patchWin32Updater"); require("./patchWin32Updater");
if (settings.winCtrlQ) {
const originalBuild = Menu.buildFromTemplate;
Menu.buildFromTemplate = function (template) {
if (template[0]?.label === "&File") {
const { submenu } = template[0];
if (Array.isArray(submenu)) {
submenu.push({
label: "Quit (Hidden)",
visible: false,
acceleratorWorksWhenHidden: true,
accelerator: "Control+Q",
click: () => app.quit()
});
}
}
return originalBuild.call(this, template);
};
}
}
class BrowserWindow extends electron.BrowserWindow { class BrowserWindow extends electron.BrowserWindow {
constructor(options: BrowserWindowConstructorOptions) { constructor(options: BrowserWindowConstructorOptions) {
if (options?.webPreferences?.preload && options.title) { if (options?.webPreferences?.preload && options.title) {
const original = options.webPreferences.preload; const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, "preload.js"); options.webPreferences.preload = join(__dirname, "preload.js");
options.webPreferences.sandbox = false; options.webPreferences.sandbox = false;
if (settings.frameless) {
options.frame = false;
}
if (settings.transparent) {
options.transparent = true;
options.backgroundColor = "#00000000";
}
process.env.DISCORD_PRELOAD = original; process.env.DISCORD_PRELOAD = original;
@ -100,8 +131,7 @@ if (!process.argv.includes("--vanilla")) {
}); });
try { try {
const settings = JSON.parse(readSettings()); if (settings?.enableReactDevtools)
if (settings.enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi") installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools")) .then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err)); .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
@ -160,21 +190,4 @@ if (!process.argv.includes("--vanilla")) {
} }
console.log("[Vencord] Loading original Discord app.asar"); console.log("[Vencord] Loading original Discord app.asar");
// Legacy Vencord Injector requires "../app.asar". However, because we
// restore the require.main above this is messed up, so monkey patch Module._load to
// redirect such requires
// FIXME: remove this eventually
if (readFileSync(injectorPath, "utf-8").includes('require("../app.asar")')) {
console.warn("[Vencord] [--> WARNING <--] You have a legacy Vencord install. Please reinject");
const Module = require("module");
const loadModule = Module._load;
Module._load = function (path: string) {
if (path === "../app.asar") {
Module._load = loadModule;
arguments[0] = require.main!.filename;
}
return loadModule.apply(this, arguments);
};
} else {
require(require.main!.filename); require(require.main!.filename);
}

View File

@ -0,0 +1,42 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "AlwaysTrust",
description: "Removes the annoying untrusted domain and suspicious file popup",
authors: [Devs.zt],
patches: [
{
find: ".displayName=\"MaskedLinkStore\"",
replacement: {
match: /\.isTrustedDomain=function\(.\){return.+?};/,
replace: ".isTrustedDomain=function(){return true};"
}
},
{
find: "\"github.com\":new RegExp(\"\\\\/releases\\\\S*\\\\/download\"),",
replacement: {
match: /const o=JSON.parse\('\[.+?'\)/,
replace: "const o=[]"
}
}
]
});

View File

@ -36,7 +36,7 @@ export default definePlugin({
replacement: { replacement: {
match: /uploadFiles:(.{1,2}),/, match: /uploadFiles:(.{1,2}),/,
replace: replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=Vencord.Plugins.plugins.AnonymiseFileNames.anonymise(f.filename)),$1(...args)),", "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),",
}, },
}, },
], ],

View File

@ -66,11 +66,20 @@ export default definePlugin({
/* Patch the badge list component on user profiles */ /* Patch the badge list component on user profiles */
{ {
find: "Messages.PROFILE_USER_BADGES,role:", find: "Messages.PROFILE_USER_BADGES,role:",
replacement: { replacement: [
{
match: /src:(\w{1,3})\[(\w{1,3})\.key\],/, match: /src:(\w{1,3})\[(\w{1,3})\.key\],/,
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} /> // <img src={badge.image ?? imageMap[badge.key]} {...badge.props} />
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,` replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
},
{
match: /spacing:(\d{1,2}),children:(.{1,40}(\i)\.jsx.+?(\i)\.onClick.+?\)})},/,
// if the badge provides it's own component, render that instead of an image
// the badge also includes info about the user that has it (type BadgeUserArgs), which is why it's passed as props
replace: (_, s, origBadgeComponent, React, badge) =>
`spacing:${s},children:${badge}.component ? () => (0,${React}.jsx)(${badge}.component, { ...${badge} }) : ${origBadgeComponent}},`
} }
]
} }
], ],

View File

@ -50,10 +50,10 @@ export default definePlugin({
}, },
// Show plugin name instead of "Built-In" // Show plugin name instead of "Built-In"
{ {
find: "().source,children", find: ".source,children",
replacement: { replacement: {
// ...children: p?.name // ...children: p?.name
match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\(\)\.source,children:)[^}]+/, match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\.source,children:)[^}]+/,
replace: "$1.plugin||($&)" replace: "$1.plugin||($&)"
} }
} }

View File

@ -0,0 +1,42 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MemberListDecoratorsAPI",
description: "API to add decorators to member list (both in servers and DMs)",
authors: [Devs.TheSun],
patches: [
{
find: "lostPermissionTooltipText,",
replacement: {
match: /Fragment,{children:\[(.{30,80})\]/,
replace: "Fragment,{children:Vencord.Api.MemberListDecorators.__addDecoratorsToList(this.props).concat($1)"
}
},
{
find: "PrivateChannel.renderAvatar",
replacement: {
match: /(subText:(.{1,2})\.renderSubtitle\(\).{1,50}decorators):(.{30,100}:null)/,
replace: "$1:Vencord.Api.MemberListDecorators.__addDecoratorsToList($2.props).concat($3)"
}
}
],
});

View File

@ -25,9 +25,9 @@ export default definePlugin({
authors: [Devs.Cyn], authors: [Devs.Cyn],
patches: [ patches: [
{ {
find: "_messageAttachmentToEmbedMedia", find: ".Messages.REMOVE_ATTACHMENT_BODY",
replacement: { replacement: {
match: /(\(\)\.container\)?,children:)(\[[^\]]+\])(}\)\};return)/, match: /(.container\)?,children:)(\[[^\]]+\])(}\)\};return)/,
replace: (_, pre, accessories, post) => replace: (_, pre, accessories, post) =>
`${pre}Vencord.Api.MessageAccessories._modifyAccessories(${accessories},this.props)${post}`, `${pre}Vencord.Api.MessageAccessories._modifyAccessories(${accessories},this.props)${post}`,
}, },

View File

@ -20,16 +20,16 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
export default definePlugin({ export default definePlugin({
name: "NoReplyMention", name: "MessageDecorationsAPI",
description: "Disables reply pings by default", description: "API to add decorations to messages",
authors: [Devs.DustyAngel47], authors: [Devs.TheSun],
patches: [ patches: [
{ {
find: "CREATE_PENDING_REPLY:function", find: ".withMentionPrefix",
replacement: { replacement: {
match: /CREATE_PENDING_REPLY:function\((.{1,2})\){/, match: /(.roleDot.{10,50}{children:.{1,2})}\)/,
replace: "CREATE_PENDING_REPLY:function($1){$1.shouldMention=false;" replace: "$1.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))})"
} }
} }
] ],
}); });

View File

@ -22,12 +22,17 @@ import definePlugin from "@utils/types";
export default definePlugin({ export default definePlugin({
name: "MessagePopoverAPI", name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.", description: "API to add buttons to message popovers.",
authors: [Devs.KingFish], authors: [Devs.KingFish, Devs.Ven],
patches: [{ patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: { replacement: {
match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,90}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/, // foo && !bar ? createElement(blah,...makeElement(addReactionData))
replace: "$1...Vencord.Api.MessagePopover._buildPopoverElements($2,$4),$3" match: /(\i&&!\i)\?\(0,\i\.jsxs?\)\(.{0,20}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
replace: (m, bools, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable");
return `...(${bools}?Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}):[]),${m}`;
}
} }
}], }],
}); });

View File

@ -16,12 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
migratePluginSettings("NoticesAPI", "NoticesApi");
export default definePlugin({ export default definePlugin({
name: "NoticesAPI", name: "NoticesAPI",
description: "Fixes notices being automatically dismissed", description: "Fixes notices being automatically dismissed",
@ -29,16 +26,16 @@ export default definePlugin({
required: true, required: true,
patches: [ patches: [
{ {
find: "updateNotice:", find: 'displayName="NoticeStore"',
replacement: [ replacement: [
{ {
match: /;(.{1,2}=null;)(?=.{0,50}updateNotice)/g, match: /;.{1,2}=null;.{0,70}getPremiumSubscription/g,
replace: replace:
";if(Vencord.Api.Notices.currentNotice)return !1;$1" ";if(Vencord.Api.Notices.currentNotice)return false$&"
}, },
{ {
match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/, match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
replace: '{if($1?.id=="VencordNotice")return ($1=null,Vencord.Api.Notices.nextNotice(),true);' replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);'
} }
] ]
} }

View File

@ -30,7 +30,7 @@ const assetManager = mapMangledModuleLazy(
} }
); );
const rpcManager = findByCodeLazy(".APPLICATION_RPC("); const lookupRpcApp = findByCodeLazy(".APPLICATION_RPC(");
async function lookupAsset(applicationId: string, key: string): Promise<string> { async function lookupAsset(applicationId: string, key: string): Promise<string> {
return (await assetManager.getAsset(applicationId, [key, undefined]))[0]; return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
@ -39,7 +39,7 @@ async function lookupAsset(applicationId: string, key: string): Promise<string>
const apps: any = {}; const apps: any = {};
async function lookupApp(applicationId: string): Promise<string> { async function lookupApp(applicationId: string): Promise<string> {
const socket: any = {}; const socket: any = {};
await rpcManager.lookupApp(socket, applicationId); await lookupRpcApp(socket, applicationId);
return socket.application; return socket.application;
} }

View File

@ -31,7 +31,7 @@ export default definePlugin({
replacement: { replacement: {
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/, match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
replace: replace:
"Vencord.Plugins.plugins.BetterGifAltText.altify(e);$1", "$self.altify(e);$1",
}, },
}, },
{ {
@ -39,7 +39,7 @@ export default definePlugin({
replacement: { replacement: {
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/, match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
replace: replace:
"?($1.alt='GIF',Vencord.Plugins.plugins.BetterGifAltText.altify($1))", "?($1.alt='GIF',$self.altify($1))",
}, },
}, },
], ],

View File

@ -33,7 +33,7 @@ export default definePlugin({
find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z", find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z",
replacement: { replacement: {
match: /viewBox:"0 0 20 20"/, match: /viewBox:"0 0 20 20"/,
replace: "$&,onClick:()=>Vencord.Plugins.plugins.BetterRoleDot.copyToClipBoard(e.color),style:{cursor:'pointer'}", replace: "$&,onClick:()=>$self.copyToClipBoard(e.color),style:{cursor:'pointer'}",
}, },
}, },
{ {
@ -41,7 +41,7 @@ export default definePlugin({
all: true, all: true,
predicate: () => Settings.plugins.BetterRoleDot.bothStyles, predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
replacement: { replacement: {
match: /"(?:username|dot)"===\w\b/g, match: /"(?:username|dot)"===\w(?!\.\w)/g,
replace: "true", replace: "true",
}, },
}, },

View File

@ -43,12 +43,12 @@ export default definePlugin({
patches: [ patches: [
{ {
find: "().embedWrapper,embed", find: ".embedWrapper,embed",
replacement: [{ replacement: [{
match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\(\)\.embedWrapper)/g, match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\.embedWrapper)/g,
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')" replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
}, { }, {
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\(\)\.embedWrapper)/g, match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')" replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
}] }]
} }

View File

@ -74,8 +74,8 @@ export default definePlugin({
patches: [{ patches: [{
find: ".renderConnectionStatus=", find: ".renderConnectionStatus=",
replacement: { replacement: {
match: /(?<=renderConnectionStatus=.+\(\)\.channel,children:)\w/, match: /(?<=renderConnectionStatus=.+\.channel,children:)\w/,
replace: "[$&, Vencord.Plugins.plugins.CallTimer.renderTimer(this.props.channel.id)]" replace: "[$&, $self.renderTimer(this.props.channel.id)]"
} }
}], }],
renderTimer(channelId: string) { renderTimer(channelId: string) {

View File

@ -0,0 +1,37 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "ColorSighted",
description: "Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord",
authors: [Devs.lewisakura],
patches: [
{
find: "Masks.STATUS_ONLINE",
replacement: {
// we can use global replacement here - these are specific to the status icons and are used nowhere else,
// so it keeps the patch and plugin small and simple
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
replace: "Masks.STATUS_ONLINE"
}
}
]
});

View File

@ -99,7 +99,7 @@ export default definePlugin({
const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4"); const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4");
const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR"); const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR");
const file = new File([buf], newName, { type: "video/mp4" }); const file = new File([buf], newName, { type: "video/mp4" });
setImmediate(() => promptToUpload([file], ctx.channel, DRAFT_TYPE)); setTimeout(() => promptToUpload([file], ctx.channel, DRAFT_TYPE), 10);
} }
}] }]
}); });

251
src/plugins/customRPC.tsx Normal file
View File

@ -0,0 +1,251 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/settings";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import { useAwaiter } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
import {
FluxDispatcher,
Forms,
GuildStore,
React,
SelectedChannelStore,
SelectedGuildStore,
UserStore
} from "@webpack/common";
const ActivityComponent = findByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const Colors = findByPropsLazy("profileColors");
// START yoinked from lastfm.tsx
const assetManager = mapMangledModuleLazy(
"getAssetImage: size must === [number, number] for Twitch",
{
getAsset: filters.byCode("apply("),
}
);
async function getApplicationAsset(key: string): Promise<string> {
return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0];
}
interface ActivityAssets {
large_image?: string;
large_text?: string;
small_image?: string;
small_text?: string;
}
interface Activity {
state: string;
details?: string;
timestamps?: {
start?: Number;
end?: Number;
};
assets?: ActivityAssets;
buttons?: Array<string>;
name: string;
application_id: string;
metadata?: {
button_urls?: Array<string>;
};
type: ActivityType;
flags: Number;
}
enum ActivityType {
PLAYING = 0,
LISTENING = 2,
WATCHING = 3,
COMPETING = 5
}
// END
const strOpt = (description: string) => ({
type: OptionType.STRING,
description,
onChange: setRpc
}) as const;
const numOpt = (description: string) => ({
type: OptionType.NUMBER,
description,
onChange: setRpc
}) as const;
const choice = (label: string, value: any, _default?: Boolean) => ({
label,
value,
default: _default
}) as const;
const choiceOpt = (description: string, options) => ({
type: OptionType.SELECT,
description,
onChange: setRpc,
options
}) as const;
const settings = definePluginSettings({
appID: strOpt("The ID of the application for the rich presence."),
appName: strOpt("The name of the presence."),
details: strOpt("Line 1 of rich presence."),
state: strOpt("Line 2 of rich presence."),
type: choiceOpt("Type of presence", [
choice("Playing", ActivityType.PLAYING, true),
choice("Listening", ActivityType.LISTENING),
choice("Watching", ActivityType.WATCHING),
choice("Competing", ActivityType.COMPETING)
]),
startTime: numOpt("Unix Timestamp for beginning of activity."),
endTime: numOpt("Unix Timestamp for end of activity."),
imageBig: strOpt("Sets the big image to the specified image."),
imageBigTooltip: strOpt("Sets the tooltip text for the big image."),
imageSmall: strOpt("Sets the small image to the specified image."),
imageSmallTooltip: strOpt("Sets the tooltip text for the small image."),
buttonOneText: strOpt("The text for the first button"),
buttonOneURL: strOpt("The URL for the first button"),
buttonTwoText: strOpt("The text for the second button"),
buttonTwoURL: strOpt("The URL for the second button")
});
async function createActivity(): Promise<Activity | undefined> {
const {
appID,
appName,
details,
state,
type,
startTime,
endTime,
imageBig,
imageBigTooltip,
imageSmall,
imageSmallTooltip,
buttonOneText,
buttonOneURL,
buttonTwoText,
buttonTwoURL
} = settings.store;
if (!appName) return;
const activity: Activity = {
application_id: appID || "0",
name: appName,
state,
details,
type,
flags: 1 << 0,
};
if (startTime) {
activity.timestamps = {
start: startTime,
};
if (endTime) {
activity.timestamps.end = endTime;
}
}
if (buttonOneText) {
activity.buttons = [
buttonOneText,
buttonTwoText
].filter(Boolean);
activity.metadata = {
button_urls: [
buttonOneURL,
buttonTwoURL
].filter(Boolean)
};
}
if (imageBig) {
activity.assets = {
large_image: await getApplicationAsset(imageBig),
large_text: imageBigTooltip
};
}
if (imageSmall) {
activity.assets = {
...activity.assets,
small_image: await getApplicationAsset(imageSmall),
small_text: imageSmallTooltip
};
}
for (const k in activity) {
if (k === "type") continue; // without type, the presence is considered invalid.
const v = activity[k];
if (!v || v.length === 0)
delete activity[k];
}
// WHAT DO YOU WANT FROM ME
// eslint-disable-next-line consistent-return
return activity;
}
async function setRpc(disable?: Boolean) {
const activity: Activity | undefined = await createActivity();
FluxDispatcher.dispatch({
type: "LOCAL_ACTIVITY_UPDATE",
activity: !disable ? activity : {}
});
}
export default definePlugin({
name: "CustomRPC",
description: "Allows you to set a custom rich presence.",
authors: [Devs.captain],
start: setRpc,
stop: () => setRpc(true),
settings,
settingsAboutComponent: () => {
const activity = useAwaiter(createActivity);
return (
<>
<Forms.FormTitle tag="h2">NOTE:</Forms.FormTitle>
<Forms.FormText>
You will need to <Link href="https://discord.com/developers/applications">create an
application</Link> and
get its ID to use this plugin.
</Forms.FormText>
<Forms.FormDivider />
<div style={{ width: "284px" }} className={Colors.profileColors}>
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
application={{ id: settings.store.appID }}
user={UserStore.getCurrentUser()} />}
</div>
</>
);
}
});

View File

@ -0,0 +1,35 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "DisableDMCallIdle",
description: "Disables automatically getting kicked from a DM voice call after 5 minutes.",
authors: [Devs.Nuckyz],
patches: [
{
find: ".Messages.BOT_CALL_IDLE_DISCONNECT",
replacement: {
match: /function (?<functionName>.{1,3})\(\){.{1,100}\.Messages\.BOT_CALL_IDLE_DISCONNECT.+?}}/,
replace: "function $<functionName>(){}",
},
},
],
});

View File

@ -50,14 +50,14 @@ function getGuildCandidates(isAnimated: boolean) {
} }
async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) { async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) {
const data = await fetch(`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`) const data = await fetch(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`)
.then(r => r.blob()); .then(r => r.blob());
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
uploadEmoji({ uploadEmoji({
guildId, guildId,
name, name: name.split("~")[0],
image: reader.result image: reader.result
}).then(() => { }).then(() => {
Toasts.show({ Toasts.show({
@ -187,7 +187,7 @@ export default definePlugin({
find: "open-native-link", find: "open-native-link",
replacement: { replacement: {
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/, match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
replace: "$&,Vencord.Plugins.plugins.EmoteCloner.makeMenu(arguments[2])" replace: "$&,$self.makeMenu(arguments[2])"
}, },
}, },
@ -226,7 +226,7 @@ export default definePlugin({
<img <img
role="presentation" role="presentation"
aria-hidden aria-hidden
src={`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`} src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`}
alt="" alt=""
height={24} height={24}
width={24} width={24}

View File

@ -42,7 +42,7 @@ export default definePlugin({
}, { }, {
find: 'type:"user",revision', find: 'type:"user",revision',
replacement: { replacement: {
match: /(\w)\|\|"CONNECTION_OPEN".+?;/g, match: /!(\w{1,3})&&"CONNECTION_OPEN".+?;/g,
replace: "$1=!0;" replace: "$1=!0;"
}, },
}, { }, {

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