├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── TODO.md ├── components ├── Plugin.jsx ├── PluginList.jsx ├── PluginSettings.jsx └── Settings.jsx ├── index.js ├── manifest.json ├── modules ├── AddonAPI.js ├── BDApi.js ├── BDV2.js ├── ContentManager.js ├── Patcher.js ├── PluginManager.js └── index.js └── style.css /.eslintignore: -------------------------------------------------------------------------------- 1 | /config 2 | /plugins 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint:recommended', 'intrnl'], 3 | env: { node: true }, 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config 2 | /plugins 3 | plugin.js 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bdCompat 2 | 3 | Compatibility layer for running BetterDiscord plugins in Powercord 4 | 5 | [![Screenshot showing a list of BetterDiscord plugins](https://i.imgur.com/xaAOdSE.png)](https://i.imgur.com/xaAOdSE.png) 6 | 7 | ## Installation 8 | 9 | Clone this repository to your Powercord install's plugins folder 10 | 11 | ``` 12 | git clone https://github.com/Juby210/bdCompat 13 | ``` 14 | 15 | ## Installing BD plugins 16 | 17 | 18 | 19 | - Put the plugin in the `plugins` folder, if it doesn't exist then create one. 20 | - Reload your Discord. 21 | - Go to User Settings and head to the `BetterDiscord Plugins` section 22 | - Enable the said plugin 23 | 24 | #### edCompat? ED plugins support? 25 | If you want EnhancedDiscord plugins support, you can use [EDPluginsLoader](https://github.com/Juby210/EDPluginsLoader) for BD. 26 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To do 2 | 3 | - [x] Revisit the idea of replacing the require function to allow directly requiring the plugin files without creating temporary files 4 | - [ ] Hot reloading of plugins 5 | - Should already be possible, just needs a function that unloads and loads, and a watcher. 6 | -------------------------------------------------------------------------------- /components/Plugin.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { shell: { openExternal } } = require('electron') 4 | 5 | const { React, i18n: { Messages } } = require('powercord/webpack') 6 | const { Tooltip, Switch, Button, Card, Divider } = require('powercord/components') 7 | const { open: openModal } = require('powercord/modal') 8 | 9 | const SettingsModal = require('./PluginSettings.jsx') 10 | 11 | let Details = () =>
Failed to load powercord module manager's details component!
12 | try { 13 | Details = require('../../../src/Powercord/coremods/moduleManager/components/parts/Details') 14 | } catch (e) { 15 | console.error('Failed to load powercord module manager\'s details component! Settings won\'t render correctly.', e) 16 | } 17 | 18 | module.exports = class Plugin extends React.Component { 19 | render () { 20 | this.props.enabled = this.props.meta.__started 21 | 22 | // We're reusing Powercord's plugin manager classes 23 | return ( 24 | 25 |
26 |

{this.props.plugin.getName()}

27 | 28 |
29 | this.togglePlugin()}/> 30 |
31 |
32 |
33 | 34 | 35 |
41 | 42 |
43 | 44 | 45 |
46 | {this.props.meta.source && 47 | 55 | } 56 | 57 | {this.props.meta.website && 58 | 66 | } 67 | 68 |
69 | 70 | {typeof this.props.plugin.getSettingsPanel === 'function' && 71 | 79 | } 80 | {typeof this.props.plugin.getSettingsPanel === 'function' && 81 |
82 | } 83 | 84 | 96 |
97 | 98 | ) 99 | } 100 | 101 | togglePlugin () { 102 | if (this.props.enabled) { 103 | this.props.onDisable() 104 | } else { 105 | this.props.onEnable() 106 | } 107 | 108 | this.forceUpdate() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /components/PluginList.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { shell: { openPath } } = require('electron') 4 | 5 | const { React } = require('powercord/webpack') 6 | const { Button } = require('powercord/components') 7 | const { TextInput } = require('powercord/components/settings') 8 | 9 | const Plugin = require('./Plugin.jsx') 10 | 11 | module.exports = class PluginList extends React.Component { 12 | constructor (props) { 13 | super(props) 14 | 15 | this.state = { 16 | search: '', 17 | } 18 | } 19 | render () { 20 | const plugins = this.__getPlugins() 21 | 22 | return ( 23 |
24 |
25 | 33 |
34 |
35 | this.setState({ search: val })} 38 | placeholder='What are you looking for?' 39 | > 40 | Search plugins 41 | 42 |
43 | 44 |
45 | {plugins.map((plugin) => 46 | this.props.pluginManager.enablePlugin(plugin.plugin.getName())} 51 | onDisable={() => this.props.pluginManager.disablePlugin(plugin.plugin.getName())} 52 | onDelete={() => this.__deletePlugin(plugin.plugin.getName())} 53 | /> 54 | )} 55 |
56 |
57 | ) 58 | } 59 | 60 | __getPlugins () { 61 | let plugins = Object.keys(window.bdplugins) 62 | .map((plugin) => window.bdplugins[plugin]) 63 | 64 | if (this.state.search !== '') { 65 | const search = this.state.search.toLowerCase() 66 | 67 | plugins = plugins.filter(({ plugin }) => 68 | plugin.getName().toLowerCase().includes(search) || 69 | plugin.getAuthor().toLowerCase().includes(search) || 70 | plugin.getDescription().toLowerCase().includes(search) 71 | ) 72 | } 73 | 74 | return plugins.sort((a, b) => { 75 | const nameA = a.plugin.getName().toLowerCase() 76 | const nameB = b.plugin.getName().toLowerCase() 77 | 78 | if (nameA < nameB) return -1 79 | if (nameA > nameB) return 1 80 | 81 | return 0 82 | }) 83 | } 84 | 85 | __deletePlugin(pluginName) { 86 | this.props.pluginManager.delete(pluginName) 87 | 88 | this.forceUpdate() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /components/PluginSettings.jsx: -------------------------------------------------------------------------------- 1 | const { React, getModuleByDisplayName } = require('powercord/webpack') 2 | const { Modal } = require('powercord/components/modal') 3 | const { close: closeModal } = require('powercord/modal') 4 | const { resolve } = require('path') 5 | 6 | const FormTitle = getModuleByDisplayName('FormTitle', false) 7 | 8 | let ErrorBoundary = props => props.children 9 | try { 10 | ErrorBoundary = require('../../../src/Powercord/coremods/settings/components/ErrorBoundary') 11 | } catch (e) { 12 | console.error('Failed to load powercord\'s ErrorBoundary component!', e) 13 | } 14 | 15 | module.exports = class PluginSettings extends React.Component { 16 | renderPluginSettings() { 17 | let panel 18 | try { 19 | panel = this.props.plugin.getSettingsPanel() 20 | } catch (e) { 21 | console.error(e) 22 | 23 | const error = (e.stack || e.toString()).split('\n') 24 | .filter(l => !l.includes('discordapp.com/assets/') && !l.includes('discord.com/assets/')) 25 | .join('\n') 26 | .split(resolve(__dirname, '..', '..')).join('') 27 | 28 | return
29 |
An error occurred while rendering settings panel.
30 | {error} 31 |
32 | } 33 | if (panel instanceof Node || typeof panel === 'string') { 34 | return
el ? panel instanceof Node ? el.append(panel) : el.innerHTML = panel : void 0}>
35 | } 36 | return typeof panel === 'function' ? React.createElement(panel) : panel 37 | } 38 | 39 | render () { 40 | const { plugin } = this.props 41 | 42 | return ( 43 | 44 | 45 | {plugin.getName()} Settings 46 | 47 | 48 | 49 |
50 | {this.renderPluginSettings()} 51 |
52 |
53 |
54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /components/Settings.jsx: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { React } = require('powercord/webpack') 4 | const { SwitchItem } = require('powercord/components/settings') 5 | 6 | const PluginList = require('./PluginList.jsx') 7 | 8 | module.exports = class Settings extends React.Component { 9 | constructor (props) { 10 | super(props) 11 | } 12 | 13 | render () { 14 | return ( 15 |
16 | this.props.toggleSetting('disableWhenStopFailed')}> 18 | Disable plugin when failed to stop 19 | 20 | 21 |
22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Plugin } = require('powercord/entities') 4 | const process = require('process') 5 | 6 | const { AddonAPI, BDApi, BDV2, ContentManager, PluginManager } = require('./modules') 7 | const Settings = require('./components/Settings') 8 | 9 | module.exports = class BDCompat extends Plugin { 10 | startPlugin () { 11 | this.loadStylesheet('style.css') 12 | this.defineGlobals() 13 | 14 | powercord.api.settings.registerSettings('bdCompat', { 15 | category: 'bdCompat', 16 | label: 'BetterDiscord Plugins', 17 | render: Settings 18 | }); 19 | } 20 | 21 | pluginWillUnload () { 22 | powercord.api.settings.unregisterSettings('bdCompat') 23 | if (window.pluginModule) window.pluginModule.destroy() 24 | if (window.ContentManager) window.ContentManager.destroy() 25 | this.destroyGlobals() 26 | } 27 | 28 | defineGlobals () { 29 | let webReq 30 | window.webpackChunkdiscord_app.push([ 31 | ['bdCompat'], 32 | {}, 33 | r => webReq = r 34 | ]) 35 | window.webpackJsonp = [] 36 | window.webpackJsonp.push = ([, mod, [[id]]]) => { 37 | return mod[id]({}, {}, webReq) 38 | } 39 | 40 | window.bdConfig = { dataPath: __dirname } 41 | window.settingsCookie = {} 42 | 43 | window.bdplugins = {} 44 | window.pluginCookie = {} 45 | window.bdpluginErrors = [] 46 | 47 | window.bdthemes = {} 48 | window.themeCookie = {} 49 | window.bdthemeErrors = [] 50 | 51 | // window.BdApi = BDApi 52 | 53 | // Orignally BdApi is an object, not a class 54 | window.BdApi = {} 55 | Object.getOwnPropertyNames(BDApi).filter(m => typeof BDApi[m] == 'function' || typeof BDApi[m] == 'object').forEach(m => { 56 | window.BdApi[m] = BDApi[m] 57 | }) 58 | window.Utils = { monkeyPatch: BDApi.monkeyPatch, suppressErrors: BDApi.suppressErrors, escapeID: BDApi.escapeID } 59 | 60 | window.BDV2 = BDV2 61 | window.ContentManager = new ContentManager 62 | window.pluginModule = new PluginManager(window.ContentManager.pluginsFolder, this.settings) 63 | 64 | // DevilBro's plugins checks whether or not it's running on ED 65 | // This isn't BetterDiscord, so we'd be better off doing this. 66 | // eslint-disable-next-line no-process-env 67 | process.env.injDir = __dirname 68 | 69 | window.BdApi.Plugins = new AddonAPI(window.bdplugins, window.pluginModule) 70 | window.BdApi.Themes = new AddonAPI({}, {}) 71 | 72 | this.log('Defined BetterDiscord globals') 73 | } 74 | 75 | destroyGlobals () { 76 | const globals = ['bdConfig', 'settingsCookie', 'bdplugins', 'pluginCookie', 'bdpluginErrors', 'bdthemes', 77 | 'themeCookie', 'bdthemeErrors', 'BdApi', 'Utils', 'BDV2', 'ContentManager', 'pluginModule', 'webpackJsonp'] 78 | 79 | globals.forEach(g => { 80 | delete window[g] 81 | }) 82 | 83 | // eslint-disable-next-line no-process-env 84 | delete process.env.injDir 85 | 86 | this.log('Destroyed BetterDiscord globals') 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BetterDiscord plugin compatibility (bdCompat)", 3 | "version": "0.3.12", 4 | "description": "Adds support for running BetterDiscord plugins", 5 | "author": "intrnl & Juby210", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /modules/AddonAPI.js: -------------------------------------------------------------------------------- 1 | module.exports = class AddonAPI { 2 | constructor(list, manager) { 3 | this.list = list 4 | this.manager = manager 5 | this.folder = manager.folder 6 | 7 | this.enable = this.manager.enable?.bind(this.manager) || (() => {}) 8 | this.disable = this.manager.disable?.bind(this.manager) || (() => {}) 9 | this.reload = this.manager.reload?.bind(this.manager) || (() => {}) 10 | } 11 | 12 | isEnabled(name) { 13 | const plugin = this.list[name] 14 | return plugin ? plugin.__started : false 15 | } 16 | 17 | toggle(name) { 18 | if (this.isEnabled(name)) return this.disable(name) 19 | this.enable(name) 20 | } 21 | 22 | get(name) { 23 | if (this.list[name]) { 24 | if (this.list[name].plugin) return this.list[name].plugin 25 | return this.list[name] 26 | } 27 | return null 28 | } 29 | 30 | getAll = () => Object.keys(this.list).map(k => this.get(k)).filter(a => a) 31 | } 32 | -------------------------------------------------------------------------------- /modules/BDApi.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | const crypto = require('crypto') 6 | 7 | const { getModule, getAllModules, getModuleByDisplayName, React, ReactDOM } = require('powercord/webpack') 8 | const { getOwnerInstance, getReactInstance } = require('powercord/util') 9 | const { inject, uninject } = require('powercord/injector') 10 | 11 | const PluginData = {} 12 | 13 | const Patcher = require('./Patcher') 14 | 15 | 16 | // __ is not part of BdApi entirely 17 | // _ is part of BD but not exactly in BdApi, but kept here anyway for easier maintain 18 | 19 | class BdApi { 20 | // React 21 | static get React () { 22 | return React 23 | } 24 | static get ReactDOM () { 25 | return ReactDOM 26 | } 27 | 28 | // Patcher 29 | static get Patcher() { 30 | return Patcher 31 | } 32 | 33 | 34 | // General 35 | static getCore () { 36 | return null 37 | } 38 | static escapeID (id) { 39 | return id.replace(/^[^a-z]+|[^\w-]+/giu, '') 40 | } 41 | 42 | static suppressErrors (method, message = '') { 43 | return (...params) => { 44 | try { 45 | return method(...params) 46 | } catch (err) { 47 | BdApi.__error(err, `Error occured in ${message}`) 48 | } 49 | } 50 | } 51 | 52 | static testJSON (data) { 53 | try { 54 | JSON.parse(data) 55 | 56 | return true 57 | } catch (err) { 58 | return false 59 | } 60 | } 61 | 62 | 63 | // Style tag 64 | static get __styleParent () { 65 | return BdApi.__elemParent('style') 66 | } 67 | 68 | static injectCSS (id, css) { 69 | const style = document.createElement('style') 70 | 71 | style.id = `bd-style-${BdApi.escapeID(id)}` 72 | style.innerHTML = css 73 | 74 | BdApi.__styleParent.append(style) 75 | } 76 | 77 | static clearCSS (id) { 78 | const elem = document.getElementById(`bd-style-${BdApi.escapeID(id)}`) 79 | if (elem) elem.remove() 80 | } 81 | 82 | 83 | // Script tag 84 | static get __scriptParent () { 85 | return BdApi.__elemParent('script') 86 | } 87 | 88 | static linkJS (id, url) { 89 | return new Promise((resolve) => { 90 | const script = document.createElement('script') 91 | 92 | script.id = `bd-script-${BdApi.escapeID(id)}` 93 | script.src = url 94 | script.type = 'text/javascript' 95 | script.onload = resolve 96 | 97 | BdApi.__scriptParent.append(script) 98 | }) 99 | } 100 | 101 | static unlinkJS (id) { 102 | const elem = document.getElementById(`bd-script-${BdApi.escapeID(id)}`) 103 | if (elem) elem.remove() 104 | } 105 | 106 | 107 | // Plugin data 108 | static get __pluginData () { 109 | return PluginData 110 | } 111 | 112 | static __getPluginConfigPath (pluginName) { 113 | return path.join(__dirname, '..', 'config', pluginName + '.config.json') 114 | } 115 | 116 | static __getPluginConfig (pluginName) { 117 | const configPath = BdApi.__getPluginConfigPath(pluginName) 118 | 119 | if (typeof BdApi.__pluginData[pluginName] === 'undefined') 120 | if (!fs.existsSync(configPath)) { 121 | BdApi.__pluginData[pluginName] = {} 122 | } else { 123 | try { 124 | BdApi.__pluginData[pluginName] = JSON.parse(fs.readFileSync(configPath)) 125 | } catch (e) { 126 | BdApi.__pluginData[pluginName] = {} 127 | BdApi.__warn(`${pluginName} has corrupted or empty config file, loaded as {}`) 128 | } 129 | } 130 | 131 | 132 | return BdApi.__pluginData[pluginName] 133 | } 134 | 135 | static __savePluginConfig (pluginName) { 136 | const configPath = BdApi.__getPluginConfigPath(pluginName) 137 | const configFolder = path.join(__dirname, '..', 'config/') 138 | 139 | if (!fs.existsSync(configFolder)) fs.mkdirSync(configFolder) 140 | fs.writeFileSync(configPath, JSON.stringify(BdApi.__pluginData[pluginName], null, 2)) 141 | } 142 | 143 | 144 | static loadData (pluginName, key) { 145 | const config = BdApi.__getPluginConfig(pluginName) 146 | 147 | return config[key] 148 | } 149 | 150 | static get getData () { 151 | return BdApi.loadData 152 | } 153 | 154 | 155 | static saveData (pluginName, key, value) { 156 | if (typeof value === 'undefined') return 157 | 158 | const config = BdApi.__getPluginConfig(pluginName) 159 | 160 | config[key] = value 161 | 162 | BdApi.__savePluginConfig(pluginName) 163 | } 164 | 165 | static get setData () { 166 | return BdApi.saveData 167 | } 168 | 169 | static deleteData (pluginName, key) { 170 | const config = BdApi.__getPluginConfig(pluginName) 171 | 172 | if (typeof config[key] === 'undefined') return 173 | delete config[key] 174 | 175 | BdApi.__savePluginConfig(pluginName) 176 | } 177 | 178 | 179 | // Plugin communication 180 | static getPlugin (name) { 181 | if (window.bdplugins[name]) return window.bdplugins[name].plugin 182 | } 183 | 184 | 185 | // Alerts and toasts 186 | static async alert(title, children) { 187 | return BdApi.showConfirmationModal(title, children, { cancelText: null }) 188 | } 189 | 190 | static async showConfirmationModal(title, children, options = {}) { 191 | const { onConfirm = () => {}, onCancel = () => {}, confirmText = 'Okay', cancelText = 'Cancel', danger = false, key } = options 192 | 193 | const { openModal } = await getModule(['openModal', 'updateModal']) 194 | const Markdown = await getModule(m => m.displayName === 'Markdown' && m.rules) 195 | const ConfirmModal = await getModuleByDisplayName('ConfirmModal') 196 | 197 | if (!Array.isArray(children)) children = [ children ] 198 | children = children.map(c => typeof c == 'string' ? React.createElement(Markdown, null, c) : c) 199 | return openModal(props => React.createElement(ConfirmModal, { 200 | header: title, red: danger, confirmText, cancelText, onConfirm, onCancel, ...props 201 | }, children), { modalKey: key }) 202 | } 203 | 204 | static showToast (content, options = {}) { 205 | const { type = '', icon = true, timeout = 3000 } = options 206 | 207 | const toastElem = document.createElement('div') 208 | toastElem.classList.add('bd-toast') 209 | toastElem.innerText = content 210 | 211 | if (type) toastElem.classList.add(`toast-${type}`) 212 | if (type && icon) toastElem.classList.add('icon') 213 | 214 | const toastWrapper = BdApi.__createToastWrapper() 215 | toastWrapper.appendChild(toastElem) 216 | 217 | setTimeout(() => { 218 | toastElem.classList.add('closing') 219 | 220 | setTimeout(() => { 221 | toastElem.remove() 222 | if (!document.querySelectorAll('.bd-toasts .bd-toast').length) toastWrapper.remove() 223 | }, 300) 224 | }, timeout) 225 | } 226 | 227 | static __createToastWrapper () { 228 | const toastWrapperElem = document.querySelector('.bd-toasts') 229 | 230 | if (!toastWrapperElem) { 231 | const DiscordElements = { 232 | settings: '.contentColumn-2hrIYH, .customColumn-Rb6toI', 233 | chat: '.chat-3bRxxu form', 234 | friends: '.container-3gCOGc', 235 | serverDiscovery: '.pageWrapper-1PgVDX', 236 | applicationStore: '.applicationStore-1pNvnv', 237 | gameLibrary: '.gameLibrary-TTDw4Y', 238 | activityFeed: '.activityFeed-28jde9', 239 | } 240 | 241 | const boundingElement = document.querySelector(Object.keys(DiscordElements).map((component) => DiscordElements[component]).join(', ')) 242 | 243 | const toastWrapper = document.createElement('div') 244 | toastWrapper.classList.add('bd-toasts') 245 | toastWrapper.style.setProperty('width', boundingElement ? boundingElement.offsetWidth + 'px' : '100%') 246 | toastWrapper.style.setProperty('left', boundingElement ? boundingElement.getBoundingClientRect().left + 'px' : '0px') 247 | toastWrapper.style.setProperty( 248 | 'bottom', 249 | (document.querySelector(DiscordElements.chat) ? document.querySelector(DiscordElements.chat).offsetHeight + 20 : 80) + 'px' 250 | ) 251 | 252 | document.querySelector('#app-mount > div[class^="app-"]').appendChild(toastWrapper) 253 | 254 | return toastWrapper 255 | } 256 | 257 | return toastWrapperElem 258 | } 259 | 260 | 261 | // Discord's internals manipulation and such 262 | static onRemoved (node, callback) { 263 | const observer = new MutationObserver((mutations) => { 264 | for (const mut in mutations) { 265 | const mutation = mutations[mut] 266 | const nodes = Array.from(mutation.removedNodes) 267 | 268 | const directMatch = nodes.indexOf(node) > -1 269 | const parentMatch = nodes.some((parent) => parent.contains(node)) 270 | 271 | if (directMatch || parentMatch) { 272 | observer.disconnect() 273 | 274 | return callback() 275 | } 276 | } 277 | }) 278 | 279 | observer.observe(document.body, { subtree: true, childList: true }) 280 | } 281 | 282 | static getInternalInstance (node) { 283 | if (!(node instanceof window.jQuery) && !(node instanceof Element)) return undefined // eslint-disable-line no-undefined 284 | if (node instanceof window.jQuery) node = node[0] // eslint-disable-line no-param-reassign 285 | 286 | return getOwnerInstance(node) 287 | } 288 | 289 | static findModule(filter) { 290 | return getModule(filter, false) 291 | } 292 | 293 | static findAllModules(filter) { 294 | return getAllModules(filter) 295 | } 296 | 297 | static findModuleByProps(...props) { 298 | return BdApi.findModule((module) => props.every((prop) => typeof module[prop] !== 'undefined')) 299 | } 300 | 301 | static findModuleByPrototypes(...protos) { 302 | return getModule(module => module.prototype && protos.every(proto => typeof module.prototype[proto] !== 'undefined'), false) 303 | } 304 | 305 | static findModuleByDisplayName(displayName) { 306 | return getModuleByDisplayName(displayName, false) 307 | } 308 | 309 | static monkeyPatch (what, methodName, options = {}) { 310 | const displayName = options.displayName || what.displayName || what[methodName].displayName 311 | || what.name || what.constructor.displayName || what.constructor.name || 'MissingName' 312 | 313 | // if (options.instead) return BdApi.__warn('Powercord API currently does not support replacing the entire method!') 314 | 315 | if (!what[methodName]) 316 | if (options.force) { 317 | // eslint-disable-next-line no-empty-function 318 | what[methodName] = function forcedFunction () {} 319 | } else { 320 | return BdApi.__error(null, `${methodName} doesn't exist in ${displayName}!`) 321 | } 322 | 323 | 324 | if (!options.silent) 325 | BdApi.__log(`Patching ${displayName}'s ${methodName} method`) 326 | 327 | const origMethod = what[methodName] 328 | 329 | if (options.instead) { 330 | const cancel = () => { 331 | if (!options.silent) BdApi.__log(`Unpatching instead of ${displayName} ${methodName}`) 332 | what[methodName] = origMethod 333 | } 334 | what[methodName] = function () { 335 | const data = { 336 | thisObject: this, 337 | methodArguments: arguments, 338 | cancelPatch: cancel, 339 | originalMethod: origMethod, 340 | callOriginalMethod: () => data.returnValue = data.originalMethod.apply(data.thisObject, data.methodArguments) 341 | } 342 | const tempRet = BdApi.suppressErrors(options.instead, "`instead` callback of " + what[methodName].displayName)(data) 343 | if (tempRet !== undefined) data.returnValue = tempRet 344 | return data.returnValue 345 | } 346 | if (displayName != 'MissingName') what[methodName].displayName = displayName 347 | return cancel 348 | } 349 | 350 | 351 | const patches = [] 352 | if (options.before) patches.push(BdApi.__injectBefore({ what, methodName, options, displayName }, origMethod)) 353 | if (options.after) patches.push(BdApi.__injectAfter({ what, methodName, options, displayName }, origMethod)) 354 | if (displayName != 'MissingName') what[methodName].displayName = displayName 355 | 356 | const finalCancelPatch = () => patches.forEach((patch) => patch()) 357 | 358 | return finalCancelPatch 359 | } 360 | 361 | static __injectBefore (data, origMethod) { 362 | const patchID = `bd-patch-before-${data.displayName.toLowerCase()}-${crypto.randomBytes(4).toString('hex')}` 363 | 364 | const cancelPatch = () => { 365 | if (!data.options.silent) BdApi.__log(`Unpatching before of ${data.displayName} ${data.methodName}`) 366 | uninject(patchID) 367 | } 368 | 369 | inject(patchID, data.what, data.methodName, function beforePatch (args, res) { 370 | const patchData = { 371 | // eslint-disable-next-line no-invalid-this 372 | thisObject: this, 373 | methodArguments: args, 374 | returnValue: res, 375 | cancelPatch: cancelPatch, 376 | originalMethod: origMethod, 377 | callOriginalMethod: () => patchData.returnValue = patchData.originalMethod.apply(patchData.thisObject, patchData.methodArguments) 378 | } 379 | 380 | try { 381 | data.options.before(patchData) 382 | } catch (err) { 383 | BdApi.__error(err, `Error in before callback of ${data.displayName} ${data.methodName}`) 384 | } 385 | 386 | if (data.options.once) cancelPatch() 387 | 388 | return patchData.methodArguments 389 | }, true) 390 | 391 | return cancelPatch 392 | } 393 | 394 | static __injectAfter (data, origMethod) { 395 | const patchID = `bd-patch-after-${data.displayName.toLowerCase()}-${crypto.randomBytes(4).toString('hex')}` 396 | 397 | const cancelPatch = () => { 398 | if (!data.options.silent) BdApi.__log(`Unpatching after of ${data.displayName} ${data.methodName}`) 399 | uninject(patchID) 400 | } 401 | 402 | inject(patchID, data.what, data.methodName, function afterPatch (args, res) { 403 | const patchData = { 404 | // eslint-disable-next-line no-invalid-this 405 | thisObject: this, 406 | methodArguments: args, 407 | returnValue: res, 408 | cancelPatch: cancelPatch, 409 | originalMethod: origMethod, 410 | callOriginalMethod: () => patchData.returnValue = patchData.originalMethod.apply(patchData.thisObject, patchData.methodArguments) 411 | } 412 | 413 | try { 414 | data.options.after(patchData) 415 | } catch (err) { 416 | BdApi.__error(err, `Error in after callback of ${data.displayName} ${data.methodName}`) 417 | } 418 | 419 | if (data.options.once) cancelPatch() 420 | 421 | return patchData.returnValue 422 | }, false) 423 | 424 | return cancelPatch 425 | } 426 | 427 | static getInternalInstance (node) { 428 | if (!(node instanceof window.jQuery) && !(node instanceof Element)) return undefined 429 | if (node instanceof window.jQuery) node = node[0] 430 | return getReactInstance(node) 431 | } 432 | 433 | static get settings() { // mess 434 | return {"Custom css live update":{id:"bda-css-0"},"Custom css auto udpate":{id:"bda-css-1"},"BetterDiscord Blue":{id:"bda-gs-b"},"Public Servers":{id:"bda-gs-1"},"Minimal Mode":{id:"bda-gs-2"},"Voice Mode":{id:"bda-gs-4"},"Hide Channels":{id:"bda-gs-3"},"Dark Mode":{id:"bda-gs-5"},"Voice Disconnect":{id:"bda-dc-0"},"24 Hour Timestamps":{id:"bda-gs-6"},"Colored Text":{id:"bda-gs-7"},"Normalize Classes":{id:"fork-ps-4"},"Content Error Modal":{id:"fork-ps-1"},"Show Toasts":{id:"fork-ps-2"},"Scroll To Settings":{id:"fork-ps-3"},"Automatic Loading":{id:"fork-ps-5"},"Developer Mode":{id:"bda-gs-8"},"Copy Selector":{id:"fork-dm-1"},"React DevTools":{id:"reactDevTools"},"Enable Transparency":{id:"fork-wp-1"},"Window Frame":{id:"fork-wp-2"},"Download Emotes":{id:"fork-es-3"},"Twitch Emotes":{id:"bda-es-7"},"FrankerFaceZ Emotes":{id:"bda-es-1"},"BetterTTV Emotes":{id:"bda-es-2"},"Emote Menu":{id:"bda-es-0"},"Emoji Menu":{id:"bda-es-9"},"Emote Auto Capitalization":{id:"bda-es-4"},"Show Names":{id:"bda-es-6"},"Show emote modifiers":{id:"bda-es-8"},"Animate On Hover":{id:"fork-es-2"}} 435 | } 436 | 437 | static isSettingEnabled(e) { 438 | return !!settingsCookie[e] 439 | } 440 | 441 | static isPluginEnabled (name) { 442 | const plugin = bdplugins[name] 443 | return plugin ? plugin.__started : false 444 | } 445 | 446 | static isThemeEnabled () { 447 | return false 448 | } 449 | 450 | // Miscellaneous, things that aren't part of BD 451 | static __elemParent (id) { 452 | const elem = document.getElementsByTagName(`bd-${id}`)[0] 453 | if (elem) return elem 454 | 455 | const newElem = document.createElement(`bd-${id}`) 456 | document.head.append(newElem) 457 | 458 | return newElem 459 | } 460 | 461 | static __log (...message) { 462 | console.log('%c[BDCompat:BdApi]', 'color: #3a71c1;', ...message) 463 | } 464 | 465 | static __warn (...message) { 466 | console.log('%c[BDCompat:BdApi]', 'color: #e8a400;', ...message) 467 | } 468 | 469 | static __error (error, ...message) { 470 | console.log('%c[BDCompat:BdApi]', 'color: red;', ...message) 471 | 472 | if (error) { 473 | console.groupCollapsed(`%cError: ${error.message}`, 'color: red;') 474 | console.error(error.stack) 475 | console.groupEnd() 476 | } 477 | } 478 | } 479 | 480 | module.exports = BdApi 481 | -------------------------------------------------------------------------------- /modules/BDV2.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Not to be confused with the actual BDv2's API 4 | class V2 { 5 | static get WebpackModules () { 6 | return { 7 | find: window.BdApi.findModule, 8 | findAll: window.BdApi.findAll, 9 | findByUniqueProperties: window.BdApi.findModuleByProps, 10 | findByDisplayName: window.BdApi.findModuleByDisplayName, 11 | } 12 | } 13 | 14 | static get react () { 15 | return window.BdApi.React 16 | } 17 | static get reactDom () { 18 | return window.BdApi.ReactDOM 19 | } 20 | 21 | static get getInternalInstance () { 22 | return window.BdApi.getInternalInstance 23 | } 24 | } 25 | 26 | module.exports = V2 27 | -------------------------------------------------------------------------------- /modules/ContentManager.js: -------------------------------------------------------------------------------- 1 | const { existsSync, mkdirSync, readFileSync } = require('fs') 2 | const { join, dirname, resolve, basename } = require('path') 3 | const { Module } = require('module') 4 | const originalRequire = Module._extensions['.js'] 5 | let _this 6 | 7 | // Purposefully incomplete ContentManager 8 | module.exports = class ContentManager { 9 | constructor() { 10 | _this = this 11 | Module._extensions['.js'] = this.getContentRequire() 12 | } 13 | 14 | destroy() { 15 | Module._extensions['.js'] = originalRequire 16 | } 17 | 18 | get pluginsFolder() { 19 | const pluginDir = join(__dirname, '..', 'plugins') 20 | 21 | if (!existsSync(pluginDir)) mkdirSync(pluginDir) 22 | 23 | return pluginDir 24 | } 25 | 26 | get themesFolder() { 27 | // We'll just pretend it exists. 28 | return join(ContentManager.pluginsFolder, '..', 'themes') 29 | } 30 | 31 | extractMeta(content) { 32 | const firstLine = content.split("\n")[0] 33 | const hasOldMeta = firstLine.includes("//META") 34 | if (hasOldMeta) return this.parseOldMeta(content) 35 | const hasNewMeta = firstLine.includes("/**") 36 | if (hasNewMeta) return this.parseNewMeta(content) 37 | throw new Error('META was not found.') 38 | } 39 | 40 | parseOldMeta(content) { 41 | const metaLine = content.split('\n')[0] 42 | const rawMeta = metaLine.substring(metaLine.lastIndexOf('//META') + 6, metaLine.lastIndexOf('*//')) 43 | 44 | if (metaLine.indexOf('META') < 0) throw new Error('META was not found.') 45 | if (!window.BdApi.testJSON(rawMeta)) throw new Error('META could not be parsed') 46 | 47 | const parsed = JSON.parse(rawMeta) 48 | if (!parsed.name) throw new Error('META missing name data') 49 | parsed.format = "json" 50 | 51 | return parsed 52 | } 53 | 54 | parseNewMeta(content) { 55 | const block = content.split("/**", 2)[1].split("*/", 1)[0] 56 | const out = {} 57 | let field = "", accum = "" 58 | for (const line of block.split(/[^\S\r\n]*?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/)) { 59 | if (line.length === 0) continue 60 | if (line.charAt(0) === "@" && line.charAt(1) !== " ") { 61 | out[field] = accum 62 | const l = line.indexOf(" ") 63 | field = line.substr(1, l - 1) 64 | accum = line.substr(l + 1) 65 | } else { 66 | accum += " " + line.replace("\\n", "\n").replace(/^\\@/, "@") 67 | } 68 | } 69 | out[field] = accum.trim() 70 | delete out[""] 71 | out.format = "jsdoc" 72 | return out 73 | } 74 | 75 | getContentRequire() { 76 | return function (module, filename) { 77 | if (!filename.endsWith('.plugin.js') || 78 | dirname(filename) !== resolve(bdConfig.dataPath, 'plugins')) 79 | return originalRequire.apply(this, arguments) 80 | 81 | let content = readFileSync(filename, 'utf8'); 82 | if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1) // Strip BOM 83 | const meta = _this.extractMeta(content) 84 | meta.filename = basename(filename) 85 | content += `\nmodule.exports = Object.assign(${JSON.stringify(meta)}, { type: module.exports.__esModule ? module.exports.default : module.exports.prototype ? module.exports : ${meta.exports || meta.name} })` 86 | module._compile( 87 | meta.name === 'ZeresPluginLibrary' ? 88 | content 89 | .replace('.getByProps("getCurrentUser")', '.getByProps("getCurrentUser", "getUser")') 90 | : content, 91 | filename 92 | ) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /modules/Patcher.js: -------------------------------------------------------------------------------- 1 | module.exports = class Patcher { 2 | static get patches() { return this._patches || (this._patches = []) } 3 | 4 | static getPatchesByCaller(name) { 5 | if (!name) return [] 6 | const patches = [] 7 | for (const patch of this.patches) { 8 | for (const childPatch of patch.children) 9 | if (childPatch.caller === name) patches.push(childPatch) 10 | } 11 | return patches 12 | } 13 | 14 | static unpatchAll(patches) { 15 | if (typeof patches === 'string') patches = this.getPatchesByCaller(patches) 16 | for (const patch of patches) patch.unpatch() 17 | } 18 | 19 | static resolveModule(module) { 20 | if (!module || typeof module === 'function' || (typeof module === 'object' && !Array.isArray(module))) return module 21 | if (typeof module === 'string') return DiscordModules[module] 22 | if (Array.isArray(module)) return BdApi.findModuleByProps(...module) 23 | return null 24 | } 25 | 26 | static makePatch(module, functionName, name) { 27 | const patch = { 28 | name, 29 | module, 30 | functionName, 31 | originalFunction: module[functionName], 32 | revert: () => { 33 | for (const child of patch.children) child.unpatch?.() 34 | patch.children = [] 35 | }, 36 | counter: 0, 37 | children: [] 38 | } 39 | 40 | this.patches.push(patch) 41 | return patch 42 | } 43 | 44 | static before(caller, module, functionName, callback, options = {}) { 45 | return this.pushChildPatch(caller, module, functionName, callback, { ...options, type: 'before' }) 46 | } 47 | 48 | static instead(caller, module, functionName, callback, options = {}) { 49 | return this.pushChildPatch(caller, module, functionName, callback, { ...options, type: 'instead' }) 50 | } 51 | 52 | static after(caller, module, functionName, callback, options = {}) { 53 | return this.pushChildPatch(caller, module, functionName, callback, { ...options, type: 'after' }) 54 | } 55 | 56 | static pushChildPatch(caller, module, functionName, callback, options = {}) { 57 | const { type = 'after', forcePatch = true } = options 58 | const mdl = this.resolveModule(module) 59 | if (!mdl) return null 60 | if (!mdl[functionName] && forcePatch) mdl[functionName] = function () {} 61 | if (typeof mdl[functionName] !== 'function') return null 62 | 63 | const displayName = options.displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name 64 | 65 | const patchId = `${displayName}.${functionName}` 66 | const patch = this.patches.find(p => p.module == module && p.functionName === functionName) || this.makePatch(module, functionName, patchId) 67 | 68 | const child = { 69 | caller, 70 | type, 71 | id: patch.counter, 72 | callback, 73 | unpatch: BdApi.monkeyPatch(mdl, functionName, { [type]: data => { 74 | const r = callback(data.thisObject, data.methodArguments, data.returnValue) 75 | if (r !== undefined) data.returnValue = r 76 | } }) 77 | } 78 | patch.children.push(child) 79 | patch.counter++ 80 | return child.unpatch 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /modules/PluginManager.js: -------------------------------------------------------------------------------- 1 | const { webFrame } = require('electron') 2 | const { join } = require('path') 3 | const { Module } = require('module') 4 | const { existsSync, readdirSync, unlinkSync } = require('fs') 5 | const { getModule, FluxDispatcher } = require('powercord/webpack') 6 | const { inject, uninject } = require('powercord/injector') 7 | 8 | // Allow loading from discords node_modules 9 | Module.globalPaths.push(join(process.resourcesPath, 'app.asar/node_modules')) 10 | 11 | module.exports = class BDPluginManager { 12 | constructor(pluginsFolder, settings) { 13 | this.folder = pluginsFolder 14 | this.settings = settings 15 | 16 | FluxDispatcher.subscribe('CHANNEL_SELECT', this.channelSwitch = () => this.fireEvent('onSwitch')) 17 | 18 | this.observer = new MutationObserver((mutations) => { 19 | for (let i = 0, mlen = mutations.length; i < mlen; i++) this.fireEvent('observer', mutations[i]) 20 | }) 21 | this.observer.observe(document, { childList: true, subtree: true }) 22 | 23 | // Wait for jQuery, then load the plugins 24 | window.BdApi.linkJS('jquery', '//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js') 25 | .then(async () => { 26 | this.__log('Loaded jQuery') 27 | 28 | if (!window.jQuery) { 29 | Object.defineProperty(window, 'jQuery', { 30 | get: () => webFrame.top.context.window.jQuery 31 | }) 32 | window.$ = window.jQuery 33 | } 34 | 35 | const ConnectionStore = await getModule(['isTryingToConnect', 'isConnected']) 36 | const listener = () => { 37 | if (!ConnectionStore.isConnected()) return 38 | ConnectionStore.removeChangeListener(listener) 39 | this.__log('Loading plugins..') 40 | this.loadAllPlugins() 41 | this.startAllEnabledPlugins() 42 | } 43 | if (ConnectionStore.isConnected()) listener() 44 | else ConnectionStore.addChangeListener(listener) 45 | }) 46 | } 47 | 48 | destroy () { 49 | window.BdApi.unlinkJS('jquery') 50 | if (this.channelSwitch) FluxDispatcher.unsubscribe('CHANNEL_SELECT', this.channelSwitch) 51 | 52 | this.observer.disconnect() 53 | this.stopAllPlugins() 54 | } 55 | 56 | 57 | startAllEnabledPlugins () { 58 | const plugins = Object.keys(window.bdplugins) 59 | 60 | plugins.forEach((pluginName) => { 61 | if (window.BdApi.loadData('BDCompat-EnabledPlugins', pluginName) === true) this.startPlugin(pluginName) 62 | }) 63 | } 64 | 65 | stopAllPlugins () { 66 | const plugins = Object.keys(window.bdplugins) 67 | 68 | plugins.forEach((pluginName) => { 69 | this.stopPlugin(pluginName) 70 | }) 71 | } 72 | 73 | 74 | isEnabled (pluginName) { 75 | const plugin = window.bdplugins[pluginName] 76 | if (!plugin) return this.__error(null, `Tried to access a missing plugin: ${pluginName}`) 77 | 78 | return plugin.__started 79 | } 80 | 81 | startPlugin (pluginName) { 82 | const plugin = window.bdplugins[pluginName] 83 | if (!plugin) return this.__error(null, `Tried to start a missing plugin: ${pluginName}`) 84 | 85 | if (plugin.__started) return 86 | 87 | try { 88 | plugin.plugin.start() 89 | plugin.__started = true 90 | this.__log(`Started plugin ${plugin.plugin.getName()}`) 91 | } catch (err) { 92 | this.__error(err, `Could not start ${plugin.plugin.getName()}`) 93 | window.BdApi.saveData('BDCompat-EnabledPlugins', plugin.plugin.getName(), false) 94 | } 95 | } 96 | stopPlugin (pluginName) { 97 | const plugin = window.bdplugins[pluginName] 98 | if (!plugin) return this.__error(null, `Tried to stop a missing plugin: ${pluginName}`) 99 | 100 | if (!plugin.__started) return 101 | 102 | try { 103 | plugin.plugin.stop() 104 | plugin.__started = false 105 | this.__log(`Stopped plugin ${plugin.plugin.getName()}`) 106 | } catch (err) { 107 | this.__error(err, `Could not stop ${plugin.plugin.getName()}`) 108 | if (this.settings.get('disableWhenStopFailed')) 109 | window.BdApi.saveData('BDCompat-EnabledPlugins', plugin.plugin.getName(), false) 110 | } 111 | } 112 | reloadPlugin (pluginName) { 113 | const plugin = window.bdplugins[pluginName] 114 | if (!plugin) return this.__error(null, `Tried to reload a missing plugin: ${pluginName}`) 115 | 116 | this.stopPlugin(pluginName) 117 | 118 | delete window.bdplugins[pluginName] 119 | delete require.cache[plugin.__filePath] 120 | 121 | this.loadPlugin(pluginName) 122 | this.startPlugin(pluginName) 123 | } 124 | 125 | enablePlugin (pluginName) { 126 | const plugin = window.bdplugins[pluginName] 127 | if (!plugin) return this.__error(null, `Tried to enable a missing plugin: ${pluginName}`) 128 | 129 | window.BdApi.saveData('BDCompat-EnabledPlugins', plugin.plugin.getName(), true) 130 | this.startPlugin(pluginName) 131 | } 132 | disablePlugin (pluginName) { 133 | const plugin = window.bdplugins[pluginName] 134 | if (!plugin) return this.__error(null, `Tried to disable a missing plugin: ${pluginName}`) 135 | 136 | window.BdApi.saveData('BDCompat-EnabledPlugins', plugin.plugin.getName(), false) 137 | this.stopPlugin(pluginName) 138 | } 139 | 140 | loadAllPlugins() { 141 | const plugins = readdirSync(this.folder) 142 | .filter((pluginFile) => pluginFile.endsWith('.plugin.js')) 143 | .map((pluginFile) => pluginFile.slice(0, -('.plugin.js'.length))) 144 | 145 | plugins.forEach((pluginName) => this.loadPlugin(pluginName)) 146 | } 147 | 148 | loadPlugin(pluginName) { 149 | const pluginPath = join(this.folder, `${pluginName}.plugin.js`) 150 | if (!existsSync(pluginPath)) return this.__error(null, `Tried to load a nonexistant plugin: ${pluginName}`) 151 | 152 | try { 153 | // eslint-disable-next-line global-require 154 | const meta = require(pluginPath) 155 | try { 156 | const plugin = new meta.type 157 | this.addMissingGetMethods(pluginName, meta, plugin) 158 | if (window.bdplugins[plugin.getName()]) window.bdplugins[plugin.getName()].plugin.stop() 159 | delete window.bdplugins[plugin.getName()] 160 | window.bdplugins[plugin.getName()] = { plugin, __filePath: pluginPath, ...meta } 161 | 162 | if (plugin.load && typeof plugin.load === 'function') 163 | try { 164 | plugin.load() 165 | 166 | if (pluginName === '0PluginLibrary') this.__patchZLibPatcher() 167 | } catch (err) { 168 | this.__error(err, `Failed to preload ${plugin.getName()}`) 169 | } 170 | 171 | this.__log(`Loaded ${plugin.getName()} v${plugin.getVersion()} by ${plugin.getAuthor()}`) 172 | } catch (e) { 173 | this.__error(e, `Failed to load ${pluginName}:`, meta) 174 | } 175 | } catch (e) { 176 | this.__error(`Failed to compile ${pluginName}:`, e) 177 | } 178 | } 179 | 180 | delete(pluginName) { 181 | const plugin = window.bdplugins[pluginName] 182 | if (!plugin) return this.__error(null, `Tried to delete a missing plugin: ${pluginName}`) 183 | 184 | this.disablePlugin(pluginName) 185 | if (typeof plugin.plugin.unload === 'function') plugin.plugin.unload() 186 | delete window.bdplugins[pluginName] 187 | 188 | unlinkSync(plugin.__filePath) 189 | } 190 | 191 | fireEvent (event, ...args) { 192 | for (const plug in window.bdplugins) { 193 | const p = window.bdplugins[plug], { plugin } = p 194 | if (!p.__started || !plugin[event] || typeof plugin[event] !== 'function') continue 195 | 196 | try { 197 | plugin[event](...args) 198 | } catch (err) { 199 | this.__error(err, `Could not fire ${event} event for ${plugin.name}`) 200 | } 201 | } 202 | } 203 | 204 | addMissingGetMethods(pluginName, meta, plugin) { 205 | if (!plugin.getName) plugin.getName = () => meta.name || pluginName 206 | if (!plugin.getDescription) plugin.getDescription = () => meta.description || '' 207 | if (!plugin.getVersion) plugin.getVersion = () => meta.version || '0.0.0' 208 | if (!plugin.getAuthor) plugin.getAuthor = () => meta.author || meta.authorId || 'Unknown' 209 | } 210 | 211 | 212 | // ZLibrary checks for instanceof Function and Function class is different in renderer and preload, so need to fix it in bdCompat 213 | __patchZLibPatcher() { 214 | this.__unpatchZLibPatcher() 215 | 216 | const _window = webFrame.top.context.window 217 | if (!window?.ZLibrary?.Patcher) return this.__error(null, 'Failed to patch ZLibrary Patcher') 218 | 219 | const origFunction = Function 220 | inject('bdCompat-zlib-patcher-pre', window.ZLibrary.Patcher, 'pushChildPatch', args => { 221 | const orig = args[1]?.[args[2]] 222 | if (orig && !(orig instanceof origFunction) && orig instanceof _window.Function) window.Function = _window.Function 223 | return args 224 | }, true) 225 | inject('bdCompat-zlib-patcher', window.ZLibrary.Patcher, 'pushChildPatch', (_, res) => { 226 | window.Function = origFunction 227 | return res 228 | }) 229 | 230 | this.__log('Patched ZLibrary Patcher') 231 | } 232 | __unpatchZLibPatcher() { 233 | uninject('bdCompat-zlib-patcher-pre') 234 | uninject('bdCompat-zlib-patcher') 235 | } 236 | 237 | 238 | disable = this.disablePlugin 239 | enable = this.enablePlugin 240 | reload = this.reloadPlugin 241 | 242 | 243 | __log (...message) { 244 | console.log('%c[BDCompat:BDPluginManager]', 'color: #3a71c1;', ...message) 245 | } 246 | 247 | __warn (...message) { 248 | console.warn('%c[BDCompat:BDPluginManager]', 'color: #e8a400;', ...message) 249 | } 250 | 251 | __error (error, ...message) { 252 | console.error('%c[BDCompat:BDPluginManager]', 'color: red;', ...message) 253 | 254 | if (error) { 255 | console.groupCollapsed(`%cError: ${error.message}`, 'color: red;') 256 | console.error(error.stack) 257 | console.groupEnd() 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /modules/index.js: -------------------------------------------------------------------------------- 1 | require('fs') 2 | .readdirSync(__dirname) 3 | .filter(file => file != 'index.js') 4 | .forEach(filename => { 5 | const moduleName = filename.split('.')[0] 6 | exports[moduleName] = require(`${__dirname}/${filename}`) 7 | }) 8 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .bdc-spacer { 2 | flex-grow: 1; 3 | flex-shrink: 1; 4 | } 5 | .bdc-margin { 6 | width: 20px; 7 | } 8 | .bdc-justifystart { 9 | display: flex; 10 | justify-content: flex-start; 11 | } 12 | .bdc-plugin .metadata > div { 13 | width: 50% !important; 14 | } 15 | .bdc-plugin .metadata > .license { 16 | display: none; 17 | } 18 | 19 | /* Toast CSS */ 20 | 21 | .bd-toasts { 22 | position: fixed; 23 | display: flex; 24 | top: 0; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: flex-end; 28 | pointer-events: none; 29 | z-index: 4000; 30 | } 31 | 32 | @keyframes toast-up { 33 | from { 34 | transform: translateY(0); 35 | opacity: 0; 36 | } 37 | } 38 | 39 | .bd-toast { 40 | animation: toast-up 300ms ease; 41 | transform: translateY(-10px); 42 | background: #36393F; 43 | padding: 10px; 44 | border-radius: 5px; 45 | box-shadow: 0 0 0 1px rgba(32, 34, 37, .6), 0 2px 10px 0 rgba(0, 0, 0, .2); 46 | font-weight: 500; 47 | color: #fff; 48 | user-select: text; 49 | font-size: 14px; 50 | opacity: 1; 51 | margin-top: 10px; 52 | pointer-events: none; 53 | user-select: none; 54 | } 55 | 56 | @keyframes toast-down { 57 | to { 58 | transform: translateY(0px); 59 | opacity: 0; 60 | } 61 | } 62 | 63 | .bd-toast.closing { 64 | animation: toast-down 200ms ease; 65 | animation-fill-mode: forwards; 66 | opacity: 1; 67 | transform: translateY(-10px); 68 | } 69 | 70 | .bd-toast.icon { 71 | padding-left: 30px; 72 | background-size: 20px 20px; 73 | background-repeat: no-repeat; 74 | background-position: 6px 50%; 75 | } 76 | 77 | .bd-toast.toast-info { 78 | background-color: #4a90e2; 79 | } 80 | 81 | .bd-toast.toast-info.icon { 82 | background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPiAgICA8cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJzNC40OCAxMCAxMCAxMCAxMC00LjQ4IDEwLTEwUzE3LjUyIDIgMTIgMnptMSAxNWgtMnYtNmgydjZ6bTAtOGgtMlY3aDJ2MnoiLz48L3N2Zz4=); 83 | } 84 | 85 | .bd-toast.toast-success { 86 | background-color: #43b581; 87 | } 88 | 89 | .bd-toast.toast-success.icon { 90 | background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPiAgICA8cGF0aCBkPSJNMTIgMkM2LjQ4IDIgMiA2LjQ4IDIgMTJzNC40OCAxMCAxMCAxMCAxMC00LjQ4IDEwLTEwUzE3LjUyIDIgMTIgMnptLTIgMTVsLTUtNSAxLjQxLTEuNDFMMTAgMTQuMTdsNy41OS03LjU5TDE5IDhsLTkgOXoiLz48L3N2Zz4=); 91 | } 92 | 93 | .bd-toast.toast-danger, 94 | .bd-toast.toast-error { 95 | background-color: #f04747; 96 | } 97 | 98 | .bd-toast.toast-danger.icon, 99 | .bd-toast.toast-error.icon { 100 | background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTEyIDJDNi40NyAyIDIgNi40NyAyIDEyczQuNDcgMTAgMTAgMTAgMTAtNC40NyAxMC0xMFMxNy41MyAyIDEyIDJ6bTUgMTMuNTlMMTUuNTkgMTcgMTIgMTMuNDEgOC40MSAxNyA3IDE1LjU5IDEwLjU5IDEyIDcgOC40MSA4LjQxIDcgMTIgMTAuNTkgMTUuNTkgNyAxNyA4LjQxIDEzLjQxIDEyIDE3IDE1LjU5eiIvPiAgICA8cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PC9zdmc+); 101 | } 102 | 103 | .bd-toast.toast-warning, 104 | .bd-toast.toast-warn { 105 | background-color: #FFA600; 106 | color: white; 107 | } 108 | 109 | .bd-toast.toast-warning.icon, 110 | .bd-toast.toast-warn.icon { 111 | background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPiAgICA8cGF0aCBkPSJNMSAyMWgyMkwxMiAyIDEgMjF6bTEyLTNoLTJ2LTJoMnYyem0wLTRoLTJ2LTRoMnY0eiIvPjwvc3ZnPg==); 112 | } 113 | --------------------------------------------------------------------------------