Added base content class

This commit is contained in:
Samuel Elliott 2018-03-06 00:24:14 +00:00
parent e5078f4160
commit 3ab4fd8a9a
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
12 changed files with 252 additions and 208 deletions

View File

@ -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, 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);
}
}

View File

@ -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

View File

@ -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()) }
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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) {

View File

@ -12,7 +12,7 @@
<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="showSettings"><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>

View File

@ -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="e => deletePlugin(plugin, e.shiftKey)" :showSettings="e => showSettings(plugin, e.shiftKey)" />
</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
});
}
}
}

View File

@ -12,7 +12,7 @@
<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="'Settings (shift + click to open settings without cloning the set)'" v-if="theme.hasSettings" :onClick="e => showSettings(theme, e.shiftKey)"><MiSettings size="18" /></Button>
<Button v-tooltip="'Recompile (shift + click to reload)'" :onClick="reloadTheme"><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>

View File

@ -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="e => reloadTheme(theme, e.shiftKey)" :showSettings="e => showSettings(theme, e.shiftKey)" :deleteTheme="e => deleteTheme(theme, e.shiftKey)" />
</div>
<div v-if="!local" class="bd-online-ph">
<h3>Coming Soon</h3>
@ -94,8 +94,10 @@
console.error(err);
}
},
showSettings(theme) {
return Modals.contentSettings(theme);
showSettings(theme, dont_clone) {
return Modals.contentSettings(theme, null, {
dont_clone
});
}
}
}

View File

@ -57,18 +57,6 @@
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);
},
showModal(item, index) {
Modals.settings(item, this.setting.headertext ? this.setting.headertext.replace(/%n/, index + 1) : this.setting.text + ` #${index + 1}`);
},

View File

@ -240,8 +240,8 @@ export default class Modals {
* @param {String} headertext A string that will be displayed in the modal header
* @return {Modal}
*/
static contentSettings(content, headertext) {
return this.settings(content.settings, headertext ? headertext : content.name + ' Settings');
static contentSettings(content, headertext, options) {
return this.settings(content.settings, headertext ? headertext : content.name + ' Settings', options);
}
/**