Merge pull request #133 from samuelthomas2774/add-generic-settings-modal

Add generic settings modal
This commit is contained in:
Alexei Stukov 2018-02-14 19:13:57 +02:00 committed by GitHub
commit a49551f7bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 128 additions and 176 deletions

View File

@ -34,7 +34,8 @@
]
},
{
"category": "Advanced",
"category": "advanced",
"category_name": "Advanced",
"type": "drawer",
"settings": [
{

View File

@ -41,7 +41,7 @@ export default class {
/**
* Update css in client
* @param {String} scss scss to compile
* @param {bool} sendSource send to css editor instance
* @param {bool} sendSource send to css editor instance
*/
static updateScss(scss, sendSource) {
if (sendSource)
@ -86,7 +86,7 @@ export default class {
/**
* Send css to open editor
* @param {any} channel
* @param {any} channel
* @param {any} data
*/
static async sendToEditor(channel, data) {
@ -94,14 +94,14 @@ export default class {
}
/**
* Current uncompiled scss
* Current uncompiled scss
*/
static get scss() {
return this._scss || '';
}
/**
* Set current scss
* Set current scss
*/
static set scss(scss) {
this.updateScss(scss, true);

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { FileUtils } from 'common';
import { Utils, FileUtils } from 'common';
import { Modals } from 'ui';
import { EventEmitter } from 'events';
import ContentConfig from './contentconfig';
@ -38,7 +38,7 @@ export default class Plugin {
constructor(pluginInternals) {
this.__pluginInternals = pluginInternals;
this.saveSettings = this.saveSettings.bind(this);
this.hasSettings = this.pluginConfig && this.pluginConfig.length > 0;
this.hasSettings = this.config && this.config.length > 0;
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
}
@ -64,7 +64,7 @@ export default class Plugin {
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new PluginEvents(this)) }
getSetting(setting_id, category_id) {
for (let category of this.pluginConfig) {
for (let category of this.config) {
if (category_id && category.category !== category_id) return;
for (let setting of category.settings) {
if (setting.id !== setting_id) return;
@ -74,17 +74,17 @@ export default class Plugin {
}
showSettingsModal() {
return Modals.pluginSettings(this);
return Modals.contentSettings(this);
}
async saveSettings(newSettings) {
const updatedSettings = [];
for (let newCategory of newSettings) {
const category = this.pluginConfig.find(c => c.category === newCategory.category);
const category = this.config.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;
if (Utils.compare(setting.value, newSetting.value)) continue;
const old_value = setting.value;
setting.value = newSetting.value;
@ -109,9 +109,9 @@ export default class Plugin {
}
async saveConfiguration() {
window.testConfig = new ContentConfig(this.pluginConfig);
window.testConfig = new ContentConfig(this.config);
try {
const config = new ContentConfig(this.pluginConfig).strip();
const config = new ContentConfig(this.config).strip();
await FileUtils.writeFile(`${this.pluginPath}/user.config.json`, JSON.stringify({
enabled: this.enabled,
config

View File

@ -12,7 +12,7 @@ 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 { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { SettingUpdatedEvent } from 'structs';
import path from 'path';
@ -113,6 +113,29 @@ export default class {
return setting ? setting.value : undefined;
}
static mergeSettings(set_id, newSettings, settingsUpdated) {
const set = this.getSet(set_id);
if (!set) return;
const updatedSettings = [];
for (let newCategory of newSettings) {
let category = set.settings.find(c => c.category === newCategory.category);
for (let newSetting of newCategory.settings) {
let setting = category.settings.find(s => s.id === newSetting.id);
if (Utils.compare(setting.value, newSetting.value)) continue;
let old_value = setting.value;
setting.value = newSetting.value;
updatedSettings.push({ set_id: set.id, category_id: category.category, setting_id: setting.id, value: setting.value, old_value });
this.settingUpdated(set.id, category.category, setting.id, setting.value, old_value);
}
}
this.saveSettings();
return settingsUpdated ? settingsUpdated(updatedSettings) : updatedSettings;
}
static setSetting(set_id, category_id, setting_id, value) {
for (let set of this.getSettings) {
if (set.id !== set_id) continue;
@ -122,7 +145,7 @@ export default class {
for (let setting of category.settings) {
if (setting.id !== setting_id) continue;
if (setting.value === value) return true;
if (Utils.compare(setting.value, value)) return true;
let old_value = setting.value;
setting.value = value;

View File

@ -12,7 +12,7 @@ import ThemeManager from './thememanager';
import { EventEmitter } from 'events';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
import { DOM, Modals } from 'ui';
import { FileUtils, ClientIPC } from 'common';
import { Utils, FileUtils, ClientIPC } from 'common';
import ContentConfig from './contentconfig';
class ThemeEvents {
@ -38,7 +38,7 @@ export default class Theme {
constructor(themeInternals) {
this.__themeInternals = themeInternals;
this.hasSettings = this.themeConfig && this.themeConfig.length > 0;
this.hasSettings = this.config && this.config.length > 0;
this.saveSettings = this.saveSettings.bind(this);
this.enable = this.enable.bind(this);
this.disable = this.disable.bind(this);
@ -64,17 +64,17 @@ export default class Theme {
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new ThemeEvents(this)) }
showSettingsModal() {
return Modals.themeSettings(this);
return Modals.contentSettings(this);
}
async saveSettings(newSettings) {
const updatedSettings = [];
for (let newCategory of newSettings) {
const category = this.themeConfig.find(c => c.category === newCategory.category);
const category = this.config.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;
if (Utils.compare(setting.value, newSetting.value)) continue;
const old_value = setting.value;
setting.value = newSetting.value;
@ -103,7 +103,7 @@ export default class Theme {
async saveConfiguration() {
try {
const config = new ContentConfig(this.themeConfig).strip();
const config = new ContentConfig(this.config).strip();
await FileUtils.writeFile(`${this.themePath}/user.config.json`, JSON.stringify({
enabled: this.enabled,
config,
@ -134,7 +134,7 @@ export default class Theme {
let css = '';
if (this.info.type === 'sass') {
css = await ClientIPC.send('bd-compileSass', {
data: ThemeManager.getConfigAsSCSS(this.themeConfig),
data: ThemeManager.getConfigAsSCSS(this.config),
path: this.paths.mainPath.replace(/\\/g, '/')
});
console.log(css);

View File

@ -3,4 +3,3 @@
@import './plugins.scss';
@import './card.scss';
@import './tooltips.scss';
@import './plugin-settings-modal.scss';

View File

@ -5,3 +5,4 @@
@import './basic-modal.scss';
@import './error-modal.scss';
@import './settings-modal.scss';

View File

@ -1,4 +1,4 @@
.bd-plugin-settings-modal {
.bd-settings-modal {
.bd-modal .bd-modal-body {
padding: 0;
}
@ -9,7 +9,7 @@
}
}
.bd-plugin-settings-body {
.bd-settings-modal-body {
padding: 0 15px;
margin: 0 0 74px;

View File

@ -38,7 +38,6 @@
};
},
created() {
console.log(this);
Events.on('bd-refresh-modals', this.eventListener = () => {
this.$forceUpdate();
});

View File

@ -89,7 +89,7 @@
})();
},
showSettings(plugin) {
return Modals.pluginSettings(plugin);
return Modals.contentSettings(plugin);
}
}
}

View File

@ -16,11 +16,11 @@
</div>
<div v-else-if="category.type === 'static'">
<div class="bd-form-header">
<span class="bd-form-header-text">{{category.category}} static with header</span>
<span class="bd-form-header-text">{{ category.category_name }}</span>
</div>
<Setting v-for="setting in category.settings" :key="setting.id" :setting="setting" :change="v => settingChange(category, setting, v)" />
</div>
<Drawer v-else-if="category.type === 'drawer'" :label="category.category">
<Drawer v-else-if="category.type === 'drawer'" :label="category.category_name">
<Setting v-for="setting in category.settings" :key="setting.id" :setting="setting" :change="v => settingChange(category, setting, v)" />
</Drawer>
<div v-else>

View File

@ -89,7 +89,7 @@
})();
},
showSettings(theme) {
return Modals.themeSettings(theme);
return Modals.contentSettings(theme);
}
}
}

View File

@ -1,117 +0,0 @@
/**
* BetterDiscord Plugin Settings Modal 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.
*/
<template>
<div class="bd-plugin-settings-modal" :class="{'bd-edited': changed}">
<Modal :headerText="plugin.name + ' Settings'" :close="attemptToClose" :class="{'bd-modal-out': modal.closing}">
<SettingsPanel :settings="configCache" :change="settingChange" slot="body" class="bd-plugin-settings-body" />
<div slot="footer" class="bd-footer-alert" :class ="{'bd-active': changed, 'bd-warn': warnclose}">
<div class="bd-footer-alert-text">Unsaved changes</div>
<div class="bd-button bd-reset-button bd-tp" :class="{'bd-disabled': saving}" @click="resetSettings">Reset</div>
<div class="bd-button bd-ok" :class="{'bd-disabled': saving}" @click="saveSettings">
<div v-if="saving" class="bd-spinner-7"></div>
<template v-else>Save Changes</template>
</div>
</div>
</Modal>
</div>
</template>
<script>
// Imports
import Vue from 'vue';
import { Modal } from '../../common';
import SettingsPanel from '../SettingsPanel.vue';
export default {
props: ['modal'],
data() {
return {
changed: false,
warnclose: false,
configCache: [],
closing: false,
saving: false
}
},
components: {
Modal,
SettingsPanel
},
computed: {
plugin() { return this.modal.plugin; }
},
methods: {
checkForChanges() {
let changed = false;
for (let category of this.configCache) {
const cat = this.plugin.pluginConfig.find(c => c.category === category.category);
for (let setting of category.settings) {
if (cat.settings.find(s => s.id === setting.id).value !== setting.value) {
changed = true;
Vue.set(setting, 'changed', true);
} else {
Vue.set(setting, 'changed', false);
}
}
}
return changed;
},
settingChange(category_id, setting_id, value) {
const category = this.configCache.find(c => c.category === category_id);
if (!category) return;
const setting = category.settings.find(s => s.id === setting_id);
if (!setting) return;
setting.value = value;
this.changed = this.checkForChanges();
this.$forceUpdate();
},
async saveSettings() {
if (this.saving) return;
this.saving = true;
try {
await this.plugin.saveSettings(this.configCache);
this.configCache = JSON.parse(JSON.stringify(this.plugin.pluginConfig));
this.changed = false;
} catch (err) {
// TODO Display error that settings failed to save
console.log(err);
}
this.saving = false;
},
resetSettings() {
if (this.saving) return;
this.configCache = JSON.parse(JSON.stringify(this.plugin.pluginConfig));
this.changed = false;
this.$forceUpdate();
},
attemptToClose(e) {
if (!this.changed) {
this.closing = true;
setTimeout(() => {
this.modal.close();
}, 200);
return;
}
this.warnclose = true;
setTimeout(() => {
this.warnclose = false;
}, 400);
}
},
beforeMount() {
this.configCache = JSON.parse(JSON.stringify(this.plugin.pluginConfig));
console.log(this.configCache);
this.changed = this.checkForChanges();
}
}
</script>

View File

@ -1,5 +1,5 @@
/**
* BetterDiscord Theme Settings Modal Component
* BetterDiscord Settings Modal Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
@ -9,10 +9,10 @@
*/
<template>
<div class="bd-plugin-settings-modal" :class="{'bd-edited': changed}">
<Modal :headerText="theme.name + ' Settings'" :close="attemptToClose" :class="{'bd-modal-out': modal.closing}">
<SettingsPanel :settings="configCache" :change="settingChange" slot="body" class="bd-plugin-settings-body" />
<div slot="footer" class="bd-footer-alert" :class ="{'bd-active': changed, 'bd-warn': warnclose}">
<div class="bd-settings-modal" :class="{'bd-edited': changed}">
<Modal :headerText="modal.headertext" :close="attemptToClose" :class="{'bd-modal-out': modal.closing}">
<SettingsPanel :settings="configCache" :change="settingChange" slot="body" class="bd-settings-modal-body" />
<div slot="footer" class="bd-footer-alert" :class="{'bd-active': changed, 'bd-warn': warnclose}">
<div class="bd-footer-alert-text">Unsaved changes</div>
<div class="bd-button bd-reset-button bd-tp" :class="{'bd-disabled': saving}" @click="resetSettings">Reset</div>
<div class="bd-button bd-ok" :class="{'bd-disabled': saving}" @click="saveSettings">
@ -28,6 +28,7 @@
import Vue from 'vue';
import { Modal } from '../../common';
import SettingsPanel from '../SettingsPanel.vue';
import { Utils } from 'common';
export default {
props: ['modal'],
@ -44,16 +45,13 @@
Modal,
SettingsPanel
},
computed: {
theme() { return this.modal.theme; }
},
methods: {
checkForChanges() {
let changed = false;
for (let category of this.configCache) {
const cat = this.theme.themeConfig.find(c => c.category === category.category);
const cat = this.modal.settings.find(c => c.category === category.category);
for (let setting of category.settings) {
if (cat.settings.find(s => s.id === setting.id).value !== setting.value) {
if (!Utils.compare(cat.settings.find(s => s.id === setting.id).value, setting.value)) {
changed = true;
Vue.set(setting, 'changed', true);
} else {
@ -79,8 +77,8 @@
if (this.saving) return;
this.saving = true;
try {
await this.theme.saveSettings(this.configCache);
this.configCache = JSON.parse(JSON.stringify(this.theme.themeConfig));
await this.modal.saveSettings(this.configCache);
this.configCache = JSON.parse(JSON.stringify(this.modal.settings));
this.changed = false;
} catch (err) {
// TODO Display error that settings failed to save
@ -90,7 +88,7 @@
},
resetSettings() {
if (this.saving) return;
this.configCache = JSON.parse(JSON.stringify(this.theme.themeConfig));
this.configCache = JSON.parse(JSON.stringify(this.modal.settings));
this.changed = false;
this.$forceUpdate();
},
@ -109,8 +107,7 @@
}
},
beforeMount() {
this.configCache = JSON.parse(JSON.stringify(this.theme.themeConfig));
console.log(this.configCache);
this.configCache = JSON.parse(JSON.stringify(this.modal.settings));
this.changed = this.checkForChanges();
}
}

View File

@ -49,7 +49,7 @@
shell.openItem(file_path);
},
removeItem(file_path) {
this.change(this.setting.id, this.setting.value.filter(f => f !== file_path));
this.change(this.setting.value.filter(f => f !== file_path));
}
}
}

View File

@ -8,12 +8,11 @@
* LICENSE file in the root directory of this source tree.
*/
import { FileUtils } from 'common';
import { Events, PluginManager, ThemeManager } from 'modules';
import { Utils, FileUtils } from 'common';
import { Settings, Events, PluginManager, ThemeManager } from 'modules';
import BasicModal from './components/bd/modals/BasicModal.vue';
import ErrorModal from './components/bd/modals/ErrorModal.vue';
import PluginSettingsModal from './components/bd/modals/PluginSettingsModal.vue';
import ThemeSettingsModal from './components/bd/modals/ThemeSettingsModal.vue';
import SettingsModal from './components/bd/modals/SettingsModal.vue';
export default class {
@ -78,12 +77,39 @@ export default class {
});
}
static pluginSettings(plugin) {
return this.add({ plugin }, PluginSettingsModal);
static settings(headertext, settings, settingsUpdated, settingUpdated, saveSettings) {
return this.add({
headertext, settings,
saveSettings: saveSettings ? saveSettings : newSettings => {
const updatedSettings = [];
for (let newCategory of newSettings) {
let category = settings.find(c => c.category === newCategory.category);
for (let newSetting of newCategory.settings) {
let setting = category.settings.find(s => s.id === newSetting.id);
if (Utils.compare(setting.value, newSetting.value)) continue;
let old_value = setting.value;
setting.value = newSetting.value;
updatedSettings.push({ category_id: category.category, setting_id: setting.id, value: setting.value, old_value });
if (settingUpdated) settingUpdated(category.category, setting.id, setting.value, old_value);
}
}
return settingsUpdated ? settingsUpdated(updatedSettings) : updatedSettings;
}
}, SettingsModal);
}
static themeSettings(theme) {
return this.add({ theme }, ThemeSettingsModal);
static internalSettings(set_id) {
const set = Settings.getSet(set_id);
if (!set) return;
return this.settings(set.headertext, set.settings, null, null, newSettings => Settings.mergeSettings(set.id, newSettings));
}
static contentSettings(content) {
return this.settings(content.name + ' Settings', content.config, null, null, content.saveSettings.bind(content));
}
static get stack() {

View File

@ -45,6 +45,27 @@ export class Utils {
});
return camelCased;
}
static compare(value1, value2) {
// Check to see if value1 and value2 contain the same data
if (typeof value1 !== typeof value2) return false;
if (value1 === null && value2 === null) return true;
if (value1 === null || value2 === null) return false;
if (typeof value1 === 'object' || typeof value1 === 'array') {
// Loop through the object and check if everything's the same
let value1array = typeof value1 === 'array' ? value1 : Object.keys(value1);
let value2array = typeof value2 === 'array' ? value2 : Object.keys(value2);
if (value1array.length !== value2array.length) return false;
for (let key in value1) {
if (!this.compare(value1[key], value2[key])) return false;
}
} else if (value1 !== value2) return false;
// value1 and value2 contain the same data
return true;
}
}
export class FileUtils {

View File

@ -8,10 +8,10 @@
"icon": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg=="
},
"main": "index.js",
"type": "plugin",
"type": "plugin",
"dependencies": {
"Example Module": "1.0"
},
"Example Module": "1.0"
},
"defaultConfig": [
{
"category_default_comment": "default category has no header and is always displayed first",
@ -125,7 +125,8 @@
},
{
"category_header_comment": "Setting a category other than default has a header with category name as the text",
"category": "Test Category",
"category": "test-category",
"category_name": "Test Category",
"drawer_type_comment": "// Drawer type will create an expandable drawer for the settings",
"type": "drawer",
"settings": [
@ -153,7 +154,8 @@
]
},
{
"category": "Test Category 2",
"category": "test-category-2",
"category_name": "Test Category 2 (static with header)",
"static_type_comment": "Static type will behave like default but will have a header",
"type": "static",
"settings": [