diff --git a/client/src/modules/contentmanager.js b/client/src/modules/contentmanager.js index f617389d..5ffbd30f 100644 --- a/client/src/modules/contentmanager.js +++ b/client/src/modules/contentmanager.js @@ -85,8 +85,9 @@ export default class { /** * Refresh locally stored content + * @param {bool} suppressErrors Suppress any errors that occur during loading of content */ - static async refreshContent() { + static async refreshContent(suppressErrors = false) { if (!this.localContent.length) return this.loadAllContent(); try { @@ -101,19 +102,45 @@ export default class { // Load if not await this.preloadContent(dir); } catch (err) { - //We don't want every plugin/theme to fail loading when one does + // We don't want every plugin/theme to fail loading when one does + this.errors.push(new ErrorEvent({ + module: this.moduleName, + message: `Failed to load ${dir}`, + err + })); + Logger.err(this.moduleName, err); } } for (let content of this.localContent) { if (directories.includes(content.dirName)) continue; - //Plugin/theme was deleted manually, stop it and remove any reference - this.unloadContent(content); + + try { + // Plugin/theme was deleted manually, stop it and remove any reference + await this.unloadContent(content); + } catch (err) { + this.errors.push(new ErrorEvent({ + module: this.moduleName, + message: `Failed to unload ${content.dirName}`, + err + })); + + Logger.err(this.moduleName, 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 this.localContent; - } catch (err) { throw err; } @@ -151,19 +178,20 @@ export default class { try { const readUserConfig = await this.readUserConfig(contentPath); userConfig.enabled = readUserConfig.enabled || false; - userConfig.config = readConfig.defaultConfig.map(config => { - const userSet = readUserConfig.config.find(c => c.category === config.category); + userConfig.config = readConfig.defaultConfig.map(category => { + let newCategory = readUserConfig.config.find(c => c.category === category.category); // return userSet || config; - if (!userSet) return config; + if (!newCategory) newCategory = {settings: []}; - config.settings = config.settings.map(setting => { - const userSetting = userSet.settings.find(s => s.id === setting.id); - if (!userSetting) return setting; + category.settings = category.settings.map(setting => { + if (setting.type === 'array' || setting.type === 'custom') setting.path = contentPath; + const newSetting = newCategory.settings.find(s => s.id === setting.id); + if (!newSetting) return setting; - setting.value = userSetting.value; + setting.value = newSetting.value; return setting; }); - return config; + return category; }); userConfig.css = readUserConfig.css || null; // userConfig.config = readUserConfig.config; @@ -175,15 +203,18 @@ export default class { defaultConfig: readConfig.defaultConfig, schemes: readConfig.configSchemes, userConfig - } + }; const paths = { contentPath, dirName, mainPath - } + }; const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies); + if (!reload && this.getContentById(content.id)) + throw {message: `A ${this.contentType} with the ID ${content.id} already exists.`}; + if (reload) this.localContent[index] = content; else this.localContent.push(content); return content; @@ -193,6 +224,43 @@ export default class { } } + /** + * Unload content + * @param {any} content Content to unload + * @param {bool} reload Whether to reload the content after + */ + static async unloadContent(content, reload) { + content = this.findContent(content); + 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); + 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); + return newcontent; + } else this.localContent.splice(index, 1); + } catch (err) { + Logger.err(this.moduleName, err); + throw err; + } + } + + /** + * Reload content + * @param {any} content Content to reload + */ + static async reloadContent(content) { + return this.unloadContent(content, true); + } + /** * Read content config file * @param {any} configPath Config file path @@ -211,19 +279,26 @@ export default class { return FileUtils.readJsonFromFile(configPath); } + /** + * Checks if the passed object is an instance of this content type. + * @param {any} content Object to check + */ + static isThisContent(content) { + return false; + } + /** * Wildcard content finder * @param {any} wild Content name | id | path | dirname + * @param {bool} nonunique Allow searching attributes that may not be unique */ - //TODO make this nicer - static findContent(wild) { - let content = this.getContentByName(wild); - if (content) return content; - content = this.getContentById(wild); - if (content) return content; - content = this.getContentByPath(wild); - if (content) return content; - return this.getContentByDirName(wild); + static findContent(wild, nonunique) { + if (this.isThisContent(wild)) return wild; + let content; + content = this.getContentById(wild); if (content) return content; + content = this.getContentByDirName(wild); if (content) return content; + content = this.getContentByPath(wild); if (content) return content; + content = this.getContentByName(wild); if (content && nonunique) return content; } static getContentIndex(content) { return this.localContent.findIndex(c => c === content) } diff --git a/client/src/modules/extmodule.js b/client/src/modules/extmodule.js index 3ebc9ee4..c7c8937a 100644 --- a/client/src/modules/extmodule.js +++ b/client/src/modules/extmodule.js @@ -45,11 +45,12 @@ export default class ExtModule { 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.info.name.toLowerCase().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 } - get pluginPath() { return this.paths.contentPath } + 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 || [] } diff --git a/client/src/modules/extmodulemanager.js b/client/src/modules/extmodulemanager.js index 1edbea1c..89bf2efd 100644 --- a/client/src/modules/extmodulemanager.js +++ b/client/src/modules/extmodulemanager.js @@ -39,7 +39,19 @@ export default class extends ContentManager { static get loadContent() { return this.loadModule } static async loadModule(paths, configs, info, main) { - return new ExtModule({ configs, info, main, paths: { contentPath: paths.contentPath, dirName: paths.dirName, mainPath: paths.mainPath } }); + return new ExtModule({ + configs, info, main, + paths: { + contentPath: paths.contentPath, + dirName: paths.dirName, + mainPath: paths.mainPath + } + }); + } + + static get isExtModule() { return this.isThisContent } + static isThisContent(module) { + return module instanceof ExtModule; } static get findModule() { return this.findContent } diff --git a/client/src/modules/plugin.js b/client/src/modules/plugin.js index 0a4aa768..1ae51846 100644 --- a/client/src/modules/plugin.js +++ b/client/src/modules/plugin.js @@ -11,6 +11,7 @@ import { Utils, FileUtils } from 'common'; import { Modals } from 'ui'; import { EventEmitter } from 'events'; +import PluginManager from './pluginmanager'; import ContentConfig from './contentconfig'; import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs'; @@ -52,10 +53,11 @@ export default class Plugin { 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.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') } + get id() { return this.info.id || this.name.toLowerCase().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 } + get contentPath() { return this.paths.contentPath } get pluginPath() { return this.paths.contentPath } get dirName() { return this.paths.dirName } get enabled() { return this.userConfig.enabled } @@ -122,25 +124,32 @@ export default class Plugin { } } - start() { + start(save = true) { if (this.onstart && !this.onstart()) return false; if (this.onStart && !this.onStart()) return false; if (!this.enabled) { this.userConfig.enabled = true; - this.saveConfiguration(); + if (save) this.saveConfiguration(); } return true; } - stop() { + stop(save = true) { if (this.onstop && !this.onstop()) return false; if (this.onStop && !this.onStop()) return false; - this.userConfig.enabled = false; - this.saveConfiguration(); + if (this.enabled) { + this.userConfig.enabled = false; + if (save) this.saveConfiguration(); + } + return true; } + unload() { + PluginManager.unloadPlugin(this); + } + } diff --git a/client/src/modules/pluginmanager.js b/client/src/modules/pluginmanager.js index 105e7125..a43755d6 100644 --- a/client/src/modules/pluginmanager.js +++ b/client/src/modules/pluginmanager.js @@ -35,10 +35,12 @@ export default class extends ContentManager { } static async loadAllPlugins(suppressErrors) { + this.loaded = false; const loadAll = await this.loadAllContent(suppressErrors); - this.localPlugins.forEach(plugin => { + this.loaded = true; + for (let plugin of this.localPlugins) { if (plugin.enabled) plugin.start(); - }); + } return loadAll; } @@ -46,7 +48,6 @@ export default class extends ContentManager { static get loadContent() { return this.loadPlugin } static async loadPlugin(paths, configs, info, main, dependencies) { - const deps = []; if (dependencies) { for (const [key, value] of Object.entries(dependencies)) { @@ -61,36 +62,21 @@ export default class extends ContentManager { } 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 } }); + const instance = new plugin({ + configs, info, main, + paths: { + contentPath: paths.contentPath, + dirName: paths.dirName, + mainPath: paths.mainPath + } + }); + + if (instance.enabled && this.loaded) instance.start(); return instance; } - static get unloadContent() { return this.unloadPlugin } - static async unloadPlugin(plugin) { - try { - if (plugin.enabled) plugin.stop(); - const { pluginPath } = plugin; - const index = this.getPluginIndex(plugin); - - delete window.require.cache[window.require.resolve(pluginPath)]; - this.localPlugins.splice(index, 1); - } catch (err) { - //This might fail but we don't have any other option at this point - Logger.err('PluginManager', err); - } - } - - static async reloadPlugin(plugin) { - const _plugin = plugin instanceof Plugin ? plugin : this.findPlugin(plugin); - if (!_plugin) throw { 'message': 'Attempted to reload a plugin that is not loaded?' }; - if (!_plugin.stop()) throw { 'message': 'Plugin failed to stop!' }; - const index = this.getPluginIndex(_plugin); - const { pluginPath, dirName } = _plugin; - - delete window.require.cache[window.require.resolve(pluginPath)]; - - return this.preloadContent(dirName, true, index); - } + static get unloadPlugin() { return this.unloadContent } + static get reloadPlugin() { return this.reloadContent } static stopPlugin(name) { const plugin = name instanceof Plugin ? name : this.getPluginByName(name); @@ -112,6 +98,11 @@ export default class extends ContentManager { return true; //Return true anyways since plugin doesn't exist } + static get isPlugin() { return this.isThisContent } + static isThisContent(plugin) { + return plugin instanceof Plugin; + } + static get findPlugin() { return this.findContent } static get getPluginIndex() { return this.getContentIndex } static get getPluginByName() { return this.getContentByName } diff --git a/client/src/modules/theme.js b/client/src/modules/theme.js index 568a1093..3755b63f 100644 --- a/client/src/modules/theme.js +++ b/client/src/modules/theme.js @@ -49,6 +49,7 @@ export default class Theme { 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 } @@ -56,6 +57,7 @@ export default class Theme { get name() { return this.info.name } 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 } @@ -115,17 +117,17 @@ export default class Theme { } } - enable() { + enable(save = true) { if (!this.enabled) { this.userConfig.enabled = true; - this.saveConfiguration(); + if (save) this.saveConfiguration(); } DOM.injectTheme(this.css, this.id); } - disable() { + disable(save = true) { this.userConfig.enabled = false; - this.saveConfiguration(); + if (save) this.saveConfiguration(); DOM.deleteTheme(this.id); } diff --git a/client/src/modules/thememanager.js b/client/src/modules/thememanager.js index 18490350..0b1ac0e9 100644 --- a/client/src/modules/thememanager.js +++ b/client/src/modules/thememanager.js @@ -30,9 +30,8 @@ export default class ThemeManager extends ContentManager { return 'themes'; } - static get loadAllThemes() { - return this.loadAllContent; - } + static get loadAllThemes() { return this.loadAllContent } + static get refreshThemes() { return this.refreshContent } static get loadContent() { return this.loadTheme } static async loadTheme(paths, configs, info, main) { @@ -53,6 +52,12 @@ export default class ThemeManager extends ContentManager { } } + static get unloadTheme() { return this.unloadContent } + static async reloadTheme(theme) { + theme = await this.reloadContent(theme); + theme.recompile(); + } + static enableTheme(theme) { theme.enable(); } @@ -61,8 +66,9 @@ export default class ThemeManager extends ContentManager { theme.disable(); } - static reloadTheme(theme) { - theme.recompile(); + static get isTheme() { return this.isThisContent } + static isThisContent(theme) { + return theme instanceof Theme; } static async getConfigAsSCSS(config) { diff --git a/client/src/styles/partials/generic/drawers.scss b/client/src/styles/partials/generic/drawers.scss index aab65af5..0575e2cf 100644 --- a/client/src/styles/partials/generic/drawers.scss +++ b/client/src/styles/partials/generic/drawers.scss @@ -1,7 +1,14 @@ .bd-drawer { - border-bottom: 1px solid rgba(114, 118, 126, 0.3); margin-bottom: 15px; + .bd-settings-category > & { + border-bottom: 1px solid rgba(114, 118, 126, 0.3); + + .bd-drawer-contents > .bd-form-item:last-child > .bd-form-divider:last-child { + display: none; + } + } + .bd-form-header { margin-top: 0; cursor: pointer; @@ -31,24 +38,20 @@ } .bd-drawer-contents { + // margin-top is set in JavaScript when the drawer is animating + // It still needs to be set here for it to animate transition: transform 0.2s ease, margin-top 0.2s ease, opacity 0.2s ease; transform: scaleY(0) translateY(0%); - margin-top: -50%; + margin-top: -100%; opacity: 0; > .bd-form-item:last-child > .bd-form-divider:last-child { display: none; } - - &::after { - content: ""; - display: block; - margin-top: 15px; - } } &.bd-drawer-open { - .bd-drawer-open-button { + > .bd-drawer-header .bd-drawer-open-button { .bd-chevron-1 { svg { transform: rotate(90deg) @@ -63,10 +66,16 @@ } } - .bd-drawer-contents { + > .bd-drawer-contents-wrap > .bd-drawer-contents { transform: scaleY(1) translateY(0%); margin-top: 0%; opacity: 1; + + &::after { + content: ""; + display: block; + margin-top: 15px; + } } } } diff --git a/client/src/styles/partials/generic/forms/main.scss b/client/src/styles/partials/generic/forms/main.scss index efb9bc1a..b4a2627c 100644 --- a/client/src/styles/partials/generic/forms/main.scss +++ b/client/src/styles/partials/generic/forms/main.scss @@ -38,3 +38,13 @@ } } } + +.bd-form-customsetting { + &.bd-form-customsetting-debug + .bd-form-divider { + margin-top: 0; + } + + > .bd-drawer { + margin-bottom: 0; + } +} diff --git a/client/src/styles/partials/generic/index.scss b/client/src/styles/partials/generic/index.scss index db30a069..e3614c84 100644 --- a/client/src/styles/partials/generic/index.scss +++ b/client/src/styles/partials/generic/index.scss @@ -6,3 +6,4 @@ @import './forms/index.scss'; @import './material-buttons.scss'; @import './drawers.scss'; +@import './preformatted.scss'; diff --git a/client/src/styles/partials/generic/preformatted.scss b/client/src/styles/partials/generic/preformatted.scss new file mode 100644 index 00000000..0581c294 --- /dev/null +++ b/client/src/styles/partials/generic/preformatted.scss @@ -0,0 +1,20 @@ +.bd-pre-wrap { + background: rgba(0, 0, 0, 0.1); + border: 1px solid rgba(0, 0, 0, 0.3); + border-radius: 3px; + color: #b9bbbe; + white-space: pre-wrap; + font-family: monospace; + overflow-y: auto; + + &:focus { + color: #fff; + border-color: #040405; + } + + @include scrollbar; +} + +.bd-pre { + padding: 11px; +} diff --git a/client/src/ui/components/bd/PluginCard.vue b/client/src/ui/components/bd/PluginCard.vue index 08851ae9..901f3b30 100644 --- a/client/src/ui/components/bd/PluginCard.vue +++ b/client/src/ui/components/bd/PluginCard.vue @@ -15,7 +15,7 @@ - + @@ -31,7 +31,7 @@ settingsOpen: false } }, - props: ['plugin', 'togglePlugin', 'reloadPlugin', 'showSettings'], + props: ['plugin', 'togglePlugin', 'reloadPlugin', 'deletePlugin', 'showSettings'], components: { Card, Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension }, diff --git a/client/src/ui/components/bd/PluginsView.vue b/client/src/ui/components/bd/PluginsView.vue index 8beb7b96..d2c2b8cf 100644 --- a/client/src/ui/components/bd/PluginsView.vue +++ b/client/src/ui/components/bd/PluginsView.vue @@ -23,7 +23,7 @@
- +
@@ -67,11 +67,8 @@ async togglePlugin(plugin) { // TODO Display error if plugin fails to start/stop try { - if (plugin.enabled) { - await PluginManager.stopPlugin(plugin); - } else { - await PluginManager.startPlugin(plugin); - } + await plugin.enabled ? PluginManager.stopPlugin(plugin) : PluginManager.startPlugin(plugin); + this.$forceUpdate(); } catch (err) { console.log(err); } @@ -84,6 +81,15 @@ console.log(err); } }, + async deletePlugin(plugin, unload) { + try { + if (unload) await PluginManager.unloadPlugin(plugin); + else await PluginManager.deletePlugin(plugin); + this.$forceUpdate(); + } catch (err) { + console.error(err); + } + }, showSettings(plugin) { return Modals.contentSettings(plugin); } diff --git a/client/src/ui/components/bd/ThemeCard.vue b/client/src/ui/components/bd/ThemeCard.vue index 95ee76b7..5776f4a0 100644 --- a/client/src/ui/components/bd/ThemeCard.vue +++ b/client/src/ui/components/bd/ThemeCard.vue @@ -13,9 +13,9 @@ - + - + @@ -31,7 +31,7 @@ settingsOpen: false } }, - props: ['theme', 'toggleTheme', 'reloadTheme', 'showSettings'], + props: ['theme', 'toggleTheme', 'reloadTheme', 'deleteTheme', 'showSettings'], components: { Card, Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension }, diff --git a/client/src/ui/components/bd/ThemesView.vue b/client/src/ui/components/bd/ThemesView.vue index 427f0b7b..db8c3f65 100644 --- a/client/src/ui/components/bd/ThemesView.vue +++ b/client/src/ui/components/bd/ThemesView.vue @@ -23,7 +23,7 @@
- +
@@ -59,31 +59,38 @@ this.local = false; }, async refreshLocal() { - await ThemeManager.refreshTheme(); + await ThemeManager.refreshThemes(); }, async refreshOnline() { }, - toggleTheme(theme) { + async toggleTheme(theme) { // TODO Display error if theme fails to enable/disable try { - if (theme.enabled) { - ThemeManager.disableTheme(theme); - } else { - ThemeManager.enableTheme(theme); - } + await theme.enabled ? ThemeManager.disableTheme(theme) : ThemeManager.enableTheme(theme); + this.$forceUpdate(); } catch (err) { console.log(err); } }, - async reloadTheme(theme) { + async reloadTheme(theme, reload) { try { - await ThemeManager.reloadTheme(theme); + if (reload) await ThemeManager.reloadTheme(theme); + else await theme.recompile(); this.$forceUpdate(); } catch (err) { console.log(err); } }, + async deleteTheme(theme, unload) { + try { + if (unload) await ThemeManager.unloadTheme(theme); + else await ThemeManager.deleteTheme(theme); + this.$forceUpdate(); + } catch (err) { + console.error(err); + } + }, showSettings(theme) { return Modals.contentSettings(theme); } diff --git a/client/src/ui/components/bd/setting/Array.vue b/client/src/ui/components/bd/setting/Array.vue index 4ccb8a5c..ee533255 100644 --- a/client/src/ui/components/bd/setting/Array.vue +++ b/client/src/ui/components/bd/setting/Array.vue @@ -104,8 +104,10 @@ for (let newCategory of newSettings) { const category = settings.find(c => c.category === newCategory.category); + if (!category) continue; for (let newSetting of newCategory.settings) { const setting = category.settings.find(s => s.id === newSetting.id); + if (!setting) continue; setting.value = setting.old_value = newSetting.value; setting.changed = false; } diff --git a/client/src/ui/components/bd/setting/Custom.vue b/client/src/ui/components/bd/setting/Custom.vue new file mode 100644 index 00000000..7fee204f --- /dev/null +++ b/client/src/ui/components/bd/setting/Custom.vue @@ -0,0 +1,68 @@ +/** + * BetterDiscord Custom Setting Component + * 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. +*/ + + + + diff --git a/client/src/ui/components/bd/setting/Setting.vue b/client/src/ui/components/bd/setting/Setting.vue index 753ea17a..1008908b 100644 --- a/client/src/ui/components/bd/setting/Setting.vue +++ b/client/src/ui/components/bd/setting/Setting.vue @@ -19,6 +19,7 @@ +
@@ -33,6 +34,7 @@ import SliderSetting from './Slider.vue'; import FileSetting from './File.vue'; import ArraySetting from './Array.vue'; + import CustomSetting from './Custom.vue'; export default { props: [ @@ -48,7 +50,8 @@ MultilineTextSetting, SliderSetting, FileSetting, - ArraySetting + ArraySetting, + CustomSetting }, computed: { changed() { diff --git a/client/src/ui/components/common/Button.vue b/client/src/ui/components/common/Button.vue index a41b758e..917f74e3 100644 --- a/client/src/ui/components/common/Button.vue +++ b/client/src/ui/components/common/Button.vue @@ -9,7 +9,7 @@ */