Merge pull request #158 from samuelthomas2774/array-merging-dynamic-settingssets-categories-fixes-and-comments
Better array merging, changeable settings sets and categories, fixes and comments
This commit is contained in:
commit
f263cfac24
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -8,6 +8,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Content from './content';
|
||||||
import Globals from './globals';
|
import Globals from './globals';
|
||||||
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
|
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
@ -245,17 +246,19 @@ export default class {
|
||||||
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
|
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (content.enabled && content.disable) content.disable(false);
|
await content.disable(false);
|
||||||
if (content.enabled && content.stop) content.stop(false);
|
await content.emit('unload', reload);
|
||||||
if (content.onunload) content.onunload(reload);
|
|
||||||
if (content.onUnload) content.onUnload(reload);
|
|
||||||
const index = this.getContentIndex(content);
|
const index = this.getContentIndex(content);
|
||||||
|
|
||||||
delete window.require.cache[window.require.resolve(content.paths.mainPath)];
|
delete window.require.cache[window.require.resolve(content.paths.mainPath)];
|
||||||
|
|
||||||
if (reload) {
|
if (reload) {
|
||||||
const newcontent = await this.preloadContent(content.dirName, true, index);
|
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;
|
return newcontent;
|
||||||
} else this.localContent.splice(index, 1);
|
} else this.localContent.splice(index, 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -268,7 +271,7 @@ export default class {
|
||||||
* Reload content
|
* Reload content
|
||||||
* @param {any} content Content to reload
|
* @param {any} content Content to reload
|
||||||
*/
|
*/
|
||||||
static async reloadContent(content) {
|
static reloadContent(content) {
|
||||||
return this.unloadContent(content, true);
|
return this.unloadContent(content, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,12 +298,20 @@ export default class {
|
||||||
* @param {any} content Object to check
|
* @param {any} content Object to check
|
||||||
*/
|
*/
|
||||||
static isThisContent(content) {
|
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
|
* 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
|
* @param {bool} nonunique Allow searching attributes that may not be unique
|
||||||
*/
|
*/
|
||||||
static findContent(wild, nonunique) {
|
static findContent(wild, nonunique) {
|
||||||
|
@ -313,10 +324,10 @@ export default class {
|
||||||
}
|
}
|
||||||
|
|
||||||
static getContentIndex(content) { return this.localContent.findIndex(c => c === content) }
|
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 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 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
|
* Wait for content to load
|
||||||
|
|
|
@ -8,37 +8,15 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AsyncEventEmitter } from 'common';
|
import Content from './content';
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
|
|
||||||
export default class ExtModule {
|
export default class ExtModule extends Content {
|
||||||
|
|
||||||
constructor(pluginInternals) {
|
constructor(internals) {
|
||||||
this.__pluginInternals = pluginInternals;
|
super(internals);
|
||||||
this.__require = window.require(this.paths.mainPath);
|
this.__require = window.require(this.paths.mainPath);
|
||||||
this.hasSettings = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get type() { return 'module' }
|
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()) }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,93 +8,19 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* 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 PluginManager from './pluginmanager';
|
||||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
import Content from './content';
|
||||||
|
|
||||||
export default class Plugin {
|
export default class Plugin extends Content {
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
get type() { return 'plugin' }
|
get type() { return 'plugin' }
|
||||||
get configs() { return this.__pluginInternals.configs }
|
|
||||||
get info() { return this.__pluginInternals.info }
|
// Don't use - these will eventually be removed!
|
||||||
get icon() { return this.info.icon }
|
get pluginPath() { return this.contentPath }
|
||||||
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 }
|
|
||||||
get pluginConfig() { return this.config }
|
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() {
|
get start() { return this.enable }
|
||||||
return Modals.contentSettings(this);
|
get stop() { return this.disable }
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
unload() {
|
unload() {
|
||||||
PluginManager.unloadPlugin(this);
|
PluginManager.unloadPlugin(this);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import ExtModuleManager from './extmodulemanager';
|
||||||
import PluginManager from './pluginmanager';
|
import PluginManager from './pluginmanager';
|
||||||
import ThemeManager from './thememanager';
|
import ThemeManager from './thememanager';
|
||||||
import Events from './events';
|
import Events from './events';
|
||||||
|
import WebpackModules from './webpackmodules';
|
||||||
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
|
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
|
||||||
import { Modals, DOM } from 'ui';
|
import { Modals, DOM } from 'ui';
|
||||||
import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue';
|
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, '-'));
|
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) }
|
loggerLog(...message) { Logger.log(this.pluginInfo.name, message) }
|
||||||
loggerErr(...message) { Logger.err(this.pluginInfo.name, message) }
|
loggerErr(...message) { Logger.err(this.pluginInfo.name, message) }
|
||||||
loggerWarn(...message) { Logger.warn(this.pluginInfo.name, message) }
|
loggerWarn(...message) { Logger.warn(this.pluginInfo.name, message) }
|
||||||
|
@ -78,6 +97,10 @@ export default class PluginApi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
get Utils() {
|
get Utils() {
|
||||||
return {
|
return {
|
||||||
overload: () => Utils.overload.apply(Utils, arguments),
|
overload: () => Utils.overload.apply(Utils, arguments),
|
||||||
|
@ -92,8 +115,12 @@ export default class PluginApi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings
|
||||||
|
*/
|
||||||
|
|
||||||
createSettingsSet(args, ...merge) {
|
createSettingsSet(args, ...merge) {
|
||||||
return new SettingsSet(args, ...merge);
|
return new SettingsSet(args || {}, ...merge);
|
||||||
}
|
}
|
||||||
createSettingsCategory(args, ...merge) {
|
createSettingsCategory(args, ...merge) {
|
||||||
return new SettingsCategory(args, ...merge);
|
return new SettingsCategory(args, ...merge);
|
||||||
|
@ -106,13 +133,17 @@ export default class PluginApi {
|
||||||
}
|
}
|
||||||
get Settings() {
|
get Settings() {
|
||||||
return {
|
return {
|
||||||
createSet: this.createSet.bind(this),
|
createSet: this.createSettingsSet.bind(this),
|
||||||
createCategory: this.createSettingsCategory.bind(this),
|
createCategory: this.createSettingsCategory.bind(this),
|
||||||
createSetting: this.createSetting.bind(this),
|
createSetting: this.createSetting.bind(this),
|
||||||
createScheme: this.createSettingsScheme.bind(this)
|
createScheme: this.createSettingsScheme.bind(this)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InternalSettings
|
||||||
|
*/
|
||||||
|
|
||||||
getInternalSetting(set, category, setting) {
|
getInternalSetting(set, category, setting) {
|
||||||
return Settings.get(set, category, setting);
|
return Settings.get(set, category, setting);
|
||||||
}
|
}
|
||||||
|
@ -122,6 +153,10 @@ export default class PluginApi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CssUtils
|
||||||
|
*/
|
||||||
|
|
||||||
get injectedStyles() {
|
get injectedStyles() {
|
||||||
return this._injectedStyles || (this._injectedStyles = []);
|
return this._injectedStyles || (this._injectedStyles = []);
|
||||||
}
|
}
|
||||||
|
@ -169,36 +204,48 @@ export default class PluginApi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modals
|
||||||
|
*/
|
||||||
|
|
||||||
get modalStack() {
|
get modalStack() {
|
||||||
return this._modalStack || (this._modalStack = []);
|
return this._modalStack || (this._modalStack = []);
|
||||||
}
|
}
|
||||||
|
get baseModalComponent() {
|
||||||
|
return Modals.baseComponent;
|
||||||
|
}
|
||||||
addModal(_modal, component) {
|
addModal(_modal, component) {
|
||||||
const modal = Modals.add(_modal, component);
|
const modal = Modals.add(_modal, component);
|
||||||
modal.close = force => this.closeModal(modal, force);
|
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);
|
this.modalStack.push(modal);
|
||||||
return modal;
|
return modal;
|
||||||
}
|
}
|
||||||
async closeModal(modal, force) {
|
closeModal(modal, force) {
|
||||||
await Modals.close(modal, force);
|
return Modals.close(modal, force);
|
||||||
this._modalStack = this.modalStack.filter(m => m !== modal);
|
|
||||||
}
|
}
|
||||||
closeAllModals() {
|
closeAllModals(force) {
|
||||||
|
const promises = [];
|
||||||
for (let modal of this.modalStack)
|
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;
|
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) {
|
settingsModal(settingsset, headertext, options) {
|
||||||
return this.addModal(Object.assign({
|
return this.addModal(Modals.settings(settingsset, headertext, options));
|
||||||
headertext: headertext ? headertext : settingsset.headertext,
|
|
||||||
settings: settingsset,
|
|
||||||
schemes: settingsset.schemes
|
|
||||||
}, options), SettingsModal);
|
|
||||||
}
|
}
|
||||||
get Modals() {
|
get Modals() {
|
||||||
return Object.defineProperty({
|
return Object.defineProperty(Object.defineProperty({
|
||||||
add: this.addModal.bind(this),
|
add: this.addModal.bind(this),
|
||||||
close: this.closeModal.bind(this),
|
close: this.closeModal.bind(this),
|
||||||
closeAll: this.closeAllModals.bind(this),
|
closeAll: this.closeAllModals.bind(this),
|
||||||
|
@ -206,14 +253,20 @@ export default class PluginApi {
|
||||||
settings: this.settingsModal.bind(this)
|
settings: this.settingsModal.bind(this)
|
||||||
}, 'stack', {
|
}, 'stack', {
|
||||||
get: () => this.modalStack
|
get: () => this.modalStack
|
||||||
|
}), 'baseComponent', {
|
||||||
|
get: () => this.baseModalComponent
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugins
|
||||||
|
*/
|
||||||
|
|
||||||
async getPlugin(plugin_id) {
|
async getPlugin(plugin_id) {
|
||||||
// This should require extra permissions
|
// This should require extra permissions
|
||||||
return await PluginManager.waitForPlugin(plugin_id);
|
return await PluginManager.waitForPlugin(plugin_id);
|
||||||
}
|
}
|
||||||
listPlugins(plugin_id) {
|
listPlugins() {
|
||||||
return PluginManager.localContent.map(plugin => plugin.id);
|
return PluginManager.localContent.map(plugin => plugin.id);
|
||||||
}
|
}
|
||||||
get Plugins() {
|
get Plugins() {
|
||||||
|
@ -223,30 +276,75 @@ export default class PluginApi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themes
|
||||||
|
*/
|
||||||
|
|
||||||
async getTheme(theme_id) {
|
async getTheme(theme_id) {
|
||||||
// This should require extra permissions
|
// This should require extra permissions
|
||||||
return await ThemeManager.waitForContent(theme_id);
|
return await ThemeManager.waitForContent(theme_id);
|
||||||
}
|
}
|
||||||
listThemes(plugin_id) {
|
listThemes() {
|
||||||
return ThemeManager.localContent.map(theme => theme.id);
|
return ThemeManager.localContent.map(theme => theme.id);
|
||||||
}
|
}
|
||||||
get Themes() {
|
get Themes() {
|
||||||
return {
|
return {
|
||||||
getTheme: this.getTheme.bind(this),
|
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);
|
* ExtModules
|
||||||
return plugin.bridge;
|
*/
|
||||||
|
|
||||||
|
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) {
|
* WebpackModules
|
||||||
const module = ExtModuleManager.findModule(m);
|
*/
|
||||||
if (module && module.__require) return module.__require;
|
|
||||||
return null;
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,11 +41,13 @@ export default class extends ContentManager {
|
||||||
const loadAll = await this.loadAllContent(true);
|
const loadAll = await this.loadAllContent(true);
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
for (let plugin of this.localPlugins) {
|
for (let plugin of this.localPlugins) {
|
||||||
|
if (!plugin.enabled) continue;
|
||||||
|
plugin.userConfig.enabled = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (plugin.enabled) plugin.start();
|
plugin.start(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Disable the plugin but don't save it - the next time BetterDiscord is started the plugin will attempt to start again
|
// 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({
|
this.errors.push(new ErrorEvent({
|
||||||
module: this.moduleName,
|
module: this.moduleName,
|
||||||
message: `Failed to start ${plugin.name}`,
|
message: `Failed to start ${plugin.name}`,
|
||||||
|
@ -72,7 +74,6 @@ export default class extends ContentManager {
|
||||||
|
|
||||||
static get loadContent() { return this.loadPlugin }
|
static get loadContent() { return this.loadPlugin }
|
||||||
static async loadPlugin(paths, configs, info, main, dependencies, permissions) {
|
static async loadPlugin(paths, configs, info, main, dependencies, permissions) {
|
||||||
|
|
||||||
if (permissions && permissions.length > 0) {
|
if (permissions && permissions.length > 0) {
|
||||||
for (let perm of permissions) {
|
for (let perm of permissions) {
|
||||||
console.log(`Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
|
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;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,26 +8,17 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Content from './content';
|
||||||
import Settings from './settings';
|
import Settings from './settings';
|
||||||
import ThemeManager from './thememanager';
|
import ThemeManager from './thememanager';
|
||||||
import { EventEmitter } from 'events';
|
import { DOM } from 'ui';
|
||||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
import { FileUtils, ClientIPC, ClientLogger as Logger } from 'common';
|
||||||
import { DOM, Modals } from 'ui';
|
|
||||||
import { Utils, FileUtils, ClientIPC, ClientLogger as Logger, AsyncEventEmitter } from 'common';
|
|
||||||
import filewatcher from 'filewatcher';
|
import filewatcher from 'filewatcher';
|
||||||
|
|
||||||
export default class Theme {
|
export default class Theme extends Content {
|
||||||
|
|
||||||
constructor(themeInternals) {
|
constructor(internals) {
|
||||||
this.__themeInternals = themeInternals;
|
super(internals);
|
||||||
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());
|
|
||||||
|
|
||||||
const watchfiles = Settings.getSetting('css', 'default', 'watch-files');
|
const watchfiles = Settings.getSetting('css', 'default', 'watch-files');
|
||||||
if (watchfiles.value) this.watchfiles = this.files;
|
if (watchfiles.value) this.watchfiles = this.files;
|
||||||
|
@ -37,63 +28,39 @@ export default class Theme {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get configs() { return this.__themeInternals.configs }
|
get type() { return 'theme' }
|
||||||
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 css() { return this.data.css }
|
get css() { return this.data.css }
|
||||||
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new AsyncEventEmitter()) }
|
|
||||||
|
|
||||||
showSettingsModal() {
|
// Don't use - these will eventually be removed!
|
||||||
return Modals.contentSettings(this);
|
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 {
|
* This is called when the theme is enabled.
|
||||||
await FileUtils.writeFile(`${this.themePath}/user.config.json`, JSON.stringify({
|
*/
|
||||||
enabled: this.enabled,
|
onstart() {
|
||||||
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();
|
|
||||||
}
|
|
||||||
DOM.injectTheme(this.css, this.id);
|
DOM.injectTheme(this.css, this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
disable(save = true) {
|
/**
|
||||||
this.userConfig.enabled = false;
|
* This is called when the theme is disabled.
|
||||||
if (save) this.saveConfiguration();
|
*/
|
||||||
|
onstop() {
|
||||||
DOM.deleteTheme(this.id);
|
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() {
|
async compile() {
|
||||||
console.log('Compiling CSS');
|
console.log('Compiling CSS');
|
||||||
|
|
||||||
|
@ -117,11 +84,15 @@ export default class Theme {
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
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() {
|
async recompile() {
|
||||||
const data = await this.compile();
|
const data = await this.compile();
|
||||||
this.data.css = data.css;
|
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
|
* @return {Array} Files being watched
|
||||||
*/
|
*/
|
||||||
get files() {
|
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
|
* @param {Array} files Files to watch
|
||||||
*/
|
*/
|
||||||
set files(files) {
|
set files(files) {
|
||||||
|
|
|
@ -86,7 +86,6 @@ const KnownModules = {
|
||||||
UserActivityStore: Filters.byProperties(['getActivity']),
|
UserActivityStore: Filters.byProperties(['getActivity']),
|
||||||
UserNameResolver: Filters.byProperties(['getName']),
|
UserNameResolver: Filters.byProperties(['getName']),
|
||||||
|
|
||||||
|
|
||||||
/* Emoji Store and Utils */
|
/* Emoji Store and Utils */
|
||||||
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
|
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
|
||||||
EmojiUtils: Filters.byProperties(['diversitySurrogate']),
|
EmojiUtils: Filters.byProperties(['diversitySurrogate']),
|
||||||
|
@ -97,7 +96,6 @@ const KnownModules = {
|
||||||
InviteResolver: Filters.byProperties(['findInvite']),
|
InviteResolver: Filters.byProperties(['findInvite']),
|
||||||
InviteActions: Filters.byProperties(['acceptInvite']),
|
InviteActions: Filters.byProperties(['acceptInvite']),
|
||||||
|
|
||||||
|
|
||||||
/* Discord Objects & Utils */
|
/* Discord Objects & Utils */
|
||||||
DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]),
|
DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]),
|
||||||
Permissions: Filters.byProperties(['getHighestRole']),
|
Permissions: Filters.byProperties(['getHighestRole']),
|
||||||
|
@ -122,7 +120,6 @@ const KnownModules = {
|
||||||
ExperimentsManager: Filters.byProperties(['isDeveloper']),
|
ExperimentsManager: Filters.byProperties(['isDeveloper']),
|
||||||
CurrentExperiment: Filters.byProperties(['getExperimentId']),
|
CurrentExperiment: Filters.byProperties(['getExperimentId']),
|
||||||
|
|
||||||
|
|
||||||
/* Images, Avatars and Utils */
|
/* Images, Avatars and Utils */
|
||||||
ImageResolver: Filters.byProperties(["getUserAvatarURL"]),
|
ImageResolver: Filters.byProperties(["getUserAvatarURL"]),
|
||||||
ImageUtils: Filters.byProperties(['getSizedImageSrc']),
|
ImageUtils: Filters.byProperties(['getSizedImageSrc']),
|
||||||
|
@ -176,7 +173,6 @@ const KnownModules = {
|
||||||
URLParser: Filters.byProperties(['Url', 'parse']),
|
URLParser: Filters.byProperties(['Url', 'parse']),
|
||||||
ExtraURLs: Filters.byProperties(['getArticleURL']),
|
ExtraURLs: Filters.byProperties(['getArticleURL']),
|
||||||
|
|
||||||
|
|
||||||
/* DOM/React Components */
|
/* DOM/React Components */
|
||||||
/* ==================== */
|
/* ==================== */
|
||||||
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
|
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
|
||||||
|
@ -201,60 +197,114 @@ const KnownModules = {
|
||||||
ExternalLink: Filters.byCode(/\.trusted\b/)
|
ExternalLink: Filters.byCode(/\.trusted\b/)
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class {
|
export default class WebpackModules {
|
||||||
/* Synchronous */
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
static getModuleByName(name, fallback) {
|
||||||
if (Cache.hasOwnProperty(name)) return Cache[name];
|
if (Cache.hasOwnProperty(name)) return Cache[name];
|
||||||
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
|
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
|
||||||
if (!fallback) return null;
|
if (!fallback) return undefined;
|
||||||
return Cache[name] = this.getModule(fallback, true);
|
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) {
|
static getModuleByDisplayName(name) {
|
||||||
return this.getModule(Filters.byDisplayName(name), true);
|
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) {
|
static getModuleByRegex(regex, first = true) {
|
||||||
return this.getModule(Filters.byCode(regex), first);
|
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) {
|
static getModuleByPrototypes(prototypes, first = true) {
|
||||||
return this.getModule(Filters.byPrototypeFields(prototypes), first);
|
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) {
|
static getModuleByProps(props, first = true) {
|
||||||
return this.getModule(Filters.byProperties(props), first);
|
return this.getModule(Filters.byProperties(props), first);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getModule(filter, first = true) {
|
/**
|
||||||
const modules = this.getAllModules();
|
* Discord's __webpack_require__ function.
|
||||||
const rm = [];
|
*/
|
||||||
for (let index in modules) {
|
static get require() {
|
||||||
if (!modules.hasOwnProperty(index)) continue;
|
if (this._require) return this._require;
|
||||||
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() {
|
|
||||||
const id = 'bd-webpackmodules';
|
const id = 'bd-webpackmodules';
|
||||||
const __webpack_require__ = window['webpackJsonp'](
|
const __webpack_require__ = window['webpackJsonp']([], {
|
||||||
[],
|
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
|
||||||
{
|
}, [id]).default;
|
||||||
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
|
|
||||||
},
|
|
||||||
[id]).default;
|
|
||||||
delete __webpack_require__.m[id];
|
delete __webpack_require__.m[id];
|
||||||
delete __webpack_require__.c[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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Setting from './setting';
|
import Setting from './setting';
|
||||||
|
import BaseSetting from './types/basesetting';
|
||||||
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
|
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
|
||||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
||||||
|
|
||||||
|
@ -24,17 +25,12 @@ export default class SettingsCategory {
|
||||||
this._merge(newCategory);
|
this._merge(newCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.__settingUpdated = this.__settingUpdated.bind(this);
|
||||||
|
this.__settingsUpdated = this.__settingsUpdated.bind(this);
|
||||||
|
|
||||||
for (let setting of this.settings) {
|
for (let setting of this.settings) {
|
||||||
setting.on('setting-updated', ({ value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({
|
setting.on('setting-updated', this.__settingUpdated);
|
||||||
category: this, category_id: this.id,
|
setting.on('settings-updated', this.__settingsUpdated);
|
||||||
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)))
|
|
||||||
})));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +49,7 @@ export default class SettingsCategory {
|
||||||
* Category name
|
* Category name
|
||||||
*/
|
*/
|
||||||
get name() {
|
get name() {
|
||||||
return this.args.category_name;
|
return this.args.name || this.args.category_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get category_name() {
|
get category_name() {
|
||||||
|
@ -83,6 +79,82 @@ export default class SettingsCategory {
|
||||||
return false;
|
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.
|
* Returns the first setting where calling {function} returns true.
|
||||||
* @param {Function} function A function to call to filter settings
|
* @param {Function} function A function to call to filter settings
|
||||||
|
@ -107,7 +179,7 @@ export default class SettingsCategory {
|
||||||
* @return {Setting}
|
* @return {Setting}
|
||||||
*/
|
*/
|
||||||
getSetting(id) {
|
getSetting(id) {
|
||||||
return this.findSetting(setting => setting.id === id);
|
return this.find(setting => setting.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,18 +27,16 @@ export default class SettingsSet {
|
||||||
this._merge(newSet);
|
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) {
|
for (let category of this.categories) {
|
||||||
category.on('setting-updated', ({ setting, value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({
|
category.on('setting-updated', this.__settingUpdated);
|
||||||
set: this, set_id: this.id,
|
category.on('settings-updated', this.__settingsUpdated);
|
||||||
category, category_id: category.id,
|
category.on('added-setting', this.__addedSetting);
|
||||||
setting, setting_id: setting.id,
|
category.on('removed-setting', this.__removedSetting);
|
||||||
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)))
|
|
||||||
})));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +99,149 @@ export default class SettingsSet {
|
||||||
return false;
|
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.
|
* Returns the first category where calling {function} returns true.
|
||||||
* @param {Function} function A function to call to filter categories
|
* @param {Function} function A function to call to filter categories
|
||||||
|
|
|
@ -14,6 +14,7 @@ import Setting from './basesetting';
|
||||||
import SettingsSet from '../settingsset';
|
import SettingsSet from '../settingsset';
|
||||||
import SettingsCategory from '../settingscategory';
|
import SettingsCategory from '../settingscategory';
|
||||||
import SettingsScheme from '../settingsscheme';
|
import SettingsScheme from '../settingsscheme';
|
||||||
|
import { SettingsUpdatedEvent } from 'structs';
|
||||||
|
|
||||||
export default class ArraySetting extends Setting {
|
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)
|
* @param {SettingsSet} item Values to merge into the new set (optional)
|
||||||
* @return {SettingsSet} The new set
|
* @return {SettingsSet} The new set
|
||||||
*/
|
*/
|
||||||
addItem(item) {
|
async addItem(_item) {
|
||||||
const newItem = this.createItem(item);
|
const item = this.createItem(_item);
|
||||||
this.args.items.push(newItem);
|
this.args.items.push(item);
|
||||||
this.updateValue();
|
await this.updateValue();
|
||||||
return newItem;
|
|
||||||
|
await this.emit('item-added', { item });
|
||||||
|
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a set from this array setting.
|
* Removes a set from this array setting.
|
||||||
* This ignores the minimum value.
|
* This ignores the minimum value.
|
||||||
* @param {SettingsSet} item The set to remove
|
* @param {SettingsSet} item The set to remove
|
||||||
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
removeItem(item) {
|
async removeItem(item) {
|
||||||
this.args.items = this.items.filter(i => i !== 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;
|
return item;
|
||||||
|
|
||||||
const set = new SettingsSet({
|
const set = new SettingsSet({
|
||||||
|
id: item ? item.args ? item.args.id : item.id : Math.random(),
|
||||||
settings: Utils.deepclone(this.settings),
|
settings: Utils.deepclone(this.settings),
|
||||||
schemes: this.schemes
|
schemes: this.schemes
|
||||||
}, item ? item.args || item : undefined);
|
}, item ? item.args || item : undefined);
|
||||||
|
|
||||||
set.setSaved();
|
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;
|
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.
|
* Function to be called after the value changes.
|
||||||
* This can be overridden by other settings types.
|
* This can be overridden by other settings types.
|
||||||
* @param {SettingUpdatedEvent} updatedSetting
|
* @param {SettingUpdatedEvent} updatedSetting
|
||||||
*/
|
*/
|
||||||
setValueHook(updatedSetting) {
|
async setValueHook(updatedSetting) {
|
||||||
this.args.items = updatedSetting.value ? updatedSetting.value.map(item => this.createItem(item)) : [];
|
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.
|
* Updates the value of this array setting.
|
||||||
* This only exists for use by array settings.
|
* This only exists for use by array settings.
|
||||||
|
|
|
@ -105,9 +105,9 @@ export default class Setting {
|
||||||
* Merges a setting into this setting without emitting events (and therefore synchronously).
|
* Merges a setting into this setting without emitting events (and therefore synchronously).
|
||||||
* This only exists for use by the constructor and SettingsCategory.
|
* 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;
|
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}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async merge(newSetting, emit_multi = true, emit = true) {
|
async merge(newSetting, emit_multi = true, emit = true) {
|
||||||
const updatedSettings = this._merge(newSetting);
|
const updatedSettings = this._merge(newSetting, false);
|
||||||
if (!updatedSettings.length) return [];
|
if (!updatedSettings.length) return [];
|
||||||
const updatedSetting = updatedSettings[0];
|
|
||||||
|
await this.setValueHook(updatedSettings[0]);
|
||||||
|
|
||||||
if (emit)
|
if (emit)
|
||||||
await this.emit('setting-updated', updatedSetting);
|
await this.emit('setting-updated', updatedSettings[0]);
|
||||||
|
|
||||||
if (emit_multi)
|
if (emit_multi)
|
||||||
await this.emit('settings-updated', new SettingsUpdatedEvent({
|
await this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||||
|
@ -135,7 +136,7 @@ export default class Setting {
|
||||||
* Sets the value of this setting.
|
* Sets the value of this setting.
|
||||||
* This only exists for use by the constructor and SettingsCategory.
|
* This only exists for use by the constructor and SettingsCategory.
|
||||||
*/
|
*/
|
||||||
_setValue(value) {
|
_setValue(value, hook = true) {
|
||||||
const old_value = this.args.value;
|
const old_value = this.args.value;
|
||||||
if (Utils.compare(value, old_value)) return [];
|
if (Utils.compare(value, old_value)) return [];
|
||||||
this.args.value = value;
|
this.args.value = value;
|
||||||
|
@ -146,7 +147,8 @@ export default class Setting {
|
||||||
value, old_value
|
value, old_value
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setValueHook(updatedSetting);
|
if (hook)
|
||||||
|
this.setValueHookSync(updatedSetting);
|
||||||
|
|
||||||
return [updatedSetting];
|
return [updatedSetting];
|
||||||
}
|
}
|
||||||
|
@ -156,7 +158,8 @@ export default class Setting {
|
||||||
* This can be overridden by other settings types.
|
* This can be overridden by other settings types.
|
||||||
* @param {SettingUpdatedEvent} updatedSetting
|
* @param {SettingUpdatedEvent} updatedSetting
|
||||||
*/
|
*/
|
||||||
setValueHook(updatedSetting) {}
|
async setValueHook(updatedSetting) {}
|
||||||
|
setValueHookSync(updatedSetting) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the value of this setting.
|
* Sets the value of this setting.
|
||||||
|
@ -164,9 +167,11 @@ export default class Setting {
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async setValue(value, emit_multi = true, emit = true) {
|
async setValue(value, emit_multi = true, emit = true) {
|
||||||
const updatedSettings = this._setValue(value);
|
const updatedSettings = this._setValue(value, false);
|
||||||
if (!updatedSettings.length) return [];
|
if (!updatedSettings.length) return [];
|
||||||
|
|
||||||
|
await this.setValueHook(updatedSettings[0]);
|
||||||
|
|
||||||
if (emit)
|
if (emit)
|
||||||
await this.emit('setting-updated', updatedSettings[0]);
|
await this.emit('setting-updated', updatedSettings[0]);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
@import './button.scss';
|
@import './button.scss';
|
||||||
@import './sidebarview.scss';
|
@import './sidebarview.scss';
|
||||||
@import './plugins.scss';
|
@import './contentview.scss';
|
||||||
@import './card.scss';
|
@import './card.scss';
|
||||||
@import './tooltips.scss';
|
@import './tooltips.scss';
|
||||||
@import './settings-schemes.scss';
|
@import './settings-schemes.scss';
|
||||||
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -50,11 +50,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bd-drawer-open {
|
&.bd-animating {
|
||||||
> .bd-drawer-contents-wrap {
|
> .bd-drawer-contents-wrap {
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bd-drawer-open {
|
||||||
> .bd-drawer-header .bd-drawer-open-button {
|
> .bd-drawer-header .bd-drawer-open-button {
|
||||||
.bd-chevron-1 {
|
.bd-chevron-1 {
|
||||||
svg {
|
svg {
|
||||||
|
|
|
@ -54,7 +54,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.bd-form-textarea {
|
.bd-form-textarea {
|
||||||
.bd-form-textarea-wrap {
|
.bd-form-textarea-wrap,
|
||||||
|
textarea.bd-textarea {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||||
|
@ -62,6 +63,7 @@
|
||||||
color: #b9bbbe;
|
color: #b9bbbe;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
max-height: 140px;
|
max-height: 140px;
|
||||||
|
transition: border-color .2s ease, color .2s ease;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
@ -71,9 +73,24 @@
|
||||||
@include scrollbar;
|
@include scrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
div[contenteditable] {
|
div[contenteditable],
|
||||||
|
textarea {
|
||||||
padding: 11px;
|
padding: 11px;
|
||||||
cursor: text;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,10 @@
|
||||||
<Card :item="plugin">
|
<Card :item="plugin">
|
||||||
<SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :checked="plugin.enabled" :change="togglePlugin" />
|
<SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :checked="plugin.enabled" :change="togglePlugin" />
|
||||||
<ButtonGroup slot="controls">
|
<ButtonGroup slot="controls">
|
||||||
<Button v-tooltip="'Settings'" v-if="plugin.hasSettings" :onClick="() => showSettings(plugin)"><MiSettings size="18" /></Button>
|
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="plugin.hasSettings" :onClick="e => showSettings(e.shiftKey)"><MiSettings size="18" /></Button>
|
||||||
<Button v-tooltip="'Reload'" :onClick="reloadPlugin"><MiRefresh size="18" /></Button>
|
<Button v-tooltip="'Reload'" :onClick="reloadPlugin"><MiRefresh size="18" /></Button>
|
||||||
<Button v-tooltip="'Edit'" :onClick="editPlugin"><MiPencil size="18" /></Button>
|
<Button v-tooltip="'Edit'" :onClick="editPlugin"><MiPencil size="18" /></Button>
|
||||||
<Button v-tooltip="'Uninstall (shift + click to unload)'" :onClick="deletePlugin" type="err"><MiDelete size="18" /></Button>
|
<Button v-tooltip="'Uninstall (shift + click to unload)'" :onClick="e => deletePlugin(e.shiftKey)" type="err"><MiDelete size="18" /></Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<div class="bd-flex bd-flex-col bd-pluginsview">
|
<div class="bd-flex bd-flex-col bd-pluginsview">
|
||||||
<div v-if="local" class="bd-flex bd-flex-grow bd-flex-col bd-plugins-container bd-local-plugins">
|
<div v-if="local" class="bd-flex bd-flex-grow bd-flex-col bd-plugins-container bd-local-plugins">
|
||||||
<PluginCard v-for="plugin in localPlugins" :plugin="plugin" :key="plugin.id" :togglePlugin="() => togglePlugin(plugin)" :reloadPlugin="() => reloadPlugin(plugin)" :deletePlugin="e => deletePlugin(plugin, e.shiftKey)" :showSettings="() => showSettings(plugin)" />
|
<PluginCard v-for="plugin in localPlugins" :plugin="plugin" :key="plugin.id" :togglePlugin="() => togglePlugin(plugin)" :reloadPlugin="() => reloadPlugin(plugin)" :deletePlugin="unload => deletePlugin(plugin, unload)" :showSettings="dont_clone => showSettings(plugin, dont_clone)" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!local" class="bd-online-ph">
|
<div v-if="!local" class="bd-online-ph">
|
||||||
<h3>Coming Soon</h3>
|
<h3>Coming Soon</h3>
|
||||||
|
@ -93,8 +93,10 @@
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showSettings(plugin) {
|
showSettings(plugin, dont_clone) {
|
||||||
return Modals.contentSettings(plugin);
|
return Modals.contentSettings(plugin, null, {
|
||||||
|
dont_clone
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,10 @@
|
||||||
<Card :item="theme">
|
<Card :item="theme">
|
||||||
<SettingSwitch slot="toggle" :checked="theme.enabled" :change="toggleTheme" />
|
<SettingSwitch slot="toggle" :checked="theme.enabled" :change="toggleTheme" />
|
||||||
<ButtonGroup slot="controls">
|
<ButtonGroup slot="controls">
|
||||||
<Button v-tooltip="'Settings'" v-if="theme.hasSettings" :onClick="showSettings"><MiSettings size="18" /></Button>
|
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="theme.hasSettings" :onClick="e => showSettings(e.shiftKey)"><MiSettings size="18" /></Button>
|
||||||
<Button v-tooltip="'Recompile (shift + click to reload)'" :onClick="reloadTheme"><MiRefresh size="18" /></Button>
|
<Button v-tooltip="'Recompile (shift + click to reload)'" :onClick="e => reloadTheme(e.shiftKey)"><MiRefresh size="18" /></Button>
|
||||||
<Button v-tooltip="'Edit'" :onClick="editTheme"><MiPencil size="18" /></Button>
|
<Button v-tooltip="'Edit'" :onClick="editTheme"><MiPencil size="18" /></Button>
|
||||||
<Button v-tooltip="'Uninstall (shift + click to unload)'" :onClick="deleteTheme" type="err"><MiDelete size="18" /></Button>
|
<Button v-tooltip="'Uninstall (shift + click to unload)'" :onClick="e => deleteTheme(e.shiftKey)" type="err"><MiDelete size="18" /></Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<div class="bd-flex bd-flex-col bd-themesview">
|
<div class="bd-flex bd-flex-col bd-themesview">
|
||||||
<div v-if="local" class="bd-flex bd-flex-grow bd-flex-col bd-themes-container bd-local-themes">
|
<div v-if="local" class="bd-flex bd-flex-grow bd-flex-col bd-themes-container bd-local-themes">
|
||||||
<ThemeCard v-for="theme in localThemes" :theme="theme" :key="theme.id" :toggleTheme="() => toggleTheme(theme)" :reloadTheme="e => reloadTheme(theme, e.shiftKey)" :showSettings="() => showSettings(theme)" :deleteTheme="e => deleteTheme(theme, e.shiftKey)" />
|
<ThemeCard v-for="theme in localThemes" :theme="theme" :key="theme.id" :toggleTheme="() => toggleTheme(theme)" :reloadTheme="reload => reloadTheme(theme, reload)" :showSettings="dont_clone => showSettings(theme, dont_clone)" :deleteTheme="unload => deleteTheme(theme, unload)" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!local" class="bd-online-ph">
|
<div v-if="!local" class="bd-online-ph">
|
||||||
<h3>Coming Soon</h3>
|
<h3>Coming Soon</h3>
|
||||||
|
@ -94,27 +94,11 @@
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showSettings(theme) {
|
showSettings(theme, dont_clone) {
|
||||||
return Modals.contentSettings(theme);
|
return Modals.contentSettings(theme, null, {
|
||||||
|
dont_clone
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.bd-online-ph {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.bd-online-ph h3 {
|
|
||||||
color: #FFF;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 20px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.bd-online-ph a {
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -48,26 +48,14 @@
|
||||||
MiSettings, MiOpenInNew, MiMinus
|
MiSettings, MiOpenInNew, MiMinus
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addItem(openModal) {
|
async addItem(openModal) {
|
||||||
if (this.setting.disabled || this.setting.max && this.setting.items.length >= this.setting.max) return;
|
if (this.setting.disabled || this.setting.max && this.setting.items.length >= this.setting.max) return;
|
||||||
const item = this.setting.addItem();
|
const item = await this.setting.addItem();
|
||||||
if (openModal) this.showModal(item, this.setting.items.length);
|
if (openModal) this.showModal(item, this.setting.items.length - 1);
|
||||||
},
|
},
|
||||||
removeItem(item) {
|
async removeItem(item) {
|
||||||
if (this.setting.disabled || this.setting.min && this.setting.items.length <= this.setting.min) return;
|
if (this.setting.disabled || this.setting.min && this.setting.items.length <= this.setting.min) return;
|
||||||
this.setting.removeItem(item);
|
await this.setting.removeItem(item);
|
||||||
},
|
|
||||||
changeInItem(item, category_id, setting_id, value) {
|
|
||||||
console.log('Setting', item, category_id, setting_id, 'to', value);
|
|
||||||
|
|
||||||
const category = item.settings.find(c => c.category === category_id);
|
|
||||||
if (!category) return;
|
|
||||||
|
|
||||||
const setting = category.settings.find(s => s.id === setting_id);
|
|
||||||
if (!setting || Utils.compare(setting.value, value)) return;
|
|
||||||
|
|
||||||
setting.value = value;
|
|
||||||
setting.changed = !Utils.compare(setting.value, setting.old_value);
|
|
||||||
},
|
},
|
||||||
showModal(item, index) {
|
showModal(item, index) {
|
||||||
Modals.settings(item, this.setting.headertext ? this.setting.headertext.replace(/%n/, index + 1) : this.setting.text + ` #${index + 1}`);
|
Modals.settings(item, this.setting.headertext ? this.setting.headertext.replace(/%n/, index + 1) : this.setting.text + ` #${index + 1}`);
|
||||||
|
|
|
@ -16,18 +16,22 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="bd-hint">{{ setting.hint }}</div>
|
<div class="bd-hint">{{ setting.hint }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bd-form-textarea-wrap">
|
<textarea class="bd-textarea" ref="textarea" @keyup.stop v-model="setting.value" :disabled="setting.disabled"></textarea>
|
||||||
<div contenteditable="true" @keyup.stop @input="input">{{ setting.value }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['setting', 'change'],
|
props: ['setting'],
|
||||||
methods: {
|
methods: {
|
||||||
input(e) {
|
recalculateHeight() {
|
||||||
this.change(e.target.textContent);
|
const { textarea } = this.$refs;
|
||||||
|
textarea.style.height = '1px';
|
||||||
|
textarea.style.height = textarea.scrollHeight + 2 + 'px';
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$watch('setting.value', this.recalculateHeight);
|
||||||
|
this.recalculateHeight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -8,69 +8,142 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Utils, FileUtils } from 'common';
|
import { Utils, FileUtils, ClientLogger as Logger, AsyncEventEmitter } from 'common';
|
||||||
import { Settings, Events, PluginManager, ThemeManager } from 'modules';
|
import { Settings, Events, PluginManager, ThemeManager } from 'modules';
|
||||||
|
import BaseModal from './components/common/Modal.vue';
|
||||||
import BasicModal from './components/bd/modals/BasicModal.vue';
|
import BasicModal from './components/bd/modals/BasicModal.vue';
|
||||||
import ConfirmModal from './components/bd/modals/ConfirmModal.vue';
|
import ConfirmModal from './components/bd/modals/ConfirmModal.vue';
|
||||||
import ErrorModal from './components/bd/modals/ErrorModal.vue';
|
import ErrorModal from './components/bd/modals/ErrorModal.vue';
|
||||||
import SettingsModal from './components/bd/modals/SettingsModal.vue';
|
import SettingsModal from './components/bd/modals/SettingsModal.vue';
|
||||||
import PermissionModal from './components/bd/modals/PermissionModal.vue';
|
import PermissionModal from './components/bd/modals/PermissionModal.vue';
|
||||||
|
|
||||||
export default class {
|
class Modal extends AsyncEventEmitter {
|
||||||
|
constructor(_modal, component) {
|
||||||
|
super();
|
||||||
|
|
||||||
static add(modal, component) {
|
for (let key in _modal)
|
||||||
modal.component = modal.component || {
|
this[key] = _modal[key];
|
||||||
|
|
||||||
|
const modal = this;
|
||||||
|
this.component = this.component || {
|
||||||
template: '<custom-modal :modal="modal" />',
|
template: '<custom-modal :modal="modal" />',
|
||||||
components: { 'custom-modal': component },
|
components: { 'custom-modal': component },
|
||||||
data() { return { modal }; },
|
data() { return { modal }; },
|
||||||
created() {
|
mounted() {
|
||||||
modal.vueInstance = this;
|
modal.vueInstance = this;
|
||||||
modal.vue = this.$children[0];
|
modal.vue = this.$children[0];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
modal.closing = false;
|
|
||||||
modal.close = force => this.close(modal, force);
|
this.closing = false;
|
||||||
modal.id = Date.now();
|
this.id = Date.now();
|
||||||
|
this.vueInstance = undefined;
|
||||||
|
this.vue = undefined;
|
||||||
|
|
||||||
|
this.close = this.close.bind(this);
|
||||||
|
this.closed = this.once('closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the modal and removes it from the stack.
|
||||||
|
* @param {Boolean} force If not true throwing an error in the close hook will stop the modal being closed
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
close(force) {
|
||||||
|
return Modals.close(this, force);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Modals {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a modal to the open stack.
|
||||||
|
* @param {Object} modal A Modal object or options used to create a Modal object
|
||||||
|
* @param {Object} component A Vue component that will be used to render the modal (optional if modal is a Modal object or it contains a component property)
|
||||||
|
* @return {Modal} The Modal object that was passed or created using the passed options
|
||||||
|
*/
|
||||||
|
static add(_modal, component) {
|
||||||
|
const modal = _modal instanceof Modal ? _modal : new Modal(_modal, component);
|
||||||
|
|
||||||
this.stack.push(modal);
|
this.stack.push(modal);
|
||||||
Events.emit('bd-refresh-modals');
|
Events.emit('bd-refresh-modals');
|
||||||
return modal;
|
return modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
static close(modal, force) {
|
/**
|
||||||
return new Promise(async (resolve, reject) => {
|
* Closes a modal and removes it from the stack.
|
||||||
|
* @param {Modal} modal The modal to close
|
||||||
|
* @param {Boolean} force If not true throwing an error in the close hook will stop the modal being closed
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static async close(modal, force) {
|
||||||
|
try {
|
||||||
if (modal.beforeClose) {
|
if (modal.beforeClose) {
|
||||||
try {
|
const beforeCloseResult = await modal.beforeClose(force);
|
||||||
const beforeCloseResult = await modal.beforeClose(force);
|
if (beforeCloseResult) throw beforeCloseResult;
|
||||||
if (beforeCloseResult && !force) return reject(beforeCloseResult);
|
|
||||||
} catch (err) {
|
|
||||||
if (!force) return reject(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
await modal.emit('close', force);
|
||||||
|
} catch (err) {
|
||||||
|
Logger.err('Modals', ['Error thrown in modal close event:', err]);
|
||||||
|
if (!force) throw err;
|
||||||
|
}
|
||||||
|
|
||||||
modal.closing = true;
|
modal.closing = true;
|
||||||
setTimeout(() => {
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
this._stack = this.stack.filter(m => m !== modal);
|
|
||||||
Events.emit('bd-refresh-modals');
|
let index;
|
||||||
resolve();
|
while ((index = this.stack.findIndex(m => m === modal)) > -1)
|
||||||
}, 200);
|
this.stack.splice(index, 1);
|
||||||
});
|
|
||||||
|
Events.emit('bd-refresh-modals');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await modal.emit('closed', force);
|
||||||
|
} catch (err) {
|
||||||
|
Logger.err('Modals', ['Error thrown in modal closed event:', err]);
|
||||||
|
if (!force) throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static closeAll() {
|
/**
|
||||||
|
* Closes all open modals and removes them from the stack.
|
||||||
|
* @param {Boolean} force If not true throwing an error in the close hook will stop that modal and any modals higher in the stack from being closed
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static closeAll(force) {
|
||||||
|
const promises = [];
|
||||||
for (let modal of this.stack)
|
for (let modal of this.stack)
|
||||||
modal.close();
|
promises.push(modal.close(force));
|
||||||
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
static closeLast() {
|
/**
|
||||||
if (!this.stack.length) return;
|
* Closes highest modal in the stack and removes it from the stack.
|
||||||
this.stack[this.stack.length - 1].close();
|
* @param {Boolean} force If not true throwing an error in the close hook will stop the modal being closed
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
static closeLast(force) {
|
||||||
|
if (!this.stack.length) return Promise.resolve();
|
||||||
|
return this.stack[this.stack.length - 1].close(force);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new basic modal and adds it to the open stack.
|
||||||
|
* @param {String} title A string that will be displayed in the modal header
|
||||||
|
* @param {String} text A string that will be displayed in the modal body
|
||||||
|
* @return {Modal}
|
||||||
|
*/
|
||||||
static basic(title, text) {
|
static basic(title, text) {
|
||||||
return this.add({ title, text }, BasicModal);
|
return this.add({ title, text }, BasicModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new confirm modal and adds it to the open stack.
|
||||||
|
* The modal will have a promise property that will be set to a Promise object that is resolved or rejected if the user clicks the confirm button or closes the modal.
|
||||||
|
* @param {String} title A string that will be displayed in the modal header
|
||||||
|
* @param {String} text A string that will be displayed in the modal body
|
||||||
|
* @return {Modal}
|
||||||
|
*/
|
||||||
static confirm(title, text) {
|
static confirm(title, text) {
|
||||||
const modal = { title, text };
|
const modal = { title, text };
|
||||||
modal.promise = new Promise((resolve, reject) => {
|
modal.promise = new Promise((resolve, reject) => {
|
||||||
|
@ -81,6 +154,14 @@ export default class {
|
||||||
return modal;
|
return modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new permissions modal and adds it to the open stack.
|
||||||
|
* The modal will have a promise property that will be set to a Promise object that is resolved or rejected if the user accepts the permissions or closes the modal.
|
||||||
|
* @param {String} title A string that will be displayed in the modal header
|
||||||
|
* @param {String} name The requesting plugin's name
|
||||||
|
* @param {Array} perms The permissions the plugin is requesting
|
||||||
|
* @return {Modal}
|
||||||
|
*/
|
||||||
static permissions(title, name, perms) {
|
static permissions(title, name, perms) {
|
||||||
const modal = { title, name, perms };
|
const modal = { title, name, perms };
|
||||||
modal.promise = new Promise((resolve, reject) => {
|
modal.promise = new Promise((resolve, reject) => {
|
||||||
|
@ -91,10 +172,20 @@ export default class {
|
||||||
return modal;
|
return modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new error modal and adds it to the open stack.
|
||||||
|
* @param {Object} event An object containing details about the error[s] to display
|
||||||
|
* @return {Modal}
|
||||||
|
*/
|
||||||
static error(event) {
|
static error(event) {
|
||||||
return this.add({ event }, ErrorModal);
|
return this.add({ event }, ErrorModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new error modal with errors from PluginManager and ThemeManager and adds it to the open stack.
|
||||||
|
* @param {Boolean} clear Whether to clear the errors array after opening the modal
|
||||||
|
* @return {Modal}
|
||||||
|
*/
|
||||||
static showContentManagerErrors(clear = true) {
|
static showContentManagerErrors(clear = true) {
|
||||||
// Get any errors from PluginManager and ThemeManager
|
// Get any errors from PluginManager and ThemeManager
|
||||||
const errors = ([]).concat(PluginManager.errors).concat(ThemeManager.errors);
|
const errors = ([]).concat(PluginManager.errors).concat(ThemeManager.errors);
|
||||||
|
@ -122,6 +213,13 @@ export default class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new settings modal and adds it to the open stack.
|
||||||
|
* @param {SettingsSet} settingsset The SettingsSet object to [clone and] display in the modal
|
||||||
|
* @param {String} headertext A string that will be displayed in the modal header
|
||||||
|
* @param {Object} options Additional options that will be passed to the modal
|
||||||
|
* @return {Modal}
|
||||||
|
*/
|
||||||
static settings(settingsset, headertext, options) {
|
static settings(settingsset, headertext, options) {
|
||||||
return this.add(Object.assign({
|
return this.add(Object.assign({
|
||||||
headertext: headertext ? headertext : settingsset.headertext,
|
headertext: headertext ? headertext : settingsset.headertext,
|
||||||
|
@ -130,18 +228,40 @@ export default class {
|
||||||
}, options), SettingsModal);
|
}, options), SettingsModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
static internalSettings(set_id) {
|
/**
|
||||||
|
* Creates a new settings modal with one of BetterDiscord's settings sets and adds it to the open stack.
|
||||||
|
* @param {SettingsSet} set_id The ID of the SettingsSet object to [clone and] display in the modal
|
||||||
|
* @param {String} headertext A string that will be displayed in the modal header
|
||||||
|
* @return {Modal}
|
||||||
|
*/
|
||||||
|
static internalSettings(set_id, headertext) {
|
||||||
const set = Settings.getSet(set_id);
|
const set = Settings.getSet(set_id);
|
||||||
if (!set) return;
|
if (!set) return;
|
||||||
return this.settings(set, set.headertext);
|
return this.settings(set, headertext);
|
||||||
}
|
}
|
||||||
|
|
||||||
static contentSettings(content) {
|
/**
|
||||||
return this.settings(content.settings, content.name + ' Settings');
|
* Creates a new settings modal with a plugin/theme's settings set and adds it to the open stack.
|
||||||
|
* @param {SettingsSet} content The plugin/theme whose settings set is to be [cloned and] displayed in the modal
|
||||||
|
* @param {String} headertext A string that will be displayed in the modal header
|
||||||
|
* @return {Modal}
|
||||||
|
*/
|
||||||
|
static contentSettings(content, headertext, options) {
|
||||||
|
return this.settings(content.settings, headertext ? headertext : content.name + ' Settings', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of open modals.
|
||||||
|
*/
|
||||||
static get stack() {
|
static get stack() {
|
||||||
return this._stack ? this._stack : (this._stack = []);
|
return this._stack || (this._stack = []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base Vue component for modals to use.
|
||||||
|
*/
|
||||||
|
static get baseComponent() {
|
||||||
|
return BaseModal;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,32 +10,51 @@
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends Node.js' EventEmitter to trigger event listeners asyncronously.
|
||||||
|
*/
|
||||||
export default class AsyncEventEmitter extends EventEmitter {
|
export default class AsyncEventEmitter extends EventEmitter {
|
||||||
|
|
||||||
emit(event, ...data) {
|
/**
|
||||||
return new Promise(async (resolve, reject) => {
|
* Emits an event.
|
||||||
let listeners = this._events[event] || [];
|
* @param {String} event The event to emit
|
||||||
listeners = Array.isArray(listeners) ? listeners : [listeners];
|
* @param {Any} ...data Data to be passed to event listeners
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
async emit(event, ...data) {
|
||||||
|
let listeners = this._events[event] || [];
|
||||||
|
listeners = Array.isArray(listeners) ? listeners : [listeners];
|
||||||
|
|
||||||
// Special treatment of internal newListener and removeListener events
|
// Special treatment of internal newListener and removeListener events
|
||||||
if(event === 'newListener' || event === 'removeListener') {
|
if(event === 'newListener' || event === 'removeListener') {
|
||||||
data = [{
|
data = [{
|
||||||
event: data,
|
event: data,
|
||||||
fn: err => {
|
fn: err => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let listener of listeners) {
|
|
||||||
try {
|
|
||||||
await listener.call(this, ...data);
|
|
||||||
} catch (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
}
|
||||||
}
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
resolve();
|
for (let listener of listeners) {
|
||||||
|
await listener.apply(this, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an event listener that will be removed when it is called and therefore only be called once.
|
||||||
|
* If a callback is not specified a promise that is resolved once the event is triggered is returned.
|
||||||
|
*/
|
||||||
|
once(event, callback) {
|
||||||
|
if (callback) {
|
||||||
|
// If a callback was specified add this event as normal
|
||||||
|
return EventEmitter.prototype.once.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return a promise that is resolved once this event is triggered
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
EventEmitter.prototype.once.call(this, event, data => {
|
||||||
|
return resolve(data);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"id": "example-plugin-4",
|
||||||
|
"name": "Example Plugin 4",
|
||||||
|
"authors": [
|
||||||
|
"Samuel Elliott"
|
||||||
|
],
|
||||||
|
"version": 1.0,
|
||||||
|
"description": "Plugin for testing array setting events as the first example plugin has a lot of stuff in it now."
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "plugin",
|
||||||
|
"defaultConfig": [
|
||||||
|
{
|
||||||
|
"category": "default",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "array-1",
|
||||||
|
"type": "array",
|
||||||
|
"text": "Test settings array",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"category": "default",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "default-0",
|
||||||
|
"type": "bool",
|
||||||
|
"value": false,
|
||||||
|
"text": "Bool Test Setting 3",
|
||||||
|
"hint": "Bool Test Setting Hint 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "default-1",
|
||||||
|
"type": "text",
|
||||||
|
"value": "defaultValue",
|
||||||
|
"text": "Text Test Setting",
|
||||||
|
"hint": "Text Test Setting Hint"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemes": [
|
||||||
|
{
|
||||||
|
"id": "scheme-1",
|
||||||
|
"name": "Test scheme",
|
||||||
|
"icon_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Cow_female_black_white.jpg/220px-Cow_female_black_white.jpg",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"category": "default",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "default-0",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scheme-2",
|
||||||
|
"name": "Another test scheme",
|
||||||
|
"icon_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Cow_female_black_white.jpg/220px-Cow_female_black_white.jpg",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"category": "default",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"id": "default-0",
|
||||||
|
"value": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "default-1",
|
||||||
|
"value": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Cow_female_black_white.jpg/220px-Cow_female_black_white.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
module.exports = (Plugin, { Logger, Settings }) => class extends Plugin {
|
||||||
|
async onstart() {
|
||||||
|
// Some array event examples
|
||||||
|
const arraySetting = this.settings.getSetting('default', 'array-1');
|
||||||
|
Logger.log('Array setting', arraySetting);
|
||||||
|
arraySetting.on('item-added', event => Logger.log('Item', event.item, 'was added to the array setting'));
|
||||||
|
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({});
|
||||||
|
const category = await set.addCategory({ id: 'default' });
|
||||||
|
|
||||||
|
const setting = await category.addSetting({
|
||||||
|
id: 'test',
|
||||||
|
type: 'text',
|
||||||
|
text: 'Enter some text',
|
||||||
|
multiline: true // Works better now
|
||||||
|
});
|
||||||
|
|
||||||
|
setting.on('setting-updated', event => Logger.log('Setting was changed to', event.value));
|
||||||
|
|
||||||
|
const scheme = await set.addScheme({
|
||||||
|
id: 'scheme-1',
|
||||||
|
name: 'Test scheme',
|
||||||
|
icon_url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Cow_female_black_white.jpg/220px-Cow_female_black_white.jpg',
|
||||||
|
settings: [{ category: 'default', settings: [{ id: 'test', value: 'Some\npresent\n\nmultiline\n\ntext' }] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
set.on('settings-updated', async updatedSettings => {
|
||||||
|
Logger.log('Updated settings', updatedSettings);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
set.setSaved();
|
||||||
|
})
|
||||||
|
|
||||||
|
set.showModal('Custom settings panel');
|
||||||
|
}
|
||||||
|
};
|
|
@ -14,7 +14,7 @@ module.exports = (Plugin, Api, Vendor, Dependencies) => {
|
||||||
Events.subscribe('TEST_EVENT', this.eventTest);
|
Events.subscribe('TEST_EVENT', this.eventTest);
|
||||||
Logger.log('onStart');
|
Logger.log('onStart');
|
||||||
|
|
||||||
Logger.log(`Plugin setting "default-0" value: ${this.getSetting('default-0')}`);
|
Logger.log(`Plugin setting "default-0" value: ${this.settings.get('default-0')}`);
|
||||||
this.events.on('setting-updated', event => {
|
this.events.on('setting-updated', event => {
|
||||||
console.log('Received plugin setting update:', event);
|
console.log('Received plugin setting update:', event);
|
||||||
});
|
});
|
||||||
|
@ -92,14 +92,14 @@ module.exports = (Plugin, Api, Vendor, Dependencies) => {
|
||||||
test1() { return 'It works!'; }
|
test1() { return 'It works!'; }
|
||||||
test2() { return 'This works too!'; }
|
test2() { return 'This works too!'; }
|
||||||
|
|
||||||
settingChanged(category, setting_id, value) {
|
settingChanged(event) {
|
||||||
if (!this.enabled) return;
|
if (!this.enabled) return;
|
||||||
Logger.log(`${category}/${setting_id} changed to ${value}`);
|
Logger.log(`${event.category_id}/${event.setting_id} changed to ${event.value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsChanged(settings) {
|
settingsChanged(event) {
|
||||||
if (!this.enabled) return;
|
if (!this.enabled) return;
|
||||||
Logger.log([ 'Settings updated', settings ]);
|
Logger.log([ 'Settings updated', event.updatedSettings ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
get settingscomponent() {
|
get settingscomponent() {
|
||||||
|
|
Loading…
Reference in New Issue