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 @@
Test
+