├── .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