├── .gitignore
├── installer
├── EnhancedDiscordUI
│ ├── ed.ico
│ ├── Resources
│ │ ├── ed_og.png
│ │ ├── discord_dev_16.png
│ │ ├── discord_dev_32.png
│ │ ├── discord_dev_64.png
│ │ ├── discord_canary_16.png
│ │ ├── discord_canary_32.png
│ │ ├── discord_canary_64.png
│ │ ├── discord_stable_16.png
│ │ ├── discord_stable_32.png
│ │ └── discord_stable_64.png
│ ├── App.config
│ ├── Properties
│ │ ├── Settings.settings
│ │ ├── Settings.Designer.cs
│ │ ├── AssemblyInfo.cs
│ │ ├── Resources.Designer.cs
│ │ └── Resources.resx
│ ├── Program.cs
│ ├── LogWriter.cs
│ ├── EnhancedDiscordUI.csproj
│ ├── Form1.resx
│ ├── Form1.Designer.cs
│ └── Form1.cs
└── EnhancedDiscordUI.sln
├── plugins
├── silent_typing.js
├── style.css
├── anti_track.js
├── silence.js
├── quick_save.js
├── double_click_mention.js
├── double_click_edit.js
├── guild_count.js
├── friend_count.js
├── css_loader.js
├── avatar_links.js
├── direct_download.js
├── css_settings.js
├── hidden_channels.js
└── ed_settings.js
├── .github
└── workflows
│ └── main.yml
├── .eslintrc.json
├── LICENSE
├── main_process_shit.js
├── README.md
├── injection.js
├── plugin.js
├── bd.css
├── bd_shit.js
└── dom_shit.js
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.vs
2 | **/bin
3 | **/obj
4 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/ed.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/ed.ico
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/ed_og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/ed_og.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/discord_dev_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/discord_dev_16.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/discord_dev_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/discord_dev_32.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/discord_dev_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/discord_dev_64.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/discord_canary_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/discord_canary_16.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/discord_canary_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/discord_canary_32.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/discord_canary_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/discord_canary_64.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/discord_stable_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/discord_stable_16.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/discord_stable_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/discord_stable_32.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Resources/discord_stable_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joe27g/EnhancedDiscord/HEAD/installer/EnhancedDiscordUI/Resources/discord_stable_64.png
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/plugins/silent_typing.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 |
3 | module.exports = new Plugin({
4 | name: 'Silent Typing',
5 | author: 'Joe 🎸#7070',
6 | description: `Never appear as typing in any channel.`,
7 | color: 'grey',
8 | disabledByDefault: true,
9 |
10 | load: async function() {
11 | EDApi.monkeyPatch(EDApi.findModule('startTyping'), 'startTyping', () => {});
12 | },
13 | unload: function() {
14 | EDApi.findModule('startTyping').startTyping.unpatch();
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using System.Windows.Forms;
6 |
7 | namespace EnhancedDiscordUI
8 | {
9 | static class Program
10 | {
11 | ///
12 | /// The main entry point for the application.
13 | ///
14 | [STAThread]
15 | static void Main()
16 | {
17 | Application.EnableVisualStyles();
18 | Application.SetCompatibleTextRenderingDefault(false);
19 | Application.Run(new EDInstaller());
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/plugins/style.css:
--------------------------------------------------------------------------------
1 | @import url(https://enhanceddiscord.com/theme.css);
2 |
3 | .theme-dark {
4 | --bg: url(https://i.imgur.com/2WHzMbs.jpg);
5 | --bg-overlay: rgba(0, 0, 0, 0.8);
6 | --accent: #900;
7 | --accent-bright: #f00;
8 | --accent-back: rgba(255, 0, 0, 0.15);
9 | --accent-back-bright: rgba(255, 0, 0, 0.4);
10 | --icon-color: rgba(250, 166, 26, 0.5);
11 | --link-color: #faa61a;
12 | --link-color-hover: #fad61a;
13 | --popup-background: #222;
14 | --popup-highlight: #333;
15 | --unread-color: var(--accent-bright);
16 | --typing-height: 25px;
17 | --gift-button: none;
18 | --gif-picker: none;
19 | }
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Discord Source detection
2 | on: [issues]
3 | jobs:
4 | autoclose:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Autoclose Discord Source issues
8 | uses: IndyV/IssueChecker@v1.0
9 | with:
10 | repo-token: ${{ secrets.GITHUB_TOKEN }}
11 | issue-close-message: "@${issue.user.login} This issue was automatically closed because we don't accept issue reports from Discord Source.\nThe reason for this is because usually these issues aren't well thought out and are often duplicates.\n\nPlease take a few more minutes to create a well-made, proper issue report."
12 | issue-pattern: "Discord Source - .Issue Report]"
13 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | "es6": true
7 | },
8 | "parserOptions": {
9 | "ecmaVersion": 8
10 | },
11 | "rules": {
12 | "quotes": ["warn", "single", {"allowTemplateLiterals": true}],
13 | "no-console": "off",
14 | "no-unused-vars": "warn",
15 | "no-useless-escape": "warn",
16 | "no-empty": "warn",
17 | "no-var": "error",
18 | "no-mixed-spaces-and-tabs": "warn",
19 | "prefer-const": "warn"
20 | },
21 | "globals": {
22 | "ED": "readonly",
23 | "EDApi": "readonly"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/plugins/anti_track.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 |
3 | module.exports = new Plugin({
4 | name: 'Anti-Track',
5 | author: 'Joe 🎸#7070',
6 | description: `Prevent Discord from sending "tracking" data that they may be selling to advertisers or otherwise sharing.`,
7 | color: 'white',
8 |
9 | load: async function() {
10 | EDApi.monkeyPatch(EDApi.findModule('track'), 'track', () => {});
11 | EDApi.monkeyPatch(EDApi.findModule('submitLiveCrashReport'), 'submitLiveCrashReport', () => {});
12 | },
13 | unload: async function() {
14 | EDApi.findModule('track').track.unpatch();
15 | EDApi.findModule('submitLiveCrashReport').submitLiveCrashReport.unpatch();
16 | }
17 | });
18 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 14
4 | VisualStudioVersion = 14.0.25420.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnhancedDiscordUI", "EnhancedDiscordUI\EnhancedDiscordUI.csproj", "{3639AE05-14E6-43B2-9DDB-1A3F4F52657C}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {3639AE05-14E6-43B2-9DDB-1A3F4F52657C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {3639AE05-14E6-43B2-9DDB-1A3F4F52657C}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {3639AE05-14E6-43B2-9DDB-1A3F4F52657C}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {3639AE05-14E6-43B2-9DDB-1A3F4F52657C}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 joe27g
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/plugins/silence.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 |
3 | module.exports = new Plugin({
4 | name: 'Shut up, Clyde',
5 | author: 'Joe 🎸#7070',
6 | description: "Silences Clyde saying stupid shit about Nitro, for users that don't have it.",
7 | color: '#7289da',
8 |
9 | load: async function() {
10 | const gg = EDApi.findModule(m => m.getChannelId && m.getGuildId && !m.getPings), bs = EDApi.findModule('Messages').Messages;
11 |
12 | EDApi.monkeyPatch(EDApi.findModule('sendBotMessage'), 'sendBotMessage', function (b) {
13 | if (gg.getGuildId() !== null) return; // don't send Clyde messages when looking at a server
14 | const message = b.methodArguments[1];
15 | if (message == bs.INVALID_ANIMATED_EMOJI_BODY_UPGRADE || message == bs.INVALID_ANIMATED_EMOJI_BODY || message == bs.INVALID_EXTERNAL_EMOJI_BODY_UPGRADE || message == bs.INVALID_EXTERNAL_EMOJI_BODY) return;
16 | return b.callOriginalMethod(b.methodArguments);
17 | });
18 | },
19 | unload: function() {
20 | EDApi.findModule('sendBotMessage').sendBotMessage.unpatch();
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace EnhancedDiscordUI.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("EnhancedDiscordUI")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("EnhancedDiscordUI")]
13 | [assembly: AssemblyCopyright("Copyright © 2018")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("3639ae05-14e6-43b2-9ddb-1a3f4f52657c")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/plugins/quick_save.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 |
3 | module.exports = new Plugin({
4 | name: 'Quick Save',
5 | author: 'Joe 🎸#7070',
6 | description: 'Use Ctrl+S or Cmd+S to save server, channel, or account settings.',
7 | color: 'salmon',
8 |
9 | load: async function() {
10 | const hcModules = EDApi.findAllModules('hasChanges');
11 | this._listener = function(e) {
12 | if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83) {
13 | e.preventDefault();
14 | const types = ['GUILD', 'CHANNEL', 'ACCOUNT', 'GUILD ROLES', 'CHANNEL OVERWRITES'];
15 | let hasChanges = false;
16 | for (const i in types) {
17 | if (hcModules[i] && hcModules[i].hasChanges()) {
18 | hasChanges = true;
19 | //module.exports.log(`saving ${types[i]} settings`);
20 | break;
21 | }
22 | }
23 | if (!hasChanges) {
24 | //module.exports.log('No setting changes detected. Aborting.');
25 | return;
26 | }
27 | const saveButton = document.querySelector('[class*="lookFilled-"][class*="colorGreen-"]');
28 | if (saveButton)
29 | try { saveButton.click(); } catch(err) { module.exports.error(err); }
30 | return;
31 | }
32 | }
33 | document.addEventListener("keydown", this._listener, false);
34 | },
35 | unload: function() {
36 | document.removeEventListener("keydown", this._listener);
37 | delete this._listener;
38 | }
39 | });
40 |
--------------------------------------------------------------------------------
/plugins/double_click_mention.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 | let userM = {}, taM = {}, avM = {}, wM = {}, ree;
3 |
4 | module.exports = new Plugin({
5 | name: 'Double-Click Mention',
6 | author: 'Joe 🎸#7070',
7 | description: 'Allows you to double-click a user\'s name to mention them.',
8 | color: '#00bbff',
9 |
10 | _userTag: '',
11 | load: async function() {
12 | taM = EDApi.findModule('textArea');
13 | userM = EDApi.findModule('username');
14 | avM = EDApi.findModule('avatar');
15 | wM = EDApi.findModule(m => m.wrapper && m.avatar);
16 | ree = this;
17 |
18 | document.addEventListener("dblclick", this.doubleListener);
19 | },
20 | unload: async function() {
21 | document.removeEventListener("dblclick", this.doubleListener);
22 | },
23 |
24 | doubleListener: function(e) {
25 | if (!e || !e.target || !e.target.parentElement) return;
26 | let tag;
27 | try {
28 | if (e.target.className === userM.username)
29 | tag = e.target.parentElement.__reactInternalInstance$.return.return.memoizedProps.message.author.tag;
30 | else if (e.target.className === wM.wrapper && e.target.parentElement.className === avM.avatar)
31 | tag = e.target.parentElement.__reactInternalInstance$.return.return.memoizedProps.user.tag;
32 | } catch(err) {
33 | ree.error(err);
34 | tag = null;
35 | }
36 | if (!tag) return;
37 |
38 | const ta = document.querySelector('.'+taM.textArea);
39 | if (!ta) return;
40 | ta.value = `${ta.value ? ta.value.endsWith(' ') ? ta.value : ta.value+' ' : ''}@${tag} `;
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/main_process_shit.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron');
2 | const ipcMain = require('electron').ipcMain;
3 | const path = require('path');
4 |
5 | ipcMain.on('main-process-info', (event, arg) => {
6 | switch(arg) {
7 | case "original-node-modules-path":
8 | event.returnValue = path.resolve(electron.app.getAppPath(), 'node_modules');
9 | case "original-preload-script":
10 | event.returnValue = event.sender.__preload;
11 | }
12 | });
13 |
14 | ipcMain.on('main-process-utils', (event, arg) => {
15 | switch(arg) {
16 | case "dialog":
17 | event.returnValue = `${electron.dialog}`;
18 | }
19 | });
20 |
21 | ipcMain.handle('custom-devtools-warning', (event, arg) => {
22 | let wc = event.sender.getOwnerBrowserWindow().webContents;
23 | wc.removeAllListeners('devtools-opened');
24 | wc.on('devtools-opened', () => {
25 | wc.executeJavaScript(`
26 | console.log('%cHold Up!', 'color: #FF5200; -webkit-text-stroke: 2px black; font-size: 72px; font-weight: bold;');
27 | console.log("%cIf you're reading this, you're probably smarter than most Discord developers.", 'font-size: 16px;');
28 | console.log('%cPasting anything in here could actually improve the Discord client.', 'font-size: 18px; font-weight: bold; color: red;');
29 | console.log("%cUnless you understand exactly what you're doing, keep this window open to browse our bad code.", 'font-size: 16px;');
30 | console.log("%cIf you don't understand exactly what you\'re doing, you should come work with us: https://discordapp.com/jobs", 'font-size: 16px;');
31 | `)
32 | });
33 | });
34 |
35 | ipcMain.handle('bd-navigate-page-listener', (event, arg) => {
36 | event.sender.getOwnerBrowserWindow().webContents.on('did-navigate-in-page', arg);
37 | })
38 |
39 | ipcMain.handle('remove-bd-navigate-page-listener', (event, arg) => {
40 | event.sender.getOwnerBrowserWindow().webContents.removeEventListener('did-navigate-in-page', arg);
41 | })
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## END OF SUPPORT
2 | __EnhancedDiscord is being shut down.__ It's been nice while it lasted, but we can't keep up with the maintenance that Discord's constant changes require. There won't be any more updates, and on **April 12th**, all support will be dropped and the site will go offline. We recommend you find another client mod that is actively maintained.
3 |
4 |
5 | # EnhancedDiscord
6 | A lightweight client mod designed to enhance your Discord experience without slowing down your PC.
7 |
8 | #### DISCLAIMER!
9 | > **Using EnhancedDiscord, or any other client mod, is against [Discord's Terms of Service](https://discordapp.com/terms). Use it at your own risk.**
10 | > *It's very unlikely any action will be taken against you, but we take no responsibility if anything happens.*
11 |
12 | ### Installing
13 |
14 | Go to https://enhanceddiscord.com and hit the 'Download' button, or if you're lazy, click [here](https://enhanceddiscord.com/EnhancedDiscord.exe).
15 |
16 | For more info, including installing on Linux or MacOS, see the [installation wiki page](https://github.com/joe27g/EnhancedDiscord/wiki/Installation).
17 |
18 | ### Themes
19 |
20 | By default, the official [EnhancedDiscord theme](https://github.com/joe27g/Discord-Theme) is loaded along with a plugin that allows you to change settings of it in **User Settings** > EnhancedDiscord > **Settings**. For more info on how to change/edit your theme, see the [FAQ](https://github.com/joe27g/EnhancedDiscord/wiki/FAQ).
21 |
22 | ### Plugins
23 |
24 | A list of included plugins and their purpose is included on the [plugins wiki page](https://github.com/joe27g/EnhancedDiscord/wiki/Plugins). It also includes some sources for finding new ED plugins.
25 |
26 | ### Custom plugins
27 |
28 | For info about how to create your own plugins, check out the [custom plugins wiki page](https://github.com/joe27g/EnhancedDiscord/wiki/Custom-plugins).
29 |
30 | ### Having issues?
31 |
32 | First, check out the [FAQ](https://github.com/joe27g/EnhancedDiscord/wiki/FAQ) to see if your issue is listed there. ~~If not, ask in #support in the support server (link below.)~~
--------------------------------------------------------------------------------
/plugins/double_click_edit.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 |
3 | module.exports = new Plugin({
4 | name: 'Double-Click Edit',
5 | author: 'Joe 🎸#7070',
6 | description: 'Allows you to double-click a message to edit or hold delete + click to delete.',
7 | color: '#ff5900',
8 |
9 | deletePressed: false,
10 | load: function() {
11 | this._mm = findRawModule(m => m.displayName == "Message");
12 | this._edm = EDApi.findModule('startEditMessage');
13 | monkeyPatch(this._mm.exports, 'default', {
14 | silent: true,
15 | before: e => e.methodArguments[0].onClick = () => this.handleClick(e.methodArguments[0])
16 | });
17 |
18 | document.addEventListener("keydown", this.keyDownListener);
19 | document.addEventListener("keyup", this.keyUpListener);
20 | },
21 | unload: function() {
22 | if (this._mm.exports.default)
23 | this._mm.exports.default.unpatch();
24 | document.removeEventListener("keydown", this.keyDownListener);
25 | document.removeEventListener("keyup", this.keyUpListener);
26 | },
27 |
28 | handleDoubleClick: function(message) {
29 | const msgObj = message.childrenMessageContent.props.message;
30 | return this._edm.startEditMessage(msgObj.channel_id, msgObj.id, msgObj.content || '');
31 | },
32 |
33 | handleClick: function(message) {
34 | if (Date.now() - message._lastClick < 250)
35 | return this.handleDoubleClick(message);
36 | message._lastClick = Date.now();
37 | console.log(this.deletePressed);
38 | if (!this.deletePressed) return;
39 | const msgObj = message.childrenMessageContent.props.message;
40 | return this._edm.deleteMessage(msgObj.channel_id, msgObj.id);
41 | },
42 |
43 | keyUpListener: e => {
44 | if (e.keyCode == 46)
45 | module.exports.deletePressed = false;
46 | },
47 | keyDownListener: e => {
48 | if (e.keyCode == 46)
49 | module.exports.deletePressed = true;
50 | }
51 | });
52 |
--------------------------------------------------------------------------------
/injection.js:
--------------------------------------------------------------------------------
1 | require('./main_process_shit');
2 | const electron = require('electron');
3 | const path = require('path');
4 | electron.app.commandLine.appendSwitch("no-force-async-hooks-checks");
5 |
6 | electron.session.defaultSession.webRequest.onHeadersReceived(function(details, callback) {
7 | if (!details.responseHeaders['content-security-policy-report-only'] && !details.responseHeaders['content-security-policy']) return callback({cancel: false});
8 | delete details.responseHeaders['content-security-policy-report-only'];
9 | delete details.responseHeaders['content-security-policy'];
10 | callback({cancel: false, responseHeaders: details.responseHeaders});
11 | });
12 |
13 | class BrowserWindow extends electron.BrowserWindow {
14 | constructor(originalOptions) {
15 | let win = new electron.BrowserWindow(originalOptions);
16 | if (!originalOptions || !originalOptions.webPreferences || !originalOptions.title) return win; // eslint-disable-line constructor-super
17 | const originalPreloadScript = originalOptions.webPreferences.preload;
18 |
19 | originalOptions.webPreferences.preload = path.join(process.env.injDir, 'dom_shit.js');
20 | originalOptions.webPreferences.transparency = true;
21 |
22 | win = new electron.BrowserWindow(originalOptions);
23 | win.webContents.__preload = originalPreloadScript;
24 | return win;
25 | }
26 | }
27 |
28 | BrowserWindow.webContents;
29 |
30 | const electron_path = require.resolve('electron');
31 | Object.assign(BrowserWindow, electron.BrowserWindow); // Assigns the new chrome-specific ones
32 |
33 | if (electron.deprecate && electron.deprecate.promisify) {
34 | const originalDeprecate = electron.deprecate.promisify; // Grab original deprecate promisify
35 | electron.deprecate.promisify = (originalFunction) => originalFunction ? originalDeprecate(originalFunction) : () => void 0; // Override with falsey check
36 | }
37 |
38 | const newElectron = Object.assign({}, electron, {BrowserWindow});
39 | // Tempfix for Injection breakage due to new version of Electron on Canary (Electron 7.x)
40 | // Found by Zerebos (Zack Rauen)
41 | delete require.cache[electron_path].exports;
42 | // /TempFix
43 | require.cache[electron_path].exports = newElectron;
44 | //const browser_window_path = require.resolve(path.resolve(electron_path, '..', '..', 'browser-window.js'));
45 | //require.cache[browser_window_path].exports = BrowserWindow;
46 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/LogWriter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 |
5 | namespace EnhancedDiscordUI
6 | {
7 | static public class Logger
8 | {
9 | static public void Log(string logMessage)
10 | {
11 | string m_exePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
12 | try
13 | {
14 | using (StreamWriter w = File.AppendText(m_exePath + "\\" + "log.txt"))
15 | {
16 | _Log("INFO", logMessage, w);
17 | }
18 | }
19 | catch (Exception ex)
20 | {
21 | }
22 | }
23 |
24 | static public void Warn(string logMessage)
25 | {
26 | string m_exePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
27 | try
28 | {
29 | using (StreamWriter w = File.AppendText(m_exePath + "\\" + "log.txt"))
30 | {
31 | _Log("WARN", logMessage, w);
32 | }
33 | }
34 | catch (Exception ex)
35 | {
36 | }
37 | }
38 |
39 | static public void Error(string logMessage)
40 | {
41 | string m_exePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
42 | try
43 | {
44 | using (StreamWriter w = File.AppendText(m_exePath + "\\" + "log.txt"))
45 | {
46 | _Log("ERROR", logMessage, w);
47 | }
48 | }
49 | catch (Exception ex)
50 | {
51 | }
52 | }
53 |
54 | static public void _Log(string type, string logMessage, TextWriter txtWriter)
55 | {
56 | try
57 | {
58 | txtWriter.WriteLine("[{0}][{1} {2}]: {3}", type, DateTime.Now.ToLongTimeString(), DateTime.Now.ToLongDateString(), logMessage);
59 | }
60 | catch (Exception ex)
61 | {
62 | }
63 | }
64 |
65 | static public void MakeDivider()
66 | {
67 | string m_exePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
68 | try
69 | {
70 | using (StreamWriter w = File.AppendText(m_exePath + "\\" + "log.txt"))
71 | {
72 | w.WriteLine("---------------------------------------------------------------------");
73 | }
74 |
75 | }
76 | catch (Exception ex)
77 | {
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/plugins/guild_count.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 |
3 | let sep = {}, ms = {}, kb = {}, gg, sub;
4 |
5 | module.exports = new Plugin({
6 | name: 'Server Count',
7 | author: 'Joe 🎸#7070',
8 | description: "Adds the number of servers you're currently in right above the list.",
9 | color: 'indigo',
10 |
11 | load: async function() {
12 | sep = EDApi.findModule('guildSeparator');
13 | ms = EDApi.findModule('modeSelectable');
14 | kb = EDApi.findModule('keybind');
15 | gg = EDApi.findModule('getGuilds');
16 | sub = EDApi.findModule('subscribe');
17 |
18 | sub.subscribe('CONNECTION_OPEN', this.refreshCount);
19 | sub.subscribe('CONNECTION_RESUMED', this.refreshCount);
20 | sub.subscribe('DISPATCH_APPLICATION_STATE_UPDATE', this.refreshCount);
21 | sub.subscribe('CHANNEL_PRELOAD', this.refreshCount);
22 | sub.subscribe('GUILD_CREATE', this.refreshCount);
23 | sub.subscribe('GUILD_DELETE', this.refreshCount);
24 | sub.subscribe('GUILD_JOIN', this.refreshCount);
25 | this.refreshCount();
26 | },
27 | refreshCount: function() {
28 | if (!sep) return;
29 | const num = Object.keys(gg.getGuilds()).length;
30 |
31 | let guildCount = document.getElementById('ed_guild_count');
32 | if (guildCount) {
33 | if (num === this._num) return; // don't update if # is the same as before
34 | guildCount.innerHTML = num + ' Servers';
35 | this._num = num;
36 | return;
37 | }
38 | const separator = document.querySelector(`.${sep.guildSeparator}`);
39 | if (separator) {
40 | guildCount = document.createElement('div');
41 | guildCount.className = `${ms ? ms.description+' ' : ''}${sep.listItem} ${kb.keybind}`;
42 | guildCount.innerHTML = num + ' Servers';
43 | guildCount.id = 'ed_guild_count';
44 | try {
45 | separator.parentElement.parentElement.insertBefore(guildCount, separator.parentElement)
46 | this._num = num;
47 | } catch(err) {
48 | this.error(err);
49 | }
50 | }
51 | return;
52 | },
53 | unload: function() {
54 | const guildCount = document.getElementById('ed_guild_count');
55 | if (guildCount) guildCount.remove();
56 |
57 | sub.unsubscribe('CONNECTION_OPEN', this.refreshCount);
58 | sub.unsubscribe('GUILD_CREATE', this.refreshCount);
59 | sub.unsubscribe('GUILD_DELETE', this.refreshCount);
60 | sub.unsubscribe('GUILD_JOIN', this.refreshCount);
61 | }
62 | });
63 |
--------------------------------------------------------------------------------
/plugins/friend_count.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 | let sep = {}, ms = {}, kb = {}, sub;
3 |
4 | module.exports = new Plugin({
5 | name: 'Friend Count',
6 | author: 'Joe 🎸#7070',
7 | description: "Adds the number of friends/online friends under the \"Home\" button in the top left.",
8 | color: 'cornflowerblue',
9 |
10 | defaultSettings: {onlineOnly: false},
11 | onSettingsUpdate: function() { return this.reload(); },
12 |
13 | addFriendCount: function() {
14 | if (!sep) return;
15 | const o = (this.settings || {}).onlineOnly;
16 | const num = o ? EDApi.findModule("getOnlineFriendCount").getOnlineFriendCount() : EDApi.findModule("getFriendIDs").getFriendIDs().length;
17 |
18 | let friendCount = document.getElementById('ed_friend_count');
19 | if (friendCount) {
20 | if (num === this._num) return; // don't update if # is the same as before
21 | friendCount.innerHTML = num + (o ? ' Online' : ' Friends');
22 | this._num = num;
23 | return;
24 | }
25 | const separator = document.querySelector(`.${sep.guildSeparator}`);
26 | if (separator) {
27 | friendCount = document.createElement('div');
28 | friendCount.className = `${ms ? ms.description+' ' : ''}${sep.listItem} ${kb.keybind}`;
29 | friendCount.innerHTML = num + (o ? ' Online' : ' Friends');
30 | friendCount.id = 'ed_friend_count';
31 | try {
32 | separator.parentElement.parentElement.insertBefore(friendCount, separator.parentElement)
33 | this._num = num;
34 | } catch(err) {
35 | this.error(err);
36 | }
37 | }
38 | },
39 |
40 | load: async function() {
41 | sep = EDApi.findModule('guildSeparator');
42 | ms = EDApi.findModule('modeSelectable');
43 | kb = EDApi.findModule('keybind');
44 | sub = EDApi.findModule('subscribe');
45 |
46 | sub.subscribe('CONNECTION_OPEN', this.addFriendCount);
47 | sub.subscribe('CONNECTION_RESUMED', this.addFriendCount);
48 | sub.subscribe('DISPATCH_APPLICATION_STATE_UPDATE', this.addFriendCount);
49 | sub.subscribe('PRESENCE_UPDATE', this.addFriendCount);
50 | sub.subscribe('RELATIONSHIP_ADD', this.addFriendCount);
51 | sub.subscribe('RELATIONSHIP_REMOVE', this.addFriendCount);
52 |
53 | this.addFriendCount();
54 | },
55 | unload: function() {
56 | const friendCount = document.getElementById('ed_friend_count');
57 | if (friendCount) friendCount.remove();
58 |
59 | sub.unsubscribe('CONNECTION_OPEN', this.addFriendCount);
60 | sub.unsubscribe('CONNECTION_RESUMED', this.addFriendCount);
61 | sub.unsubscribe('PRESENCE_UPDATE', this.addFriendCount);
62 | sub.unsubscribe('RELATIONSHIP_ADD', this.addFriendCount);
63 | sub.unsubscribe('RELATIONSHIP_REMOVE', this.addFriendCount);
64 | },
65 | generateSettings: () => ([{
66 | type: "input:boolean",
67 | configName: "onlineOnly",
68 | title: "Online Only",
69 | note: "Only show the number of friends online rather than all friends.",
70 | }])
71 | });
72 |
--------------------------------------------------------------------------------
/plugins/css_loader.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 | const path = require('path');
3 | const fs = require('fs');
4 | const readFile = require('util').promisify(fs.readFile);
5 |
6 | module.exports = new Plugin({
7 | name: 'CSS Loader',
8 | author: 'Joe 🎸#7070',
9 | description: 'Loads and hot-reloads CSS.',
10 | preload: true, //load this before Discord has finished starting up
11 | color: 'blue',
12 |
13 | defaultSettings: {path: './plugins/style.css'},
14 | resetSettings: function(toastText) {
15 | EDApi.showToast(toastText);
16 | this.settings = this.defaultSettings;
17 | },
18 | onSettingsUpdate: function() {
19 | const filePath = (this.settings || {}).path;
20 | if (!filePath) {
21 | return this.resetSettings('Empty path. Settings have been reset.');
22 | }
23 | if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
24 | return this.resetSettings('Invalid file path. Must be a local CSS file (stored on your computer, not a URL.)');
25 | }
26 | if (!filePath.endsWith('.css')) {
27 | return this.resetSettings('Invalid file path. Must be a CSS file.');
28 | }
29 | if (path.isAbsolute(filePath)) {
30 | if (!fs.existsSync(filePath)) {
31 | return this.resetSettings('Invalid file path. File does not exist.');
32 | }
33 | } else {
34 | const p = path.join(process.env.injDir, filePath);
35 | if (!fs.existsSync(p)) {
36 | return this.resetSettings('Invalid file path. File does not exist.');
37 | }
38 | }
39 | return this.reload();
40 | },
41 |
42 | load: async function() {
43 | const filePath = (this.settings || {}).path;
44 | if (!filePath) return;
45 | const cssPath = path.isAbsolute(filePath) ? filePath : path.join(process.env.injDir, filePath);
46 |
47 | readFile(cssPath).then(css => {
48 | if (!ED.customCss) {
49 | ED.customCss = document.createElement('style');
50 | document.head.appendChild(ED.customCss);
51 | }
52 | ED.customCss.innerHTML = css;
53 | this.info('Custom CSS loaded!', ED.customCss);
54 |
55 | if (ED.cssWatcher == null) {
56 | ED.cssWatcher = fs.watch(cssPath, { encoding: 'utf-8' },
57 | eventType => {
58 | if (eventType == 'change') {
59 | readFile(cssPath).then(newCss => ED.customCss.innerHTML = newCss);
60 | }
61 | });
62 | }
63 | }).catch(() => console.info('Custom CSS not found. Skipping...'));
64 | },
65 | unload: function() {
66 | if (ED.customCss) {
67 | document.head.removeChild(ED.customCss);
68 | ED.customCss = null;
69 | }
70 | if (ED.cssWatcher) {
71 | ED.cssWatcher.close();
72 | ED.cssWatcher = null;
73 | }
74 | },
75 | generateSettings: function() { return [{
76 | type: "input:text",
77 | configName: "path",
78 | title: "Custom CSS Path",
79 | desc: "This can be relative to the EnhancedDiscord directory (e.g. `./big_gay.css`) or absolute (e.g. `C:/theme.css`.)",
80 | placeholder: this.defaultSettings.path
81 | }]}
82 | });
83 |
--------------------------------------------------------------------------------
/plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Plugin Class
3 | */
4 | class Plugin {
5 | /**
6 | * Create your plugin, must have a name and load() function
7 | * @constructor
8 | * @param {object} options - Plugin options
9 | */
10 | constructor (opts = {}) {
11 | if (!opts.name || typeof opts.load !== 'function')
12 | return 'Invalid plugin. Needs a name and a load() function.';
13 |
14 | Object.assign(this, opts);
15 | if (!this.color)
16 | this.color = 'orange';
17 | if (!this.author)
18 | this.author = '';
19 | }
20 |
21 | /**
22 | * Load this plugin.
23 | */
24 | load () {}
25 |
26 | /**
27 | * Unload this plugin.
28 | */
29 | unload () {}
30 |
31 | /**
32 | * Reload this plugin.
33 | */
34 | reload () {
35 | this.log('Reloading...');
36 | this.unload();
37 | delete require.cache[require.resolve(`./plugins/${this.id}`)];
38 | const newPlugin = require(`./plugins/${this.id}`);
39 | ED.plugins[this.id] = newPlugin;
40 | newPlugin.id = this.id;
41 | return newPlugin.load();
42 | }
43 |
44 | /**
45 | * Send a decorated console.log prefixed with ED and your plugin name
46 | * @param {...string} msg - Message to be logged
47 | */
48 | log (...msg) {
49 | console.log(`%c[EnhancedDiscord] %c[${this.name}]`, 'color: red;', `color: ${this.color}`, ...msg);
50 | }
51 |
52 | /**
53 | * Send a decorated console.info prefixed with ED and your plugin name
54 | * @param {...string} msg - Message to be logged
55 | */
56 | info (...msg) {
57 | console.info(`%c[EnhancedDiscord] %c[${this.name}]`, 'color: red;', `color: ${this.color}`, ...msg);
58 | }
59 |
60 | /**
61 | * Send a decorated console.warn prefixed with ED and your plugin name
62 | * @param {...string} msg - Message to be logged
63 | */
64 | warn (...msg) {
65 | console.warn(`%c[EnhancedDiscord] %c[${this.name}]`, 'color: red;', `color: ${this.color}`, ...msg);
66 | }
67 |
68 | /**
69 | * Send a decorated console.error prefixed with ED and your plugin name
70 | * @param {...string} msg - Message to be logged
71 | */
72 | error (...msg) {
73 | console.error(`%c[EnhancedDiscord] %c[${this.name}]`, 'color: red;', `color: ${this.color}`, ...msg);
74 | }
75 |
76 | /**
77 | * Returns a Promise that resolves after ms milliseconds.
78 | * @param {number} ms - How long to wait before resolving the promise
79 | */
80 | sleep (ms) {
81 | return new Promise(resolve => {
82 | setTimeout(resolve, ms);
83 | });
84 | }
85 |
86 | /**
87 | * Get plugin settings.
88 | * @returns {Object} Plugin settings object
89 | */
90 | get settings() {
91 | return EDApi.loadPluginSettings(this.id);
92 | }
93 |
94 | /**
95 | * Get particular plugin setting.
96 | * @param {string} key
97 | * @returns Plugin setting value
98 | */
99 | getSetting(key) {
100 | return EDApi.loadData(this.id, key);
101 | }
102 |
103 | /**
104 | * Save plugin settings.
105 | * @param {Object} newSets - New plugin settings object
106 | */
107 | set settings(newSets = {}) {
108 | return EDApi.savePluginSettings(this.id, newSets);
109 | }
110 |
111 | /**
112 | * Set particular plugin setting.
113 | * @param {string} key
114 | * @param data
115 | */
116 | setSetting(key, data) {
117 | return EDApi.saveData(this.id, key, data);
118 | }
119 | }
120 |
121 | module.exports = Plugin;
122 |
--------------------------------------------------------------------------------
/plugins/avatar_links.js:
--------------------------------------------------------------------------------
1 | const Plugin = require("../plugin");
2 | const Clipboard = require("electron").clipboard;
3 |
4 | let cm = {}, Dispatcher, ImageResolver, ContextMenuActions, ree;
5 |
6 | module.exports = new Plugin({
7 | name: "Avatar Links",
8 | author: "Joe 🎸#7070",
9 | description: "Lets you copy a user or guild's avatar URL by right-clicking on it.",
10 | color: "#ffe000",
11 |
12 | load: async function() {
13 | ree = this;
14 | cm = EDApi.findModule('menu');
15 | Dispatcher = EDApi.findModule("dispatch");
16 | ImageResolver = EDApi.findModule("getUserAvatarURL");
17 | ContextMenuActions = EDApi.findModule("closeContextMenu");
18 |
19 | Dispatcher.subscribe("CONTEXT_MENU_OPEN", this.checkMenu);
20 | },
21 |
22 | unload: function() {
23 | Dispatcher.unsubscribe("CONTEXT_MENU_OPEN", this.checkMenu);
24 | },
25 |
26 | checkMenu: async function() {
27 | // Make sure it's already in the DOM
28 | await new Promise(r => {setTimeout(r, 5)});
29 | const theMenu = document.querySelector('.'+cm.menu);
30 |
31 | const reactData = theMenu[Object.keys(theMenu).find(key => key.startsWith("__reactInternalInstance") || key.startsWith("__reactFiber"))];
32 |
33 | let label = "";
34 | let url = "";
35 | let props = {onHeightUpdate: () => {}};
36 |
37 | // For users
38 | if (
39 | reactData.return &&
40 | reactData.return.return &&
41 | reactData.return.return.return &&
42 | reactData.return.return.return.return &&
43 | reactData.return.return.return.return.return &&
44 | reactData.return.return.return.return.return.memoizedProps &&
45 | reactData.return.return.return.return.return.memoizedProps.user
46 | ) {
47 | props = reactData.return.return.return.return.return.memoizedProps;
48 | label = "Copy Avatar URL";
49 | const user = props.user;
50 | const imageType = ImageResolver.hasAnimatedAvatar(user) ? "gif" : "png";
51 |
52 | // Internal module maxes at 1024 hardcoded, so do that and change to 4096.
53 | url = ImageResolver.getUserAvatarURL(user, imageType, 1024).replace("size=1024", "size=4096");
54 | // For default avatars
55 | if (!url.startsWith("http") && url.startsWith("/assets"))
56 | url = `https://discordapp.com${url}`;
57 | }
58 |
59 | // For guilds
60 | if (
61 | reactData.return &&
62 | reactData.return.return &&
63 | reactData.return.return.memoizedProps &&
64 | reactData.return.return.memoizedProps.guild &&
65 | !reactData.return.return.memoizedProps.channel
66 |
67 | ) {
68 | props = reactData.return.return.memoizedProps;
69 | label = "Copy Icon URL";
70 | const guild = props.guild;
71 |
72 | // Internal module maxes at 1024 hardcoded, so do that and change to 4096.
73 | url = ImageResolver.getGuildIconURL({id: guild.id, icon: guild.icon, size: 1024}).replace("size=1024", "size=4096");
74 |
75 | // No way to make it return the animated version, do it manually
76 | if (ImageResolver.hasAnimatedGuildIcon(guild))
77 | url = url.replace(".webp?", ".gif?");
78 | else
79 | url = url.replace(".webp?", ".png?");
80 | }
81 |
82 | // Assume it is already in the DOM and add item ASAP
83 | if (label && url)
84 | ree.addMenuItem(url, label, props);
85 | },
86 |
87 | addMenuItem: function(imageURL, text, menu) {
88 | const cmGroups = document.getElementsByClassName(cm.scroller);
89 | if (!cmGroups || cmGroups.length == 0) return;
90 |
91 | const newCmItem = document.createElement("div");
92 | newCmItem.className = cm.item+' '+cm.labelContainer+' '+cm.colorDefault;
93 | const newCmItemContent = document.createElement("div");
94 | newCmItemContent.className = cm.label;
95 | newCmItemContent.innerHTML = text;
96 | newCmItem.appendChild(newCmItemContent);
97 | const lastGroup = cmGroups[cmGroups.length-1];
98 | lastGroup.appendChild(newCmItem);
99 |
100 | menu.onHeightUpdate();
101 |
102 | newCmItem.onclick = () => {
103 | Clipboard.write({text: imageURL});
104 | ContextMenuActions.closeContextMenu();
105 | }
106 | }
107 | });
108 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/EnhancedDiscordUI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {3639AE05-14E6-43B2-9DDB-1A3F4F52657C}
8 | WinExe
9 | Properties
10 | EnhancedDiscordUI
11 | EnhancedDiscordUI
12 | v4.7.1
13 | 512
14 | true
15 |
16 |
17 |
18 |
19 |
20 | AnyCPU
21 | true
22 | full
23 | false
24 | bin\Debug\
25 | DEBUG;TRACE
26 | prompt
27 | 4
28 |
29 |
30 | AnyCPU
31 | pdbonly
32 | true
33 | bin\Release\
34 | TRACE
35 | prompt
36 | 4
37 |
38 |
39 | ed.ico
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Form
59 |
60 |
61 | Form1.cs
62 |
63 |
64 |
65 |
66 |
67 | Form1.cs
68 |
69 |
70 | ResXFileCodeGenerator
71 | Designer
72 | Resources.Designer.cs
73 |
74 |
75 | SettingsSingleFileGenerator
76 | Settings.Designer.cs
77 |
78 |
79 | True
80 | True
81 | Resources.resx
82 |
83 |
84 | True
85 | Settings.settings
86 | True
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
129 |
130 |
131 |
138 |
--------------------------------------------------------------------------------
/plugins/direct_download.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 |
3 | // contains modified code from https://stackoverflow.com/a/47820271
4 | const ipcRenderer = require('electron').ipcRenderer;
5 | const dialog = ipcRenderer.sendSync('main-process-utils', 'dialog');
6 | const http = require('https');
7 | const fs = require('fs');
8 | let ttM = {}, iteM = {};
9 |
10 | function saveAs(url, filename, fileExtension) {
11 | dialog.showSaveDialog({ defaultPath: filename, title: 'Where would you like to store the stolen memes?', buttonLabel: 'Steal this meme', filters: [{ name: 'Stolen meme', extensions: [fileExtension] }] })
12 | .then(e => {
13 | if (!e.canceled) {
14 | download(url, e.filePath, () => {
15 | const wrap = document.createElement('div');
16 | wrap.className = 'theme-dark';
17 | const gay = document.createElement('div');
18 | gay.style = 'position: fixed; bottom: 10%; left: calc(50% - 88px);'
19 | gay.className = `${ttM.tooltip} ${ttM.tooltipTop} ${ttM.tooltipBlack}`;
20 | gay.innerHTML = 'Successfully downloaded | ' + e.filePath;
21 | document.body.appendChild(wrap);
22 | wrap.appendChild(gay);
23 | setTimeout(() => wrap.remove(), 2000);
24 | });
25 | }
26 | });
27 | }
28 | function download (url, dest, cb) {
29 | const file = fs.createWriteStream(dest);
30 | http.get(url, function(response) {
31 | response.pipe(file);
32 | file.on('finish', function() {
33 | file.close(cb);
34 | });
35 | }).on('error', function(err) {
36 | fs.unlink(dest);
37 | if (cb) cb(err.message);
38 | });
39 | }
40 |
41 | function addMenuItem(url, text, filename = true, fileExtension) {
42 | const cmGroups = document.querySelectorAll(`.${iteM.menu} [role='group']`);
43 | if (!cmGroups || cmGroups.length == 0) return;
44 |
45 | const newCmItem = document.createElement('div');
46 | newCmItem.className = `${iteM.item} ${iteM.labelContainer} ${iteM.colorDefault}`;
47 |
48 | // Discord uses JS to add classes, not sure how to recreate
49 | newCmItem.onmouseenter = function() {
50 | this.classList.add(iteM.focused);
51 | };
52 | newCmItem.onmouseleave = function() {
53 | this.classList.remove(iteM.focused);
54 | };
55 |
56 | const newCmItemLabel = document.createElement('div');
57 | newCmItemLabel.className = iteM.label;
58 | newCmItemLabel.innerText = text;
59 |
60 | newCmItem.appendChild(newCmItemLabel);
61 |
62 | const lastGroup = cmGroups[cmGroups.length-1];
63 | lastGroup.appendChild(newCmItem);
64 | newCmItem.onclick = () => saveAs(url, filename, fileExtension);
65 | }
66 |
67 | // contains code modified from https://github.com/Metalloriff/BetterDiscordPlugins/blob/master/SaveTo.plugin.js
68 |
69 | module.exports = new Plugin({
70 | name: 'Direct Download',
71 | author: 'Joe 🎸#7070',
72 | description: `Download files Steal memes without opening a browser.`,
73 | color: '#18770e',
74 |
75 | load: async function() {
76 | this._cmClass = EDApi.findModule('hideInteraction').menu;
77 | this._contClass = EDApi.findModule('embedWrapper').container;
78 | ttM = EDApi.findModule('tooltipPointer');
79 | iteM = EDApi.findModule('hideInteraction');
80 | Dispatcher = EDApi.findModule("dispatch");
81 | Dispatcher.subscribe("CONTEXT_MENU_OPEN", this.listener);
82 | },
83 | listener(e) {
84 | if (document.getElementsByClassName(this._cmClass).length == 0) setTimeout(() => module.exports.onContextMenu(e), 0);
85 | else this.onContextMenu(e);
86 | },
87 | onContextMenu(e) {
88 | e = e.contextMenu;
89 | const messageGroup = e.target.closest('.'+this._contClass);
90 | const parentElem = e.target.parentElement;
91 | const guildWrapper = EDApi.findModule('childWrapper').wrapper;
92 | const memberAvatar = EDApi.findModule('nameAndDecorators').avatar;
93 |
94 | if (e.target.localName != 'a' && e.target.localName != 'img' && e.target.localName != 'video' && !messageGroup && !e.target.className.includes(guildWrapper) && !parentElem.className.includes(memberAvatar) && !e.target.className.includes('avatar-')) return;
95 | let saveLabel = 'Download',
96 | url = e.target.poster || e.target.style.backgroundImage.substring(e.target.style.backgroundImage.indexOf(`'`) + 1, e.target.style.backgroundImage.lastIndexOf(`'`)) || e.target.href || e.target.src;
97 |
98 | if (e.target.className.includes(guildWrapper)) {
99 | saveLabel = 'Download Icon';
100 | if (e.target.firstChild.src) { // Make sure guild box has an icon
101 | url = e.target.firstChild.src;
102 | }
103 | }
104 | else if (e.target.className.includes('avatar-') || (parentElem.nodeName == 'DIV' && parentElem.className.includes(memberAvatar))) {
105 | saveLabel = 'Download Avatar';
106 |
107 | if (parentElem.className.includes(memberAvatar)) {
108 | url = e.target.firstChild.firstChild.firstChild.src;
109 | }
110 | }
111 |
112 | if (!url || e.target.classList.contains('emote') || url.includes('youtube.com/watch?v=') || url.includes('youtu.be/') || url.lastIndexOf('/') > url.lastIndexOf('.')) return;
113 |
114 | url = url.split('?')[0];
115 |
116 | url = url.replace('.webp', '.png');
117 |
118 | let fileName = url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'));
119 | const fileExtension = url.substr(url.lastIndexOf('.') + 1, url.length);
120 |
121 | if (saveLabel.includes('Avatar') || saveLabel.includes('Icon')) url += '?size=2048';
122 |
123 | if (e.target.classList.contains('emoji')) {
124 | saveLabel = 'Download Emoji';
125 | fileName = e.target.alt.replace(/[^A-Za-z_-]/g, '');
126 | }
127 | //console.log({url, saveLabel, fileName, fileExtension});
128 |
129 | setTimeout(() => addMenuItem(url, saveLabel, fileName, fileExtension), 5);
130 | },
131 | unload: function() {
132 | Dispatcher.unsubscribe("CONTEXT_MENU_OPEN", this.listener);
133 | }
134 | });
135 |
--------------------------------------------------------------------------------
/bd.css:
--------------------------------------------------------------------------------
1 | #bd-settingspane-container h2.ui-form-title {
2 | font-size: 16px;
3 | font-weight: 600;
4 | line-height: 20px;
5 | text-transform: uppercase;
6 | display: inline-block;
7 | margin-bottom: 20px;
8 | }
9 | #bd-settingspane-container h2.ui-form-title {
10 | color: #f6f6f7;
11 | }
12 | .theme-light #bd-settingspane-container h2.ui-form-title {
13 | color: #4f545c;
14 | }
15 |
16 | #bd-settingspane-container .ui-switch-item {
17 | flex-direction: column;
18 | margin-top: 8px;
19 | }
20 |
21 | #bd-settingspane-container .ui-switch-item h3 {
22 | font-size: 16px;
23 | font-weight: 500;
24 | line-height: 24px;
25 | flex: 1;
26 | }
27 | #bd-settingspane-container .ui-switch-item h3 {
28 | color: #f6f6f7;
29 | }
30 | .theme-light #bd-settingspane-container .ui-switch-item h3 {
31 | color: #4f545c;
32 | }
33 |
34 | #bd-settingspane-container .ui-switch-item .style-description {
35 | font-size: 14px;
36 | font-weight: 500;
37 | line-height: 20px;
38 | margin-bottom: 10px;
39 | padding-bottom: 10px;
40 | border-bottom: 1px solid hsla(218,5%,47%,.3);
41 | }
42 | #bd-settingspane-container .ui-switch-item .style-description {
43 | color: #72767d;
44 | }
45 | .theme-light #bd-settingspane-container .ui-switch-item .style-description {
46 | color: rgba(114,118,125,.6);
47 | }
48 |
49 | #bd-settingspane-container .ui-switch-item .ui-switch-wrapper {
50 | -webkit-user-select: none;
51 | -moz-user-select: none;
52 | -ms-user-select: none;
53 | user-select: none;
54 | position: relative;
55 | width: 44px;
56 | height: 24px;
57 | display: block;
58 | flex: 0 0 auto;
59 | }
60 |
61 | #bd-settingspane-container .ui-switch-item .ui-switch-wrapper input {
62 | position: absolute;
63 | opacity: 0;
64 | cursor: pointer;
65 | width: 100%;
66 | height: 100%;
67 | z-index: 1;
68 | }
69 |
70 | #bd-settingspane-container .ui-switch-item .ui-switch-wrapper .ui-switch {
71 | background: #7289da;
72 | position: absolute;
73 | top: 0;
74 | right: 0;
75 | bottom: 0;
76 | left: 0;
77 | background: #72767d;
78 | border-radius: 14px;
79 | transition: background .15s ease-in-out,box-shadow .15s ease-in-out,border .15s ease-in-out;
80 | }
81 |
82 | #bd-settingspane-container .ui-switch-item .ui-switch-wrapper .ui-switch:before {
83 | content: '';
84 | display: block;
85 | width: 18px;
86 | height: 18px;
87 | position: absolute;
88 | top: 3px;
89 | left: 3px;
90 | bottom: 3px;
91 | background: #f6f6f7;
92 | border-radius: 10px;
93 | transition: all .15s ease;
94 | box-shadow: 0 3px 1px 0 rgba(0,0,0,.05),0 2px 2px 0 rgba(0,0,0,.1),0 3px 3px 0 rgba(0,0,0,.05);
95 | }
96 |
97 | #bd-settingspane-container .ui-switch-item .ui-switch-wrapper .ui-switch.checked {
98 | background: #7289da;
99 | }
100 |
101 | #bd-settingspane-container .ui-switch-item .ui-switch-wrapper .ui-switch.checked:before {
102 | transform: translateX(20px);
103 | }
104 |
105 | #bd-settingspane-container .plugin-settings {
106 | padding: 0 12px 12px 20px;
107 | }
108 |
109 | @keyframes bd-modal-backdrop {
110 | to { opacity: 0.85; }
111 | }
112 |
113 | @keyframes bd-modal-anim {
114 | to { transform: scale(1); opacity: 1; }
115 | }
116 |
117 | @keyframes bd-modal-backdrop-closing {
118 | to { opacity: 0; }
119 | }
120 |
121 | @keyframes bd-modal-closing {
122 | to { transform: scale(0.7); opacity: 0; }
123 | }
124 |
125 | #bd-settingspane-container .backdrop {
126 | animation: bd-modal-backdrop 250ms ease;
127 | animation-fill-mode: forwards;
128 | background-color: rgb(0, 0, 0);
129 | transform: translateZ(0px);
130 | }
131 |
132 | #bd-settingspane-container.closing .backdrop {
133 | animation: bd-modal-backdrop-closing 200ms linear;
134 | animation-fill-mode: forwards;
135 | animation-delay: 50ms;
136 | opacity: 0.85;
137 | }
138 |
139 | #bd-settingspane-container.closing .modal {
140 | animation: bd-modal-closing 250ms cubic-bezier(0.19, 1, 0.22, 1);
141 | animation-fill-mode: forwards;
142 | opacity: 1;
143 | transform: scale(1);
144 | }
145 |
146 | #bd-settingspane-container .modal {
147 | animation: bd-modal-anim 250ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
148 | animation-fill-mode: forwards;
149 | transform: scale(0.7);
150 | transform-origin: 50% 50%;
151 | }
152 | /* Toast CSS */
153 |
154 | .toasts {
155 | position: fixed;
156 | display: flex;
157 | top: 0;
158 | flex-direction: column;
159 | align-items: center;
160 | justify-content: flex-end;
161 | pointer-events: none;
162 | z-index: 4000;
163 | }
164 |
165 | @keyframes toast-up {
166 | from {
167 | transform: translateY(0);
168 | opacity: 0;
169 | }
170 | }
171 |
172 | .toast {
173 | animation: toast-up 300ms ease;
174 | transform: translateY(-10px);
175 | background: #36393F;
176 | padding: 10px;
177 | border-radius: 5px;
178 | box-shadow: 0 0 0 1px rgba(32,34,37,.6), 0 2px 10px 0 rgba(0,0,0,.2);
179 | font-weight: 500;
180 | color: #fff;
181 | user-select: text;
182 | font-size: 14px;
183 | opacity: 1;
184 | margin-top: 10px;
185 | pointer-events: none;
186 | user-select: none;
187 | }
188 |
189 | @keyframes toast-down {
190 | to {
191 | transform: translateY(0px);
192 | opacity: 0;
193 | }
194 | }
195 |
196 | .toast.closing {
197 | animation: toast-down 200ms ease;
198 | animation-fill-mode: forwards;
199 | opacity: 1;
200 | transform: translateY(-10px);
201 | }
202 |
203 |
204 | .toast.icon {
205 | padding-left: 30px;
206 | background-size: 20px 20px;
207 | background-repeat: no-repeat;
208 | background-position: 6px 50%;
209 | }
210 |
211 | .toast.toast-info {
212 | background-color: #4a90e2;
213 | }
214 |
215 | .toast.toast-info.icon {
216 | background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPiAgICA8cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJzNC40OCAxMCAxMCAxMCAxMC00LjQ4IDEwLTEwUzE3LjUyIDIgMTIgMnptMSAxNWgtMnYtNmgydjZ6bTAtOGgtMlY3aDJ2MnoiLz48L3N2Zz4=);
217 | }
218 |
219 | .toast.toast-success {
220 | background-color: #43b581;
221 | }
222 |
223 | .toast.toast-success.icon {
224 | background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPiAgICA8cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJzNC40OCAxMCAxMCAxMCAxMC00LjQ4IDEwLTEwUzE3LjUyIDIgMTIgMnptLTIgMTVsLTUtNSAxLjQxLTEuNDFMMTAgMTQuMTdsNy41OS03LjU5TDE5IDhsLTkgOXoiLz48L3N2Zz4=);
225 | }
226 | .toast.toast-danger,
227 | .toast.toast-error {
228 | background-color: #f04747;
229 | }
230 |
231 | .toast.toast-danger.icon,
232 | .toast.toast-error.icon {
233 | background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTEyIDJDNi40NyAyIDIgNi40NyAyIDEyczQuNDcgMTAgMTAgMTAgMTAtNC40NyAxMC0xMFMxNy41MyAyIDEyIDJ6bTUgMTMuNTlMMTUuNTkgMTcgMTIgMTMuNDEgOC40MSAxNyA3IDE1LjU5IDEwLjU5IDEyIDcgOC40MSA4LjQxIDcgMTIgMTAuNTkgMTUuNTkgNyAxNyA4LjQxIDEzLjQxIDEyIDE3IDE1LjU5eiIvPiAgICA8cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PC9zdmc+);
234 | }
235 |
236 | .toast.toast-warning,
237 | .toast.toast-warn {
238 | background-color: #FFA600;
239 | color: white;
240 | }
241 |
242 | .toast.toast-warning.icon,
243 | .toast.toast-warn.icon {
244 | background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPiAgICA8cGF0aCBkPSJNMSAyMWgyMkwxMiAyIDEgMjF6bTEyLTNoLTJ2LTJoMnYyem0wLTRoLTJ2LTRoMnY0eiIvPjwvc3ZnPg==);
245 | }
246 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace EnhancedDiscordUI.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EnhancedDiscordUI.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 |
63 | ///
64 | /// Looks up a localized resource of type System.Drawing.Bitmap.
65 | ///
66 | internal static System.Drawing.Bitmap discord_canary_16 {
67 | get {
68 | object obj = ResourceManager.GetObject("discord_canary_16", resourceCulture);
69 | return ((System.Drawing.Bitmap)(obj));
70 | }
71 | }
72 |
73 | ///
74 | /// Looks up a localized resource of type System.Drawing.Bitmap.
75 | ///
76 | internal static System.Drawing.Bitmap discord_canary_32 {
77 | get {
78 | object obj = ResourceManager.GetObject("discord_canary_32", resourceCulture);
79 | return ((System.Drawing.Bitmap)(obj));
80 | }
81 | }
82 |
83 | ///
84 | /// Looks up a localized resource of type System.Drawing.Bitmap.
85 | ///
86 | internal static System.Drawing.Bitmap discord_canary_64 {
87 | get {
88 | object obj = ResourceManager.GetObject("discord_canary_64", resourceCulture);
89 | return ((System.Drawing.Bitmap)(obj));
90 | }
91 | }
92 |
93 | ///
94 | /// Looks up a localized resource of type System.Drawing.Bitmap.
95 | ///
96 | internal static System.Drawing.Bitmap discord_dev_16 {
97 | get {
98 | object obj = ResourceManager.GetObject("discord_dev_16", resourceCulture);
99 | return ((System.Drawing.Bitmap)(obj));
100 | }
101 | }
102 |
103 | ///
104 | /// Looks up a localized resource of type System.Drawing.Bitmap.
105 | ///
106 | internal static System.Drawing.Bitmap discord_dev_32 {
107 | get {
108 | object obj = ResourceManager.GetObject("discord_dev_32", resourceCulture);
109 | return ((System.Drawing.Bitmap)(obj));
110 | }
111 | }
112 |
113 | ///
114 | /// Looks up a localized resource of type System.Drawing.Bitmap.
115 | ///
116 | internal static System.Drawing.Bitmap discord_dev_64 {
117 | get {
118 | object obj = ResourceManager.GetObject("discord_dev_64", resourceCulture);
119 | return ((System.Drawing.Bitmap)(obj));
120 | }
121 | }
122 |
123 | ///
124 | /// Looks up a localized resource of type System.Drawing.Bitmap.
125 | ///
126 | internal static System.Drawing.Bitmap discord_stable_16 {
127 | get {
128 | object obj = ResourceManager.GetObject("discord_stable_16", resourceCulture);
129 | return ((System.Drawing.Bitmap)(obj));
130 | }
131 | }
132 |
133 | ///
134 | /// Looks up a localized resource of type System.Drawing.Bitmap.
135 | ///
136 | internal static System.Drawing.Bitmap discord_stable_32 {
137 | get {
138 | object obj = ResourceManager.GetObject("discord_stable_32", resourceCulture);
139 | return ((System.Drawing.Bitmap)(obj));
140 | }
141 | }
142 |
143 | ///
144 | /// Looks up a localized resource of type System.Drawing.Bitmap.
145 | ///
146 | internal static System.Drawing.Bitmap discord_stable_64 {
147 | get {
148 | object obj = ResourceManager.GetObject("discord_stable_64", resourceCulture);
149 | return ((System.Drawing.Bitmap)(obj));
150 | }
151 | }
152 |
153 | ///
154 | /// Looks up a localized resource of type System.Drawing.Bitmap.
155 | ///
156 | internal static System.Drawing.Bitmap ed_og {
157 | get {
158 | object obj = ResourceManager.GetObject("ed_og", resourceCulture);
159 | return ((System.Drawing.Bitmap)(obj));
160 | }
161 | }
162 |
163 | ///
164 | /// Looks up a localized string similar to require(`${process.env.injDir}/injection.js`);.
165 | ///
166 | internal static string injection {
167 | get {
168 | return ResourceManager.GetString("injection", resourceCulture);
169 | }
170 | }
171 |
172 | ///
173 | /// Looks up a localized string similar to https://codeload.github.com/joe27g/EnhancedDiscord/zip/.
174 | ///
175 | internal static string zipLink {
176 | get {
177 | return ResourceManager.GetString("zipLink", resourceCulture);
178 | }
179 | }
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Form1.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | 19, 20
122 |
123 |
124 |
125 |
126 | AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABMLAAATCwAAAAAAAAAA
127 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
128 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
129 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
130 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
131 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAACAAAAAgAAAAIAAAACAAAAAAAAAAEAAAACAAAAAgAA
132 | AAIAAAACAAAAAQAAAAAAAAAAAAAAAAAON1cADTaWAA02lwANNpcADTaYAA0zkgAHHzgADTZUAA02mAAN
133 | NpcADTaXAA01lwAMMYQACCJFAAEDCQACCgAAFVOdABVW/wAVU/gAFVPwABVT8AAVU/EAEUWrABNNrAAV
134 | Vv8AFVT5ABVU8wAVVPUAFVX9ABRR7AAOO3gAAAAJABxvnQAccv8AF1y/ABRSgwAVVYQAE051ABFHOAAb
135 | baEAHHH/ABZXjQAWWTIAF1w2ABllbwAcce0AGmjmAA01QQAiip0AI47/ACOM+gAji/UAI4z2ACGE5wAV
136 | U1sAIomZACOM/wAaaGwAM80AABZaAAAXXwgAIoq5ACKK/AAWWmcAKaWdACqp/wAkkM0AIoeeACKJnwAi
137 | iJ4AHXNaACGFVAAhg5IAGGFQABFFIwATSiUAIYNWACqn4wApo+4AGmhJADDBnQAyxv8AL7vvAC643wAu
138 | ud8ALbTbAB97bAAml0UALrfQAC654gAuuOIALrnkADDA9gAxw/kAKqqOAA87CwA11WwANtiyADbZsgA2
139 | 2bMANtmzADfZtQAvu5IAHHAeADbWeQA32bUANtmzADbYswA10p4AMMBZACKIDAApowAAMsUDAC61BQAt
140 | tAUALbQFAC20BQAttAUANM4FACqoAQAxwgIALrYFAC20BQAqqQUAHXQCACqmAAAAAAAAAAAAAAAAAAAA
141 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
142 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
143 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
144 | AAAAAAAA//8AAP//AAD//wAA//8AAAIHAAAAAQAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAQAAAAcAAP//
145 | AAD//wAA//8AAA==
146 |
147 |
148 |
--------------------------------------------------------------------------------
/bd_shit.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const Module = require('module').Module;
4 | const originalRequire = Module._extensions['.js'];
5 | const EDPlugin = require('./plugin');
6 | const electron = require('electron');
7 |
8 | const splitRegex = /[^\S\r\n]*?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
9 | const escapedAtRegex = /^\\@/;
10 | module.exports = class BDManager {
11 |
12 | static async setup() {
13 | this.defineGlobals();
14 | this.jqueryElement = document.createElement('script');
15 | this.jqueryElement.src = `//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js`;
16 | await new Promise(resolve => {
17 | this.jqueryElement.onload = resolve;
18 | document.head.appendChild(this.jqueryElement);
19 | });
20 |
21 | this.observer = new MutationObserver((mutations) => {
22 | for (let i = 0, mlen = mutations.length; i < mlen; i++) this.fireEvent('observer', mutations[i]);
23 | });
24 | this.observer.observe(document, {childList: true, subtree: true});
25 |
26 | //this.currentWindow.webContents.on('did-navigate-in-page', BDManager.onSwitch);
27 | try { electron.ipcRenderer.invoke('bd-navigate-page-listener', BDManager.onSwitch); }
28 | catch(err) { throw new Error(`Could not add navigate page listener. ${err}\n${err.stack}`); };
29 |
30 | fs.readFile(path.join(process.env.injDir, 'bd.css'), (err, text) => {
31 | if (err) return console.error(err);
32 | EDApi.injectCSS('BDManager', text);
33 | });
34 |
35 | Module._extensions['.js'] = this.pluginRequire();
36 | }
37 |
38 | static destroy() {
39 | EDApi.clearCSS('BDManager');
40 | this.observer.disconnect();
41 | //this.currentWindow.webContents.removeEventListener('did-navigate-in-page', BDManager.onSwitch);
42 | try { electron.ipcRenderer.invoke('remove-bd-navigate-page-listener', BDManager.onSwitch); }
43 | catch(err) { throw new Error(`Could not remove navigate page listener. ${err}\n${err.stack}`); };
44 | this.jqueryElement.remove();
45 | Module._extensions['.js'] = originalRequire;
46 | }
47 |
48 | static onSwitch() {
49 | BDManager.fireEvent('onSwitch');
50 | }
51 |
52 | static extractMeta(content) {
53 | const firstLine = content.split('\n')[0];
54 | const hasOldMeta = firstLine.includes('//META');
55 | if (hasOldMeta) return BDManager.parseOldMeta(content);
56 | const hasNewMeta = firstLine.includes('/**');
57 | if (hasNewMeta) return BDManager.parseNewMeta(content);
58 | throw new Error('META was not found.');
59 | }
60 |
61 | static parseOldMeta(content) {
62 | const meta = content.split('\n')[0];
63 | const rawMeta = meta.substring(meta.lastIndexOf('//META') + 6, meta.lastIndexOf('*//'));
64 | if (meta.indexOf('META') < 0) throw new Error('META was not found.');
65 | const parsed = EDApi.testJSON(rawMeta);
66 | if (!parsed) throw new Error('META could not be parsed.');
67 | if (!parsed.name) throw new Error('META missing name data.');
68 | parsed.format = 'json';
69 | return parsed;
70 | }
71 |
72 | static parseNewMeta(content) {
73 | const block = content.split('/**', 2)[1].split('*/', 1)[0];
74 | const out = {};
75 | let field = '';
76 | let accum = '';
77 | for (const line of block.split(splitRegex)) {
78 | if (line.length === 0) continue;
79 | if (line.charAt(0) === '@' && line.charAt(1) !== ' ') {
80 | out[field] = accum;
81 | const l = line.indexOf(' ');
82 | field = line.substr(1, l - 1);
83 | accum = line.substr(l + 1);
84 | }
85 | else {
86 | accum += ' ' + line.replace('\\n', '\n').replace(escapedAtRegex, '@');
87 | }
88 | }
89 | out[field] = accum.trim();
90 | delete out[''];
91 | out.format = 'jsdoc';
92 | return out;
93 | }
94 |
95 | static isEmpty(obj) {
96 | if (obj == null || obj == undefined || obj == '') return true;
97 | if (typeof(obj) !== 'object') return false;
98 | if (Array.isArray(obj)) return obj.length == 0;
99 | for (const key in obj) {
100 | if (obj.hasOwnProperty(key)) return false;
101 | }
102 | return true;
103 | }
104 |
105 | static pluginRequire() {
106 | return function(moduleWrap, filename) {
107 | if (!filename.endsWith('.plugin.js') || path.dirname(filename) !== path.resolve(process.env.injDir, 'plugins')) return Reflect.apply(originalRequire, this, arguments);
108 | let content = fs.readFileSync(filename, 'utf8');
109 | if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1); // Strip BOM
110 | const meta = BDManager.extractMeta(content);
111 | meta.filename = path.basename(filename);
112 |
113 | moduleWrap._compile(content, filename);
114 | const noExport = BDManager.isEmpty(moduleWrap.exports);
115 | if (noExport) {
116 | content += `\nmodule.exports = ${meta.name};`;
117 | moduleWrap._compile(content, filename);
118 | }
119 | if (moduleWrap.exports.default) moduleWrap.exports = moduleWrap.exports.default;
120 | moduleWrap.exports = BDManager.convertPlugin(new moduleWrap.exports());
121 | };
122 | }
123 |
124 | static fireEvent(event, ...args) {
125 | const plugins = Object.values(ED.plugins);
126 | for (let p = 0; p < plugins.length; p++) {
127 | const plugin = plugins[p];
128 | if (!plugin[event] || typeof plugin[event] !== 'function') continue;
129 | try { plugin[event](...args); }
130 | catch (error) { throw new Error(`Could not fire ${event} for plugin ${plugin.name}.`); }
131 | }
132 | }
133 |
134 | static convertPlugin(plugin) {
135 | const newPlugin = new EDPlugin({
136 | name: plugin.getName(),
137 | load: function() {
138 | if (plugin.load) plugin.load();
139 | plugin.start();
140 | },
141 | unload: function() {plugin.stop();},
142 | config: {},
143 | bdplugin: plugin
144 | });
145 | Object.defineProperties(newPlugin, {
146 | name: {
147 | enumerable: true, configurable: true,
148 | get() {return plugin.getName();}
149 | },
150 | author: {
151 | enumerable: true, configurable: true,
152 | get() {return plugin.getAuthor();}
153 | },
154 | description: {
155 | enumerable: true, configurable: true,
156 | get() {return plugin.getDescription();}
157 | }
158 | });
159 | if (typeof plugin.getSettingsPanel == 'function') {
160 | newPlugin.settingsSectionName = plugin.getName();
161 | newPlugin.generateSettingsSection = function() {return plugin.getSettingsPanel();};
162 | newPlugin.getSettingsPanel = function() {return plugin.getSettingsPanel();};
163 | }
164 | if (typeof plugin.onSwitch == 'function') newPlugin.onSwitch = function() {return plugin.onSwitch();};
165 | if (typeof plugin.observer == 'function') newPlugin.observer = function(e) {return plugin.observer(e);};
166 | return newPlugin;
167 | }
168 |
169 | static defineGlobals() {
170 | window.bdConfig = {dataPath: process.env.injDir};
171 | window.bdplugins = window.bdthemes = window.pluginCookie = window.themeCookie = window.settingsCookie = {};
172 | window.bdpluginErrors = window.bdthemeErrors = [];
173 |
174 | window.bdPluginStorage = {get: EDApi.getData, set: EDApi.setData};
175 | window.Utils = {monkeyPatch: EDApi.monkeyPatch, suppressErrors: EDApi.suppressErrors, escapeID: EDApi.escapeID};
176 |
177 | window.BDV2 = class V2 {
178 | static get WebpackModules() {return {find: EDApi.findModule, findAll: EDApi.findAllModules, findByUniqueProperties: EDApi.findModuleByProps, findByDisplayName: EDApi.findModuleByDisplayName};}
179 | static getInternalInstance(node) {return EDApi.getInternalInstance(node);}
180 | static get react() {return EDApi.React;}
181 | static get reactDom() {return EDApi.ReactDOM;}
182 | };
183 | }
184 | };
185 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 |
122 | ..\Resources\discord_canary_64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
123 |
124 |
125 | ..\Resources\discord_dev_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
126 |
127 |
128 | ..\Resources\discord_canary_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
129 |
130 |
131 | ..\Resources\discord_stable_64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
132 |
133 |
134 | ..\Resources\discord_canary_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
135 |
136 |
137 | ..\Resources\discord_dev_64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
138 |
139 |
140 | ..\Resources\discord_stable_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
141 |
142 |
143 | ..\Resources\discord_dev_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
144 |
145 |
146 | ..\Resources\discord_stable_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
147 |
148 |
149 | ..\Resources\ed_og.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
150 |
151 |
152 | require(`${process.env.injDir}/injection.js`);
153 |
154 |
155 | https://codeload.github.com/joe27g/EnhancedDiscord/zip/
156 |
157 |
--------------------------------------------------------------------------------
/plugins/css_settings.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 | const fs = require('fs');
3 | const path = require('path');
4 | const propMap = {
5 | bg_color: 'bg',
6 | bg_opacity: 'bg-overlay'
7 | }
8 |
9 | module.exports = new Plugin({
10 | name: 'CSS Settings',
11 | author: 'Joe 🎸#7070',
12 | description: 'Allows you to modify options for the default theme.',
13 | color: 'blue',
14 |
15 | load: () => {},
16 | unload: () => {},
17 |
18 | customLoad: function(prop) {
19 | const st = ED.plugins.css_loader.settings;
20 | if (!prop || !st || !st.path) return;
21 | const cssPath = path.isAbsolute(st.path) ? st.path : path.join(process.env.injDir, st.path);
22 |
23 | let content;
24 | try {
25 | content = fs.readFileSync(cssPath, 'utf-8');
26 | } catch (e) {
27 | this.error(e);
28 | return null;
29 | }
30 |
31 | const lines = content.split(/[\r\n][\r\n]?/).filter(l => l);
32 | for (const i in lines) {
33 | if (lines[i] && lines[i].match(/^\s*(?:\/\*)?\s*--/)) {
34 | const reg = new RegExp(`--${propMap[prop] || prop}: ([^;]+);`);
35 | const matches = lines[i].match(reg);
36 | if (!matches) continue;
37 | const raw = matches[1];
38 | if (!raw) continue;
39 |
40 | if (prop === 'bg' && raw.startsWith('url(')) {
41 | return raw.substring(4, raw.length - 1)
42 | } else if (prop === 'bg') {
43 | return null;
44 | }
45 | if (prop === 'bg_color' && !raw.startsWith('url(')) {
46 | return raw;
47 | } else if (prop === 'bg_color') {
48 | return null;
49 | }
50 | if (prop === 'bg_opacity' && raw.startsWith('rgba(')) {
51 | const str = raw.substring(5, raw.length - 1).split(',').pop();
52 | if (!str) return null;
53 | return 100 - Math.round(parseFloat(str)*100);
54 | } else if (prop === 'bg_opacity') {
55 | return 100;
56 | } else if (prop === 'gift-button' || prop === 'gif-picker') {
57 | return raw === 'none' ? false : true;
58 | }
59 | // TODO: proper transparency support?
60 | return raw;
61 | }
62 | }
63 | },
64 | customSave: function(prop, data) {
65 | let varName = prop, finalValue = data;
66 | switch (prop) {
67 | case 'bg':
68 | if (data) {
69 | if (!data.startsWith('https://') && !data.startsWith('http://'))
70 | EDApi.showToast('Warning: This location probably won\'t work - it needs to be a remote URL, not a local file.')
71 | finalValue = `url(${data})`;
72 | } else {
73 | finalValue = 'transparent'
74 | }
75 | break;
76 | case 'bg_color':
77 | varName = 'bg';
78 | finalValue = data || 'transparent';
79 | break;
80 | case 'bg_opacity':
81 | varName = 'bg-overlay';
82 | finalValue = `rgba(0, 0, 0, ${(100 - data) / 100.0})`;
83 | break;
84 | case 'typing-height':
85 | finalValue = finalValue || 0;
86 | break;
87 | case 'gift-button':
88 | case 'gif-picker':
89 | finalValue = finalValue ? 'flex' : 'none';
90 | break;
91 | default:
92 | finalValue = data || 'transparent';
93 | // TODO: proper transparency support?
94 | }
95 | const reg = new RegExp(`--${varName}: ([^;]+);`);
96 |
97 | const st = ED.plugins.css_loader.settings;
98 | if (!st || !st.path) return;
99 | const cssPath = path.isAbsolute(st.path) ? st.path : path.join(process.env.injDir, st.path);
100 |
101 | fs.readFile(cssPath, 'utf-8', (err, content) => {
102 | if (err) return module.exports.error(err);
103 | const lines = content.split(/\r\n/g);
104 | let changed = false;
105 | for (const i in lines) {
106 | if (lines[i] && reg.test(lines[i])) {
107 | lines[i] = lines[i].replace(reg, `--${varName}: ${finalValue};`);
108 | changed = true;
109 | }
110 | }
111 | if (changed) {
112 | fs.writeFile(cssPath, lines.join('\n'), err => {
113 | if (err) {
114 | EDApi.showToast('Error saving: '+err);
115 | return module.exports.error(err);
116 | }
117 | EDApi.showToast('Saved.');
118 | return;
119 | })
120 | } else {
121 | return EDApi.showToast('Already saved.');
122 | }
123 | });
124 | },
125 |
126 | settingsSectionName: 'Theme Settings',
127 | generateSettingsSection: function() {
128 | if (!ED.plugins.css_loader || !ED.customCss || !ED.customCss.innerHTML || (!ED.customCss.innerHTML.includes("enhanceddiscord.com/theme") && !ED.customCss.innerHTML.includes("EnhancedDiscord Theme"))) return;
129 |
130 | const els = [{
131 | type: "input:text",
132 | configName: "bg",
133 | title: "Background Image",
134 | desc: "Must be a remote URL, not a local file."
135 | }, {
136 | type: "input:colorpicker",
137 | title: 'Background Color',
138 | configName: "bg_color",
139 | desc: "Set your background to a simple color rather than an image. See the list of [valid css colors](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).",
140 | colors: [0x3C0D0D, 0x0D3C13, 0x0D193C, 0x250D3C, 0x4B1741, 0x552A13, 0x2D261F, 0x153538, 0x000000, 0x151515],
141 | defaultColor: 0x36393F
142 | }, {
143 | type: "input:slider",
144 | configName: "bg_opacity",
145 | title: "Background Opacity",
146 | desc: "Opacity of the background image underneath other elements. Lowering it increases readability.",
147 | defaultValue: 20,
148 | highlightDefaultValue: true,
149 | markers: [
150 | 0,10,20,30,40,50,60,69,80,90,100
151 | ],
152 | formatTooltip: e => e.toFixed(0)+'%',
153 | minValue: 0,
154 | maxValue: 100,
155 | }, {
156 | type: "input:boolean",
157 | title: "Nitro gift button",
158 | configName: "gift-button",
159 | note: "Show Nitro gift button in the chat textarea (next to emoji picker or gif picker)"
160 | }, {
161 | type: "input:boolean",
162 | title: "GIF picker",
163 | configName: "gif-picker",
164 | note: "Show GIF picker button in the chat textarea (next to emoji picker)"
165 | }, {
166 | type: "input:colorpicker",
167 | title: 'Accent Color',
168 | configName: "accent",
169 | desc: "Prominent color in the UI. Affects buttons, switches, mentions, etc.",
170 | colors: [0x7390DB], // TODO: need more defaults
171 | defaultColor: 0x990000
172 | }, {
173 | type: "input:colorpicker",
174 | title: 'Accent Background',
175 | configName: "accent-back",
176 | desc: "Background for mentions and other misc. things.",
177 | colors: [], // TODO: need more defaults
178 | defaultColor: 0x660000
179 | }, {
180 | type: "input:colorpicker",
181 | title: 'Bright Accent Color',
182 | configName: "accent-bright",
183 | desc: "Color of mentions while hovering and other misc. things.",
184 | colors: [0xFFFFFF], // TODO: need more defaults
185 | defaultColor: 0xFF0000
186 | }, {
187 | type: "input:colorpicker",
188 | title: 'Bright Accent Background',
189 | configName: "accent-back-bright",
190 | desc: "Background for mentions while hovering and other misc. things.",
191 | colors: [], // TODO: need more defaults
192 | defaultColor: 0x880000
193 | }, {
194 | type: "input:colorpicker",
195 | title: 'Icon Color',
196 | configName: "icon-color",
197 | desc: "Color of icons for channels, home sections, etc.",
198 | colors: [], // TODO: need more defaults
199 | defaultColor: 0xFAA61A
200 | }, {
201 | type: "input:colorpicker",
202 | title: 'Link Color',
203 | configName: "link-color",
204 | desc: "Color of links.",
205 | colors: [], // TODO: need more defaults
206 | defaultColor: 0xFAA61A
207 | }, {
208 | type: "input:colorpicker",
209 | title: 'Hovered Link Color',
210 | configName: "link-color-hover",
211 | desc: "Color of links while hovering over them.",
212 | colors: [], // TODO: need more defaults
213 | defaultColor: 0xFAD61A
214 | }, {
215 | type: "input:colorpicker",
216 | title: 'Popup Background',
217 | configName: "popup-background",
218 | desc: "Background color of modals and popups, such as pinned messages, context menus, and confirmation dialogs.",
219 | colors: [], // TODO: need more defaults
220 | defaultColor: 0x222222
221 | }, {
222 | type: "input:colorpicker",
223 | title: 'Popup Headers & Footers',
224 | configName: "popup-highlight",
225 | desc: "Background color of headers and footers on modals and popups.",
226 | colors: [], // TODO: need more defaults
227 | defaultColor: 0x333333
228 | }, {
229 | type: "input:colorpicker",
230 | title: 'Unread Color',
231 | configName: "unread-color",
232 | desc: "Color of channel/server unread or selected indicators.",
233 | colors: [0xFFFFFF], // TODO: need more defaults
234 | defaultColor: 0x990000
235 | }, {
236 | type: "input:text",
237 | configName: "typing-height",
238 | title: "Typing Height",
239 | desc: "Height of typing element and margin underneath chat to allow space for it."
240 | }];
241 | els.forEach(item => {
242 | let dat = this.customLoad(item.configName);
243 | if (dat && /^#\d{3}$/.test(dat)) {
244 | dat = dat[0]+dat[1]+dat[1]+dat[2]+dat[2]+dat[3]+dat[3];
245 | }
246 | // TODO: proper transparency support?
247 | if (item.type === "input:colorpicker")
248 | item.currentColor = dat ? parseInt(dat.substr(1), 16) : null;
249 | });
250 | return els;
251 | }
252 | });
253 |
--------------------------------------------------------------------------------
/plugins/hidden_channels.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 |
3 | let getChannel, g_dc, g_cat, ha, disp, chanM, fm, reb, sv, cs, csp, ghp, gs, gsr, pf, sw = {}, g = {}, ai = {};
4 |
5 | module.exports = new Plugin({
6 | name: 'Hidden Channels',
7 | description: 'Shows hidden channels and lets you view server permissions.',
8 | color: 'magenta',
9 | author: 'Joe 🎸#7070',
10 |
11 | load: async function() {
12 | disp = EDApi.findModule("dispatch");
13 | getChannel = EDApi.findModule('getChannel').getChannel;
14 | sw = EDApi.findModule('switchItem');
15 | g = EDApi.findModule(m => m.group && m.item);
16 | ai = EDApi.findModule('actionIcon');
17 |
18 | const getUser = EDApi.findModule('getCurrentUser').getCurrentUser;
19 | const getAllChannels = EDApi.findModule('getMutableGuildChannels').getMutableGuildChannels;
20 | const can = EDApi.findModule('computePermissions').can;
21 |
22 | g_dc = EDApi.findModule('getDefaultChannel');
23 | EDApi.monkeyPatch(g_dc, 'getChannels', b => {
24 | const og = b.callOriginalMethod(b.methodArguments);
25 | if (!b.methodArguments[0]) return og;
26 | const hidden = [], allChans = getAllChannels();
27 | for (const i in allChans) {
28 | if (allChans[i].guild_id === b.methodArguments[0]) {
29 | if (allChans[i].type !== 4 && !can({data: 1024n}, getUser(), getChannel(allChans[i].id))) {
30 | hidden.push(allChans[i]);
31 | }
32 | }
33 | }
34 | og.HIDDEN = hidden;
35 | return og;
36 | });
37 | chanM = EDApi.findModule(m => m.prototype && m.prototype.isManaged);
38 | chanM.prototype.isHidden = function() {
39 | return [0, 4, 5].includes(this.type) && !can({data: 1024n}, getUser(), this);
40 | }
41 |
42 | g_cat = EDApi.findModule(m => m.getCategories && !m.EMOJI_NAME_RE);
43 | EDApi.monkeyPatch(g_cat, 'getCategories', b => {
44 | const og = b.callOriginalMethod(b.methodArguments);
45 | const chs = g_dc.getChannels(b.methodArguments[0]);
46 | chs.HIDDEN.forEach(c => {
47 | const result = og[c.parent_id || "null"].filter(item => item.channel.id === c.id);
48 | if (result.length) return; // already added
49 | og[c.parent_id || "null"].push({channel: c, index: 0})
50 | });
51 | return og;
52 | });
53 |
54 | ha = EDApi.findModule('hasUnread').__proto__;
55 | EDApi.monkeyPatch(ha, 'hasUnread', function(b) {
56 | if (getChannel(b.methodArguments[0]) && getChannel(b.methodArguments[0]).isHidden())
57 | return false; // don't show hidden channels as unread.
58 | return b.callOriginalMethod(b.methodArguments);
59 | });
60 | EDApi.monkeyPatch(ha, 'hasUnreadPins', function(b) {
61 | if (getChannel(b.methodArguments[0]) && getChannel(b.methodArguments[0]).isHidden())
62 | return false; // don't show icon on hidden channel pins.
63 | return b.callOriginalMethod(b.methodArguments);
64 | });
65 |
66 | disp.subscribe("CHANNEL_SELECT", module.exports.dispatchSubscription);
67 |
68 | fm = EDApi.findModule("fetchMessages");
69 | EDApi.monkeyPatch(fm, "fetchMessages", function(b) {
70 | if (getChannel(b.methodArguments[0]) && getChannel(b.methodArguments[0]).isHidden()) return;
71 | return b.callOriginalMethod(b.methodArguments);
72 | });
73 |
74 | const clk = window.EDApi.findModuleByDisplayName("Clickable")
75 | const Tooltip = window.EDApi.findModule('TooltipContainer').TooltipContainer;
76 | const { Messages } = window.EDApi.findModule('Messages');
77 | const getIcon = window.EDApi.findModule(m => m.id && typeof m.keys === 'function' && m.keys().includes('./Gear'));
78 | const Gear = getIcon('./Gear').default;
79 |
80 | reb = window.EDApi.findModule(m => m.default && m.default.prototype && m.default.prototype.renderEditButton).default.prototype;
81 | window.EDApi.monkeyPatch(reb, "renderEditButton", function(b) {
82 | return window.EDApi.React.createElement(Tooltip, { text: Messages.EDIT_CHANNEL }, window.EDApi.React.createElement(clk, {
83 | className: ai.iconItem,
84 | onClick: function() {
85 | module.exports._editingGuild = null;
86 | module.exports._editingChannel = b.thisObject.props.channel.id;
87 | return b.thisObject.handleEditClick.apply(b.thisObject, arguments);
88 | },
89 | onMouseEnter: b.thisObject.props.onMouseEnter,
90 | onMouseLeave: b.thisObject.props.onMouseLeave
91 | }, window.EDApi.React.createElement(Gear, {
92 | width: 16,
93 | height: 16,
94 | className: ai.actionIcon
95 | })));
96 | });
97 |
98 | sv = EDApi.findModuleByDisplayName("SettingsView").prototype;
99 | EDApi.monkeyPatch(sv, 'getPredicateSections', {before: b => {
100 | const permSect = b.thisObject.props.sections.find(item => item.section === 'PERMISSIONS');
101 | if (permSect) permSect.predicate = () => true;
102 | }, silent: true});
103 |
104 | cs = EDApi.findModuleByDisplayName("FluxContainer(ChannelSettings)").prototype;
105 | EDApi.monkeyPatch(cs, 'render', b => {
106 | const egg = b.callOriginalMethod(b.methodArguments);
107 | egg.props.canManageRoles = true;
108 | return egg;
109 | });
110 |
111 | csp = EDApi.findModuleByDisplayName("FluxContainer(ChannelSettingsPermissions)").prototype;
112 | EDApi.monkeyPatch(csp, 'render', b => {
113 | const egg = b.callOriginalMethod(b.methodArguments);
114 | const chan = getChannel(egg.props.channel.id);
115 | if (!chan || !chan.isHidden()) return egg;
116 | egg.props.canSyncChannel = false;
117 | egg.props.locked = true;
118 | setTimeout(() => {
119 | document.querySelectorAll('.'+g.group).forEach(elem => elem.style = "opacity: 0.5; pointer-events: none;");
120 | });
121 | return egg;
122 | });
123 |
124 | /*ghp = EDApi.findModuleByDisplayName("FluxContainer(GuildHeaderPopout)").prototype;
125 | EDApi.monkeyPatch(ghp, 'render', b => {
126 | const egg = b.callOriginalMethod(b.methodArguments);
127 | egg.props.canAccessSettings = true;
128 | return egg;
129 | });
130 |
131 | gs = EDApi.findModuleByDisplayName("FluxContainer(GuildSettings)").prototype;
132 | EDApi.monkeyPatch(gs, 'render', b => {
133 | const egg = b.callOriginalMethod(b.methodArguments);
134 | module.exports._editingChannel = null;
135 | module.exports._editingGuild = egg.props.guild.id;
136 | egg.props.canManageRoles = true;
137 | return egg;
138 | });*/
139 |
140 | const cancan = EDApi.findModuleByProps('can').can;
141 | gsr = EDApi.findModuleByDisplayName("FluxContainer(GuildSettingsRoles)").prototype;
142 | EDApi.monkeyPatch(gsr, 'render', b => {
143 | const egg = b.callOriginalMethod(b.methodArguments);
144 | const hasPerm = cancan({data: 268435456n}, { guildId: egg.props.guild.id });
145 | if (hasPerm) return;
146 | setTimeout(() => {
147 | document.querySelectorAll('.'+sw.switchItem).forEach(elem => elem.classList.add(sw.disabled));
148 | });
149 | return egg;
150 | });
151 |
152 | const getGuild = EDApi.findModule('getGuild').getGuild;
153 | pf = EDApi.findModuleByDisplayName("PermissionsForm").prototype;
154 | EDApi.monkeyPatch(pf, 'render', b => {
155 | const egg = b.callOriginalMethod(b.methodArguments);
156 | const guild = module.exports._editingGuild ? getGuild(module.exports._editingGuild) : null;
157 | const channel = module.exports._editingChannel ? getChannel(module.exports._editingChannel) : null;
158 | if (!guild && !channel) return egg;
159 | const hasPerm = cancan({data: 268435456n}, guild ? { guildId: guild.id } : { channelId: channel.id });
160 | if (hasPerm) return egg;
161 |
162 | if (!egg.props.children || !egg.props.children[1]) return egg;
163 | egg.props.children[1].forEach(item => {item.disabled = true; item.props.disabled = true;});
164 | return egg;
165 | });
166 | },
167 | unload: function() {
168 | g_dc.getChannels.unpatch();
169 | g_cat.getCategories.unpatch();
170 | ha.hasUnread.unpatch();
171 | ha.hasUnreadPins.unpatch();
172 | fm.fetchMessages.unpatch();
173 | reb.renderEditButton.unpatch();
174 |
175 | for (const mod of [sv, cs, csp, ghp, gs, gsr, pf])
176 | if (mod && mod.render && mod.render.unpatch) mod.render.unpatch();
177 |
178 | disp.unsubscribe("CHANNEL_SELECT", module.exports.dispatchSubscription);
179 | },
180 | dispatchSubscription: function (data) {
181 | if (data.type !== "CHANNEL_SELECT") return;
182 |
183 | if (getChannel(data.channelId) && getChannel(data.channelId).isHidden()) {
184 | setTimeout(module.exports.attachHiddenChanNotice);
185 | }
186 | },
187 | attachHiddenChanNotice: function () {
188 | const messagesWrapper = document.querySelector(`.${EDApi.findModule("messagesWrapper").messagesWrapper}`);
189 | if (!messagesWrapper) return;
190 |
191 | messagesWrapper.firstChild.style.display = "none"; // Remove messages shit.
192 | messagesWrapper.parentElement.children[1].style.display = "none"; // Remove message box.
193 | messagesWrapper.parentElement.parentElement.children[1].style.display = "none"; // Remove user list.
194 |
195 | const toolbar = document.querySelector("."+EDApi.findModule(m => {
196 | if (m instanceof Window) return;
197 | if (m.toolbar && m.selected) return m;
198 | }).toolbar);
199 |
200 | toolbar.style.display = "none";
201 |
202 | const hiddenChannelNotif = document.createElement("div");
203 |
204 | // Class name modules
205 | const txt = EDApi.findModule("h5");
206 | const flx = EDApi.findModule("flex");
207 |
208 | hiddenChannelNotif.className = flx.flexCenter;
209 | hiddenChannelNotif.style.width = "100%";
210 |
211 | hiddenChannelNotif.innerHTML = `
212 |
213 |
This is a hidden channel.
214 | You cannot see the contents of this channel. However, you may see its name and topic.
215 | `;
216 |
217 | messagesWrapper.appendChild(hiddenChannelNotif);
218 | }
219 | });
220 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Form1.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace EnhancedDiscordUI
2 | {
3 | partial class EDInstaller
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows Form Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.components = new System.ComponentModel.Container();
32 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(EDInstaller));
33 | this.InstallButton = new System.Windows.Forms.Button();
34 | this.Title = new System.Windows.Forms.Label();
35 | this.UninstallButton = new System.Windows.Forms.Button();
36 | this.UpdateButton = new System.Windows.Forms.Button();
37 | this.InstallProgress = new System.Windows.Forms.ProgressBar();
38 | this.StatusLabel2 = new System.Windows.Forms.Label();
39 | this.StatusLabel = new System.Windows.Forms.Label();
40 | this.StatusCloseButton = new System.Windows.Forms.Button();
41 | this.StatusText = new System.Windows.Forms.TextBox();
42 | this.toolTip1 = new System.Windows.Forms.ToolTip(this.components);
43 | this.DevButton = new System.Windows.Forms.Button();
44 | this.CanaryButton = new System.Windows.Forms.Button();
45 | this.PTBButton = new System.Windows.Forms.Button();
46 | this.StableButton = new System.Windows.Forms.Button();
47 | this.ReinjectButton = new System.Windows.Forms.Button();
48 | this.pictureBox1 = new System.Windows.Forms.PictureBox();
49 | this.OpenFolderButton = new System.Windows.Forms.Button();
50 | this.BetaRadio = new System.Windows.Forms.RadioButton();
51 | ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
52 | this.SuspendLayout();
53 | //
54 | // InstallButton
55 | //
56 | this.InstallButton.Anchor = System.Windows.Forms.AnchorStyles.Top;
57 | this.InstallButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
58 | this.InstallButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
59 | this.InstallButton.ForeColor = System.Drawing.Color.WhiteSmoke;
60 | this.InstallButton.Location = new System.Drawing.Point(155, 127);
61 | this.InstallButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
62 | this.InstallButton.Name = "InstallButton";
63 | this.InstallButton.Size = new System.Drawing.Size(131, 30);
64 | this.InstallButton.TabIndex = 0;
65 | this.InstallButton.Text = "Install";
66 | this.toolTip1.SetToolTip(this.InstallButton, "Downloads and injects ED into your Discord client.");
67 | this.InstallButton.UseVisualStyleBackColor = true;
68 | this.InstallButton.Click += new System.EventHandler(this.InstallButton_Click);
69 | //
70 | // Title
71 | //
72 | this.Title.AutoSize = true;
73 | this.Title.Font = new System.Drawing.Font("Segoe UI Semibold", 18F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
74 | this.Title.ForeColor = System.Drawing.Color.WhiteSmoke;
75 | this.Title.Location = new System.Drawing.Point(131, 11);
76 | this.Title.Name = "Title";
77 | this.Title.Size = new System.Drawing.Size(255, 41);
78 | this.Title.TabIndex = 1;
79 | this.Title.Text = "EnhancedDiscord";
80 | //
81 | // UninstallButton
82 | //
83 | this.UninstallButton.Anchor = System.Windows.Forms.AnchorStyles.Top;
84 | this.UninstallButton.Enabled = false;
85 | this.UninstallButton.FlatAppearance.BorderColor = System.Drawing.Color.WhiteSmoke;
86 | this.UninstallButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
87 | this.UninstallButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
88 | this.UninstallButton.ForeColor = System.Drawing.Color.WhiteSmoke;
89 | this.UninstallButton.Location = new System.Drawing.Point(155, 162);
90 | this.UninstallButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
91 | this.UninstallButton.Name = "UninstallButton";
92 | this.UninstallButton.Size = new System.Drawing.Size(131, 30);
93 | this.UninstallButton.TabIndex = 2;
94 | this.UninstallButton.Text = "Uninstall";
95 | this.toolTip1.SetToolTip(this.UninstallButton, "Uninjects ED and prompts you to delete ED\'s files.");
96 | this.UninstallButton.UseVisualStyleBackColor = true;
97 | this.UninstallButton.Click += new System.EventHandler(this.UninstallButton_Click);
98 | //
99 | // UpdateButton
100 | //
101 | this.UpdateButton.Anchor = System.Windows.Forms.AnchorStyles.Top;
102 | this.UpdateButton.Enabled = false;
103 | this.UpdateButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
104 | this.UpdateButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
105 | this.UpdateButton.ForeColor = System.Drawing.Color.WhiteSmoke;
106 | this.UpdateButton.Location = new System.Drawing.Point(87, 197);
107 | this.UpdateButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
108 | this.UpdateButton.Name = "UpdateButton";
109 | this.UpdateButton.Size = new System.Drawing.Size(131, 30);
110 | this.UpdateButton.TabIndex = 3;
111 | this.UpdateButton.Text = "Update";
112 | this.toolTip1.SetToolTip(this.UpdateButton, "Replaces the ED files with the most recent ones.");
113 | this.UpdateButton.UseVisualStyleBackColor = true;
114 | this.UpdateButton.Click += new System.EventHandler(this.UpdateButton_Click);
115 | //
116 | // InstallProgress
117 | //
118 | this.InstallProgress.Location = new System.Drawing.Point(12, 218);
119 | this.InstallProgress.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
120 | this.InstallProgress.Name = "InstallProgress";
121 | this.InstallProgress.Size = new System.Drawing.Size(415, 23);
122 | this.InstallProgress.Style = System.Windows.Forms.ProgressBarStyle.Continuous;
123 | this.InstallProgress.TabIndex = 5;
124 | this.InstallProgress.Visible = false;
125 | //
126 | // StatusLabel2
127 | //
128 | this.StatusLabel2.AutoSize = true;
129 | this.StatusLabel2.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
130 | this.StatusLabel2.ForeColor = System.Drawing.Color.WhiteSmoke;
131 | this.StatusLabel2.Location = new System.Drawing.Point(9, 190);
132 | this.StatusLabel2.Name = "StatusLabel2";
133 | this.StatusLabel2.Size = new System.Drawing.Size(89, 19);
134 | this.StatusLabel2.TabIndex = 8;
135 | this.StatusLabel2.Text = "Lorem ipsum";
136 | this.StatusLabel2.Visible = false;
137 | //
138 | // StatusLabel
139 | //
140 | this.StatusLabel.AutoSize = true;
141 | this.StatusLabel.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
142 | this.StatusLabel.ForeColor = System.Drawing.Color.WhiteSmoke;
143 | this.StatusLabel.Location = new System.Drawing.Point(8, 167);
144 | this.StatusLabel.Name = "StatusLabel";
145 | this.StatusLabel.Size = new System.Drawing.Size(165, 28);
146 | this.StatusLabel.TabIndex = 9;
147 | this.StatusLabel.Text = "Installation failed.";
148 | this.StatusLabel.Visible = false;
149 | //
150 | // StatusCloseButton
151 | //
152 | this.StatusCloseButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
153 | this.StatusCloseButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
154 | this.StatusCloseButton.ForeColor = System.Drawing.Color.WhiteSmoke;
155 | this.StatusCloseButton.Location = new System.Drawing.Point(251, 128);
156 | this.StatusCloseButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
157 | this.StatusCloseButton.Name = "StatusCloseButton";
158 | this.StatusCloseButton.Size = new System.Drawing.Size(64, 30);
159 | this.StatusCloseButton.TabIndex = 10;
160 | this.StatusCloseButton.Text = "Close";
161 | this.StatusCloseButton.UseVisualStyleBackColor = true;
162 | this.StatusCloseButton.Visible = false;
163 | this.StatusCloseButton.Click += new System.EventHandler(this.StatusCloseButton_Click);
164 | //
165 | // StatusText
166 | //
167 | this.StatusText.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(31)))), ((int)(((byte)(36)))), ((int)(((byte)(36)))));
168 | this.StatusText.BorderStyle = System.Windows.Forms.BorderStyle.None;
169 | this.StatusText.Font = new System.Drawing.Font("Segoe UI", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
170 | this.StatusText.ForeColor = System.Drawing.Color.WhiteSmoke;
171 | this.StatusText.Location = new System.Drawing.Point(11, 68);
172 | this.StatusText.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
173 | this.StatusText.Multiline = true;
174 | this.StatusText.Name = "StatusText";
175 | this.StatusText.ReadOnly = true;
176 | this.StatusText.Size = new System.Drawing.Size(416, 26);
177 | this.StatusText.TabIndex = 11;
178 | this.StatusText.Text = "Make sure to launch Discord before installing!";
179 | this.StatusText.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
180 | //
181 | // DevButton
182 | //
183 | this.DevButton.Anchor = System.Windows.Forms.AnchorStyles.Top;
184 | this.DevButton.FlatAppearance.BorderSize = 2;
185 | this.DevButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
186 | this.DevButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
187 | this.DevButton.ForeColor = System.Drawing.Color.White;
188 | this.DevButton.Image = global::EnhancedDiscordUI.Properties.Resources.discord_dev_64;
189 | this.DevButton.ImageAlign = System.Drawing.ContentAlignment.TopCenter;
190 | this.DevButton.Location = new System.Drawing.Point(320, 98);
191 | this.DevButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
192 | this.DevButton.Name = "DevButton";
193 | this.DevButton.RightToLeft = System.Windows.Forms.RightToLeft.Yes;
194 | this.DevButton.Size = new System.Drawing.Size(91, 110);
195 | this.DevButton.TabIndex = 15;
196 | this.DevButton.Text = "Dev";
197 | this.DevButton.TextAlign = System.Drawing.ContentAlignment.BottomCenter;
198 | this.toolTip1.SetToolTip(this.DevButton, "Discord Development (aka Local.)");
199 | this.DevButton.UseVisualStyleBackColor = true;
200 | this.DevButton.Visible = false;
201 | this.DevButton.Click += new System.EventHandler(this.DevButton_Click);
202 | //
203 | // CanaryButton
204 | //
205 | this.CanaryButton.Anchor = System.Windows.Forms.AnchorStyles.Top;
206 | this.CanaryButton.FlatAppearance.BorderSize = 2;
207 | this.CanaryButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
208 | this.CanaryButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
209 | this.CanaryButton.ForeColor = System.Drawing.Color.Gold;
210 | this.CanaryButton.Image = global::EnhancedDiscordUI.Properties.Resources.discord_canary_64;
211 | this.CanaryButton.ImageAlign = System.Drawing.ContentAlignment.TopCenter;
212 | this.CanaryButton.Location = new System.Drawing.Point(224, 98);
213 | this.CanaryButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
214 | this.CanaryButton.Name = "CanaryButton";
215 | this.CanaryButton.RightToLeft = System.Windows.Forms.RightToLeft.Yes;
216 | this.CanaryButton.Size = new System.Drawing.Size(91, 110);
217 | this.CanaryButton.TabIndex = 14;
218 | this.CanaryButton.Text = "Canary";
219 | this.CanaryButton.TextAlign = System.Drawing.ContentAlignment.BottomCenter;
220 | this.toolTip1.SetToolTip(this.CanaryButton, "Discord Canary");
221 | this.CanaryButton.UseVisualStyleBackColor = true;
222 | this.CanaryButton.Visible = false;
223 | this.CanaryButton.Click += new System.EventHandler(this.CanaryButton_Click);
224 | //
225 | // PTBButton
226 | //
227 | this.PTBButton.Anchor = System.Windows.Forms.AnchorStyles.Top;
228 | this.PTBButton.FlatAppearance.BorderSize = 2;
229 | this.PTBButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
230 | this.PTBButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
231 | this.PTBButton.ForeColor = System.Drawing.Color.SteelBlue;
232 | this.PTBButton.Image = global::EnhancedDiscordUI.Properties.Resources.discord_stable_64;
233 | this.PTBButton.ImageAlign = System.Drawing.ContentAlignment.TopCenter;
234 | this.PTBButton.Location = new System.Drawing.Point(128, 98);
235 | this.PTBButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
236 | this.PTBButton.Name = "PTBButton";
237 | this.PTBButton.RightToLeft = System.Windows.Forms.RightToLeft.Yes;
238 | this.PTBButton.Size = new System.Drawing.Size(91, 110);
239 | this.PTBButton.TabIndex = 13;
240 | this.PTBButton.Text = "PTB";
241 | this.PTBButton.TextAlign = System.Drawing.ContentAlignment.BottomCenter;
242 | this.toolTip1.SetToolTip(this.PTBButton, "Discord PTB (Public Test Build)");
243 | this.PTBButton.UseVisualStyleBackColor = true;
244 | this.PTBButton.Visible = false;
245 | this.PTBButton.Click += new System.EventHandler(this.PTBButton_Click);
246 | //
247 | // StableButton
248 | //
249 | this.StableButton.Anchor = System.Windows.Forms.AnchorStyles.Top;
250 | this.StableButton.FlatAppearance.BorderSize = 2;
251 | this.StableButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
252 | this.StableButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
253 | this.StableButton.ForeColor = System.Drawing.Color.SteelBlue;
254 | this.StableButton.Image = global::EnhancedDiscordUI.Properties.Resources.discord_stable_64;
255 | this.StableButton.ImageAlign = System.Drawing.ContentAlignment.TopCenter;
256 | this.StableButton.Location = new System.Drawing.Point(32, 98);
257 | this.StableButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
258 | this.StableButton.Name = "StableButton";
259 | this.StableButton.RightToLeft = System.Windows.Forms.RightToLeft.Yes;
260 | this.StableButton.Size = new System.Drawing.Size(91, 110);
261 | this.StableButton.TabIndex = 12;
262 | this.StableButton.Text = "Stable";
263 | this.StableButton.TextAlign = System.Drawing.ContentAlignment.BottomCenter;
264 | this.toolTip1.SetToolTip(this.StableButton, "Normal version of Discord.");
265 | this.StableButton.UseVisualStyleBackColor = true;
266 | this.StableButton.Visible = false;
267 | this.StableButton.Click += new System.EventHandler(this.StableButton_Click);
268 | //
269 | // ReinjectButton
270 | //
271 | this.ReinjectButton.Anchor = System.Windows.Forms.AnchorStyles.Top;
272 | this.ReinjectButton.Enabled = false;
273 | this.ReinjectButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
274 | this.ReinjectButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
275 | this.ReinjectButton.ForeColor = System.Drawing.Color.WhiteSmoke;
276 | this.ReinjectButton.Location = new System.Drawing.Point(225, 197);
277 | this.ReinjectButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
278 | this.ReinjectButton.Name = "ReinjectButton";
279 | this.ReinjectButton.Size = new System.Drawing.Size(131, 30);
280 | this.ReinjectButton.TabIndex = 17;
281 | this.ReinjectButton.Text = "Reinject";
282 | this.toolTip1.SetToolTip(this.ReinjectButton, "Reinjects without changing your ED folder; useful after Discord updates.");
283 | this.ReinjectButton.UseVisualStyleBackColor = true;
284 | this.ReinjectButton.Click += new System.EventHandler(this.ReinjectButton_Click);
285 | //
286 | // pictureBox1
287 | //
288 | this.pictureBox1.Image = global::EnhancedDiscordUI.Properties.Resources.ed_og;
289 | this.pictureBox1.Location = new System.Drawing.Point(41, 12);
290 | this.pictureBox1.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
291 | this.pictureBox1.Name = "pictureBox1";
292 | this.pictureBox1.Size = new System.Drawing.Size(87, 50);
293 | this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
294 | this.pictureBox1.TabIndex = 4;
295 | this.pictureBox1.TabStop = false;
296 | //
297 | // OpenFolderButton
298 | //
299 | this.OpenFolderButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
300 | this.OpenFolderButton.Font = new System.Drawing.Font("Segoe UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
301 | this.OpenFolderButton.ForeColor = System.Drawing.Color.WhiteSmoke;
302 | this.OpenFolderButton.Location = new System.Drawing.Point(113, 128);
303 | this.OpenFolderButton.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
304 | this.OpenFolderButton.Name = "OpenFolderButton";
305 | this.OpenFolderButton.Size = new System.Drawing.Size(125, 30);
306 | this.OpenFolderButton.TabIndex = 16;
307 | this.OpenFolderButton.Text = "Open Folder";
308 | this.OpenFolderButton.UseVisualStyleBackColor = true;
309 | this.OpenFolderButton.Visible = false;
310 | this.OpenFolderButton.Click += new System.EventHandler(this.OpenFolderButton_Click);
311 | //
312 | // BetaRadio
313 | //
314 | this.BetaRadio.AutoSize = true;
315 | this.BetaRadio.Font = new System.Drawing.Font("Segoe UI", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
316 | this.BetaRadio.ForeColor = System.Drawing.SystemColors.ControlLightLight;
317 | this.BetaRadio.Location = new System.Drawing.Point(115, 95);
318 | this.BetaRadio.Name = "BetaRadio";
319 | this.BetaRadio.Size = new System.Drawing.Size(200, 27);
320 | this.BetaRadio.TabIndex = 18;
321 | this.BetaRadio.TabStop = true;
322 | this.BetaRadio.Text = "Opt-in to beta version";
323 | this.BetaRadio.UseVisualStyleBackColor = true;
324 | //
325 | // EDInstaller
326 | //
327 | this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
328 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
329 | this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(31)))), ((int)(((byte)(36)))), ((int)(((byte)(36)))));
330 | this.ClientSize = new System.Drawing.Size(439, 254);
331 | this.Controls.Add(this.BetaRadio);
332 | this.Controls.Add(this.ReinjectButton);
333 | this.Controls.Add(this.OpenFolderButton);
334 | this.Controls.Add(this.UninstallButton);
335 | this.Controls.Add(this.StatusCloseButton);
336 | this.Controls.Add(this.StatusLabel);
337 | this.Controls.Add(this.StatusLabel2);
338 | this.Controls.Add(this.InstallProgress);
339 | this.Controls.Add(this.pictureBox1);
340 | this.Controls.Add(this.UpdateButton);
341 | this.Controls.Add(this.Title);
342 | this.Controls.Add(this.InstallButton);
343 | this.Controls.Add(this.StatusText);
344 | this.Controls.Add(this.StableButton);
345 | this.Controls.Add(this.DevButton);
346 | this.Controls.Add(this.CanaryButton);
347 | this.Controls.Add(this.PTBButton);
348 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
349 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
350 | this.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
351 | this.Name = "EDInstaller";
352 | this.Text = "EnhancedDiscord Installer";
353 | ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
354 | this.ResumeLayout(false);
355 | this.PerformLayout();
356 |
357 | }
358 |
359 | #endregion
360 |
361 | private System.Windows.Forms.Button InstallButton;
362 | private System.Windows.Forms.Label Title;
363 | private System.Windows.Forms.Button UninstallButton;
364 | private System.Windows.Forms.Button UpdateButton;
365 | private System.Windows.Forms.PictureBox pictureBox1;
366 | private System.Windows.Forms.ProgressBar InstallProgress;
367 | private System.Windows.Forms.Label StatusLabel2;
368 | private System.Windows.Forms.Label StatusLabel;
369 | private System.Windows.Forms.Button StatusCloseButton;
370 | private System.Windows.Forms.TextBox StatusText;
371 | private System.Windows.Forms.ToolTip toolTip1;
372 | private System.Windows.Forms.Button StableButton;
373 | private System.Windows.Forms.Button PTBButton;
374 | private System.Windows.Forms.Button CanaryButton;
375 | private System.Windows.Forms.Button DevButton;
376 | private System.Windows.Forms.Button OpenFolderButton;
377 | private System.Windows.Forms.Button ReinjectButton;
378 | private System.Windows.Forms.RadioButton BetaRadio;
379 | }
380 | }
381 |
382 |
--------------------------------------------------------------------------------
/dom_shit.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 | const electron = require('electron');
4 |
5 | const mainProcessInfo = {
6 | originalNodeModulesPath: electron.ipcRenderer.sendSync('main-process-info', 'original-node-modules-path'),
7 | originalPreloadScript: electron.ipcRenderer.sendSync('main-process-info', 'original-preload-script')
8 | };
9 | const Module = require('module');
10 | Module.globalPaths.push(mainProcessInfo.originalNodeModulesPath);
11 | if (mainProcessInfo.originalPreloadScript) {
12 | process.electronBinding('command_line').appendSwitch('preload', mainProcessInfo.originalPreloadScript);
13 | // This hack is no longer needed due to context isolation having to be on
14 | //electron.contextBridge.exposeInMainWorld = (key, val) => window[key] = val; // Expose DiscordNative
15 | require(mainProcessInfo.originalPreloadScript);
16 | }
17 |
18 | //electron.ipcRenderer.sendSync('current-web-contents');
19 |
20 | //Get inject directory
21 | if (!process.env.injDir) process.env.injDir = __dirname;
22 |
23 | //set up global functions
24 | const c = {
25 | log: function(msg, plugin) {
26 | if (plugin && plugin.name)
27 | console.log(`%c[EnhancedDiscord] %c[${plugin.name}]`, 'color: red;', `color: ${plugin.color}`, msg);
28 | else console.log('%c[EnhancedDiscord]', 'color: red;', msg);
29 | },
30 | info: function(msg, plugin) {
31 | if (plugin && plugin.name)
32 | console.info(`%c[EnhancedDiscord] %c[${plugin.name}]`, 'color: red;', `color: ${plugin.color}`, msg);
33 | else console.info('%c[EnhancedDiscord]', 'color: red;', msg);
34 | },
35 | warn: function(msg, plugin) {
36 | if (plugin && plugin.name)
37 | console.warn(`%c[EnhancedDiscord] %c[${plugin.name}]`, 'color: red;', `color: ${plugin.color}`, msg);
38 | else console.warn('%c[EnhancedDiscord]', 'color: red;', msg);
39 | },
40 | error: function(msg, plugin) {
41 | if (plugin && plugin.name)
42 | console.error(`%c[EnhancedDiscord] %c[${plugin.name}]`, 'color: red;', `color: ${plugin.color}`, msg);
43 | else console.error('%c[EnhancedDiscord]', 'color: red;', msg);
44 | },
45 | sleep: function(ms) {
46 | return new Promise(resolve => {
47 | setTimeout(resolve, ms);
48 | });
49 | }
50 | };
51 |
52 | // config util
53 | window.ED = { plugins: {}, version: '2.8.1' };
54 | Object.defineProperty(ED, 'config', {
55 | get: function() {
56 | let conf;
57 | try{
58 | conf = require('./config.json');
59 | } catch (err) {
60 | if(err.code !== 'MODULE_NOT_FOUND')
61 | c.error(err);
62 | conf = {};
63 | }
64 | return conf;
65 | },
66 | set: function(newSets = {}) {
67 | let confPath;
68 | let bDelCache;
69 | try{
70 | confPath = require.resolve('./config.json');
71 | bDelCache = true;
72 | } catch (err) {
73 | if(err.code !== 'MODULE_NOT_FOUND')
74 | c.error(err);
75 | confPath = path.join(process.env.injDir, 'config.json');
76 | bDelCache = false;
77 | }
78 |
79 | try {
80 | fs.writeFileSync(confPath, JSON.stringify(newSets));
81 | if(bDelCache)
82 | delete require.cache[confPath];
83 | } catch(err) {
84 | c.error(err);
85 | }
86 | return this.config;
87 | }
88 | });
89 |
90 | function loadPlugin(plugin) {
91 | try {
92 | if (plugin.preload)
93 | console.log(`%c[EnhancedDiscord] %c[PRELOAD] %cLoading plugin %c${plugin.name}`, 'color: red;', 'color: yellow;', '', `color: ${plugin.color}`, `by ${plugin.author}...`);
94 | else console.log(`%c[EnhancedDiscord] %cLoading plugin %c${plugin.name}`, 'color: red;', '', `color: ${plugin.color}`, `by ${plugin.author}...`);
95 | plugin.load();
96 | } catch(err) {
97 | c.error(`Failed to load:\n${err.stack}`, plugin);
98 | }
99 | }
100 |
101 | ED.localStorage = window.localStorage;
102 |
103 | process.once('loaded', async () => {
104 | c.log(`v${ED.version} is running. Validating plugins...`);
105 |
106 | const pluginFiles = fs.readdirSync(path.join(process.env.injDir, 'plugins'));
107 | const plugins = {};
108 | for (const i in pluginFiles) {
109 | if (!pluginFiles[i].endsWith('.js') || pluginFiles[i].endsWith('.plugin.js')) continue;
110 | let p;
111 | const pName = pluginFiles[i].replace(/\.js$/, '');
112 | try {
113 | p = require(path.join(process.env.injDir, 'plugins', pName));
114 | if (typeof p.name !== 'string' || typeof p.load !== 'function') {
115 | throw new Error('Plugin must have a name and load() function.');
116 | }
117 | plugins[pName] = Object.assign(p, {id: pName});
118 | }
119 | catch (err) {
120 | c.warn(`Failed to load ${pluginFiles[i]}: ${err}\n${err.stack}`, p);
121 | }
122 | }
123 | for (const id in plugins) {
124 | if (!plugins[id] || !plugins[id].name || typeof plugins[id].load !== 'function') {
125 | c.info(`Skipping invalid plugin: ${id}`); delete plugins[id]; continue;
126 | }
127 | plugins[id].settings; // this will set default settings in config if necessary
128 | }
129 | ED.plugins = plugins;
130 | c.log(`Plugins validated.`);
131 |
132 | // work-around to wait for webpack
133 | while (true) {
134 | await c.sleep(100);
135 | if(electron.webFrame.top.context.window && electron.webFrame.top.context.window.webpackJsonp) break;
136 | };
137 |
138 | ED.webSocket = window._ws;
139 |
140 | /* Add helper functions that make plugins easy to create */
141 | window.req = electron.webFrame.top.context.window.webpackJsonp.push([[], {
142 | '__extra_id__': (module, exports, req) => module.exports = req
143 | }, [['__extra_id__']]]);
144 | delete window.req.m['__extra_id__'];
145 | delete window.req.c['__extra_id__'];
146 |
147 | window.findModule = EDApi.findModule;
148 | window.findModules = EDApi.findAllModules;
149 | window.findRawModule = EDApi.findRawModule;
150 | window.monkeyPatch = EDApi.monkeyPatch;
151 |
152 | while (!EDApi.findModule('dispatch'))
153 | await c.sleep(100);
154 |
155 | c.log(`Loading preload plugins...`);
156 | for (const id in plugins) {
157 | if (ED.config[id] && ED.config[id].enabled == false) continue;
158 | if (!plugins[id].preload) continue;
159 | loadPlugin(plugins[id]);
160 | }
161 |
162 | const d = {resolve: () => {}};
163 | window.monkeyPatch(window.findModule('dispatch'), 'dispatch', {before: b => {
164 | // modules seem to all be loaded when RPC server loads
165 | if (b.methodArguments[0].type === 'RPC_SERVER_READY') {
166 | window.findModule('dispatch').dispatch.unpatch();
167 | d.resolve();
168 | }
169 | }});
170 |
171 | await new Promise(resolve => {
172 | d.resolve = resolve;
173 | });
174 | c.log(`Modules done loading (${Object.keys(window.req.c).length})`);
175 |
176 | if (ED.config.bdPlugins) {
177 | try {
178 | await require('./bd_shit').setup();
179 | c.log(`Preparing BD plugins...`);
180 | for (const i in pluginFiles) {
181 | if (!pluginFiles[i].endsWith('.js') || !pluginFiles[i].endsWith('.plugin.js')) continue;
182 | let p;
183 | const pName = pluginFiles[i].replace(/\.js$/, '');
184 | try {
185 | p = require(path.join(process.env.injDir, 'plugins', pName));
186 | if (typeof p.name !== 'string' || typeof p.load !== 'function') {
187 | throw new Error('Plugin must have a name and load() function.');
188 | }
189 | plugins[pName] = Object.assign(p, {id: pName});
190 | }
191 | catch (err) {
192 | c.warn(`Failed to load ${pluginFiles[i]}: ${err}\n${err.stack}`, p);
193 | }
194 | }
195 | for (const id in plugins) {
196 | if (!plugins[id] || !plugins[id].name || typeof plugins[id].load !== 'function') {
197 | c.info(`Skipping invalid plugin: ${id}`); delete plugins[id]; continue;
198 | }
199 | }
200 | }
201 | catch (err) {
202 | c.warn(`Failed to load BD plugins support: ${err}\n${err.stack}`);
203 | }
204 | }
205 |
206 | c.log(`Loading plugins...`);
207 | for (const id in plugins) {
208 | if (ED.config[id] && ED.config[id].enabled == false) continue;
209 | if (plugins[id].preload) continue;
210 | if (ED.config[id] && ED.config[id].enabled !== true && plugins[id].disabledByDefault) {
211 | plugins[id].settings.enabled = false; continue;
212 | }
213 | loadPlugin(plugins[id]);
214 | }
215 |
216 |
217 | const ht = EDApi.findModule('hideToken');
218 | // prevent client from removing token from localstorage when dev tools is opened, or reverting your token if you change it
219 | EDApi.monkeyPatch(ht, 'hideToken', () => {});
220 | window.fixedShowToken = () => {
221 | // Only allow this to add a token, not replace it. This allows for changing of the token in dev tools.
222 | if (!ED.localStorage || ED.localStorage.getItem('token')) return;
223 | return ED.localStorage.setItem('token', '"' + ht.getToken() + '"');
224 | };
225 | EDApi.monkeyPatch(ht, 'showToken', window.fixedShowToken);
226 | if (!ED.localStorage.getItem('token') && ht.getToken())
227 | window.fixedShowToken(); // prevent you from being logged out for no reason
228 |
229 | // change the console warning to be more fun
230 | electron.ipcRenderer.invoke('custom-devtools-warning');
231 |
232 | // expose stuff for devtools
233 | Object.assign(electron.webFrame.top.context.window, {ED, EDApi, BdApi});
234 | });
235 |
236 |
237 |
238 | /* BD/ED joint api */
239 | window.EDApi = window.BdApi = class EDApi {
240 | static get React() { return this.findModuleByProps('createElement'); }
241 | static get ReactDOM() { return this.findModuleByProps('findDOMNode'); }
242 |
243 | static escapeID(id) {
244 | return id.replace(/^[^a-z]+|[^\w-]+/gi, '');
245 | }
246 |
247 | static injectCSS(id, css) {
248 | const style = document.createElement('style');
249 | style.id = this.escapeID(id);
250 | style.innerHTML = css;
251 | document.head.append(style);
252 | }
253 |
254 | static clearCSS(id) {
255 | const element = document.getElementById(this.escapeID(id));
256 | if (element) element.remove();
257 | }
258 |
259 | static linkJS(id, url) {
260 | return new Promise(resolve => {
261 | const script = document.createElement('script');
262 | script.id = this.escapeID(id);
263 | script.src = url;
264 | script.type = 'text/javascript';
265 | script.onload = resolve;
266 | document.head.append(script);
267 | });
268 | }
269 |
270 | static unlinkJS(id) {
271 | const element = document.getElementById(this.escapeID(id));
272 | if (element) element.remove();
273 | }
274 |
275 | static getPlugin(name) {
276 | const plugin = Object.values(ED.plugins).find(p => p.name == name);
277 | if (!plugin) return null;
278 | return plugin.bdplugin ? plugin.bdplugin : plugin;
279 | }
280 |
281 | static alert(title, body) {
282 | return this.showConfirmationModal(title, body, {cancelText: null});
283 | }
284 |
285 | /**
286 | * Shows a generic but very customizable confirmation modal with optional confirm and cancel callbacks.
287 | * @param {string} title - title of the modal
288 | * @param {(string|ReactElement|Array)} children - a single or mixed array of react elements and strings. Every string is wrapped in Discord's `Markdown` component so strings will show and render properly.
289 | * @param {object} [options] - options to modify the modal
290 | * @param {boolean} [options.danger=false] - whether the main button should be red or not
291 | * @param {string} [options.confirmText=Okay] - text for the confirmation/submit button
292 | * @param {string} [options.cancelText=Cancel] - text for the cancel button
293 | * @param {callable} [options.onConfirm=NOOP] - callback to occur when clicking the submit button
294 | * @param {callable} [options.onCancel=NOOP] - callback to occur when clicking the cancel button
295 | * @param {string} [options.key] - key used to identify the modal. If not provided, one is generated and returned
296 | * @returns {string} - the key used for this modal
297 | */
298 | static showConfirmationModal(title, content, options = {}) {
299 | const ModalActions = this.findModuleByProps('openModal', 'updateModal');
300 | const Markdown = this.findModuleByDisplayName('Markdown');
301 | const ConfirmationModal = this.findModuleByDisplayName('ConfirmModal');
302 | if (!ModalActions || !ConfirmationModal || !Markdown) return window.alert(content);
303 |
304 | const emptyFunction = () => {};
305 | const {onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = 'Okay', cancelText = 'Cancel', danger = false, key = undefined} = options;
306 |
307 | if (!Array.isArray(content)) content = [content];
308 | content = content.map(c => typeof(c) === 'string' ? this.React.createElement(Markdown, null, c) : c);
309 | return ModalActions.openModal(props => {
310 | return this.React.createElement(ConfirmationModal, Object.assign({
311 | header: title,
312 | red: danger,
313 | confirmText: confirmText,
314 | cancelText: cancelText,
315 | onConfirm: onConfirm,
316 | onCancel: onCancel
317 | }, props), content);
318 | }, {modalKey: key});
319 | }
320 |
321 | static loadPluginSettings(pluginName) {
322 | const pl = ED.plugins[pluginName];
323 | if (!pl) return null;
324 |
325 | if (!ED.config[pluginName]) {
326 | this.savePluginSettings(pluginName, pl.defaultSettings || {enabled: !pl.disabledByDefault});
327 | }
328 | return ED.config[pluginName];
329 | }
330 |
331 | static savePluginSettings(pluginName, data) {
332 | const pl = ED.plugins[pluginName];
333 | if (!pl) return null;
334 | ED.config[pluginName] = data;
335 | ED.config = ED.config; // eslint-disable-line no-self-assign
336 | }
337 |
338 | static loadData(pluginName, key) {
339 | const pl = ED.plugins[pluginName] || Object.values(ED.plugins).find(p => p.name === pluginName);
340 | if (!pl) return null;
341 | const id = pl.id;
342 |
343 | if (!ED.plugins[id]) return null;
344 | return this.loadPluginSettings(id)[key];
345 | }
346 |
347 | static saveData(pluginName, key, data) {
348 | const pl = ED.plugins[pluginName] || Object.values(ED.plugins).find(p => p.name === pluginName);
349 | if (!pl) return null;
350 | const id = pl.id;
351 |
352 | const obj = this.loadPluginSettings(id);
353 | obj[key] = data;
354 | return this.savePluginSettings(id, obj);
355 | }
356 |
357 | static getData(pluginName, key) {
358 | return this.loadData(pluginName, key);
359 | }
360 |
361 | static setData(pluginName, key, data) {
362 | this.saveData(pluginName, key, data);
363 | }
364 |
365 | static getInternalInstance(node) {
366 | if (!(node instanceof window.jQuery) && !(node instanceof Element)) return undefined;
367 | if (node instanceof window.jQuery) node = node[0];
368 | return node[Object.keys(node).find(k => k.startsWith('__reactInternalInstance'))];
369 | }
370 |
371 | static showToast(content, options = {}) {
372 | if (!document.querySelector('.toasts')) {
373 | const container = document.querySelector('.sidebar-2K8pFh + div') || null;
374 | const memberlist = container ? container.querySelector('.membersWrap-2h-GB4') : null;
375 | const form = container ? container.querySelector('form') : null;
376 | const left = container ? container.getBoundingClientRect().left : 310;
377 | const right = memberlist ? memberlist.getBoundingClientRect().left : 0;
378 | const width = right ? right - container.getBoundingClientRect().left : Math.max(document.documentElement.clientWidth, window.innerWidth || 0) - left - 240;
379 | const bottom = form ? form.offsetHeight : 80;
380 | const toastWrapper = document.createElement('div');
381 | toastWrapper.classList.add('toasts');
382 | toastWrapper.style.setProperty('left', left + 'px');
383 | toastWrapper.style.setProperty('width', width + 'px');
384 | toastWrapper.style.setProperty('bottom', bottom + 'px');
385 | document.querySelector('#app-mount').appendChild(toastWrapper);
386 | }
387 | const {type = '', icon = true, timeout = 3000} = options;
388 | const toastElem = document.createElement('div');
389 | toastElem.classList.add('toast');
390 | if (type) toastElem.classList.add('toast-' + type);
391 | if (type && icon) toastElem.classList.add('icon');
392 | toastElem.innerText = content;
393 | document.querySelector('.toasts').appendChild(toastElem);
394 | setTimeout(() => {
395 | toastElem.classList.add('closing');
396 | setTimeout(() => {
397 | toastElem.remove();
398 | if (!document.querySelectorAll('.toasts .toast').length) document.querySelector('.toasts').remove();
399 | }, 300);
400 | }, timeout);
401 | }
402 |
403 | static findModule(filter, silent = true) {
404 | const moduleName = typeof filter === 'string' ? filter : null;
405 | for (const i in window.req.c) {
406 | if (window.req.c.hasOwnProperty(i)) {
407 | const m = window.req.c[i].exports;
408 | if (m && m.__esModule && m.default && (moduleName ? m.default[moduleName] : filter(m.default))) return m.default;
409 | if (m && (moduleName ? m[moduleName] : filter(m))) return m;
410 | }
411 | }
412 | if (!silent) c.warn(`Could not find module ${module}.`, {name: 'Modules', color: 'black'});
413 | return null;
414 | }
415 |
416 | static findRawModule(filter, silent = true) {
417 | const moduleName = typeof filter === 'string' ? filter : null;
418 | for (const i in window.req.c) {
419 | if (window.req.c.hasOwnProperty(i)) {
420 | const m = window.req.c[i].exports;
421 | if (m && m.__esModule && m.default && (moduleName ? m.default[moduleName] : filter(m.default)))
422 | return window.req.c[i];
423 | if (m && (moduleName ? m[moduleName] : filter(m)))
424 | return window.req.c[i];
425 | }
426 | }
427 | if (!silent) c.warn(`Could not find module ${module}.`, {name: 'Modules', color: 'black'});
428 | return null;
429 | }
430 |
431 | static findAllModules(filter) {
432 | const moduleName = typeof filter === 'string' ? filter : null;
433 | const modules = [];
434 | for (const i in window.req.c) {
435 | if (window.req.c.hasOwnProperty(i)) {
436 | const m = window.req.c[i].exports;
437 | if (m && m.__esModule && m.default && (moduleName ? m.default[moduleName] : filter(m.default))) modules.push(m.default);
438 | else if (m && (moduleName ? m[moduleName] : filter(m))) modules.push(m);
439 | }
440 | }
441 | return modules;
442 | }
443 |
444 | static findModuleByProps(...props) {
445 | return this.findModule(module => props.every(prop => module[prop] !== undefined));
446 | }
447 |
448 | static findModuleByDisplayName(name) {
449 | return this.findModule(module => module.displayName === name);
450 | }
451 |
452 | static monkeyPatch(what, methodName, options) {
453 | if (typeof options === 'function') {
454 | const newOptions = {instead: options, silent: true};
455 | options = newOptions;
456 | }
457 | const {before, after, instead, once = false, silent = false, force = false} = options;
458 | const displayName = options.displayName || what.displayName || what.name || what.constructor ? (what.constructor.displayName || what.constructor.name) : null;
459 | if (!silent) console.log(`%c[EnhancedDiscord] %c[Modules]`, 'color: red;', `color: black;`, `Patched ${methodName} in module ${displayName || ''}:`, what); // eslint-disable-line no-console
460 | if (!what[methodName]) {
461 | if (force) what[methodName] = function() {};
462 | else return console.warn(`%c[EnhancedDiscord] %c[Modules]`, 'color: red;', `color: black;`, `Method ${methodName} doesn't exist in module ${displayName || ''}`, what); // eslint-disable-line no-console
463 | }
464 | const origMethod = what[methodName];
465 | const cancel = () => {
466 | if (!silent) console.log(`%c[EnhancedDiscord] %c[Modules]`, 'color: red;', `color: black;`, `Unpatched ${methodName} in module ${displayName || ''}:`, what); // eslint-disable-line no-console
467 | what[methodName] = origMethod;
468 | };
469 | what[methodName] = function() {
470 | const data = {
471 | thisObject: this,
472 | methodArguments: arguments,
473 | cancelPatch: cancel,
474 | originalMethod: origMethod,
475 | callOriginalMethod: () => data.returnValue = data.originalMethod.apply(data.thisObject, data.methodArguments)
476 | };
477 | if (instead) {
478 | const tempRet = EDApi.suppressErrors(instead, '`instead` callback of ' + what[methodName].displayName)(data);
479 | if (tempRet !== undefined) data.returnValue = tempRet;
480 | }
481 | else {
482 | if (before) EDApi.suppressErrors(before, '`before` callback of ' + what[methodName].displayName)(data);
483 | data.callOriginalMethod();
484 | if (after) EDApi.suppressErrors(after, '`after` callback of ' + what[methodName].displayName)(data);
485 | }
486 | if (once) cancel();
487 | return data.returnValue;
488 | };
489 | what[methodName].__monkeyPatched = true;
490 | what[methodName].displayName = 'patched ' + (what[methodName].displayName || methodName);
491 | what[methodName].unpatch = cancel;
492 | return cancel;
493 | }
494 |
495 | static testJSON(data) {
496 | try {
497 | return JSON.parse(data);
498 | }
499 | catch (err) {
500 | return false;
501 | }
502 | }
503 |
504 | static suppressErrors(method, description) {
505 | return (...params) => {
506 | try { return method(...params); }
507 | catch (e) { console.error('Error occurred in ' + description, e); }
508 | };
509 | }
510 |
511 | static formatString(string, values) {
512 | for (const val in values) {
513 | string = string.replace(new RegExp(`\\{\\{${val}\\}\\}`, 'g'), values[val]);
514 | }
515 | return string;
516 | }
517 |
518 | static isPluginEnabled(name) {
519 | const plugins = Object.values(ED.plugins);
520 | const plugin = plugins.find(p => p.id == name || p.name == name);
521 | if (!plugin) return false;
522 | return !(plugin.settings.enabled === false);
523 | }
524 |
525 | static isThemeEnabled() {
526 | return false;
527 | }
528 |
529 | static isSettingEnabled(id) {
530 | return ED.config[id];
531 | }
532 | };
533 |
534 | window.BdApi.Plugins = new class AddonAPI {
535 |
536 | get folder() {return path.join(process.env.injDir, 'plugins');}
537 |
538 | isEnabled(name) {
539 | const plugins = Object.values(ED.plugins);
540 | const plugin = plugins.find(p => p.id == name || p.name == name);
541 | if (!plugin) return false;
542 | return !(plugin.settings.enabled === false);
543 | }
544 |
545 | enable(name) {
546 | const plugin = ED.plugins[name];
547 | if (!plugin || plugin.settings.enabled !== false) return;
548 | plugin.settings.enabled = true;
549 | ED.plugins[name].settings = plugin.settings;
550 | plugin.load();
551 | }
552 |
553 | disable(name) {
554 | const plugin = ED.plugins[name];
555 | if (!plugin || plugin.settings.enabled === false) return;
556 | plugin.settings.enabled = false;
557 | ED.plugins[name].settings = plugin.settings;
558 | plugin.unload();
559 | }
560 |
561 | toggle(name) {
562 | if (this.isEnabled(name)) this.disable(name);
563 | else this.enable(name);
564 | }
565 |
566 | reload(name) {
567 | const plugin = ED.plugins[name];
568 | if (!plugin) return;
569 | plugin.reload();
570 | }
571 |
572 | get(name) {
573 | return ED.plugins[name] || null;
574 | }
575 |
576 | getAll() {
577 | return Object.values(ED.plugins);
578 | }
579 | };
580 |
581 | window.BdApi.Themes = new class AddonAPI {
582 | get folder() {return '';}
583 | isEnabled() {}
584 | enable() {}
585 | disable() {}
586 | toggle() {}
587 | reload() {}
588 | get() {return null;}
589 | getAll() {return [];}
590 | };
--------------------------------------------------------------------------------
/plugins/ed_settings.js:
--------------------------------------------------------------------------------
1 | const Plugin = require('../plugin');
2 | const BD = require('../bd_shit');
3 |
4 | const edSettingsID = require("path").parse(__filename).name;
5 |
6 | module.exports = new Plugin({
7 | name: 'ED Settings (React)',
8 | author: 'jakuski#9191',
9 | description: 'Adds an EnhancedDiscord tab in user settings.',
10 | color: 'darkred',
11 | async load () {
12 | const discordConstants = EDApi.findModule("API_HOST");
13 | const UserSettings = module.exports.utils.getComponentFromFluxContainer(
14 | EDApi.findModule('getUserSettingsSections').default
15 | );
16 |
17 | if (!ED.classMaps) {
18 | ED.classMaps = {};
19 | }
20 |
21 | if (!ED.discordComponents) {
22 | ED.discordComponents = {};
23 | }
24 |
25 | this._initClassMaps(ED.classMaps);
26 | this._initDiscordComponents(ED.discordComponents);
27 | this.components = this._initReactComponents();
28 | this.settingsSections = this._getDefaultSections(); // Easily allow plugins to add in their own sections if need be.
29 |
30 | this.unpatch = EDApi.monkeyPatch(
31 | UserSettings.prototype,
32 | "generateSections",
33 | data => {
34 | const sections = data.originalMethod.call(data.thisObject);
35 | // We use the devIndex as a base so that should Discord add more sections later on, our sections shouldn't move possibly fucking up the UI.
36 | const devIndex = this._getDevSectionIndex(sections, discordConstants);
37 | let workingIndex = devIndex + 2;
38 |
39 | sections.splice(workingIndex, 0, ...this.settingsSections);
40 | workingIndex += this.settingsSections.length;
41 |
42 | const customSections = this._getPluginSections();
43 | if (customSections.length) {
44 | sections.splice(workingIndex, 0, ...customSections);
45 | workingIndex += customSections.length;
46 | }
47 |
48 | sections.splice(workingIndex, 0, { section: "DIVIDER" });
49 |
50 | return sections;
51 | }
52 | )
53 | },
54 | unload () {
55 | if (this.unpatch && typeof this.unpatch === "function") this.unpatch();
56 | },
57 | utils: {
58 | join (...args) {
59 | return args.join(" ")
60 | },
61 | getComponentFromFluxContainer (component) {
62 | return (new component({})).render().type;
63 | },
64 | shouldPluginRender (id) {
65 | let shouldRender = true;
66 |
67 | // BetterDiscord plugins settings render in their own modal activated in their listing.
68 | if (ED.plugins[id].getSettingsPanel && typeof ED.plugins[id].getSettingsPanel == 'function') {
69 | shouldRender = false;
70 | }
71 |
72 | if (ED.plugins[id].settings.enabled === false || !ED.plugins[id].generateSettings) {
73 | shouldRender = false;
74 | }
75 |
76 | return shouldRender;
77 | }
78 | },
79 | _getDevSectionIndex(sections, constants) {
80 | const indexOf = sections.indexOf(
81 | sections.find(sect => sect.section === constants.UserSettingsSections.DEVELOPER_OPTIONS)
82 | );
83 |
84 | if (indexOf !== -1) return indexOf;
85 | else return 28; // Hardcoded index fallback incase Discord mess with something
86 | },
87 | _getDefaultSections() {
88 | /*
89 |
90 | For future reference:
91 |
92 | normal sections / pages
93 | section: [string] an id string of some sort, must be unique.
94 | label: [string] self-explanatory
95 | element: [optional] [react-renderable] the page that will be rendered on the right when the section is clicked
96 | color: [optional] [string (hex)] a colour to be applied to the section (see the log out / discord nitro btn)
97 | onClick: [optional] [function] a function to be executed whenever the element is clicked
98 |
99 | special sections
100 | headers
101 | section: "HEADER"
102 | label: [string]
103 | divider
104 | section: "DIVIDER"
105 | custom element
106 | section: "CUSTOM"
107 | element: [react-renderable]
108 |
109 | all sections regardless of type can have the following
110 | predicate: [function => boolean] determine whether the section should be shown
111 |
112 | */
113 | return [{
114 | section: "CUSTOM",
115 | element: () => {
116 | const { join } = module.exports.utils;
117 | const { header } = EDApi.findModule("topPill");
118 |
119 | return EDApi.React.createElement("div", { className: join(header, "ed-settings") }, "EnhancedDiscord")
120 | }
121 | },{
122 | section: "ED/Plugins",
123 | label: "Plugins",
124 | element: this.components.PluginsPage
125 | },{
126 | section: "ED/Settings",
127 | label: "Settings",
128 | element: this.components.SettingsPage
129 | }];
130 | },
131 | _getPluginSections() {
132 | const arr = [];
133 | for (const key in ED.plugins) {
134 | if (typeof ED.plugins[key].generateSettingsSection !== 'function') continue;
135 |
136 | const label = ED.plugins[key].settingsSectionName || ED.plugins[key].name;
137 | arr.push({
138 | section: 'ED/'+key,
139 | label,
140 | element: () => EDApi.React.createElement(this.components.PluginSection, {id: key, label})
141 | });
142 | }
143 | return arr;
144 | },
145 | _initClassMaps(obj) {
146 | const divM = EDApi.findModule(m => m.divider && Object.keys(m).length === 1)
147 | obj.headers = EDApi.findModule('defaultMarginh2');
148 | obj.margins = EDApi.findModule('marginBottom8');
149 | obj.divider = divM ? divM.divider : '';
150 | obj.checkbox = EDApi.findModule('checkboxEnabled');
151 | obj.buttons = EDApi.findModule('lookFilled');
152 | obj.switch = EDApi.findModule('switch');
153 | obj.alignment = EDApi.findModule('horizontalReverse');
154 | obj.description = EDApi.findModule('formText');
155 | // New
156 | obj.shadows = EDApi.findModule("elevationHigh");
157 | },
158 | _initDiscordComponents(obj) {
159 | obj.Textbox = EDApi.findModuleByDisplayName("TextInput");
160 | obj.Select = EDApi.findModuleByDisplayName("SelectTempWrapper");
161 | obj.RadioGroup = EDApi.findModuleByDisplayName("RadioGroup");
162 | obj.Title = EDApi.findModuleByDisplayName("FormTitle");
163 | obj.Text = EDApi.findModuleByDisplayName("FormText");
164 | obj.FormSection = EDApi.findModuleByDisplayName("FormSection");
165 | obj.Icon = EDApi.findModuleByDisplayName("Icon");
166 | obj.LoadingSpinner = EDApi.findModuleByDisplayName("Spinner");
167 | obj.Card = EDApi.findModuleByDisplayName("FormNotice");
168 | obj.Flex = EDApi.findModuleByDisplayName("Flex");
169 | obj.Switch = EDApi.findModuleByDisplayName("Switch");
170 | obj.SwitchItem = EDApi.findModuleByDisplayName("SwitchItem");
171 | obj.Slider = EDApi.findModuleByDisplayName("Slider");
172 | obj.Select = EDApi.findModuleByDisplayName("SelectTempWrapper");
173 | obj.Tooltip = EDApi.findModuleByDisplayName("Tooltip");
174 | obj.Button = EDApi.findModule("Sizes");
175 | //obj.ColorPicker = EDApi.findModuleByDisplayName("ColorPicker");
176 |
177 | /*
178 | Props: any valid props you can apply to a div element
179 | */
180 | obj.Divider = props => {
181 | props.className = props.className ? props.className + " " + ED.classMaps.divider : ED.classMaps.divider
182 | return EDApi.React.createElement("div", Object.assign({}, props))
183 | }
184 | },
185 | _initReactComponents () {
186 | const { createElement:e, Component, Fragment, useState, useEffect, useReducer, createRef, isValidElement } = EDApi.React;
187 | const { FormSection, Divider, Flex, Switch, Title, Text, Button, SwitchItem, Textbox, RadioGroup, Select, Slider /*, ColorPicker*/ } = ED.discordComponents;
188 | const { margins } = ED.classMaps;
189 | const { join } = module.exports.utils;
190 |
191 | const PluginsPage = () => {
192 | return e(FormSection, {title: "EnhancedDiscord Plugins", tag: "h2"},
193 | e(Flex, {},
194 | e(OpenPluginDirBtn)
195 | ),
196 | e(Divider, {className: join(margins.marginTop20, margins.marginBottom20)}),
197 | Object
198 | .keys(ED.plugins)
199 | .map(id => e(PluginListing, {id, plugin: ED.plugins[id]}))
200 | )
201 | }
202 |
203 | const SettingsPage = () => {
204 | return e(Fragment, null,
205 | e(FormSection, {title: "EnhancedDiscord Settings", tag: "h2"},
206 | e(BDPluginToggle),
207 | Object
208 | .keys(ED.plugins)
209 | .filter(module.exports.utils.shouldPluginRender)
210 | .map((id, index) => e(PluginSettings, {id, index})),
211 | )
212 | )
213 | }
214 |
215 | class PluginListing extends Component {
216 | constructor(props) {
217 | super(props);
218 |
219 | this.state = {
220 | reloadBtnText: "Reload"
221 | }
222 |
223 | this.canBeManagedByUser = this.canBeManagedByUser.bind(this);
224 | this.isPluginEnabled = this.isPluginEnabled.bind(this);
225 | this.handleReload = this.handleReload.bind(this);
226 | this.handleToggle = this.handleToggle.bind(this);
227 | this.handleLoad = this.handleLoad.bind(this);
228 | this.handleUnload = this.handleUnload.bind(this);
229 | }
230 | isPluginEnabled() {
231 | return this.props.plugin.settings.enabled !== false;
232 | }
233 | canBeManagedByUser () {
234 | return !(this.props.id === edSettingsID)
235 | }
236 | handleReload () {
237 | this.setState({reloadBtnText: "Reloading..."});
238 | try {
239 | this.props.plugin.reload();
240 | this.setState({reloadBtnText: "Reloaded!"});
241 | } catch (err) {
242 | module.exports.error(err);
243 | this.setState({reloadBtnText: `Failed to reload (${err.name} - see console.)`})
244 | }
245 |
246 | setTimeout(
247 | setState => setState({reloadBtnText: "Reload"}),
248 | 3000,
249 | this.setState.bind(this)
250 | )
251 | }
252 | handleToggle(newStatus) {
253 | if (newStatus) this.handleLoad();
254 | else this.handleUnload();
255 |
256 | this.forceUpdate();
257 | }
258 | handleLoad () {
259 | if (this.isPluginEnabled()) return;
260 |
261 | this.props.plugin.settings.enabled = true;
262 | ED.plugins[this.props.id].settings = this.props.plugin.settings;
263 | this.props.plugin.load();
264 | }
265 | handleUnload () {
266 | if (!this.isPluginEnabled()) return;
267 |
268 | this.props.plugin.settings.enabled = false;
269 | ED.plugins[this.props.id].settings = this.props.plugin.settings;
270 | this.props.plugin.unload();
271 | }
272 | render () {
273 | const { plugin } = this.props;
274 | const { MarginRight, ColorBlob } = this.constructor;
275 |
276 | return e(Fragment, null,
277 | e(Flex, { direction: Flex.Direction.VERTICAL},
278 | e(Flex, {align: Flex.Align.CENTER},
279 | e(Title, {tag: "h3", className: ""}, plugin.name),
280 | e(ColorBlob, {color: plugin.color || "orange"}),
281 | e(MarginRight, null,
282 | e(Button, {size: Button.Sizes.NONE, onClick: this.handleReload}, this.state.reloadBtnText)
283 | ),
284 | this.canBeManagedByUser() && e(Switch, {checked: this.isPluginEnabled(), onChange: this.handleToggle})
285 | ),
286 | e(Text, {type: Text.Types.DESCRIPTION},
287 | VariableTypeRenderer.render(plugin.description)
288 | )
289 | ),
290 | e(Divider, {className: join(margins.marginTop20, margins.marginBottom20)})
291 | )
292 | }
293 | }
294 |
295 | PluginListing.MarginRight = props => e("div", {style:{marginRight: "10px"}}, props.children);
296 | PluginListing.ColorBlob = props => e("div", {style: {
297 | backgroundColor: props.color,
298 | boxShadow: `0 0 5px 2px ${props.color}`,
299 | borderRadius: "50%",
300 | height: "10px",
301 | width: "10px",
302 | marginRight: "8px"
303 | }});
304 |
305 | const PluginSettings = props => {
306 | const plugin = ED.plugins[props.id];
307 |
308 | return e(Fragment, null,
309 | e(Divider, { style:{ marginTop: props.index === 0 ? "0px" : undefined}, className: join(margins.marginTop8, margins.marginBottom20)}),
310 | e(Title, {tag: "h2"}, plugin.name),
311 | VariableTypeRenderer.render(
312 | plugin.generateSettings(),
313 | plugin.settingsListeners,
314 | props.id
315 | )
316 | )
317 | }
318 |
319 | const PluginSection = props => {
320 | const plugin = ED.plugins[props.id];
321 |
322 | return e(Fragment, null,
323 | e(Title, {tag: "h2"}, props.label),
324 | VariableTypeRenderer.render(
325 | plugin.generateSettingsSection(),
326 | plugin.settingsListeners,
327 | props.id
328 | )
329 | )
330 | }
331 |
332 | const BDPluginToggle = () => {
333 | const [ enabled, setEnabled ] = useState(ED.config.bdPlugins);
334 |
335 | useEffect(() => {
336 | if (enabled === ED.config.bdPlugins) return; // Prevent unneccesary file write
337 |
338 | ED.config.bdPlugins = enabled;
339 | ED.config = ED.config;
340 | });
341 |
342 | return e(SwitchItem, {
343 | onChange: () => setEnabled(!enabled),
344 | value: enabled,
345 | hideBorder: true,
346 | note: "Allows EnhancedDiscord to load BetterDiscord plugins natively. Reload (ctrl+r) for changes to take effect."
347 | },
348 | "BetterDiscord Plugins"
349 | );
350 | }
351 |
352 | const OpenPluginDirBtn = () => {
353 | const [ string, setString ] = useState("Open Plugins Directory");
354 |
355 | return e(Button, {size: Button.Sizes.SMALL, color: Button.Colors.GREEN, style: {margin: '0 5px', display: 'inline-block'}, onClick: e => {
356 | setString("Opening...");
357 | const sucess = require("electron").shell.openPath(
358 | e.shiftKey ?
359 | process.env.injDir :
360 | require("path").join(process.env.injDir, "plugins")
361 | );
362 |
363 | if (sucess) setString("Opened!");
364 | else setString("Failed to open...");
365 |
366 | setTimeout(() => {
367 | setString("Open Plugins Directory")
368 | }, 1500)
369 | }}, string)
370 | }
371 |
372 | class VariableTypeRenderer {
373 | static render (value, listeners, plugin) {
374 | const { DOMStringRenderer, HTMLElementInstanceRenderer } = this;
375 |
376 | let typeOf = null;
377 |
378 | if (typeof value === "string") typeOf = "domstring";
379 | if (isValidElement(value)) typeOf = "react";
380 | if (value instanceof HTMLElement) typeOf = "htmlelement-instance";
381 | if (Array.isArray(value)) typeOf = "auto";
382 | if (value == null) typeOf = "blank";
383 |
384 | if (typeOf === null) return module.exports.error("Unable to figure out how to render value ", value);
385 |
386 | switch(typeOf) {
387 | case "domstring": return e(DOMStringRenderer, {html: value, listeners});
388 | case "react": return value;
389 | case "htmlelement-instance": return e(HTMLElementInstanceRenderer, {instance: value});
390 | case "auto": return DiscordUIGenerator.render(value, plugin);
391 | case "blank": return e(Fragment);
392 | }
393 | }
394 | }
395 | VariableTypeRenderer.DOMStringRenderer = class DOMString extends Component {
396 | componentDidMount() {
397 | if (!this.props.listeners) return;
398 |
399 | this.props.listeners.forEach(listener => {
400 | document.querySelector(listener.el).addEventListener(listener.type, listener.eHandler)
401 | });
402 | }
403 | render () {
404 | return e("div", {dangerouslySetInnerHTML:{__html: this.props.html}});
405 | }
406 | }
407 | VariableTypeRenderer.HTMLElementInstanceRenderer = class HTMLElementInstance extends Component {
408 | constructor() {
409 | super();
410 | this.ref = createRef();
411 | }
412 | componentDidMount() {
413 | this.ref.current.appendChild(this.props.instance)
414 | }
415 | render () {
416 | return e("div", {ref: this.ref});
417 | }
418 | }
419 |
420 | const DiscordUIGenerator = {
421 | reactMarkdownRules: (() => {
422 | const simpleMarkdown = EDApi.findModule("markdownToReact");
423 | const rules = require("electron").webFrame.top.context.window._.clone(simpleMarkdown.defaultRules);
424 |
425 | rules.paragraph.react = (node, output, state) => {
426 | return e(Fragment, null, output(node.content, state))
427 | }
428 |
429 | rules.em.react = (node, output, state) => {
430 | return e("i", null, output(node.content, state))
431 | }
432 |
433 | rules.link.react = (node, output, state) => {
434 | return e("a", {
435 | href: node.target,
436 | target: "_blank",
437 | rel: "noreferrer noopener"
438 | }, output(node.content, state))
439 | }
440 |
441 | return rules;
442 | })(),
443 | render (ui, pluginID) {
444 | const { _types } = DiscordUIGenerator;
445 |
446 | return e("div", {},
447 | ui.map(element => {
448 | if (isValidElement(element)) return element;
449 |
450 | const component = _types[element.type];
451 | if (!component) return module.exports.error("[DiscordUIGenerator] Invalid element type:", element.type);
452 |
453 | return e(component, Object.assign({}, element, {pluginID}))
454 | })
455 | )
456 | },
457 | _parseMD (content) {
458 | const { reactMarkdownRules } = DiscordUIGenerator;
459 | const { markdownToReact } = EDApi.findModule("markdownToReact");
460 |
461 | return markdownToReact(content, reactMarkdownRules);
462 | },
463 | _loadData (props) {
464 | const plug = ED.plugins[props.pluginID];
465 | if (!plug) return;
466 | if (plug.customLoad) { // custom function for loading settings
467 | return plug.customLoad(props.configName)
468 | }
469 | return EDApi.loadData(props.pluginID, props.configName)
470 | },
471 | _saveData (props, data) {
472 | const plug = ED.plugins[props.pluginID];
473 | if (!plug) return;
474 | if (plug.customSave) { // custom function for saving settings
475 | return plug.customSave(props.configName, data)
476 | }
477 | const r = EDApi.saveData(props.pluginID, props.configName, data)
478 | if (plug.onSettingsUpdate) { // custom function to run after settings have updated
479 | plug.onSettingsUpdate(props.configName, data)
480 | }
481 | return r;
482 | },
483 | _cfgNameCheck(props, name) {
484 | if (!props.configName || typeof props.configName !== "string") {
485 | module.exports.error(`[DiscordUIGenerator] Input component (${name}) was not passed a configName value!`);
486 | throw new Error("Stopping react render. Please fix above error");
487 | }
488 | },
489 | _inputWrapper (props) {
490 | const { _parseMD } = DiscordUIGenerator;
491 |
492 | return e(Fragment, null,
493 | props.title && e(Title, { tag: "h5"}, props.title),
494 | props.children,
495 | props.desc && e(Text, {type: Text.Types.DESCRIPTION, className: join(margins.marginTop8, margins.marginBottom20)}, _parseMD(props.desc))
496 | )
497 | },
498 | _types: {
499 | "std:title": props => {
500 | return e(Title, { id: props.id, tag: props.tag || "h5" },
501 | props.content
502 | )
503 | },
504 | "std:description": props => {
505 | const {_parseMD} = DiscordUIGenerator;
506 |
507 | return e(Text, { id: props.id, type: props.descriptionType || "description" }, _parseMD(props.content))
508 | },
509 | "std:divider": props => {
510 | return e(Divider, {id: props.id, style: {marginTop: props.top, marginBottom: props.bottom}})
511 | },
512 | "std:spacer": props => {
513 | return e("div", {id: props.id, style: {marginBottom: props.space}})
514 | },
515 | "input:text": props => {
516 | const {_loadData: load, _saveData: save, _cfgNameCheck, _inputWrapper} = DiscordUIGenerator;
517 |
518 | _cfgNameCheck(props, "input:text");
519 |
520 | const [value, setValue] = useState(load(props));
521 |
522 | return e(_inputWrapper, {title: props.title, desc: props.desc},
523 | e(Textbox, {
524 | id: props.id,
525 | onChange: val => setValue(val),
526 | onBlur: e => save(props, e.currentTarget.value),
527 | value,
528 | placeholder: props.placeholder,
529 | size: props.mini ? "mini" : "default",
530 | disabled: props.disabled,
531 | type: props.number ? "number" : "text"
532 | })
533 | )
534 | },
535 | "input:button": props => {
536 | const [ string, setString ] = useState(props.name);
537 |
538 | return e(Button, {
539 | id: props.id,
540 | size: Button.Sizes[(props.size || '').toUpperCase() || 'SMALL'],
541 | color: Button.Colors[(props.color || '').toUpperCase() || 'BRAND'],
542 | look: Button.Looks[(props.look || '').toUpperCase() || 'FILLED'],
543 | style: {margin: '0 5px', display: 'inline-block'},
544 | disabled: props.disabled,
545 | onClick: () => props.onClick(setString)
546 | }, string)
547 | },
548 | "input:colorpicker": props => {
549 | // TODO: proper transparency support? would need to use different/modified component
550 | const {_loadData: load, _saveData: save, _cfgNameCheck, _inputWrapper} = DiscordUIGenerator;
551 |
552 | _cfgNameCheck(props, "input:text");
553 |
554 | const [value, setValue] = useState(load(props));
555 |
556 | return e("div",
557 | e(Title, { tag: "h5" }, props.title),
558 | /*e(ColorPicker, {
559 | onChange: value => {
560 | const hexValue = '#'+value.toString(16).padStart(6, '0');
561 | const inp = document.getElementById('ed_'+props.configName);
562 | if (inp) inp.value = hexValue;
563 | save(props, hexValue);
564 | },
565 | colors: props.colors || [],
566 | defaultColor: props.defaultColor,
567 | customColor: props.currentColor,
568 | value: props.currentColor
569 | }),*/
570 | e("div", {style: {marginBottom: 10}}),
571 | e(_inputWrapper, {desc: props.desc},
572 | e(Textbox, {
573 | id: 'ed_'+props.configName,
574 | onChange: val => setValue(val),
575 | onBlur: e => save(props, e.currentTarget.value),
576 | value,
577 | placeholder: props.placeholder,
578 | size: "mini",
579 | disabled: props.disabled
580 | })
581 | )
582 | )
583 | },
584 | "input:boolean": props => {
585 | const {_loadData: load, _saveData: save, _cfgNameCheck, _parseMD} = DiscordUIGenerator;
586 |
587 | _cfgNameCheck(props, "input:boolean");
588 |
589 | const [ enabled, toggle ] = useReducer((state) => {
590 | const newState = !state;
591 |
592 | save(props, newState);
593 |
594 | return newState;
595 | }, load(props))
596 |
597 | return e(SwitchItem, {
598 | id: props.id,
599 | onChange: toggle,
600 | value: enabled,
601 | hideBorder: props.hideBorder,
602 | note: _parseMD(props.note),
603 | disabled: props.disabled
604 | },
605 | props.title
606 | )
607 | },
608 | "input:radio": props => {
609 | const {_loadData: load, _saveData: save, _cfgNameCheck, _inputWrapper} = DiscordUIGenerator;
610 |
611 | _cfgNameCheck(props, "input:radio");
612 |
613 | const [ currentSetting, setSetting ] = useReducer((state, data) => {
614 | const newState = data.value;
615 |
616 | save(props, newState);
617 |
618 | return newState;
619 | }, load(props))
620 |
621 | return e(_inputWrapper, {title: props.title, desc: props.desc},
622 | e(RadioGroup, {
623 | id: props.id,
624 | onChange: setSetting,
625 | value: currentSetting,
626 | size: props.size ? props.size : "10px",
627 | disabled: props.disabled,
628 | options: props.options
629 | })
630 | )
631 | },
632 | "input:select": props => {
633 | const {_loadData: load, _saveData: save, _cfgNameCheck, _inputWrapper} = DiscordUIGenerator;
634 |
635 | _cfgNameCheck(props, "input:select");
636 |
637 | const [ currentSetting, setSetting ] = useReducer((state, data) => {
638 | const newState = data.value;
639 |
640 | save(props, newState);
641 |
642 | return newState;
643 | }, load(props))
644 |
645 | return e(_inputWrapper, {title: props.title, desc: props.desc},
646 | e(Select, {
647 | id: props.id,
648 | onChange: setSetting,
649 | value: currentSetting,
650 | disabled: props.disabled,
651 | options: props.options,
652 | searchable: props.searchable || false
653 | })
654 | )
655 | },
656 | "input:slider": props => {
657 | const {_loadData: load, _saveData: save, _cfgNameCheck, _inputWrapper} = DiscordUIGenerator;
658 |
659 | _cfgNameCheck(props, "input:slider");
660 |
661 | const [ currentSetting, setSetting ] = useReducer((state, data) => {
662 | const newState = data;
663 |
664 | save(props, newState);
665 |
666 | return newState;
667 | }, load(props) || props.defaultValue);
668 |
669 | const defaultOnValueRender = e => {
670 | return e.toFixed(0) + "%"
671 | }
672 |
673 | return e(_inputWrapper, {title: props.title, desc: props.desc},
674 | e(Slider, {
675 | id: props.id,
676 | onValueChange: setSetting,
677 | onValueRender: props.formatTooltip ? props.formatTooltip : defaultOnValueRender,
678 | initialValue: currentSetting,
679 | defaultValue: props.highlightDefaultValue ? props.defaultValue : null,
680 | minValue: props.minValue,
681 | maxValue: props.maxValue,
682 | disabled: props.disabled,
683 | markers: props.markers,
684 | stickToMarkers: props.stickToMarkers
685 | })
686 | )
687 | }
688 | }
689 | }
690 |
691 | return {
692 | PluginsPage,
693 | SettingsPage,
694 | PluginListing,
695 | PluginSettings,
696 | PluginSection,
697 | BDPluginToggle,
698 | OpenPluginDirBtn,
699 | __VariableTypeRenderer: VariableTypeRenderer,
700 | __DiscordUIGenerator: DiscordUIGenerator
701 | }
702 | }
703 | });
704 |
--------------------------------------------------------------------------------
/installer/EnhancedDiscordUI/Form1.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Data;
5 | using System.Drawing;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.Windows.Forms;
10 | using System.Diagnostics;
11 | using System.IO;
12 | using System.Net;
13 | using System.IO.Compression;
14 | using System.Runtime.InteropServices;
15 |
16 | namespace EnhancedDiscordUI
17 | {
18 | public partial class EDInstaller : Form
19 | {
20 | private Process stableProcess;
21 | private Process ptbProcess;
22 | private Process canaryProcess;
23 | private Process devProcess;
24 | private string operation = "INSTALL";
25 | private string platform;
26 | private string branch = "master";
27 |
28 | public EDInstaller()
29 | {
30 | Logger.MakeDivider();
31 | Logger.Log("Starting...");
32 | InitializeComponent();
33 | if (Directory.Exists("./EnhancedDiscord"))
34 | {
35 | UninstallButton.Enabled = true;
36 | UpdateButton.Enabled = true;
37 | ReinjectButton.Enabled = true;
38 | }
39 | }
40 | private void endInstallation(string reason, bool failed)
41 | {
42 | InstallProgress.Value = 100;
43 | BetaRadio.Hide();
44 | InstallButton.Hide();
45 | UninstallButton.Hide();
46 | UpdateButton.Hide();
47 | ReinjectButton.Hide();
48 | StatusText.Hide();
49 | StatusLabel.Show();
50 | StatusLabel.Text = operation == "UPDATE" ? "Update " + (failed ? "failed" : "complete") : (operation == "UNINSTALL" ? "Unin" : "In") + "stallation " + (failed ? " failed." : "completed!");
51 | StatusLabel.ForeColor = failed ? Color.Red : Color.Lime;
52 | StatusLabel2.Show();
53 | StatusLabel2.Text = reason;
54 | StatusCloseButton.Show();
55 | if (platform != "Linux")
56 | {
57 | OpenFolderButton.Show();
58 | }
59 | }
60 | private void InstallButton_Click(object sender, EventArgs e)
61 | {
62 | if (BetaRadio.Checked)
63 | {
64 | branch = "beta";
65 | }
66 | BetaRadio.Hide();
67 | InstallButton.Hide();
68 | UninstallButton.Hide();
69 | UpdateButton.Hide();
70 | ReinjectButton.Hide();
71 | StatusText.Show();
72 | InstallProgress.Show();
73 | StatusText.Text = "Finding Discord processes...";
74 |
75 | Process[] stable = Process.GetProcessesByName("Discord");
76 | Process[] canary = Process.GetProcessesByName("DiscordCanary");
77 | Process[] ptb = Process.GetProcessesByName("DiscordPtb");
78 | Process[] dev = Process.GetProcessesByName("DiscordDevelopment");
79 |
80 | List discordProcesses = new List();
81 | discordProcesses.AddRange(stable);
82 | discordProcesses.AddRange(canary);
83 | discordProcesses.AddRange(ptb);
84 | discordProcesses.AddRange(dev);
85 |
86 | if (discordProcesses.Count == 0)
87 | {
88 | endInstallation("No Discord processes found. Please open Discord and try again.", true); return;
89 | }
90 | List uniqueProcesses = new List();
91 | // First look for processes with unique filenames that have a title
92 | for (int i = 0; i < discordProcesses.Count; i++)
93 | {
94 | bool isUnique = true;
95 | for (int j = 0; j < uniqueProcesses.Count; j++)
96 | {
97 | if (uniqueProcesses[j].MainModule.FileName.Equals(discordProcesses[i].MainModule.FileName))
98 | {
99 | isUnique = false; break;
100 | }
101 | }
102 | if (!isUnique || discordProcesses[i].MainWindowTitle == "" || discordProcesses[i].MainWindowTitle.StartsWith("Developer Tools")) continue;
103 |
104 | uniqueProcesses.Add(discordProcesses[i]);
105 | }
106 | // Then look for all processes with unique filenames
107 | for (int i = 0; i < discordProcesses.Count; i++)
108 | {
109 | bool isUnique = true;
110 | for (int j = 0; j < uniqueProcesses.Count; j++)
111 | {
112 | if (uniqueProcesses[j].MainModule.FileName.Equals(discordProcesses[i].MainModule.FileName))
113 | {
114 | isUnique = false; break;
115 | }
116 | }
117 | if (!isUnique) continue;
118 | uniqueProcesses.Add(discordProcesses[i]);
119 | }
120 | StatusText.Text = "Found " + uniqueProcesses.Count + " Discord process" + (uniqueProcesses.Count == 1 ? "" : "es") + ".";
121 | InstallProgress.Value = 10;
122 | Process finalProcess = uniqueProcesses[0];
123 | if (uniqueProcesses.Count > 1)
124 | {
125 | // Enable selection buttons
126 | List