diff --git a/client/src/modules/content.js b/client/src/modules/content.js
new file mode 100644
index 00000000..cba21ed6
--- /dev/null
+++ b/client/src/modules/content.js
@@ -0,0 +1,162 @@
+/**
+ * BetterDiscord Content Base
+ * 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, FileUtils, ClientLogger as Logger, AsyncEventEmitter } from 'common';
+import { Modals } from 'ui';
+
+export default class Content {
+
+ constructor(internals) {
+ this.__internals = internals;
+
+ this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
+ this.settings.on('settings-updated', event => this.events.emit('settings-updated', event));
+ this.settings.on('settings-updated', event => this.__settingsUpdated(event));
+
+ // Add hooks
+ if (this.onstart) this.on('start', event => this.onstart(event));
+ if (this.onStart) this.on('start', event => this.onStart(event));
+ if (this.onstop) this.on('stop', event => this.onstop(event));
+ if (this.onStop) this.on('stop', event => this.onStop(event));
+ if (this.onunload) this.on('unload', event => this.onunload(event));
+ if (this.onUnload) this.on('unload', event => this.onUnload(event));
+ if (this.settingUpdated) this.on('setting-updated', event => this.settingUpdated(event));
+ if (this.settingsUpdated) this.on('settings-updated', event => this.settingsUpdated(event));
+ }
+
+ get type() { return undefined }
+ get configs() { return this.__internals.configs }
+ get info() { return this.__internals.info }
+ get paths() { return this.__internals.paths }
+ get main() { return this.__internals.main }
+ get defaultConfig() { return this.configs.defaultConfig }
+ get userConfig() { return this.configs.userConfig }
+ get configSchemes() { return this.configs.schemes }
+ get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
+ get name() { return this.info.name }
+ get icon() { return this.info.icon }
+ get description() { return this.info.description }
+ get authors() { return this.info.authors }
+ get version() { return this.info.version }
+ get contentPath() { return this.paths.contentPath }
+ get dirName() { return this.paths.dirName }
+ get enabled() { return this.userConfig.enabled }
+ get settings() { return this.userConfig.config }
+ get config() { return this.settings.categories }
+ get data() { return this.userConfig.data || (this.userConfig.data = {}) }
+ get events() { return this.EventEmitter || (this.EventEmitter = new AsyncEventEmitter()) }
+
+ /**
+ * Opens a settings modal for this content.
+ */
+ showSettingsModal() {
+ return Modals.contentSettings(this);
+ }
+
+ /**
+ * Whether this content has any settings.
+ */
+ get hasSettings() {
+ return !!this.settings.findSetting(() => true);
+ }
+
+ /**
+ * Saves the content's current configuration.
+ */
+ async saveConfiguration() {
+ try {
+ await FileUtils.writeFile(`${this.contentPath}/user.config.json`, JSON.stringify({
+ enabled: this.enabled,
+ config: this.settings.strip().settings,
+ data: this.data
+ }));
+
+ this.settings.setSaved();
+ } catch (err) {
+ Logger.err(this.name, ['Failed to save configuration', err]);
+ throw err;
+ }
+ }
+
+ /**
+ * Called when settings are updated.
+ * This can be overridden by other content types.
+ */
+ __settingsUpdated(event) {
+ return this.saveConfiguration();
+ }
+
+ /**
+ * Enables the content.
+ * @param {Boolean} save Whether to save the new enabled state
+ * @return {Promise}
+ */
+ async enable(save = true) {
+ if (this.enabled) return;
+ await this.emit('enable');
+ await this.emit('start');
+
+ this.userConfig.enabled = true;
+ if (save) await this.saveConfiguration();
+ }
+
+ /**
+ * Disables the content.
+ * @param {Boolean} save Whether to save the new enabled state
+ * @return {Promise}
+ */
+ async disable(save = true) {
+ if (!this.enabled) return;
+ await this.emit('stop');
+ await this.emit('disable');
+
+ this.userConfig.enabled = false;
+ if (save) await this.saveConfiguration();
+ }
+
+ /**
+ * Adds an event listener.
+ * @param {String} event The event to add the listener to
+ * @param {Function} callback The function to call when the event is emitted
+ */
+ on(...args) {
+ return this.events.on(...args);
+ }
+
+ /**
+ * Removes an event listener.
+ * @param {String} event The event to remove the listener from
+ * @param {Function} callback The bound callback (optional)
+ */
+ off(...args) {
+ return this.events.removeListener(...args);
+ }
+
+ /**
+ * Adds an event listener that removes itself when called, therefore only being called once.
+ * @param {String} event The event to add the listener to
+ * @param {Function} callback The function to call when the event is emitted
+ * @return {Promise|undefined}
+ */
+ once(...args) {
+ return this.events.once(...args);
+ }
+
+ /**
+ * Emits an event.
+ * @param {String} event The event to emit
+ * @param {Any} data Data to be passed to listeners
+ * @return {Promise|undefined}
+ */
+ emit(...args) {
+ return this.events.emit(...args);
+ }
+
+}
diff --git a/client/src/modules/contentmanager.js b/client/src/modules/contentmanager.js
index b5620b87..789a7975 100644
--- a/client/src/modules/contentmanager.js
+++ b/client/src/modules/contentmanager.js
@@ -8,6 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
+import Content from './content';
import Globals from './globals';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
@@ -245,17 +246,19 @@ export default class {
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
try {
- if (content.enabled && content.disable) content.disable(false);
- if (content.enabled && content.stop) content.stop(false);
- if (content.onunload) content.onunload(reload);
- if (content.onUnload) content.onUnload(reload);
+ await content.disable(false);
+ await content.emit('unload', reload);
+
const index = this.getContentIndex(content);
delete window.require.cache[window.require.resolve(content.paths.mainPath)];
if (reload) {
const newcontent = await this.preloadContent(content.dirName, true, index);
- if (newcontent.enabled && newcontent.start) newcontent.start(false);
+ if (newcontent.enabled) {
+ newcontent.userConfig.enabled = false;
+ newcontent.start(false);
+ }
return newcontent;
} else this.localContent.splice(index, 1);
} catch (err) {
@@ -268,7 +271,7 @@ export default class {
* Reload content
* @param {any} content Content to reload
*/
- static async reloadContent(content) {
+ static reloadContent(content) {
return this.unloadContent(content, true);
}
@@ -295,12 +298,20 @@ export default class {
* @param {any} content Object to check
*/
static isThisContent(content) {
- return false;
+ return content instanceof Content;
+ }
+
+ /**
+ * Returns the first content where calling {function} returns true.
+ * @param {Function} function A function to call to filter content
+ */
+ static find(f) {
+ return this.localContent.find(f);
}
/**
* Wildcard content finder
- * @param {any} wild Content name | id | path | dirname
+ * @param {any} wild Content ID / directory name / path / name
* @param {bool} nonunique Allow searching attributes that may not be unique
*/
static findContent(wild, nonunique) {
@@ -313,10 +324,10 @@ export default class {
}
static getContentIndex(content) { return this.localContent.findIndex(c => c === content) }
- static getContentByName(name) { return this.localContent.find(c => c.name === name) }
static getContentById(id) { return this.localContent.find(c => c.id === id) }
- static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
static getContentByDirName(dirName) { return this.localContent.find(c => c.dirName === dirName) }
+ static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
+ static getContentByName(name) { return this.localContent.find(c => c.name === name) }
/**
* Wait for content to load
diff --git a/client/src/modules/extmodule.js b/client/src/modules/extmodule.js
index 43f37811..4b8fb0db 100644
--- a/client/src/modules/extmodule.js
+++ b/client/src/modules/extmodule.js
@@ -8,37 +8,15 @@
* LICENSE file in the root directory of this source tree.
*/
-import { AsyncEventEmitter } from 'common';
-import { EventEmitter } from 'events';
+import Content from './content';
-export default class ExtModule {
+export default class ExtModule extends Content {
- constructor(pluginInternals) {
- this.__pluginInternals = pluginInternals;
+ constructor(internals) {
+ super(internals);
this.__require = window.require(this.paths.mainPath);
- this.hasSettings = false;
}
get type() { return 'module' }
- get configs() { return this.__pluginInternals.configs }
- get info() { return this.__pluginInternals.info }
- get icon() { return this.info.icon }
- get paths() { return this.__pluginInternals.paths }
- get main() { return this.__pluginInternals.main }
- get defaultConfig() { return this.configs.defaultConfig }
- get userConfig() { return this.configs.userConfig }
- get configSchemes() { return this.configs.schemes }
- get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
- get name() { return this.info.name }
- get description() { return this.info.description }
- get authors() { return this.info.authors }
- get version() { return this.info.version }
- get contentPath() { return this.paths.contentPath }
- get modulePath() { return this.paths.contentPath }
- get dirName() { return this.paths.dirName }
- get enabled() { return true }
- get config() { return this.userConfig.config || [] }
- get data() { return this.userConfig.data || (this.userConfig.data = {}) }
- get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new AsyncEventEmitter()) }
}
diff --git a/client/src/modules/plugin.js b/client/src/modules/plugin.js
index 889e2b4d..7b786eff 100644
--- a/client/src/modules/plugin.js
+++ b/client/src/modules/plugin.js
@@ -8,93 +8,19 @@
* LICENSE file in the root directory of this source tree.
*/
-import { Utils, FileUtils, AsyncEventEmitter } from 'common';
-import { Modals } from 'ui';
-import { EventEmitter } from 'events';
import PluginManager from './pluginmanager';
-import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
+import Content from './content';
-export default class Plugin {
-
- constructor(pluginInternals) {
- this.__pluginInternals = pluginInternals;
- this.saveConfiguration = this.saveConfiguration.bind(this);
- this.hasSettings = this.config && this.config.length > 0;
- this.start = this.start.bind(this);
- this.stop = this.stop.bind(this);
-
- this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
- this.settings.on('settings-updated', event => this.events.emit('settings-updated', event));
- this.settings.on('settings-updated', event => this.saveConfiguration());
- }
+export default class Plugin extends Content {
get type() { return 'plugin' }
- get configs() { return this.__pluginInternals.configs }
- get info() { return this.__pluginInternals.info }
- get icon() { return this.info.icon }
- get paths() { return this.__pluginInternals.paths }
- get main() { return this.__pluginInternals.main }
- get defaultConfig() { return this.configs.defaultConfig }
- get userConfig() { return this.configs.userConfig }
- get configSchemes() { return this.configs.schemes }
- get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
- get name() { return this.info.name }
- get description() { return this.info.description }
- get authors() { return this.info.authors }
- get version() { return this.info.version }
- get contentPath() { return this.paths.contentPath }
- get pluginPath() { return this.paths.contentPath }
- get dirName() { return this.paths.dirName }
- get enabled() { return this.userConfig.enabled }
- get settings() { return this.userConfig.config }
- get config() { return this.settings.settings }
+
+ // Don't use - these will eventually be removed!
+ get pluginPath() { return this.contentPath }
get pluginConfig() { return this.config }
- get data() { return this.userConfig.data || (this.userConfig.data = {}) }
- get exports() { return this._exports ? this._exports : (this._exports = this.getExports()) }
- get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new AsyncEventEmitter()) }
- showSettingsModal() {
- return Modals.contentSettings(this);
- }
-
- async saveConfiguration() {
- try {
- await FileUtils.writeFile(`${this.pluginPath}/user.config.json`, JSON.stringify({
- enabled: this.enabled,
- config: this.settings.strip().settings,
- data: this.data
- }));
-
- this.settings.setSaved();
- } catch (err) {
- console.error(`Plugin ${this.id} configuration failed to save`, err);
- throw err;
- }
- }
-
- start(save = true) {
- if (this.onstart && !this.onstart()) return false;
- if (this.onStart && !this.onStart()) return false;
-
- if (!this.enabled) {
- this.userConfig.enabled = true;
- if (save) this.saveConfiguration();
- }
-
- return true;
- }
-
- stop(save = true) {
- if (this.onstop && !this.onstop()) return false;
- if (this.onStop && !this.onStop()) return false;
-
- if (this.enabled) {
- this.userConfig.enabled = false;
- if (save) this.saveConfiguration();
- }
-
- return true;
- }
+ get start() { return this.enable }
+ get stop() { return this.disable }
unload() {
PluginManager.unloadPlugin(this);
diff --git a/client/src/modules/pluginapi.js b/client/src/modules/pluginapi.js
index 7b628259..dd07cf35 100644
--- a/client/src/modules/pluginapi.js
+++ b/client/src/modules/pluginapi.js
@@ -14,6 +14,7 @@ import ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager';
import ThemeManager from './thememanager';
import Events from './events';
+import WebpackModules from './webpackmodules';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { Modals, DOM } from 'ui';
import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue';
@@ -63,6 +64,24 @@ export default class PluginApi {
return PluginManager.getPluginById(this.pluginInfo.id || this.pluginInfo.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-'));
}
+ async bridge(plugin_id) {
+ const plugin = await PluginManager.waitForPlugin(plugin_id);
+ return plugin.bridge;
+ }
+
+ get require() { return this.import }
+ import(m) {
+ const module = ExtModuleManager.findModule(m);
+ if (module && module.__require) return module.__require;
+ return null;
+ }
+
+ get Api() { return this }
+
+ /**
+ * Logger
+ */
+
loggerLog(...message) { Logger.log(this.pluginInfo.name, message) }
loggerErr(...message) { Logger.err(this.pluginInfo.name, message) }
loggerWarn(...message) { Logger.warn(this.pluginInfo.name, message) }
@@ -78,6 +97,10 @@ export default class PluginApi {
};
}
+ /**
+ * Utils
+ */
+
get Utils() {
return {
overload: () => Utils.overload.apply(Utils, arguments),
@@ -92,8 +115,12 @@ export default class PluginApi {
};
}
+ /**
+ * Settings
+ */
+
createSettingsSet(args, ...merge) {
- return new SettingsSet(args, ...merge);
+ return new SettingsSet(args || {}, ...merge);
}
createSettingsCategory(args, ...merge) {
return new SettingsCategory(args, ...merge);
@@ -106,13 +133,17 @@ export default class PluginApi {
}
get Settings() {
return {
- createSet: this.createSet.bind(this),
+ createSet: this.createSettingsSet.bind(this),
createCategory: this.createSettingsCategory.bind(this),
createSetting: this.createSetting.bind(this),
createScheme: this.createSettingsScheme.bind(this)
};
}
+ /**
+ * InternalSettings
+ */
+
getInternalSetting(set, category, setting) {
return Settings.get(set, category, setting);
}
@@ -122,6 +153,10 @@ export default class PluginApi {
};
}
+ /**
+ * CssUtils
+ */
+
get injectedStyles() {
return this._injectedStyles || (this._injectedStyles = []);
}
@@ -169,36 +204,48 @@ export default class PluginApi {
};
}
+ /**
+ * Modals
+ */
+
get modalStack() {
return this._modalStack || (this._modalStack = []);
}
+ get baseModalComponent() {
+ return Modals.baseComponent;
+ }
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)
+ this.modalStack.splice(index, 1);
+ });
this.modalStack.push(modal);
return modal;
}
- async closeModal(modal, force) {
- await Modals.close(modal, force);
- this._modalStack = this.modalStack.filter(m => m !== modal);
+ closeModal(modal, force) {
+ return Modals.close(modal, force);
}
- closeAllModals() {
+ closeAllModals(force) {
+ const promises = [];
for (let modal of this.modalStack)
- modal.close();
+ promises.push(modal.close(force));
+ return Promise.all(promises);
}
- closeLastModal() {
+ closeLastModal(force) {
if (!this.modalStack.length) return;
- this.modalStack[this.modalStack.length - 1].close();
+ return this.modalStack[this.modalStack.length - 1].close(force);
+ }
+ basicModal(title, text) {
+ return this.addModal(Modals.basic(title, text));
}
settingsModal(settingsset, headertext, options) {
- return this.addModal(Object.assign({
- headertext: headertext ? headertext : settingsset.headertext,
- settings: settingsset,
- schemes: settingsset.schemes
- }, options), SettingsModal);
+ return this.addModal(Modals.settings(settingsset, headertext, options));
}
get Modals() {
- return Object.defineProperty({
+ return Object.defineProperty(Object.defineProperty({
add: this.addModal.bind(this),
close: this.closeModal.bind(this),
closeAll: this.closeAllModals.bind(this),
@@ -206,14 +253,20 @@ export default class PluginApi {
settings: this.settingsModal.bind(this)
}, 'stack', {
get: () => this.modalStack
+ }), 'baseComponent', {
+ get: () => this.baseModalComponent
});
}
+ /**
+ * Plugins
+ */
+
async getPlugin(plugin_id) {
// This should require extra permissions
return await PluginManager.waitForPlugin(plugin_id);
}
- listPlugins(plugin_id) {
+ listPlugins() {
return PluginManager.localContent.map(plugin => plugin.id);
}
get Plugins() {
@@ -223,30 +276,75 @@ export default class PluginApi {
};
}
+ /**
+ * Themes
+ */
+
async getTheme(theme_id) {
// This should require extra permissions
return await ThemeManager.waitForContent(theme_id);
}
- listThemes(plugin_id) {
+ listThemes() {
return ThemeManager.localContent.map(theme => theme.id);
}
get Themes() {
return {
getTheme: this.getTheme.bind(this),
- getThemes: this.listThemes.bind(this)
+ listThemes: this.listThemes.bind(this)
};
}
- async bridge(plugin_id) {
- const plugin = await PluginManager.waitForPlugin(plugin_id);
- return plugin.bridge;
+ /**
+ * ExtModules
+ */
+
+ async getModule(module_id) {
+ // This should require extra permissions
+ return await ExtModuleManager.waitForContent(module_id);
+ }
+ listModules() {
+ return ExtModuleManager.localContent.map(module => module.id);
+ }
+ get ExtModules() {
+ return {
+ getModule: this.getModule.bind(this),
+ listModules: this.listModules.bind(this)
+ };
}
- get require() { return this.import }
- import(m) {
- const module = ExtModuleManager.findModule(m);
- if (module && module.__require) return module.__require;
- return null;
+ /**
+ * WebpackModules
+ */
+
+ get webpackRequire() {
+ return WebpackModules.require;
+ }
+ getWebpackModule(filter, first = true) {
+ return WebpackModules.getModule(filter, first);
+ }
+ getWebpackModuleByName(name, fallback) {
+ return WebpackModules.getModuleByName(name, fallback);
+ }
+ getWebpackModuleByRegex(regex, first = true) {
+ return WebpackModules.getModuleByRegex(regex, first);
+ }
+ getWebpackModuleByProperties(props, first = true) {
+ return WebpackModules.getModuleByProps(props, first);
+ }
+ getWebpackModuleByPrototypeFields(props, first = true) {
+ return WebpackModules.getModuleByPrototypes(props, first);
+ }
+ get WebpackModules() {
+ return Object.defineProperty({
+ getModule: this.getWebpackModule.bind(this),
+ getModuleByName: this.getWebpackModuleByName.bind(this),
+ getModuleByDisplayName: this.getWebpackModuleByName.bind(this),
+ getModuleByRegex: this.getWebpackModuleByRegex.bind(this),
+ getModuleByProperties: this.getWebpackModuleByProperties.bind(this),
+ getModuleByPrototypeFields: this.getWebpackModuleByPrototypeFields.bind(this)
+ }, 'require', {
+ get: () => this.webpackRequire
+ });
}
}
diff --git a/client/src/modules/pluginmanager.js b/client/src/modules/pluginmanager.js
index d042f915..6f8a3b12 100644
--- a/client/src/modules/pluginmanager.js
+++ b/client/src/modules/pluginmanager.js
@@ -41,11 +41,13 @@ export default class extends ContentManager {
const loadAll = await this.loadAllContent(true);
this.loaded = true;
for (let plugin of this.localPlugins) {
+ if (!plugin.enabled) continue;
+ plugin.userConfig.enabled = false;
+
try {
- if (plugin.enabled) plugin.start();
+ plugin.start(false);
} catch (err) {
// Disable the plugin but don't save it - the next time BetterDiscord is started the plugin will attempt to start again
- plugin.userConfig.enabled = false;
this.errors.push(new ErrorEvent({
module: this.moduleName,
message: `Failed to start ${plugin.name}`,
@@ -72,7 +74,6 @@ export default class extends ContentManager {
static get loadContent() { return this.loadPlugin }
static async loadPlugin(paths, configs, info, main, dependencies, permissions) {
-
if (permissions && permissions.length > 0) {
for (let perm of permissions) {
console.log(`Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
@@ -107,7 +108,10 @@ export default class extends ContentManager {
}
});
- if (instance.enabled && this.loaded) instance.start();
+ if (instance.enabled && this.loaded) {
+ instance.userConfig.enabled = false;
+ instance.start(false);
+ }
return instance;
}
diff --git a/client/src/modules/theme.js b/client/src/modules/theme.js
index da96e5ae..bd68cef9 100644
--- a/client/src/modules/theme.js
+++ b/client/src/modules/theme.js
@@ -8,26 +8,17 @@
* LICENSE file in the root directory of this source tree.
*/
+import Content from './content';
import Settings from './settings';
import ThemeManager from './thememanager';
-import { EventEmitter } from 'events';
-import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
-import { DOM, Modals } from 'ui';
-import { Utils, FileUtils, ClientIPC, ClientLogger as Logger, AsyncEventEmitter } from 'common';
+import { DOM } from 'ui';
+import { FileUtils, ClientIPC, ClientLogger as Logger } from 'common';
import filewatcher from 'filewatcher';
-export default class Theme {
+export default class Theme extends Content {
- constructor(themeInternals) {
- this.__themeInternals = themeInternals;
- this.hasSettings = this.config && this.config.length > 0;
- this.saveConfiguration = this.saveConfiguration.bind(this);
- this.enable = this.enable.bind(this);
- this.disable = this.disable.bind(this);
-
- this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
- this.settings.on('settings-updated', event => this.events.emit('settings-updated', event));
- this.settings.on('settings-updated', event => this.recompile());
+ constructor(internals) {
+ super(internals);
const watchfiles = Settings.getSetting('css', 'default', 'watch-files');
if (watchfiles.value) this.watchfiles = this.files;
@@ -37,63 +28,39 @@ export default class Theme {
});
}
- get configs() { return this.__themeInternals.configs }
- get info() { return this.__themeInternals.info }
- get icon() { return this.info.icon }
- get paths() { return this.__themeInternals.paths }
- get main() { return this.__themeInternals.main }
- get loaded() { return this.__themeInternals.loaded }
- get defaultConfig() { return this.configs.defaultConfig }
- get userConfig() { return this.configs.userConfig }
- get configSchemes() { return this.configs.schemes }
- get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/\s+/g, '-') }
- get name() { return this.info.name }
- get description() { return this.info.description }
- get authors() { return this.info.authors }
- get version() { return this.info.version }
- get contentPath() { return this.paths.contentPath }
- get themePath() { return this.paths.contentPath }
- get dirName() { return this.paths.dirName }
- get enabled() { return this.userConfig.enabled }
- get settings() { return this.userConfig.config }
- get config() { return this.settings.settings }
- get themeConfig() { return this.config }
- get data() { return this.userConfig.data || (this.userConfig.data = {}) }
+ get type() { return 'theme' }
get css() { return this.data.css }
- get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new AsyncEventEmitter()) }
- showSettingsModal() {
- return Modals.contentSettings(this);
+ // Don't use - these will eventually be removed!
+ get themePath() { return this.contentPath }
+ get themeConfig() { return this.config }
+
+ /**
+ * Called when settings are updated.
+ * This can be overridden by other content types.
+ */
+ __settingsUpdated(event) {
+ return this.recompile();
}
- async saveConfiguration() {
- try {
- await FileUtils.writeFile(`${this.themePath}/user.config.json`, JSON.stringify({
- enabled: this.enabled,
- config: this.settings.strip().settings,
- data: this.data
- }));
-
- this.settings.setSaved();
- } catch (err) {
- throw err;
- }
- }
-
- enable(save = true) {
- if (!this.enabled) {
- this.userConfig.enabled = true;
- if (save) this.saveConfiguration();
- }
+ /**
+ * This is called when the theme is enabled.
+ */
+ onstart() {
DOM.injectTheme(this.css, this.id);
}
- disable(save = true) {
- this.userConfig.enabled = false;
- if (save) this.saveConfiguration();
+ /**
+ * This is called when the theme is disabled.
+ */
+ onstop() {
DOM.deleteTheme(this.id);
}
+ /**
+ * Compiles the theme and returns an object containing the CSS and an array of files that were included.
+ * @return {Promise}
+ */
async compile() {
console.log('Compiling CSS');
@@ -117,11 +84,15 @@ export default class Theme {
};
} else {
return {
- css: FileUtils.readFile(this.paths.mainPath)
+ css: await FileUtils.readFile(this.paths.mainPath)
};
}
}
+ /**
+ * Compiles the theme and updates and saves the CSS and the list of include files.
+ * @return {Promise}
+ */
async recompile() {
const data = await this.compile();
this.data.css = data.css;
@@ -136,7 +107,7 @@ export default class Theme {
}
/**
- * An array of files that are imported in custom CSS.
+ * An array of files that are imported in the theme's SCSS.
* @return {Array} Files being watched
*/
get files() {
@@ -144,7 +115,7 @@ export default class Theme {
}
/**
- * Sets all files that are imported in custom CSS.
+ * Sets all files that are imported in the theme's SCSS.
* @param {Array} files Files to watch
*/
set files(files) {
diff --git a/client/src/modules/webpackmodules.js b/client/src/modules/webpackmodules.js
index 8416f810..1f173c4d 100644
--- a/client/src/modules/webpackmodules.js
+++ b/client/src/modules/webpackmodules.js
@@ -86,7 +86,6 @@ const KnownModules = {
UserActivityStore: Filters.byProperties(['getActivity']),
UserNameResolver: Filters.byProperties(['getName']),
-
/* Emoji Store and Utils */
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
EmojiUtils: Filters.byProperties(['diversitySurrogate']),
@@ -97,7 +96,6 @@ const KnownModules = {
InviteResolver: Filters.byProperties(['findInvite']),
InviteActions: Filters.byProperties(['acceptInvite']),
-
/* Discord Objects & Utils */
DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]),
Permissions: Filters.byProperties(['getHighestRole']),
@@ -122,7 +120,6 @@ const KnownModules = {
ExperimentsManager: Filters.byProperties(['isDeveloper']),
CurrentExperiment: Filters.byProperties(['getExperimentId']),
-
/* Images, Avatars and Utils */
ImageResolver: Filters.byProperties(["getUserAvatarURL"]),
ImageUtils: Filters.byProperties(['getSizedImageSrc']),
@@ -176,7 +173,6 @@ const KnownModules = {
URLParser: Filters.byProperties(['Url', 'parse']),
ExtraURLs: Filters.byProperties(['getArticleURL']),
-
/* DOM/React Components */
/* ==================== */
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
@@ -201,60 +197,114 @@ const KnownModules = {
ExternalLink: Filters.byCode(/\.trusted\b/)
};
-export default class {
- /* Synchronous */
+export default class WebpackModules {
+
+ /**
+ * Finds a module using a filter function.
+ * @param {Function} filter A function to use to filter modules
+ * @param {Boolean} first Whether to return only the first matching module
+ * @return {Any}
+ */
+ static getModule(filter, first = true) {
+ const modules = this.getAllModules();
+ const rm = [];
+ for (let index in modules) {
+ if (!modules.hasOwnProperty(index)) continue;
+ const module = modules[index];
+ const { exports } = module;
+ let foundModule = null;
+
+ if (!exports) continue;
+ if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
+ if (filter(exports)) foundModule = exports;
+ if (!foundModule) continue;
+ if (first) return foundModule;
+ rm.push(foundModule);
+ }
+ return first || rm.length == 0 ? undefined : rm;
+ }
+
+ /**
+ * Finds a module by it's name.
+ * @param {String} name The name of the module
+ * @param {Function} fallback A function to use to filter modules if not finding a known module
+ * @return {Any}
+ */
static getModuleByName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name];
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
- if (!fallback) return null;
- return Cache[name] = this.getModule(fallback, true);
+ if (!fallback) return undefined;
+ const module = this.getModule(fallback, true);
+ return module ? Cache[name] = module : undefined;
}
+ /**
+ * Finds a module by it's display name.
+ * @param {String} name The display name of the module
+ * @return {Any}
+ */
static getModuleByDisplayName(name) {
return this.getModule(Filters.byDisplayName(name), true);
}
+ /**
+ * Finds a module using it's code.
+ * @param {RegEx} regex A regular expression to use to filter modules
+ * @param {Boolean} first Whether to return the only the first matching module
+ * @return {Any}
+ */
static getModuleByRegex(regex, first = true) {
return this.getModule(Filters.byCode(regex), first);
}
+ /**
+ * Finds a module using properties on it's prototype.
+ * @param {Array} props Properties to use to filter modules
+ * @param {Boolean} first Whether to return only the first matching module
+ * @return {Any}
+ */
static getModuleByPrototypes(prototypes, first = true) {
return this.getModule(Filters.byPrototypeFields(prototypes), first);
}
+ /**
+ * Finds a module using it's own properties.
+ * @param {Array} props Properties to use to filter modules
+ * @param {Boolean} first Whether to return only the first matching module
+ * @return {Any}
+ */
static getModuleByProps(props, first = true) {
return this.getModule(Filters.byProperties(props), first);
}
- static getModule(filter, first = true) {
- const modules = this.getAllModules();
- const rm = [];
- for (let index in modules) {
- if (!modules.hasOwnProperty(index)) continue;
- const module = modules[index];
- const { exports } = module;
- let foundModule = null;
-
- if (!exports) continue;
- if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
- if (filter(exports)) foundModule = exports;
- if (!foundModule) continue;
- if (first) return foundModule;
- rm.push(foundModule);
- }
- return first || rm.length == 0 ? null : rm;
- }
-
- static getAllModules() {
+ /**
+ * Discord's __webpack_require__ function.
+ */
+ static get require() {
+ if (this._require) return this._require;
const id = 'bd-webpackmodules';
- const __webpack_require__ = window['webpackJsonp'](
- [],
- {
- [id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
- },
- [id]).default;
+ const __webpack_require__ = window['webpackJsonp']([], {
+ [id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
+ }, [id]).default;
delete __webpack_require__.m[id];
delete __webpack_require__.c[id];
- return __webpack_require__.c;
+ return this._require = __webpack_require__;
}
+
+ /**
+ * Returns all loaded modules.
+ * @return {Array}
+ */
+ static getAllModules() {
+ return this.require.c;
+ }
+
+ /**
+ * Returns an array of known modules.
+ * @return {Array}
+ */
+ static listKnownModules() {
+ return Object.keys(KnownModules);
+ }
+
}
diff --git a/client/src/structs/settings/settingscategory.js b/client/src/structs/settings/settingscategory.js
index 4ac07453..e9ecb7fa 100644
--- a/client/src/structs/settings/settingscategory.js
+++ b/client/src/structs/settings/settingscategory.js
@@ -9,6 +9,7 @@
*/
import Setting from './setting';
+import BaseSetting from './types/basesetting';
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
@@ -24,17 +25,12 @@ export default class SettingsCategory {
this._merge(newCategory);
}
+ this.__settingUpdated = this.__settingUpdated.bind(this);
+ this.__settingsUpdated = this.__settingsUpdated.bind(this);
+
for (let setting of this.settings) {
- setting.on('setting-updated', ({ value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({
- category: this, category_id: this.id,
- setting, setting_id: setting.id,
- value, old_value
- })));
- setting.on('settings-updated', ({ updatedSettings }) => this.emit('settings-updated', new SettingsUpdatedEvent({
- updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
- category: this, category_id: this.id
- }, updatedSetting)))
- })));
+ setting.on('setting-updated', this.__settingUpdated);
+ setting.on('settings-updated', this.__settingsUpdated);
}
}
@@ -53,7 +49,7 @@ export default class SettingsCategory {
* Category name
*/
get name() {
- return this.args.category_name;
+ return this.args.name || this.args.category_name;
}
get category_name() {
@@ -83,6 +79,82 @@ export default class SettingsCategory {
return false;
}
+ /**
+ * Setting event listeners.
+ * This only exists for use by the constructor and settingscategory.addSetting.
+ */
+ __settingUpdated({ setting, value, old_value }) {
+ return this.emit('setting-updated', new SettingUpdatedEvent({
+ category: this, category_id: this.id,
+ setting, setting_id: setting.id,
+ value, old_value
+ }));
+ }
+
+ __settingsUpdated({ updatedSettings }) {
+ return this.emit('settings-updated', new SettingsUpdatedEvent({
+ updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
+ category: this, category_id: this.id
+ }, updatedSetting)))
+ }));
+ }
+
+ /**
+ * Dynamically adds a setting to this category.
+ * @param {Setting} category The setting to add to this category
+ * @param {Number} index The index to add the setting at (optional)
+ * @return {Promise}
+ */
+ async addSetting(setting, index) {
+ if (this.settings.find(s => s === setting)) return;
+
+ if (!(setting instanceof BaseSetting))
+ setting = new Setting(setting);
+
+ if (this.getSetting(setting.id))
+ throw {message: 'A setting with this ID already exists.'};
+
+ setting.on('setting-updated', this.__settingUpdated);
+ setting.on('settings-updated', this.__settingsUpdated);
+
+ if (index === undefined) index = this.settings.length;
+ this.settings.splice(index, 0, setting);
+
+ const event = {
+ category: this, category_id: this.id,
+ setting, setting_id: setting.id,
+ at_index: index
+ };
+
+ await setting.emit('added-to', event);
+ await this.emit('added-setting', event);
+ return setting;
+ }
+
+ /**
+ * Dynamically removes a setting from this category.
+ * @param {Setting} setting The setting to remove from this category
+ * @return {Promise}
+ */
+ async removeSetting(setting) {
+ setting.off('setting-updated', this.__settingUpdated);
+ setting.off('settings-updated', this.__settingsUpdated);
+
+ let index;
+ while ((index = this.settings.findIndex(s => s === setting)) > -1) {
+ this.settings.splice(index, 0);
+ }
+
+ const event = {
+ set: this, set_id: this.id,
+ category: this, category_id: this.id,
+ from_index: index
+ };
+
+ await setting.emit('removed-from', event);
+ await this.emit('removed-category', event);
+ }
+
/**
* Returns the first setting where calling {function} returns true.
* @param {Function} function A function to call to filter settings
@@ -107,7 +179,7 @@ export default class SettingsCategory {
* @return {Setting}
*/
getSetting(id) {
- return this.findSetting(setting => setting.id === id);
+ return this.find(setting => setting.id === id);
}
/**
diff --git a/client/src/structs/settings/settingsset.js b/client/src/structs/settings/settingsset.js
index e488f6ef..5b324a69 100644
--- a/client/src/structs/settings/settingsset.js
+++ b/client/src/structs/settings/settingsset.js
@@ -27,18 +27,16 @@ export default class SettingsSet {
this._merge(newSet);
}
+ this.__settingUpdated = this.__settingUpdated.bind(this);
+ this.__settingsUpdated = this.__settingsUpdated.bind(this);
+ this.__addedSetting = this.__addedSetting.bind(this);
+ this.__removedSetting = this.__removedSetting.bind(this);
+
for (let category of this.categories) {
- category.on('setting-updated', ({ setting, value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({
- set: this, set_id: this.id,
- category, category_id: category.id,
- setting, setting_id: setting.id,
- value, old_value
- })));
- category.on('settings-updated', ({ updatedSettings }) => this.emit('settings-updated', new SettingsUpdatedEvent({
- updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
- set: this, set_id: this.id
- }, updatedSetting)))
- })));
+ category.on('setting-updated', this.__settingUpdated);
+ category.on('settings-updated', this.__settingsUpdated);
+ category.on('added-setting', this.__addedSetting);
+ category.on('removed-setting', this.__removedSetting);
}
}
@@ -101,6 +99,149 @@ export default class SettingsSet {
return false;
}
+ /**
+ * Category event listeners.
+ * These only exists for use by the constructor and settingsset.addCategory.
+ */
+ __settingUpdated({ category, setting, value, old_value }) {
+ return this.emit('setting-updated', new SettingUpdatedEvent({
+ set: this, set_id: this.id,
+ category, category_id: category.id,
+ setting, setting_id: setting.id,
+ value, old_value
+ }));
+ }
+
+ __settingsUpdated({ updatedSettings }) {
+ return this.emit('settings-updated', new SettingsUpdatedEvent({
+ updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
+ set: this, set_id: this.id
+ }, updatedSetting)))
+ }));
+ }
+
+ __addedSetting({ category, setting, at_index }) {
+ return this.emit('added-setting', {
+ set: this, set_id: this.id,
+ category, category_id: category.id,
+ setting, setting_id: setting.id,
+ at_index
+ });
+ }
+
+ __removedSetting({ category, setting, from_index }) {
+ return this.emit('removed-setting', {
+ set: this, set_id: this.id,
+ category, category_id: category.id,
+ setting, setting_id: setting.id,
+ from_index
+ });
+ }
+
+ /**
+ * Dynamically adds a category to this set.
+ * @param {SettingsCategory} category The category to add to this set
+ * @param {Number} index The index to add the category at (optional)
+ * @return {Promise}
+ */
+ async addCategory(category, index) {
+ if (this.categories.find(c => c === category)) return;
+
+ if (!(category instanceof SettingsCategory))
+ category = new SettingsCategory(category);
+
+ if (this.getCategory(category.id))
+ throw {message: 'A category with this ID already exists.'};
+
+ category.on('setting-updated', this.__settingUpdated);
+ category.on('settings-updated', this.__settingsUpdated);
+ category.on('added-setting', this.__addedSetting);
+ category.on('removed-setting', this.__removedSetting);
+
+ if (index === undefined) index = this.categories.length;
+ this.categories.splice(index, 0, category);
+
+ const event = {
+ set: this, set_id: this.id,
+ category, category_id: category.id,
+ at_index: index
+ };
+
+ await category.emit('added-to', event);
+ await this.emit('added-category', event);
+ return category;
+ }
+
+ /**
+ * Dynamically removes a category from this set.
+ * @param {SettingsCategory} category The category to remove from this set
+ * @return {Promise}
+ */
+ async removeCategory(category) {
+ category.off('setting-updated', this.__settingUpdated);
+ category.off('settings-updated', this.__settingsUpdated);
+ category.off('added-setting', this.__addedSetting);
+ category.off('removed-setting', this.__removedSetting);
+
+ let index;
+ while ((index = this.categories.findIndex(c => c === category)) > -1) {
+ this.categories.splice(index, 0);
+ }
+
+ const event = {
+ set: this, set_id: this.id,
+ category, category_id: category.id,
+ from_index: index
+ };
+
+ await category.emit('removed-from', event);
+ await this.emit('removed-category', event);
+ }
+
+ /**
+ * Dynamically adds a scheme to this set.
+ * @param {SettingsScheme} scheme The scheme to add to this set
+ * @param {Number} index The index to add the scheme at (optional)
+ * @return {Promise}
+ */
+ async addScheme(scheme, index) {
+ if (this.schemes.find(c => c === scheme)) return;
+
+ if (!(scheme instanceof SettingsScheme))
+ scheme = new SettingsScheme(scheme);
+
+ if (this.schemes.find(s => s.id === scheme.id))
+ throw {message: 'A scheme with this ID already exists.'};
+
+ if (index === undefined) index = this.schemes.length;
+ this.schemes.splice(index, 0, scheme);
+
+ await this.emit('added-scheme', {
+ set: this, set_id: this.id,
+ scheme, scheme_id: scheme.id,
+ at_index: index
+ });
+ return scheme;
+ }
+
+ /**
+ * Dynamically removes a scheme from this set.
+ * @param {SettingsScheme} scheme The scheme to remove from this set
+ * @return {Promise}
+ */
+ async removeScheme(scheme) {
+ let index;
+ while ((index = this.schemes.findIndex(s => s === scheme)) > -1) {
+ this.schemes.splice(index, 0);
+ }
+
+ await this.emit('removed-scheme', {
+ set: this, set_id: this.id,
+ scheme, scheme_id: scheme.id,
+ from_index: index
+ });
+ }
+
/**
* Returns the first category where calling {function} returns true.
* @param {Function} function A function to call to filter categories
diff --git a/client/src/structs/settings/types/array.js b/client/src/structs/settings/types/array.js
index e06ae06f..6b128864 100644
--- a/client/src/structs/settings/types/array.js
+++ b/client/src/structs/settings/types/array.js
@@ -14,6 +14,7 @@ import Setting from './basesetting';
import SettingsSet from '../settingsset';
import SettingsCategory from '../settingscategory';
import SettingsScheme from '../settingsscheme';
+import { SettingsUpdatedEvent } from 'structs';
export default class ArraySetting extends Setting {
@@ -108,21 +109,27 @@ export default class ArraySetting extends Setting {
* @param {SettingsSet} item Values to merge into the new set (optional)
* @return {SettingsSet} The new set
*/
- addItem(item) {
- const newItem = this.createItem(item);
- this.args.items.push(newItem);
- this.updateValue();
- return newItem;
+ async addItem(_item) {
+ const item = this.createItem(_item);
+ this.args.items.push(item);
+ await this.updateValue();
+
+ await this.emit('item-added', { item });
+
+ return item;
}
/**
* Removes a set from this array setting.
* This ignores the minimum value.
* @param {SettingsSet} item The set to remove
+ * @return {Promise}
*/
- removeItem(item) {
+ async removeItem(item) {
this.args.items = this.items.filter(i => i !== item);
- this.updateValue();
+ await this.updateValue();
+
+ await this.emit('item-removed', { item });
}
/**
@@ -135,24 +142,84 @@ export default class ArraySetting extends Setting {
return item;
const set = new SettingsSet({
+ id: item ? item.args ? item.args.id : item.id : Math.random(),
settings: Utils.deepclone(this.settings),
schemes: this.schemes
}, item ? item.args || item : undefined);
set.setSaved();
- set.on('settings-updated', () => this.updateValue());
+ set.on('settings-updated', async event => {
+ await this.emit('item-updated', { item: set, event, updatedSettings: event.updatedSettings });
+ if (event.args.updating_array !== this) await this.updateValue();
+ });
return set;
}
+ /**
+ * Function to be called after the value changes.
+ * This can be overridden by other settings types.
+ * This function is used when the value needs to be updated synchronously (basically just in the constructor - so there won't be any events to emit anyway).
+ * @param {SettingUpdatedEvent} updatedSetting
+ */
+ setValueHookSync(updatedSetting) {
+ this.args.items = updatedSetting.value ? updatedSetting.value.map(item => this.createItem(item)) : [];
+ }
+
/**
* Function to be called after the value changes.
* This can be overridden by other settings types.
* @param {SettingUpdatedEvent} updatedSetting
*/
- setValueHook(updatedSetting) {
- this.args.items = updatedSetting.value ? updatedSetting.value.map(item => this.createItem(item)) : [];
+ async setValueHook(updatedSetting) {
+ const newItems = [];
+ let error;
+
+ for (let newItem of updatedSetting.value) {
+ try {
+ const item = this.items.find(i => i.id && i.id === newItem.id);
+
+ if (item) {
+ // Merge the new item into the original item
+ newItems.push(item);
+ const updatedSettings = await item.merge(newItem, false);
+ if (!updatedSettings.length) continue;
+
+ const event = new SettingsUpdatedEvent({
+ updatedSettings,
+ updating_array: this
+ });
+
+ await item.emit('settings-updated', event);
+ // await this.emit('item-updated', { item, event, updatedSettings });
+ } else {
+ // Add a new item
+ const item = this.createItem(newItem);
+ newItems.push(item);
+ await this.emit('item-added', { item });
+ }
+ } catch (e) { error = e; }
+ }
+
+ for (let item of this.items) {
+ if (newItems.includes(item)) continue;
+
+ try {
+ // Item removed
+ await this.emit('item-removed', { item });
+ } catch (e) { error = e; }
+ }
+
+ this.args.items = newItems;
+
+ // We can't throw anything before the items array is updated, otherwise the array setting would be in an inconsistent state where the values in this.items wouldn't match the values in this.value
+ if (error) throw error;
}
+ // emit(...args) {
+ // console.log('Emitting event', args[0], 'with data', args[1]);
+ // return this.emitter.emit(...args);
+ // }
+
/**
* Updates the value of this array setting.
* This only exists for use by array settings.
diff --git a/client/src/structs/settings/types/basesetting.js b/client/src/structs/settings/types/basesetting.js
index 19a74a10..b174bcea 100644
--- a/client/src/structs/settings/types/basesetting.js
+++ b/client/src/structs/settings/types/basesetting.js
@@ -105,9 +105,9 @@ export default class Setting {
* Merges a setting into this setting without emitting events (and therefore synchronously).
* This only exists for use by the constructor and SettingsCategory.
*/
- _merge(newSetting) {
+ _merge(newSetting, hook = true) {
const value = newSetting.args ? newSetting.args.value : newSetting.value;
- return this._setValue(value);
+ return this._setValue(value, hook);
}
/**
@@ -116,12 +116,13 @@ export default class Setting {
* @return {Promise}
*/
async merge(newSetting, emit_multi = true, emit = true) {
- const updatedSettings = this._merge(newSetting);
+ const updatedSettings = this._merge(newSetting, false);
if (!updatedSettings.length) return [];
- const updatedSetting = updatedSettings[0];
+
+ await this.setValueHook(updatedSettings[0]);
if (emit)
- await this.emit('setting-updated', updatedSetting);
+ await this.emit('setting-updated', updatedSettings[0]);
if (emit_multi)
await this.emit('settings-updated', new SettingsUpdatedEvent({
@@ -135,7 +136,7 @@ export default class Setting {
* Sets the value of this setting.
* This only exists for use by the constructor and SettingsCategory.
*/
- _setValue(value) {
+ _setValue(value, hook = true) {
const old_value = this.args.value;
if (Utils.compare(value, old_value)) return [];
this.args.value = value;
@@ -146,7 +147,8 @@ export default class Setting {
value, old_value
});
- this.setValueHook(updatedSetting);
+ if (hook)
+ this.setValueHookSync(updatedSetting);
return [updatedSetting];
}
@@ -156,7 +158,8 @@ export default class Setting {
* This can be overridden by other settings types.
* @param {SettingUpdatedEvent} updatedSetting
*/
- setValueHook(updatedSetting) {}
+ async setValueHook(updatedSetting) {}
+ setValueHookSync(updatedSetting) {}
/**
* Sets the value of this setting.
@@ -164,9 +167,11 @@ export default class Setting {
* @return {Promise}
*/
async setValue(value, emit_multi = true, emit = true) {
- const updatedSettings = this._setValue(value);
+ const updatedSettings = this._setValue(value, false);
if (!updatedSettings.length) return [];
+ await this.setValueHook(updatedSettings[0]);
+
if (emit)
await this.emit('setting-updated', updatedSettings[0]);
diff --git a/client/src/styles/partials/bdsettings/contentview.scss b/client/src/styles/partials/bdsettings/contentview.scss
new file mode 100644
index 00000000..090c9966
--- /dev/null
+++ b/client/src/styles/partials/bdsettings/contentview.scss
@@ -0,0 +1,20 @@
+.bd-pluginsview,
+.bd-themesview {
+ .bd-online-ph {
+ display: flex;
+ flex-direction: column;
+
+ h3 {
+ color: #fff;
+ font-weight: 700;
+ font-size: 20px;
+ text-align: center;
+ padding: 20px;
+ }
+
+ a {
+ padding: 20px;
+ text-align: center;
+ }
+ }
+}
diff --git a/client/src/styles/partials/bdsettings/index.scss b/client/src/styles/partials/bdsettings/index.scss
index 258a9019..ad0fe545 100644
--- a/client/src/styles/partials/bdsettings/index.scss
+++ b/client/src/styles/partials/bdsettings/index.scss
@@ -1,6 +1,6 @@
@import './button.scss';
@import './sidebarview.scss';
-@import './plugins.scss';
+@import './contentview.scss';
@import './card.scss';
@import './tooltips.scss';
@import './settings-schemes.scss';
diff --git a/client/src/styles/partials/bdsettings/plugins.scss b/client/src/styles/partials/bdsettings/plugins.scss
deleted file mode 100644
index 7294c03e..00000000
--- a/client/src/styles/partials/bdsettings/plugins.scss
+++ /dev/null
@@ -1,55 +0,0 @@
-/*.bd-pluginsView {
- .bd-button {
- text-align: center;
- background: transparent;
- display: flex;
- border-bottom: 2px solid #2b2d31;
- align-items: center;
-
- h3 {
- -webkit-user-select: none;
- user-select: none;
- display: block;
- font-size: 1.17em;
- margin-top: 1em;
- margin-bottom: 1em;
- margin-left: 0;
- margin-right: 0;
- font-weight: bold;
- flex-grow: 1;
- }
-
- .material-design-icon {
- display: flex;
- align-items: center;
- fill: #fff;
- }
-
- &:hover,
- &.bd-active {
- color: #fff;
- background: transparent;
- border-bottom: 2px solid #3e82e5;
- }
- }
-
- .bd-spinner-container {
- display: flex;
- flex-grow: 1;
- align-items: center;
- align-content: center;
- justify-content: center;
-
- .bd-spinner-2 {
- width: 200px;
- height: 200px;
- }
- }
-}
- */
-
-.bd-pluginsView {
-
-
-
-}
\ No newline at end of file
diff --git a/client/src/styles/partials/generic/drawers.scss b/client/src/styles/partials/generic/drawers.scss
index 5b7dc9ba..cfe4fb94 100644
--- a/client/src/styles/partials/generic/drawers.scss
+++ b/client/src/styles/partials/generic/drawers.scss
@@ -50,11 +50,13 @@
}
}
- &.bd-drawer-open {
+ &.bd-animating {
> .bd-drawer-contents-wrap {
- overflow: visible;
+ overflow: hidden;
}
+ }
+ &.bd-drawer-open {
> .bd-drawer-header .bd-drawer-open-button {
.bd-chevron-1 {
svg {
diff --git a/client/src/styles/partials/generic/forms/text.scss b/client/src/styles/partials/generic/forms/text.scss
index 7709b624..738af627 100644
--- a/client/src/styles/partials/generic/forms/text.scss
+++ b/client/src/styles/partials/generic/forms/text.scss
@@ -54,7 +54,8 @@
}
.bd-form-textarea {
- .bd-form-textarea-wrap {
+ .bd-form-textarea-wrap,
+ textarea.bd-textarea {
margin-top: 15px;
background: rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.3);
@@ -62,6 +63,7 @@
color: #b9bbbe;
overflow-y: scroll;
max-height: 140px;
+ transition: border-color .2s ease, color .2s ease;
&:focus {
color: #fff;
@@ -71,9 +73,24 @@
@include scrollbar;
}
- div[contenteditable] {
+ div[contenteditable],
+ textarea {
padding: 11px;
cursor: text;
+ min-height: 45px;
+ }
+
+ textarea {
+ background: transparent;
+ border: none;
+ resize: none;
+ outline: none;
+ width: 100%;
+ color: inherit;
+ font-size: inherit;
+ box-sizing: border-box;
+ overflow-y: visible;
+ max-height: 140px;
}
}
diff --git a/client/src/ui/components/bd/PluginCard.vue b/client/src/ui/components/bd/PluginCard.vue
index 901f3b30..a35cf18e 100644
--- a/client/src/ui/components/bd/PluginCard.vue
+++ b/client/src/ui/components/bd/PluginCard.vue
@@ -12,10 +12,10 @@