From 5be5002ea1371517edd45bef991a6a5620d867f8 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Wed, 21 Feb 2018 12:41:26 +0000 Subject: [PATCH 1/5] Fix drawer open/close animation where height > width --- .../src/styles/partials/generic/drawers.scss | 16 ++++++++------ client/src/ui/components/common/Drawer.vue | 22 +++++++++++++++---- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/client/src/styles/partials/generic/drawers.scss b/client/src/styles/partials/generic/drawers.scss index aab65af5..b2854af7 100644 --- a/client/src/styles/partials/generic/drawers.scss +++ b/client/src/styles/partials/generic/drawers.scss @@ -31,20 +31,16 @@ } .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 { @@ -67,6 +63,12 @@ transform: scaleY(1) translateY(0%); margin-top: 0%; opacity: 1; + + &::after { + content: ""; + display: block; + margin-top: 15px; + } } } } diff --git a/client/src/ui/components/common/Drawer.vue b/client/src/ui/components/common/Drawer.vue index 6a0e61a4..fcfeeca3 100644 --- a/client/src/ui/components/common/Drawer.vue +++ b/client/src/ui/components/common/Drawer.vue @@ -9,7 +9,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/tests/plugins/Example/component.js b/tests/plugins/Example/component.js new file mode 100644 index 00000000..c82c1359 --- /dev/null +++ b/tests/plugins/Example/component.js @@ -0,0 +1,4 @@ +module.exports.default = { + template: "
Test custom setting {{ setting.id }}. This is from component.js in the plugin/theme's directory. (It can use functions.)
", + props: ['setting', 'change'] +}; diff --git a/tests/plugins/Example/config.json b/tests/plugins/Example/config.json index 25ca74e5..b32022b0 100644 --- a/tests/plugins/Example/config.json +++ b/tests/plugins/Example/config.json @@ -194,6 +194,51 @@ } ] }, + { + "category": "custom-settings", + "category_name": "Custom settings", + "type": "drawer", + "settings": [ + { + "id": "custom-1", + "type": "custom", + "value": false, + "component": { + "template": "
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.)
", + "props": [ + "setting", + "change" + ] + }, + "debug": true + }, + { + "id": "custom-2", + "type": "custom", + "value": false, + "file": "component.js" + }, + { + "id": "custom-3", + "type": "custom", + "value": false, + "component": "settingscomponent" + }, + { + "id": "custom-4", + "type": "custom", + "value": false, + "function": "getSettingsComponent" + }, + { + "id": "custom-5", + "type": "custom", + "value": false, + "function": "getSettingsComponentHTMLElement", + "debug": true + } + ] + }, { "category_header_comment": "Setting a category other than default has a header with category name as the text", "category": "test-category", diff --git a/tests/plugins/Example/index.js b/tests/plugins/Example/index.js index 74042970..c1aef744 100644 --- a/tests/plugins/Example/index.js +++ b/tests/plugins/Example/index.js @@ -56,6 +56,44 @@ module.exports = (Plugin, Api, Vendor, Dependencies) => { if (!this.enabled) return; Logger.log([ 'Settings updated', settings ]); } + + get settingscomponent() { + const plugin = this; + return this._settingscomponent ? this._settingscomponent : this._settingscomponent = { + template: "
Test custom setting {{ setting.id }}. This is from Plugin.settingscomponent.
Plugin ID: {{ plugin.id }}
", + props: ['setting', 'change'], + data() { return { plugin }; } + }; + } + + getSettingsComponent(setting, change) { + return this._settingscomponent2 ? this._settingscomponent2 : this.settingscomponent2 = { + template: "
Test custom setting {{ setting.id }}. This is from Plugin.getSettingsComponent().
", + props: ['setting', 'change'] + }; + } + + getSettingsComponentHTMLElement(setting, change) { + const el = document.createElement('div'); + el.setAttribute('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;'); + el.textContent = `Test custom setting ${setting.id}. This is from Plugin.getSettingsComponentHTMLElement(). Current value: ${setting.value}.`; + + const button1 = document.createElement('button'); + button1.setAttribute('class', 'bd-button bd-button-primary'); + button1.setAttribute('style', 'display: inline-block; margin-left: 10px;'); + button1.addEventListener('click', () => change(1)); + button1.textContent = 'Set value to 1'; + el.appendChild(button1); + + const button2 = document.createElement('button'); + button2.setAttribute('class', 'bd-button bd-button-primary'); + button2.setAttribute('style', 'display: inline-block; margin-left: 10px;'); + button2.addEventListener('click', () => change(2)); + button2.textContent = 'Set value to 2'; + el.appendChild(button2); + + return el; + } } } From 4cfa4ee4664f15dba10edfb15a16660f36f0f6ad Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Wed, 21 Feb 2018 17:46:27 +0000 Subject: [PATCH 3/5] Content reloading --- client/src/modules/contentmanager.js | 65 ++++++++++++++++++--- client/src/modules/extmodule.js | 5 +- client/src/modules/extmodulemanager.js | 14 ++++- client/src/modules/plugin.js | 19 ++++-- client/src/modules/pluginmanager.js | 51 +++++++--------- client/src/modules/theme.js | 10 ++-- client/src/modules/thememanager.js | 17 ++++-- client/src/ui/components/bd/PluginCard.vue | 4 +- client/src/ui/components/bd/PluginsView.vue | 18 ++++-- client/src/ui/components/bd/ThemeCard.vue | 6 +- client/src/ui/components/bd/ThemesView.vue | 27 +++++---- client/src/ui/components/common/Button.vue | 2 +- tests/plugins/Example/index.js | 5 ++ 13 files changed, 165 insertions(+), 78 deletions(-) diff --git a/client/src/modules/contentmanager.js b/client/src/modules/contentmanager.js index cdf89451..f3b26d39 100644 --- a/client/src/modules/contentmanager.js +++ b/client/src/modules/contentmanager.js @@ -185,6 +185,9 @@ export default class { }; 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; @@ -194,6 +197,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 @@ -212,19 +252,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; + if (content = this.getContentById(wild)) return content; + if (content = this.getContentByDirName(wild)) return content; + if (content = this.getContentByPath(wild)) return content; + if (content = nonunique && this.getContentByName(wild)) 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 9ada7016..14f85de3 100644 --- a/client/src/modules/plugin.js +++ b/client/src/modules/plugin.js @@ -52,7 +52,7 @@ 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 } @@ -123,25 +123,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..ada6a968 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,9 @@ export default class ThemeManager extends ContentManager { } } + static get unloadTheme() { return this.unloadContent } + static get reloadTheme() { return this.reloadContent } + static enableTheme(theme) { theme.enable(); } @@ -61,10 +63,17 @@ export default class ThemeManager extends ContentManager { theme.disable(); } - static reloadTheme(theme) { + static get unloadTheme() { return this.unloadContent } + static async reloadTheme(theme) { + theme = await this.reloadContent(theme); theme.recompile(); } + static get isTheme() { return this.isThisContent } + static isThisContent(theme) { + return theme instanceof Theme; + } + static async getConfigAsSCSS(config) { const variables = []; 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/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 @@ */