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.
|
||||
*/
|
||||
|
||||
import Content from './content';
|
||||
import Globals from './globals';
|
||||
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
|
||||
import path from 'path';
|
||||
|
@ -245,17 +246,19 @@ export default class {
|
|||
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
|
||||
|
||||
try {
|
||||
if (content.enabled && content.disable) content.disable(false);
|
||||
if (content.enabled && content.stop) content.stop(false);
|
||||
if (content.onunload) content.onunload(reload);
|
||||
if (content.onUnload) content.onUnload(reload);
|
||||
await content.disable(false);
|
||||
await content.emit('unload', reload);
|
||||
|
||||
const index = this.getContentIndex(content);
|
||||
|
||||
delete window.require.cache[window.require.resolve(content.paths.mainPath)];
|
||||
|
||||
if (reload) {
|
||||
const newcontent = await this.preloadContent(content.dirName, true, index);
|
||||
if (newcontent.enabled && newcontent.start) newcontent.start(false);
|
||||
if (newcontent.enabled) {
|
||||
newcontent.userConfig.enabled = false;
|
||||
newcontent.start(false);
|
||||
}
|
||||
return newcontent;
|
||||
} else this.localContent.splice(index, 1);
|
||||
} catch (err) {
|
||||
|
@ -268,7 +271,7 @@ export default class {
|
|||
* Reload content
|
||||
* @param {any} content Content to reload
|
||||
*/
|
||||
static async reloadContent(content) {
|
||||
static reloadContent(content) {
|
||||
return this.unloadContent(content, true);
|
||||
}
|
||||
|
||||
|
@ -295,12 +298,20 @@ export default class {
|
|||
* @param {any} content Object to check
|
||||
*/
|
||||
static isThisContent(content) {
|
||||
return false;
|
||||
return content instanceof Content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first content where calling {function} returns true.
|
||||
* @param {Function} function A function to call to filter content
|
||||
*/
|
||||
static find(f) {
|
||||
return this.localContent.find(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wildcard content finder
|
||||
* @param {any} wild Content name | id | path | dirname
|
||||
* @param {any} wild Content ID / directory name / path / name
|
||||
* @param {bool} nonunique Allow searching attributes that may not be unique
|
||||
*/
|
||||
static findContent(wild, nonunique) {
|
||||
|
@ -313,10 +324,10 @@ export default class {
|
|||
}
|
||||
|
||||
static getContentIndex(content) { return this.localContent.findIndex(c => c === content) }
|
||||
static getContentByName(name) { return this.localContent.find(c => c.name === name) }
|
||||
static getContentById(id) { return this.localContent.find(c => c.id === id) }
|
||||
static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
|
||||
static getContentByDirName(dirName) { return this.localContent.find(c => c.dirName === dirName) }
|
||||
static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
|
||||
static getContentByName(name) { return this.localContent.find(c => c.name === name) }
|
||||
|
||||
/**
|
||||
* Wait for content to load
|
||||
|
|
|
@ -8,37 +8,15 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { AsyncEventEmitter } from 'common';
|
||||
import { EventEmitter } from 'events';
|
||||
import Content from './content';
|
||||
|
||||
export default class ExtModule {
|
||||
export default class ExtModule extends Content {
|
||||
|
||||
constructor(pluginInternals) {
|
||||
this.__pluginInternals = pluginInternals;
|
||||
constructor(internals) {
|
||||
super(internals);
|
||||
this.__require = window.require(this.paths.mainPath);
|
||||
this.hasSettings = false;
|
||||
}
|
||||
|
||||
get type() { return 'module' }
|
||||
get configs() { return this.__pluginInternals.configs }
|
||||
get info() { return this.__pluginInternals.info }
|
||||
get icon() { return this.info.icon }
|
||||
get paths() { return this.__pluginInternals.paths }
|
||||
get main() { return this.__pluginInternals.main }
|
||||
get defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
get configSchemes() { return this.configs.schemes }
|
||||
get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
|
||||
get name() { return this.info.name }
|
||||
get description() { return this.info.description }
|
||||
get authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
get contentPath() { return this.paths.contentPath }
|
||||
get modulePath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return true }
|
||||
get config() { return this.userConfig.config || [] }
|
||||
get data() { return this.userConfig.data || (this.userConfig.data = {}) }
|
||||
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new AsyncEventEmitter()) }
|
||||
|
||||
}
|
||||
|
|
|
@ -8,93 +8,19 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { Utils, FileUtils, AsyncEventEmitter } from 'common';
|
||||
import { Modals } from 'ui';
|
||||
import { EventEmitter } from 'events';
|
||||
import PluginManager from './pluginmanager';
|
||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
||||
import Content from './content';
|
||||
|
||||
export default class Plugin {
|
||||
|
||||
constructor(pluginInternals) {
|
||||
this.__pluginInternals = pluginInternals;
|
||||
this.saveConfiguration = this.saveConfiguration.bind(this);
|
||||
this.hasSettings = this.config && this.config.length > 0;
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
|
||||
this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
|
||||
this.settings.on('settings-updated', event => this.events.emit('settings-updated', event));
|
||||
this.settings.on('settings-updated', event => this.saveConfiguration());
|
||||
}
|
||||
export default class Plugin extends Content {
|
||||
|
||||
get type() { return 'plugin' }
|
||||
get configs() { return this.__pluginInternals.configs }
|
||||
get info() { return this.__pluginInternals.info }
|
||||
get icon() { return this.info.icon }
|
||||
get paths() { return this.__pluginInternals.paths }
|
||||
get main() { return this.__pluginInternals.main }
|
||||
get defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
get configSchemes() { return this.configs.schemes }
|
||||
get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
|
||||
get name() { return this.info.name }
|
||||
get description() { return this.info.description }
|
||||
get authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
get contentPath() { return this.paths.contentPath }
|
||||
get pluginPath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return this.userConfig.enabled }
|
||||
get settings() { return this.userConfig.config }
|
||||
get config() { return this.settings.settings }
|
||||
|
||||
// Don't use - these will eventually be removed!
|
||||
get pluginPath() { return this.contentPath }
|
||||
get pluginConfig() { return this.config }
|
||||
get data() { return this.userConfig.data || (this.userConfig.data = {}) }
|
||||
get exports() { return this._exports ? this._exports : (this._exports = this.getExports()) }
|
||||
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new AsyncEventEmitter()) }
|
||||
|
||||
showSettingsModal() {
|
||||
return Modals.contentSettings(this);
|
||||
}
|
||||
|
||||
async saveConfiguration() {
|
||||
try {
|
||||
await FileUtils.writeFile(`${this.pluginPath}/user.config.json`, JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
config: this.settings.strip().settings,
|
||||
data: this.data
|
||||
}));
|
||||
|
||||
this.settings.setSaved();
|
||||
} catch (err) {
|
||||
console.error(`Plugin ${this.id} configuration failed to save`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
start(save = true) {
|
||||
if (this.onstart && !this.onstart()) return false;
|
||||
if (this.onStart && !this.onStart()) return false;
|
||||
|
||||
if (!this.enabled) {
|
||||
this.userConfig.enabled = true;
|
||||
if (save) this.saveConfiguration();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
stop(save = true) {
|
||||
if (this.onstop && !this.onstop()) return false;
|
||||
if (this.onStop && !this.onStop()) return false;
|
||||
|
||||
if (this.enabled) {
|
||||
this.userConfig.enabled = false;
|
||||
if (save) this.saveConfiguration();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
get start() { return this.enable }
|
||||
get stop() { return this.disable }
|
||||
|
||||
unload() {
|
||||
PluginManager.unloadPlugin(this);
|
||||
|
|
|
@ -14,6 +14,7 @@ import ExtModuleManager from './extmodulemanager';
|
|||
import PluginManager from './pluginmanager';
|
||||
import ThemeManager from './thememanager';
|
||||
import Events from './events';
|
||||
import WebpackModules from './webpackmodules';
|
||||
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
|
||||
import { Modals, DOM } from 'ui';
|
||||
import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue';
|
||||
|
@ -63,6 +64,24 @@ export default class PluginApi {
|
|||
return PluginManager.getPluginById(this.pluginInfo.id || this.pluginInfo.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-'));
|
||||
}
|
||||
|
||||
async bridge(plugin_id) {
|
||||
const plugin = await PluginManager.waitForPlugin(plugin_id);
|
||||
return plugin.bridge;
|
||||
}
|
||||
|
||||
get require() { return this.import }
|
||||
import(m) {
|
||||
const module = ExtModuleManager.findModule(m);
|
||||
if (module && module.__require) return module.__require;
|
||||
return null;
|
||||
}
|
||||
|
||||
get Api() { return this }
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
|
||||
loggerLog(...message) { Logger.log(this.pluginInfo.name, message) }
|
||||
loggerErr(...message) { Logger.err(this.pluginInfo.name, message) }
|
||||
loggerWarn(...message) { Logger.warn(this.pluginInfo.name, message) }
|
||||
|
@ -78,6 +97,10 @@ export default class PluginApi {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
get Utils() {
|
||||
return {
|
||||
overload: () => Utils.overload.apply(Utils, arguments),
|
||||
|
@ -92,8 +115,12 @@ export default class PluginApi {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
|
||||
createSettingsSet(args, ...merge) {
|
||||
return new SettingsSet(args, ...merge);
|
||||
return new SettingsSet(args || {}, ...merge);
|
||||
}
|
||||
createSettingsCategory(args, ...merge) {
|
||||
return new SettingsCategory(args, ...merge);
|
||||
|
@ -106,13 +133,17 @@ export default class PluginApi {
|
|||
}
|
||||
get Settings() {
|
||||
return {
|
||||
createSet: this.createSet.bind(this),
|
||||
createSet: this.createSettingsSet.bind(this),
|
||||
createCategory: this.createSettingsCategory.bind(this),
|
||||
createSetting: this.createSetting.bind(this),
|
||||
createScheme: this.createSettingsScheme.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* InternalSettings
|
||||
*/
|
||||
|
||||
getInternalSetting(set, category, setting) {
|
||||
return Settings.get(set, category, setting);
|
||||
}
|
||||
|
@ -122,6 +153,10 @@ export default class PluginApi {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* CssUtils
|
||||
*/
|
||||
|
||||
get injectedStyles() {
|
||||
return this._injectedStyles || (this._injectedStyles = []);
|
||||
}
|
||||
|
@ -169,36 +204,48 @@ export default class PluginApi {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Modals
|
||||
*/
|
||||
|
||||
get modalStack() {
|
||||
return this._modalStack || (this._modalStack = []);
|
||||
}
|
||||
get baseModalComponent() {
|
||||
return Modals.baseComponent;
|
||||
}
|
||||
addModal(_modal, component) {
|
||||
const modal = Modals.add(_modal, component);
|
||||
modal.close = force => this.closeModal(modal, force);
|
||||
modal.on('close', () => {
|
||||
let index;
|
||||
while ((index = this.modalStack.findIndex(m => m === modal)) > -1)
|
||||
this.modalStack.splice(index, 1);
|
||||
});
|
||||
this.modalStack.push(modal);
|
||||
return modal;
|
||||
}
|
||||
async closeModal(modal, force) {
|
||||
await Modals.close(modal, force);
|
||||
this._modalStack = this.modalStack.filter(m => m !== modal);
|
||||
closeModal(modal, force) {
|
||||
return Modals.close(modal, force);
|
||||
}
|
||||
closeAllModals() {
|
||||
closeAllModals(force) {
|
||||
const promises = [];
|
||||
for (let modal of this.modalStack)
|
||||
modal.close();
|
||||
promises.push(modal.close(force));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
closeLastModal() {
|
||||
closeLastModal(force) {
|
||||
if (!this.modalStack.length) return;
|
||||
this.modalStack[this.modalStack.length - 1].close();
|
||||
return this.modalStack[this.modalStack.length - 1].close(force);
|
||||
}
|
||||
basicModal(title, text) {
|
||||
return this.addModal(Modals.basic(title, text));
|
||||
}
|
||||
settingsModal(settingsset, headertext, options) {
|
||||
return this.addModal(Object.assign({
|
||||
headertext: headertext ? headertext : settingsset.headertext,
|
||||
settings: settingsset,
|
||||
schemes: settingsset.schemes
|
||||
}, options), SettingsModal);
|
||||
return this.addModal(Modals.settings(settingsset, headertext, options));
|
||||
}
|
||||
get Modals() {
|
||||
return Object.defineProperty({
|
||||
return Object.defineProperty(Object.defineProperty({
|
||||
add: this.addModal.bind(this),
|
||||
close: this.closeModal.bind(this),
|
||||
closeAll: this.closeAllModals.bind(this),
|
||||
|
@ -206,14 +253,20 @@ export default class PluginApi {
|
|||
settings: this.settingsModal.bind(this)
|
||||
}, 'stack', {
|
||||
get: () => this.modalStack
|
||||
}), 'baseComponent', {
|
||||
get: () => this.baseModalComponent
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugins
|
||||
*/
|
||||
|
||||
async getPlugin(plugin_id) {
|
||||
// This should require extra permissions
|
||||
return await PluginManager.waitForPlugin(plugin_id);
|
||||
}
|
||||
listPlugins(plugin_id) {
|
||||
listPlugins() {
|
||||
return PluginManager.localContent.map(plugin => plugin.id);
|
||||
}
|
||||
get Plugins() {
|
||||
|
@ -223,30 +276,75 @@ export default class PluginApi {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Themes
|
||||
*/
|
||||
|
||||
async getTheme(theme_id) {
|
||||
// This should require extra permissions
|
||||
return await ThemeManager.waitForContent(theme_id);
|
||||
}
|
||||
listThemes(plugin_id) {
|
||||
listThemes() {
|
||||
return ThemeManager.localContent.map(theme => theme.id);
|
||||
}
|
||||
get Themes() {
|
||||
return {
|
||||
getTheme: this.getTheme.bind(this),
|
||||
getThemes: this.listThemes.bind(this)
|
||||
listThemes: this.listThemes.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
async bridge(plugin_id) {
|
||||
const plugin = await PluginManager.waitForPlugin(plugin_id);
|
||||
return plugin.bridge;
|
||||
/**
|
||||
* ExtModules
|
||||
*/
|
||||
|
||||
async getModule(module_id) {
|
||||
// This should require extra permissions
|
||||
return await ExtModuleManager.waitForContent(module_id);
|
||||
}
|
||||
listModules() {
|
||||
return ExtModuleManager.localContent.map(module => module.id);
|
||||
}
|
||||
get ExtModules() {
|
||||
return {
|
||||
getModule: this.getModule.bind(this),
|
||||
listModules: this.listModules.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
get require() { return this.import }
|
||||
import(m) {
|
||||
const module = ExtModuleManager.findModule(m);
|
||||
if (module && module.__require) return module.__require;
|
||||
return null;
|
||||
/**
|
||||
* WebpackModules
|
||||
*/
|
||||
|
||||
get webpackRequire() {
|
||||
return WebpackModules.require;
|
||||
}
|
||||
getWebpackModule(filter, first = true) {
|
||||
return WebpackModules.getModule(filter, first);
|
||||
}
|
||||
getWebpackModuleByName(name, fallback) {
|
||||
return WebpackModules.getModuleByName(name, fallback);
|
||||
}
|
||||
getWebpackModuleByRegex(regex, first = true) {
|
||||
return WebpackModules.getModuleByRegex(regex, first);
|
||||
}
|
||||
getWebpackModuleByProperties(props, first = true) {
|
||||
return WebpackModules.getModuleByProps(props, first);
|
||||
}
|
||||
getWebpackModuleByPrototypeFields(props, first = true) {
|
||||
return WebpackModules.getModuleByPrototypes(props, first);
|
||||
}
|
||||
get WebpackModules() {
|
||||
return Object.defineProperty({
|
||||
getModule: this.getWebpackModule.bind(this),
|
||||
getModuleByName: this.getWebpackModuleByName.bind(this),
|
||||
getModuleByDisplayName: this.getWebpackModuleByName.bind(this),
|
||||
getModuleByRegex: this.getWebpackModuleByRegex.bind(this),
|
||||
getModuleByProperties: this.getWebpackModuleByProperties.bind(this),
|
||||
getModuleByPrototypeFields: this.getWebpackModuleByPrototypeFields.bind(this)
|
||||
}, 'require', {
|
||||
get: () => this.webpackRequire
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,11 +41,13 @@ export default class extends ContentManager {
|
|||
const loadAll = await this.loadAllContent(true);
|
||||
this.loaded = true;
|
||||
for (let plugin of this.localPlugins) {
|
||||
if (!plugin.enabled) continue;
|
||||
plugin.userConfig.enabled = false;
|
||||
|
||||
try {
|
||||
if (plugin.enabled) plugin.start();
|
||||
plugin.start(false);
|
||||
} catch (err) {
|
||||
// Disable the plugin but don't save it - the next time BetterDiscord is started the plugin will attempt to start again
|
||||
plugin.userConfig.enabled = false;
|
||||
this.errors.push(new ErrorEvent({
|
||||
module: this.moduleName,
|
||||
message: `Failed to start ${plugin.name}`,
|
||||
|
@ -72,7 +74,6 @@ export default class extends ContentManager {
|
|||
|
||||
static get loadContent() { return this.loadPlugin }
|
||||
static async loadPlugin(paths, configs, info, main, dependencies, permissions) {
|
||||
|
||||
if (permissions && permissions.length > 0) {
|
||||
for (let perm of permissions) {
|
||||
console.log(`Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
|
||||
|
@ -107,7 +108,10 @@ export default class extends ContentManager {
|
|||
}
|
||||
});
|
||||
|
||||
if (instance.enabled && this.loaded) instance.start();
|
||||
if (instance.enabled && this.loaded) {
|
||||
instance.userConfig.enabled = false;
|
||||
instance.start(false);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,26 +8,17 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Content from './content';
|
||||
import Settings from './settings';
|
||||
import ThemeManager from './thememanager';
|
||||
import { EventEmitter } from 'events';
|
||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
||||
import { DOM, Modals } from 'ui';
|
||||
import { Utils, FileUtils, ClientIPC, ClientLogger as Logger, AsyncEventEmitter } from 'common';
|
||||
import { DOM } from 'ui';
|
||||
import { FileUtils, ClientIPC, ClientLogger as Logger } from 'common';
|
||||
import filewatcher from 'filewatcher';
|
||||
|
||||
export default class Theme {
|
||||
export default class Theme extends Content {
|
||||
|
||||
constructor(themeInternals) {
|
||||
this.__themeInternals = themeInternals;
|
||||
this.hasSettings = this.config && this.config.length > 0;
|
||||
this.saveConfiguration = this.saveConfiguration.bind(this);
|
||||
this.enable = this.enable.bind(this);
|
||||
this.disable = this.disable.bind(this);
|
||||
|
||||
this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
|
||||
this.settings.on('settings-updated', event => this.events.emit('settings-updated', event));
|
||||
this.settings.on('settings-updated', event => this.recompile());
|
||||
constructor(internals) {
|
||||
super(internals);
|
||||
|
||||
const watchfiles = Settings.getSetting('css', 'default', 'watch-files');
|
||||
if (watchfiles.value) this.watchfiles = this.files;
|
||||
|
@ -37,63 +28,39 @@ export default class Theme {
|
|||
});
|
||||
}
|
||||
|
||||
get configs() { return this.__themeInternals.configs }
|
||||
get info() { return this.__themeInternals.info }
|
||||
get icon() { return this.info.icon }
|
||||
get paths() { return this.__themeInternals.paths }
|
||||
get main() { return this.__themeInternals.main }
|
||||
get loaded() { return this.__themeInternals.loaded }
|
||||
get defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
get configSchemes() { return this.configs.schemes }
|
||||
get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/\s+/g, '-') }
|
||||
get name() { return this.info.name }
|
||||
get description() { return this.info.description }
|
||||
get authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
get contentPath() { return this.paths.contentPath }
|
||||
get themePath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return this.userConfig.enabled }
|
||||
get settings() { return this.userConfig.config }
|
||||
get config() { return this.settings.settings }
|
||||
get themeConfig() { return this.config }
|
||||
get data() { return this.userConfig.data || (this.userConfig.data = {}) }
|
||||
get type() { return 'theme' }
|
||||
get css() { return this.data.css }
|
||||
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new AsyncEventEmitter()) }
|
||||
|
||||
showSettingsModal() {
|
||||
return Modals.contentSettings(this);
|
||||
// Don't use - these will eventually be removed!
|
||||
get themePath() { return this.contentPath }
|
||||
get themeConfig() { return this.config }
|
||||
|
||||
/**
|
||||
* Called when settings are updated.
|
||||
* This can be overridden by other content types.
|
||||
*/
|
||||
__settingsUpdated(event) {
|
||||
return this.recompile();
|
||||
}
|
||||
|
||||
async saveConfiguration() {
|
||||
try {
|
||||
await FileUtils.writeFile(`${this.themePath}/user.config.json`, JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
config: this.settings.strip().settings,
|
||||
data: this.data
|
||||
}));
|
||||
|
||||
this.settings.setSaved();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
enable(save = true) {
|
||||
if (!this.enabled) {
|
||||
this.userConfig.enabled = true;
|
||||
if (save) this.saveConfiguration();
|
||||
}
|
||||
/**
|
||||
* This is called when the theme is enabled.
|
||||
*/
|
||||
onstart() {
|
||||
DOM.injectTheme(this.css, this.id);
|
||||
}
|
||||
|
||||
disable(save = true) {
|
||||
this.userConfig.enabled = false;
|
||||
if (save) this.saveConfiguration();
|
||||
/**
|
||||
* This is called when the theme is disabled.
|
||||
*/
|
||||
onstop() {
|
||||
DOM.deleteTheme(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the theme and returns an object containing the CSS and an array of files that were included.
|
||||
* @return {Promise}
|
||||
*/
|
||||
async compile() {
|
||||
console.log('Compiling CSS');
|
||||
|
||||
|
@ -117,11 +84,15 @@ export default class Theme {
|
|||
};
|
||||
} else {
|
||||
return {
|
||||
css: FileUtils.readFile(this.paths.mainPath)
|
||||
css: await FileUtils.readFile(this.paths.mainPath)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the theme and updates and saves the CSS and the list of include files.
|
||||
* @return {Promise}
|
||||
*/
|
||||
async recompile() {
|
||||
const data = await this.compile();
|
||||
this.data.css = data.css;
|
||||
|
@ -136,7 +107,7 @@ export default class Theme {
|
|||
}
|
||||
|
||||
/**
|
||||
* An array of files that are imported in custom CSS.
|
||||
* An array of files that are imported in the theme's SCSS.
|
||||
* @return {Array} Files being watched
|
||||
*/
|
||||
get files() {
|
||||
|
@ -144,7 +115,7 @@ export default class Theme {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets all files that are imported in custom CSS.
|
||||
* Sets all files that are imported in the theme's SCSS.
|
||||
* @param {Array} files Files to watch
|
||||
*/
|
||||
set files(files) {
|
||||
|
|
|
@ -86,7 +86,6 @@ const KnownModules = {
|
|||
UserActivityStore: Filters.byProperties(['getActivity']),
|
||||
UserNameResolver: Filters.byProperties(['getName']),
|
||||
|
||||
|
||||
/* Emoji Store and Utils */
|
||||
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
|
||||
EmojiUtils: Filters.byProperties(['diversitySurrogate']),
|
||||
|
@ -97,7 +96,6 @@ const KnownModules = {
|
|||
InviteResolver: Filters.byProperties(['findInvite']),
|
||||
InviteActions: Filters.byProperties(['acceptInvite']),
|
||||
|
||||
|
||||
/* Discord Objects & Utils */
|
||||
DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]),
|
||||
Permissions: Filters.byProperties(['getHighestRole']),
|
||||
|
@ -122,7 +120,6 @@ const KnownModules = {
|
|||
ExperimentsManager: Filters.byProperties(['isDeveloper']),
|
||||
CurrentExperiment: Filters.byProperties(['getExperimentId']),
|
||||
|
||||
|
||||
/* Images, Avatars and Utils */
|
||||
ImageResolver: Filters.byProperties(["getUserAvatarURL"]),
|
||||
ImageUtils: Filters.byProperties(['getSizedImageSrc']),
|
||||
|
@ -176,7 +173,6 @@ const KnownModules = {
|
|||
URLParser: Filters.byProperties(['Url', 'parse']),
|
||||
ExtraURLs: Filters.byProperties(['getArticleURL']),
|
||||
|
||||
|
||||
/* DOM/React Components */
|
||||
/* ==================== */
|
||||
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
|
||||
|
@ -201,60 +197,114 @@ const KnownModules = {
|
|||
ExternalLink: Filters.byCode(/\.trusted\b/)
|
||||
};
|
||||
|
||||
export default class {
|
||||
/* Synchronous */
|
||||
export default class WebpackModules {
|
||||
|
||||
/**
|
||||
* Finds a module using a filter function.
|
||||
* @param {Function} filter A function to use to filter modules
|
||||
* @param {Boolean} first Whether to return only the first matching module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModule(filter, first = true) {
|
||||
const modules = this.getAllModules();
|
||||
const rm = [];
|
||||
for (let index in modules) {
|
||||
if (!modules.hasOwnProperty(index)) continue;
|
||||
const module = modules[index];
|
||||
const { exports } = module;
|
||||
let foundModule = null;
|
||||
|
||||
if (!exports) continue;
|
||||
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
|
||||
if (filter(exports)) foundModule = exports;
|
||||
if (!foundModule) continue;
|
||||
if (first) return foundModule;
|
||||
rm.push(foundModule);
|
||||
}
|
||||
return first || rm.length == 0 ? undefined : rm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module by it's name.
|
||||
* @param {String} name The name of the module
|
||||
* @param {Function} fallback A function to use to filter modules if not finding a known module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByName(name, fallback) {
|
||||
if (Cache.hasOwnProperty(name)) return Cache[name];
|
||||
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
|
||||
if (!fallback) return null;
|
||||
return Cache[name] = this.getModule(fallback, true);
|
||||
if (!fallback) return undefined;
|
||||
const module = this.getModule(fallback, true);
|
||||
return module ? Cache[name] = module : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module by it's display name.
|
||||
* @param {String} name The display name of the module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByDisplayName(name) {
|
||||
return this.getModule(Filters.byDisplayName(name), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module using it's code.
|
||||
* @param {RegEx} regex A regular expression to use to filter modules
|
||||
* @param {Boolean} first Whether to return the only the first matching module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByRegex(regex, first = true) {
|
||||
return this.getModule(Filters.byCode(regex), first);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module using properties on it's prototype.
|
||||
* @param {Array} props Properties to use to filter modules
|
||||
* @param {Boolean} first Whether to return only the first matching module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByPrototypes(prototypes, first = true) {
|
||||
return this.getModule(Filters.byPrototypeFields(prototypes), first);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module using it's own properties.
|
||||
* @param {Array} props Properties to use to filter modules
|
||||
* @param {Boolean} first Whether to return only the first matching module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByProps(props, first = true) {
|
||||
return this.getModule(Filters.byProperties(props), first);
|
||||
}
|
||||
|
||||
static getModule(filter, first = true) {
|
||||
const modules = this.getAllModules();
|
||||
const rm = [];
|
||||
for (let index in modules) {
|
||||
if (!modules.hasOwnProperty(index)) continue;
|
||||
const module = modules[index];
|
||||
const { exports } = module;
|
||||
let foundModule = null;
|
||||
|
||||
if (!exports) continue;
|
||||
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
|
||||
if (filter(exports)) foundModule = exports;
|
||||
if (!foundModule) continue;
|
||||
if (first) return foundModule;
|
||||
rm.push(foundModule);
|
||||
}
|
||||
return first || rm.length == 0 ? null : rm;
|
||||
}
|
||||
|
||||
static getAllModules() {
|
||||
/**
|
||||
* Discord's __webpack_require__ function.
|
||||
*/
|
||||
static get require() {
|
||||
if (this._require) return this._require;
|
||||
const id = 'bd-webpackmodules';
|
||||
const __webpack_require__ = window['webpackJsonp'](
|
||||
[],
|
||||
{
|
||||
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
|
||||
},
|
||||
[id]).default;
|
||||
const __webpack_require__ = window['webpackJsonp']([], {
|
||||
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
|
||||
}, [id]).default;
|
||||
delete __webpack_require__.m[id];
|
||||
delete __webpack_require__.c[id];
|
||||
return __webpack_require__.c;
|
||||
return this._require = __webpack_require__;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all loaded modules.
|
||||
* @return {Array}
|
||||
*/
|
||||
static getAllModules() {
|
||||
return this.require.c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of known modules.
|
||||
* @return {Array}
|
||||
*/
|
||||
static listKnownModules() {
|
||||
return Object.keys(KnownModules);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
*/
|
||||
|
||||
import Setting from './setting';
|
||||
import BaseSetting from './types/basesetting';
|
||||
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
|
||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
||||
|
||||
|
@ -24,17 +25,12 @@ export default class SettingsCategory {
|
|||
this._merge(newCategory);
|
||||
}
|
||||
|
||||
this.__settingUpdated = this.__settingUpdated.bind(this);
|
||||
this.__settingsUpdated = this.__settingsUpdated.bind(this);
|
||||
|
||||
for (let setting of this.settings) {
|
||||
setting.on('setting-updated', ({ value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({
|
||||
category: this, category_id: this.id,
|
||||
setting, setting_id: setting.id,
|
||||
value, old_value
|
||||
})));
|
||||
setting.on('settings-updated', ({ updatedSettings }) => this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||
updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
|
||||
category: this, category_id: this.id
|
||||
}, updatedSetting)))
|
||||
})));
|
||||
setting.on('setting-updated', this.__settingUpdated);
|
||||
setting.on('settings-updated', this.__settingsUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,7 +49,7 @@ export default class SettingsCategory {
|
|||
* Category name
|
||||
*/
|
||||
get name() {
|
||||
return this.args.category_name;
|
||||
return this.args.name || this.args.category_name;
|
||||
}
|
||||
|
||||
get category_name() {
|
||||
|
@ -83,6 +79,82 @@ export default class SettingsCategory {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting event listeners.
|
||||
* This only exists for use by the constructor and settingscategory.addSetting.
|
||||
*/
|
||||
__settingUpdated({ setting, value, old_value }) {
|
||||
return this.emit('setting-updated', new SettingUpdatedEvent({
|
||||
category: this, category_id: this.id,
|
||||
setting, setting_id: setting.id,
|
||||
value, old_value
|
||||
}));
|
||||
}
|
||||
|
||||
__settingsUpdated({ updatedSettings }) {
|
||||
return this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||
updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
|
||||
category: this, category_id: this.id
|
||||
}, updatedSetting)))
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically adds a setting to this category.
|
||||
* @param {Setting} category The setting to add to this category
|
||||
* @param {Number} index The index to add the setting at (optional)
|
||||
* @return {Promise}
|
||||
*/
|
||||
async addSetting(setting, index) {
|
||||
if (this.settings.find(s => s === setting)) return;
|
||||
|
||||
if (!(setting instanceof BaseSetting))
|
||||
setting = new Setting(setting);
|
||||
|
||||
if (this.getSetting(setting.id))
|
||||
throw {message: 'A setting with this ID already exists.'};
|
||||
|
||||
setting.on('setting-updated', this.__settingUpdated);
|
||||
setting.on('settings-updated', this.__settingsUpdated);
|
||||
|
||||
if (index === undefined) index = this.settings.length;
|
||||
this.settings.splice(index, 0, setting);
|
||||
|
||||
const event = {
|
||||
category: this, category_id: this.id,
|
||||
setting, setting_id: setting.id,
|
||||
at_index: index
|
||||
};
|
||||
|
||||
await setting.emit('added-to', event);
|
||||
await this.emit('added-setting', event);
|
||||
return setting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically removes a setting from this category.
|
||||
* @param {Setting} setting The setting to remove from this category
|
||||
* @return {Promise}
|
||||
*/
|
||||
async removeSetting(setting) {
|
||||
setting.off('setting-updated', this.__settingUpdated);
|
||||
setting.off('settings-updated', this.__settingsUpdated);
|
||||
|
||||
let index;
|
||||
while ((index = this.settings.findIndex(s => s === setting)) > -1) {
|
||||
this.settings.splice(index, 0);
|
||||
}
|
||||
|
||||
const event = {
|
||||
set: this, set_id: this.id,
|
||||
category: this, category_id: this.id,
|
||||
from_index: index
|
||||
};
|
||||
|
||||
await setting.emit('removed-from', event);
|
||||
await this.emit('removed-category', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first setting where calling {function} returns true.
|
||||
* @param {Function} function A function to call to filter settings
|
||||
|
@ -107,7 +179,7 @@ export default class SettingsCategory {
|
|||
* @return {Setting}
|
||||
*/
|
||||
getSetting(id) {
|
||||
return this.findSetting(setting => setting.id === id);
|
||||
return this.find(setting => setting.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,18 +27,16 @@ export default class SettingsSet {
|
|||
this._merge(newSet);
|
||||
}
|
||||
|
||||
this.__settingUpdated = this.__settingUpdated.bind(this);
|
||||
this.__settingsUpdated = this.__settingsUpdated.bind(this);
|
||||
this.__addedSetting = this.__addedSetting.bind(this);
|
||||
this.__removedSetting = this.__removedSetting.bind(this);
|
||||
|
||||
for (let category of this.categories) {
|
||||
category.on('setting-updated', ({ setting, value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
setting, setting_id: setting.id,
|
||||
value, old_value
|
||||
})));
|
||||
category.on('settings-updated', ({ updatedSettings }) => this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||
updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
|
||||
set: this, set_id: this.id
|
||||
}, updatedSetting)))
|
||||
})));
|
||||
category.on('setting-updated', this.__settingUpdated);
|
||||
category.on('settings-updated', this.__settingsUpdated);
|
||||
category.on('added-setting', this.__addedSetting);
|
||||
category.on('removed-setting', this.__removedSetting);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,6 +99,149 @@ export default class SettingsSet {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Category event listeners.
|
||||
* These only exists for use by the constructor and settingsset.addCategory.
|
||||
*/
|
||||
__settingUpdated({ category, setting, value, old_value }) {
|
||||
return this.emit('setting-updated', new SettingUpdatedEvent({
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
setting, setting_id: setting.id,
|
||||
value, old_value
|
||||
}));
|
||||
}
|
||||
|
||||
__settingsUpdated({ updatedSettings }) {
|
||||
return this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||
updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
|
||||
set: this, set_id: this.id
|
||||
}, updatedSetting)))
|
||||
}));
|
||||
}
|
||||
|
||||
__addedSetting({ category, setting, at_index }) {
|
||||
return this.emit('added-setting', {
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
setting, setting_id: setting.id,
|
||||
at_index
|
||||
});
|
||||
}
|
||||
|
||||
__removedSetting({ category, setting, from_index }) {
|
||||
return this.emit('removed-setting', {
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
setting, setting_id: setting.id,
|
||||
from_index
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically adds a category to this set.
|
||||
* @param {SettingsCategory} category The category to add to this set
|
||||
* @param {Number} index The index to add the category at (optional)
|
||||
* @return {Promise}
|
||||
*/
|
||||
async addCategory(category, index) {
|
||||
if (this.categories.find(c => c === category)) return;
|
||||
|
||||
if (!(category instanceof SettingsCategory))
|
||||
category = new SettingsCategory(category);
|
||||
|
||||
if (this.getCategory(category.id))
|
||||
throw {message: 'A category with this ID already exists.'};
|
||||
|
||||
category.on('setting-updated', this.__settingUpdated);
|
||||
category.on('settings-updated', this.__settingsUpdated);
|
||||
category.on('added-setting', this.__addedSetting);
|
||||
category.on('removed-setting', this.__removedSetting);
|
||||
|
||||
if (index === undefined) index = this.categories.length;
|
||||
this.categories.splice(index, 0, category);
|
||||
|
||||
const event = {
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
at_index: index
|
||||
};
|
||||
|
||||
await category.emit('added-to', event);
|
||||
await this.emit('added-category', event);
|
||||
return category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically removes a category from this set.
|
||||
* @param {SettingsCategory} category The category to remove from this set
|
||||
* @return {Promise}
|
||||
*/
|
||||
async removeCategory(category) {
|
||||
category.off('setting-updated', this.__settingUpdated);
|
||||
category.off('settings-updated', this.__settingsUpdated);
|
||||
category.off('added-setting', this.__addedSetting);
|
||||
category.off('removed-setting', this.__removedSetting);
|
||||
|
||||
let index;
|
||||
while ((index = this.categories.findIndex(c => c === category)) > -1) {
|
||||
this.categories.splice(index, 0);
|
||||
}
|
||||
|
||||
const event = {
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
from_index: index
|
||||
};
|
||||
|
||||
await category.emit('removed-from', event);
|
||||
await this.emit('removed-category', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically adds a scheme to this set.
|
||||
* @param {SettingsScheme} scheme The scheme to add to this set
|
||||
* @param {Number} index The index to add the scheme at (optional)
|
||||
* @return {Promise}
|
||||
*/
|
||||
async addScheme(scheme, index) {
|
||||
if (this.schemes.find(c => c === scheme)) return;
|
||||
|
||||
if (!(scheme instanceof SettingsScheme))
|
||||
scheme = new SettingsScheme(scheme);
|
||||
|
||||
if (this.schemes.find(s => s.id === scheme.id))
|
||||
throw {message: 'A scheme with this ID already exists.'};
|
||||
|
||||
if (index === undefined) index = this.schemes.length;
|
||||
this.schemes.splice(index, 0, scheme);
|
||||
|
||||
await this.emit('added-scheme', {
|
||||
set: this, set_id: this.id,
|
||||
scheme, scheme_id: scheme.id,
|
||||
at_index: index
|
||||
});
|
||||
return scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically removes a scheme from this set.
|
||||
* @param {SettingsScheme} scheme The scheme to remove from this set
|
||||
* @return {Promise}
|
||||
*/
|
||||
async removeScheme(scheme) {
|
||||
let index;
|
||||
while ((index = this.schemes.findIndex(s => s === scheme)) > -1) {
|
||||
this.schemes.splice(index, 0);
|
||||
}
|
||||
|
||||
await this.emit('removed-scheme', {
|
||||
set: this, set_id: this.id,
|
||||
scheme, scheme_id: scheme.id,
|
||||
from_index: index
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first category where calling {function} returns true.
|
||||
* @param {Function} function A function to call to filter categories
|
||||
|
|
|
@ -14,6 +14,7 @@ import Setting from './basesetting';
|
|||
import SettingsSet from '../settingsset';
|
||||
import SettingsCategory from '../settingscategory';
|
||||
import SettingsScheme from '../settingsscheme';
|
||||
import { SettingsUpdatedEvent } from 'structs';
|
||||
|
||||
export default class ArraySetting extends Setting {
|
||||
|
||||
|
@ -108,21 +109,27 @@ export default class ArraySetting extends Setting {
|
|||
* @param {SettingsSet} item Values to merge into the new set (optional)
|
||||
* @return {SettingsSet} The new set
|
||||
*/
|
||||
addItem(item) {
|
||||
const newItem = this.createItem(item);
|
||||
this.args.items.push(newItem);
|
||||
this.updateValue();
|
||||
return newItem;
|
||||
async addItem(_item) {
|
||||
const item = this.createItem(_item);
|
||||
this.args.items.push(item);
|
||||
await this.updateValue();
|
||||
|
||||
await this.emit('item-added', { item });
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a set from this array setting.
|
||||
* This ignores the minimum value.
|
||||
* @param {SettingsSet} item The set to remove
|
||||
* @return {Promise}
|
||||
*/
|
||||
removeItem(item) {
|
||||
async removeItem(item) {
|
||||
this.args.items = this.items.filter(i => i !== item);
|
||||
this.updateValue();
|
||||
await this.updateValue();
|
||||
|
||||
await this.emit('item-removed', { item });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -135,24 +142,84 @@ export default class ArraySetting extends Setting {
|
|||
return item;
|
||||
|
||||
const set = new SettingsSet({
|
||||
id: item ? item.args ? item.args.id : item.id : Math.random(),
|
||||
settings: Utils.deepclone(this.settings),
|
||||
schemes: this.schemes
|
||||
}, item ? item.args || item : undefined);
|
||||
|
||||
set.setSaved();
|
||||
set.on('settings-updated', () => this.updateValue());
|
||||
set.on('settings-updated', async event => {
|
||||
await this.emit('item-updated', { item: set, event, updatedSettings: event.updatedSettings });
|
||||
if (event.args.updating_array !== this) await this.updateValue();
|
||||
});
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to be called after the value changes.
|
||||
* This can be overridden by other settings types.
|
||||
* This function is used when the value needs to be updated synchronously (basically just in the constructor - so there won't be any events to emit anyway).
|
||||
* @param {SettingUpdatedEvent} updatedSetting
|
||||
*/
|
||||
setValueHookSync(updatedSetting) {
|
||||
this.args.items = updatedSetting.value ? updatedSetting.value.map(item => this.createItem(item)) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to be called after the value changes.
|
||||
* This can be overridden by other settings types.
|
||||
* @param {SettingUpdatedEvent} updatedSetting
|
||||
*/
|
||||
setValueHook(updatedSetting) {
|
||||
this.args.items = updatedSetting.value ? updatedSetting.value.map(item => this.createItem(item)) : [];
|
||||
async setValueHook(updatedSetting) {
|
||||
const newItems = [];
|
||||
let error;
|
||||
|
||||
for (let newItem of updatedSetting.value) {
|
||||
try {
|
||||
const item = this.items.find(i => i.id && i.id === newItem.id);
|
||||
|
||||
if (item) {
|
||||
// Merge the new item into the original item
|
||||
newItems.push(item);
|
||||
const updatedSettings = await item.merge(newItem, false);
|
||||
if (!updatedSettings.length) continue;
|
||||
|
||||
const event = new SettingsUpdatedEvent({
|
||||
updatedSettings,
|
||||
updating_array: this
|
||||
});
|
||||
|
||||
await item.emit('settings-updated', event);
|
||||
// await this.emit('item-updated', { item, event, updatedSettings });
|
||||
} else {
|
||||
// Add a new item
|
||||
const item = this.createItem(newItem);
|
||||
newItems.push(item);
|
||||
await this.emit('item-added', { item });
|
||||
}
|
||||
} catch (e) { error = e; }
|
||||
}
|
||||
|
||||
for (let item of this.items) {
|
||||
if (newItems.includes(item)) continue;
|
||||
|
||||
try {
|
||||
// Item removed
|
||||
await this.emit('item-removed', { item });
|
||||
} catch (e) { error = e; }
|
||||
}
|
||||
|
||||
this.args.items = newItems;
|
||||
|
||||
// We can't throw anything before the items array is updated, otherwise the array setting would be in an inconsistent state where the values in this.items wouldn't match the values in this.value
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
// emit(...args) {
|
||||
// console.log('Emitting event', args[0], 'with data', args[1]);
|
||||
// return this.emitter.emit(...args);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Updates the value of this array setting.
|
||||
* This only exists for use by array settings.
|
||||
|
|
|
@ -105,9 +105,9 @@ export default class Setting {
|
|||
* Merges a setting into this setting without emitting events (and therefore synchronously).
|
||||
* This only exists for use by the constructor and SettingsCategory.
|
||||
*/
|
||||
_merge(newSetting) {
|
||||
_merge(newSetting, hook = true) {
|
||||
const value = newSetting.args ? newSetting.args.value : newSetting.value;
|
||||
return this._setValue(value);
|
||||
return this._setValue(value, hook);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,12 +116,13 @@ export default class Setting {
|
|||
* @return {Promise}
|
||||
*/
|
||||
async merge(newSetting, emit_multi = true, emit = true) {
|
||||
const updatedSettings = this._merge(newSetting);
|
||||
const updatedSettings = this._merge(newSetting, false);
|
||||
if (!updatedSettings.length) return [];
|
||||
const updatedSetting = updatedSettings[0];
|
||||
|
||||
await this.setValueHook(updatedSettings[0]);
|
||||
|
||||
if (emit)
|
||||
await this.emit('setting-updated', updatedSetting);
|
||||
await this.emit('setting-updated', updatedSettings[0]);
|
||||
|
||||
if (emit_multi)
|
||||
await this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||
|
@ -135,7 +136,7 @@ export default class Setting {
|
|||
* Sets the value of this setting.
|
||||
* This only exists for use by the constructor and SettingsCategory.
|
||||
*/
|
||||
_setValue(value) {
|
||||
_setValue(value, hook = true) {
|
||||
const old_value = this.args.value;
|
||||
if (Utils.compare(value, old_value)) return [];
|
||||
this.args.value = value;
|
||||
|
@ -146,7 +147,8 @@ export default class Setting {
|
|||
value, old_value
|
||||
});
|
||||
|
||||
this.setValueHook(updatedSetting);
|
||||
if (hook)
|
||||
this.setValueHookSync(updatedSetting);
|
||||
|
||||
return [updatedSetting];
|
||||
}
|
||||
|
@ -156,7 +158,8 @@ export default class Setting {
|
|||
* This can be overridden by other settings types.
|
||||
* @param {SettingUpdatedEvent} updatedSetting
|
||||
*/
|
||||
setValueHook(updatedSetting) {}
|
||||
async setValueHook(updatedSetting) {}
|
||||
setValueHookSync(updatedSetting) {}
|
||||
|
||||
/**
|
||||
* Sets the value of this setting.
|
||||
|
@ -164,9 +167,11 @@ export default class Setting {
|
|||
* @return {Promise}
|
||||
*/
|
||||
async setValue(value, emit_multi = true, emit = true) {
|
||||
const updatedSettings = this._setValue(value);
|
||||
const updatedSettings = this._setValue(value, false);
|
||||
if (!updatedSettings.length) return [];
|
||||
|
||||
await this.setValueHook(updatedSettings[0]);
|
||||
|
||||
if (emit)
|
||||
await this.emit('setting-updated', updatedSettings[0]);
|
||||
|
||||
|
|
|
@ -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 './sidebarview.scss';
|
||||
@import './plugins.scss';
|
||||
@import './contentview.scss';
|
||||
@import './card.scss';
|
||||
@import './tooltips.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 {
|
||||
overflow: visible;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.bd-drawer-open {
|
||||
> .bd-drawer-header .bd-drawer-open-button {
|
||||
.bd-chevron-1 {
|
||||
svg {
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
}
|
||||
|
||||
.bd-form-textarea {
|
||||
.bd-form-textarea-wrap {
|
||||
.bd-form-textarea-wrap,
|
||||
textarea.bd-textarea {
|
||||
margin-top: 15px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
|
@ -62,6 +63,7 @@
|
|||
color: #b9bbbe;
|
||||
overflow-y: scroll;
|
||||
max-height: 140px;
|
||||
transition: border-color .2s ease, color .2s ease;
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
|
@ -71,9 +73,24 @@
|
|||
@include scrollbar;
|
||||
}
|
||||
|
||||
div[contenteditable] {
|
||||
div[contenteditable],
|
||||
textarea {
|
||||
padding: 11px;
|
||||
cursor: text;
|
||||
min-height: 45px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
background: transparent;
|
||||
border: none;
|
||||
resize: none;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
box-sizing: border-box;
|
||||
overflow-y: visible;
|
||||
max-height: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
<Card :item="plugin">
|
||||
<SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :checked="plugin.enabled" :change="togglePlugin" />
|
||||
<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="'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>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
<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">
|
||||
<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 v-if="!local" class="bd-online-ph">
|
||||
<h3>Coming Soon</h3>
|
||||
|
@ -93,8 +93,10 @@
|
|||
console.error(err);
|
||||
}
|
||||
},
|
||||
showSettings(plugin) {
|
||||
return Modals.contentSettings(plugin);
|
||||
showSettings(plugin, dont_clone) {
|
||||
return Modals.contentSettings(plugin, null, {
|
||||
dont_clone
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
<Card :item="theme">
|
||||
<SettingSwitch slot="toggle" :checked="theme.enabled" :change="toggleTheme" />
|
||||
<ButtonGroup slot="controls">
|
||||
<Button v-tooltip="'Settings'" v-if="theme.hasSettings" :onClick="showSettings"><MiSettings size="18" /></Button>
|
||||
<Button v-tooltip="'Recompile (shift + click to reload)'" :onClick="reloadTheme"><MiRefresh 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="e => reloadTheme(e.shiftKey)"><MiRefresh 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>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
<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">
|
||||
<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 v-if="!local" class="bd-online-ph">
|
||||
<h3>Coming Soon</h3>
|
||||
|
@ -94,27 +94,11 @@
|
|||
console.error(err);
|
||||
}
|
||||
},
|
||||
showSettings(theme) {
|
||||
return Modals.contentSettings(theme);
|
||||
showSettings(theme, dont_clone) {
|
||||
return Modals.contentSettings(theme, null, {
|
||||
dont_clone
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</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
|
||||
},
|
||||
methods: {
|
||||
addItem(openModal) {
|
||||
async addItem(openModal) {
|
||||
if (this.setting.disabled || this.setting.max && this.setting.items.length >= this.setting.max) return;
|
||||
const item = this.setting.addItem();
|
||||
if (openModal) this.showModal(item, this.setting.items.length);
|
||||
const item = await this.setting.addItem();
|
||||
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;
|
||||
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);
|
||||
await this.setting.removeItem(item);
|
||||
},
|
||||
showModal(item, index) {
|
||||
Modals.settings(item, this.setting.headertext ? this.setting.headertext.replace(/%n/, index + 1) : this.setting.text + ` #${index + 1}`);
|
||||
|
|
|
@ -16,18 +16,22 @@
|
|||
</div>
|
||||
<div class="bd-hint">{{ setting.hint }}</div>
|
||||
</div>
|
||||
<div class="bd-form-textarea-wrap">
|
||||
<div contenteditable="true" @keyup.stop @input="input">{{ setting.value }}</div>
|
||||
</div>
|
||||
<textarea class="bd-textarea" ref="textarea" @keyup.stop v-model="setting.value" :disabled="setting.disabled"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['setting', 'change'],
|
||||
props: ['setting'],
|
||||
methods: {
|
||||
input(e) {
|
||||
this.change(e.target.textContent);
|
||||
recalculateHeight() {
|
||||
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>
|
||||
|
|
|
@ -8,69 +8,142 @@
|
|||
* 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 BaseModal from './components/common/Modal.vue';
|
||||
import BasicModal from './components/bd/modals/BasicModal.vue';
|
||||
import ConfirmModal from './components/bd/modals/ConfirmModal.vue';
|
||||
import ErrorModal from './components/bd/modals/ErrorModal.vue';
|
||||
import SettingsModal from './components/bd/modals/SettingsModal.vue';
|
||||
import PermissionModal from './components/bd/modals/PermissionModal.vue';
|
||||
|
||||
export default class {
|
||||
class Modal extends AsyncEventEmitter {
|
||||
constructor(_modal, component) {
|
||||
super();
|
||||
|
||||
static add(modal, component) {
|
||||
modal.component = modal.component || {
|
||||
for (let key in _modal)
|
||||
this[key] = _modal[key];
|
||||
|
||||
const modal = this;
|
||||
this.component = this.component || {
|
||||
template: '<custom-modal :modal="modal" />',
|
||||
components: { 'custom-modal': component },
|
||||
data() { return { modal }; },
|
||||
created() {
|
||||
mounted() {
|
||||
modal.vueInstance = this;
|
||||
modal.vue = this.$children[0];
|
||||
}
|
||||
};
|
||||
modal.closing = false;
|
||||
modal.close = force => this.close(modal, force);
|
||||
modal.id = Date.now();
|
||||
|
||||
this.closing = false;
|
||||
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);
|
||||
Events.emit('bd-refresh-modals');
|
||||
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) {
|
||||
try {
|
||||
const beforeCloseResult = await modal.beforeClose(force);
|
||||
if (beforeCloseResult && !force) return reject(beforeCloseResult);
|
||||
} catch (err) {
|
||||
if (!force) return reject(err);
|
||||
}
|
||||
const beforeCloseResult = await modal.beforeClose(force);
|
||||
if (beforeCloseResult) throw beforeCloseResult;
|
||||
}
|
||||
await modal.emit('close', force);
|
||||
} catch (err) {
|
||||
Logger.err('Modals', ['Error thrown in modal close event:', err]);
|
||||
if (!force) throw err;
|
||||
}
|
||||
|
||||
modal.closing = true;
|
||||
setTimeout(() => {
|
||||
this._stack = this.stack.filter(m => m !== modal);
|
||||
Events.emit('bd-refresh-modals');
|
||||
resolve();
|
||||
}, 200);
|
||||
});
|
||||
modal.closing = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
let index;
|
||||
while ((index = this.stack.findIndex(m => m === modal)) > -1)
|
||||
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)
|
||||
modal.close();
|
||||
promises.push(modal.close(force));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
static closeLast() {
|
||||
if (!this.stack.length) return;
|
||||
this.stack[this.stack.length - 1].close();
|
||||
/**
|
||||
* Closes highest modal in the stack 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}
|
||||
*/
|
||||
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) {
|
||||
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) {
|
||||
const modal = { title, text };
|
||||
modal.promise = new Promise((resolve, reject) => {
|
||||
|
@ -81,6 +154,14 @@ export default class {
|
|||
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) {
|
||||
const modal = { title, name, perms };
|
||||
modal.promise = new Promise((resolve, reject) => {
|
||||
|
@ -91,10 +172,20 @@ export default class {
|
|||
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) {
|
||||
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) {
|
||||
// Get any errors from PluginManager and ThemeManager
|
||||
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) {
|
||||
return this.add(Object.assign({
|
||||
headertext: headertext ? headertext : settingsset.headertext,
|
||||
|
@ -130,18 +228,40 @@ export default class {
|
|||
}, 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);
|
||||
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() {
|
||||
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';
|
||||
|
||||
/**
|
||||
* Extends Node.js' EventEmitter to trigger event listeners asyncronously.
|
||||
*/
|
||||
export default class AsyncEventEmitter extends EventEmitter {
|
||||
|
||||
emit(event, ...data) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let listeners = this._events[event] || [];
|
||||
listeners = Array.isArray(listeners) ? listeners : [listeners];
|
||||
/**
|
||||
* Emits an event.
|
||||
* @param {String} event The event to emit
|
||||
* @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
|
||||
if(event === 'newListener' || event === 'removeListener') {
|
||||
data = [{
|
||||
event: data,
|
||||
fn: err => {
|
||||
if (err) throw err;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
for (let listener of listeners) {
|
||||
try {
|
||||
await listener.call(this, ...data);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
// Special treatment of internal newListener and removeListener events
|
||||
if(event === 'newListener' || event === 'removeListener') {
|
||||
data = [{
|
||||
event: data,
|
||||
fn: err => {
|
||||
if (err) throw 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);
|
||||
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 => {
|
||||
console.log('Received plugin setting update:', event);
|
||||
});
|
||||
|
@ -92,14 +92,14 @@ module.exports = (Plugin, Api, Vendor, Dependencies) => {
|
|||
test1() { return 'It works!'; }
|
||||
test2() { return 'This works too!'; }
|
||||
|
||||
settingChanged(category, setting_id, value) {
|
||||
settingChanged(event) {
|
||||
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;
|
||||
Logger.log([ 'Settings updated', settings ]);
|
||||
Logger.log([ 'Settings updated', event.updatedSettings ]);
|
||||
}
|
||||
|
||||
get settingscomponent() {
|
||||
|
|
Loading…
Reference in New Issue