diff --git a/client/src/data/user.settings.default.json b/client/src/data/user.settings.default.json index fc35312f..be4d3676 100644 --- a/client/src/data/user.settings.default.json +++ b/client/src/data/user.settings.default.json @@ -30,6 +30,12 @@ "hint": "Disconnect from voice server when Discord closes", "value": false, "disabled": true + }, + { + "id": "menu-keybind", + "type": "keybind", + "text": "Menu keybind", + "value": "mod+b" } ] }, @@ -45,6 +51,13 @@ "hint": "BetterDiscord developer mode", "value": false, "disabled": true + }, + { + "id": "ignore-content-manager-errors", + "type": "bool", + "text": "Ignore content manager errors", + "hint": "Only when starting Discord. It gets annoying when you're reloading Discord often and have plugins that are meant to fail.", + "value": false } ] } diff --git a/client/src/index.js b/client/src/index.js index f37523b5..e6620fcc 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -29,7 +29,9 @@ class BetterDiscord { window.bdmodals = Modals; window.bdlogs = Logger; window.emotes = EmoteModule; + EmoteModule.observe(); + DOM.injectStyle(BdCss, 'bdmain'); Events.on('global-ready', this.globalReady.bind(this)); } @@ -42,20 +44,21 @@ class BetterDiscord { await ExtModuleManager.loadAllModules(true); await PluginManager.loadAllPlugins(true); await ThemeManager.loadAllThemes(true); - Modals.showContentManagerErrors(); + + if (!Settings.get('core', 'advanced', 'ignore-content-manager-errors')) + Modals.showContentManagerErrors(); + Events.emit('ready'); Events.emit('discord-ready'); } catch (err) { - console.log('FAILED TO LOAD!', err); + Logger.err('main', ['FAILED TO LOAD!', err]); } } globalReady() { BdUI.initUiEvents(); this.vueInstance = BdUI.injectUi(); - (async () => { - this.init(); - })(); + this.init(); } } diff --git a/client/src/modules/content.js b/client/src/modules/content.js index 0517c78b..2200c30a 100644 --- a/client/src/modules/content.js +++ b/client/src/modules/content.js @@ -15,6 +15,9 @@ import Database from './database'; export default class Content { constructor(internals) { + Utils.deepfreeze(internals.info); + Object.freeze(internals.paths); + this.__internals = internals; this.settings.on('setting-updated', event => this.events.emit('setting-updated', event)); @@ -170,3 +173,5 @@ export default class Content { } } + +Object.freeze(Content.prototype); diff --git a/client/src/modules/contentmanager.js b/client/src/modules/contentmanager.js index c35c4b89..cf2f8165 100644 --- a/client/src/modules/contentmanager.js +++ b/client/src/modules/contentmanager.js @@ -10,15 +10,16 @@ import Content from './content'; import Globals from './globals'; +import Database from './database'; import { Utils, FileUtils, ClientLogger as Logger } from 'common'; -import path from 'path'; import { Events } from 'modules'; import { SettingsSet, ErrorEvent } from 'structs'; import { Modals } from 'ui'; -import Database from './database'; +import path from 'path'; +import Combokeys from 'combokeys'; /** - * Base class for external content managing + * Base class for managing external content */ export default class { @@ -209,10 +210,12 @@ export default class { userConfig.config.setSaved(); for (let setting of userConfig.config.findSettings(() => true)) { + // This will load custom settings + // Setting the content's path on only the live config (and not the default config) ensures that custom settings will not be loaded on the default settings setting.setContentPath(contentPath); } - Utils.deepfreeze(defaultConfig); + Utils.deepfreeze(defaultConfig, object => object instanceof Combokeys); const configs = { defaultConfig, diff --git a/client/src/modules/eventswrapper.js b/client/src/modules/eventswrapper.js new file mode 100644 index 00000000..49a654cd --- /dev/null +++ b/client/src/modules/eventswrapper.js @@ -0,0 +1,45 @@ +/** + * BetterDiscord Events Wrapper Module + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. +*/ + +const eventemitters = new WeakMap(); + +export default class EventsWrapper { + constructor(eventemitter) { + eventemitters.set(this, eventemitter); + } + + get eventSubs() { + return this._eventSubs || (this._eventSubs = []); + } + + subscribe(event, callback) { + if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return; + this.eventSubs.push({ + event, + callback + }); + eventemitters.get(this).on(event, callback); + } + + unsubscribe(event, callback) { + for (let index of this.eventSubs) { + if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) return; + eventemitters.get(this).off(event, this.eventSubs[index].callback); + this.eventSubs.splice(index, 1); + } + } + + unsubscribeAll() { + for (let event of this.eventSubs) { + eventemitters.get(this).off(event.event, event.callback); + } + this.eventSubs.splice(0, this.eventSubs.length); + } +} diff --git a/client/src/modules/modules.js b/client/src/modules/modules.js index b93216a3..71956f05 100644 --- a/client/src/modules/modules.js +++ b/client/src/modules/modules.js @@ -13,3 +13,4 @@ export { default as SocketProxy } from './socketproxy'; export { default as EventHook } from './eventhook'; export { default as Permissions } from './permissionmanager'; export { default as Database } from './database'; +export { default as EventsWrapper } from './eventswrapper'; diff --git a/client/src/modules/permissionmanager.js b/client/src/modules/permissionmanager.js index a5fc0d94..b052207c 100644 --- a/client/src/modules/permissionmanager.js +++ b/client/src/modules/permissionmanager.js @@ -11,27 +11,27 @@ const PermissionMap = { IDENTIFY: { HEADER: 'Access your account information', - BODY: 'Allows :NAME: to read your account information(excluding user token)' + BODY: 'Allows :NAME: to read your account information (excluding user token).' }, READ_MESSAGES: { HEADER: 'Read all messages', - BODY: 'Allows :NAME: to read all messages accessible through your Discord account' + BODY: 'Allows :NAME: to read all messages accessible through your Discord account.' }, SEND_MESSAGES: { HEADER: 'Send messages', - BODY: 'Allows :NAME: to send messages on your behalf' + BODY: 'Allows :NAME: to send messages on your behalf.' }, DELETE_MESSAGES: { HEADER: 'Delete messages', - BODY: 'Allows :NAME: to delete messages on your behalf' + BODY: 'Allows :NAME: to delete messages on your behalf.' }, EDIT_MESSAGES: { HEADER: 'Edit messages', - BODY: 'Allows :NAME: to edit messages on your behalf' + BODY: 'Allows :NAME: to edit messages on your behalf.' }, JOIN_SERVERS: { HEADER: 'Join servers for you', - BODY: 'Allows :NAME: to join servers on your behalf' + BODY: 'Allows :NAME: to join servers on your behalf.' } } diff --git a/client/src/modules/pluginapi.js b/client/src/modules/pluginapi.js index dd07cf35..b95c84b9 100644 --- a/client/src/modules/pluginapi.js +++ b/client/src/modules/pluginapi.js @@ -14,50 +14,20 @@ import ExtModuleManager from './extmodulemanager'; import PluginManager from './pluginmanager'; import ThemeManager from './thememanager'; import Events from './events'; +import EventsWrapper from './eventswrapper'; import WebpackModules from './webpackmodules'; import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs'; -import { Modals, DOM } from 'ui'; +import { BdMenuItems, Modals, DOM } from 'ui'; import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue'; -class EventsWrapper { - constructor(eventemitter) { - this.__eventemitter = eventemitter; - } - - get eventSubs() { - return this._eventSubs || (this._eventSubs = []); - } - - subscribe(event, callback) { - if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return; - this.eventSubs.push({ - event, - callback - }); - this.__eventemitter.on(event, callback); - } - - unsubscribe(event, callback) { - for (let index of this.eventSubs) { - if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) return; - this.__eventemitter.off(event, this.eventSubs[index].callback); - this.eventSubs.splice(index, 1); - } - } - - unsubscribeAll() { - for (let event of this.eventSubs) { - this.__eventemitter.off(event.event, event.callback); - } - this._eventSubs = []; - } -} - export default class PluginApi { constructor(pluginInfo) { this.pluginInfo = pluginInfo; this.Events = new EventsWrapper(Events); + this._menuItems = undefined; + this._injectedStyles = undefined; + this._modalStack = undefined; } get plugin() { @@ -153,6 +123,54 @@ export default class PluginApi { }; } + /** + * BdMenu + */ + + get BdMenu() { + return { + BdMenuItems: this.BdMenuItems + }; + } + + /** + * BdMenuItems + */ + + get menuItems() { + return this._menuItems || (this._menuItems = []); + } + addMenuItem(item) { + return BdMenuItems.add(item); + } + addMenuSettingsSet(category, set, text) { + const item = BdMenuItems.addSettingsSet(category, set, text); + return this.menuItems.push(item); + } + addMenuVueComponent(category, text, component) { + const item = BdMenuItems.addVueComponent(category, text, component); + return this.menuItems.push(item); + } + removeMenuItem(item) { + BdMenuItems.remove(item); + Utils.removeFromArray(this.menuItems, item); + } + removeAllMenuItems() { + for (let item of this.menuItems) + BdMenuItems.remove(item); + } + get BdMenuItems() { + return Object.defineProperty({ + add: this.addMenuItem.bind(this), + addSettingsSet: this.addMenuSettingsSet.bind(this), + addVueComponent: this.addMenuVueComponent.bind(this), + remove: this.removeMenuItem.bind(this), + removeAll: this.removeAllMenuItems.bind(this) + }, 'items', { + get: () => this.menuItems + }); + } + /** * CssUtils */ @@ -172,8 +190,8 @@ export default class PluginApi { injectStyle(id, css) { if (id && !css) css = id, id = undefined; this.deleteStyle(id); - const styleid = `plugin-${this.getPlugin().id}-${id}`; - this.injectedStyles.push(styleid); + const styleid = `plugin-${this.plugin.id}-${id}`; + this.injectedStyles.push(id); DOM.injectStyle(css, styleid); } async injectSass(id, scss, options) { @@ -183,7 +201,7 @@ export default class PluginApi { this.injectStyle(id, css, options); } deleteStyle(id) { - const styleid = `plugin-${this.getPlugin().id}-${id}`; + const styleid = `plugin-${this.plugin.id}-${id}`; this.injectedStyles.splice(this.injectedStyles.indexOf(styleid), 1); DOM.deleteStyle(styleid); } @@ -216,7 +234,6 @@ export default class PluginApi { } addModal(_modal, component) { const modal = Modals.add(_modal, component); - modal.close = force => this.closeModal(modal, force); modal.on('close', () => { let index; while ((index = this.modalStack.findIndex(m => m === modal)) > -1) @@ -245,16 +262,20 @@ export default class PluginApi { return this.addModal(Modals.settings(settingsset, headertext, options)); } get Modals() { - return Object.defineProperty(Object.defineProperty({ + return Object.defineProperties({ add: this.addModal.bind(this), close: this.closeModal.bind(this), closeAll: this.closeAllModals.bind(this), closeLast: this.closeLastModal.bind(this), + basic: this.basicModal.bind(this), settings: this.settingsModal.bind(this) - }, 'stack', { - get: () => this.modalStack - }), 'baseComponent', { - get: () => this.baseModalComponent + }, { + stack: { + get: () => this.modalStack + }, + baseComponent: { + get: () => this.baseModalComponent + } }); } @@ -348,3 +369,8 @@ export default class PluginApi { } } + +// Stop plugins from modifying the plugin API for all plugins +// Plugins can still modify their own plugin API object +Object.freeze(PluginApi); +Object.freeze(PluginApi.prototype); diff --git a/client/src/modules/pluginmanager.js b/client/src/modules/pluginmanager.js index 6f8a3b12..198d06e1 100644 --- a/client/src/modules/pluginmanager.js +++ b/client/src/modules/pluginmanager.js @@ -99,6 +99,9 @@ export default class extends ContentManager { } const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor, deps); + if (!(plugin.prototype instanceof Plugin)) + throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`}; + const instance = new plugin({ configs, info, main, paths: { diff --git a/client/src/modules/settings.js b/client/src/modules/settings.js index 49e0dc91..f03117bd 100644 --- a/client/src/modules/settings.js +++ b/client/src/modules/settings.js @@ -18,7 +18,23 @@ import path from 'path'; export default new class Settings { constructor() { - this.settings = []; + this.settings = defaultSettings.map(_set => { + const set = new SettingsSet(_set); + + set.on('setting-updated', event => { + const { category, setting, value, old_value } = event; + Logger.log('Settings', `${set.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`); + Events.emit('setting-updated', event); + Events.emit(`setting-updated-${set.id}_${category.id}_${setting.id}`, event); + }); + + set.on('settings-updated', async (event) => { + await this.saveSettings(); + Events.emit('settings-updated', event); + }); + + return set; + }); } async loadSettings() { @@ -29,22 +45,12 @@ export default new class Settings { const user_config = await FileUtils.readJsonFromFile(settingsPath); const { settings, scss, css, css_editor_files, scss_error, css_editor_bounds } = user_config; - this.settings = defaultSettings.map(set => { - const newSet = new SettingsSet(set); - newSet.merge(settings.find(s => s.id === newSet.id)); - newSet.setSaved(); - newSet.on('setting-updated', event => { - const { category, setting, value, old_value } = event; - Logger.log('Settings', `${newSet.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`); - Events.emit('setting-updated', event); - Events.emit(`setting-updated-${newSet.id}_${category.id}_${setting.id}`, event); - }); - newSet.on('settings-updated', async (event) => { - await this.saveSettings(); - Events.emit('settings-updated', event); - }); - return newSet; - }); + for (let set of this.settings) { + const newSet = settings.find(s => s.id === set.id); + if (!newSet) continue; + set.merge(newSet); + set.setSaved(); + } CssEditor.setState(scss, css, css_editor_files, scss_error); CssEditor.editor_bounds = css_editor_bounds || {}; diff --git a/client/src/modules/theme.js b/client/src/modules/theme.js index bd68cef9..b02ba3ed 100644 --- a/client/src/modules/theme.js +++ b/client/src/modules/theme.js @@ -46,7 +46,8 @@ export default class Theme extends Content { /** * This is called when the theme is enabled. */ - onstart() { + async onstart() { + if (!this.css) await this.recompile(); DOM.injectTheme(this.css, this.id); } diff --git a/client/src/modules/thememanager.js b/client/src/modules/thememanager.js index 9c404121..81276c5d 100644 --- a/client/src/modules/thememanager.js +++ b/client/src/modules/thememanager.js @@ -45,8 +45,10 @@ export default class ThemeManager extends ContentManager { mainPath: paths.mainPath } }); - if (!instance.css) instance.recompile(); - else if (instance.enabled) instance.enable(); + if (instance.enabled) { + instance.userConfig.enabled = false; + instance.enable(); + } return instance; } catch (err) { throw err; diff --git a/client/src/structs/settings/setting.js b/client/src/structs/settings/setting.js index 2927b342..7785e03b 100644 --- a/client/src/structs/settings/setting.js +++ b/client/src/structs/settings/setting.js @@ -17,27 +17,29 @@ import DropdownSetting from './types/dropdown'; import RadioSetting from './types/radio'; import SliderSetting from './types/slider'; import ColourSetting from './types/colour'; +import KeybindSetting from './types/keybind'; import FileSetting from './types/file'; import ArraySetting from './types/array'; import CustomSetting from './types/custom'; export default class Setting { - constructor(args) { + constructor(args, ...merge) { args = args.args || args; if (args.type === 'color') args.type = 'colour'; - if (args.type === 'bool') return new BoolSetting(args); - else if (args.type === 'text') return new StringSetting(args); - else if (args.type === 'number') return new NumberSetting(args); - else if (args.type === 'dropdown') return new DropdownSetting(args); - else if (args.type === 'radio') return new RadioSetting(args); - else if (args.type === 'slider') return new SliderSetting(args); - else if (args.type === 'colour') return new ColourSetting(args); - else if (args.type === 'file') return new FileSetting(args); - else if (args.type === 'array') return new ArraySetting(args); - else if (args.type === 'custom') return new CustomSetting(args); + if (args.type === 'bool') return new BoolSetting(args, ...merge); + else if (args.type === 'text') return new StringSetting(args, ...merge); + else if (args.type === 'number') return new NumberSetting(args, ...merge); + else if (args.type === 'dropdown') return new DropdownSetting(args, ...merge); + else if (args.type === 'radio') return new RadioSetting(args, ...merge); + else if (args.type === 'slider') return new SliderSetting(args, ...merge); + else if (args.type === 'colour') return new ColourSetting(args, ...merge); + else if (args.type === 'keybind') return new KeybindSetting(args, ...merge); + else if (args.type === 'file') return new FileSetting(args, ...merge); + else if (args.type === 'array') return new ArraySetting(args, ...merge); + else if (args.type === 'custom') return new CustomSetting(args, ...merge); else throw {message: `Setting type ${args.type} unknown`}; } diff --git a/client/src/structs/settings/types/keybind.js b/client/src/structs/settings/types/keybind.js new file mode 100644 index 00000000..efbe3f90 --- /dev/null +++ b/client/src/structs/settings/types/keybind.js @@ -0,0 +1,28 @@ +/** + * BetterDiscord Keybind Setting Struct + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Setting from './basesetting'; +import Combokeys from 'combokeys'; + +export default class KeybindSetting extends Setting { + + constructor(args, ...merge) { + super(args, ...merge); + + this.combokeys = new Combokeys(document); + this.combokeys.bind(this.value, event => this.emit('keybind-activated', event)); + } + + setValueHook() { + this.combokeys.reset(); + this.combokeys.bind(this.value, event => this.emit('keybind-activated', event)); + } + +} diff --git a/client/src/styles/partials/generic/forms/colourpicker.scss b/client/src/styles/partials/generic/forms/colourpickers.scss similarity index 100% rename from client/src/styles/partials/generic/forms/colourpicker.scss rename to client/src/styles/partials/generic/forms/colourpickers.scss diff --git a/client/src/styles/partials/generic/forms/index.scss b/client/src/styles/partials/generic/forms/index.scss index e1154656..e6cef62f 100644 --- a/client/src/styles/partials/generic/forms/index.scss +++ b/client/src/styles/partials/generic/forms/index.scss @@ -1,9 +1,10 @@ @import './main.scss'; +@import './switches.scss'; @import './text.scss'; @import './files.scss'; @import './dropdowns.scss'; @import './radios.scss'; @import './sliders.scss'; -@import './switches.scss'; +@import './colourpickers.scss'; +@import './keybinds.scss'; @import './arrays.scss'; -@import './colourpicker.scss'; diff --git a/client/src/styles/partials/generic/forms/keybinds.scss b/client/src/styles/partials/generic/forms/keybinds.scss new file mode 100644 index 00000000..49bea00c --- /dev/null +++ b/client/src/styles/partials/generic/forms/keybinds.scss @@ -0,0 +1,52 @@ +.bd-keybind { + padding: 10px; + display: flex; + // width: 180px; + margin-top: 10px; + background-color: rgba(0,0,0,.1); + border: 1px solid rgba(0,0,0,.3); + transition: border .15s ease; + border-radius: 3px; + box-sizing: border-box; + min-height: 40px; + + .bd-keybind-selected { + flex: 1 1 auto; + color: #f6f6f7; + font-size: 14px; + } + + &.bd-keybind-unset { + .bd-keybind-selected { + color: hsla(240,6%,97%,.3); + font-weight: 600; + } + } + + .bd-button { + border-radius: 2px; + margin: -4px -4px -4px 10px; + padding: 2px 20px; + transition: background-color .2s ease-in-out, color .2s ease-in-out; + font-size: 14px; + font-weight: 500; + flex: 0 0 auto; + cursor: pointer; + } + + &.bd-active { + border-color: $colerr; + animation: bd-keybind-pulse 1s infinite; + + .bd-button { + color: $colerr; + background-color: rgba($colerr, .3); + } + } +} + +@keyframes bd-keybind-pulse { + 0% { box-shadow: 0 0 6px rgba(240,71,71,.3) } + 50% { box-shadow: 0 0 10px rgba(240,71,71,.6) } + 100% { box-shadow: 0 0 6px rgba(240,71,71,.3) } +} diff --git a/client/src/styles/partials/generic/forms/main.scss b/client/src/styles/partials/generic/forms/main.scss index 9ac4e8ce..470e6379 100644 --- a/client/src/styles/partials/generic/forms/main.scss +++ b/client/src/styles/partials/generic/forms/main.scss @@ -1,12 +1,13 @@ +.bd-setting-switch, .bd-form-textinput, .bd-form-textarea, -.bd-form-fileinput, +.bd-form-numberinput, .bd-form-dropdown, .bd-form-radio, -.bd-form-numberinput, .bd-form-slider, .bd-form-colourpicker, -.bd-setting-switch, +.bd-form-keybind, +.bd-form-fileinput, .bd-form-settingsarray { .bd-title { display: flex; diff --git a/client/src/ui/bdmenu.js b/client/src/ui/bdmenu.js new file mode 100644 index 00000000..c8f6cd60 --- /dev/null +++ b/client/src/ui/bdmenu.js @@ -0,0 +1,61 @@ +/** + * BetterDiscord Menu Module + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. +*/ + +import { Utils } from 'common'; + +let items = 0; + +const BdMenuItems = new class { + + constructor() { + window.bdmenu = this; + + this.items = []; + + this.addSettingsSet('Internal', 'core', 'Core'); + this.addSettingsSet('Internal', 'ui', 'UI'); + this.addSettingsSet('Internal', 'emotes', 'Emotes'); + + this.add({category: 'Internal', contentid: 'css', text: 'CSS Editor'}); + this.add({category: 'External', contentid: 'plugins', text: 'Plugins'}); + this.add({category: 'External', contentid: 'themes', text: 'Themes'}); + } + + add(item) { + item.id = items++; + item.contentid = item.contentid || (items++ + ''); + item.active = false; + item.hidden = item.hidden || false; + item._type = item._type || 'button'; + + this.items.push(item); + return item; + } + + addSettingsSet(category, set, text) { + return this.add({ + category, set, + text: text || set.text + }); + } + + addVueComponent(category, text, component) { + return this.add({ + category, text, component + }); + } + + remove(item) { + Utils.removeFromArray(this.items, item); + } + +}; + +export { BdMenuItems }; diff --git a/client/src/ui/bdui.js b/client/src/ui/bdui.js index 44341cc7..e25031ce 100644 --- a/client/src/ui/bdui.js +++ b/client/src/ui/bdui.js @@ -86,4 +86,5 @@ export default class { return vueInstance; } + } diff --git a/client/src/ui/components/BdSettings.vue b/client/src/ui/components/BdSettings.vue index da976a86..0fbff82d 100644 --- a/client/src/ui/components/BdSettings.vue +++ b/client/src/ui/components/BdSettings.vue @@ -16,7 +16,10 @@ ESC - +
v2.0.0a by Jiiks/JsSucks @@ -31,19 +34,21 @@
-
- - +
+ + + + -
-
- -
-
- -
-
- + + + + + + +
@@ -53,32 +58,22 @@ // Imports import { shell } from 'electron'; import { Settings } from 'modules'; + import { BdMenuItems } from 'ui'; import { SidebarView, Sidebar, SidebarItem, ContentColumn } from './sidebar'; import { SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView } from './bd'; import { SvgX, MiGithubCircle, MiWeb, MiClose, MiTwitterCircle } from './common'; - // Constants - const sidebarItems = [ - { text: 'Internal', _type: 'header' }, - { id: 0, contentid: "core", text: 'Core', active: false, _type: 'button' }, - { id: 1, contentid: "ui", text: 'UI', active: false, _type: 'button' }, - { id: 2, contentid: "emotes", text: 'Emotes', active: false, _type: 'button' }, - { id: 3, contentid: "css", text: 'CSS Editor', active: false, _type: 'button' }, - { text: 'External', _type: 'header' }, - { id: 4, contentid: "plugins", text: 'Plugins', active: false, _type: 'button' }, - { id: 5, contentid: "themes", text: 'Themes', active: false, _type: 'button' } - ]; - export default { data() { return { - sidebarItems, + BdMenuItems, activeIndex: -1, lastActiveIndex: -1, animating: false, first: true, Settings, - timeout: null + timeout: null, + SettingsWrapper } }, props: ['active', 'close'], @@ -87,6 +82,20 @@ SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView, MiGithubCircle, MiWeb, MiClose, MiTwitterCircle }, + computed: { + sidebarItems() { + return this.BdMenuItems.items; + }, + sidebar() { + const categories = {}; + for (let item of this.sidebarItems) { + if (item.hidden) continue; + const category = categories[item.category] || (categories[item.category] = []); + category.push(item); + } + return categories; + } + }, methods: { itemOnClick(id) { if (this.animating || id === this.activeIndex) return; diff --git a/client/src/ui/components/BdSettingsWrapper.vue b/client/src/ui/components/BdSettingsWrapper.vue index facb0333..49b273ec 100644 --- a/client/src/ui/components/BdSettingsWrapper.vue +++ b/client/src/ui/components/BdSettingsWrapper.vue @@ -20,7 +20,8 @@ diff --git a/client/src/ui/components/bd/setting/Setting.vue b/client/src/ui/components/bd/setting/Setting.vue index 4132f44d..147060c0 100644 --- a/client/src/ui/components/bd/setting/Setting.vue +++ b/client/src/ui/components/bd/setting/Setting.vue @@ -10,17 +10,18 @@ @@ -33,10 +34,11 @@ import StringSetting from './String.vue'; import MultilineTextSetting from './Multiline.vue'; import SliderSetting from './Slider.vue'; + import ColourSetting from './Colour.vue'; + import KeybindSetting from './Keybind.vue'; import FileSetting from './File.vue'; import ArraySetting from './Array.vue'; import CustomSetting from './Custom.vue'; - import ColourSetting from './Colour.vue'; export default { props: [ @@ -50,10 +52,11 @@ StringSetting, MultilineTextSetting, SliderSetting, + ColourSetting, + KeybindSetting, FileSetting, ArraySetting, - CustomSetting, - ColourSetting + CustomSetting }, computed: { changed() { diff --git a/client/src/ui/ui.js b/client/src/ui/ui.js index 4a24e0cb..7c3d8b7f 100644 --- a/client/src/ui/ui.js +++ b/client/src/ui/ui.js @@ -1,5 +1,6 @@ export { default as DOM } from './dom'; export { default as BdUI } from './bdui'; export { default as VueInjector } from './vueinjector'; +export * from './bdmenu'; export { default as Modals } from './modals'; export { default as ProfileBadges } from './profilebadges'; diff --git a/common/modules/monkeypatch.js b/common/modules/monkeypatch.js index 75ab45a7..a7fe0da0 100644 --- a/common/modules/monkeypatch.js +++ b/common/modules/monkeypatch.js @@ -10,10 +10,15 @@ import { ClientLogger as Logger } from './logger'; +const patchedFunctions = new WeakMap(); + export class PatchedFunction { constructor(object, methodName, replaceOriginal = true) { - if (object[methodName].__monkeyPatch) - return object[methodName].__monkeyPatch; + if (patchedFunctions.has(object[methodName])) { + const patchedFunction = patchedFunctions.get(object[methodName]); + if (replaceOriginal) patchedFunction.replaceOriginal(); + return patchedFunction; + } this.object = object; this.methodName = methodName; @@ -25,7 +30,9 @@ export class PatchedFunction { this.replace = function(...args) { patchedFunction.call(this, arguments); }; - this.replace.__monkeyPatch = this; + + patchedFunctions.set(object[methodName], this); + patchedFunctions.set(this.replace, this); if (replaceOriginal) this.replaceOriginal(); diff --git a/common/modules/utils.js b/common/modules/utils.js index 2f1e829a..99aecfa2 100644 --- a/common/modules/utils.js +++ b/common/modules/utils.js @@ -146,12 +146,14 @@ export class Utils { return value; } - static deepfreeze(object) { + static deepfreeze(object, exclude) { + if (exclude && exclude(object)) return; + if (typeof object === 'object' && object !== null) { const properties = Object.getOwnPropertyNames(object); for (let property of properties) { - this.deepfreeze(object[property]); + this.deepfreeze(object[property], exclude); } Object.freeze(object); @@ -159,6 +161,13 @@ export class Utils { return object; } + + static removeFromArray(array, item) { + let index; + while ((index = array.indexOf(item)) > -1) + array.splice(index, 1); + return array; + } } export class FileUtils { diff --git a/package-lock.json b/package-lock.json index b6bca9da..15334a32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1323,6 +1323,15 @@ "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", "dev": true }, + "binary-search-tree": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/binary-search-tree/-/binary-search-tree-0.2.5.tgz", + "integrity": "sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=", + "dev": true, + "requires": { + "underscore": "1.4.4" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -2023,6 +2032,12 @@ "delayed-stream": "1.0.0" } }, + "combokeys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/combokeys/-/combokeys-3.0.0.tgz", + "integrity": "sha1-lVxZo5Wa9A0mhGq2/DxoJEjnVy4=", + "dev": true + }, "commander": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", @@ -5652,6 +5667,12 @@ "integrity": "sha1-YSKJv7PCIOGGpYEYYY1b6MG6sCE=", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6293,6 +6314,15 @@ "type-check": "0.3.2" } }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "dev": true, + "requires": { + "immediate": "3.0.6" + } + }, "liftoff": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", @@ -6347,6 +6377,15 @@ "json5": "0.5.1" } }, + "localforage": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.6.0.tgz", + "integrity": "sha1-iwBZvus4dcSBJChsp/2/I9UrjJc=", + "dev": true, + "requires": { + "lie": "3.1.1" + } + }, "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", @@ -6979,6 +7018,27 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nedb": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/nedb/-/nedb-1.8.0.tgz", + "integrity": "sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=", + "dev": true, + "requires": { + "async": "0.2.10", + "binary-search-tree": "0.2.5", + "localforage": "1.6.0", + "mkdirp": "0.5.1", + "underscore": "1.4.4" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + } + } + }, "neo-async": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.0.tgz", @@ -10569,6 +10629,12 @@ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", "dev": true }, + "underscore": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", + "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=", + "dev": true + }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", diff --git a/package.json b/package.json index 55e0f401..d31ba623 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "codemirror": "^5.23.0", + "combokeys": "^3.0.0", "css-loader": "^0.28.9", "electron": "^1.6.15", "electron-rebuild": "^1.7.3", diff --git a/tests/plugins/Example 4/config.json b/tests/plugins/Example 4/config.json index c38da573..1c06afe2 100644 --- a/tests/plugins/Example 4/config.json +++ b/tests/plugins/Example 4/config.json @@ -77,6 +77,13 @@ ] } ] + }, + { + "id": "keybind-1", + "type": "keybind", + "value": "mod+.", + "text": "Test Keybind Setting 1", + "hint": "Test Keybind Setting Hint 1" } ] } diff --git a/tests/plugins/Example 4/index.js b/tests/plugins/Example 4/index.js index a42c60ed..7cc502ca 100644 --- a/tests/plugins/Example 4/index.js +++ b/tests/plugins/Example 4/index.js @@ -1,5 +1,7 @@ -module.exports = (Plugin, { Logger, Settings }) => class extends Plugin { +module.exports = (Plugin, { Logger, Settings, Modals, BdMenu: { BdMenuItems }, Api }) => class extends Plugin { async onstart() { + this.keybindEvent = this.keybindEvent.bind(this); + // Some array event examples const arraySetting = this.settings.getSetting('default', 'array-1'); Logger.log('Array setting', arraySetting); @@ -7,8 +9,15 @@ module.exports = (Plugin, { Logger, Settings }) => class extends Plugin { arraySetting.on('item-updated', event => Logger.log('Item', event.item, 'of the array setting was updated', event)); arraySetting.on('item-removed', event => Logger.log('Item', event.item, 'removed from the array setting')); - // Create a new settings set and show it in a modal - const set = Settings.createSet({}); + // Keybind setting examples + const keybindSetting = this.settings.getSetting('default', 'keybind-1'); + Logger.log('Keybind setting', keybindSetting); + keybindSetting.on('keybind-activated', this.keybindEvent); + + // Create a new settings set and add it to the menu + const set = Settings.createSet({ + text: this.name + }); const category = await set.addCategory({ id: 'default' }); const setting = await category.addSetting({ @@ -33,6 +42,37 @@ module.exports = (Plugin, { Logger, Settings }) => class extends Plugin { set.setSaved(); }) - set.showModal('Custom settings panel'); + const setting2 = await category.addSetting({ + id: 'setting-2', + type: 'text', + text: 'Enter some text', + fullwidth: true + }); + + setting2.on('setting-updated', event => Logger.log('Setting 2 was changed to', event.value)); + + this.menuItem = BdMenuItems.addSettingsSet('Plugins', set, 'Plugin 4'); + + this.menuItem2 = BdMenuItems.addVueComponent('Plugins', 'Also Plugin 4', { + template: ` +

Test

+
`, + props: ['SettingsWrapper'], + data() { return { + Api, plugin: Api.plugin + }; } + }); } + + onstop() { + const keybindSetting = this.settings.getSetting('default', 'keybind-1'); + keybindSetting.off('keybind-activated', this.keybindEvent); + + BdMenuItems.removeAll(); + } + + keybindEvent(event) { + Logger.log('Keybind pressed', event); + Modals.basic('Example Plugin 4', 'Test keybind activated.'); + } };