Updated plugin API

- Renamed Settings to InternalSettings
- Renamed getPlugins/getThemes to listPlugins/listThemes
- Add Utils to the plugin API
- Add CssUtils to the plugin API
- Add error handling on plugin start
- Changed ThemeManager.getConfigAsSCSS[Map] to accept a SettingsSet instead of an array of SettingsCategory objects
- Add examples to the example plugin
This commit is contained in:
Samuel Elliott 2018-03-02 19:42:17 +00:00
parent 3168012fde
commit f9e278cc75
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
9 changed files with 178 additions and 63 deletions

View File

@ -72,9 +72,9 @@ export default class Plugin {
getSetting(setting_id, category_id) {
for (let category of this.config) {
if (category_id && category.category !== category_id) return;
if (category_id && category.category !== category_id) continue;
for (let setting of category.settings) {
if (setting.id !== setting_id) return;
if (setting.id !== setting_id) continue;
return setting.value;
}
}

View File

@ -8,24 +8,64 @@
* LICENSE file in the root directory of this source tree.
*/
import { ClientLogger as Logger } from 'common';
import { ClientLogger as Logger, ClientIPC } from 'common';
import Settings from './settings';
import ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager';
import ThemeManager from './thememanager';
import Events from './events';
import { DOM } from 'ui';
class EventsWrapper {
constructor(eventemitter) {
this.__eventemitter = eventemitter;
}
get eventSubs() {
return this._eventSubs || (this._eventSubs = []);
}
subscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
this.eventSubs.push({
event,
callback
});
this.__eventemitter.on(event, callback);
}
unsubscribe(event, callback) {
for (let index of this.eventSubs) {
if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) return;
this.__eventemitter.off(event, this.eventSubs[index].callback);
this.eventSubs.splice(index, 1);
}
}
unsubscribeAll() {
for (let event of this.eventSubs) {
this.__eventemitter.off(event.event, event.callback);
}
this._eventSubs = [];
}
}
export default class PluginApi {
constructor(pluginInfo) {
this.pluginInfo = pluginInfo;
this.Events = new EventsWrapper(Events);
}
loggerLog(message) { Logger.log(this.pluginInfo.name, message) }
loggerErr(message) { Logger.err(this.pluginInfo.name, message) }
loggerWarn(message) { Logger.warn(this.pluginInfo.name, message) }
loggerInfo(message) { Logger.info(this.pluginInfo.name, message) }
loggerDbg(message) { Logger.dbg(this.pluginInfo.name, message) }
get plugin() {
return PluginManager.getPluginById(this.pluginInfo.id || this.pluginInfo.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-'));
}
loggerLog(...message) { Logger.log(this.pluginInfo.name, message) }
loggerErr(...message) { Logger.err(this.pluginInfo.name, message) }
loggerWarn(...message) { Logger.warn(this.pluginInfo.name, message) }
loggerInfo(...message) { Logger.info(this.pluginInfo.name, message) }
loggerDbg(...message) { Logger.dbg(this.pluginInfo.name, message) }
get Logger() {
return {
log: this.loggerLog.bind(this),
@ -36,44 +76,65 @@ export default class PluginApi {
};
}
get eventSubs() {
return this._eventSubs || (this._eventSubs = []);
}
eventSubscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event)) return;
this.eventSubs.push({
event,
callback
});
Events.on(event, callback);
}
eventUnsubscribe(event) {
const index = this.eventSubs.findIndex(e => e.event === event);
if (index < 0) return;
Events.off(event, this.eventSubs[0].callback);
this.eventSubs.splice(index, 1);
}
eventUnsubscribeAll() {
this.eventSubs.forEach(event => {
Events.off(event.event, event.callback);
});
this._eventSubs = [];
}
get Events() {
get Utils() {
return {
subscribe: this.eventSubscribe.bind(this),
unsubscribe: this.eventUnsubscribe.bind(this),
unsubscribeAll: this.eventUnsubscribeAll.bind(this)
}
overload: () => Utils.overload.apply(Utils, arguments),
tryParseJson: () => Utils.tryParseJson.apply(Utils, arguments),
toCamelCase: () => Utils.toCamelCase.apply(Utils, arguments),
compare: () => Utils.compare.apply(Utils, arguments),
deepclone: () => Utils.deepclone.apply(Utils, arguments)
};
}
getSetting(set, category, setting) {
getInternalSetting(set, category, setting) {
return Settings.get(set, category, setting);
}
get Settings() {
get InternalSettings() {
return {
get: this.getSetting.bind(this)
get: this.getInternalSetting.bind(this)
};
}
get injectedStyles() {
return this._injectedStyles || (this._injectedStyles = []);
}
compileSass(scss, options) {
return ClientIPC.send('bd-compileSass', Object.assign({ data: scss }, options));
}
getConfigAsSCSS(settingsset) {
return ThemeManager.getConfigAsSCSS(settingsset ? settingsset : this.plugin.settings);
}
injectStyle(id, css) {
if (id && !css) css = id, id = undefined;
this.deleteStyle(id);
const styleid = `plugin-${this.getPlugin().id}-${id}`;
this.injectedStyles.push(styleid);
DOM.injectStyle(css, styleid);
}
async injectSass(id, scss, options) {
// In most cases a plugin's styles should be precompiled instead of using this
if (id && !scss && !options) scss = id, id = undefined;
const css = await this.compileSass(scss, options);
this.injectStyle(id, css, options);
}
deleteStyle(id) {
const styleid = `plugin-${this.getPlugin().id}-${id}`;
this.injectedStyles.splice(this.injectedStyles.indexOf(styleid), 1);
DOM.deleteStyle(styleid);
}
deleteAllStyles(id, css) {
for (let id of this.injectedStyles) {
this.deleteStyle(id);
}
}
get CssUtils() {
return {
compileSass: this.compileSass.bind(this),
getConfigAsSCSS: this.getConfigAsSCSS.bind(this),
injectStyle: this.injectStyle.bind(this),
injectSass: this.injectSass.bind(this),
deleteStyle: this.deleteStyle.bind(this),
deleteAllStyles: this.deleteAllStyles.bind(this)
};
}
@ -81,13 +142,13 @@ export default class PluginApi {
// This should require extra permissions
return await PluginManager.waitForPlugin(plugin_id);
}
getPlugins(plugin_id) {
listPlugins(plugin_id) {
return PluginManager.localContent.map(plugin => plugin.id);
}
get Plugins() {
return {
getPlugin: this.getPlugin.bind(this),
getPlugins: this.getPlugins.bind(this)
listPlugins: this.listPlugins.bind(this)
};
}
@ -95,13 +156,13 @@ export default class PluginApi {
// This should require extra permissions
return await ThemeManager.waitForContent(theme_id);
}
getThemes(plugin_id) {
listThemes(plugin_id) {
return ThemeManager.localContent.map(theme => theme.id);
}
get Themes() {
return {
getTheme: this.getTheme.bind(this),
getThemes: this.getThemes.bind(this)
getThemes: this.listThemes.bind(this)
};
}

View File

@ -16,6 +16,7 @@ import Vendor from './vendor';
import { ClientLogger as Logger } from 'common';
import { Events, Permissions } from 'modules';
import { Modals } from 'ui';
import { ErrorEvent } from 'structs';
export default class extends ContentManager {
@ -37,10 +38,32 @@ export default class extends ContentManager {
static async loadAllPlugins(suppressErrors) {
this.loaded = false;
const loadAll = await this.loadAllContent(suppressErrors);
const loadAll = await this.loadAllContent(true);
this.loaded = true;
for (let plugin of this.localPlugins) {
if (plugin.enabled) plugin.start();
try {
if (plugin.enabled) plugin.start();
} 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}`,
err
}));
Logger.err(this.moduleName, [`Failed to start plugin ${plugin.name}:`, err]);
}
}
if (this.errors.length && !suppressErrors) {
Modals.error({
header: `${this.moduleName} - ${this.errors.length} ${this.contentType}${this.errors.length !== 1 ? 's' : ''} failed to load`,
module: this.moduleName,
type: 'err',
content: this.errors
});
this._errors = [];
}
return loadAll;

View File

@ -117,7 +117,7 @@ export default class Theme {
let css = '';
if (this.info.type === 'sass') {
const config = await ThemeManager.getConfigAsSCSS(this.config);
const config = await ThemeManager.getConfigAsSCSS(this.settings);
css = await ClientIPC.send('bd-compileSass', {
data: config,

View File

@ -72,10 +72,10 @@ export default class ThemeManager extends ContentManager {
return theme instanceof Theme;
}
static async getConfigAsSCSS(config) {
static async getConfigAsSCSS(settingsset) {
const variables = [];
for (let category of config) {
for (let category of settingsset.categories) {
for (let setting of category.settings) {
const setting_scss = await this.parseSetting(setting);
if (setting_scss) variables.push(`$${setting_scss[0]}: ${setting_scss[1]};`);
@ -85,10 +85,10 @@ export default class ThemeManager extends ContentManager {
return variables.join('\n');
}
static async getConfigAsSCSSMap(config) {
static async getConfigAsSCSSMap(settingsset) {
const variables = [];
for (let category of config) {
for (let category of settingsset.categories) {
for (let setting of category.settings) {
const setting_scss = await this.parseSetting(setting);
if (setting_scss) variables.push(`${setting_scss[0]}: (${setting_scss[1]})`);

View File

@ -20,8 +20,6 @@ export default class ArraySetting extends Setting {
constructor(args) {
super(args);
console.log(this);
this.args.settings = this.settings.map(category => new SettingsCategory(category));
this.args.schemes = this.schemes.map(scheme => new SettingsScheme(scheme));
this.args.items = this.value ? this.value.map(item => this.createItem(item.args || item)) : [];
@ -101,7 +99,6 @@ export default class ArraySetting extends Setting {
updateValue(emit_multi = true, emit = true) {
return this.__proto__.__proto__.setValue.call(this, this.items.map(item => {
console.log('ArraySetting.updateValue:', item);
if (!item) return;
item.setSaved();
return item.strip();
@ -121,7 +118,7 @@ export default class ArraySetting extends Setting {
async toSCSS() {
const maps = [];
for (let item of this.items)
maps.push(await ThemeManager.getConfigAsSCSSMap(item.settings));
maps.push(await ThemeManager.getConfigAsSCSSMap(item));
// Final comma ensures the variable is a list
return maps.length ? maps.join(', ') + ',' : '()';

View File

@ -84,7 +84,7 @@ class DOMObserver {
}
class DOM {
export default class DOM {
static get observer() {
return this._observer || (this._observer = new DOMObserver());
@ -123,7 +123,7 @@ class DOM {
static deleteStyle(id) {
const exists = this.getElement(`bd-styles > #${id}`);
if (exists) exists.remove();
}
}
static injectStyle(css, id) {
this.deleteStyle(id);
@ -153,5 +153,3 @@ class DOM {
return style;
}
}
export default DOM;

View File

@ -4,7 +4,7 @@ module.exports.default = {
};
const component = {
template: "<div style=\"margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;\">Test custom setting {{ setting.id }}. This is included inline with the plugin/theme's config. (Which means it can't use any functions, but can still bind functions to events.) <button class=\"bd-button bd-button-primary\" style=\"display: inline-block; margin-left: 10px;\" @click=\"change(1)\">Set value to 1</button> <button class=\"bd-button bd-button-primary\" style=\"display: inline-block; margin-left: 10px;\" @click=\"change(2)\">Set value to 2</button></div>",
template: "<div style=\"margin-bottom: 15px; background-color: rgba(0, 0, 0, 0.2); border: 1px dashed rgba(255, 255, 255, 0.2); padding: 10px; color: #f6f6f7; font-weight: 500; font-size: 15px;\">Test custom setting {{ setting.id }}. Also in component.js. It extends the CustomSetting class. <button class=\"bd-button bd-button-primary\" style=\"display: inline-block; margin-left: 10px;\" @click=\"change(1)\">Set value to 1</button> <button class=\"bd-button bd-button-primary\" style=\"display: inline-block; margin-left: 10px;\" @click=\"change(2)\">Set value to 2</button></div>",
props: ['setting', 'change']
};

View File

@ -1,10 +1,12 @@
module.exports = (Plugin, Api, Vendor, Dependencies) => {
const { $, moment, _ } = Vendor;
const { Events, Logger } = Api;
const { Events, Logger, InternalSettings, CssUtils } = Api;
return class extends Plugin {
onStart() {
async onStart() {
await this.injectStyles();
Events.subscribe('TEST_EVENT', this.eventTest);
Logger.log('onStart');
@ -16,7 +18,24 @@ module.exports = (Plugin, Api, Vendor, Dependencies) => {
console.log('Received plugin settings update:', event);
});
Logger.log(`Internal setting "core/default/test-setting" value: ${Api.Settings.get('core', 'default', 'test-setting')}`);
this.settings.on('setting-updated', event => {
Logger.log(`Setting ${event.category.id}/${event.setting.id} changed from ${event.old_value} to ${event.value}:`, event);
});
// this.settings.categories.find(c => c.id === 'default').settings.find(s => s.id === 'default-5')
this.settings.getSetting('default', 'default-0').on('setting-updated', async event => {
Logger.log(`Some feature ${event.value ? 'enabled' : 'disabled'}`);
});
this.settings.on('settings-updated', async event => {
await this.injectStyles();
Logger.log('Settings updated:', event, 'Waiting before saving complete...');
await new Promise(resolve => setTimeout(resolve, 5000));
Logger.log('Done');
});
Logger.log(`Internal setting "core/default/test-setting" value: ${InternalSettings.get('core', 'default', 'test-setting')}`);
Events.subscribe('setting-updated', event => {
console.log('Received internal setting update:', event);
});
@ -26,7 +45,24 @@ module.exports = (Plugin, Api, Vendor, Dependencies) => {
return true;
}
async injectStyles() {
const scss = await CssUtils.getConfigAsSCSS() + `.layer-kosS71 .guilds-wrapper + * {
&::before {
content: 'Example plugin stuff (test radio setting #{$default-5} selected)';
display: block;
padding: 10px 40px;
color: #eee;
background-color: #202225;
text-align: center;
font-size: 14px;
}
}`;
Logger.log('Plugin SCSS:', scss);
await CssUtils.injectSass(scss);
}
onStop() {
CssUtils.deleteAllStyles();
Events.unsubscribeAll();
Logger.log('onStop');
console.log(this.showSettingsModal());