Merge remote-tracking branch 'upstream/master' into add-generic-settings-modal
# Conflicts: # client/src/modules/plugin.js # tests/plugins/Example/config.json
This commit is contained in:
commit
0af22b21d4
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* BetterDiscord Content Config Utility
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
export default class ContentConfig {
|
||||
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
map(cb) {
|
||||
return this.data.map(cb);
|
||||
}
|
||||
|
||||
strip() {
|
||||
return this.map(cat => ({
|
||||
category: cat.category,
|
||||
settings: cat.settings.map(setting => ({
|
||||
id: setting.id, value: setting.value
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
|
@ -12,24 +12,43 @@ import Globals from './globals';
|
|||
import { FileUtils, ClientLogger as Logger } from 'common';
|
||||
import path from 'path';
|
||||
import { Events } from 'modules';
|
||||
import { Error } from 'structs';
|
||||
import { ErrorEvent } from 'structs';
|
||||
import { Modals } from 'ui';
|
||||
|
||||
/**
|
||||
* Base class for external content managing
|
||||
*/
|
||||
export default class {
|
||||
|
||||
/**
|
||||
* Any errors that happened
|
||||
* returns {Array}
|
||||
*/
|
||||
static get errors() {
|
||||
return this._errors || (this._errors = []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locallly stored content
|
||||
* returns {Array}
|
||||
*/
|
||||
static get localContent() {
|
||||
return this._localContent ? this._localContent : (this._localContent = []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Local path for content
|
||||
* returns {String}
|
||||
*/
|
||||
static get contentPath() {
|
||||
return this._contentPath ? this._contentPath : (this._contentPath = Globals.getObject('paths').find(path => path.id === this.pathId).path);
|
||||
}
|
||||
|
||||
static async loadAllContent(supressErrors) {
|
||||
/**
|
||||
* Load all locally stored content
|
||||
* @param {bool} suppressErrors Suppress any errors that occur during loading of content
|
||||
*/
|
||||
static async loadAllContent(suppressErrors = false) {
|
||||
try {
|
||||
await FileUtils.ensureDirectory(this.contentPath);
|
||||
const directories = await FileUtils.listDirectory(this.contentPath);
|
||||
|
@ -38,7 +57,7 @@ export default class {
|
|||
try {
|
||||
await this.preloadContent(dir);
|
||||
} catch (err) {
|
||||
this.errors.push(new Error({
|
||||
this.errors.push(new ErrorEvent({
|
||||
module: this.moduleName,
|
||||
message: `Failed to load ${dir}`,
|
||||
err
|
||||
|
@ -48,7 +67,7 @@ export default class {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.errors.length && !supressErrors) {
|
||||
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,
|
||||
|
@ -64,6 +83,9 @@ export default class {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh locally stored content
|
||||
*/
|
||||
static async refreshContent() {
|
||||
if (!this.localContent.length) return this.loadAllContent();
|
||||
|
||||
|
@ -97,6 +119,12 @@ export default class {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common loading procedure for loading content before passing it to the actual loader
|
||||
* @param {any} dirName Base directory for content
|
||||
* @param {any} reload Is content being reloaded
|
||||
* @param {any} index Index of content in {localContent}
|
||||
*/
|
||||
static async preloadContent(dirName, reload = false, index) {
|
||||
try {
|
||||
const contentPath = path.join(this.contentPath, dirName);
|
||||
|
@ -154,7 +182,7 @@ export default class {
|
|||
mainPath
|
||||
}
|
||||
|
||||
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.type);
|
||||
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies);
|
||||
if (reload) this.localContent[index] = content;
|
||||
else this.localContent.push(content);
|
||||
return content;
|
||||
|
@ -164,16 +192,28 @@ export default class {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read content config file
|
||||
* @param {any} configPath Config file path
|
||||
*/
|
||||
static async readConfig(configPath) {
|
||||
configPath = path.resolve(configPath, 'config.json');
|
||||
return FileUtils.readJsonFromFile(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read content user config file
|
||||
* @param {any} configPath User config file path
|
||||
*/
|
||||
static async readUserConfig(configPath) {
|
||||
configPath = path.resolve(configPath, 'user.config.json');
|
||||
return FileUtils.readJsonFromFile(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wildcard content finder
|
||||
* @param {any} wild Content name | id | path | dirname
|
||||
*/
|
||||
//TODO make this nicer
|
||||
static findContent(wild) {
|
||||
let content = this.getContentByName(wild);
|
||||
|
@ -191,6 +231,10 @@ export default class {
|
|||
static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
|
||||
static getContentByDirName(dirName) { return this.localContent.find(c => c.dirName === dirName) }
|
||||
|
||||
/**
|
||||
* Wait for content to load
|
||||
* @param {any} content_id
|
||||
*/
|
||||
static waitForContent(content_id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const check = () => {
|
||||
|
|
|
@ -12,8 +12,14 @@ import { ClientIPC } from 'common';
|
|||
import Settings from './settings';
|
||||
import { DOM } from 'ui';
|
||||
|
||||
/**
|
||||
* Custom css editor communications
|
||||
*/
|
||||
export default class {
|
||||
|
||||
/**
|
||||
* Init css editor
|
||||
*/
|
||||
static init() {
|
||||
ClientIPC.on('bd-get-scss', () => this.sendToEditor('set-scss', { scss: this.scss }));
|
||||
ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss));
|
||||
|
@ -25,10 +31,18 @@ export default class {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show css editor, flashes if already visible
|
||||
*/
|
||||
static async show() {
|
||||
await ClientIPC.send('openCssEditor', this.editor_bounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update css in client
|
||||
* @param {String} scss scss to compile
|
||||
* @param {bool} sendSource send to css editor instance
|
||||
*/
|
||||
static updateScss(scss, sendSource) {
|
||||
if (sendSource)
|
||||
this.sendToEditor('set-scss', { scss });
|
||||
|
@ -46,31 +60,57 @@ export default class {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save css to file
|
||||
*/
|
||||
static async save() {
|
||||
Settings.saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current editor bounds
|
||||
* @param {Rectangle} bounds editor bounds
|
||||
*/
|
||||
static saveEditorBounds(bounds) {
|
||||
this.editor_bounds = bounds;
|
||||
Settings.saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send scss to core for compilation
|
||||
* @param {String} scss scss string
|
||||
*/
|
||||
static async compile(scss) {
|
||||
return await ClientIPC.send('bd-compileSass', { data: scss });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send css to open editor
|
||||
* @param {any} channel
|
||||
* @param {any} data
|
||||
*/
|
||||
static async sendToEditor(channel, data) {
|
||||
return await ClientIPC.send('sendToCssEditor', { channel, data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Current uncompiled scss
|
||||
*/
|
||||
static get scss() {
|
||||
return this._scss || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current scss
|
||||
*/
|
||||
static set scss(scss) {
|
||||
this.updateScss(scss, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject compiled css to head
|
||||
* {DOM}
|
||||
*/
|
||||
static set css(css) {
|
||||
DOM.injectStyle(css, 'bd-customcss');
|
||||
}
|
||||
|
|
|
@ -17,6 +17,10 @@ import {
|
|||
} from '../structs/socketstructs';
|
||||
|
||||
|
||||
/**
|
||||
* Discord socket event hook
|
||||
* @extends {EventListener}
|
||||
*/
|
||||
export default class extends EventListener {
|
||||
|
||||
bindings() {
|
||||
|
@ -29,14 +33,16 @@ export default class extends EventListener {
|
|||
];
|
||||
}
|
||||
|
||||
hook() {
|
||||
hook() {}
|
||||
|
||||
}
|
||||
|
||||
get eventsModule() {
|
||||
|
||||
}
|
||||
get eventsModule() {}
|
||||
|
||||
/**
|
||||
* Discord emit overload
|
||||
* @param {any} e
|
||||
* @param {any} action
|
||||
* @param {any} data
|
||||
*/
|
||||
emit(e, action, data) {
|
||||
switch (e) {
|
||||
case 'dispatch':
|
||||
|
@ -44,6 +50,11 @@ export default class extends EventListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit callback
|
||||
* @param {any} e Event Action
|
||||
* @param {any} d Event Args
|
||||
*/
|
||||
dispatch(e, d) {
|
||||
|
||||
Events.emit('raw-event', { type: e, data: d });
|
||||
|
@ -77,6 +88,9 @@ export default class extends EventListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All known socket actions
|
||||
*/
|
||||
get actions() {
|
||||
return {
|
||||
READY: 'READY', // Socket ready
|
||||
|
|
|
@ -8,6 +8,27 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
class ExtModuleEvents {
|
||||
constructor(extmodule) {
|
||||
this.extmodule = extmodule;
|
||||
this.emitter = new EventEmitter();
|
||||
}
|
||||
|
||||
on(eventname, callback) {
|
||||
this.emitter.on(eventname, callback);
|
||||
}
|
||||
|
||||
off(eventname, callback) {
|
||||
this.emitter.removeListener(eventname, callback);
|
||||
}
|
||||
|
||||
emit(...args) {
|
||||
this.emitter.emit(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default class ExtModule {
|
||||
|
||||
constructor(pluginInternals) {
|
||||
|
@ -31,5 +52,7 @@ export default class ExtModule {
|
|||
get pluginPath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return true }
|
||||
get pluginConfig() { return this.userConfig.config || [] }
|
||||
get config() { return this.userConfig.config || [] }
|
||||
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new ExtModuleEvents(this)) }
|
||||
|
||||
}
|
||||
|
|
|
@ -38,11 +38,10 @@ export default class extends ContentManager {
|
|||
static get refreshModules() { return this.refreshContent }
|
||||
|
||||
static get loadContent() { return this.loadModule }
|
||||
static async loadModule(paths, configs, info, main, type) {
|
||||
static async loadModule(paths, configs, info, main) {
|
||||
return new ExtModule({ configs, info, main, paths: { contentPath: paths.contentPath, dirName: paths.dirName, mainPath: paths.mainPath } });
|
||||
}
|
||||
|
||||
|
||||
static get findModule() { return this.findContent }
|
||||
static get getModuleIndex() { return this.getContentIndex }
|
||||
static get getModuleByName() { return this.getContentByName }
|
||||
|
|
|
@ -10,6 +10,28 @@
|
|||
|
||||
import { Utils, FileUtils } from 'common';
|
||||
import { Modals } from 'ui';
|
||||
import { EventEmitter } from 'events';
|
||||
import ContentConfig from './contentconfig';
|
||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
||||
|
||||
class PluginEvents {
|
||||
constructor(plugin) {
|
||||
this.plugin = plugin;
|
||||
this.emitter = new EventEmitter();
|
||||
}
|
||||
|
||||
on(eventname, callback) {
|
||||
this.emitter.on(eventname, callback);
|
||||
}
|
||||
|
||||
off(eventname, callback) {
|
||||
this.emitter.removeListener(eventname, callback);
|
||||
}
|
||||
|
||||
emit(...args) {
|
||||
this.emitter.emit(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Plugin {
|
||||
|
||||
|
@ -29,7 +51,7 @@ export default class Plugin {
|
|||
get main() { return this.__pluginInternals.main }
|
||||
get defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
get id() { return this.info.id || this.info.name.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
|
||||
get id() { return this.info.id || this.name.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
|
||||
get name() { return this.info.name }
|
||||
get authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
|
@ -39,6 +61,7 @@ export default class Plugin {
|
|||
get config() { return this.userConfig.config || [] }
|
||||
get pluginConfig() { return this.config }
|
||||
get exports() { return this._exports ? this._exports : (this._exports = this.getExports()) }
|
||||
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new PluginEvents(this)) }
|
||||
|
||||
getSetting(setting_id, category_id) {
|
||||
for (let category of this.pluginConfig) {
|
||||
|
@ -55,38 +78,43 @@ export default class Plugin {
|
|||
}
|
||||
|
||||
async saveSettings(newSettings) {
|
||||
for (let category of newSettings) {
|
||||
const oldCategory = this.pluginConfig.find(c => c.category === category.category);
|
||||
for (let setting of category.settings) {
|
||||
const oldSetting = oldCategory.settings.find(s => s.id === setting.id);
|
||||
if (Utils.compare(oldSetting.value, setting.value)) continue;
|
||||
oldSetting.value = setting.value;
|
||||
if (this.settingChanged) this.settingChanged(category.category, setting.id, setting.value);
|
||||
const updatedSettings = [];
|
||||
|
||||
for (let newCategory of newSettings) {
|
||||
const category = this.pluginConfig.find(c => c.category === newCategory.category);
|
||||
for (let newSetting of newCategory.settings) {
|
||||
const setting = category.settings.find(s => s.id === newSetting.id);
|
||||
if (Utils.compare(setting.value, newSetting.value)) continue;
|
||||
|
||||
const old_value = setting.value;
|
||||
setting.value = newSetting.value;
|
||||
updatedSettings.push({ category_id: category.category, setting_id: setting.id, value: setting.value, old_value });
|
||||
this.settingUpdated(category.category, setting.id, setting.value, old_value);
|
||||
}
|
||||
}
|
||||
|
||||
this.saveConfiguration();
|
||||
return this.settingsUpdated(updatedSettings);
|
||||
}
|
||||
|
||||
if (this.settingsChanged) this.settingsChanged(this.pluginConfig);
|
||||
settingUpdated(category_id, setting_id, value, old_value) {
|
||||
const event = new SettingUpdatedEvent({ category_id, setting_id, value, old_value });
|
||||
this.events.emit('setting-updated', event);
|
||||
this.events.emit(`setting-updated_{$category_id}_${setting_id}`, event);
|
||||
}
|
||||
|
||||
return this.pluginConfig;
|
||||
settingsUpdated(updatedSettings) {
|
||||
const event = new SettingsUpdatedEvent({ settings: updatedSettings.map(s => new SettingUpdatedEvent(s)) });
|
||||
this.events.emit('settings-updated', event);
|
||||
}
|
||||
|
||||
async saveConfiguration() {
|
||||
window.testConfig = new ContentConfig(this.pluginConfig);
|
||||
try {
|
||||
const config = new ContentConfig(this.pluginConfig).strip();
|
||||
await FileUtils.writeFile(`${this.pluginPath}/user.config.json`, JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
config: this.config.map(category => {
|
||||
return {
|
||||
category: category.category,
|
||||
settings: category.settings.map(setting => {
|
||||
return {
|
||||
id: setting.id,
|
||||
value: setting.value
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
config
|
||||
}));
|
||||
} catch (err) {
|
||||
throw err;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
*/
|
||||
|
||||
import { ClientLogger as Logger } from 'common';
|
||||
import Settings from './settings';
|
||||
import ExtModuleManager from './extmodulemanager';
|
||||
import PluginManager from './pluginmanager';
|
||||
import ThemeManager from './thememanager';
|
||||
|
@ -67,6 +68,15 @@ export default class PluginApi {
|
|||
}
|
||||
}
|
||||
|
||||
getSetting(set, category, setting) {
|
||||
return Settings.get(set, category, setting);
|
||||
}
|
||||
get Settings() {
|
||||
return {
|
||||
get: this.getSetting.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
async getPlugin(plugin_id) {
|
||||
// This should require extra permissions
|
||||
return await PluginManager.waitForPlugin(plugin_id);
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
*/
|
||||
|
||||
import ContentManager from './contentmanager';
|
||||
import ExtModuleManager from './extmodulemanager';
|
||||
import Plugin from './plugin';
|
||||
import PluginApi from './pluginapi';
|
||||
import Vendor from './vendor';
|
||||
import { ClientLogger as Logger } from 'common';
|
||||
import { Events } from 'modules';
|
||||
|
||||
|
||||
export default class extends ContentManager {
|
||||
|
||||
static get localPlugins() {
|
||||
|
@ -34,8 +34,8 @@ export default class extends ContentManager {
|
|||
return 'plugins';
|
||||
}
|
||||
|
||||
static async loadAllPlugins(supressErrors) {
|
||||
const loadAll = await this.loadAllContent(supressErrors);
|
||||
static async loadAllPlugins(suppressErrors) {
|
||||
const loadAll = await this.loadAllContent(suppressErrors);
|
||||
this.localPlugins.forEach(plugin => {
|
||||
if (plugin.enabled) plugin.start();
|
||||
});
|
||||
|
@ -45,8 +45,22 @@ export default class extends ContentManager {
|
|||
static get refreshPlugins() { return this.refreshContent }
|
||||
|
||||
static get loadContent() { return this.loadPlugin }
|
||||
static async loadPlugin(paths, configs, info, main, type) {
|
||||
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor);
|
||||
static async loadPlugin(paths, configs, info, main, dependencies) {
|
||||
|
||||
const deps = [];
|
||||
if (dependencies) {
|
||||
for (const [key, value] of Object.entries(dependencies)) {
|
||||
const extModule = ExtModuleManager.findModule(key);
|
||||
if (!extModule) {
|
||||
throw {
|
||||
'message': `Dependency: ${key}:${value} is not loaded`
|
||||
};
|
||||
}
|
||||
deps[key] = extModule.__require;
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor, deps);
|
||||
const instance = new plugin({ configs, info, main, paths: { contentPath: paths.contentPath, dirName: paths.dirName, mainPath: paths.mainPath } });
|
||||
return instance;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
import defaultSettings from '../data/user.settings.default';
|
||||
import Globals from './globals';
|
||||
import CssEditor from './csseditor';
|
||||
import Events from './events';
|
||||
import { FileUtils, ClientLogger as Logger } from 'common';
|
||||
import { SettingUpdatedEvent } from 'structs';
|
||||
import path from 'path';
|
||||
|
||||
export default class {
|
||||
|
@ -43,7 +45,7 @@ export default class {
|
|||
}
|
||||
|
||||
CssEditor.updateScss(scss, true);
|
||||
CssEditor.editor_bounds = css_editor_bounds;
|
||||
CssEditor.editor_bounds = css_editor_bounds || {};
|
||||
} catch (err) {
|
||||
// There was an error loading settings
|
||||
// This probably means that the user doesn't have any settings yet
|
||||
|
@ -122,8 +124,9 @@ export default class {
|
|||
if (setting.id !== setting_id) continue;
|
||||
if (setting.value === value) return true;
|
||||
|
||||
let old_value = setting.value;
|
||||
setting.value = value;
|
||||
this.settingUpdated(set_id, category_id, setting_id, value);
|
||||
this.settingUpdated(set_id, category_id, setting_id, value, old_value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -132,8 +135,12 @@ export default class {
|
|||
return false;
|
||||
}
|
||||
|
||||
static settingUpdated(set_id, category_id, setting_id, value) {
|
||||
Logger.log('Settings', `${set_id}/${category_id}/${setting_id} was set to ${value}`);
|
||||
static settingUpdated(set_id, category_id, setting_id, value, old_value) {
|
||||
Logger.log('Settings', `${set_id}/${category_id}/${setting_id} was changed from ${old_value} to ${value}`);
|
||||
|
||||
const event = new SettingUpdatedEvent({ set_id, category_id, setting_id, value, old_value });
|
||||
Events.emit('setting-updated', event);
|
||||
Events.emit(`setting-updated-${set_id}_{$category_id}_${setting_id}`, event);
|
||||
}
|
||||
|
||||
static get getSettings() {
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* BetterDiscord Theme Module
|
||||
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
|
||||
* All rights reserved.
|
||||
* https://betterdiscord.net
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import ThemeManager from './thememanager';
|
||||
import { EventEmitter } from 'events';
|
||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
||||
import { DOM, Modals } from 'ui';
|
||||
import { FileUtils, ClientIPC } from 'common';
|
||||
import ContentConfig from './contentconfig';
|
||||
|
||||
class ThemeEvents {
|
||||
constructor(theme) {
|
||||
this.theme = theme;
|
||||
this.emitter = new EventEmitter();
|
||||
}
|
||||
|
||||
on(eventname, callback) {
|
||||
this.emitter.on(eventname, callback);
|
||||
}
|
||||
|
||||
off(eventname, callback) {
|
||||
this.emitter.removeListener(eventname, callback);
|
||||
}
|
||||
|
||||
emit(...args) {
|
||||
this.emitter.emit(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Theme {
|
||||
|
||||
constructor(themeInternals) {
|
||||
this.__themeInternals = themeInternals;
|
||||
this.hasSettings = this.themeConfig && this.themeConfig.length > 0;
|
||||
this.saveSettings = this.saveSettings.bind(this);
|
||||
this.enable = this.enable.bind(this);
|
||||
this.disable = this.disable.bind(this);
|
||||
}
|
||||
|
||||
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 defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
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 authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
get themePath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return this.userConfig.enabled }
|
||||
get config() { return this.userConfig.config || [] }
|
||||
get themeConfig() { return this.config }
|
||||
get css() { return this.userConfig.css }
|
||||
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new ThemeEvents(this)) }
|
||||
|
||||
showSettingsModal() {
|
||||
return Modals.themeSettings(this);
|
||||
}
|
||||
|
||||
async saveSettings(newSettings) {
|
||||
const updatedSettings = [];
|
||||
|
||||
for (let newCategory of newSettings) {
|
||||
const category = this.themeConfig.find(c => c.category === newCategory.category);
|
||||
for (let newSetting of newCategory.settings) {
|
||||
const setting = category.settings.find(s => s.id === newSetting.id);
|
||||
if (setting.value === newSetting.value) continue;
|
||||
|
||||
const old_value = setting.value;
|
||||
setting.value = newSetting.value;
|
||||
updatedSettings.push({ category_id: category.category, setting_id: setting.id, value: setting.value, old_value });
|
||||
this.settingUpdated(category.category, setting.id, setting.value, old_value);
|
||||
}
|
||||
}
|
||||
|
||||
// As the theme's configuration has changed it needs recompiling
|
||||
// When the compiled CSS has been save it will also save the configuration
|
||||
await this.recompile();
|
||||
|
||||
return this.settingsUpdated(updatedSettings);
|
||||
}
|
||||
|
||||
settingUpdated(category_id, setting_id, value, old_value) {
|
||||
const event = new SettingUpdatedEvent({ category_id, setting_id, value, old_value });
|
||||
this.events.emit('setting-updated', event);
|
||||
this.events.emit(`setting-updated_{$category_id}_${setting_id}`, event);
|
||||
}
|
||||
|
||||
settingsUpdated(updatedSettings) {
|
||||
const event = new SettingsUpdatedEvent({ settings: updatedSettings.map(s => new SettingUpdatedEvent(s)) });
|
||||
this.events.emit('settings-updated', event);
|
||||
}
|
||||
|
||||
async saveConfiguration() {
|
||||
try {
|
||||
const config = new ContentConfig(this.themeConfig).strip();
|
||||
await FileUtils.writeFile(`${this.themePath}/user.config.json`, JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
config,
|
||||
css: this.css
|
||||
}));
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this.enabled) {
|
||||
this.userConfig.enabled = true;
|
||||
this.saveConfiguration();
|
||||
}
|
||||
DOM.injectTheme(this.css, this.id);
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.userConfig.enabled = false;
|
||||
this.saveConfiguration();
|
||||
DOM.deleteTheme(this.id);
|
||||
}
|
||||
|
||||
async compile() {
|
||||
console.log('Compiling CSS');
|
||||
|
||||
let css = '';
|
||||
if (this.info.type === 'sass') {
|
||||
css = await ClientIPC.send('bd-compileSass', {
|
||||
data: ThemeManager.getConfigAsSCSS(this.themeConfig),
|
||||
path: this.paths.mainPath.replace(/\\/g, '/')
|
||||
});
|
||||
console.log(css);
|
||||
} else {
|
||||
css = await FileUtils.readFile(this.paths.mainPath);
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
async recompile() {
|
||||
const css = await this.compile();
|
||||
this.userConfig.css = css;
|
||||
|
||||
await this.saveConfiguration();
|
||||
|
||||
if (this.enabled) {
|
||||
DOM.deleteTheme(this.id);
|
||||
DOM.injectTheme(this.css, this.id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -9,126 +9,7 @@
|
|||
*/
|
||||
|
||||
import ContentManager from './contentmanager';
|
||||
import { DOM, Modals } from 'ui';
|
||||
import { FileUtils, ClientIPC } from 'common';
|
||||
|
||||
class Theme {
|
||||
|
||||
constructor(themeInternals) {
|
||||
this.__themeInternals = themeInternals;
|
||||
this.hasSettings = this.themeConfig && this.themeConfig.length > 0;
|
||||
this.saveSettings = this.saveSettings.bind(this);
|
||||
this.enable = this.enable.bind(this);
|
||||
this.disable = this.disable.bind(this);
|
||||
}
|
||||
|
||||
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 defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
get name() { return this.info.name }
|
||||
get authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
get themePath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return this.userConfig.enabled }
|
||||
get themeConfig() { return this.userConfig.config }
|
||||
get css() { return this.userConfig.css }
|
||||
get id() { return this.name.toLowerCase().replace(/\s+/g, '-') }
|
||||
|
||||
showSettingsModal() {
|
||||
return Modals.themeSettings(this);
|
||||
}
|
||||
|
||||
async saveSettings(newSettings) {
|
||||
for (let category of newSettings) {
|
||||
const oldCategory = this.themeConfig.find(c => c.category === category.category);
|
||||
for (let setting of category.settings) {
|
||||
const oldSetting = oldCategory.settings.find(s => s.id === setting.id);
|
||||
if (oldSetting.value === setting.value) continue;
|
||||
oldSetting.value = setting.value;
|
||||
if (this.settingChanged) this.settingChanged(category.category, setting.id, setting.value);
|
||||
}
|
||||
}
|
||||
|
||||
// As the theme's configuration has changed it needs recompiling
|
||||
// When the compiled CSS has been save it will also save the configuration
|
||||
await this.recompile();
|
||||
|
||||
if (this.settingsChanged) this.settingsChanged(this.themeConfig);
|
||||
|
||||
return this.pluginConfig;
|
||||
}
|
||||
|
||||
async saveConfiguration() {
|
||||
try {
|
||||
await FileUtils.writeFile(`${this.themePath}/user.config.json`, JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
config: this.themeConfig.map(category => {
|
||||
return {
|
||||
category: category.category,
|
||||
settings: category.settings.map(setting => {
|
||||
return {
|
||||
id: setting.id,
|
||||
value: setting.value
|
||||
};
|
||||
})
|
||||
};
|
||||
}),
|
||||
css: this.css
|
||||
}));
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this.enabled) {
|
||||
this.userConfig.enabled = true;
|
||||
this.saveConfiguration();
|
||||
}
|
||||
DOM.injectTheme(this.css, this.id);
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.userConfig.enabled = false;
|
||||
this.saveConfiguration();
|
||||
DOM.deleteTheme(this.id);
|
||||
}
|
||||
|
||||
async compile() {
|
||||
console.log('Compiling CSS');
|
||||
|
||||
let css = '';
|
||||
if (this.info.type === 'sass') {
|
||||
css = await ClientIPC.send('bd-compileSass', {
|
||||
data: ThemeManager.getConfigAsSCSS(this.themeConfig),
|
||||
path: this.paths.mainPath.replace(/\\/g, '/')
|
||||
});
|
||||
console.log(css);
|
||||
} else {
|
||||
css = await FileUtils.readFile(this.paths.mainPath);
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
async recompile() {
|
||||
const css = await this.compile();
|
||||
this.userConfig.css = css;
|
||||
|
||||
await this.saveConfiguration();
|
||||
|
||||
if (this.enabled) {
|
||||
DOM.deleteTheme(this.id);
|
||||
DOM.injectTheme(this.css, this.id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
import Theme from './theme';
|
||||
|
||||
export default class ThemeManager extends ContentManager {
|
||||
|
||||
|
@ -213,7 +94,6 @@ export default class ThemeManager extends ContentManager {
|
|||
if (typeof value === 'string') {
|
||||
return `$${name}: ${setting.scss_raw ? value : `'${setting.value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}'`};`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export class Error {
|
||||
import Event from './event';
|
||||
|
||||
export default class ErrorEvent extends Event {
|
||||
|
||||
constructor(args) {
|
||||
this.args = args;
|
||||
super(args);
|
||||
this.showStack = false; // For error modal
|
||||
}
|
||||
|
||||
|
@ -31,8 +33,8 @@ export class Error {
|
|||
return this.err.stack;
|
||||
}
|
||||
|
||||
get _type() {
|
||||
return 'err';
|
||||
get __eventType() {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* BetterDiscord Base Event Struct
|
||||
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
|
||||
* All rights reserved.
|
||||
* https://betterdiscord.net
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export default class Event {
|
||||
|
||||
constructor(args) {
|
||||
this.__eventInfo = {
|
||||
args: arguments,
|
||||
type: this.__eventType
|
||||
};
|
||||
}
|
||||
|
||||
get event() {
|
||||
return this.__eventInfo;
|
||||
}
|
||||
|
||||
get args() {
|
||||
return this.event.args[0];
|
||||
}
|
||||
|
||||
get __eventType() { return null; }
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { default as SettingUpdatedEvent } from './settingupdated';
|
||||
export { default as SettingsUpdatedEvent } from './settingsupdated';
|
||||
export { default as ErrorEvent } from './error';
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* BetterDiscord Settings Updated Event Struct
|
||||
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
|
||||
* All rights reserved.
|
||||
* https://betterdiscord.net
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Event from './event';
|
||||
|
||||
export default class SettingsUpdatedEvent extends Event {
|
||||
|
||||
get updatedSettings() {
|
||||
return this.args.updatedSettings;
|
||||
}
|
||||
|
||||
get __eventType() {
|
||||
return 'settings-updated';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* BetterDiscord Setting Updated Event Struct
|
||||
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
|
||||
* All rights reserved.
|
||||
* https://betterdiscord.net
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Event from './event';
|
||||
|
||||
export default class SettingUpdatedEvent extends Event {
|
||||
|
||||
get set() {
|
||||
return this.args.set_id;
|
||||
}
|
||||
|
||||
get category() {
|
||||
return this.args.category_id;
|
||||
}
|
||||
|
||||
get setting() {
|
||||
return this.args.setting_id;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.args.value;
|
||||
}
|
||||
|
||||
get old_value() {
|
||||
return this.args.old_value;
|
||||
}
|
||||
|
||||
get __eventType() {
|
||||
return 'setting-updated';
|
||||
}
|
||||
|
||||
}
|
|
@ -1 +1 @@
|
|||
export * from './error';
|
||||
export * from './events/index';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<MiError v-if="modal.event.type === 'err'" slot="icon" size="20"/>
|
||||
<div slot="body">
|
||||
<div v-for="(content, index) in modal.event.content">
|
||||
<div v-if="content._type === 'err'" class="bd-modal-error" :class="{'bd-open': content.showStack}">
|
||||
<div class="bd-modal-error" :class="{'bd-open': content.showStack}">
|
||||
<div class="bd-modal-error-title bd-flex">
|
||||
<span class="bd-modal-title-text bd-flex-grow">{{content.message}}</span>
|
||||
<span class="bd-modal-titlelink" v-if="content.showStack" @click="() => { content.showStack = false; $forceUpdate(); }">Hide Stacktrace</span>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"info": {
|
||||
"id": "depend-error",
|
||||
"name": "Depend Error",
|
||||
"authors": [ "Jiiks" ],
|
||||
"version": 1.0,
|
||||
"description": "Depend Error Plugin Description",
|
||||
"icon": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg=="
|
||||
},
|
||||
"main": "index.js",
|
||||
"type": "plugin",
|
||||
"dependencies": {
|
||||
"Nonexistent Module": "1.0"
|
||||
},
|
||||
"defaultConfig": []
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
module.exports = (Plugin, Api, Vendor) => {
|
||||
|
||||
return class extends Plugin {
|
||||
onStart() {
|
||||
return true;
|
||||
}
|
||||
|
||||
onStop() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,9 @@
|
|||
},
|
||||
"main": "index.js",
|
||||
"type": "plugin",
|
||||
"dependencies": {
|
||||
"Example Module": "1.0"
|
||||
},
|
||||
"defaultConfig": [
|
||||
{
|
||||
"category_default_comment": "default category has no header and is always displayed first",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module.exports = (Plugin, Api, Vendor) => {
|
||||
module.exports = (Plugin, Api, Vendor, Dependencies) => {
|
||||
|
||||
const { $, moment, _ } = Vendor;
|
||||
const { Events, Logger } = Api;
|
||||
|
@ -7,9 +7,21 @@ module.exports = (Plugin, Api, Vendor) => {
|
|||
onStart() {
|
||||
Events.subscribe('TEST_EVENT', this.eventTest);
|
||||
Logger.log('onStart');
|
||||
Logger.log(`Setting "default-0" value: ${this.getSetting('default-0')}`);
|
||||
|
||||
const exampleModule = new (Api.import('Example Module'));
|
||||
Logger.log(`Plugin setting "default-0" value: ${this.getSetting('default-0')}`);
|
||||
this.events.on('setting-updated', event => {
|
||||
console.log('Received plugin setting update:', event);
|
||||
});
|
||||
this.events.on('settings-updated', event => {
|
||||
console.log('Received plugin settings update:', event);
|
||||
});
|
||||
|
||||
Logger.log(`Internal setting "core/default/test-setting" value: ${Api.Settings.get('core', 'default', 'test-setting')}`);
|
||||
Events.subscribe('setting-updated', event => {
|
||||
console.log('Received internal setting update:', event);
|
||||
});
|
||||
|
||||
const exampleModule = new Dependencies['Example Module'];
|
||||
Logger.log(`2+4=${exampleModule.add(2, 4)}`);
|
||||
return true;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue