Compare commits

...

403 Commits

Author SHA1 Message Date
Vendicated
79b35d5797 initial custom plugin loading 2023-04-20 01:19:13 +02:00
V
62194674eb Delete highResAvatars.ts 2023-04-20 00:45:24 +02:00
V
04da98498f Restore src/Vencord.ts 2023-04-20 00:44:48 +02:00
Vendicated
6fa0fb017b more additions for external plugins 2023-04-20 00:41:02 +02:00
Vendicated
11ecc45b71 more fixes 2023-04-19 23:12:39 +02:00
Vendicated
82cd8d98f6 convert import aliases 2023-04-19 23:08:07 +02:00
Vendicated
c815f1c5f3 Run autofix to sort these imports! 2023-04-19 22:59:57 +02:00
Vendicated
e248f58d9f stuffs 3 2023-04-19 22:37:58 +02:00
Vendicated
3171b78a36 stuffs 2 2023-04-19 21:49:12 +02:00
Vendicated
525aa3af33 stuffs 2023-04-19 21:36:17 +02:00
Vendicated
b7299ea2cc npm types package 2023-04-19 20:18:42 +02:00
Vendicated
8dd70f5d1a Fix inserting text when markdown preview is off 2023-04-18 23:13:10 +02:00
Amsyar Rasyiq
8be6c6e3ce fix(ReviewDB): fix margin in self-profile preview (#935)
Co-authored-by: V <vendicated@riseup.net>
2023-04-18 18:53:11 +02:00
V
7e96b5dcfb RelationShipNotifier: Support multiple users (#944) 2023-04-18 16:52:46 +00:00
Đỗ Văn Hoài Tuân
99a7d78e9b [skip ci] USRBG: Update README link to a more accurate one (#943) 2023-04-18 16:04:17 +02:00
Nuckyz
e70d00d008 BadgesAPI: Don't depend on getBadges module not being undefined (#942) 2023-04-18 05:25:14 +00:00
Nuckyz
c0ac6a4b86 SilentMessageToggle: Option to persist state (#941) 2023-04-18 04:54:21 +00:00
Vendicated
29749e93c7 CI Reporter fixes 2023-04-18 02:54:22 +02:00
Vendicated
993c6be219 Fix badges 2023-04-18 02:47:48 +02:00
Đỗ Văn Hoài Tuân
e2e1cf2bfd feat(plugin): USRBG (#844)
Co-authored-by: amsyarasyiq <82711525+amsyarasyiq@users.noreply.github.com>
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-04-18 00:18:18 +02:00
Vendicated
59e3c2c609 ci: fix cding into wrong folder 2023-04-17 04:09:20 +02:00
Nuckyz
43d7ca4c30 Make chat bar buttons have a consistent size (#927)
Co-authored-by: V <vendicated@riseup.net>
2023-04-17 02:09:11 +00:00
V
5305447f44 firefox: Fix csp (QuickCss, themes, some plugins) (#554) 2023-04-17 04:05:01 +02:00
Vendicated
76e74b3e40 SendTimestamps: Fix chatbox having a scrollbar 2023-04-17 02:55:08 +02:00
Vendicated
e767da4b08 Fix errors on setups with no SpeechSynthesis support (part 2) 2023-04-17 01:52:46 +02:00
Vendicated
e4f3f57a28 bump to v1.1.7 2023-04-17 01:48:53 +02:00
Lily • Lylythii
72f6dd84ee SilentTyping: Make Tooltip text casing consistent w other plugins (#918) 2023-04-17 01:46:51 +02:00
Mufaro
9c929a4d98 messageTags: Fix duplicate entries & replies (#922)
Co-authored-by: V <vendicated@riseup.net>
2023-04-16 23:44:33 +00:00
Luca LeBlanc
dac9cad873 Improve FakeNitro emoji popup message (#924)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: V <vendicated@riseup.net>
2023-04-17 01:43:11 +02:00
Vendicated
6fd5c7874f Remove ContextMenuAPIs from plugin dependencies (now required) 2023-04-17 01:39:16 +02:00
Vendicated
a56dfe269c Improve unintuitive plugin descriptions 2023-04-17 01:37:55 +02:00
Vendicated
7d55a81bac MessageLogger: Add toggle deleted highlight rightclick button 2023-04-17 01:27:25 +02:00
Vendicated
ce64631310 MessageLogger: Apply styles to gifs, stickers & new markdown 2023-04-17 01:18:45 +02:00
Vendicated
1caaa78490 PinDMs: Add option to sort by most recent message 2023-04-17 01:06:54 +02:00
Vendicated
d35654b887 Improve themes tab instructions 2023-04-17 00:23:22 +02:00
Vendicated
ca5d24385f Fix errors on setups with no Notifications/SpeechSynthesis support 2023-04-17 00:21:49 +02:00
V
cb3bd4b881 ci: Also upload extension zips 2023-04-15 15:02:31 +02:00
Lewis Crichton
ff3589d157 CloudSync: fix accidently applying stale settings (#915
* modify the cloud timestamp before importing

* use util/native relaunch

* lambda why

* "should work!" - Vendicated, 15th April 2023
2023-04-15 14:50:00 +02:00
dimden
7a98f1dfcb Make Vencord Extension run in iframes (#914)
Fixes support for https://github.com/NeverDecaf/discord-PWA
2023-04-15 14:49:48 +02:00
fawn
9e6d3459e3 fix(smyn): reversed settings (#913) 2023-04-15 12:20:49 +02:00
Jack
ea30ca418f feat: Add AlwaysAnimate plugin (#908)
Co-authored-by: V <vendicated@riseup.net>
2023-04-15 05:10:03 +00:00
Nuckyz
1f7ec93a24 SHC: Small improvements (#907) 2023-04-15 03:40:42 +00:00
Nuckyz
336c7bdd5e SHC: Fix emoji rendering & allowed users/roles edge cases (#895)
Co-authored-by: V <vendicated@riseup.net>
2023-04-15 05:02:08 +02:00
V
88ad4f1b05 SendTimestamps (#891)
Co-authored-by: Tyler Flowers <contact@ggtylerr.dev>
2023-04-15 04:42:18 +02:00
Sofia Lima
f75f887861 feat(plugin): ShowMeYourName (#901) 2023-04-15 03:58:22 +02:00
Nuckyz
96f640da67 Make Context Menu API support hooks (#902)
Co-authored-by: V <vendicated@riseup.net>
2023-04-15 02:47:03 +02:00
Manti
e8809fc57b add reviewdb bacc (#898) 2023-04-15 02:34:53 +02:00
Ryan Cao
ca91ef4e39 feat(moreUserTags): add HTML data attributes to user tags (#883)
Co-authored-by: V <vendicated@riseup.net>
2023-04-15 02:31:36 +02:00
V
db7fc3769b Fix settings on Vencord Mobile (#905) 2023-04-15 02:27:11 +02:00
V
6c719f5ee9 PinDMs (#879) 2023-04-15 02:26:46 +02:00
Nuckyz
c6fd8cae16 Fix MuteNewGuild (#896) 2023-04-14 18:02:03 +02:00
V
1adbf9e41a Delete muteNewGuild.ts
This plugin has a bad bug that breaks all DM notifications. Removed for now to prevent more damage, will be back once fixed!

I will try to make this future update fix the problem for any users affected by it.
For now, if you're affected, see https://gist.github.com/Vendicated/c86016b7fa68a754885ecd82a6ac0f2a for a quick fix
2023-04-14 08:53:40 +02:00
Nuckyz
aee6bed48c Revert "Delete DisableDMCallIdle: Discord removed this 'feature'" (#889)
THEY ADDED IT BACK WHYYYY
2023-04-14 03:37:14 +02:00
Vendicated
c8817e805f Fix badges 2023-04-13 21:04:19 +02:00
Vendicated
c6f0d0763c Remove noisy notifications from notification log 2023-04-13 19:15:36 +02:00
Vendicated
3bd3012aa9 Delete DisableDMCallIdle: Discord removed this 'feature' 2023-04-13 19:12:09 +02:00
Vendicated
694a693a8e MemberCount: Fall back to approx member count if necessary 2023-04-13 19:10:20 +02:00
Elliott Tallis
ed827c2d81 Shiki: Make CodeBlock button texts not copyable (#864) 2023-04-13 04:26:43 +02:00
Kode
71849cac9a Fix ViewIcons on webhooks and default avatars! (#880)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-04-13 04:25:38 +02:00
Nuckyz
e34da54271 Option to transform emotes/stickers in compound messages (#876)
+ ContextMenu refactor to not call callbacks for same children multiple times

Co-authored-by: V <vendicated@riseup.net>
2023-04-13 04:22:38 +02:00
Vendicated
cfe41ef656 ViewIcons: Add format setting & user context menu + cleanup 2023-04-12 03:27:31 +02:00
V
4d836524c1 GreetStickerPicker: greet with stickers of your choice (#866)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2023-04-12 02:33:51 +02:00
Dziurwa
edc96387f5 Fix FriendInvites (#874) 2023-04-12 02:33:05 +02:00
Vendicated
358eb6ad8e ImageZoom Fixes 2023-04-11 02:00:42 +02:00
Syncx
c997cb4958 ImageZoom: Fix not being able to close carousels on sides (#857)
Co-authored-by: V <vendicated@riseup.net>
2023-04-10 22:38:37 +00:00
Kode
83dab24fb9 PronounDB: Fix pronouns flickering (#862)
Co-authored-by: V <vendicated@riseup.net>
2023-04-11 00:37:24 +02:00
Vendicated
8a305d2d11 clean up spotify controls css 2023-04-11 00:32:11 +02:00
Ezzud
7eb12f0fb7 SpotifyControls: Fix flashing button row when using show on hover (#850)
Co-authored-by: V <vendicated@riseup.net>
2023-04-10 22:21:30 +00:00
Luna
0a3dc5c6e8 VoiceUserShow: Fix lack of bottom margin (#853)
Co-authored-by: V <vendicated@riseup.net>
2023-04-10 22:10:21 +00:00
Cat
b21516d44e Make QuickCss window title consistent with other windows (#859)
Co-authored-by: V <vendicated@riseup.net>
2023-04-11 00:07:36 +02:00
Nuckyz
65f7cf9503 Nicer GameActivity & SilentMsg UX; Fix [object Object] jumpscare (#863) 2023-04-10 23:59:48 +02:00
V
40a7aa5079 UserScript: Fix cors check 2023-04-09 06:58:29 +02:00
Amsyar Rasyiq
c4a3d25d37 feat(NoAutoReplyMention): Inverse shift reply behaviour (#839)
Co-authored-by: V <vendicated@riseup.net>
2023-04-09 06:55:04 +02:00
Ryan Cao
613fa9a57b feat: add translucency option for macOS (#849)
Co-authored-by: V <vendicated@riseup.net>
2023-04-09 02:06:09 +00:00
V
08822dd190 Improvements for VencordDesktop (#847) 2023-04-09 04:04:02 +02:00
Đỗ Văn Hoài Tuân
bfa20f2634 feat(setting): Disable minimum window size #834 (#848) 2023-04-09 03:41:55 +02:00
Vendicated
840da146b9 UX: Make possibly copy-relevant text in settings copyable 2023-04-08 23:28:12 +02:00
Vendicated
acc874c34f WebContextMenus: Fix jpegs being uncopyable 2023-04-08 23:05:38 +02:00
Vendicated
0dee968e98 WebContextMenus: Don't include queryparams in filename on save 2023-04-08 22:57:34 +02:00
Vendicated
09e919f0c6 bump to 1.1.6 2023-04-08 03:53:32 +02:00
V
eaf1af75bd WebContextMenus: Port more menus (#818)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2023-04-08 03:51:37 +02:00
exit
7c514e4b1d SupportHelper: Add missing dependency - CommandsAPI (#823)
Co-authored-by: V <vendicated@riseup.net>
2023-04-07 19:19:12 +00:00
LordElias
1432baa28b ignore userplugins when linting (#822)
Co-authored-by: V <vendicated@riseup.net>
2023-04-07 19:17:54 +00:00
exit
f1f61195c3 InvisibleChat: Add missing dependency on MessagePopoverAPI (#817)
Co-authored-by: V <vendicated@riseup.net>
2023-04-07 19:16:18 +00:00
Đỗ Văn Hoài Tuân
8fefa2b716 FakeNitro: Fix stickers with space in name #819 (#820) 2023-04-07 21:15:11 +02:00
Ryan Cao
2a0c30b66d feat(moreusertags): add option to not show more tags for bots (#812)
Co-authored-by: V <vendicated@riseup.net>
2023-04-07 00:31:21 +00:00
Lewis Crichton
97f8d4d515 feat: Cloud settings sync (#505)
Co-authored-by: Ven <vendicated@riseup.net>
2023-04-07 02:27:18 +02:00
Vendicated
2672dea8e3 ci: bump action 2023-04-06 03:34:02 +02:00
Vendicated
63f5b0a663 bump pnpm to v8 2023-04-06 03:34:02 +02:00
ActuallyTheSun
e40ebacc5b feat(plugin): WebhookTags -> MoreUserTags (#378)
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-04-06 03:28:38 +02:00
LordElias
e261c93563 feat(plugin): User Voice Show (#694)
Co-authored-by: V <vendicated@riseup.net>
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2023-04-06 03:22:54 +02:00
Syncx
df7357b357 feat(plugin): Image Zoom (#510)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: Ven <vendicated@riseup.net>
2023-04-06 01:06:11 +00:00
Đỗ Văn Hoài Tuân
2e6c5eacf7 BetterFolders: Fix Close all not working (#808) 2023-04-06 03:02:53 +02:00
Dziurwa
c9fd404012 Fix FriendInvites (#802)
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-04-05 23:01:11 +02:00
V
814302e272 Fix Badges (#801) 2023-04-05 22:45:14 +02:00
Anubis
72ba83924c SpotifyControls: add album art hover transition (#797) 2023-04-05 22:45:03 +02:00
Nuckyz
9d742094cb ShowHiddenChannels: Use Discord's new overlay vars (#795)
* Fix SHC css for new Discord vars

* I'm dumb

* improvements to work with themes

---------

Co-authored-by: V <vendicated@riseup.net>
2023-04-05 20:44:03 +00:00
Nuckyz
38f3aac98d Fix VolumeBooster and improve ContextMenuAPI patch (#793)
Co-authored-by: V <vendicated@riseup.net>
2023-04-05 03:07:17 +00:00
Nuckyz
12ffb9d642 Fake Nitro Transform Stickers option and other stuff (#683)
Co-authored-by: V <vendicated@riseup.net>
2023-04-05 05:06:04 +02:00
Vendicated
99391a4f0e fix generatePluginList 2023-04-05 04:54:54 +02:00
Vendicated
6492908a62 VencordDesktop: Fix Updater 2023-04-05 04:34:39 +02:00
Vendicated
676bc612d9 VencordDesktop: Include web plugins & use proper showItemInFolder 2023-04-05 04:09:42 +02:00
Vendicated
d8a5e43034 Fix Themes Tab 2023-04-04 22:24:16 +02:00
Vendicated
8ad710abca Fix ContextMenuAPI 2023-04-04 22:19:52 +02:00
Vendicated
368cb7bc6b Fix Toasts 2023-04-04 21:51:03 +02:00
Vendicated
4aa7a052d0 Bump to v1.1.5 2023-04-04 21:29:39 +02:00
Vendicated
f088f17a0a Remove accidently introduced patch 2023-04-04 21:28:38 +02:00
Vendicated
a55c758b0e Fix SpotifyControls 2023-04-04 21:27:44 +02:00
Vendicated
f092f434fe Fix Vencord 2023-04-04 21:14:55 +02:00
Remty
2e6dfaa879 FakeProfileThemes: add usage guide (#778)
Co-authored-by: V <vendicated@riseup.net>
2023-04-04 13:28:41 +00:00
Nuckyz
96dc2e12d0 Fix Web & Game Activity Toggle (#777) 2023-04-04 15:26:53 +02:00
Đỗ Văn Hoài Tuân
d931790ed0 BetterFolders: Fix unread indicator & read all buttons being duplicated (#776) 2023-04-04 05:33:11 +02:00
V
6b26c12bfa Add additional build flavours for Vencord Desktop (#765) 2023-04-04 01:16:29 +02:00
Vendicated
5bb08bdb64 SpotifyControls: Fix crashing on canary
Vencord is still pretty broken on Canary and likely will be for a bit,
but this should at least fix instantly crashing
2023-04-03 21:25:14 +02:00
Vendicated
405be7ef13 Fix weird style on username sheet 2023-04-03 03:13:54 +02:00
Vendicated
a7e2fb48ba fix oopsie 2023-04-03 02:36:54 +02:00
Nuckyz
ae80749dd8 Game Activity Toggle and SettingsStoreAPI (#587) 2023-04-03 02:13:44 +02:00
Vendicated
8c47b7080d QuickReply & Up Key: Do not attempt to edit/reply to logged deleted message 2023-04-02 22:14:58 +02:00
Juby210
8378638ee4 BetterFolders: fix mentions display (#761)
closes #759
2023-04-02 20:31:10 +02:00
V
7c563471f6 Fix typo 2023-04-02 18:31:23 +02:00
Juby210
29382d2781 Add BetterFolders plugin (#530)
Co-authored-by: Ven <vendicated@riseup.net>
2023-04-02 17:43:06 +02:00
Vendicated
6226672ee8 Web: Update extension icon from trolley to Vencord logo 2023-04-02 16:55:36 +02:00
V
5b5ee82f27 Update Contributor Badge to new logo 2023-04-02 16:16:15 +02:00
Remty
62f74f5917 feat(plugin): FakeProfileThemes (#710) 2023-04-02 16:12:19 +02:00
V
265c7a18a7 Delete corruptMp4s.ts
Discord/Electron fixed this bug, so mp4s created by this plugin just look normal on Electron 22, not fixable
2023-04-02 04:33:17 +02:00
Vendicated
462f191051 Bump to v1.1.4 2023-04-02 04:26:05 +02:00
V
6960a439c9 Add Notification log (#745) 2023-04-01 02:47:49 +02:00
Vendicated
4dff1c5bd5 RelationShipNotifier: Delay by 5s to fix false positives 2023-03-31 17:17:50 +02:00
nick
2c8ebdce7d feat(plugin): RelationshipNotifier (#450)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-31 05:07:35 +00:00
Nuckyz
dae7cb67ef Fix IgnoreActivities broken patch (#743) 2023-03-31 04:11:15 +00:00
Berlin
081b01b667 feat(plugin): Wikisearch (#585)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-31 04:09:19 +00:00
Vendicated
5340ea7ba0 Add back window transparency with temporary unsafe settings key 2023-03-31 05:59:45 +02:00
Vendicated
84a649a671 docs: fix ToC 2023-03-31 05:56:08 +02:00
Vendicated
efd9927696 Fix broken plugins 2023-03-31 05:55:25 +02:00
V
c86a34a15d Update 1_INSTALLING.md 2023-03-31 05:30:45 +02:00
Vendicated
ff16513f21 Fix onHeadersReceived clashes when using OpenAsar (fix github raw styles) 2023-03-31 01:18:57 +02:00
Vendicated
906c265aea FakeNitro: Fix fake emote rendering incorrectly in thread previews 2023-03-31 00:15:51 +02:00
Vendicated
708c16176b Remove transparency feature
This not only causes incredibly confusion among users because they
expect it to work without themes, it also causes freezes/whitescreens
for some users. Thus, this feature is disabled for now until someone
contributes a fix!
2023-03-30 23:48:26 +02:00
whqwert
035d1e24b2 feat(SpotifyControls): Fix background color for built-in themes (#731)
Co-authored-by: V <vendicated@riseup.net>
2023-03-30 17:09:04 +02:00
Vendicated
48e9b1be7a new Plugin: GifPaste - Insert Gif links instead of sending 2023-03-30 15:58:20 +02:00
Vendicated
6acdaf207d NoTrack: Update description & authors 2023-03-30 01:41:18 +02:00
Vendicated
9d41b360c9 Fix NoTrack 2023-03-30 01:35:42 +02:00
Vendicated
12cbd73e7f SpotifyControls: Add right click menus to title/album/artists 2023-03-30 01:29:34 +02:00
Phil
420b068094 Fix makeProxy returning stale proxies after assigning objects (#722) 2023-03-28 18:26:57 +00:00
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
316 changed files with 18371 additions and 4743 deletions

View File

@ -37,7 +37,7 @@
" * Vencord, a modification for Discord's desktop app",
{
"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",
@ -82,9 +82,13 @@
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
"dot-notation": "error",
"no-useless-escape": "error",
"no-useless-escape": [
"error",
{
"extra": "i"
}
],
"no-fallthrough": "error",
"for-direction": "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
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
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: |
mv dist/*.xpi dist/Vencord-for-Firefox.xpi
mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip
rm -rf dist/extension-v2-unpacked
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
- name: Get some values needed for the release
id: release_values
run: |
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Upload Devbuild
- name: Upload DevBuild as release
run: |
gh release upload devbuild --clobber dist/*
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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

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

@ -0,0 +1,60 @@
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: |
# 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
cd dist/chromium-unpacked
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
# Firefox
cd ../firefox-unpacked
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,7 +36,21 @@ jobs:
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser)
esbuild test/generateReport.ts > dist/report.mjs
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 scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}

View File

@ -15,7 +15,7 @@ jobs:
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ node_modules
vencord_installer
.idea
.DS_Store
yarn.lock
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": [
"EditorConfig.EditorConfig",
"pmneo.tsimporter",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"EditorConfig.EditorConfig",
"ExodiusStudios.comment-anchors",
"formulahendry.auto-rename-tag",
"GregorBiswanger.json2ts",
"eamodio.gitlens",
"kamikillerto.vscode-colorize"
"stylelint.vscode-stylelint"
]
}

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
A Discord client mod that does things differently
The cutest Discord client mod
## Features
- Super easy to install, no git or node or anything else required
- Many 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
- Browser Support: Run Vencord in your Browser via extension or UserScript
- Super easy to install (Download Installer, open, click install button, done)
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
- Some highlights: SpotifyControls, GameActivityToggle, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- 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)
- 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
If you're just a normal user, use [our simple gui installer!](https://github.com/Vendicated/VencordInstaller#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)
[![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)
## 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,
except run `pnpm buildWeb` instead of `pnpm build`, and your outputs will be in the dist folder
## Building from Source
```sh
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!
See the docs folder
## Contributing
@ -56,3 +41,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS
[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]
## 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"]?.toLowerCase().split(/,\s/g);
if (methods && !methods.includes(method.toLowerCase())) 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 +1,32 @@
/*
* 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 };
/**
* @template T
* @param {T[]} arr
* @param {(v: T) => boolean} predicate
*/
function removeFirst(arr, predicate) {
const idx = arr.findIndex(predicate);
if (idx !== -1) arr.splice(idx, 1);
}
var cspHeaders = [
"content-security-policy",
"content-security-policy-report-only",
];
chrome.webRequest.onHeadersReceived.addListener(
({ responseHeaders, type, url }) => {
if (!responseHeaders) return;
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"]
if (type === "main_frame") {
// In main frame requests, the CSP needs to be removed to enable fetching of custom css
// as desired by the user
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy");
} else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com")) {
// Most users will load css from GitHub, but GitHub doesn't set the correct content type,
// so we fix it here
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-type");
responseHeaders.push({
name: "Content-Type",
value: "text/css"
});
}
return { responseHeaders };
},
{ urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"], types: ["main_frame", "stylesheet"] },
["blocking", "responseHeaders"]
);

View File

@ -2,7 +2,18 @@ if (typeof browser === "undefined") {
var browser = chrome;
}
var script = document.createElement("script");
const script = document.createElement("script");
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: 1.1 KiB

View File

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

View File

@ -1,25 +1,41 @@
{
"manifest_version": 2,
"minimum_chrome_version": "91",
"name": "Vencord Web",
"description": "The Vencord Client Mod for Discord Web.",
"version": "1.0.0",
"description": "The cutest Discord mod now in your browser",
"author": "Vendicated",
"homepage_url": "https://github.com/Vendicated/Vencord",
"icons": {
"128": "icon.png"
},
"permissions": [
"webRequest",
"webRequestBlocking",
"*://*.discord.com/*",
"https://raw.githubusercontent.com/*"
],
"content_scripts": [
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["content.js"]
"js": ["content.js"],
"all_frames": true
}
],
"web_accessible_resources": ["dist/Vencord.js"],
"background": {
"scripts": ["background.js"]
},
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
"browser_specific_settings": {
"gecko": {
"id": "vencord-firefox@vendicated.dev",
"strict_min_version": "91.0"
}
}
}

View File

@ -15,7 +15,7 @@
]
},
"condition": {
"resourceTypes": ["main_frame"]
"resourceTypes": ["main_frame", "sub_frame"]
}
},
{

View File

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

@ -1,5 +1,5 @@
> **Warning**
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
# Installation Guide
@ -13,12 +13,6 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
- [Installing Vencord](#installing-vencord)
- [Updating Vencord](#updating-vencord)
- [Uninstalling Vencord](#uninstalling-vencord)
- [Manually Installing Vencord](#manually-installing-vencord)
- [On Windows](#on-windows)
- [On Linux](#on-linux)
- [On MacOS](#on-macos)
- [Manual Patching](#manual-patching)
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
## Dependencies
@ -27,16 +21,16 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
## Installing Vencord
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
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/root depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
```shell
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:
```shell
@ -101,103 +95,4 @@ Simply run:
pnpm uninject
```
The above command may ask you to also run:
```shell
pnpm install --frozen-lockfile
pnpm uninject
```
## Manually Installing Vencord
- [Windows](#on-windows)
- [Linux](#on-linux)
- [MacOS](#on-macos)
### On Windows
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
Now follow the instructions at [Manual Patching](#manual-patching)
### On Linux
The Discord folder is usually in one of the following paths:
- /usr/share
- /usr/lib64
- /opt
- /home/$USER/.local/share
If you use flatpak, it will usually be in one of the following paths:
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
You will need to give flatpak access to vencord with one of the following commands:
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
If Discord flatpak install is in /home/:
```shell
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
```
If Discord flatpak install not in /home/:
```shell
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
```
Now follow the instructions at [Manual Patching](#manual-patching)
### On MacOS
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
Go to the `Contents/Resources` folder.
Now follow the instructions at [Manual Patching](#manual-patching)
### Manual Patching
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
Make a new folder in `resources` called `app`. In here, we will make two files:
`package.json` and `index.js`
In `index.js`:
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
```js
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
require("../app.asar");
```
And in `package.json`:
```json
{ "name": "discord", "main": "index.js" }
```
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
### Manually Uninstalling Vencord
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

View File

@ -26,6 +26,10 @@ export default definePlugin({
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: [],
// Delete these two below if you are only using code patches
start() {},

View File

@ -1,9 +1,8 @@
{
"name": "vencord",
"private": "true",
"version": "1.0.1",
"description": "A Discord client mod that does things differently",
"keywords": [],
"version": "1.1.9",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
"url": "https://github.com/Vendicated/Vencord/issues"
@ -20,32 +19,37 @@
"scripts": {
"build": "node scripts/build/build.mjs",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"inject": "node scripts/patcher/install.js",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"generatePluginJson": "tsx scripts/generatePluginList.ts",
"inject": "node scripts/runInstaller.mjs",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern src/userplugins",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"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",
"testTsc": "tsc --noEmit",
"uninject": "node scripts/patcher/uninstall.js",
"watch": "node scripts/build/build.mjs --watch"
"uninject": "node scripts/runInstaller.mjs",
"watch": "node scripts/build/build.mjs --watch",
"buildTypes": "ttsc --emitDeclarationOnly --declaration --outDir packages/vencord-types"
},
"dependencies": {
"fflate": "^0.7.4"
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.3",
"fflate": "^0.7.4",
"nanoid": "^4.0.2",
"virtual-merge": "^1.0.1"
},
"devDependencies": {
"@types/diff": "^5.0.2",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.3",
"console-menu": "^0.1.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.49.0",
"diff": "^5.1.0",
"discord-types": "^1.3.26",
"esbuild": "^0.15.16",
"esbuild": "^0.15.18",
"eslint": "^8.28.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-header": "^3.1.1",
@ -54,15 +58,33 @@
"eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0",
"moment": "^2.29.4",
"puppeteer-core": "^19.3.0",
"puppeteer-core": "^19.6.0",
"standalone-electron-types": "^1.0.0",
"type-fest": "^3.3.0",
"typescript": "^4.9.3"
"stylelint": "^14.16.1",
"stylelint-config-standard": "^29.0.0",
"tsx": "^3.12.6",
"ttypescript": "^1.5.15",
"type-fest": "^3.5.3",
"typescript": "^4.9.4",
"typescript-transform-paths": "^3.4.6"
},
"packageManager": "pnpm@7.13.4",
"packageManager": "pnpm@8.1.1",
"pnpm": {
"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": {
@ -71,5 +93,9 @@
"overwriteDest": true
},
"sourceDir": "./dist/extension-v2-unpacked"
},
"engines": {
"node": ">=18",
"pnpm": ">=8"
}
}

7
packages/vencord-types/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*
!.*ignore
!package.json
!README.md
!prepare.ts
!index.d.ts
!globals.d.ts

View File

@ -0,0 +1,3 @@
node_modules
prepare.ts
.gitignore

View File

@ -0,0 +1,11 @@
# Vencord Types
Typings for Vencord's api, published to npm
```sh
npm i @vencord/types
yarn add @vencord/types
pnpm add @vencord/types
```

24
packages/vencord-types/globals.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
/*
* 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/>.
*/
declare global {
export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord");
}
export { };

5
packages/vencord-types/index.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/* eslint-disable */
/// <reference path="Vencord.d.ts" />
/// <reference path="globals.d.ts" />
/// <reference path="modules.d.ts" />

View File

@ -0,0 +1,26 @@
{
"name": "@vencord/types",
"private": false,
"version": "0.1.3",
"description": "",
"types": "index.d.ts",
"scripts": {
"prepublishOnly": "tsx ./prepare.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Vencord",
"license": "GPL-3.0",
"devDependencies": {
"tsx": "^3.12.6"
},
"dependencies": {
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"discord-types": "^1.3.26",
"standalone-electron-types": "^1.0.0",
"type-fest": "^3.5.3"
}
}

View File

@ -0,0 +1,44 @@
/*
* 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 { cpSync, readdirSync, rmSync } from "fs";
import { join } from "path";
const SRC = join(__dirname, "..", "..", "src");
for (const file of ["preload.d.ts", "userplugins", "main", "debug"]) {
rmSync(join(__dirname, file), { recursive: true, force: true });
}
function copyDtsFiles(from: string, to: string) {
for (const file of readdirSync(from, { withFileTypes: true })) {
// bad
if (from === SRC && file.name === "globals.d.ts") continue;
const fullFrom = join(from, file.name);
const fullTo = join(to, file.name);
if (file.isDirectory()) {
copyDtsFiles(fullFrom, fullTo);
} else if (file.name.endsWith(".d.ts")) {
cpSync(fullFrom, fullTo);
}
}
}
copyDtsFiles(SRC, __dirname);

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

2226
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
packages:
- packages/*

View File

@ -48,6 +48,7 @@ const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.j
const sourcemap = watch ? "inline" : "external";
await Promise.all([
// common preload
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
@ -55,12 +56,19 @@ await Promise.all([
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
}),
// Discord Desktop main & renderer
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/patcher.ts"],
entryPoints: ["src/main/index.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VENCORD_DESKTOP: false
}
}),
esbuild.build({
...commonOpts,
@ -68,16 +76,52 @@ await Promise.all([
outfile: "dist/renderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
footer: { js: "Vencord.Plugins.loadExternalPlugins();\n//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
globalName: "Vencord",
sourcemap,
plugins: [
globPlugins,
globPlugins("discordDesktop"),
...commonOpts.plugins
],
define: {
...defines,
IS_WEB: false
IS_WEB: false,
IS_DISCORD_DESKTOP: true,
IS_VENCORD_DESKTOP: false
}
}),
// Vencord Desktop main & renderer
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/vencordDesktopMain.js",
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VENCORD_DESKTOP: true
}
}),
esbuild.build({
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/vencordDesktopRenderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "Vencord.Plugins.loadExternalPlugins();\n//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
globalName: "Vencord",
sourcemap,
plugins: [
globPlugins("vencordDesktop"),
...commonOpts.plugins
],
define: {
...defines,
IS_WEB: false,
IS_DISCORD_DESKTOP: false,
IS_VENCORD_DESKTOP: true
}
}),
]).catch(err => {

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

@ -20,13 +20,13 @@
import esbuild from "esbuild";
import { zip } from "fflate";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
import { readFile } from "fs/promises";
import { join, resolve } from "path";
import { readFileSync } from "fs";
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
import { join } from "path";
// wtf is this assert syntax
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}
@ -36,18 +36,18 @@ const commonOptions = {
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife",
external: ["plugins", "git-hash"],
external: ["plugins", "git-hash", "/assets/*"],
plugins: [
globPlugins,
gitHashPlugin,
gitRemotePlugin,
fileIncludePlugin
globPlugins("web"),
...commonOpts.plugins,
],
target: ["esnext"],
define: {
IS_WEB: "true",
IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch)
IS_DEV: JSON.stringify(watch),
IS_DISCORD_DESKTOP: "false",
IS_VENCORD_DESKTOP: "false"
}
};
@ -60,51 +60,89 @@ await Promise.all(
}),
esbuild.build({
...commonOptions,
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
"window": "unsafeWindow",
...(commonOptions?.define)
},
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`)
},
footer: {
// 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) {
const entries = {
"dist/Vencord.js": readFileSync("dist/browser.js"),
...Object.fromEntries(await Promise.all(files.map(async f => [
(f.startsWith("manifest") ? "manifest.json" : f),
await readFile(join("browser", f))
]))),
"dist/Vencord.js": await readFile("dist/browser.js"),
"dist/Vencord.css": await readFile("dist/browser.css"),
...Object.fromEntries(await Promise.all(files.map(async 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) {
zip(entries, {}, (err, data) => {
if (err) {
console.error(err);
process.exitCode = 1;
} else {
writeFileSync("dist/" + target, data);
console.info("Extension written to dist/" + target);
}
return new Promise((resolve, reject) => {
zip(entries, {}, (err, data) => {
if (err) {
reject(err);
} else {
const out = join("dist", target);
writeFile(out, data).then(() => {
console.info("Extension written to " + out);
resolve();
}).catch(reject);
}
});
});
} else {
if (existsSync(target))
rmSync(target, { recursive: true });
for (const entry in entries) {
const destination = "dist/" + target + "/" + entry;
const parentDirectory = resolve(destination, "..");
mkdirSync(parentDirectory, { recursive: true });
writeFileSync(destination, entries[entry]);
}
await rm(target, { recursive: true, force: true });
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
const dest = join("dist", target, file);
const parentDirectory = join(dest, "..");
await mkdir(parentDirectory, { recursive: true });
await writeFile(dest, content);
}));
console.info("Unpacked Extension written to dist/" + target);
}
}
await buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true);
await buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true);
await buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false);
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
const cssRuntime = `
;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("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
buildPluginZip("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"], false),
]);

View File

@ -17,9 +17,9 @@
*/
import { exec, execSync } from "child_process";
import { existsSync } from "fs";
import { existsSync, readFileSync } from "fs";
import { readdir, readFile } from "fs/promises";
import { join } from "path";
import { join, relative } from "path";
import { promisify } from "util";
export const watch = process.argv.includes("--watch");
@ -33,9 +33,11 @@ export const banner = {
`.trim()
};
const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/**
* @type {esbuild.Plugin}
* @type {import("esbuild").Plugin}
*/
export const makeAllPackagesExternalPlugin = {
name: "make-all-packages-external",
@ -46,9 +48,9 @@ export const makeAllPackagesExternalPlugin = {
};
/**
* @type {esbuild.Plugin}
* @type {(kind: "web" | "discordDesktop" | "vencordDesktop") => import("esbuild").Plugin}
*/
export const globPlugins = {
export const globPlugins = kind => ({
name: "glob-plugins",
setup: build => {
const filter = /^~plugins$/;
@ -68,11 +70,20 @@ export const globPlugins = {
if (!existsSync(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`);
for (const file of files) {
if (file === "index.ts") {
continue;
if (file.startsWith(".")) 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" && kind === "discordDesktop") continue;
if (mod === "desktop" && kind === "web") continue;
if (mod === "discordDesktop" && kind !== "discordDesktop") continue;
if (mod === "vencordDesktop" && kind !== "vencordDesktop") continue;
}
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`;
i++;
}
@ -84,10 +95,10 @@ export const globPlugins = {
};
});
}
};
});
/**
* @type {esbuild.Plugin}
* @type {import("esbuild").Plugin}
*/
export const gitHashPlugin = {
name: "git-hash-plugin",
@ -103,7 +114,7 @@ export const gitHashPlugin = {
};
/**
* @type {esbuild.Plugin}
* @type {import("esbuild").Plugin}
*/
export const gitRemotePlugin = {
name: "git-remote-plugin",
@ -125,7 +136,7 @@ export const gitRemotePlugin = {
};
/**
* @type {esbuild.Plugin}
* @type {import("esbuild").Plugin}
*/
export const fileIncludePlugin = {
name: "file-include-plugin",
@ -147,6 +158,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}
*/
@ -158,8 +194,8 @@ export const commonOpts = {
sourcemap: watch ? "inline" : "",
legalComments: "linked",
banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
external: ["~plugins", "~git-hash", "~git-remote"],
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",

View File

@ -16,6 +16,6 @@
* 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 =
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);

View File

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

View File

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

View File

@ -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: "discordDesktop" | "vencordDesktop" | "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", "discordDesktop", "vencordDesktop", "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({
headless: true,
executablePath: process.env.CHROMIUM_BIN
});
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) {
return (handle as JSHandle<Error>)?.getProperty("message")
@ -65,7 +67,7 @@ function toCodeBlock(s: string) {
}
async function printReport() {
console.log("# Vencord Report");
console.log("# Vencord Report" + (CANARY ? " (Canary)" : ""));
console.log();
console.log("## Bad Patches");
@ -98,7 +100,7 @@ async function printReport() {
},
body: JSON.stringify({
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",
embeds: [
{
@ -184,8 +186,20 @@ page.on("console", async e => {
} else if (isDebug) {
console.error(e.text());
} else if (level === "error") {
console.error("Got unexpected error", e.text());
report.otherErrors.push(e.text());
const text = await Promise.all(
e.args().map(async a => {
try {
return await maybeGetError(a) || await a.jsonValue();
} catch (e) {
return a.toString();
}
})
).then(a => a.join(" "));
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 +221,7 @@ function runTime(token: string) {
// Monkey patch Logger to not log with custom css
// @ts-ignore
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
if (level === "warn" || level === "error")
console[level]("[Vencord]", this.name + ":", ...args);
@ -215,6 +230,9 @@ function runTime(token: string) {
// force enable all plugins and patches
Vencord.Plugins.patches.length = 0;
Object.values(Vencord.Plugins.plugins).forEach(p => {
// Needs native server to run
if (p.name === "WebRichPresence (arRPC)") return;
p.required = true;
p.patches?.forEach(patch => {
patch.plugin = p.name;
@ -248,6 +266,8 @@ function runTime(token: string) {
if (!isWasm)
await wreq.e(id as any);
await new Promise(r => setTimeout(r, 500));
}
console.error("[PUP_DEBUG]", "Finished loading chunks!");
@ -271,4 +291,4 @@ await page.evaluateOnNewDocument(`
;(${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 Plugins from "./plugins";
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater";
@ -28,34 +27,77 @@ export { PlainSettings, Settings };
import "./utils/quickCss";
import "./webpack/patchWebpack";
import { popNotice, showNotice } from "./api/Notices";
import { showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins";
import { checkForUpdates, UpdateLogger } from "./utils/updater";
import { localStorage } from "./utils/localStorage";
import { relaunch } from "./utils/native";
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { Router } from "./webpack/common";
import { SettingsRouter } from "./webpack/common";
export let Components: any;
async function syncSettings() {
if (
Settings.cloud.settingsSync && // if it's enabled
Settings.cloud.authenticated // if cloud integrations are enabled
) {
if (localStorage.Vencord_settingsDirty) {
await putCloudSettings();
delete localStorage.Vencord_settingsDirty;
} else if (await getCloudSettings(false)) { // if we synchronized something (false means no sync)
// we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
// potential notifications that might occur. getCloudSettings() will always send a notification regardless if
// there was an error to notify the user, but besides that we only want to show one notification instead of all
// of the possible ones it has (such as when your settings are newer).
showNotification({
title: "Cloud Settings",
body: "Your settings have been updated! Click here to restart to fully apply changes!",
color: "var(--green-360)",
onClick: relaunch
});
}
}
}
async function init() {
await onceReady;
startAllPlugins();
Components = await import("./components");
syncSettings();
if (!IS_WEB) {
try {
const isOutdated = await checkForUpdates();
if (isOutdated && Settings.notifyAboutUpdates)
setTimeout(() => {
showNotice(
"A Vencord update is available!",
"View Update",
() => {
popNotice();
Router.open("VencordUpdater");
}
);
}, 10000);
if (!isOutdated) return;
if (Settings.autoUpdate) {
await update();
await rebuild();
if (Settings.autoUpdateNotification)
setTimeout(() => showNotification({
title: "Vencord has been updated!",
body: "Click here to restart",
permanent: true,
noPersist: true,
onClick: relaunch
}), 10_000);
return;
}
if (Settings.notifyAboutUpdates)
setTimeout(() => showNotification({
title: "A Vencord update is available!",
body: "Click here to view the update",
permanent: true,
noPersist: true,
onClick() {
SettingsRouter.open("VencordUpdater");
}
}), 10_000);
} catch (err) {
UpdateLogger.error("Failed to check for updates", err);
}
@ -76,3 +118,12 @@ async function init() {
}
init();
if (IS_DISCORD_DESKTOP && 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/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { User } from "discord-types/general";
import { HTMLProps } from "react";
import { ComponentType, HTMLProps } from "react";
import Plugins from "~plugins";
@ -27,20 +28,22 @@ export enum BadgePosition {
}
export interface ProfileBadge {
/** The tooltip to show on hover */
tooltip: string;
/** The tooltip to show on hover. Required for image badges */
description?: string;
/** Custom component for the badge (tooltip not included) */
component?: ComponentType<ProfileBadge & BadgeUserArgs>;
/** The custom image to use */
image?: string;
link?: string;
/** Action to perform when you click the badge */
onClick?(): void;
/** Should the user display this badge? */
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>;
/** Insert at start or end? */
position?: BadgePosition;
/** The badge name to display. Discord uses this, but we don't. */
/** The badge name to display, Discord uses this. Required for component badges */
key?: string;
}
@ -51,6 +54,7 @@ const Badges = new Set<ProfileBadge>();
* @param badge The badge to register
*/
export function addBadge(badge: ProfileBadge) {
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
Badges.add(badge);
}
@ -66,17 +70,19 @@ export function removeBadge(badge: ProfileBadge) {
* Inject badges into the profile badges array.
* You probably don't need to use this.
*/
export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
export function _getBadges(args: BadgeUserArgs) {
const badges = [] as ProfileBadge[];
for (const badge of Badges) {
if (!badge.shouldShow || badge.shouldShow(args)) {
badge.position === BadgePosition.START
? badgeArray.unshift(badge)
: badgeArray.push(badge);
? badges.unshift({ ...badge, ...args })
: badges.push({ ...badge, ...args });
}
}
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);
const donorBadge = (Plugins.BadgeAPI as any).getDonorBadge(args.user.id);
if (donorBadge) badges.unshift(donorBadge);
return badgeArray;
return badges;
}
export interface BadgeUserArgs {

View File

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

View File

@ -111,6 +111,7 @@ function registerSubCommands(cmd: Command, plugin: string) {
...o,
type: ApplicationCommandType.CHAT_INPUT,
name: `${cmd.name} ${o.name}`,
id: `${o.name}-${cmd.id}`,
displayName: `${cmd.name} ${o.name}`,
subCommandPath: [{
name: o.name,

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

@ -0,0 +1,155 @@
/*
* 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";
type ContextMenuPatchCallbackReturn = (() => void) | void;
/**
* @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
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
/**
* @param navId 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
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
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
* @param patch The patch to be removed
* @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
* @param children The context menu children
*/
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;
}
const patchedMenus = new WeakSet();
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 {
const callback = patch(props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
} catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
}
}
}
for (const patch of globalPatches) {
try {
const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
} catch (err) {
ContextMenuLogger.error("Global patch errored,", err);
}
}
patchedMenus.add(props);
}

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/>.
*/
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 = {
callback: AccessoryCallback;
position?: number;
@ -44,6 +44,15 @@ export function _modifyAccessories(
props: Record<string, any>
) {
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(
accessory.position != null
? accessory.position < 0
@ -51,7 +60,7 @@ export function _modifyAccessories(
: accessory.position
: elements.length,
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 { MessageStore } from "@webpack/common";
import type { Channel, Message } from "discord-types/general";
import type { Promisable } from "type-fest";
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
@ -41,16 +42,16 @@ export interface MessageExtra {
stickerIds?: string[];
}
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void>;
const sendListeners = new Set<SendListener>();
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) {
try {
const result = listener(channelId, messageObj, extra);
const result = await listener(channelId, messageObj, extra);
if (result && result.cancel === true) {
return true;
}
@ -61,10 +62,10 @@ export function _handlePreSend(channelId: string, messageObj: MessageObject, ext
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) {
try {
listener(channelId, messageId, messageObj);
await listener(channelId, messageId, messageObj);
} catch (e) {
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
}

View File

@ -0,0 +1,123 @@
/*
* 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 { classes } from "@utils/misc";
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,
className,
dismissOnClick
}: NotificationData & { className?: string; }) {
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={classes("vc-notification-root", className)}
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
onClick={() => {
onClick?.();
if (dismissOnClick !== false)
onClose!();
}}
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,110 @@
/*
* 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";
import { persistNotification } from "./notificationLog";
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;
/** Whether this notification should not be persisted in the Notification Log */
noPersist?: boolean;
/** Whether this notification should be dismissed when clicked (defaults to true) */
dismissOnClick?: 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() {
if (typeof Notification === "undefined") return false;
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) {
persistNotification(data);
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,203 @@
/*
* 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 * as DataStore from "@api/DataStore";
import { Settings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import { useAwaiter } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { nanoid } from "nanoid";
import type { DispatchWithoutAction } from "react";
import NotificationComponent from "./NotificationComponent";
import type { NotificationData } from "./Notifications";
interface PersistentNotificationData extends Pick<NotificationData, "title" | "body" | "image" | "icon" | "color"> {
timestamp: number;
id: string;
}
const KEY = "notification-log";
const getLog = async () => {
const log = await DataStore.get(KEY) as PersistentNotificationData[] | undefined;
return log ?? [];
};
const cl = classNameFactory("vc-notification-log-");
const signals = new Set<DispatchWithoutAction>();
export async function persistNotification(notification: NotificationData) {
if (notification.noPersist) return;
const limit = Settings.notifications.logLimit;
if (limit === 0) return;
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
const log = old ?? [];
// Omit stuff we don't need
const {
onClick, onClose, richBody, permanent, noPersist, dismissOnClick,
...pureNotification
} = notification;
log.unshift({
...pureNotification,
timestamp: Date.now(),
id: nanoid()
});
if (log.length > limit && limit !== 200)
log.length = limit;
return log;
});
signals.forEach(x => x());
}
export async function deleteNotification(timestamp: number) {
const log = await getLog();
const index = log.findIndex(x => x.timestamp === timestamp);
if (index === -1) return;
log.splice(index, 1);
await DataStore.set(KEY, log);
signals.forEach(x => x());
}
export function useLogs() {
const [signal, setSignal] = useReducer(x => x + 1, 0);
useEffect(() => {
signals.add(setSignal);
return () => void signals.delete(setSignal);
}, []);
const [log, _, pending] = useAwaiter(getLog, {
fallbackValue: [],
deps: [signal]
});
return [log, pending] as const;
}
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
const [removing, setRemoving] = useState(false);
const ref = React.useRef<HTMLDivElement>(null);
useEffect(() => {
const div = ref.current!;
const setHeight = () => {
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
div.style.height = `${div.clientHeight}px`;
};
setHeight();
}, []);
return (
<div className={cl("wrapper", { removing })} ref={ref}>
<NotificationComponent
{...data}
permanent={true}
dismissOnClick={false}
onClose={() => {
if (removing) return;
setRemoving(true);
setTimeout(() => deleteNotification(data.timestamp), 200);
}}
richBody={
<div className={cl("body")}>
{data.body}
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
</div>
}
/>
</div>
);
}
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
if (!log.length && !pending)
return (
<div className={cl("container")}>
<div className={cl("empty")} />
<Forms.FormText style={{ textAlign: "center" }}>
No notifications yet
</Forms.FormText>
</div>
);
return (
<div className={cl("container")}>
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
</div>
);
}
function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
const [log, pending] = useLogs();
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
<ModalCloseButton onClick={close} />
</ModalHeader>
<ModalContent>
<NotificationLog log={log} pending={pending} />
</ModalContent>
<ModalFooter>
<Button
disabled={log.length === 0}
onClick={() => {
Alerts.show({
title: "Are you sure?",
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
async onConfirm() {
await DataStore.set(KEY, []);
signals.forEach(x => x());
},
confirmText: "Do it!",
confirmColor: "vc-notification-log-danger-btn",
cancelText: "Nevermind"
});
}}
>
Clear Notification Log
</Button>
</ModalFooter>
</ModalRoot>
);
}
export function openNotificationLogModal() {
const key = openModal(modalProps => (
<LogModal
modalProps={modalProps}
close={() => closeModal(key)}
/>
));
}

View File

@ -0,0 +1,122 @@
.vc-notification-root {
/* clear default button styles */
all: unset;
display: flex;
flex-direction: column;
color: var(--text-normal);
background-color: var(--background-secondary-alt);
border-radius: 6px;
overflow: hidden;
cursor: pointer;
width: 100%;
}
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
position: absolute;
z-index: 2147483647;
right: 1rem;
width: 25vw;
min-height: 10vh;
}
.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%;
}
.vc-notification-log-empty {
height: 218px;
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
margin-bottom: 40px;
}
.vc-notification-log-container {
display: flex;
flex-direction: column;
padding: 1em;
overflow: hidden;
}
.vc-notification-log-wrapper {
transition: 200ms ease;
transition-property: height, opacity;
}
.vc-notification-log-wrapper:not(:last-child) {
margin-bottom: 1em;
}
.vc-notification-log-removing {
height: 0 !important;
opacity: 0;
margin-bottom: 1em;
}
.vc-notification-log-body {
display: flex;
flex-direction: column;
}
.vc-notification-log-timestamp {
margin-left: auto;
font-size: 0.8em;
font-weight: lighter;
}
.vc-notification-log-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}

69
src/api/SettingsStore.ts Normal file
View File

@ -0,0 +1,69 @@
/*
* 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 { proxyLazy } from "@utils/proxyLazy";
import { findModuleId, wreq } from "@webpack";
import { Settings } from "./settings";
interface Setting<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T | ((old: T) => T)): Promise<void>;
/**
* React hook for automatically updating components when the setting is updated
*/
useSetting(): T;
settingsStoreApiGroup: string;
settingsStoreApiName: string;
}
const SettingsStores: Array<Setting<any>> | undefined = proxyLazy(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"');
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
const mod = wreq(modId);
if (mod == null) return;
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
});
/**
* Get the store for a setting
* @param group The setting group
* @param name The name of the setting
*/
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
}
/**
* getSettingStore but lazy
*/
export function getSettingStoreLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getSettingStore<T>(group, name));
}

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,19 @@
import * as $Badges from "./Badges";
import * as $Commands from "./Commands";
import * as $ContextMenu from "./ContextMenu";
import * as $DataStore from "./DataStore";
import * as $MemberListDecorators from "./MemberListDecorators";
import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageDecorations from "./MessageDecorations";
import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
import * as $Settings from "./settings";
import * as $SettingsStore from "./SettingsStore";
import * as $Styles from "./Styles";
/**
* An API allowing you to listen to Message Clicks or run your own logic
@ -31,16 +38,16 @@ import * as $ServerList from "./ServerList";
*
* 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
* (snackbars on the top, like the Update prompt)
*/
const Notices = $Notices;
export const Notices = $Notices;
/**
* An API allowing you to register custom commands
*/
const Commands = $Commands;
export const Commands = $Commands;
/**
* A wrapper around IndexedDB. This can store arbitrarily
* large data and supports a lot of datatypes (Blob, Map, ...).
@ -55,22 +62,52 @@ const Commands = $Commands;
* 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}
*/
const DataStore = $DataStore;
export const DataStore = $DataStore;
/**
* 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
*/
const MessagePopover = $MessagePopover;
export const MessagePopover = $MessagePopover;
/**
* 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
*/
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 read, manipulate and automatically update components based on Discord settings
*/
export const SettingsStore = $SettingsStore;
/**
* 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;
/**
* Settings lol
*/
export const Settings = $Settings;
export const settings = $Settings;

View File

@ -16,10 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { debounce } from "@utils/debounce";
import IpcEvents from "@utils/IpcEvents";
import { localStorage } from "@utils/localStorage";
import Logger from "@utils/Logger";
import { mergeDefaults } from "@utils/misc";
import { OptionType } from "@utils/types";
import { putCloudSettings } from "@utils/settingsSync";
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common";
import plugins from "~plugins";
@ -27,23 +30,67 @@ import plugins from "~plugins";
const logger = new Logger("Settings");
export interface Settings {
notifyAboutUpdates: boolean;
autoUpdate: boolean;
autoUpdateNotification: boolean,
useQuickCss: boolean;
enableReactDevtools: boolean;
themeLinks: string[];
frameless: boolean;
transparent: boolean;
winCtrlQ: boolean;
macosTranslucency: boolean;
disableMinSize: boolean;
winNativeTitleBar: boolean;
plugins: {
[plugin: string]: {
enabled: boolean;
[setting: string]: any;
};
};
notifications: {
timeout: number;
position: "top-right" | "bottom-right";
useNative: "always" | "never" | "not-focused";
logLimit: number;
};
cloud: {
authenticated: boolean;
url: string;
settingsSync: boolean;
settingsSyncVersion: number;
};
}
const DefaultSettings: Settings = {
notifyAboutUpdates: true,
autoUpdate: false,
autoUpdateNotification: true,
useQuickCss: true,
themeLinks: [],
enableReactDevtools: false,
plugins: {}
frameless: false,
transparent: false,
winCtrlQ: false,
macosTranslucency: false,
disableMinSize: false,
winNativeTitleBar: false,
plugins: {},
notifications: {
timeout: 5000,
position: "bottom-right",
useNative: "not-focused",
logLimit: 50
},
cloud: {
authenticated: false,
url: "https://api.vencord.dev/",
settingsSync: false,
settingsSyncVersion: 0
}
};
try {
@ -54,6 +101,13 @@ try {
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
}
const saveSettingsOnFrequentAction = debounce(async () => {
if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
await putCloudSettings();
delete localStorage.Vencord_settingsDirty;
}
}, 60_000);
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
const subscriptions = new Set<SubscriptionCallback>();
@ -70,7 +124,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
// Return empty for plugins with no settings
if (path === "plugins" && p in plugins)
return target[p] = makeProxy({
enabled: plugins[p].required ?? false
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
}, root, `plugins.${p}`);
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
@ -109,12 +163,16 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
target[p] = v;
// Call any listeners that are listening to a setting of this path
const setPath = `${path}${path && "."}${p}`;
delete proxyCache[setPath];
for (const subscription of subscriptions) {
if (!subscription._path || subscription._path === setPath) {
subscription(v, setPath);
}
}
// And don't forget to persist the settings!
PlainSettings.cloud.settingsSyncVersion = Date.now();
localStorage.Vencord_settingsDirty = true;
saveSettingsOnFrequentAction();
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
return true;
}
@ -144,11 +202,12 @@ export const Settings = makeProxy(settings);
* @param paths An optional list of paths to whitelist for rerenders
* @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 onUpdate: SubscriptionCallback = paths
? (value, path) => paths.includes(path) && forceUpdate()
? (value, path) => paths.includes(path as UseSettings<Settings>) && forceUpdate()
: forceUpdate;
React.useEffect(() => {
@ -198,3 +257,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 { Margins } from "@utils/margins";
import { LazyComponent } from "@utils/misc";
import { Margins, React } from "@webpack/common";
import { React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard";
interface Props {
interface Props<T = any> {
/** Render nothing if an error occurs */
noop?: boolean;
/** Fallback component to render if an error occurs */
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
/** called when an error occurs */
onError?(error: Error, errorInfo: React.ErrorInfo): void;
/** called when an error occurs. The props property is only available if using .wrap */
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
/** Custom error message */
message?: string;
/** The props passed to the wrapped component. Only used by wrap */
wrappedProps?: T;
}
const color = "#e78284";
@ -65,7 +69,7 @@ const ErrorBoundary = LazyComponent(() => {
}
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("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.";
return (
<ErrorCard style={{
overflow: "hidden",
}}>
<ErrorCard style={{ overflow: "hidden" }}>
<h1>Oh no!</h1>
<p>{msg}</p>
<code>
{this.state.message}
{!!this.state.stack && (
<pre className={Margins.marginTop8}>
<pre className={Margins.top8}>
{this.state.stack}
</pre>
)}
@ -103,11 +105,11 @@ const ErrorBoundary = LazyComponent(() => {
};
}) as
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 {...errorBoundaryProps}>
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
<Component {...props} />
</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/>.
*/
import { Card } from "@webpack/common";
import "./ErrorCard.css";
interface Props {
style?: React.CSSProperties;
className?: string;
}
export function ErrorCard(props: React.PropsWithChildren<Props>) {
import { classes } from "@utils/misc";
import type { HTMLProps } from "react";
export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
return (
<Card className={props.className} style={
{
padding: "2em",
backgroundColor: "#e7828430",
borderColor: "#e78284",
color: "var(--text-normal)",
...props.style
}
}>
<div {...props} className={classes(props.className, "vc-error-card")}>
{props.children}
</Card>
</div>
);
}

View File

@ -29,7 +29,12 @@ const setCss = debounce((css: string) => {
});
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.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
@ -41,4 +46,6 @@ export async function launchMonacoEditor() {
: "vs-dark";
win.document.write(monacoHtml);
window.__VENCORD_MONACO_WIN__ = new WeakRef(win);
}

View File

@ -17,9 +17,12 @@
*/
import { debounce } from "@utils/debounce";
import { Margins } from "@utils/margins";
import { makeCodeblock } from "@utils/misc";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { ReplaceFn } from "@utils/types";
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 ErrorBoundary from "./ErrorBoundary";
@ -41,20 +44,29 @@ const findCandidates = debounce(function ({ find, setModule, setError }) {
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 [compileResult, setCompileResult] = React.useState<[boolean, string]>();
const [patchedCode, matchResult, diff] = React.useMemo(() => {
const src: string = fact.toString().replaceAll("\n", "");
const canonicalMatch = canonicalizeMatch(match);
try {
var patched = src.replace(match, replacement);
const canonicalReplace = canonicalizeReplace(replacement, "YourPlugin");
var patched = src.replace(canonicalMatch, canonicalReplace as string);
setReplacementError(void 0);
} catch (e) {
setReplacementError((e as Error).message);
return ["", [], []];
}
const m = src.match(match);
const m = src.match(canonicalMatch);
return [patched, m, makeDiff(src, patched, m)];
}, [id, match, replacement]);
@ -118,7 +130,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
)}
{!!diff?.length && (
<Button className={Margins.marginTop20} onClick={() => {
<Button className={Margins.top20} onClick={() => {
try {
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
setCompileResult([true, "Compiled successfully"]);
@ -174,24 +186,26 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
error={error ?? replacementError}
/>
{!isFunc && (
<>
<div className="vc-text-selectable">
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
{Object.entries({
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
"$$": "Insert a $",
"$&": "Insert the entire match",
"$`": "Insert the substring before the match",
"$`\u200b": "Insert the substring before 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]) => (
<Forms.FormText key={placeholder}>
{Parser.parse("`" + placeholder + "`")}: {desc}
</Forms.FormText>
))}
</>
</div>
)}
<Switch
className={Margins.marginTop8}
className={Margins.top8}
value={isFunc}
onChange={setIsFunc}
note="'replacement' will be evaled if this is toggled"
@ -206,7 +220,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
function PatchHelper() {
const [find, setFind] = 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>();
@ -245,7 +259,7 @@ function PatchHelper() {
return (
<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>
<TextInput
type="text"
@ -285,7 +299,7 @@ function PatchHelper() {
{!!(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>
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
</>

View File

@ -20,8 +20,9 @@ import { generateId } from "@api/Commands";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { LazyComponent } from "@utils/misc";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { Margins } from "@utils/margins";
import { classes, LazyComponent } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { proxyLazy } from "@utils/proxyLazy";
import { OptionType, Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack";
@ -84,6 +85,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const canSubmit = () => Object.values(errors).every(e => !e);
const hasSettings = Boolean(pluginSettings && plugin.options);
React.useEffect(() => {
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
@ -121,33 +124,34 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
}
function renderSettings() {
if (!pluginSettings || !plugin.options) {
if (!hasSettings || !plugin.options) {
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) {
@ -171,15 +175,17 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
}
return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-md/bold">{plugin.name}</Text>
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM} className="vc-text-selectable">
<ModalHeader separator={false}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>{plugin.name}</Text>
<ModalCloseButton onClick={onClose} />
</ModalHeader>
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
<Forms.FormSection>
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
<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
users={authors}
count={plugin.authors.length}
@ -193,10 +199,10 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</div>
</Forms.FormSection>
{!!plugin.settingsAboutComponent && (
<div style={{ marginBottom: 8 }}>
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
<Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
<plugin.settingsAboutComponent />
<plugin.settingsAboutComponent tempSettings={tempSettings} />
</ErrorBoundary>
</Forms.FormSection>
</div>
@ -206,13 +212,14 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
{renderSettings()}
</Forms.FormSection>
</ModalContent>
<ModalFooter>
{hasSettings && <ModalFooter>
<Flex flexDirection="column" style={{ width: "100%" }}>
<Flex style={{ marginLeft: "auto" }}>
<Button
onClick={onClose}
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
color={Button.Colors.WHITE}
look={Button.Looks.LINK}
>
Cancel
</Button>
@ -233,7 +240,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</Flex>
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
</Flex>
</ModalFooter>
</ModalFooter>}
</ModalRoot>
);
}

View File

@ -21,7 +21,7 @@ import { Forms, React, Select } from "@webpack/common";
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 [state, setState] = React.useState(def ?? false);
@ -37,7 +37,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
];
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);
else if (!isValid) setError("Invalid input provided.");
else {
@ -51,7 +51,7 @@ export function SettingBooleanComponent({ option, pluginSettings, id, onChange,
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select
isDisabled={option.disabled?.() ?? false}
isDisabled={option.disabled?.call(definedSettings) ?? false}
options={options}
placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5}

View File

@ -23,7 +23,7 @@ import { ISettingElementProps } from ".";
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) {
if (option.type === OptionType.BIGINT) return BigInt(value);
return Number(value);
@ -37,10 +37,13 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
}, [error]);
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);
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}`);
onChange(serialize(newValue));
} else {
@ -58,7 +61,7 @@ export function SettingNumericComponent({ option, pluginSettings, id, onChange,
value={state}
onChange={handleChange}
placeholder={option.placeholder ?? "Enter a number"}
disabled={option.disabled?.() ?? false}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
/>
{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 ".";
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 [state, setState] = React.useState<any>(def ?? null);
@ -32,10 +32,11 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
}, [error]);
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);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
setState(newValue);
onChange(newValue);
}
@ -45,7 +46,7 @@ export function SettingSelectComponent({ option, pluginSettings, onChange, onErr
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select
isDisabled={option.disabled?.() ?? false}
isDisabled={option.disabled?.call(definedSettings) ?? false}
options={option.options}
placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5}

View File

@ -29,7 +29,7 @@ export function makeRange(start: number, end: number, step = 1) {
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 [error, setError] = React.useState<string | null>(null);
@ -39,7 +39,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
}, [error]);
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);
else if (!isValid) setError("Invalid input provided.");
else {
@ -52,7 +52,7 @@ export function SettingSliderComponent({ option, pluginSettings, id, onChange, o
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Slider
disabled={option.disabled?.() ?? false}
disabled={option.disabled?.call(definedSettings) ?? false}
markers={option.markers}
minValue={option.markers[0]}
maxValue={option.markers[option.markers.length - 1]}

View File

@ -21,7 +21,7 @@ import { Forms, React, TextInput } from "@webpack/common";
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 [error, setError] = React.useState<string | null>(null);
@ -30,10 +30,11 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
}, [error]);
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);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
setState(newValue);
onChange(newValue);
}
@ -47,7 +48,7 @@ export function SettingTextComponent({ option, pluginSettings, id, onChange, onE
value={state}
onChange={handleChange}
placeholder={option.placeholder ?? "Enter a value"}
disabled={option.disabled?.() ?? false}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
/>
{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/>.
*/
import { PluginOptionBase } from "@utils/types";
import { DefinedSettings, PluginOptionBase } from "@utils/types";
export interface ISettingElementProps<T extends PluginOptionBase> {
option: T;
@ -27,8 +27,10 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
};
id: string;
onError(hasError: boolean): void;
definedSettings?: DefinedSettings;
}
export * from "../../Badge";
export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent";
export * from "./SettingNumericComponent";

View File

@ -16,29 +16,37 @@
* 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 { Settings, useSettings } from "@api/settings";
import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
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 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 { Plugin } from "@utils/types";
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 { 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 InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
@ -54,23 +62,27 @@ function showErrorToast(message: string) {
});
}
interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> {
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." : ".";
function ReloadRequiredCard({ required }: { required: boolean; }) {
return (
<ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}>
<span style={{ margin: "auto 0" }}>
{pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix}
</span>
<Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button>
</ErrorCard>
<Card className={cl("info-card", { "restart-card": required })}>
{required ? (
<>
<Forms.FormTitle tag="h5">Restart required!</Forms.FormTitle>
<Forms.FormText className={cl("dep-text")}>
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 +90,13 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
plugin: Plugin;
disabled: boolean;
onRestartNeeded(name: string): void;
isNew?: boolean;
}
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave }: PluginCardProps) {
const settings = useSettings();
const pluginSettings = settings.plugins[plugin.name];
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = useSettings([`plugins.${plugin.name}.enabled`]).plugins[plugin.name];
const [iconHover, setIconHover] = React.useState(false);
function isEnabled() {
return pluginSettings?.enabled || plugin.started;
}
const isEnabled = () => settings.enabled ?? false;
function openModal() {
openModalLazy(async () => {
@ -110,7 +118,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
return;
} else if (restartNeeded) {
// If any dependencies have patches, don't start the plugin yet.
pluginSettings.enabled = true;
settings.enabled = true;
onRestartNeeded(plugin.name);
return;
}
@ -118,14 +126,14 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
if (plugin.patches) {
pluginSettings.enabled = !wasEnabled;
settings.enabled = !wasEnabled;
onRestartNeeded(plugin.name);
return;
}
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
if (wasEnabled && !plugin.started) {
pluginSettings.enabled = !wasEnabled;
settings.enabled = !wasEnabled;
return;
}
@ -138,53 +146,38 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
return;
}
pluginSettings.enabled = !wasEnabled;
settings.enabled = !wasEnabled;
}
return (
<Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Switch
onChange={toggleEnabled}
disabled={disabled}
value={isEnabled()}
note={<Text variant="text-md/normal" style={{
height: 40,
overflow: "hidden",
// mfw css is so bad you need whatever this is to get multi line overflow ellipsis to work
textOverflow: "ellipsis",
display: "-webkit-box", // firefox users will cope (it doesn't support it)
WebkitLineClamp: 2,
lineClamp: 2,
WebkitBoxOrient: "vertical",
boxOrient: "vertical"
}}>
{plugin.description}
</Text>}
hideBorder={true}
>
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}>
<Text variant="text-md/bold" style={{ flexGrow: "1" }}>{plugin.name}</Text>
<button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur">
{plugin.options
? <CogWheel
style={{ color: iconHover ? "" : "var(--text-muted)" }}
onMouseEnter={() => setIconHover(true)}
onMouseLeave={() => setIconHover(false)}
/>
: <InfoIcon
width="24" height="24"
style={{ color: iconHover ? "" : "var(--text-muted)" }}
onMouseEnter={() => setIconHover(true)}
onMouseLeave={() => setIconHover(false)}
/>}
</button>
</Flex>
</Switch>
</Flex>
<Flex className={cl("card", { "card-disabled": disabled })} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<div className={cl("card-header")}>
<Text variant="text-md/bold" className={cl("name")}>
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
</Text>
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
{plugin.options
? <CogWheel />
: <InfoIcon width="24" height="24" />}
</button>
<Switch
checked={isEnabled()}
onChange={toggleEnabled}
disabled={disabled}
/>
</div>
<Text className={cl("note")} variant="text-sm/normal">{plugin.description}</Text>
</Flex >
);
}
export default ErrorBoundary.wrap(function Settings() {
enum SearchStatus {
ALL,
ENABLED,
DISABLED
}
export default ErrorBoundary.wrap(function PluginSettings() {
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []);
@ -225,41 +218,102 @@ export default ErrorBoundary.wrap(function Settings() {
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
.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 onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status }));
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
const showEnabled = searchValue.status === "enabled" || searchValue.status === "all";
const showDisabled = searchValue.status === "disabled" || searchValue.status === "all";
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started;
const enabled = settings.plugins[plugin.name]?.enabled;
if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
if (!searchValue.value.length) return true;
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 (
<Forms.FormSection>
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
<Forms.FormSection className={Margins.top16}>
<ReloadRequiredCard required={changes.hasChanges} />
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
Filters
</Forms.FormTitle>
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
<div style={styles.FiltersBar}>
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
<div className={cl("filter-controls")}>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
<div className={InputStyles.inputWrapper}>
<Select
className={InputStyles.inputDefault}
options={[
{ label: "Show All", value: "all", default: true },
{ label: "Show Enabled", value: "enabled" },
{ label: "Show Disabled", value: "disabled" }
{ label: "Show All", value: SearchStatus.ALL, default: true },
{ label: "Show Enabled", value: SearchStatus.ENABLED },
{ label: "Show Disabled", value: SearchStatus.DISABLED }
]}
serialize={String}
select={onStatusChange}
@ -269,51 +323,19 @@ export default ErrorBoundary.wrap(function Settings() {
</div>
</div>
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
<div style={styles.PluginsGrid}>
{sortedPlugins?.length ? sortedPlugins
.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 className={cl("grid")}>
{plugins}
</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
</Forms.FormTitle>
<div style={styles.PluginsGrid}>
{sortedPlugins?.length ? sortedPlugins
.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 className={cl("grid")}>
{requiredPlugins}
</div>
</Forms.FormSection >
);
@ -326,11 +348,7 @@ function makeDependencyList(deps: string[]) {
return (
<React.Fragment>
<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>
);
}
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,39 +18,36 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
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() {
return (
<Forms.FormSection title="Settings Sync">
<Card style={{
backgroundColor: "var(--info-warning-background)",
borderColor: "var(--info-warning-foreground)",
color: "var(--info-warning-text)",
padding: "1em",
marginBottom: "0.5em",
}}>
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
<Flex flexDirection="column">
<strong>Warning</strong>
<span>Importing a settings file will overwrite your current settings.</span>
</Flex>
</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.
This allows you to easily transfer your settings to another device,
or recover your settings after reinstalling Vencord or Discord.
</Text>
<Text variant="text-md/normal" className={Margins.marginBottom8}>
<Text variant="text-md/normal" className={Margins.bottom8}>
Settings Export contains:
<ul>
<li>&mdash; Custom QuickCSS</li>
<li>&mdash; Theme Links</li>
<li>&mdash; Plugin Settings</li>
</ul>
</Text>
<Flex>
<Button
onClick={uploadSettingsBackup}
onClick={() => uploadSettingsBackup()}
size={Button.Sizes.SMALL}
>
Import Settings

View File

@ -0,0 +1,164 @@
/*
* 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 { showNotification } from "@api/Notifications";
import { Settings, useSettings } from "@api/settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins";
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
function validateUrl(url: string) {
try {
new URL(url);
return true;
} catch {
return "Invalid URL";
}
}
async function eraseAllData() {
const res = await fetch(new URL("/v1/", getCloudUrl()), {
method: "DELETE",
headers: new Headers({
Authorization: await getCloudAuth()
})
});
if (!res.ok) {
cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
showNotification({
title: "Cloud Integrations",
body: `Could not erase all data (API returned ${res.status}), please contact support.`,
color: "var(--red-360)"
});
return;
}
Settings.cloud.authenticated = false;
await deauthorizeCloud();
showNotification({
title: "Cloud Integrations",
body: "Successfully erased all data.",
color: "var(--green-360)"
});
}
function SettingsSyncSection() {
const { cloud } = useSettings(["cloud.authenticated", "cloud.settingsSync"]);
const sectionEnabled = cloud.authenticated && cloud.settingsSync;
return (
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
minimal effort.
</Forms.FormText>
<Switch
key="cloud-sync"
disabled={!cloud.authenticated}
value={cloud.settingsSync}
onChange={v => { cloud.settingsSync = v; }}
>
Settings Sync
</Switch>
<div className="vc-cloud-settings-sync-grid">
<Button
size={Button.Sizes.SMALL}
disabled={!sectionEnabled}
onClick={() => putCloudSettings()}
>Sync to Cloud</Button>
<Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
{({ onMouseLeave, onMouseEnter }) => (
<Button
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
disabled={!sectionEnabled}
onClick={() => getCloudSettings(true, true)}
>Sync from Cloud</Button>
)}
</Tooltip>
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
disabled={!sectionEnabled}
onClick={() => deleteCloudSettings()}
>Delete Cloud Settings</Button>
</div>
</Forms.FormSection>
);
}
function CloudTab() {
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
return (
<>
<Forms.FormSection title="Cloud Settings" className={Margins.top16}>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
Vencord comes with a cloud integration that adds goodies like settings sync across devices.
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
can host it yourself.
</Forms.FormText>
<Switch
key="backend"
value={settings.cloud.authenticated}
onChange={v => { v && authorizeCloud(); if (!v) settings.cloud.authenticated = v; }}
note="This will request authorization if you have not yet set up cloud integrations."
>
Enable Cloud Integrations
</Switch>
<Forms.FormTitle tag="h5">Backend URL</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8}>
Which backend to use when using cloud integrations.
</Forms.FormText>
<CheckedTextInput
key="backendUrl"
value={settings.cloud.url}
onChange={v => { settings.cloud.url = v; settings.cloud.authenticated = false; deauthorizeCloud(); }}
validate={validateUrl}
/>
<Button
className={Margins.top8}
size={Button.Sizes.MEDIUM}
color={Button.Colors.RED}
disabled={!settings.cloud.authenticated}
onClick={() => Alerts.show({
title: "Are you sure?",
body: "Once your data is erased, we cannot recover it. There's no going back!",
onConfirm: eraseAllData,
confirmText: "Erase it!",
confirmColor: "vc-cloud-erase-data-danger-btn",
cancelText: "Nevermind"
})}
>Erase All Data</Button>
<Forms.FormDivider className={Margins.top16} />
</Forms.FormSection >
<SettingsSyncSection />
</>
);
}
export default ErrorBoundary.wrap(CloudTab);

View File

@ -19,9 +19,10 @@
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { useAwaiter } from "@utils/misc";
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");
@ -51,13 +52,14 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
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>
<div>
{themeLinks.map(link => (
<Card style={{
padding: ".5em",
marginBottom: ".5em"
marginBottom: ".5em",
marginTop: ".5em"
}} key={link}>
<Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word"
@ -74,11 +76,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
export default ErrorBoundary.wrap(function () {
const settings = useSettings();
const ref = React.useRef<HTMLTextAreaElement>();
const [themeText, setThemeText] = React.useState(settings.themeLinks.join("\n"));
function onBlur() {
settings.themeLinks = [...new Set(
ref.current!.value
themeText
.trim()
.split(/\n+/)
.map(s => s.trim())
@ -88,28 +90,24 @@ export default ErrorBoundary.wrap(function () {
return (
<>
<Card style={{
padding: "1em",
marginBottom: "1em",
marginTop: "1em"
}}>
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
<Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to .theme.css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>Be careful to use the raw links or github.io links!</Forms.FormText>
<Forms.FormDivider />
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
<div>
<div style={{ marginBottom: ".5em" }}>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
BetterDiscord Themes
</Link>
<Link href="https://github.com/search?q=discord+theme">Github</Link>
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
</div>
<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, click on it, then click the "Raw" button</Forms.FormText>
<Forms.FormText>
If the theme has configuration that requires you to edit the file:
<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> Edit the file</li>
<li> Use the link to your own repository instead</li>
@ -118,13 +116,9 @@ export default ErrorBoundary.wrap(function () {
</Card>
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
<TextArea
style={{
padding: ".5em",
border: "1px solid var(--background-modifier-accent)"
}}
ref={ref}
defaultValue={settings.themeLinks.join("\n")}
className={TextAreaProps.textarea}
value={themeText}
onChange={setThemeText}
className={`${TextAreaProps.textarea} vc-settings-theme-links`}
placeholder="Theme Links"
spellCheck={false}
onBlur={onBlur}

View File

@ -16,14 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes, useAwaiter } from "@utils/misc";
import { relaunch } from "@utils/native";
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";
@ -69,14 +72,18 @@ interface CommonProps {
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; }) {
return (
<Card style={{ padding: ".5em" }}>
{updates.map(({ hash, author, message }) => (
<div>
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}>
<code>{hash}</code>
</Link>
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
<span style={{
marginLeft: "0.5em",
color: "var(--text-normal)"
@ -104,21 +111,21 @@ function Updatable(props: CommonProps) {
</ErrorCard>
</>
) : (
<Forms.FormText className={Margins.marginBottom8}>
<Forms.FormText className={Margins.bottom8}>
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
</Forms.FormText>
)}
{isOutdated && <Changes updates={updates} {...props} />}
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
<Flex className={classes(Margins.bottom8, Margins.top8)}>
{isOutdated && <Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsUpdating, async () => {
if (await update()) {
setUpdates([]);
const needFullRestart = await rebuild();
await rebuild();
await new Promise<void>(r => {
Alerts.show({
title: "Update Success!",
@ -126,10 +133,7 @@ function Updatable(props: CommonProps) {
confirmText: "Restart",
cancelText: "Not now!",
onConfirm() {
if (needFullRestart)
window.DiscordNative.app.relaunch();
else
location.reload();
relaunch();
r();
},
onCancel: r
@ -170,7 +174,7 @@ function Updatable(props: CommonProps) {
function Newer(props: CommonProps) {
return (
<>
<Forms.FormText className={Margins.marginBottom8}>
<Forms.FormText className={Margins.bottom8}>
Your local copy has more recent commits. Please stash or reset them.
</Forms.FormText>
<Changes {...props} updates={changes} />
@ -179,6 +183,8 @@ function Newer(props: CommonProps) {
}
function Updater() {
const settings = useSettings(["notifyAboutUpdates", "autoUpdate", "autoUpdateNotification"]);
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => {
@ -192,16 +198,49 @@ function Updater() {
};
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.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)} ({gitHash})</Forms.FormText>
<Forms.FormText className="vc-text-selectable">
{repoPending
? repo
: err
? "Failed to retrieve - check console"
: (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)
}
{" "}(<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>

View File

@ -17,14 +17,26 @@
*/
import { useSettings } from "@api/settings";
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { Settings, useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import IpcEvents from "@utils/IpcEvents";
import { useAwaiter } from "@utils/misc";
import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
import { Margins } from "@utils/margins";
import { identity, useAwaiter } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native";
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() {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
@ -32,17 +44,63 @@ function VencordSettings() {
});
const settings = useSettings();
const [donateImage] = React.useState(
Math.random() > 0.5
? "https://cdn.discordapp.com/emojis/1026533090627174460.png"
: "https://media.discordapp.net/stickers/1039992459209490513.png"
);
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
const isWindows = navigator.platform.toLowerCase().startsWith("win");
const isMac = navigator.platform.toLowerCase().startsWith("mac");
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 && (!IS_DISCORD_DESKTOP || !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 && false /* This causes electron to freeze / white screen for some people */ && {
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"
},
IS_DISCORD_DESKTOP && {
key: "disableMinSize",
title: "Disable minimum window size",
note: "Requires a full restart"
},
IS_DISCORD_DESKTOP && isMac && {
key: "macosTranslucency",
title: "Enable translucent window",
note: "Requires a full restart"
}
];
return (
<React.Fragment>
<DonateCard image={donateImage} />
<Forms.FormSection title="Quick Actions">
<Card className={st("QuickActionCard")}>
<Card className={cl("quick-actions-card")}>
{IS_WEB ? (
<Button
onClick={() => require("../Monaco").launchMonacoEditor()}
@ -53,7 +111,7 @@ function VencordSettings() {
) : (
<React.Fragment>
<Button
onClick={() => window.DiscordNative.app.relaunch()}
onClick={relaunch}
size={Button.Sizes.SMALL}>
Restart Client
</Button>
@ -64,7 +122,7 @@ function VencordSettings() {
Open QuickCSS File
</Button>
<Button
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
onClick={() => showItemInFolder(settingsDir)}
size={Button.Sizes.SMALL}
disabled={settingsDirPending}>
Open Settings Folder
@ -82,38 +140,110 @@ function VencordSettings() {
<Forms.FormDivider />
<Forms.FormSection title="Settings">
<Forms.FormText className={Margins.marginBottom20}>
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
<Forms.FormText className={Margins.bottom20}>
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
</Forms.FormText>
<Switch
value={settings.useQuickCss}
onChange={(v: boolean) => settings.useQuickCss = v}
note="Loads styles from your QuickCss file">
Use QuickCss
</Switch>
{!IS_WEB && (
<React.Fragment>
<Switch
value={settings.enableReactDevtools}
onChange={(v: boolean) => settings.enableReactDevtools = v}
note="Requires a full restart">
Enable React Developer Tools
</Switch>
<Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a Toast on StartUp">
Get notified about new Updates
</Switch>
</React.Fragment>
)}
{Switches.map(s => s && (
<Switch
key={s.key}
value={settings[s.key]}
onChange={v => settings[s.key] = v}
note={s.note}
>
{s.title}
</Switch>
))}
</Forms.FormSection>
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
</React.Fragment>
);
}
function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) {
return (
<>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{settings.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["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => settings.useNative = v}
isSelected={v => v === settings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={settings.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["position"]; } & Record<string, any>>}
select={v => settings.position = v}
isSelected={v => v === settings.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={settings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={settings.timeout}
onValueChange={v => settings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={settings.logLimit}
onValueChange={v => settings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
<Button
onClick={openNotificationLogModal}
disabled={settings.logLimit === 0}
>
Open Notification Log
</Button>
</>
);
}
interface DonateCardProps {
image: string;
@ -121,18 +251,10 @@ interface DonateCardProps {
function DonateCard({ image }: DonateCardProps) {
return (
<Card style={{
padding: "1em",
display: "flex",
flexDirection: "row",
marginBottom: "1em",
marginTop: "1em"
}}>
<Card className={cl("card", "donate")}>
<div>
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
<Forms.FormText>
Please consider supporting the Development of Vencord by donating!
</Forms.FormText>
<Forms.FormText>Please consider supporting the development of Vencord by donating!</Forms.FormText>
<DonateButton style={{ transform: "translateX(-1em)" }} />
</div>
<img
@ -140,7 +262,7 @@ function DonateCard({ image }: DonateCardProps) {
src={image}
alt=""
height={128}
style={{ marginLeft: "auto", transform: "rotate(10deg)" }}
style={{ marginLeft: "auto", transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : "" }}
/>
</Card>
);

View File

@ -16,25 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { findByCodeLazy } from "@webpack";
import { Forms, Router, Text } from "@webpack/common";
import "./settingsStyles.css";
import cssText from "~fileContent/settingsStyles.css";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { isMobile } from "@utils/misc";
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
import BackupRestoreTab from "./BackupRestoreTab";
import CloudTab from "./CloudTab";
import PluginsTab from "./PluginsTab";
import ThemesTab from "./ThemesTab";
import Updater from "./Updater";
import VencordSettings from "./VencordTab";
const style = document.createElement("style");
style.textContent = cssText;
document.head.appendChild(style);
const st = (style: string) => `vcSettings${style}`;
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
const cl = classNameFactory("vc-settings-");
interface SettingsProps {
tab: string;
@ -50,7 +47,8 @@ const SettingsTabs: Record<string, SettingsTab> = {
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> },
VencordCloud: { name: "Cloud", component: () => <CloudTab /> },
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> }
};
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
@ -58,23 +56,26 @@ if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater /
function Settings(props: SettingsProps) {
const { tab = "VencordSettings" } = props;
const CurrentTab = SettingsTabs[tab]?.component;
const CurrentTab = SettingsTabs[tab]?.component ?? null;
if (isMobile) {
return CurrentTab && <CurrentTab />;
}
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
type={TabBar.Types.TOP}
look={TabBar.Looks.BRAND}
className={st("TabBar")}
type="top"
look="brand"
className={cl("tab-bar")}
selectedItem={tab}
onItemSelect={Router.open}
onItemSelect={SettingsRouter.open}
>
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
if (!component) return null;
return <TabBar.Item
id={key}
className={st("TabBarItem")}
className={cl("tab-bar-item")}
key={key}>
{name}
</TabBar.Item>;
@ -86,7 +87,7 @@ function Settings(props: SettingsProps) {
}
export default function (props: SettingsProps) {
return <ErrorBoundary>
return <ErrorBoundary onError={handleComponentFailed}>
<Settings tab={props.tab} />
</ErrorBoundary>;
}

View File

@ -1,23 +1,68 @@
.vcSettingsTabBar {
.vc-settings-tab-bar {
margin-top: 20px;
margin-bottom: -2px;
border-bottom: 2px solid var(--background-modifier-accent);
}
.vcSettingsTabBarItem {
.vc-settings-tab-bar-item {
margin-right: 32px;
padding-bottom: 16px;
margin-bottom: -2px;
}
.vcSettingsQuickActionCard {
.vc-settings-quick-actions-card {
padding: 1em;
display: flex;
gap: 1em;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
flex-grow: 1;
flex-direction: row;
flex-flow: row wrap;
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);
}
.vc-cloud-settings-sync-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1em;
}
.vc-cloud-erase-data-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}
.vc-text-selectable,
.vc-text-selectable :not(a, button) {
/* make text selectable, silly discord makes the entirety of settings not selectable */
user-select: text;
/* discord also sets cursor: default which prevents the cursor from showing as text */
cursor: initial;
}

View File

@ -16,29 +16,12 @@
* 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() {
if (isOutdated) {
setImmediate(async () => {
const wantsUpdate = confirm(
"Uh Oh! Failed to render this Page." +
" 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!");
}
}
});
}
export function handleComponentFailed() {
maybePromptToUpdate(
"Uh Oh! Failed to render this Page." +
" However, there is an update available that might fix it." +
" Would you like to update and restart now?"
);
}

View File

@ -16,6 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export { default as ErrorBoundary } from "./ErrorBoundary";
export * from "./ErrorCard";
export * from "./Flex";
export * from "./Heart";
export * from "./Link";
export { default as PatchHelper } from "./PatchHelper";
export { default as PluginSettings } from "./PluginSettings";
export * from "./Switch";
export { default as VencordSettings } from "./VencordSettings";

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<title>QuickCss Editor</title>
<title>Vencord QuickCSS Editor</title>
<link rel="stylesheet" data-name="vs/editor/editor.main"
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css">
<style>

15
src/globals.d.ts vendored
View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { LoDashStatic } from "lodash";
declare global {
/**
@ -34,9 +35,17 @@ declare global {
export var IS_WEB: boolean;
export var IS_DEV: boolean;
export var IS_STANDALONE: boolean;
export var IS_DISCORD_DESKTOP: boolean;
export var IS_VENCORD_DESKTOP: boolean;
export var VencordNative: typeof import("./VencordNative").default;
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: {
set(setting: string, v: any): void;
};
@ -44,16 +53,18 @@ declare global {
* Only available when running in Electron, undefined on web.
* 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
* `target: "DESKTOP"`
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
*/
export var DiscordNative: any;
export var VencordDesktop: any;
export var VencordDesktopNative: any;
interface Window {
webpackChunkdiscord_app: {
push(chunk: any): any;
pop(): any;
};
_: LoDashStatic;
[k: string]: any;
}
}

110
src/main/index.ts Normal file
View File

@ -0,0 +1,110 @@
/*
* 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 { app, protocol, session } from "electron";
import { join } from "path";
import { getSettings } from "./ipcMain";
import { IS_VANILLA } from "./utils/constants";
import { installExt } from "./utils/extensions";
if (IS_VENCORD_DESKTOP || !IS_VANILLA) {
app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "vencordDesktopRenderer.js.map":
case "preload.js.map":
case "patcher.js.map":
case "vencordDesktopMain.js.map":
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
}
});
try {
if (getSettings().enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// Remove CSP
type PolicyResult = Record<string, string[]>;
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
}
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
});
}
if (IS_DISCORD_DESKTOP) {
require("./patcher");
}

View File

@ -21,14 +21,14 @@ import "./updater";
import { debounce } from "@utils/debounce";
import IpcEvents from "@utils/IpcEvents";
import { Queue } from "@utils/Queue";
import { BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
import { mkdirSync, readFileSync, watch } from "fs";
import { BrowserWindow, ipcMain, shell } from "electron";
import { mkdirSync, readdirSync, readFileSync, watch } from "fs";
import { open, readFile, writeFile } from "fs/promises";
import { join } from "path";
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
import { ALLOWED_PROTOCOLS, PLUGINS_DIR, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
mkdirSync(SETTINGS_DIR, { recursive: true });
@ -44,8 +44,13 @@ export function readSettings() {
}
}
// Fix for screensharing in Electron >= 17
ipcMain.handle(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, (_, opts) => desktopCapturer.getSources(opts));
export function getSettings(): typeof import("@api/settings").Settings {
try {
return JSON.parse(readSettings());
} catch {
return {} as any;
}
}
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
@ -80,7 +85,7 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
export function initIpc(mainWindow: BrowserWindow) {
open(QUICKCSS_PATH, "a+").then(fd => {
fd.close();
watch(QUICKCSS_PATH, debounce(async () => {
watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => {
mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss());
}, 50));
});
@ -88,14 +93,27 @@ export function initIpc(mainWindow: BrowserWindow) {
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
const win = new BrowserWindow({
title: "QuickCss Editor",
title: "Vencord QuickCSS Editor",
autoHideMenuBar: true,
darkTheme: true,
webPreferences: {
preload: join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false
nodeIntegration: false,
sandbox: false
}
});
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
});
ipcMain.on(IpcEvents.GET_PLUGINS, e => {
try {
const files = readdirSync(PLUGINS_DIR).filter(f => f.endsWith(".js"));
console.log(files);
e.returnValue = files.map(f => [f, readFileSync(join(PLUGINS_DIR, f), "utf-8")]);
} catch (err) {
console.error(err);
e.returnValue = [];
}
});

View File

@ -17,7 +17,7 @@
*/
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";
const { setAppUserModelId } = app;
@ -44,58 +44,50 @@ function isNewer($new: string, old: string) {
}
function patchLatest() {
const currentAppPath = dirname(process.execPath);
const currentVersion = basename(currentAppPath);
const discordPath = join(currentAppPath, "..");
try {
const currentAppPath = dirname(process.execPath);
const currentVersion = basename(currentAppPath);
const discordPath = join(currentAppPath, "..");
const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
return (curr.startsWith("app-") && isNewer(curr, prev))
? curr
: prev;
}, currentVersion as string);
const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
return (curr.startsWith("app-") && isNewer(curr, prev))
? curr
: prev;
}, currentVersion as string);
if (latestVersion === currentVersion) return;
if (latestVersion === currentVersion) return;
const app = join(discordPath, latestVersion, "resources", "app");
if (existsSync(app)) return;
const resources = join(discordPath, latestVersion, "resources");
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");
mkdirSync(app);
writeFileSync(join(app, "package.json"), JSON.stringify({
name: "discord",
main: "index.js"
}));
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(patcherPath)});`);
console.info("[Vencord] Detected Host Update. Repatching...");
renameSync(app, _app);
mkdirSync(app);
writeFileSync(join(app, "package.json"), JSON.stringify({
name: "discord",
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
// need to reinject
function patchUpdater() {
const main = require.main!;
const buildInfo = require(join(process.resourcesPath, "build_info.json"));
try {
if (buildInfo?.newUpdater) {
const autoStartScript = join(main.filename, "..", "autoStart", "win32.js");
const { update } = require(autoStartScript);
const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js");
const { update } = require(autoStartScript);
// New Updater Injection
require.cache[autoStartScript]!.exports.update = function () {
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);
};
}
require.cache[autoStartScript]!.exports.update = function () {
update.apply(this, arguments);
patchLatest();
};
} catch {
// OpenAsar uses electrons autoUpdater on Windows
const { quitAndInstall } = autoUpdater;

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