Compare commits

..

259 Commits

Author SHA1 Message Date
Vendicated
ee943c4284 Bump to v1.1.3 2023-03-28 19:09:48 +02:00
Vendicated
337b3709d6 types: Make ErrorBoundary.wrap explicitly return Function 2023-03-28 19:06:58 +02:00
Elliott Tallis
eb318c678f feat(ViewRaw): Improve View Raw action icon (#720)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-28 16:59:30 +00:00
Vendicated
081df6beb7 Fix SilentMessage/SilentTyping toggles showing in wrong sections
Closes #656
2023-03-28 18:56:12 +02:00
Vendicated
ab911b48b5 TypingTweaks: Make names open profile on click
Closes #718
2023-03-28 18:43:45 +02:00
Skye
8cb3491086 feat(uwuify): improve uwuification algorithm (#706)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-28 16:23:51 +00:00
Lewis Crichton
ee794d140f fix: no more theme box obliteration (#707)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-28 16:20:06 +00:00
Vendicated
a00542b61b MessageLinkEmbeds: Fix weird commas in title 2023-03-26 01:27:30 +01:00
Vendicated
041a13c9d3 DevCompanion: Always use original 2023-03-26 01:27:01 +01:00
Lewis Crichton
24aa90bd9c fix API plugins being force enabled unconditionally (#704)
* only enable dependencies if required

* fixme note
2023-03-25 15:20:00 +00:00
Ven
c574f53417 Add Code of Conduct (#680) 2023-03-25 12:41:39 +00:00
Nuckyz
92b84a9e94 Fix broken patches (#701) 2023-03-25 08:42:26 +00:00
RuiNtD
bbf3c74cb2 Update LastFM plugin (#483)
Co-authored-by: Ven <vendicated@riseup.net>
Co-authored-by: Sofia Lima <me@dzshn.xyz>
2023-03-25 04:00:27 +01:00
hunter
93cb51a975 feat(MessageEvents): Promisable send/edit listeners (#514)
* promisable send/edit listeners

* added self

* Apply suggestions from code review

Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>

* fix patches

---------

Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-25 03:54:20 +01:00
Syncx
0b4ae729a3 feat(plugin): SearchReply (#551)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-03-25 02:37:29 +00:00
TheKodeToad
b90392576e PronounDB: Add support for compact mode & clean up (#604) 2023-03-25 01:30:24 +00:00
Ven
e143260891 MessageLogger: Add context menu entry to remove history (#693) 2023-03-25 00:55:40 +00:00
Đỗ Văn Hoài Tuân
644c5c4faa Make Vencord title look consistent with Discord (#685)
closes (#649)
2023-03-25 00:42:18 +00:00
Ven
8d8cedd72c Also add Emote Cloner to Emote picker rightclick menu (#664) 2023-03-24 03:42:38 +01:00
Nuckyz
082ac62eda feat(FakeNitro): Transform fake emojis into real ones (#669) 2023-03-23 10:45:39 +00:00
Nuckyz
7923a790e6 Fix MessagePopoverAPI and any error Fake Nitro client theme bypass might have (#665) 2023-03-23 02:11:28 -03:00
Vendicated
1368c25824 ci: Auto generate plugin json 2023-03-23 04:37:53 +01:00
iwa
d0b3678ad6 fix messagelogger deleted styles (#642)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-22 04:37:04 +01:00
Vendicated
cae8b1a93b Bump to v1.1.2 2023-03-22 04:07:12 +01:00
Nuckyz
a1c1fec8cb Improve Fake Nitro client themes bypass (#654) 2023-03-22 03:01:32 +00:00
Lewis Crichton
55a66dbb39 fix(RoleColorEverywhere): MessageLinkEmbeds DM error (#648) 2023-03-21 23:57:53 -03:00
Nuckyz
a2f0c912f0 Fix Fake Nitro making Discord unusable and ColorSighted not working sometimes (#640) 2023-03-21 09:41:31 +00:00
Nuckyz
e29bbf73aa Fix Nitro Themes with FakeNitro (#639) 2023-03-21 09:03:28 +00:00
Nuckyz
0ba3e9f469 I'm sorry for hurting you Fake Nitro (#637) 2023-03-21 06:41:11 +00:00
Nuckyz
6f200e9218 Fix grammar and SHC patches matching wrong var (#636) 2023-03-21 06:30:09 +00:00
Nuckyz
586b26d2d4 Fixes and ShowHiddenChannels improvements (#634)
~ Fixes #630
~ Fixes #618
2023-03-21 06:07:16 +00:00
Ven
d482d33d6f Fix the infamous MessageClickActions bug 2023-03-21 03:19:02 +01:00
Dossy Shiobara
37c2a8a5de fix: settings input validation and error handling (#609)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-21 02:16:01 +00:00
Dossy Shiobara
265547213c docs: clarify empty patches array behavior (#610)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-21 02:14:27 +00:00
afn
87e46f5a5a chore(friendInvites): improve descriptions formatting (#628) 2023-03-21 03:13:11 +01:00
Nuckyz
e36f4e5b0a Fixes and make guild tooltip show users inside hidden voice channels (#613)
* Fix #509

* Fix #597

* Fix #594
2023-03-19 22:03:33 -03:00
Xinto
4aff11421f Replace update notices with notifications (#558) 2023-03-19 09:21:26 +00:00
Nuckyz
ea642d9e90 Fix #598 (#612) 2023-03-19 08:44:11 +00:00
fawn
17c3496542 feat(typingIndicator): Option to not show indicator for blocked users (#513) 2023-03-19 05:13:17 -03:00
Nuckyz
0fb79b763d Improvements, changes and fixes (#611) 2023-03-19 04:53:00 -03:00
whqwert
5873bde6a6 fix(apiMessagePopover): fix match (#608) 2023-03-18 22:38:08 +01:00
Nuckyz
0b79387800 feat(PlatformIndicators): Colored mobile indicator option (#536)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-18 04:58:49 +01:00
Lewis Crichton
6b493bc7d9 feat(plugin): F8Break (#581)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-18 04:54:19 +01:00
LordElias
de53bc7991 messageLogger: fix edited timestamp styling & add i18n (#607) 2023-03-18 04:37:55 +01:00
Lewis Crichton
4c5a56a8a5 fix(RoleColorEverywhere): Chat mentions (#605) 2023-03-15 22:27:46 -03:00
Vendicated
ed873ef9de Bump to v1.1.1 2023-03-15 18:01:04 +01:00
LordElias
d8a553feb0 improve MessageLogger deleted image hover animation (#603) 2023-03-14 18:48:36 +01:00
Ven
4717612090 :shipit: 2023-03-12 16:37:41 +01:00
Vendicated
5d1283bd85 Add Web/Desktop specific plugin capabilities; misc fixes 2023-03-11 14:18:32 +01:00
Ven
3b945b87b8 Delete src/plugins/reviewDB directory
Api owner refusing to properly moderate hate speech and related illegal / ToS infringing content
2023-03-11 12:26:54 +01:00
Vendicated
19c762f9c1 DevCompanion: Fix Deps 2023-03-11 00:28:27 +01:00
Vendicated
990adf7527 Fix casing in filename 2023-03-11 00:27:02 +01:00
Vendicated
983414d024 Add DevCompanion plugin (https://github.com/Vencord/Companion) 2023-03-11 00:25:49 +01:00
Vendicated
d5c05d857f Add DevOnly plugin capability 2023-03-11 00:25:32 +01:00
Nuckyz
bff6788546 feat(plugins): SilentMessageToggle (#586)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-09 01:19:28 +01:00
Nuckyz
253183a16a Fix Emote Cloner and improve ReverseImageSearch (#489) 2023-03-08 04:01:24 -03:00
Nuckyz
0fb3901a18 Fix Context Menu API (#583) 2023-03-08 06:01:15 +00:00
Nuckyz
1b199ec5d8 feat: Context Menu API (#496) 2023-03-08 01:59:50 -03:00
Nuckyz
40395d562a Improvements for patches and misc stuff (#582) 2023-03-08 00:11:59 -03:00
Nuckyz
7322c3af04 Fix Crash Loops and prevent metrics (#580) 2023-03-06 22:54:01 +01:00
Nuckyz
36c27f1111 VCDoubleClick: Fix applying to non voice channels (#572) 2023-03-06 02:39:53 +01:00
Nuckyz
95db6c32a3 Fix Ignore Activities button on platforms different than Windows (#528)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-06 00:12:52 +01:00
Nuckyz
bed5e98bb0 Misc fixes and improvements (#555)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-05 22:49:59 +01:00
Nico
a5392e5c53 fix(silentTyping): fix chatbar icon patch (#570) 2023-03-05 22:30:37 +01:00
Sammy
abbd298b31 Fix(InvisibleChat) Fix chatbar icon patch (closes #560) (#566)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-05 22:05:46 +01:00
Nuckyz
e219aaa062 Notifications: Permanent option and close button (#563)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-04 18:49:15 +01:00
Vendicated
cab72e1be6 Strongly type useSettings (supersedes #559) 2023-03-04 18:41:32 +01:00
Berlin
92372bde1d Update 1_INSTALLING.md (#562) 2023-03-03 23:55:21 +00:00
megumin
6747276a87 Add admin warnings to INSTALLING.md (#561) 2023-03-03 23:07:48 +00:00
Vendicated
03915b7533 Bump to v1.1.0 2023-03-02 21:19:33 +01:00
Vendicated
5e2ec368ad patches: Make $self more robust 2023-03-02 21:17:15 +01:00
Vendicated
ab8c93fbac Rewrite MessageLinkEmbeds part 2 2023-03-02 21:05:09 +01:00
Vendicated
d6a3edefd9 Rewrite MessageLinkEmbeds to improve Code Quality 2023-03-02 21:01:31 +01:00
Vendicated
727297ec4e Fix messageLinkEmbeds 2023-03-02 18:49:24 +01:00
megumin
eccc4b0be1 feat(plugins): add FixInbox plugin (#552) 2023-03-02 04:55:30 +00:00
Vendicated
8465140bc4 Bump to v1.0.9 2023-03-01 21:40:31 +01:00
Lewis Crichton
e6ccb751a0 Fix for latest Discord Update (#550)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-03-01 21:35:08 +01:00
Marvin Witt
dfc7a15083 chore: extend description of NoDevtoolsWarning plugin (#545)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-01 18:32:58 +01:00
Vendicated
37003edae9 fix(Notifications): Correctly close errored notifications 2023-03-01 05:45:17 +01:00
Nuckyz
faa90eccd3 feat: Crash Handler (#531)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-01 05:26:13 +01:00
Cloudburst
c91b0df607 GMPolyfill: add header prop (#543) 2023-02-28 23:13:49 +01:00
Ven
f56d99e133 Update README.md 2023-02-28 22:38:02 +01:00
Ven
c690662802 Improve README 2023-02-28 22:37:09 +01:00
Vendicated
4918d699d5 Windows: Add Option to use native titlebar ~ Closes #537 2023-02-28 22:17:39 +01:00
Justice Almanzar
5ec517875e typings for defaultless settings (#512)
* typings for defaultless settings

* fix other silly typings

* type guard utils

---------

Co-authored-by: Ven <vendicated@riseup.net>
2023-02-28 06:12:35 +01:00
Vendicated
cf56ad985b oop oop oop 2023-02-28 02:43:58 +01:00
Vendicated
c09d1558f7 Add SupportHelper plugin 2023-02-28 02:40:45 +01:00
Vendicated
eb190b660e Bump to v1.0.8 2023-02-28 01:50:17 +01:00
Lewis Crichton
d6f9068695 feat: SearchableSelect (#518)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-28 00:48:58 +01:00
Nico
cb507babaa fix: vcDoubleClick and revealAllSpoilers patch (#517)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-28 00:41:14 +01:00
Vendicated
235d114193 Improve ConsoleShortcuts plugin 2023-02-28 00:38:28 +01:00
Vendicated
9aba70dcb1 Fix MenuItemDeobfuscator 2023-02-28 00:17:39 +01:00
Vendicated
0b61d29c31 Fix TypingTweaks 2023-02-28 00:17:28 +01:00
megumin
335a13a38a fix tooltip component check (#541) 2023-02-27 21:19:01 +00:00
Vendicated
128ee41252 ErrorBoundary: Do not use any Discord components to be more robust 2023-02-25 19:10:01 +01:00
Vendicated
ccca41a168 Bump to v1.0.7 2023-02-24 06:08:45 +01:00
Vendicated
af4c7d8a90 Fix Cards (they look ugly now, wtf Discord) 2023-02-24 05:48:37 +01: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
242 changed files with 11943 additions and 4123 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,45 @@ 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: Generate plugin list
run: pnpm generatePluginJson dist/plugins.json
- 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

@ -36,6 +36,20 @@ jobs:
export PATH="$PWD/node_modules/.bin:$PATH" export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser) export CHROMIUM_BIN=$(which chromium-browser)
esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
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 esbuild test/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env: env:

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

20
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,20 @@
# Code of Conduct
Our community is welcoming to everyone, regardless of their characteristics.
As such, we expect you to treat everyone with respect and contribute to an open and welcoming community.
DO
- have empathy and be nice to others
- be respectful of differing opinions, even if you disagree
- give and accept constructive criticism
DON'T
- use offensive or derogatory language
- troll or spam
- personally attack or harass others
Repetitive violations of these guidelines might get your access to the repository restricted.
If you feel like a user is violating these guidelines or feel treated unfairly, please refrain from publicly challenging them and instead contact a Moderator on our Discord server or send an email to vendicated+conduct@riseup.net!

View File

@ -1,47 +1,32 @@
# 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 (Download Installer, open, click install button, done)
- Many plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033) - 100+ 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 - Fairly lightweight despite the many inbuilt plugins
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
- 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) - Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
- 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 the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
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
@ -56,3 +41,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS
[join]: https://discord.gg/D9uwnFnqmd [join]: https://discord.gg/D9uwnFnqmd
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]
## Disclaimer
Discord is trademark of Discord Inc. and solely mentioned for the sake of descriptivity.
Mention of it does not imply any affiliation with or endorsement by Discord Inc.

108
browser/GMPolyfill.js Normal file
View File

@ -0,0 +1,108 @@
/*
* 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));
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
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

@ -31,12 +31,14 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
Install `pnpm`: Install `pnpm`:
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal. > :exclamation: This next command may need to be run as admin/sudo depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
```shell ```shell
npm i -g pnpm npm i -g pnpm
``` ```
> :exclamation: **IMPORTANT** Make sure you aren't using an admin/root terminal from here onwards. It **will** mess up your Discord/Vencord instance and you **will** most likely have to reinstall.
Clone Vencord: Clone Vencord:
```shell ```shell
@ -183,7 +185,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

@ -26,6 +26,10 @@ export default definePlugin({
name: "Your Name", name: "Your Name",
}, },
], ],
// Delete `patches` if you are not using code patches, as it will make
// your plugin require restarts, and your stop() method will not be
// invoked at all. The presence of the key in the object alone is
// enough to trigger this behavior, even if the value is an empty array.
patches: [], patches: [],
// Delete these two below if you are only using code patches // Delete these two below if you are only using code patches
start() {}, start() {},

View File

@ -1,9 +1,9 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.0.1", "version": "1.1.3",
"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": {
"url": "https://github.com/Vendicated/Vencord/issues" "url": "https://github.com/Vendicated/Vencord/issues"
@ -20,32 +20,34 @@
"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", "generatePluginJson": "tsx scripts/generatePluginList.ts",
"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 +56,31 @@
"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",
"tsx": "^3.12.6",
"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 +89,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));

1439
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

99
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) {
zip(entries, {}, (err, data) => { return new Promise((resolve, reject) => {
if (err) { zip(entries, {}, (err, data) => {
console.error(err); if (err) {
process.exitCode = 1; reject(err);
} 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");
@ -33,9 +33,11 @@ export const banner = {
`.trim() `.trim()
}; };
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
// 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 +48,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 +70,18 @@ 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 === "index.ts") { if (file.startsWith(".")) continue;
continue; if (file === "index.ts") continue;
const fileBits = file.split(".");
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1))) {
const mod = fileBits.at(-2);
if (mod === "dev" && !watch) continue;
if (mod === "web" && !isWeb) continue;
if (mod === "desktop" && isWeb) 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 +96,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 +112,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 +134,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 +156,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 +192,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

@ -16,12 +16,11 @@
* 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 interface Review { (window.VencordStyles ??= new Map()).set(STYLE_NAME, {
comment: string, name: STYLE_NAME,
id: number, source: STYLE_SOURCE,
senderdiscordid: string, classNames: {},
senderuserid: number, dom: null,
star: number, });
username: string,
profile_photo: string; 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

@ -0,0 +1,191 @@
/*
* 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 { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
import { access, readFile } from "fs/promises";
import { join } from "path";
import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
interface Dev {
name: string;
id: string;
}
interface PluginData {
name: string;
description: string;
authors: Dev[];
dependencies: string[];
hasPatches: boolean;
hasCommands: boolean;
required: boolean;
enabledByDefault: boolean;
target: "desktop" | "web" | "dev";
}
const devs = {} as Record<string, Dev>;
function getName(node: NamedDeclaration) {
return node.name && isIdentifier(node.name) ? node.name.text : undefined;
}
function hasName(node: NamedDeclaration, name: string) {
return getName(node) === name;
}
function getObjectProp(node: ObjectLiteralExpression, name: string) {
const prop = node.properties.find(p => hasName(p, name));
if (prop && isPropertyAssignment(prop)) return prop.initializer;
return prop;
}
function parseDevs() {
const file = createSourceFile("constants.ts", readFileSync("src/utils/constants.ts", "utf8"), ScriptTarget.Latest);
for (const child of file.getChildAt(0).getChildren()) {
if (!isVariableStatement(child)) continue;
const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, "Devs"));
if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue;
const value = devsDeclaration.initializer.arguments[0];
if (!isObjectLiteralExpression(value)) return;
for (const prop of value.properties) {
const name = (prop.name as Identifier).text;
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse devs: ${name} is not an object literal`);
devs[name] = {
name: (getObjectProp(value, "name") as StringLiteral).text,
id: (getObjectProp(value, "id") as BigIntLiteral).text.slice(0, -1)
};
}
return;
}
throw new Error("Could not find Devs constant");
}
async function parseFile(fileName: string) {
const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
const fail = (reason: string) => {
return new Error(`Invalid plugin ${fileName}, because ${reason}`);
};
for (const node of file.getChildAt(0).getChildren()) {
if (!isExportAssignment(node) || !isCallExpression(node.expression)) continue;
const call = node.expression;
if (!isIdentifier(call.expression) || call.expression.text !== "definePlugin") continue;
const pluginObj = node.expression.arguments[0];
if (!isObjectLiteralExpression(pluginObj)) throw fail("no object literal passed to definePlugin");
const data = {
hasPatches: false,
hasCommands: false,
enabledByDefault: false,
required: false,
} as PluginData;
for (const prop of pluginObj.properties) {
const key = getName(prop);
const value = isPropertyAssignment(prop) ? prop.initializer : prop;
switch (key) {
case "name":
case "description":
if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
data[key] = value.text;
break;
case "patches":
data.hasPatches = true;
break;
case "commands":
data.hasCommands = true;
break;
case "authors":
if (!isArrayLiteralExpression(value)) throw fail("authors is not an array literal");
data.authors = value.elements.map(e => {
if (!isPropertyAccessExpression(e)) throw fail("authors array contains non-property access expressions");
return devs[getName(e)!];
});
break;
case "dependencies":
if (!isArrayLiteralExpression(value)) throw fail("dependencies is not an array literal");
const { elements } = value;
if (elements.some(e => !isStringLiteral(e))) throw fail("dependencies array contains non-string elements");
data.dependencies = (elements as NodeArray<StringLiteral>).map(e => e.text);
break;
case "required":
case "enabledByDefault":
data[key] = value.kind === SyntaxKind.TrueKeyword;
if (!data[key] && value.kind !== SyntaxKind.FalseKeyword) throw fail(`${key} is not a boolean literal`);
break;
}
}
if (!data.name || !data.description || !data.authors) throw fail("name, description or authors are missing");
const fileBits = fileName.split(".");
if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1)!)) {
const mod = fileBits.at(-2)!;
if (!["web", "desktop", "dev"].includes(mod)) throw fail(`invalid target ${fileBits.at(-2)}`);
data.target = mod as any;
}
return data;
}
throw fail("no default export called 'definePlugin' found");
}
async function getEntryPoint(dirent: Dirent) {
const base = join("./src/plugins", dirent.name);
if (!dirent.isDirectory()) return base;
for (const name of ["index.ts", "index.tsx"]) {
const full = join(base, name);
try {
await access(full);
return full;
} catch { }
}
throw new Error(`${dirent.name}: Couldn't find entry point`);
}
(async () => {
parseDevs();
const plugins = readdirSync("./src/plugins", { withFileTypes: true }).filter(d => d.name !== "index.ts");
const promises = plugins.map(async dirent => parseFile(await getEntryPoint(dirent)));
const data = JSON.stringify(await Promise.all(promises));
if (process.argv.length > 2) {
writeFileSync(process.argv[2], data);
} else {
console.log(data);
}
})();

View File

@ -31,13 +31,15 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
} }
} }
const CANARY = process.env.USE_CANARY === "true";
const browser = await pup.launch({ const browser = await pup.launch({
headless: true, headless: true,
executablePath: process.env.CHROMIUM_BIN executablePath: process.env.CHROMIUM_BIN
}); });
const page = await browser.newPage(); const page = await browser.newPage();
await page.setUserAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"); await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
function maybeGetError(handle: JSHandle) { function maybeGetError(handle: JSHandle) {
return (handle as JSHandle<Error>)?.getProperty("message") return (handle as JSHandle<Error>)?.getProperty("message")
@ -65,7 +67,7 @@ function toCodeBlock(s: string) {
} }
async function printReport() { async function printReport() {
console.log("# Vencord Report"); console.log("# Vencord Report" + (CANARY ? " (Canary)" : ""));
console.log(); console.log();
console.log("## Bad Patches"); console.log("## Bad Patches");
@ -98,7 +100,7 @@ async function printReport() {
}, },
body: JSON.stringify({ body: JSON.stringify({
description: "Here's the latest Vencord Report!", description: "Here's the latest Vencord Report!",
username: "Vencord Reporter", username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp", avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp",
embeds: [ embeds: [
{ {
@ -184,8 +186,11 @@ page.on("console", async e => {
} else if (isDebug) { } else if (isDebug) {
console.error(e.text()); console.error(e.text());
} else if (level === "error") { } else if (level === "error") {
console.error("Got unexpected error", e.text()); const text = e.text();
report.otherErrors.push(e.text()); if (!text.startsWith("Failed to load resource: the server responded with a status of")) {
console.error("Got unexpected error", text);
report.otherErrors.push(text);
}
} }
}); });
@ -207,6 +212,7 @@ function runTime(token: string) {
// Monkey patch Logger to not log with custom css // Monkey patch Logger to not log with custom css
// @ts-ignore
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) { Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
if (level === "warn" || level === "error") if (level === "warn" || level === "error")
console[level]("[Vencord]", this.name + ":", ...args); console[level]("[Vencord]", this.name + ":", ...args);
@ -215,6 +221,9 @@ function runTime(token: string) {
// force enable all plugins and patches // force enable all plugins and patches
Vencord.Plugins.patches.length = 0; Vencord.Plugins.patches.length = 0;
Object.values(Vencord.Plugins.plugins).forEach(p => { Object.values(Vencord.Plugins.plugins).forEach(p => {
// Needs native server to run
if (p.name === "WebRichPresence (arRPC)") return;
p.required = true; p.required = true;
p.patches?.forEach(patch => { p.patches?.forEach(patch => {
patch.plugin = p.name; patch.plugin = p.name;
@ -248,6 +257,8 @@ function runTime(token: string) {
if (!isWasm) if (!isWasm)
await wreq.e(id as any); await wreq.e(id as any);
await new Promise(r => setTimeout(r, 100));
} }
console.error("[PUP_DEBUG]", "Finished loading chunks!"); console.error("[PUP_DEBUG]", "Finished loading chunks!");
@ -271,4 +282,4 @@ await page.evaluateOnNewDocument(`
;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); ;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
`); `);
await page.goto("https://discord.com/login"); await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");

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";
@ -28,12 +27,12 @@ export { PlainSettings, Settings };
import "./utils/quickCss"; import "./utils/quickCss";
import "./webpack/patchWebpack"; import "./webpack/patchWebpack";
import { popNotice, showNotice } from "./api/Notices"; import { showNotification } from "./api/Notifications";
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,35 @@ 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;
setTimeout(() => {
showNotice( if (Settings.autoUpdate) {
"A Vencord update is available!", await update();
"View Update", const needsFullRestart = await rebuild();
() => { if (Settings.autoUpdateNotification)
popNotice(); setTimeout(() => showNotification({
Router.open("VencordUpdater"); title: "Vencord has been updated!",
body: "Click here to restart",
permanent: true,
onClick() {
if (needsFullRestart)
window.DiscordNative.app.relaunch();
else
location.reload();
} }
); }), 10_000);
}, 10000); return;
}
if (Settings.notifyAboutUpdates)
setTimeout(() => showNotification({
title: "A Vencord update is available!",
body: "Click here to view the update",
permanent: true,
onClick() {
SettingsRouter.open("VencordUpdater");
}
}), 10_000);
} catch (err) { } catch (err) {
UpdateLogger.error("Failed to check for updates", err); UpdateLogger.error("Failed to check for updates", err);
} }
@ -76,3 +93,12 @@ async function init() {
} }
init(); init();
if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
document.addEventListener("DOMContentLoaded", () => {
document.head.append(Object.assign(document.createElement("style"), {
id: "vencord-native-titlebar-style",
textContent: "[class*=titleBar-]{display: none!important}"
}));
}, { once: true });
}

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

144
src/api/ContextMenu.ts Normal file
View File

@ -0,0 +1,144 @@
/*
* 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 Logger from "@utils/Logger";
import type { ReactElement } from "react";
/**
* @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
*/
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...args: Array<any>) => void;
/**
* @param The navId of the context menu being patched
* @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
*/
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => void;
const ContextMenuLogger = new Logger("ContextMenu");
export const navPatches = new Map<string, Set<NavContextMenuPatchCallback>>();
export const globalPatches = new Set<GlobalContextMenuPatchCallback>();
/**
* Add a context menu patch
* @param navId The navId(s) for the context menu(s) to patch
* @param patch The patch to be applied
*/
export function addContextMenuPatch(navId: string | Array<string>, patch: NavContextMenuPatchCallback) {
if (!Array.isArray(navId)) navId = [navId];
for (const id of navId) {
let contextMenuPatches = navPatches.get(id);
if (!contextMenuPatches) {
contextMenuPatches = new Set();
navPatches.set(id, contextMenuPatches);
}
contextMenuPatches.add(patch);
}
}
/**
* Add a global context menu patch that fires the patch for all context menus
* @param patch The patch to be applied
*/
export function addGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback) {
globalPatches.add(patch);
}
/**
* Remove a context menu patch
* @param navId The navId(s) for the context menu(s) to remove the patch
* @param patch The patch to be removed
* @returns Wheter the patch was sucessfully removed from the context menu(s)
*/
export function removeContextMenuPatch<T extends string | Array<string>>(navId: T, patch: NavContextMenuPatchCallback): T extends string ? boolean : Array<boolean> {
const navIds = Array.isArray(navId) ? navId : [navId as string];
const results = navIds.map(id => navPatches.get(id)?.delete(patch) ?? false);
return (Array.isArray(navId) ? results : results[0]) as T extends string ? boolean : Array<boolean>;
}
/**
* Remove a global context menu patch
* @returns Wheter the patch was sucessfully removed
*/
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
return globalPatches.delete(patch);
}
/**
* A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs
* @param id The id of the child
*/
export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | null {
for (const child of children) {
if (child == null) continue;
if (child.props?.id === id) return itemsArray ?? null;
let nextChildren = child.props?.children;
if (nextChildren) {
if (!Array.isArray(nextChildren)) {
nextChildren = [nextChildren];
child.props.children = nextChildren;
}
const found = findGroupChildrenByChildId(id, nextChildren, nextChildren);
if (found !== null) return found;
}
}
return null;
}
interface ContextMenuProps {
contextMenuApiArguments?: Array<any>;
navId: string;
children: Array<ReactElement>;
"aria-label": string;
onSelect: (() => void) | undefined;
onClose: (callback: (...args: Array<any>) => any) => void;
}
export function _patchContextMenu(props: ContextMenuProps) {
props.contextMenuApiArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId);
if (!Array.isArray(props.children)) props.children = [props.children];
if (contextMenuPatches) {
for (const patch of contextMenuPatches) {
try {
patch(props.children, ...props.contextMenuApiArguments);
} catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
}
}
}
for (const patch of globalPatches) {
try {
patch(props.navId, props.children, ...props.contextMenuApiArguments);
} catch (err) {
ContextMenuLogger.error("Global patch errored,", err);
}
}
}

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

@ -19,6 +19,7 @@
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { MessageStore } from "@webpack/common"; import { MessageStore } from "@webpack/common";
import type { Channel, Message } from "discord-types/general"; import type { Channel, Message } from "discord-types/general";
import type { Promisable } from "type-fest";
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890"); const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
@ -41,16 +42,16 @@ export interface MessageExtra {
stickerIds?: string[]; stickerIds?: string[];
} }
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; }; export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void; export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
const sendListeners = new Set<SendListener>(); const sendListeners = new Set<SendListener>();
const editListeners = new Set<EditListener>(); const editListeners = new Set<EditListener>();
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) { export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
for (const listener of sendListeners) { for (const listener of sendListeners) {
try { try {
const result = listener(channelId, messageObj, extra); const result = await listener(channelId, messageObj, extra);
if (result && result.cancel === true) { if (result && result.cancel === true) {
return true; return true;
} }
@ -61,10 +62,10 @@ export function _handlePreSend(channelId: string, messageObj: MessageObject, ext
return false; return false;
} }
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) { export async function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
for (const listener of editListeners) { for (const listener of editListeners) {
try { try {
listener(channelId, messageId, messageObj); await listener(channelId, messageId, messageObj);
} catch (e) { } catch (e) {
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e); MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
} }

View File

@ -0,0 +1,119 @@
/*
* 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 { 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,
permanent
}: 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 || permanent) 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={() => {
onClose!();
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">
<div className="vc-notification-header">
<h2 className="vc-notification-title">{title}</h2>
<button
className="vc-notification-close-btn"
onClick={e => {
e.preventDefault();
e.stopPropagation();
onClose!();
}}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
role="img"
aria-labelledby="vc-notification-dismiss-title"
>
<title id="vc-notification-dismiss-title">Dismiss Notification</title>
<path fill="currentColor" d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
</svg>
</button>
</div>
<div>
{richBody ?? <p className="vc-notification-p">{body}</p>}
</div>
</div>
</div>
{image && <img className="vc-notification-img" src={image} alt="" />}
{timeout !== 0 && !permanent && (
<div
className="vc-notification-progressbar"
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
/>
)}
</button>
);
}, {
onError: ({ props }) => props.onClose!()
});

View File

@ -0,0 +1,101 @@
/*
* 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;
/** Whether this notification should not have a timeout */
permanent?: boolean;
}
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,74 @@
.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-content {
width: 100%;
}
.vc-notification-header {
display: flex;
justify-content: space-between;
}
.vc-notification-title {
color: var(--header-primary);
font-size: 1rem;
font-weight: 600;
line-height: 1.25rem;
text-transform: uppercase;
}
.vc-notification-close-btn {
all: unset;
cursor: pointer;
color: var(--interactive-normal);
opacity: 0.5;
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
}
.vc-notification-close-btn:hover {
color: var(--interactive-hover);
opacity: 1;
}
.vc-notification-icon {
height: 4rem;
width: 4rem;
border-radius: 6px;
}
.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

@ -18,12 +18,17 @@
import * as $Badges from "./Badges"; import * as $Badges from "./Badges";
import * as $Commands from "./Commands"; import * as $Commands from "./Commands";
import * as $ContextMenu from "./ContextMenu";
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 +36,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 +60,42 @@ 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;
/**
* 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;
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList }; /**
* An api allowing you to patch and add/remove items to/from context menus
*/
export const ContextMenu = $ContextMenu;

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,47 @@ 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;
autoUpdateNotification: boolean,
useQuickCss: boolean; useQuickCss: boolean;
enableReactDevtools: boolean; enableReactDevtools: boolean;
themeLinks: string[]; themeLinks: string[];
frameless: boolean;
transparent: boolean;
winCtrlQ: boolean;
winNativeTitleBar: 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,
autoUpdateNotification: true,
useQuickCss: true, useQuickCss: true,
themeLinks: [], themeLinks: [],
enableReactDevtools: false, enableReactDevtools: false,
plugins: {} frameless: false,
transparent: false,
winCtrlQ: false,
winNativeTitleBar: false,
plugins: {},
notifications: {
timeout: 5000,
position: "bottom-right",
useNative: "not-focused"
}
}; };
try { try {
@ -70,7 +94,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
// Return empty for plugins with no settings // Return empty for plugins with no settings
if (path === "plugins" && p in plugins) if (path === "plugins" && p in plugins)
return target[p] = makeProxy({ return target[p] = makeProxy({
enabled: plugins[p].required ?? false enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
}, root, `plugins.${p}`); }, root, `plugins.${p}`);
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve // Since the property is not set, check if this is a plugin's setting and if so, try to resolve
@ -144,11 +168,12 @@ 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
*/ */
export function useSettings(paths?: string[]) { // TODO: Representing paths as essentially "string[].join('.')" wont allow dots in paths, change to "paths?: string[][]" later
export function useSettings(paths?: UseSettings<Settings>[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {}); const [, forceUpdate] = React.useReducer(() => ({}), {});
const onUpdate: SubscriptionCallback = paths const onUpdate: SubscriptionCallback = paths
? (value, path) => paths.includes(path) && forceUpdate() ? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
: forceUpdate; : forceUpdate;
React.useEffect(() => { React.useEffect(() => {
@ -198,3 +223,31 @@ 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}`) as UseSettings<Settings>[]
).plugins[definedSettings.pluginName] as any,
def,
checks: checks ?? {},
pluginName: "",
};
return definedSettings;
}
type UseSettings<T extends object> = ResolveUseSettings<T>[keyof T];
type ResolveUseSettings<T extends object> = {
[Key in keyof T]:
Key extends string
? T[Key] extends Record<string, unknown>
// @ts-ignore "Type instantiation is excessively deep and possibly infinite"
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
: Key
: never;
};

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

@ -17,20 +17,24 @@
*/ */
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { Margins } from "@utils/margins";
import { LazyComponent } from "@utils/misc"; import { LazyComponent } from "@utils/misc";
import { Margins, React } from "@webpack/common"; import { React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard"; import { ErrorCard } from "./ErrorCard";
interface Props { interface Props<T = any> {
/** Render nothing if an error occurs */ /** Render nothing if an error occurs */
noop?: boolean; noop?: boolean;
/** Fallback component to render if an error occurs */ /** Fallback component to render if an error occurs */
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>; fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
/** called when an error occurs */ /** called when an error occurs. The props property is only available if using .wrap */
onError?(error: Error, errorInfo: React.ErrorInfo): void; onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
/** Custom error message */ /** Custom error message */
message?: string; message?: string;
/** The props passed to the wrapped component. Only used by wrap */
wrappedProps?: T;
} }
const color = "#e78284"; const color = "#e78284";
@ -65,7 +69,7 @@ const ErrorBoundary = LazyComponent(() => {
} }
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.props.onError?.(error, errorInfo); this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
logger.error("A component threw an Error\n", error); logger.error("A component threw an Error\n", error);
logger.error("Component Stack", errorInfo.componentStack); logger.error("Component Stack", errorInfo.componentStack);
} }
@ -84,15 +88,13 @@ const ErrorBoundary = LazyComponent(() => {
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console."; const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
return ( return (
<ErrorCard style={{ <ErrorCard style={{ overflow: "hidden" }}>
overflow: "hidden",
}}>
<h1>Oh no!</h1> <h1>Oh no!</h1>
<p>{msg}</p> <p>{msg}</p>
<code> <code>
{this.state.message} {this.state.message}
{!!this.state.stack && ( {!!this.state.stack && (
<pre className={Margins.marginTop8}> <pre className={Margins.top8}>
{this.state.stack} {this.state.stack}
</pre> </pre>
)} )}
@ -103,11 +105,11 @@ 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?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
}; };
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => ( ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
<ErrorBoundary {...errorBoundaryProps}> <ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
<Component {...props} /> <Component {...props} />
</ErrorBoundary> </ErrorBoundary>
); );

View File

@ -0,0 +1,7 @@
.vc-error-card {
padding: 2em;
background-color: #e7828430;
border: 1px solid #e78284;
border-radius: 5px;
color: var(--text-normal, white);
}

View File

@ -16,24 +16,15 @@
* 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 { Card } from "@webpack/common"; import "./ErrorCard.css";
interface Props { import { classes } from "@utils/misc";
style?: React.CSSProperties; import type { HTMLProps } from "react";
className?: string;
} export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
export function ErrorCard(props: React.PropsWithChildren<Props>) {
return ( return (
<Card className={props.className} style={ <div {...props} className={classes(props.className, "vc-error-card")}>
{
padding: "2em",
backgroundColor: "#e7828430",
borderColor: "#e78284",
color: "var(--text-normal)",
...props.style
}
}>
{props.children} {props.children}
</Card> </div>
); );
} }

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

@ -17,9 +17,12 @@
*/ */
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import { Margins } from "@utils/margins";
import { makeCodeblock } from "@utils/misc"; import { makeCodeblock } from "@utils/misc";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { ReplaceFn } from "@utils/types";
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, Parser, React, Switch, Text, TextInput } from "@webpack/common";
import { CheckedTextInput } from "./CheckedTextInput"; import { CheckedTextInput } from "./CheckedTextInput";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
@ -41,20 +44,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]);
@ -118,7 +130,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
)} )}
{!!diff?.length && ( {!!diff?.length && (
<Button className={Margins.marginTop20} onClick={() => { <Button className={Margins.top20} onClick={() => {
try { try {
Function(patchedCode.replace(/^function\(/, "function patchedModule(")); Function(patchedCode.replace(/^function\(/, "function patchedModule("));
setCompileResult([true, "Compiled successfully"]); setCompileResult([true, "Compiled successfully"]);
@ -179,9 +191,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}
@ -191,7 +204,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
)} )}
<Switch <Switch
className={Margins.marginTop8} className={Margins.top8}
value={isFunc} value={isFunc}
onChange={setIsFunc} onChange={setIsFunc}
note="'replacement' will be evaled if this is toggled" note="'replacement' will be evaled if this is toggled"
@ -206,7 +219,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>();
@ -245,7 +258,7 @@ function PatchHelper() {
return ( return (
<Forms.FormSection> <Forms.FormSection>
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text> <Text variant="heading-md/normal" tag="h2" className={Margins.bottom8}>Patch Helper</Text>
<Forms.FormTitle>find</Forms.FormTitle> <Forms.FormTitle>find</Forms.FormTitle>
<TextInput <TextInput
type="text" type="text"
@ -285,7 +298,7 @@ function PatchHelper() {
{!!(find && match && replacement) && ( {!!(find && match && replacement) && (
<> <>
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle> <Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div> <div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button> <Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
</> </>

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,33 +123,34 @@ 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]) => {
function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue }));
}
function onError(hasError: boolean) {
setErrors(e => ({ ...e, [key]: hasError }));
}
const Component = Components[setting.type];
return (
<Component
id={key}
key={key}
option={setting}
onChange={onChange}
onError={onError}
pluginSettings={pluginSettings}
definedSettings={plugin.settings}
/>
);
});
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
} }
const options = Object.entries(plugin.options).map(([key, setting]) => {
function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue }));
}
function onError(hasError: boolean) {
setErrors(e => ({ ...e, [key]: hasError }));
}
const Component = Components[setting.type];
return (
<Component
id={key}
key={key}
option={setting}
onChange={onChange}
onError={onError}
pluginSettings={pluginSettings}
/>
);
});
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
} }
function renderMoreUsers(_label: string, count: number) { function renderMoreUsers(_label: string, count: number) {
@ -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,10 +37,13 @@ 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;
setError(null);
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) {
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
setState(`${Number.MAX_SAFE_INTEGER}`); setState(`${Number.MAX_SAFE_INTEGER}`);
onChange(serialize(newValue)); onChange(serialize(newValue));
} else { } else {
@ -58,7 +61,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,10 +32,11 @@ 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 {
setError(null);
setState(newValue); setState(newValue);
onChange(newValue); onChange(newValue);
} }
@ -45,7 +46,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,10 +30,11 @@ 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 {
setError(null);
setState(newValue); setState(newValue);
onChange(newValue); onChange(newValue);
} }
@ -47,7 +48,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,33 @@
* 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 { Margins } from "@utils/margins";
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, 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 +61,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 +89,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}.enabled`]).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 +117,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 +125,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 +145,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}>
<Switch <div className={cl("card-header")}>
onChange={toggleEnabled} <Text variant="text-md/bold" className={cl("name")}>
disabled={disabled} {plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
value={isEnabled()} </Text>
note={<Text variant="text-md/normal" style={{ <button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
height: 40, {plugin.options
overflow: "hidden", ? <CogWheel />
// mfw css is so bad you need whatever this is to get multi line overflow ellipsis to work : <InfoIcon width="24" height="24" />}
textOverflow: "ellipsis", </button>
display: "-webkit-box", // firefox users will cope (it doesn't support it) <Switch
WebkitLineClamp: 2, checked={isEnabled()}
lineClamp: 2, onChange={toggleEnabled}
WebkitBoxOrient: "vertical", disabled={disabled}
boxOrient: "vertical" />
}}> </div>
{plugin.description} <Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text>
</Text>} </Flex >
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
width="24" height="24"
style={{ color: iconHover ? "" : "var(--text-muted)" }}
onMouseEnter={() => setIconHover(true)}
onMouseLeave={() => setIconHover(false)}
/>}
</button>
</Flex>
</Switch>
</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 +217,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.description.toLowerCase().includes(searchValue.value.toLowerCase())
plugin.name.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.top16}>
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}> <ReloadRequiredCard required={changes.hasChanges} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
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.bottom20} />
<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}
@ -269,51 +322,19 @@ export default ErrorBoundary.wrap(function Settings() {
</div> </div>
</div> </div>
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle> <Forms.FormTitle className={Margins.top20}>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.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}> <Forms.FormDivider className={Margins.top20} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
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 +347,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;
}

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

@ -0,0 +1,77 @@
/*
* 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 { classes } from "@utils/misc";
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={classes(SwitchClasses.container, "default-colors", checked ? SwitchClasses.checked : void 0)} 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,30 +18,26 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
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, Text } from "@webpack/common";
function BackupRestoreTab() { function BackupRestoreTab() {
return ( return (
<Forms.FormSection title="Settings Sync"> <Forms.FormSection title="Settings Sync" className={Margins.top16}>
<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>
</Flex> </Flex>
</Card> </Card>
<Text variant="text-md/normal" className={Margins.marginBottom8}> <Text variant="text-md/normal" className={Margins.bottom8}>
You can import and export your Vencord settings as a JSON file. You can import and export your Vencord settings as a JSON file.
This allows you to easily transfer your settings to another device, This allows you to easily transfer your settings to another device,
or recover your settings after reinstalling Vencord or Discord. or recover your settings after reinstalling Vencord or Discord.
</Text> </Text>
<Text variant="text-md/normal" className={Margins.marginBottom8}> <Text variant="text-md/normal" className={Margins.bottom8}>
Settings Export contains: Settings Export contains:
<ul> <ul>
<li>&mdash; Custom QuickCSS</li> <li>&mdash; Custom QuickCSS</li>
@ -50,7 +46,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

@ -19,9 +19,10 @@
import { useSettings } from "@api/settings"; import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { useAwaiter } from "@utils/misc"; import { useAwaiter } from "@utils/misc";
import { findLazy } from "@webpack"; import { findLazy } from "@webpack";
import { Card, Forms, Margins, React, TextArea } from "@webpack/common"; import { Card, Forms, React, TextArea } from "@webpack/common";
const TextAreaProps = findLazy(m => typeof m.textarea === "string"); const TextAreaProps = findLazy(m => typeof m.textarea === "string");
@ -51,13 +52,14 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
return ( return (
<> <>
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle> <Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText> <Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div> <div>
{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 +76,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 +90,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.top8 + " " + Margins.bottom8} />
<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>
@ -118,13 +116,9 @@ export default ErrorBoundary.wrap(function () {
</Card> </Card>
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle> <Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
<TextArea <TextArea
style={{ value={themeText}
padding: ".5em", onChange={e => setThemeText(e.currentTarget.value)}
border: "1px solid var(--background-modifier-accent)" className={`${TextAreaProps.textarea} vc-settings-theme-links`}
}}
ref={ref}
defaultValue={settings.themeLinks.join("\n")}
className={TextAreaProps.textarea}
placeholder="Theme Links" placeholder="Theme Links"
spellCheck={false} spellCheck={false}
onBlur={onBlur} onBlur={onBlur}

View File

@ -16,14 +16,16 @@
* 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";
import { handleComponentFailed } from "@components/handleComponentFailed"; import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
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, Parser, React, Switch, Toasts } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
@ -69,14 +71,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)"
@ -104,14 +110,14 @@ function Updatable(props: CommonProps) {
</ErrorCard> </ErrorCard>
</> </>
) : ( ) : (
<Forms.FormText className={Margins.marginBottom8}> <Forms.FormText className={Margins.bottom8}>
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"} {isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
</Forms.FormText> </Forms.FormText>
)} )}
{isOutdated && <Changes updates={updates} {...props} />} {isOutdated && <Changes updates={updates} {...props} />}
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}> <Flex className={classes(Margins.bottom8, Margins.top8)}>
{isOutdated && <Button {isOutdated && <Button
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking} disabled={isUpdating || isChecking}
@ -170,7 +176,7 @@ function Updatable(props: CommonProps) {
function Newer(props: CommonProps) { function Newer(props: CommonProps) {
return ( return (
<> <>
<Forms.FormText className={Margins.marginBottom8}> <Forms.FormText className={Margins.bottom8}>
Your local copy has more recent commits. Please stash or reset them. Your local copy has more recent commits. Please stash or reset them.
</Forms.FormText> </Forms.FormText>
<Changes {...props} updates={changes} /> <Changes {...props} updates={changes} />
@ -179,6 +185,8 @@ function Newer(props: CommonProps) {
} }
function Updater() { function Updater() {
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." }); const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => { React.useEffect(() => {
@ -192,16 +200,41 @@ function Updater() {
}; };
return ( return (
<Forms.FormSection> <Forms.FormSection className={Margins.top16}>
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
<Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a notification 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>
<Switch
value={settings.autoUpdateNotification}
onChange={(v: boolean) => settings.autoUpdateNotification = v}
note="Shows a notification when Vencord automatically updates"
disabled={!settings.autoUpdate}
>
Get notified when an automatic update completes
</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.top8 + " " + Margins.bottom8} />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle> <Forms.FormTitle tag="h5">Updates</Forms.FormTitle>

View File

@ -18,31 +18,77 @@
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"
} : {
key: "winNativeTitleBar",
title: "Use Windows' native title bar instead of Discord's custom one",
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 +128,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>
<Switch {Switches.map(s => s && (
value={settings.useQuickCss} <Switch
onChange={(v: boolean) => settings.useQuickCss = v} key={s.key}
note="Loads styles from your QuickCss file"> value={settings[s.key]}
Use QuickCss onChange={v => settings[s.key] = v}
</Switch> note={s.note}
{!IS_WEB && ( >
<React.Fragment> {s.title}
<Switch </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 +209,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 +220,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,13 @@
* 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 "./settingsStyles.css";
import { findByCodeLazy } from "@webpack";
import { Forms, Router, Text } from "@webpack/common";
import cssText from "~fileContent/settingsStyles.css"; import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { findByCodeLazy } from "@webpack";
import { Forms, SettingsRouter, Text } from "@webpack/common";
import BackupRestoreTab from "./BackupRestoreTab"; import BackupRestoreTab from "./BackupRestoreTab";
import PluginsTab from "./PluginsTab"; import PluginsTab from "./PluginsTab";
@ -28,11 +30,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"]');
@ -61,20 +59,20 @@ function Settings(props: SettingsProps) {
const CurrentTab = SettingsTabs[tab]?.component; const CurrentTab = SettingsTabs[tab]?.component;
return <Forms.FormSection> return <Forms.FormSection>
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text> <Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">Vencord Settings</Text>
<TabBar <TabBar
type={TabBar.Types.TOP} type="top"
look={TabBar.Looks.BRAND} look="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>;
@ -86,7 +84,7 @@ function Settings(props: SettingsProps) {
} }
export default function (props: SettingsProps) { export default function (props: SettingsProps) {
return <ErrorBoundary> return <ErrorBoundary onError={handleComponentFailed}>
<Settings tab={props.tab} /> <Settings tab={props.tab} />
</ErrorBoundary>; </ErrorBoundary>;
} }

View File

@ -1,23 +1,48 @@
.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;
}
.vc-settings-theme-links {
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
display: inline-block !important;
color: var(--text-normal) !important;
padding: 0.5em;
border: 1px solid var(--background-modifier-accent);
}

View File

@ -16,29 +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 { isOutdated, rebuild, update } from "@utils/updater"; import { maybePromptToUpdate } from "@utils/updater";
export async function handleComponentFailed() { export function handleComponentFailed() {
if (isOutdated) { maybePromptToUpdate(
setImmediate(async () => { "Uh Oh! Failed to render this Page." +
const wantsUpdate = confirm( " However, there is an update available that might fix it." +
"Uh Oh! Failed to render this Page." + " Would you like to update and restart now?"
" However, there is an update available that might fix it." + );
" Would you like to update and restart now?"
);
if (wantsUpdate) {
try {
await update();
await rebuild();
if (IS_WEB)
location.reload();
else
DiscordNative.app.relaunch();
} catch (e) {
console.error(e);
alert("That also failed :( Try updating or reinstalling with the installer!");
}
}
});
}
} }

11
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;
}; };
@ -44,8 +51,7 @@ declare global {
* Only available when running in Electron, undefined on web. * Only available when running in Electron, undefined on web.
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard. * Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
* *
* If you really must use it, mark your plugin as Desktop App only via * If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
* `target: "DESKTOP"`
*/ */
export var DiscordNative: any; export var DiscordNative: any;
@ -54,6 +60,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,58 +44,50 @@ function isNewer($new: string, old: string) {
} }
function patchLatest() { function patchLatest() {
const currentAppPath = dirname(process.execPath); try {
const currentVersion = basename(currentAppPath); const currentAppPath = dirname(process.execPath);
const discordPath = join(currentAppPath, ".."); const currentVersion = basename(currentAppPath);
const discordPath = join(currentAppPath, "..");
const latestVersion = readdirSync(discordPath).reduce((prev, curr) => { const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
return (curr.startsWith("app-") && isNewer(curr, prev)) return (curr.startsWith("app-") && isNewer(curr, prev))
? curr ? curr
: prev; : prev;
}, currentVersion as string); }, currentVersion as string);
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");
console.info("[Vencord] Detected Host Update. Repatching..."); if (!existsSync(app) || statSync(app).isDirectory()) return;
const patcherPath = join(__dirname, "patcher.js"); console.info("[Vencord] Detected Host Update. Repatching...");
mkdirSync(app);
writeFileSync(join(app, "package.json"), JSON.stringify({ renameSync(app, _app);
name: "discord", mkdirSync(app);
main: "index.js" writeFileSync(join(app, "package.json"), JSON.stringify({
})); name: "discord",
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(patcherPath)});`); main: "index.js"
}));
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 () { update.apply(this, arguments);
patchLatest(); patchLatest();
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();
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,51 @@ 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;
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
delete options.frame;
}
if (settings.transparent) {
options.transparent = true;
options.backgroundColor = "#00000000";
}
process.env.DISCORD_PRELOAD = original; process.env.DISCORD_PRELOAD = original;
@ -100,8 +134,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 +193,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 require(require.main!.filename);
// 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);
}

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: '"7z","ade","adp"',
replacement: {
match: /JSON\.parse\('\[.+?'\)/,
replace: "[]"
}
}
]
});

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

@ -24,9 +24,10 @@ import { Heart } from "@components/Heart";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import IpcEvents from "@utils/IpcEvents"; import IpcEvents from "@utils/IpcEvents";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { Margins } from "@utils/margins";
import { closeModal, Modals, openModal } from "@utils/modal"; import { closeModal, Modals, openModal } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { Forms, Margins } from "@webpack/common"; import { Forms } from "@webpack/common";
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp"; const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
@ -66,11 +67,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\],/, {
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} /> match: /src:(\w{1,3})\[(\w{1,3})\.key\],/,
replace: (_, imageMap, badge) => `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,`
},
{
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}},`
}
]
} }
], ],
@ -141,7 +151,7 @@ export default definePlugin({
<Forms.FormText> <Forms.FormText>
This Badge is a special perk for Vencord Donors This Badge is a special perk for Vencord Donors
</Forms.FormText> </Forms.FormText>
<Forms.FormText className={Margins.marginTop20}> <Forms.FormText className={Margins.top20}>
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!! Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
</Forms.FormText> </Forms.FormText>
</div> </div>

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,99 @@
/*
* 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 { Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { type PatchReplacement } from "@utils/types";
import { addListener, removeListener } from "@webpack";
/**
* The last var name corresponding to the Context Menu API (Discord, not ours) module
*/
let lastVarName = "";
/**
* @param target The patch replacement object
* @param exportKey The key exporting the build Context Menu component function
*/
function makeReplacementProxy(target: PatchReplacement, exportKey: string) {
return new Proxy(target, {
get(_, p) {
if (p === "match") return RegExp(`${exportKey},{(?<=${lastVarName}\\.${exportKey},{)`, "g");
// @ts-expect-error
return Reflect.get(...arguments);
}
});
}
function listener(exports: any, id: number) {
if (!Settings.plugins.ContextMenuAPI.enabled) return removeListener(listener);
if (typeof exports !== "object" || exports === null) return;
for (const key in exports) if (key.length <= 3) {
const prop = exports[key];
if (typeof prop !== "function") continue;
const str = Function.prototype.toString.call(prop);
if (str.includes('path:["empty"]')) {
Vencord.Plugins.patches.push({
plugin: "ContextMenuAPI",
all: true,
noWarn: true,
find: "navId:",
replacement: [
{
// Set the lastVarName for our proxy to use
match: RegExp(`${id}(?<=(\\i)=.+?)`),
replace: (id, varName) => {
lastVarName = varName;
return id;
}
},
/**
* We are using a proxy here to utilize the whole code the patcher gives us, instead of matching the entire module (which is super slow)
* Our proxy returns the corresponding match for that module utilizing lastVarName, which is set by the patch before
*/
makeReplacementProxy({
match: "", // Needed to canonicalizeDescriptor
replace: "$&contextMenuApiArguments:arguments,",
}, key)
]
});
removeListener(listener);
}
}
}
addListener(listener);
export default definePlugin({
name: "ContextMenuAPI",
description: "API for adding/removing items to/from context menus.",
authors: [Devs.Nuckyz],
patches: [
{
find: "♫ (つ。◕‿‿◕。)つ ♪",
replacement: {
match: /(?<=function \i\((\i)\){)(?=var \i,\i=\i\.navId)/,
replace: (_, props) => `Vencord.Api.ContextMenu._patchContextMenu(${props});`
}
}
]
});

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

@ -43,7 +43,7 @@ export default definePlugin({
{ {
find: '"Menu API', find: '"Menu API',
replacement: { replacement: {
match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s, match: /function.{0,80}type===(\i)\).{0,50}navigable:.+?Menu API/s,
replace: (m, mod) => { replace: (m, mod) => {
let nicenNames = ""; let nicenNames = "";
const redefines = [] as string[]; const redefines = [] as string[];

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

@ -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: "MessageDecorationsAPI",
description: "API to add decorations to messages",
authors: [Devs.TheSun],
patches: [
{
find: ".withMentionPrefix",
replacement: {
match: /(.roleDot.{10,50}{children:.{1,2})}\)/,
replace: "$1.concat(Vencord.Api.MessageDecorations.__addDecorationsToMessage(arguments[0]))})"
}
}
],
});

View File

@ -22,22 +22,22 @@ import definePlugin from "@utils/types";
export default definePlugin({ export default definePlugin({
name: "MessageEventsAPI", name: "MessageEventsAPI",
description: "Api required by anything using message events.", description: "Api required by anything using message events.",
authors: [Devs.Arjix], authors: [Devs.Arjix, Devs.hunt],
patches: [ patches: [
{ {
find: "sendMessage:function", find: '"MessageActionCreators"',
replacement: [{ replacement: [{
match: /(?<=_sendMessage:function\([^)]+\)){/, match: /_sendMessage:(function\([^)]+\)){/,
replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};" replace: "_sendMessage:async $1{if(await Vencord.Api.MessageEvents._handlePreSend(...arguments))return;"
}, { }, {
match: /(?<=\beditMessage:function\([^)]+\)){/, match: /\beditMessage:(function\([^)]+\)){/,
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);" replace: "editMessage:async $1{await Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
}] }]
}, },
{ {
find: '("interactionUsernameProfile', find: '("interactionUsernameProfile',
replacement: { replacement: {
match: /var \w=(\w)\.id,\w=(\w)\.id;return .{1,2}\.useCallback\(\(?function\((.{1,2})\){/, match: /var \i=(\i)\.id,\i=(\i)\.id;return \i\.useCallback\(\(?function\((\i)\){/,
replace: (m, message, channel, event) => replace: (m, message, channel, event) =>
// the message param is shadowed by the event param, so need to alias them // the message param is shadowed by the event param, so need to alias them
`var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});` `var _msg=${message},_chan=${channel};${m}Vencord.Api.MessageEvents._handleClick(_msg, _chan, ${event});`

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,200}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
replace: (m, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable");
return `...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,15 @@ 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: /(?=;\i=null;.{0,70}getPremiumSubscription)/g,
replace: replace: ";if(Vencord.Api.Notices.currentNotice)return false"
";if(Vencord.Api.Notices.currentNotice)return !1;$1"
}, },
{ {
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: (_, notice) => `if(${notice}.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);`
} }
] ]
} }

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