Merge pull request #152 from samuelthomas2774/settings

Refactor settings, add CSS/SCSS utilities to the plugin API and add automatic recompile of theme/custom SCSS
This commit is contained in:
Alexei Stukov 2018-03-04 22:29:45 +02:00 committed by GitHub
commit 1b6a846102
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 3918 additions and 1385 deletions

View File

@ -76,6 +76,32 @@
"headertext": "Emote Settings",
"settings": []
},
{
"id": "css",
"text": "CSS Editor",
"hidden": true,
"settings": [
{
"category": "default",
"settings": [
{
"id": "live-update",
"type": "bool",
"text": "Live update",
"hint": "Automatically recompile custom CSS when typing in the custom CSS editor.",
"value": true
},
{
"id": "watch-files",
"type": "bool",
"text": "Watch included files",
"hint": "Automatically recompile theme and custom CSS when a file it imports is changed.",
"value": true
}
]
}
]
},
{
"id": "security",
"text": "Security",

View File

@ -20,6 +20,7 @@ class BetterDiscord {
window.ClientIPC = ClientIPC;
window.css = CssEditor;
window.pm = PluginManager;
window.tm = ThemeManager;
window.events = Events;
window.wpm = WebpackModules;
window.bdsettings = Settings;

View File

@ -1,31 +0,0 @@
/**
* 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
}))
}));
}
}

View File

@ -9,10 +9,10 @@
*/
import Globals from './globals';
import { FileUtils, ClientLogger as Logger } from 'common';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import { Events } from 'modules';
import { ErrorEvent } from 'structs';
import { SettingsSet, ErrorEvent } from 'structs';
import { Modals } from 'ui';
/**
@ -54,6 +54,10 @@ export default class {
const directories = await FileUtils.listDirectory(this.contentPath);
for (let dir of directories) {
try {
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; }
try {
await this.preloadContent(dir);
} catch (err) {
@ -98,6 +102,10 @@ export default class {
// If content is already loaded this should resolve.
if (this.getContentByDirName(dir)) continue;
try {
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; }
try {
// Load if not
await this.preloadContent(dir);
@ -168,37 +176,42 @@ export default class {
const readConfig = await this.readConfig(contentPath);
const mainPath = path.join(contentPath, readConfig.main);
readConfig.defaultConfig = readConfig.defaultConfig || [];
const defaultConfig = new SettingsSet({
settings: readConfig.defaultConfig,
schemes: readConfig.configSchemes
});
const userConfig = {
enabled: false,
config: readConfig.defaultConfig
config: undefined,
data: {}
};
try {
const readUserConfig = await this.readUserConfig(contentPath);
userConfig.enabled = readUserConfig.enabled || false;
for (let category of userConfig.config) {
const newCategory = readUserConfig.config.find(c => c.category === category.category);
for (let setting of category.settings) {
setting.path = contentPath;
if (!newCategory) continue;
const newSetting = newCategory.settings.find(s => s.id === setting.id);
if (!newSetting) continue;
setting.value = newSetting.value;
}
}
userConfig.css = readUserConfig.css || null;
// await userConfig.config.merge({ settings: readUserConfig.config });
// userConfig.config.setSaved();
// userConfig.config = userConfig.config.clone({ settings: readUserConfig.config });
userConfig.config = readUserConfig.config;
userConfig.data = readUserConfig.data || {};
} catch (err) { /*We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config*/
Logger.info(this.moduleName, `Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`);
Logger.err(this.moduleName, err);
}
userConfig.config = defaultConfig.clone({ settings: userConfig.config });
userConfig.config.setSaved();
for (let setting of userConfig.config.findSettings(() => true)) {
setting.setContentPath(contentPath);
}
Utils.deepfreeze(defaultConfig);
const configs = {
defaultConfig: readConfig.defaultConfig,
schemes: readConfig.configSchemes,
defaultConfig,
schemes: userConfig.schemes,
userConfig
};
@ -209,7 +222,7 @@ export default class {
};
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions);
if (!content) return null;
if (!content) return undefined;
if (!reload && this.getContentById(content.id))
throw {message: `A ${this.contentType} with the ID ${content.id} already exists.`};

View File

@ -8,19 +8,33 @@
* LICENSE file in the root directory of this source tree.
*/
import { ClientIPC } from 'common';
import { FileUtils, ClientLogger as Logger, ClientIPC } from 'common';
import Settings from './settings';
import { DOM } from 'ui';
import filewatcher from 'filewatcher';
import path from 'path';
import electron from 'electron';
/**
* Custom css editor communications
*/
export default class {
export default new class {
constructor() {
this._scss = '';
this._css = '';
this._error = undefined;
this.editor_bounds = undefined;
this._files = undefined;
this._filewatcher = undefined;
this._watchfiles = undefined;
this.compiling = false;
}
/**
* Init css editor
*/
static init() {
init() {
ClientIPC.on('bd-get-scss', () => this.sendToEditor('set-scss', { scss: this.scss }));
ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss));
ClientIPC.on('bd-save-csseditor-bounds', (e, bounds) => this.saveEditorBounds(bounds));
@ -29,12 +43,26 @@ export default class {
await this.updateScss(scss);
await this.save();
});
this.liveupdate = Settings.getSetting('css', 'default', 'live-update');
this.liveupdate.on('setting-updated', event => {
this.sendToEditor('set-liveupdate', event.value);
});
ClientIPC.on('bd-get-liveupdate', () => this.sendToEditor('set-liveupdate', this.liveupdate.value));
ClientIPC.on('bd-set-liveupdate', (e, value) => this.liveupdate.value = value);
this.watchfilessetting = Settings.getSetting('css', 'default', 'watch-files');
this.watchfilessetting.on('setting-updated', event => {
if (event.value) this.watchfiles = this.files;
else this.watchfiles = [];
});
}
/**
* Show css editor, flashes if already visible
*/
static async show() {
async show() {
await ClientIPC.send('openCssEditor', this.editor_bounds);
}
@ -43,33 +71,35 @@ export default class {
* @param {String} scss scss to compile
* @param {bool} sendSource send to css editor instance
*/
static updateScss(scss, sendSource) {
async updateScss(scss, sendSource) {
if (sendSource)
this.sendToEditor('set-scss', { scss });
if (!scss) {
this._scss = this.css = '';
this.sendToEditor('scss-error', null);
return Promise.resolve();
return;
}
return new Promise((resolve, reject) => {
this.compile(scss).then(css => {
this.css = css;
this._scss = scss;
this.sendToEditor('scss-error', null);
resolve();
}).catch(err => {
this.sendToEditor('scss-error', err);
reject(err);
});
});
try {
this.compiling = true;
const result = await this.compile(scss);
this.css = result.css.toString();
this._scss = scss;
this.files = result.stats.includedFiles;
this.error = null;
this.compiling = false;
} catch (err) {
this.compiling = false;
this.error = err;
throw err;
}
}
/**
* Save css to file
*/
static async save() {
async save() {
Settings.saveSettings();
}
@ -77,7 +107,7 @@ export default class {
* Save current editor bounds
* @param {Rectangle} bounds editor bounds
*/
static saveEditorBounds(bounds) {
saveEditorBounds(bounds) {
this.editor_bounds = bounds;
Settings.saveSettings();
}
@ -86,39 +116,192 @@ export default class {
* Send scss to core for compilation
* @param {String} scss scss string
*/
static async compile(scss) {
return await ClientIPC.send('bd-compileSass', { data: scss });
async compile(scss) {
return await ClientIPC.send('bd-compileSass', {
data: scss,
path: await this.fileExists() ? this.filePath : undefined
});
}
/**
* Send css to open editor
* Recompile the current SCSS
* @return {Promise}
*/
async recompile() {
return await this.updateScss(this.scss);
}
/**
* Send data to open editor
* @param {any} channel
* @param {any} data
*/
static async sendToEditor(channel, data) {
async sendToEditor(channel, data) {
return await ClientIPC.send('sendToCssEditor', { channel, data });
}
/**
* Opens an SCSS file in a system editor
*/
async openSystemEditor() {
try {
await FileUtils.fileExists(this.filePath);
} catch (err) {
// File doesn't exist
// Create it
await FileUtils.writeFile(this.filePath, '');
}
Logger.log('CSS Editor', `Opening file ${this.filePath} in the user's default editor.`);
// For some reason this doesn't work
// if (!electron.shell.openItem(this.filePath))
if (!electron.shell.openExternal('file://' + this.filePath))
throw {message: 'Failed to open system editor.'};
}
/** Set current state
* @param {String} scss Current uncompiled SCSS
* @param {String} css Current compiled CSS
* @param {String} files Files imported in the SCSS
* @param {String} err Current compiler error
*/
setState(scss, css, files, err) {
this._scss = scss;
this.sendToEditor('set-scss', { scss });
this.css = css;
this.files = files;
this.error = err;
}
/**
* Current uncompiled scss
*/
static get scss() {
get scss() {
return this._scss || '';
}
/**
* Set current scss
*/
static set scss(scss) {
set scss(scss) {
this.updateScss(scss, true);
}
/**
* Current compiled css
*/
get css() {
return this._css || '';
}
/**
* Inject compiled css to head
* {DOM}
*/
static set css(css) {
set css(css) {
this._css = css;
DOM.injectStyle(css, 'bd-customcss');
}
/**
* Current error
*/
get error() {
return this._error || undefined;
}
/**
* Set current error
* {DOM}
*/
set error(err) {
this._error = err;
this.sendToEditor('scss-error', err);
}
/**
* An array of files that are imported in custom CSS.
* @return {Array} Files being watched
*/
get files() {
return this._files || (this._files = []);
}
/**
* Sets all files that are imported in custom CSS.
* @param {Array} files Files to watch
*/
set files(files) {
this._files = files;
if (Settings.get('css', 'default', 'watch-files'))
this.watchfiles = files;
}
/**
* A filewatcher instance.
*/
get filewatcher() {
if (this._filewatcher) return this._filewatcher;
this._filewatcher = filewatcher();
this._filewatcher.on('change', (file, stat) => {
// Recompile SCSS
this.recompile();
});
return this._filewatcher;
}
/**
* An array of files that are being watched for changes.
* @return {Array} Files being watched
*/
get watchfiles() {
return this._watchfiles || (this._watchfiles = []);
}
/**
* Sets all files to be watched.
* @param {Array} files Files to watch
*/
set watchfiles(files) {
for (let file of files) {
if (!this.watchfiles.includes(file)) {
this.filewatcher.add(file);
this.watchfiles.push(file);
Logger.log('CSS Editor', `Watching file ${file} for changes`);
}
}
for (let index in this.watchfiles) {
let file = this.watchfiles[index];
while (file && !files.find(f => f === file)) {
this.filewatcher.remove(file);
this.watchfiles.splice(index, 1);
Logger.log('CSS Editor', `No longer watching file ${file} for changes`);
file = this.watchfiles[index];
}
}
}
/**
* The path of the file the system editor should save to.
* @return {String}
*/
get filePath() {
return path.join(Settings.dataPath, 'user.scss');
}
/**
* Checks if the system editor's file exists.
* @return {Boolean}
*/
async fileExists() {
try {
await FileUtils.fileExists(this.filePath);
return true;
} catch (err) {
return false;
}
}
}

View File

@ -56,6 +56,7 @@ export default class ExtModule {
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 ExtModuleEvents(this)) }
}

View File

@ -12,7 +12,6 @@ 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';
class PluginEvents {
@ -42,6 +41,10 @@ export default class Plugin {
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());
}
get type() { return 'plugin' }
@ -62,16 +65,18 @@ export default class Plugin {
get pluginPath() { return this.paths.contentPath }
get dirName() { return this.paths.dirName }
get enabled() { return this.userConfig.enabled }
get config() { return this.userConfig.config || [] }
get settings() { return this.userConfig.config }
get config() { return this.settings.settings }
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 PluginEvents(this)) }
getSetting(setting_id, category_id) {
for (let category of this.config) {
if (category_id && category.category !== category_id) return;
if (category_id && category.category !== category_id) continue;
for (let setting of category.settings) {
if (setting.id !== setting_id) return;
if (setting.id !== setting_id) continue;
return setting.value;
}
}
@ -82,45 +87,23 @@ export default class Plugin {
}
async saveSettings(newSettings) {
const updatedSettings = [];
const updatedSettings = this.settings.merge(newSettings);
for (let newCategory of newSettings) {
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 (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);
}
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);
await this.saveConfiguration();
return updatedSettings;
}
async saveConfiguration() {
window.testConfig = new ContentConfig(this.config);
try {
const config = new ContentConfig(this.config).strip();
await FileUtils.writeFile(`${this.pluginPath}/user.config.json`, JSON.stringify({
enabled: this.enabled,
config
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;
}
}

View File

@ -8,24 +8,66 @@
* LICENSE file in the root directory of this source tree.
*/
import { ClientLogger as Logger } from 'common';
import { Utils, ClientLogger as Logger, ClientIPC } from 'common';
import Settings from './settings';
import ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager';
import ThemeManager from './thememanager';
import Events from './events';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { Modals, DOM } from 'ui';
import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue';
class EventsWrapper {
constructor(eventemitter) {
this.__eventemitter = eventemitter;
}
get eventSubs() {
return this._eventSubs || (this._eventSubs = []);
}
subscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
this.eventSubs.push({
event,
callback
});
this.__eventemitter.on(event, callback);
}
unsubscribe(event, callback) {
for (let index of this.eventSubs) {
if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) return;
this.__eventemitter.off(event, this.eventSubs[index].callback);
this.eventSubs.splice(index, 1);
}
}
unsubscribeAll() {
for (let event of this.eventSubs) {
this.__eventemitter.off(event.event, event.callback);
}
this._eventSubs = [];
}
}
export default class PluginApi {
constructor(pluginInfo) {
this.pluginInfo = pluginInfo;
this.Events = new EventsWrapper(Events);
}
loggerLog(message) { Logger.log(this.pluginInfo.name, message) }
loggerErr(message) { Logger.err(this.pluginInfo.name, message) }
loggerWarn(message) { Logger.warn(this.pluginInfo.name, message) }
loggerInfo(message) { Logger.info(this.pluginInfo.name, message) }
loggerDbg(message) { Logger.dbg(this.pluginInfo.name, message) }
get plugin() {
return PluginManager.getPluginById(this.pluginInfo.id || this.pluginInfo.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-'));
}
loggerLog(...message) { Logger.log(this.pluginInfo.name, message) }
loggerErr(...message) { Logger.err(this.pluginInfo.name, message) }
loggerWarn(...message) { Logger.warn(this.pluginInfo.name, message) }
loggerInfo(...message) { Logger.info(this.pluginInfo.name, message) }
loggerDbg(...message) { Logger.dbg(this.pluginInfo.name, message) }
get Logger() {
return {
log: this.loggerLog.bind(this),
@ -36,58 +78,148 @@ export default class PluginApi {
};
}
get eventSubs() {
return this._eventSubs || (this._eventSubs = []);
}
eventSubscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event)) return;
this.eventSubs.push({
event,
callback
});
Events.on(event, callback);
}
eventUnsubscribe(event) {
const index = this.eventSubs.findIndex(e => e.event === event);
if (index < 0) return;
Events.off(event, this.eventSubs[0].callback);
this.eventSubs.splice(index, 1);
}
eventUnsubscribeAll() {
this.eventSubs.forEach(event => {
Events.off(event.event, event.callback);
});
this._eventSubs = [];
}
get Events() {
get Utils() {
return {
subscribe: this.eventSubscribe.bind(this),
unsubscribe: this.eventUnsubscribe.bind(this),
unsubscribeAll: this.eventUnsubscribeAll.bind(this)
}
overload: () => Utils.overload.apply(Utils, arguments),
monkeyPatch: () => Utils.monkeyPatch.apply(Utils, arguments),
monkeyPatchOnce: () => Utils.monkeyPatchOnce.apply(Utils, arguments),
compatibleMonkeyPatch: () => Utils.monkeyPatchOnce.apply(Utils, arguments),
tryParseJson: () => Utils.tryParseJson.apply(Utils, arguments),
toCamelCase: () => Utils.toCamelCase.apply(Utils, arguments),
compare: () => Utils.compare.apply(Utils, arguments),
deepclone: () => Utils.deepclone.apply(Utils, arguments),
deepfreeze: () => Utils.deepfreeze.apply(Utils, arguments)
};
}
getSetting(set, category, setting) {
return Settings.get(set, category, setting);
createSettingsSet(args, ...merge) {
return new SettingsSet(args, ...merge);
}
createSettingsCategory(args, ...merge) {
return new SettingsCategory(args, ...merge);
}
createSetting(args, ...merge) {
return new Setting(args, ...merge);
}
createSettingsScheme(args) {
return new SettingsScheme(args);
}
get Settings() {
return {
get: this.getSetting.bind(this)
createSet: this.createSet.bind(this),
createCategory: this.createSettingsCategory.bind(this),
createSetting: this.createSetting.bind(this),
createScheme: this.createSettingsScheme.bind(this)
};
}
getInternalSetting(set, category, setting) {
return Settings.get(set, category, setting);
}
get InternalSettings() {
return {
get: this.getInternalSetting.bind(this)
};
}
get injectedStyles() {
return this._injectedStyles || (this._injectedStyles = []);
}
compileSass(scss, options) {
return ClientIPC.send('bd-compileSass', Object.assign({ data: scss }, options));
}
getConfigAsSCSS(settingsset) {
return ThemeManager.getConfigAsSCSS(settingsset ? settingsset : this.plugin.settings);
}
getConfigAsSCSSMap(settingsset) {
return ThemeManager.getConfigAsSCSSMap(settingsset ? settingsset : this.plugin.settings);
}
injectStyle(id, css) {
if (id && !css) css = id, id = undefined;
this.deleteStyle(id);
const styleid = `plugin-${this.getPlugin().id}-${id}`;
this.injectedStyles.push(styleid);
DOM.injectStyle(css, styleid);
}
async injectSass(id, scss, options) {
// In most cases a plugin's styles should be precompiled instead of using this
if (id && !scss && !options) scss = id, id = undefined;
const css = (await this.compileSass(scss, options)).css.toString();
this.injectStyle(id, css, options);
}
deleteStyle(id) {
const styleid = `plugin-${this.getPlugin().id}-${id}`;
this.injectedStyles.splice(this.injectedStyles.indexOf(styleid), 1);
DOM.deleteStyle(styleid);
}
deleteAllStyles(id, css) {
for (let id of this.injectedStyles) {
this.deleteStyle(id);
}
}
get CssUtils() {
return {
compileSass: this.compileSass.bind(this),
getConfigAsSCSS: this.getConfigAsSCSS.bind(this),
getConfigAsSCSSMap: this.getConfigAsSCSSMap.bind(this),
injectStyle: this.injectStyle.bind(this),
injectSass: this.injectSass.bind(this),
deleteStyle: this.deleteStyle.bind(this),
deleteAllStyles: this.deleteAllStyles.bind(this)
};
}
get modalStack() {
return this._modalStack || (this._modalStack = []);
}
addModal(_modal, component) {
const modal = Modals.add(_modal, component);
modal.close = force => this.closeModal(modal, force);
this.modalStack.push(modal);
return modal;
}
async closeModal(modal, force) {
await Modals.close(modal, force);
this._modalStack = this.modalStack.filter(m => m !== modal);
}
closeAllModals() {
for (let modal of this.modalStack)
modal.close();
}
closeLastModal() {
if (!this.modalStack.length) return;
this.modalStack[this.modalStack.length - 1].close();
}
settingsModal(settingsset, headertext, options) {
return this.addModal(Object.assign({
headertext: headertext ? headertext : settingsset.headertext,
settings: settingsset,
schemes: settingsset.schemes
}, options), SettingsModal);
}
get Modals() {
return Object.defineProperty({
add: this.addModal.bind(this),
close: this.closeModal.bind(this),
closeAll: this.closeAllModals.bind(this),
closeLast: this.closeLastModal.bind(this),
settings: this.settingsModal.bind(this)
}, 'stack', {
get: () => this.modalStack
});
}
async getPlugin(plugin_id) {
// This should require extra permissions
return await PluginManager.waitForPlugin(plugin_id);
}
getPlugins(plugin_id) {
listPlugins(plugin_id) {
return PluginManager.localContent.map(plugin => plugin.id);
}
get Plugins() {
return {
getPlugin: this.getPlugin.bind(this),
getPlugins: this.getPlugins.bind(this)
listPlugins: this.listPlugins.bind(this)
};
}
@ -95,13 +227,13 @@ export default class PluginApi {
// This should require extra permissions
return await ThemeManager.waitForContent(theme_id);
}
getThemes(plugin_id) {
listThemes(plugin_id) {
return ThemeManager.localContent.map(theme => theme.id);
}
get Themes() {
return {
getTheme: this.getTheme.bind(this),
getThemes: this.getThemes.bind(this)
getThemes: this.listThemes.bind(this)
};
}

View File

@ -16,6 +16,7 @@ import Vendor from './vendor';
import { ClientLogger as Logger } from 'common';
import { Events, Permissions } from 'modules';
import { Modals } from 'ui';
import { ErrorEvent } from 'structs';
export default class extends ContentManager {
@ -37,10 +38,32 @@ export default class extends ContentManager {
static async loadAllPlugins(suppressErrors) {
this.loaded = false;
const loadAll = await this.loadAllContent(suppressErrors);
const loadAll = await this.loadAllContent(true);
this.loaded = true;
for (let plugin of this.localPlugins) {
if (plugin.enabled) plugin.start();
try {
if (plugin.enabled) plugin.start();
} 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}`,
err
}));
Logger.err(this.moduleName, [`Failed to start plugin ${plugin.name}:`, 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 loadAll;

View File

@ -13,38 +13,40 @@ import Globals from './globals';
import CssEditor from './csseditor';
import Events from './events';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { SettingUpdatedEvent } from 'structs';
import { SettingsSet, SettingUpdatedEvent } from 'structs';
import path from 'path';
export default class {
static async loadSettings() {
export default new class Settings {
constructor() {
this.settings = [];
}
async loadSettings() {
try {
await FileUtils.ensureDirectory(this.dataPath);
const settingsPath = path.resolve(this.dataPath, 'user.settings.json');
const user_config = await FileUtils.readJsonFromFile(settingsPath);
const { settings, scss, css_editor_bounds } = user_config;
const { settings, scss, css, css_editor_files, scss_error, css_editor_bounds } = user_config;
this.settings = defaultSettings;
this.settings = defaultSettings.map(set => {
const newSet = new SettingsSet(set);
newSet.merge(settings.find(s => s.id === newSet.id));
newSet.setSaved();
newSet.on('setting-updated', event => {
const { category, setting, value, old_value } = event;
Logger.log('Settings', `${newSet.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`);
Events.emit('setting-updated', event);
Events.emit(`setting-updated-${newSet.id}_${category.id}_${setting.id}`, event);
});
newSet.on('settings-updated', async (event) => {
await this.saveSettings();
Events.emit('settings-updated', event);
});
return newSet;
});
for (let newSet of settings) {
let set = this.settings.find(s => s.id === newSet.id);
if (!set) continue;
for (let newCategory of newSet.settings) {
let category = set.settings.find(c => c.category === newCategory.category);
if (!category) continue;
for (let newSetting of newCategory.settings) {
let setting = category.settings.find(s => s.id === newSetting.id);
if (!setting) continue;
setting.value = newSetting.value;
}
}
}
CssEditor.updateScss(scss, true);
CssEditor.setState(scss, css, css_editor_files, scss_error);
CssEditor.editor_bounds = css_editor_bounds || {};
} catch (err) {
// There was an error loading settings
@ -53,29 +55,17 @@ export default class {
}
}
static async saveSettings() {
async saveSettings() {
try {
await FileUtils.ensureDirectory(this.dataPath);
const settingsPath = path.resolve(this.dataPath, 'user.settings.json');
await FileUtils.writeJsonToFile(settingsPath, {
settings: this.getSettings.map(set => {
return {
id: set.id,
settings: set.settings.map(category => {
return {
category: category.category,
settings: category.settings.map(setting => {
return {
id: setting.id,
value: setting.value
};
})
};
})
};
}),
settings: this.settings.map(set => set.strip()),
scss: CssEditor.scss,
css: CssEditor.css,
css_editor_files: CssEditor.files,
scss_error: CssEditor.error,
css_editor_bounds: {
width: CssEditor.editor_bounds.width,
height: CssEditor.editor_bounds.height,
@ -83,94 +73,60 @@ export default class {
y: CssEditor.editor_bounds.y
}
});
for (let set of this.getSettings) {
set.setSaved();
}
} catch (err) {
// There was an error loading settings
// This probably means that the user doesn't have any settings yet
// There was an error saving settings
Logger.err('Settings', err);
throw err;
}
}
static getSet(set_id) {
getSet(set_id) {
return this.getSettings.find(s => s.id === set_id);
}
static getCategory(set_id, category_id) {
get core() { return this.getSet('core') }
get ui() { return this.getSet('ui') }
get emotes() { return this.getSet('emotes') }
get css() { return this.getSet('css') }
get security() { return this.getSet('security') }
getCategory(set_id, category_id) {
const set = this.getSet(set_id);
return set ? set.getCategory(category_id) : undefined;
}
getSetting(set_id, category_id, setting_id) {
const set = this.getSet(set_id);
return set ? set.getSetting(category_id, setting_id) : undefined;
}
get(set_id, category_id, setting_id) {
const set = this.getSet(set_id);
return set ? set.get(category_id, setting_id) : undefined;
}
async mergeSettings(set_id, newSettings) {
const set = this.getSet(set_id);
if (!set) return;
return set.settings.find(c => c.category === category_id);
return await set.merge(newSettings);
}
static getSetting(set_id, category_id, setting_id) {
const category = this.getCategory(set_id, category_id);
if (!category) return;
return category.settings.find(s => s.id === setting_id);
}
static get(set_id, category_id, setting_id) {
setSetting(set_id, category_id, setting_id, value) {
const setting = this.getSetting(set_id, category_id, setting_id);
return setting ? setting.value : undefined;
if (!setting) throw {message: `Tried to set ${set_id}/${category_id}/${setting_id}, which doesn't exist`};
setting.value = value;
}
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;
get getSettings() {
return this.settings;
}
static setSetting(set_id, category_id, setting_id, value) {
for (let set of this.getSettings) {
if (set.id !== set_id) continue;
for (let category of set.settings) {
if (category.category !== category_id) continue;
for (let setting of category.settings) {
if (setting.id !== setting_id) continue;
if (Utils.compare(setting.value, value)) return true;
let old_value = setting.value;
setting.value = value;
this.settingUpdated(set_id, category_id, setting_id, value, old_value);
return true;
}
}
}
return false;
}
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() {
return this.settings ? this.settings : defaultSettings;
}
static get dataPath() {
get dataPath() {
return this._dataPath ? this._dataPath : (this._dataPath = Globals.getObject('paths').find(p => p.id === 'data').path);
}
}

View File

@ -8,12 +8,13 @@
* LICENSE file in the root directory of this source tree.
*/
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 } from 'common';
import ContentConfig from './contentconfig';
import filewatcher from 'filewatcher';
class ThemeEvents {
constructor(theme) {
@ -42,6 +43,17 @@ export default class Theme {
this.saveSettings = this.saveSettings.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());
const watchfiles = Settings.getSetting('css', 'default', 'watch-files');
if (watchfiles.value) this.watchfiles = this.files;
watchfiles.on('setting-updated', event => {
if (event.value) this.watchfiles = this.files;
else this.watchfiles = [];
});
}
get configs() { return this.__themeInternals.configs }
@ -62,9 +74,11 @@ export default class Theme {
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 settings() { return this.userConfig.config }
get config() { return this.settings.settings }
get themeConfig() { return this.config }
get css() { return this.userConfig.css }
get data() { return this.userConfig.data || (this.userConfig.data = {}) }
get css() { return this.data.css }
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new ThemeEvents(this)) }
showSettingsModal() {
@ -72,47 +86,24 @@ export default class Theme {
}
async saveSettings(newSettings) {
const updatedSettings = [];
for (let newCategory of newSettings) {
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 (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);
}
}
const updatedSettings = this.settings.merge(newSettings);
// As the theme's configuration has changed it needs recompiling
// When the compiled CSS has been save it will also save the configuration
// When the compiled CSS has been saved 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.config).strip();
await FileUtils.writeFile(`${this.themePath}/user.config.json`, JSON.stringify({
enabled: this.enabled,
config,
css: this.css
config: this.settings.strip().settings,
data: this.data
}));
this.settings.setSaved();
} catch (err) {
throw err;
}
@ -135,29 +126,35 @@ export default class Theme {
async compile() {
console.log('Compiling CSS');
let css = '';
if (this.info.type === 'sass') {
const config = await ThemeManager.getConfigAsSCSS(this.config);
const config = await ThemeManager.getConfigAsSCSS(this.settings);
css = await ClientIPC.send('bd-compileSass', {
const result = await ClientIPC.send('bd-compileSass', {
data: config,
path: this.paths.mainPath.replace(/\\/g, '/')
});
Logger.log(this.name, ['Finished compiling theme', new class Info {
get SCSS_variables() { console.log(config); }
get Compiled_SCSS() { console.log(css); }
get Compiled_SCSS() { console.log(result.css.toString()); }
get Result() { console.log(result); }
}]);
} else {
css = await FileUtils.readFile(this.paths.mainPath);
}
return css;
return {
css: result.css.toString(),
files: result.stats.includedFiles
};
} else {
return {
css: FileUtils.readFile(this.paths.mainPath)
};
}
}
async recompile() {
const css = await this.compile();
this.userConfig.css = css;
const data = await this.compile();
this.data.css = data.css;
this.files = data.files;
await this.saveConfiguration();
@ -167,4 +164,67 @@ export default class Theme {
}
}
/**
* An array of files that are imported in custom CSS.
* @return {Array} Files being watched
*/
get files() {
return this.data.files || (this.data.files = []);
}
/**
* Sets all files that are imported in custom CSS.
* @param {Array} files Files to watch
*/
set files(files) {
this.data.files = files;
if (Settings.get('css', 'default', 'watch-files'))
this.watchfiles = files;
}
/**
* A filewatcher instance.
*/
get filewatcher() {
if (this._filewatcher) return this._filewatcher;
this._filewatcher = filewatcher();
this._filewatcher.on('change', (file, stat) => {
// Recompile SCSS
this.recompile();
});
return this._filewatcher;
}
/**
* An array of files that are being watched for changes.
* @return {Array} Files being watched
*/
get watchfiles() {
return this._watchfiles || (this._watchfiles = []);
}
/**
* Sets all files to be watched.
* @param {Array} files Files to watch
*/
set watchfiles(files) {
for (let file of files) {
if (!this.watchfiles.includes(file)) {
this.filewatcher.add(file);
this.watchfiles.push(file);
Logger.log(this.name, `Watching file ${file} for changes`);
}
}
for (let index in this.watchfiles) {
let file = this.watchfiles[index];
while (file && !files.find(f => f === file)) {
this.filewatcher.remove(file);
this.watchfiles.splice(index, 1);
Logger.log(this.name, `No longer watching file ${file} for changes`);
file = this.watchfiles[index];
}
}
}
}

View File

@ -72,10 +72,10 @@ export default class ThemeManager extends ContentManager {
return theme instanceof Theme;
}
static async getConfigAsSCSS(config) {
static async getConfigAsSCSS(settingsset) {
const variables = [];
for (let category of config) {
for (let category of settingsset.categories) {
for (let setting of category.settings) {
const setting_scss = await this.parseSetting(setting);
if (setting_scss) variables.push(`$${setting_scss[0]}: ${setting_scss[1]};`);
@ -85,10 +85,10 @@ export default class ThemeManager extends ContentManager {
return variables.join('\n');
}
static async getConfigAsSCSSMap(config) {
static async getConfigAsSCSSMap(settingsset) {
const variables = [];
for (let category of config) {
for (let category of settingsset.categories) {
for (let setting of category.settings) {
const setting_scss = await this.parseSetting(setting);
if (setting_scss) variables.push(`${setting_scss[0]}: (${setting_scss[1]})`);
@ -101,68 +101,9 @@ export default class ThemeManager extends ContentManager {
static async parseSetting(setting) {
const { type, id, value } = setting;
const name = id.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
const scss = await setting.toSCSS();
if (type === 'colour' || type === 'color') {
return [name, value];
}
if (type === 'array') {
const items = JSON.parse(JSON.stringify(value)) || [];
const settings_json = JSON.stringify(setting.settings);
for (let item of items) {
const settings = JSON.parse(settings_json);
for (let category of settings) {
const newCategory = item.settings.find(c => c.category === category.category);
for (let setting of category.settings) {
const newSetting = newCategory.settings.find(s => s.id === setting.id);
setting.value = setting.old_value = newSetting.value;
setting.changed = false;
}
}
item.settings = settings;
}
console.log('items', items);
// Final comma ensures the variable is a list
const maps = [];
for (let item of items)
maps.push(await this.getConfigAsSCSSMap(item.settings));
return [name, maps.length ? maps.join(', ') + ',' : '()'];
}
if (type === 'file' && Array.isArray(value)) {
if (!value || !value.length) return [name, '()'];
const files = [];
for (let filepath of value) {
const buffer = await FileUtils.readFileBuffer(path.resolve(setting.path, filepath));
const type = await FileUtils.getFileType(buffer);
files.push(`(data: ${this.toSCSSString(buffer.toString('base64'))}, type: ${this.toSCSSString(type.mime)}, url: ${this.toSCSSString(await FileUtils.toDataURI(buffer, type.mime))})`);
}
return [name, files.length ? files.join(', ') : '()'];
}
if (type === 'slider') {
return [name, value * setting.multi || 1];
}
if (type === 'dropdown' || type === 'radio') {
const option = setting.options.find(opt => opt.id === value);
return [name, option ? option.value : value];
}
if (typeof value === 'boolean' || typeof value === 'number') {
return [name, value];
}
if (typeof value === 'string') {
return [name, this.toSCSSString(value)];
}
if (scss) return [name, scss];
}
static toSCSSString(value) {

View File

@ -13,15 +13,27 @@ import Event from './event';
export default class SettingUpdatedEvent extends Event {
get set() {
return this.args.set_id;
return this.args.set;
}
get set_id() {
return this.args.set.id;
}
get category() {
return this.args.category_id;
return this.args.category;
}
get category_id() {
return this.args.category.id;
}
get setting() {
return this.args.setting_id;
return this.args.setting;
}
get setting_id() {
return this.args.setting.id;
}
get value() {

View File

@ -0,0 +1,4 @@
export { default as SettingsSet } from './settingsset';
export { default as SettingsCategory } from './settingscategory';
export { default as Setting } from './setting';
export { default as SettingsScheme } from './settingsscheme';

View File

@ -0,0 +1,35 @@
/**
* BetterDiscord Multiple Choice Option 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 { Utils } from 'common';
export default class MultipleChoiceOption {
constructor(args) {
this.args = args.args || args;
}
get id() {
return this.args.id || this.value;
}
get text() {
return this.args.text;
}
get value() {
return this.args.value;
}
clone() {
return new MultipleChoiceOption(Utils.deepclone(this.args));
}
}

View File

@ -0,0 +1,44 @@
/**
* BetterDiscord Setting 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 { Utils } from 'common';
import BoolSetting from './types/bool';
import StringSetting from './types/text';
import NumberSetting from './types/number';
import DropdownSetting from './types/dropdown';
import RadioSetting from './types/radio';
import SliderSetting from './types/slider';
import ColourSetting from './types/colour';
import FileSetting from './types/file';
import ArraySetting from './types/array';
import CustomSetting from './types/custom';
export default class Setting {
constructor(args) {
args = args.args || args;
if (args.type === 'color') args.type = 'colour';
if (args.type === 'bool') return new BoolSetting(args);
else if (args.type === 'text') return new StringSetting(args);
else if (args.type === 'number') return new NumberSetting(args);
else if (args.type === 'dropdown') return new DropdownSetting(args);
else if (args.type === 'radio') return new RadioSetting(args);
else if (args.type === 'slider') return new SliderSetting(args);
else if (args.type === 'colour') return new ColourSetting(args);
else if (args.type === 'file') return new FileSetting(args);
else if (args.type === 'array') return new ArraySetting(args);
else if (args.type === 'custom') return new CustomSetting(args);
else throw {message: `Setting type ${args.type} unknown`};
}
}

View File

@ -0,0 +1,211 @@
/**
* BetterDiscord Settings Category 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 Setting from './setting';
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
export default class SettingsCategory {
constructor(args, ...merge) {
this.emitter = new AsyncEventEmitter();
this.args = args.args || args;
this.args.settings = this.settings.map(setting => new Setting(setting));
for (let newCategory of merge) {
this._merge(newCategory);
}
for (let setting of this.settings) {
setting.on('setting-updated', ({ value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({
category: this, category_id: this.id,
setting, setting_id: setting.id,
value, old_value
})));
setting.on('settings-updated', ({ updatedSettings }) => this.emit('settings-updated', new SettingsUpdatedEvent({
updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
category: this, category_id: this.id
}, updatedSetting)))
})));
}
}
/**
* Category ID
*/
get id() {
return this.args.id || this.args.category;
}
get category() {
return this.id;
}
/**
* Category name
*/
get name() {
return this.args.category_name;
}
get category_name() {
return this.name;
}
/**
* Category type
* Currently either "drawer", "static", or undefined.
*/
get type() {
return this.args.type;
}
/**
* An array of settings in this category.
*/
get settings() {
return this.args.settings || [];
}
/**
* Whether any setting in this category has been changed.
*/
get changed() {
if (this.settings.find(setting => setting.changed)) return true;
return false;
}
/**
* Returns the first setting where calling {function} returns true.
* @param {Function} function A function to call to filter settings
* @return {Setting}
*/
find(f) {
return this.settings.find(f);
}
/**
* Returns all settings where calling {function} returns true.
* @param {Function} function A function to call to filter settings
* @return {Array} An array of matching Setting objects
*/
findSettings(f) {
return this.settings.filter(f);
}
/**
* Returns the setting with the ID {id}.
* @param {String} id The ID of the setting to look for
* @return {Setting}
*/
getSetting(id) {
return this.findSetting(setting => setting.id === id);
}
/**
* Merges a category into this category without emitting events (and therefore synchronously).
* This only exists for use by the constructor and SettingsSet.
*/
_merge(newCategory) {
let updatedSettings = [];
for (let newSetting of newCategory.settings) {
const setting = this.settings.find(setting => setting.id === newSetting.id);
if (!setting) {
Logger.warn('SettingsCategory', `Trying to merge setting ${this.id}/${newSetting.id}, which does not exist.`);
continue;
}
const updatedSetting = setting._merge(newSetting);
if (!updatedSetting) continue;
updatedSettings = updatedSettings.concat(updatedSetting.map(({ setting, value, old_value }) => ({
category: this, category_id: this.id,
setting, setting_id: setting.id,
value, old_value
})));
}
return updatedSettings;
}
/**
* Merges another category into this category.
* @param {SettingsCategory} newCategory The category to merge into this category
* @return {Promise}
*/
async merge(newCategory, emit_multi = true) {
let updatedSettings = [];
for (let newSetting of newCategory.settings) {
const setting = this.settings.find(setting => setting.id === newSetting.id);
if (!setting) {
Logger.warn('SettingsCategory', `Trying to merge setting ${this.id}/${newSetting.id}, which does not exist.`);
continue;
}
const updatedSetting = await setting.merge(newSetting, false);
if (!updatedSetting) continue;
updatedSettings = updatedSettings.concat(updatedSetting.map(({ setting, value, old_value }) => ({
category: this, category_id: this.id,
setting, setting_id: setting.id,
value, old_value
})));
}
if (emit_multi)
await this.emit('settings-updated', new SettingsUpdatedEvent({
updatedSettings
}));
return updatedSettings;
}
/**
* Marks all settings in this set as saved (not changed).
*/
setSaved() {
for (let setting of this.settings) {
setting.setSaved();
}
}
/**
* Returns an object that can be stored as JSON and later merged back into a category with settingscategory.merge.
* @return {Object}
*/
strip() {
return {
category: this.category,
settings: this.settings.map(setting => setting.strip())
};
}
/**
* Returns a copy of this category that can be changed and then merged back into a set with settingscategory.merge.
* @param {SettingsCategory} ...merge A set to merge into the new set
* @return {SettingsCategory}
*/
clone(...merge) {
return new SettingsCategory({
id: this.id,
category: this.id,
name: this.name,
category_name: this.category_name,
type: this.type,
settings: this.settings.map(setting => setting.clone())
}, ...merge);
}
on(...args) { return this.emitter.on(...args); }
off(...args) { return this.emitter.removeListener(...args); }
emit(...args) { return this.emitter.emit(...args); }
}

View File

@ -0,0 +1,77 @@
/**
* BetterDiscord Settings Scheme 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 { Utils, ClientLogger as Logger } from 'common';
export default class SettingsScheme {
constructor(args) {
this.args = args.args || args;
this.args.settings = this.settings.map(({ category, settings }) => ({
category, settings: settings.map(({ id, value }) => ({
id, value
}))
}));
Object.freeze(this);
}
get id() {
return this.args.id;
}
get icon_url() {
return this.args.icon_url;
}
get name() {
return this.args.name;
}
get hint() {
return this.args.hint;
}
get settings() {
return this.args.settings || [];
}
isActive(set) {
for (let schemeCategory of this.settings) {
const category = set.categories.find(c => c.category === schemeCategory.category);
if (!category) {
Logger.warn('SettingsScheme', `Category ${schemeCategory.category} does not exist`);
return false;
}
for (let schemeSetting of schemeCategory.settings) {
const setting = category.settings.find(s => s.id === schemeSetting.id);
if (!setting) {
Logger.warn('SettingsScheme', `Setting ${schemeCategory.category}/${schemeSetting.id} does not exist`);
return false;
}
if (!Utils.compare(setting.value, schemeSetting.value)) return false;
}
}
return true;
}
applyTo(set) {
return set.merge({ settings: this.settings });
}
clone() {
return new SettingsScheme(Utils.deepclone(this.args));
}
}

View File

@ -0,0 +1,316 @@
/**
* BetterDiscord Settings Set 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 SettingsCategory from './settingscategory';
import SettingsScheme from './settingsscheme';
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
import { Modals } from 'ui';
export default class SettingsSet {
constructor(args, ...merge) {
this.emitter = new AsyncEventEmitter();
this.args = args.args || args;
this.args.categories = this.categories.map(category => new SettingsCategory(category));
this.args.schemes = this.schemes.map(scheme => new SettingsScheme(scheme));
for (let newSet of merge) {
this._merge(newSet);
}
for (let category of this.categories) {
category.on('setting-updated', ({ setting, value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({
set: this, set_id: this.id,
category, category_id: category.id,
setting, setting_id: setting.id,
value, old_value
})));
category.on('settings-updated', ({ updatedSettings }) => this.emit('settings-updated', new SettingsUpdatedEvent({
updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
set: this, set_id: this.id
}, updatedSetting)))
})));
}
}
/**
* Set ID
*/
get id() {
return this.args.id;
}
/**
* Set name
*/
get text() {
return this.args.text;
}
/**
* Text to be displayed with the set.
*/
get headertext() {
return this.args.headertext || `${this.text} Settings`;
}
set headertext(headertext) {
this.args.headertext = headertext;
}
/**
* Whether this set should be displayed.
* Currently only used in the settings menu.
*/
get hidden() {
return this.args.hidden || false;
}
/**
* An array of SettingsCategory objects in this set.
*/
get categories() {
return this.args.categories || this.args.settings || [];
}
get settings() {
return this.categories;
}
/**
* An array of SettingsScheme objects that can be used in this set.
*/
get schemes() {
return this.args.schemes || [];
}
/**
* Whether any setting in this set has been changed.
*/
get changed() {
if (this.categories.find(category => category.changed)) return true;
return false;
}
/**
* Returns the first category where calling {function} returns true.
* @param {Function} function A function to call to filter categories
* @return {SettingsCategory}
*/
find(f) {
return this.categories.find(f);
}
/**
* Returns all categories where calling {function} returns true.
* @param {Function} function A function to call to filter categories
* @return {Array} An array of matching SettingsCategory objects
*/
findCategories(f) {
return this.categories.filter(f);
}
/**
* Returns the category with the ID {id}.
* @param {String} id The ID of the category to look for
* @return {SettingsCategory}
*/
getCategory(id) {
return this.find(category => category.id === id);
}
/**
* Returns the first setting where calling {function} returns true.
* @param {Function} function A function to call to filter settings
* @return {Setting}
*/
findSetting(f) {
for (let category of this.categories) {
const setting = category.findSetting(f);
if (setting) return setting;
}
}
/**
* Returns all settings where calling {function} returns true.
* @param {Function} function A function to call to filter settings
* @return {Array} An array of matching Setting objects
*/
findSettings(f) {
return this.findSettingsInCategory(() => true, f);
}
/**
* Returns the first setting where calling {function} returns true.
* @param {Function} categoryFunction A function to call to filter categories
* @param {Function} function A function to call to filter settings
* @return {Array} An array of matching Setting objects
*/
findSettingInCategory(cf, f) {
for (let category of this.categories.filter(cf)) {
const setting = category.find(f);
if (setting) return setting;
}
}
/**
* Returns all settings where calling {function} returns true.
* @param {Function} categoryFunction A function to call to filter categories
* @param {Function} function A function to call to filter settings
* @return {Array} An array of matching Setting objects
*/
findSettingsInCategory(cf, f) {
let settings = [];
for (let category of this.categories.filter(cf)) {
settings = settings.concat(category.findSettings(f));
}
return settings;
}
/**
* Returns the setting with the ID {id}.
* @param {String} categoryid The ID of the category to look in (optional)
* @param {String} id The ID of the setting to look for
* @return {Setting}
*/
getSetting(id, sid) {
if (sid) return this.findSettingInCategory(category => category.id === id, setting => setting.id === sid);
return this.findSetting(setting => setting.id === id);
}
/**
* Returns the value of the setting with the ID {id}.
* @param {String} categoryid The ID of the category to look in (optional)
* @param {String} id The ID of the setting to look for
* @return {Any}
*/
get(cid, sid) {
const setting = this.getSetting(cid, sid);
return setting ? setting.value : undefined;
}
/**
* Opens this set in a modal.
* @param {String} headertext Text to be displayed in the modal header
* @param {Object} options Additional options to pass to Modals.settings
* @return {Modal}
*/
showModal(headertext, options) {
return Modals.settings(this, headertext ? headertext : this.headertext, options);
}
/**
* Merges a set into this set without emitting events (and therefore synchronously).
* This only exists for use by the constructor.
*/
_merge(newSet, emit_multi = true) {
let updatedSettings = [];
// const categories = newSet instanceof Array ? newSet : newSet.settings;
const categories = newSet && newSet.args ? newSet.args.settings : newSet ? newSet.settings : newSet;
if (!categories) return [];
for (let newCategory of categories) {
const category = this.find(category => category.category === newCategory.category);
if (!category) {
Logger.warn('SettingsCategory', `Trying to merge category ${newCategory.id}, which does not exist.`);
continue;
}
const updatedSetting = category._merge(newCategory, false);
if (!updatedSetting) continue;
updatedSettings = updatedSettings.concat(updatedSetting.map(({ category, setting, value, old_value }) => ({
set: this, set_id: this.id,
category, category_id: category.id,
setting, setting_id: setting.id,
value, old_value
})));
}
return updatedSettings;
}
/**
* Merges another set into this set.
* @param {SettingsSet} newSet The set to merge into this set
* @return {Promise}
*/
async merge(newSet, emit_multi = true) {
let updatedSettings = [];
// const categories = newSet instanceof Array ? newSet : newSet.settings;
const categories = newSet && newSet.args ? newSet.args.settings : newSet ? newSet.settings : newSet;
if (!categories) return [];
for (let newCategory of categories) {
const category = this.find(category => category.category === newCategory.category);
if (!category) {
Logger.warn('SettingsCategory', `Trying to merge category ${newCategory.id}, which does not exist.`);
continue;
}
const updatedSetting = await category.merge(newCategory, false);
if (!updatedSetting) continue;
updatedSettings = updatedSettings.concat(updatedSetting.map(({ category, setting, value, old_value }) => ({
set: this, set_id: this.id,
category, category_id: category.id,
setting, setting_id: setting.id,
value, old_value
})));
}
if (emit_multi)
await this.emit('settings-updated', new SettingsUpdatedEvent({
updatedSettings
}));
return updatedSettings;
}
/**
* Marks all settings in this set as saved (not changed).
*/
setSaved() {
for (let category of this.categories) {
category.setSaved();
}
}
/**
* Returns an object that can be stored as JSON and later merged back into a set with settingsset.merge.
* @return {Object}
*/
strip() {
const stripped = {};
if (this.id) stripped.id = this.id;
stripped.settings = this.categories.map(category => category.strip());
return stripped;
}
/**
* Returns a copy of this set that can be changed and then merged back into a set with settingsset.merge.
* @param {SettingsSet} ...merge A set to merge into the new set
* @return {SettingsSet}
*/
clone(...merge) {
return new SettingsSet({
id: this.id,
text: this.text,
headertext: this.headertext,
settings: this.categories.map(category => category.clone()),
schemes: this.schemes.map(scheme => scheme.clone())
}, ...merge);
}
on(...args) { return this.emitter.on(...args); }
off(...args) { return this.emitter.removeListener(...args); }
emit(...args) { return this.emitter.emit(...args); }
}

View File

@ -0,0 +1,201 @@
/**
* BetterDiscord Array Setting 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 { ThemeManager } from 'modules';
import { Utils } from 'common';
import Setting from './basesetting';
import SettingsSet from '../settingsset';
import SettingsCategory from '../settingscategory';
import SettingsScheme from '../settingsscheme';
export default class ArraySetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
this.args.settings = this.settings.map(category => new SettingsCategory(category));
this.args.schemes = this.schemes.map(scheme => new SettingsScheme(scheme));
this.args.items = this.value ? this.value.map(item => this.createItem(item.args || item)) : [];
this.updateValue(false, false);
}
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return [];
}
/**
* An array of sets currently in this array setting.
*/
get items() {
return this.args.items || [];
}
set items(items) {
this.args.items = items ? items.map(item => this.createItem(item)) : [];
this.updateValue();
}
/**
* Whether the setting should take the full width of the settings panel.
* This is always false for array settings.
*/
get fullwidth() {
return false;
}
/**
* An array of SettingsCategory objects that each set in this setting should have.
*/
get categories() {
return this.args.categories || this.args.settings || [];
}
get settings() {
return this.categories;
}
/**
* An array of SettingsScheme objects that can be used in this array's sets.
*/
get schemes() {
return this.args.schemes || [];
}
/**
* Whether to display this array setting's sets inline instead of opening them in a modal.
*/
get inline() {
return this.args.inline || false;
}
/**
* Whether to allow opening this array setting's sets in a modal.
* This is always true when inline is false.
*/
get allow_external() {
return this.args.allow_external || !this.inline;
}
/**
* The minimum amount of sets the user may create.
* This only restricts deleting sets when there is less or equal sets than this, and does not ensure that this number of items actually exists.
*/
get min() {
return this.args.min || 0;
}
/**
* The maximum amount of sets the user may create.
*/
get max() {
return this.args.max || null;
}
/**
* Adds a new set to this array setting.
* This ignores the maximum value.
* @param {SettingsSet} item Values to merge into the new set (optional)
* @return {SettingsSet} The new set
*/
addItem(item) {
const newItem = this.createItem(item);
this.args.items.push(newItem);
this.updateValue();
return newItem;
}
/**
* Removes a set from this array setting.
* This ignores the minimum value.
* @param {SettingsSet} item The set to remove
*/
removeItem(item) {
this.args.items = this.items.filter(i => i !== item);
this.updateValue();
}
/**
* Creates a new set for this array setting.
* @param {SettingsSet} item Values to merge into the new set (optional)
* @return {SettingsSet} The new set
*/
createItem(item) {
const set = new SettingsSet({
settings: Utils.deepclone(this.settings),
schemes: this.schemes
}, item ? item.args || item : undefined);
// if (item) set.merge(item.args || item);
set.setSaved();
set.on('settings-updated', () => this.updateValue());
return set;
}
/**
* Sets the value of this setting.
* This is only intended for use by settings.
* @param {SettingsSetting} value The new value of this setting
* @param {Boolean} emit_multi Whether to emit a SettingsUpdatedEvent
* @param {Boolean} emit Whether to emit a SettingUpdatedEvent
* @return {Promise}
*/
setValue(value, emit_multi = true, emit = true) {
this.args.items = value ? value.map(item => this.createItem(item)) : [];
this.updateValue(emit_multi, emit);
}
/**
* Updates the value of this array setting.
* This only exists for use by array settings.
* @param {Boolean} emit_multi Whether to emit a SettingsUpdatedEvent
* @param {Boolean} emit Whether to emit a SettingUpdatedEvent
* @return {Promise}
*/
updateValue(emit_multi = true, emit = true) {
return this.__proto__.__proto__.setValue.call(this, this.items.map(item => {
if (!item) return;
item.setSaved();
return item.strip();
}), emit_multi, emit);
}
/**
* Sets the path of the plugin/theme this setting is part of.
* This is passed to this array setting's settings.
* @param {String} contentPath The plugin/theme's directory path
*/
setContentPath(contentPath) {
this.args.path = contentPath;
for (let category of this.categories) {
for (let setting of category.settings) {
setting.setContentPath(contentPath);
}
}
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {Promise}
*/
async toSCSS() {
const maps = [];
for (let item of this.items)
maps.push(await ThemeManager.getConfigAsSCSSMap(item));
// Final comma ensures the variable is a list
return maps.length ? maps.join(', ') + ',' : '()';
}
}

View File

@ -0,0 +1,236 @@
/**
* BetterDiscord Setting 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 { ThemeManager } from 'modules';
import { Utils, AsyncEventEmitter } from 'common';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
export default class Setting {
constructor(args, ...merge) {
this.args = args.args || args;
if (!this.args.hasOwnProperty('value'))
this.args.value = this.defaultValue;
if (!this.args.hasOwnProperty('saved_value'))
this.args.saved_value = this.args.value;
for (let newSetting of merge) {
this._merge(newSetting);
}
this.emitter = new AsyncEventEmitter();
this.changed = !Utils.compare(this.args.value, this.args.saved_value);
}
/**
* Setting ID
*/
get id() {
return this.args.id;
}
/**
* Setting type
* This defines how this class will be extended.
*/
get type() {
return this.args.type;
}
/**
* The current value.
*/
get value() {
return this.args.value;
}
set value(value) {
this.setValue(value);
}
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return undefined;
}
/**
* Setting name
*/
get text() {
return this.args.text;
}
/**
* Text to be displayed with the setting.
*/
get hint() {
return this.args.hint;
}
/**
* The path of the plugin/theme this setting is part of.
* Used by settings of type "array", "custom" and "file".
*/
get path() {
return this.args.path;
}
/**
* Whether the user should be able to change the value of the setting.
* This does not prevent the setting being changed by a plugin.
*/
get disabled() {
return this.args.disabled || false;
}
/**
* Whether the setting should take the full width of the settings panel.
* This is only customisable in some setting types.
*/
get fullwidth() {
return this.args.fullwidth || false;
}
/**
* Merges a setting into this setting without emitting events (and therefore synchronously).
* This only exists for use by the constructor and SettingsCategory.
*/
_merge(newSetting) {
const value = newSetting.args ? newSetting.args.value : newSetting.value;
const old_value = this.args.value;
if (Utils.compare(value, old_value)) return [];
this.args.value = value;
this.changed = !Utils.compare(this.args.value, this.args.saved_value);
return [{
setting: this, setting_id: this.id,
value, old_value
}];
}
/**
* Merges another setting into this setting.
* @param {SettingsSetting} newSetting The setting to merge into this setting
* @param {Boolean} emit_multi Whether to emit a SettingsUpdatedEvent
* @param {Boolean} emit Whether to emit a SettingUpdatedEvent
* @return {Promise}
*/
async merge(newSetting, emit_multi = true, emit = true) {
const value = newSetting.args ? newSetting.args.value : newSetting.value;
const old_value = this.args.value;
if (Utils.compare(value, old_value)) return [];
this.args.value = value;
this.changed = !Utils.compare(this.args.value, this.args.saved_value);
const updatedSetting = {
setting: this, setting_id: this.id,
value, old_value
};
if (emit)
await this.emit('setting-updated', new SettingUpdatedEvent(updatedSetting));
if (emit_multi)
await this.emit('settings-updated', new SettingsUpdatedEvent({
updatedSettings: [updatedSetting]
}));
return [updatedSetting];
}
/**
* Sets the value of this setting.
* This is only intended for use by settings.
* @param {Any} value The new value of this setting
* @param {Boolean} emit_multi Whether to emit a SettingsUpdatedEvent
* @param {Boolean} emit Whether to emit a SettingUpdatedEvent
* @return {Promise}
*/
setValue(value, emit_multi = true, emit = true) {
const old_value = this.args.value;
if (Utils.compare(value, old_value)) return [];
this.args.value = value;
this.changed = !Utils.compare(this.args.value, this.args.saved_value);
const updatedSetting = {
setting: this, setting_id: this.id,
value, old_value
};
if (emit)
this.emit('setting-updated', new SettingUpdatedEvent(updatedSetting));
if (emit_multi)
this.emit('settings-updated', new SettingsUpdatedEvent({
updatedSettings: [updatedSetting]
}));
return [updatedSetting];
}
/**
* Marks this setting as saved (not changed).
*/
setSaved() {
this.args.saved_value = this.args.value;
this.changed = false;
}
/**
* Sets the path of the plugin/theme this setting is part of.
* Used by settings of type "array", "custom" and "file".
* @param {String} contentPath The plugin/theme's directory path
*/
setContentPath(contentPath) {
this.args.path = contentPath;
}
/**
* Returns an object that can be stored as JSON and later merged back into a setting with setting.merge.
* @return {Object}
*/
strip() {
return {
id: this.id,
value: this.args.value
};
}
/**
* Returns a copy of this setting that can be changed and then merged back into a set with setting.merge.
* @param {Setting} ...merge A setting to merge into the new setting
* @return {Setting}
*/
clone(...merge) {
return new this.constructor(Utils.deepclone(this.args), ...merge);
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String|Promise}
*/
toSCSS() {
if (typeof this.value === 'boolean' || typeof this.value === 'number') {
return this.value;
}
if (typeof this.value === 'string') {
return ThemeManager.toSCSSString(this.value);
}
}
on(...args) { return this.emitter.on(...args); }
off(...args) { return this.emitter.removeListener(...args); }
emit(...args) { return this.emitter.emit(...args); }
}

View File

@ -0,0 +1,30 @@
/**
* BetterDiscord Boolean Setting 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 Setting from './basesetting';
export default class BoolSetting extends Setting {
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return false;
}
/**
* Whether the setting should take the full width of the settings panel.
* This is always false for boolean settings.
*/
get fullwidth() {
return false;
}
}

View File

@ -0,0 +1,30 @@
/**
* BetterDiscord Colour Setting 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 Setting from './basesetting';
export default class ColourSetting extends Setting {
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return 'rgba(0, 0, 0, 0)';
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String|Promise}
*/
toSCSS() {
return this.value;
}
}

View File

@ -0,0 +1,80 @@
/**
* BetterDiscord Custom Setting 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 Setting from './basesetting';
import SettingsCategory from '../settingscategory';
import SettingsScheme from '../settingsscheme';
import path from 'path';
export default class CustomSetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
if (this.args.class_file && this.path)
this.setClass(this.args.class_file, this.args.class);
}
/**
* The file to load the custom setting from.
*/
get file() {
return this.args.file;
}
/**
* The name of a function on the plugin's main object that will be called to get a Vue component or a HTML element.
*/
get function() {
return this.args.function;
}
/**
* The name of an export of {file}, or a Vue component.
*/
get component() {
return this.args.component;
}
/**
* Whether to show a debug view under the custom setting's component.
*/
get debug() {
return this.args.debug || false;
}
/**
* Sets the path of the plugin/theme this setting is part of.
* Used by settings of type "array", "custom" and "file".
* @param {String} contentPath The plugin/theme's directory path
*/
setContentPath(_path) {
this.args.path = _path;
if (this.args.class_file)
this.setClass(this.args.class_file, this.args.class);
}
/**
* Replaces the custom setting's prototype with a new one that extends CustomSetting.
* @param {String} classFile The path of a file relative to the plugin/theme's directory that will be required
* @param {String} classExport The name of a property of the file's exports that will be used (optional)
*/
setClass(class_file, class_export) {
const component = window.require(path.join(this.path, class_file));
const setting_class = class_export ? component[class_export](CustomSetting) : component.default ? component.default(CustomSetting) : component(CustomSetting);
if (!(setting_class.prototype instanceof CustomSetting))
throw {message: 'Custom setting class function returned a class that doesn\'t extend from CustomSetting.'};
this.__proto__ = setting_class.prototype;
}
}

View File

@ -0,0 +1,63 @@
/**
* BetterDiscord Dropdown Setting 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 Setting from './basesetting';
import MultipleChoiceOption from '../multiplechoiceoption';
export default class DropdownSetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
this.args.options = this.options.map(option => new MultipleChoiceOption(option));
}
/**
* The current value.
*/
get value() {
const selected = this.selected_option;
if (selected) return selected.value;
return this.args.value;
}
set value(value) {
const selected = this.options.find(option => option.value === value);
if (selected) this.setValue(selected.id);
else this.setValue(value);
}
/**
* An array of MultipleChoiceOption objects.
*/
get options() {
return this.args.options || [];
}
/**
* The currently selected option.
*/
get selected_option() {
return this.options.find(option => option.id === this.args.value);
}
set selected_option(selected_option) {
this.args.value = selected_option.id;
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String}
*/
toSCSS() {
return this.value;
}
}

View File

@ -0,0 +1,61 @@
/**
* BetterDiscord File Setting 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 { ThemeManager } from 'modules';
import { FileUtils, ClientIPC } from 'common';
import Setting from './basesetting';
import path from 'path';
export default class FileSetting extends Setting {
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return [];
}
/**
* An object that will be passed to electron.dialog.showOpenDialog.
*/
get dialogOptions() {
return this.args.dialogOptions || {};
}
/**
* Opens the file selection dialog and sets this file setting's value to an array of selected file paths.
* @return {Promise}
*/
async openDialog() {
if (this.disabled) return;
const filenames = await ClientIPC.send('bd-native-open', this.dialogOptions);
if (filenames)
this.value = filenames;
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String|Promise}
*/
async toSCSS() {
if (!this.value || !this.value.length) return '()';
const files = [];
for (let filepath of this.value) {
const buffer = await FileUtils.readFileBuffer(path.resolve(this.path, filepath));
const type = await FileUtils.getFileType(buffer);
files.push(`(data: ${ThemeManager.toSCSSString(buffer.toString('base64'))}, type: ${ThemeManager.toSCSSString(type.mime)}, url: ${ThemeManager.toSCSSString(await FileUtils.toDataURI(buffer, type.mime))})`);
}
return files.length ? files.join(', ') : '()';
}
}

View File

@ -0,0 +1,15 @@
/**
* BetterDiscord Number Setting 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 Setting from './basesetting';
export default class NumberSetting extends Setting {
}

View File

@ -0,0 +1,63 @@
/**
* BetterDiscord Radio Setting 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 Setting from './basesetting';
import MultipleChoiceOption from '../multiplechoiceoption';
export default class RadioSetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
this.args.options = this.options.map(option => new MultipleChoiceOption(option));
}
/**
* The current value.
*/
get value() {
const selected = this.selected_option;
if (selected) return selected.value;
return this.args.value;
}
set value(value) {
const selected = this.options.find(option => option.value === value);
if (selected) this.setValue(selected.id);
else this.setValue(value);
}
/**
* An array of MultipleChoiceOption objects.
*/
get options() {
return this.args.options || [];
}
/**
* The currently selected option.
*/
get selected_option() {
return this.options.find(option => option.id === this.args.value);
}
set selected_option(selected_option) {
this.args.value = selected_option.id;
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String}
*/
toSCSS() {
return this.value;
}
}

View File

@ -0,0 +1,57 @@
/**
* BetterDiscord Slider Setting 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 Setting from './basesetting';
export default class SliderSetting extends Setting {
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return this.min;
}
/**
* The smallest number the user may select.
*/
get min() {
return this.args.min || 0;
}
/**
* The largest number the user may select.
*/
get max() {
return this.args.max || null;
}
/**
* How much the user may change the value at once by moving the slider.
*/
get step() {
return this.args.step || 1;
}
/**
* A string that will be displayed with the value.
*/
get unit() {
return this.args.unit || '';
}
/**
* An object mapping points on the slider to labels.
*/
get points() {
return this.args.points;
}
}

View File

@ -0,0 +1,37 @@
/**
* BetterDiscord String Setting 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 Setting from './basesetting';
export default class StringSetting extends Setting {
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return '';
}
/**
* Whether the setting should take the full width of the settings panel.
* This is always true when multiline is true.
*/
get fullwidth() {
return this.args.fullwidth && !this.multiline;
}
/**
* Whether to display a multiline text area instead of a single line text input.
*/
get multiline() {
return this.args.multiline || false;
}
}

View File

@ -1 +1,2 @@
export * from './events/index';
export * from './settings/index';

View File

@ -23,10 +23,8 @@
.bd-hint {
flex: 1 1 auto;
color: #72767d;
font-size: 14px;
font-weight: 500;
margin-top: 5px;
margin-bottom: 0;
line-height: 20px;
border-bottom: 0px solid rgba(114, 118, 126, 0.1);
}

View File

@ -0,0 +1,15 @@
.bd-err {
color: $colerr;
}
.bd-p {
color: $coldimwhite;
font-size: 14px;
font-weight: 500;
margin: 10px 0;
}
.bd-hint {
@extend .bd-p;
color: #72767d;
}

View File

@ -10,3 +10,4 @@
@import './profilebadges.scss';
@import './discordoverrides.scss';
@import './helpers.scss';

View File

@ -21,12 +21,11 @@ export default class {
const defer = setInterval(() => {
if (!this.profilePopupModule) return;
clearInterval(defer);
this.profilePopupModule.open = Utils.overload(this.profilePopupModule.open, userid => {
Events.emit('ui-event', {
event: 'profile-popup-open',
data: { userid }
});
});
Utils.monkeyPatch(this.profilePopupModule, 'open', 'after', (data, userid) => Events.emit('ui-event', {
event: 'profile-popup-open',
data: { userid }
}));
}, 100);
}

View File

@ -9,7 +9,7 @@
*/
<template>
<div class="bd-settings" :class="{active: active, 'bd-settings-out': this.activeIndex === -1 && this.lastActiveIndex >= 0}" @keyup="close">
<div class="bd-settings" :class="{active: active, 'bd-settings-out': activeIndex === -1 && lastActiveIndex >= 0}" @keyup="close">
<SidebarView :contentVisible="this.activeIndex >= 0 || this.lastActiveIndex >= 0" :animating="this.animating" :class="{'bd-stop': !first}">
<Sidebar slot="sidebar">
<div class="bd-settings-x" @click="close">
@ -31,9 +31,9 @@
</div>
</div>
<ContentColumn slot="content">
<div v-for="set in settings" v-if="activeContent(set.id) || animatingContent(set.id)" :class="{active: activeContent(set.id), animating: animatingContent(set.id)}">
<div v-for="set in Settings.settings" v-if="!set.hidden && activeContent(set.id) || animatingContent(set.id)" :class="{active: activeContent(set.id), animating: animatingContent(set.id)}">
<SettingsWrapper :headertext="set.headertext">
<SettingsPanel :settings="set.settings" :schemes="set.schemes" :change="(c, s, v) => changeSetting(set.id, c, s, v)" />
<SettingsPanel :settings="set" :schemes="set.schemes" />
</SettingsWrapper>
</div>
<div v-if="activeContent('css') || animatingContent('css')" :class="{active: activeContent('css'), animating: animatingContent('css')}">
@ -77,7 +77,7 @@
lastActiveIndex: -1,
animating: false,
first: true,
settings: Settings.getSettings,
Settings,
timeout: null
}
},
@ -106,13 +106,11 @@
},
activeContent(s) {
const item = this.sidebarItems.find(item => item.contentid === s);
if (!item) return false;
return item.id === this.activeIndex;
return item && item.id === this.activeIndex;
},
animatingContent(s) {
const item = this.sidebarItems.find(item => item.contentid === s);
if (!item) return false;
return item.id === this.lastActiveIndex;
return item && item.id === this.lastActiveIndex;
},
closeContent() {
if (this.activeIndex >= 0) this.sidebarItems.find(item => item.id === this.activeIndex).active = false;
@ -127,10 +125,6 @@
this.timeout = null;
}, 400);
},
changeSetting(set_id, category_id, setting_id, value) {
Settings.setSetting(set_id, category_id, setting_id, value);
Settings.saveSettings();
},
openGithub() {
shell.openExternal('https://github.com/JsSucks/BetterDiscordApp');
},

View File

@ -11,58 +11,97 @@
<template>
<SettingsWrapper headertext="CSS Editor">
<div class="bd-css-editor">
<div v-if="CssEditor.error" class="bd-form-item">
<h5 style="margin-bottom: 10px;">Compiler error</h5>
<div class="bd-err bd-pre-wrap"><div class="bd-pre">{{ CssEditor.error.formatted }}</div></div>
<div class="bd-form-divider"></div>
</div>
<div class="bd-form-item">
<h5>Custom Editor</h5>
<div class="bd-form-warning">
<div class="bd-text">Custom Editor is not installed!</div>
<FormButton>Install</FormButton>
</div>
<span style="color: #FFF; font-size: 12px; font-weight: 700;">*This is displayed if editor is not installed</span>
<FormButton :onClick="openInternalEditor">Open</FormButton>
<FormButton v-if="internalEditorIsInstalled" :onClick="openInternalEditor">Open</FormButton>
<template v-else>
<div class="bd-form-warning">
<div class="bd-text">Custom Editor is not installed!</div>
<FormButton>Install</FormButton>
</div>
<span style="color: #fff; font-size: 12px; font-weight: 700;">* This is displayed if editor is not installed</span>
</template>
</div>
<div class="bd-form-divider"></div>
<Setting :setting="liveUpdateSetting" :change="enabled => liveUpdateSetting.value = enabled" />
<div class="bd-form-item">
<h5>System Editor</h5>
<FormButton>
Open
</FormButton>
<FormButton :onClick="openSystemEditor">Open</FormButton>
<p class="bd-hint">This will open {{ systemEditorPath }} in your system's default editor.</p>
</div>
<div class="bd-form-divider"></div>
<FormButton :onClick="() => {}">Enabled</FormButton>
<FormButton :disabled="true"><span>Disabled</span></FormButton>
<FormButton :loading="true" />
<div class="bd-form-item">
<h5 style="margin-bottom: 10px;">Settings</h5>
</div>
<SettingsPanel :settings="settingsset" />
<!-- <Setting :setting="liveUpdateSetting" />
<Setting :setting="watchFilesSetting" /> -->
<FormButton :onClick="recompile" :loading="compiling">Recompile</FormButton>
</div>
</SettingsWrapper>
</template>
<script>
// Imports
import { CssEditor } from 'modules';
import { Settings, CssEditor } from 'modules';
import { SettingsWrapper } from './';
import { FormButton } from '../common';
import SettingsPanel from './SettingsPanel.vue';
import Setting from './setting/Setting.vue';
export default {
components: {
SettingsWrapper,
SettingsPanel,
Setting,
FormButton
},
data() {
return {
liveUpdateSetting: {
id: "live-update",
type: "bool",
text: "Live Update",
hint: "Automatically update client css when saved.",
value: true
}
CssEditor
};
},
computed: {
error() {
return this.CssEditor.error;
},
compiling() {
return this.CssEditor.compiling;
},
systemEditorPath() {
return this.CssEditor.filePath;
},
liveUpdateSetting() {
return Settings.getSetting('css', 'default', 'live-update');
},
watchFilesSetting() {
return Settings.getSetting('css', 'default', 'watch-files');
},
settingsset() {
return Settings.css;
},
internalEditorIsInstalled() {
return true;
}
},
methods: {
openInternalEditor() {
CssEditor.show();
this.CssEditor.show();
},
openSystemEditor() {
this.CssEditor.openSystemEditor();
},
recompile() {
this.CssEditor.recompile();
}
}
}

View File

@ -21,7 +21,7 @@
</div>
</div>
<div class="bd-flex bd-flex-col bd-pluginsView">
<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)" />
</div>

View File

@ -13,7 +13,7 @@
<div class="bd-settings-schemes" v-if="schemes && schemes.length">
<div class="bd-settings-schemes-container">
<template v-for="scheme in schemes">
<div class="bd-settings-scheme" :class="{'bd-active': checkSchemeActive(scheme)}" @click="() => setActiveScheme(scheme)">
<div class="bd-settings-scheme" :class="{'bd-active': scheme.isActive(settings)}" @click="() => scheme.applyTo(settings)">
<div class="bd-settings-scheme-icon" :style="{'background-image': `url(&quot;${scheme.icon_url}&quot;)`}"></div>
<div class="bd-settings-scheme-name" v-if="scheme.name">{{ scheme.name }}</div>
<div class="bd-settings-scheme-hint" v-if="scheme.hint">{{ scheme.hint }}</div>
@ -23,22 +23,22 @@
</div>
<div class="bd-settings-categories">
<template v-for="category in settings">
<template v-for="category in settings.categories">
<div class="bd-settings-category">
<div v-if="category.category === 'default' || !category.type">
<Setting v-for="setting in category.settings" :key="setting.id" :setting="setting" :change="v => settingChange(category, setting, v)" />
<Setting v-for="setting in category.settings" :key="setting.id" :setting="setting" />
</div>
<div class="bd-settings-static" v-else-if="category.type === 'static'">
<div class="bd-form-header">
<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)" />
<Setting v-for="setting in category.settings" :key="setting.id" :setting="setting" />
</div>
<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)" />
<Setting v-for="setting in category.settings" :key="setting.id" :setting="setting" />
</Drawer>
<div v-else>
<Setting v-for="setting in category.settings" :key="setting.id" :setting="setting" :change="v => settingChange(category, setting, v)" />
<Setting v-for="setting in category.settings" :key="setting.id" :setting="setting" />
</div>
</div>
</template>
@ -53,48 +53,10 @@
import Drawer from '../common/Drawer.vue';
export default {
props: ['settings', 'schemes', 'change'],
props: ['settings', 'schemes'],
components: {
Setting,
Drawer
},
methods: {
checkSchemeActive(scheme) {
for (let schemeCategory of scheme.settings) {
const category = this.settings.find(c => c.category === schemeCategory.category);
if (!category) return false;
for (let schemeSetting of schemeCategory.settings) {
const setting = category.settings.find(s => s.id === schemeSetting.id);
if (!setting || !Utils.compare(setting.value, schemeSetting.value)) return false;
}
}
return true;
},
setActiveScheme(scheme) {
for (let schemeCategory of scheme.settings) {
const category = this.settings.find(c => c.category === schemeCategory.category);
if (!category) {
console.err(`Category ${schemeCategory.category} does not exist`);
continue;
}
for (let schemeSetting of schemeCategory.settings) {
const setting = category.settings.find(s => s.id === schemeSetting.id);
if (!setting) {
console.err(`Setting ${schemeCategory.category}/${schemeSetting.id} does not exist`);
continue;
}
this.change(category.category, setting.id, schemeSetting.value);
}
}
},
settingChange(category, setting, value) {
if (setting.disabled) return;
this.change(category.category, setting.id, value);
}
}
}
</script>

View File

@ -21,7 +21,7 @@
</div>
</div>
<div class="bd-flex bd-flex-col bd-themesView">
<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)" />
</div>

View File

@ -11,8 +11,8 @@
<template>
<div class="bd-settings-modal" :class="{'bd-edited': changed}">
<Modal :headerText="modal.headertext" :close="modal.close" :class="{'bd-modal-out': modal.closing}">
<SettingsPanel :settings="configCache" :schemes="modal.schemes" :change="settingChange" slot="body" class="bd-settings-modal-body" />
<div slot="footer" class="bd-footer-alert" :class="{'bd-active': changed, 'bd-warn': warnclose}" :style="{pointerEvents: changed ? 'all' : 'none'}">
<SettingsPanel :settings="settings" :schemes="modal.schemes" slot="body" class="bd-settings-modal-body" />
<div slot="footer" class="bd-footer-alert" :class="{'bd-active': changed || saving, 'bd-warn': warnclose}" :style="{pointerEvents: changed ? 'all' : 'none'}">
<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,15 +28,14 @@
import Vue from 'vue';
import { Modal } from '../../common';
import SettingsPanel from '../SettingsPanel.vue';
import { Utils } from 'common';
import { Utils, ClientLogger as Logger } from 'common';
export default {
props: ['modal'],
data() {
return {
changed: false,
warnclose: false,
configCache: [],
settings: null,
closing: false,
saving: false
}
@ -45,52 +44,30 @@
Modal,
SettingsPanel
},
computed: {
changed() {
return this.settings.categories.find(category => category.changed);
}
},
methods: {
checkForChanges() {
let changed = false;
for (let category of this.configCache) {
const cat = this.modal.settings.find(c => c.category === category.category);
for (let setting of category.settings) {
if (!Utils.compare(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.modal.saveSettings(this.configCache);
this.configCache = JSON.parse(JSON.stringify(this.modal.settings));
this.changed = false;
if (this.modal.saveSettings) await this.modal.saveSettings(this.settings);
else await this.modal.settings.merge(this.settings);
} catch (err) {
// TODO Display error that settings failed to save
console.log(err);
Logger.err('SettingsModal', ['Failed to save settings:', err]);
}
this.saving = false;
},
resetSettings() {
if (this.saving) return;
this.configCache = JSON.parse(JSON.stringify(this.modal.settings));
this.changed = false;
this.$forceUpdate();
this.cloneSettings();
},
cloneSettings() {
this.settings = this.modal.dont_clone ? this.modal.settings : this.modal.settings.clone();
}
},
created() {
@ -100,11 +77,13 @@
setTimeout(() => this.warnclose = false, 400);
throw {message: 'Settings have been changed'};
}
}
};
this.modal.settings.on('settings-updated', this.cloneSettings);
this.cloneSettings();
},
beforeMount() {
this.configCache = JSON.parse(JSON.stringify(this.modal.settings));
this.changed = this.checkForChanges();
destroyed() {
this.modal.settings.off('settings-updated', this.cloneSettings);
}
}
</script>

View File

@ -12,23 +12,23 @@
<div class="bd-form-settingsarray" :class="{'bd-form-settingsarray-inline': setting.inline}">
<div class="bd-title">
<h3>{{ setting.text }}</h3>
<button class="bd-button bd-button-primary" :class="{'bd-disabled': setting.disabled || setting.max && items.length >= setting.max}" @click="() => addItem(!setting.inline)">Add</button>
<button class="bd-button bd-button-primary" :class="{'bd-disabled': setting.disabled || setting.max && setting.items.length >= setting.max}" @click="() => addItem(!setting.inline)">Add</button>
</div>
<div class="bd-hint">{{ setting.hint }}</div>
<div class="bd-settingsarray-items">
<div class="bd-settingsarray-item" v-for="(item, index) in items">
<div class="bd-settingsarray-item" v-for="(item, index) in setting.items">
<div class="bd-settingsarray-item-marker">{{ index + 1 }}</div>
<SettingsPanel class="bd-settingsarray-item-contents" v-if="setting.inline" :settings="item.settings" :change="(c, s, v) => changeInItem(item, c, s, v)" />
<SettingsPanel class="bd-settingsarray-item-contents" v-if="setting.inline" :settings="item" />
<div class="bd-settingsarray-item-contents" v-else>
<div class="bd-settingsarray-item-hint">
<span v-if="item.settings[0] && item.settings[0].settings[0]">{{ item.settings[0].settings[0].text }}: {{ item.settings[0].settings[0].value }}</span><span v-if="item.settings[0] && item.settings[0].settings[1]">, {{ item.settings[0].settings[1].text }}: {{ item.settings[0].settings[1].value }}</span><span v-if="item.settings[0] && item.settings[0].settings[2] || item.settings[1] && item.settings[1].settings[0]">, ...</span>
<span v-if="getItemSettings(item)[0]">{{ getItemSettings(item)[0].text }}: {{ getItemSettings(item)[0].value }}</span><span v-if="getItemSettings(item)[1]">, {{ getItemSettings(item)[1].text }}: {{ getItemSettings(item)[1].value }}</span><span v-if="getItemSettings(item)[2]">, ...</span>
</div>
</div>
<div class="bd-settingsarray-item-controls">
<span class="bd-settingsarray-open" v-if="typeof setting.allow_external !== 'undefined' ? setting.allow_external || !setting.inline : true" @click="() => showModal(item, index)"><MiOpenInNew v-if="setting.inline" /><MiSettings v-else /></span>
<span class="bd-settingsarray-remove" :class="{'bd-disabled': setting.disabled || setting.min && items.length <= setting.min}" @click="() => removeItem(item)"><MiMinus /></span>
<span class="bd-settingsarray-open" v-if="setting.allow_external" @click="() => showModal(item, index)"><MiOpenInNew v-if="setting.inline" /><MiSettings v-else /></span>
<span class="bd-settingsarray-remove" :class="{'bd-disabled': setting.disabled || setting.min && setting.items.length <= setting.min}" @click="() => removeItem(item)"><MiMinus /></span>
</div>
</div>
</div>
@ -47,29 +47,15 @@
components: {
MiSettings, MiOpenInNew, MiMinus
},
data() {
return {
items: []
};
},
watch: {
setting(value) {
// this.setting was changed
this.reloadSettings();
}
},
methods: {
addItem(openModal) {
if (this.setting.disabled || this.setting.max && this.items.length >= this.setting.max) return;
const item = { settings: this.getItemSettings({}) };
if (openModal) this.showModal(item, this.items.length);
this.items.push(item);
this.update();
if (this.setting.disabled || this.setting.max && this.setting.items.length >= this.setting.max) return;
const item = this.setting.addItem();
if (openModal) this.showModal(item, this.setting.items.length);
},
removeItem(item) {
if (this.setting.disabled || this.setting.min && this.items.length <= this.setting.min) return;
this.items = this.items.filter(i => i !== item);
this.update();
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);
@ -82,50 +68,17 @@
setting.value = value;
setting.changed = !Utils.compare(setting.value, setting.old_value);
this.update();
},
update() {
this.change(this.items.map(item => ({
settings: item.settings ? item.settings.map(category => ({
category: category.category,
settings: category.settings.map(setting => ({
id: setting.id,
value: setting.value
}))
})) : []
})));
},
showModal(item, index) {
Modals.settings(this.setting.headertext ? this.setting.headertext.replace(/%n/, index + 1) : this.setting.text + ` #${index + 1}`, item.settings, this.setting.schemes, () => this.update());
Modals.settings(item, this.setting.headertext ? this.setting.headertext.replace(/%n/, index + 1) : this.setting.text + ` #${index + 1}`);
},
getItemSettings(item) {
const settings = JSON.parse(JSON.stringify(this.setting.settings));
const newSettings = item.settings || [];
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;
}
}
return settings;
},
reloadSettings() {
this.items = JSON.parse(JSON.stringify(this.setting.value)) || [];
this.items = this.items.map(item => ({ settings: this.getItemSettings(item) }));
return item.findSettings(() => true);
}
},
beforeCreate() {
// https://vuejs.org/v2/guide/components.html#Circular-References-Between-Components
this.$options.components.SettingsPanel = SettingsPanel;
},
beforeMount() {
this.reloadSettings();
}
}
</script>

View File

@ -26,11 +26,6 @@
props: ['setting', 'change'],
components: {
RadioGroup
},
methods: {
selectOption(option) {
this.change(option.id);
}
}
}
</script>

View File

@ -9,7 +9,7 @@
*/
<template>
<div class="bd-form-item" :class="{'bd-form-item-changed': changed, 'bd-disabled': disabled, 'bd-form-item-noheader': !setting.text, 'bd-form-item-fullwidth': setting.fullwidth}">
<div class="bd-form-item" :class="{'bd-form-item-changed': setting.changed, 'bd-disabled': disabled, 'bd-form-item-noheader': !setting.text, 'bd-form-item-fullwidth': setting.fullwidth}">
<BoolSetting v-if="setting.type === 'bool'" :setting="setting" :change="change"/>
<DropdownSetting v-if="setting.type === 'dropdown'" :setting="setting" :change="change"/>
<NumberSetting v-if="setting.type === 'number'" :setting="setting" :change="change"/>
@ -20,7 +20,7 @@
<FileSetting v-if="setting.type === 'file'" :setting="setting" :change="change"/>
<ArraySetting v-if="setting.type === 'array'" :setting="setting" :change="change" />
<CustomSetting v-if="setting.type === 'custom'" :setting="setting" :change="change" />
<ColourSetting v-if="setting.type === 'colour' || setting.type === 'color'" :setting="setting" :change="change"/>
<ColourSetting v-if="setting.type === 'colour'" :setting="setting" :change="change"/>
<div class="bd-form-divider"></div>
</div>
</template>
@ -40,8 +40,7 @@
export default {
props: [
'setting',
'change'
'setting'
],
components: {
BoolSetting,
@ -63,6 +62,12 @@
disabled() {
return this.setting.disabled || false;
}
},
methods: {
change(value) {
if (this.disabled) return;
this.setting.value = value;
}
}
}
</script>

View File

@ -11,13 +11,13 @@
<template>
<div class="bd-dropdown" :class="{'bd-active': active, 'bd-disabled': disabled}">
<div class="bd-dropdown-current" @click="() => active = !active && !disabled">
<span class="bd-dropdown-text">{{ getOptionText(selected) }}</span>
<span class="bd-dropdown-text">{{ getSelectedText() }}</span>
<span class="bd-dropdown-arrow-wrap">
<span class="bd-dropdown-arrow"></span>
</span>
</div>
<div class="bd-dropdown-options bd-flex bd-flex-col" ref="options" v-if="active">
<div class="bd-dropdown-option" v-for="option in options" :class="{'bd-dropdown-option-selected': selected === option.id}" @click="selectOption(option)">{{ option.text }}</div>
<div class="bd-dropdown-option" v-for="option in options" :class="{'bd-dropdown-option-selected': selected === option.value}" @click="change(option.value); active = false">{{ option.text }}</div>
</div>
</div>
</template>
@ -30,14 +30,9 @@
};
},
methods: {
getOptionText(value) {
let matching = this.options.filter(opt => opt.id === value);
if (matching.length == 0) return "";
else return matching[0].text;
},
selectOption(option) {
this.active = false;
this.change(option.id);
getSelectedText() {
const selected_option = this.options.find(option => option.value === this.selected);
return selected_option ? selected_option.text : this.selected;
}
},
mounted() {

View File

@ -16,8 +16,8 @@
<slot name="icon" />
</div>
<span class="bd-modal-headertext">{{headerText}}</span>
<div class="bd-modal-x" @click="close">
<MiClose size="18"/>
<div class="bd-modal-x" @click="e => close(e.shiftKey, e)">
<MiClose size="18" />
</div>
</div>
<div class="bd-modal-body">

View File

@ -10,7 +10,7 @@
<template>
<div class="bd-radio-group" :class="{'bd-disabled': disabled}">
<label class="bd-radio" v-for="option in options" :class="{'bd-radio-selected': selected === option.id}" @click="selectOption(option)">
<label class="bd-radio" v-for="option in options" :class="{'bd-radio-selected': selected === option.value}" @click="change(option.value)">
<div class="bd-radio-control-wrap">
<svg class="bd-radio-control" name="Checkmark" width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><polyline stroke="#3e82e5" stroke-width="2" points="3.5 9.5 7 13 15 5"></polyline></g></svg>
</div>
@ -20,11 +20,6 @@
</template>
<script>
export default {
props: ['options', 'selected', 'disabled', 'change'],
methods: {
selectOption(option) {
this.change(option.id);
}
}
props: ['options', 'selected', 'disabled', 'change']
}
</script>

View File

@ -84,7 +84,7 @@ class DOMObserver {
}
class DOM {
export default class DOM {
static get observer() {
return this._observer || (this._observer = new DOMObserver());
@ -116,33 +116,39 @@ class DOM {
return document.querySelector(e);
}
static getElements(e) {
return document.querySelectorAll(e);
}
static createElement(tag = 'div', className = null, id = null) {
return new BdNode(tag, className, id);
}
static deleteStyle(id) {
const exists = this.getElement(`bd-styles > #${id}`);
const exists = Array.from(this.bdStyles.children).find(e => e.id === id);
if (exists) exists.remove();
}
}
static injectStyle(css, id) {
this.deleteStyle(id);
this.bdStyles.append(this.createStyle(css, id));
const style = Array.from(this.bdStyles.children).find(e => e.id === id) || this.createElement('style', null, id).element;
style.textContent = css;
this.bdStyles.append(style);
}
static getStyleCss(id) {
const exists = this.getElement(`bd-styles > #${id}`);
const exists = this.bdStyles.children.find(e => e.id === id);
return exists ? exists.textContent : '';
}
static deleteTheme(id) {
const exists = this.getElement(`bd-themes > #${id}`);
const exists = Array.from(this.bdThemes.children).find(e => e.id === id);
if (exists) exists.remove();
}
static injectTheme(css, id) {
this.deleteTheme(id);
this.bdThemes.append(this.createStyle(css, id));
const style = Array.from(this.bdThemes.children).find(e => e.id === id) || this.createElement('style', null, id).element;
style.textContent = css;
this.bdThemes.append(style);
}
static createStyle(css, id) {
@ -153,5 +159,3 @@ class DOM {
return style;
}
}
export default DOM;

View File

@ -23,7 +23,10 @@ export default class {
template: '<custom-modal :modal="modal" />',
components: { 'custom-modal': component },
data() { return { modal }; },
created() { modal.vue = this; }
created() {
modal.vueInstance = this;
modal.vue = this.$children[0];
}
};
modal.closing = false;
modal.close = force => this.close(modal, force);
@ -38,10 +41,7 @@ export default class {
return new Promise(async (resolve, reject) => {
if (modal.beforeClose) {
try {
let beforeCloseResult = modal.beforeClose(force);
if (beforeCloseResult instanceof Promise)
beforeCloseResult = await beforeCloseResult;
const beforeCloseResult = await modal.beforeClose(force);
if (beforeCloseResult && !force) return reject(beforeCloseResult);
} catch (err) {
if (!force) return reject(err);
@ -73,17 +73,16 @@ export default class {
static confirm(title, text) {
const modal = { title, text };
const promise = new Promise((resolve, reject) => {
modal.promise = new Promise((resolve, reject) => {
modal.confirm = () => resolve(true);
modal.beforeClose = () => reject();
this.add(modal, ConfirmModal);
});
modal.promise = promise;
return modal;
}
static permissions(title, name, perms) {
const modal = { title,name, perms };
const modal = { title, name, perms };
modal.promise = new Promise((resolve, reject) => {
modal.confirm = () => resolve(true);
modal.beforeClose = () => reject();
@ -123,39 +122,22 @@ export default class {
}
}
static settings(headertext, settings, schemes, settingsUpdated, settingUpdated, saveSettings) {
return this.add({
headertext, settings, schemes,
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 settings(settingsset, headertext, options) {
return this.add(Object.assign({
headertext: headertext ? headertext : settingsset.headertext,
settings: settingsset,
schemes: settingsset.schemes
}, options), SettingsModal);
}
static internalSettings(set_id) {
const set = Settings.getSet(set_id);
if (!set) return;
return this.settings(set.headertext, set.settings, set.schemes, null, null, newSettings => Settings.mergeSettings(set.id, newSettings));
return this.settings(set, set.headertext);
}
static contentSettings(content) {
return this.settings(content.name + ' Settings', content.config, content.configSchemes, null, null, content.saveSettings.bind(content));
return this.settings(content.settings, content.name + ' Settings');
}
static get stack() {

View File

@ -40,7 +40,7 @@ export default class extends EventListener {
setTimeout(() => {
let hasBadges = false;
let root = document.querySelector('[class*=profileBadges]');
let root = document.querySelector('[class*="profileBadges"]');
if (root) {
hasBadges = true;
} else {

View File

@ -0,0 +1,42 @@
/**
* BetterDiscord Async EventEmitter
* 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 EventEmitter from 'events';
export default class AsyncEventEmitter extends EventEmitter {
emit(event, ...data) {
return new Promise(async (resolve, reject) => {
let listeners = this._events[event] || [];
listeners = Array.isArray(listeners) ? listeners : [listeners];
// Special treatment of internal newListener and removeListener events
if(event === 'newListener' || event === 'removeListener') {
data = [{
event: data,
fn: err => {
if (err) throw err;
}
}];
}
for (let listener of listeners) {
try {
await listener.call(this, ...data);
} catch (err) {
return reject(err);
}
}
resolve();
});
}
}

View File

@ -1,3 +1,4 @@
export { ClientIPC } from './bdipc';
export * from './utils';
export { ClientLogger } from './logger';
export { default as AsyncEventEmitter } from './async-eventemitter';

View File

@ -0,0 +1,162 @@
/**
* BetterDiscord Monkeypatch
* 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 { ClientLogger as Logger } from './logger';
export class PatchedFunction {
constructor(object, methodName, replaceOriginal = true) {
if (object[methodName].__monkeyPatch)
return object[methodName].__monkeyPatch;
this.object = object;
this.methodName = methodName;
this.patches = [];
this.originalMethod = object[methodName];
this.replaced = false;
const patchedFunction = this;
this.replace = function(...args) {
patchedFunction.call(this, arguments);
};
this.replace.__monkeyPatch = this;
if (replaceOriginal)
this.replaceOriginal();
}
addPatch(patch) {
if (!this.patches.includes(patch))
this.patches.push(patch);
}
removePatch(patch, restoreOriginal = true) {
let i = 0;
while (this.patches[i]) {
if (this.patches[i] !== patch) i++;
else this.patches.splice(i, 1);
}
if (!this.patches.length && restoreOriginal)
this.restoreOriginal();
}
replaceOriginal() {
if (this.replaced) return;
this.object[this.methodName] = Object.assign(this.replace, this.object[this.methodName]);
this.replaced = true;
}
restoreOriginal() {
if (!this.replaced) return;
this.object[this.methodName] = Object.assign(this.object[this.methodName], this.replace);
this.replaced = false;
}
call(_this, args) {
const data = {
this: _this,
arguments: args,
return: undefined,
originalMethod: this.originalMethod,
callOriginalMethod: () => {
Logger.log('MonkeyPatch', [`Calling original method`, this, data]);
data.return = this.originalMethod.apply(data.this, data.arguments);
}
};
// Work through the patches calling each patch's hooks as if each patch had overridden the previous patch
for (let patch of this.patches) {
let callOriginalMethod = data.callOriginalMethod;
data.callOriginalMethod = () => {
const patch_data = Object.assign({}, data, {
callOriginalMethod, patch
});
patch.call(patch_data);
data.arguments = patch_data.arguments;
data.return = patch_data.return;
};
}
data.callOriginalMethod();
return data.return;
}
}
export class Patch {
constructor(patchedFunction, options, f) {
this.patchedFunction = patchedFunction;
if (options instanceof Function) {
f = options;
options = {
instead: data => {
f.call(this, data, ...data.arguments);
}
};
} else if (options === 'before') {
options = {
before: data => {
f.call(this, data, ...data.arguments);
}
};
} else if (options === 'after') {
options = {
after: data => {
f.call(this, data, ...data.arguments);
}
};
}
this.before = options.before || undefined;
this.instead = options.instead || undefined;
this.after = options.after || undefined;
this.once = options.once || false;
this.silent = options.silent || false;
this.suppressErrors = typeof options.suppressErrors === 'boolean' ? options.suppressErrors : true;
}
call(data) {
if (this.once)
this.cancel();
this.callBefore(data);
this.callInstead(data);
this.callAfter(data);
}
callBefore(data) {
if (this.before)
this.callHook('before', this.before, data);
}
callInstead(data) {
if (this.instead)
this.callHook('instead', this.instead, data);
else data.callOriginalMethod();
}
callAfter(data) {
if (this.after)
this.callHook('after', this.after, data);
}
callHook(hook, f, data) {
try {
f.call(this, data, ...data.arguments);
} catch (err) {
Logger.log('MonkeyPatch', [`Error thrown in ${hook} hook of`, this, '- :', err]);
if (!this.suppressErrors) throw err;
}
}
cancel() {
this.patchedFunction.removePatch(this);
}
}

View File

@ -13,6 +13,7 @@ const
fs = require('fs'),
_ = require('lodash');
import { PatchedFunction, Patch } from './monkeypatch';
import { Vendor } from 'modules';
import filetype from 'file-type';
@ -25,6 +26,67 @@ export class Utils {
}
}
/**
* Monkey-patches an object's method.
*/
static monkeyPatch(object, methodName, options, f) {
const patchedFunction = new PatchedFunction(object, methodName);
const patch = new Patch(patchedFunction, options, f);
patchedFunction.addPatch(patch);
return patch;
}
/**
* Monkey-patches an object's method and returns a promise that will be resolved with the data object when the method is called.
* You will have to call data.callOriginalMethod() if it wants the original method to be called.
*/
static monkeyPatchOnce(object, methodName) {
return new Promise((resolve, reject) => {
this.monkeyPatch(object, methodName, data => {
data.patch.cancel();
resolve(data);
});
});
}
/**
* Monkeypatch function that is compatible with samogot's Lib Discord Internals.
* Don't use this for writing new plugins as it will eventually be removed!
*/
static compatibleMonkeyPatch(what, methodName, options) {
const { before, instead, after, once = false, silent = false } = options;
const cancelPatch = () => patch.cancel();
const compatible_function = _function => data => {
const compatible_data = {
thisObject: data.this,
methodArguments: data.arguments,
returnValue: data.return,
cancelPatch,
originalMethod: data.originalMethod,
callOriginalMethod: () => data.callOriginalMethod()
};
try {
_function(compatible_data);
data.arguments = compatible_data.methodArguments;
data.return = compatible_data.returnValue;
} catch (err) {
data.arguments = compatible_data.methodArguments;
data.return = compatible_data.returnValue;
throw err;
}
};
const patch = this.monkeyPatch(what, methodName, {
before: before ? compatible_function(before) : undefined,
instead: instead ? compatible_function(instead) : undefined,
after: after ? compatible_function(after) : undefined,
once
});
return cancelPatch;
}
static async tryParseJson(jsonString) {
try {
return JSON.parse(jsonString);
@ -67,6 +129,36 @@ export class Utils {
// value1 and value2 contain the same data
return true;
}
static deepclone(value) {
if (typeof value === 'object') {
if (value instanceof Array) return value.map(i => this.deepclone(i));
const clone = Object.assign({}, value);
for (let key in clone) {
clone[key] = this.deepclone(clone[key]);
}
return clone;
}
return value;
}
static deepfreeze(object) {
if (typeof object === 'object' && object !== null) {
const properties = Object.getOwnPropertyNames(object);
for (let property of properties) {
this.deepfreeze(object[property]);
}
Object.freeze(object);
}
return object;
}
}
export class FileUtils {

View File

@ -84,7 +84,7 @@ class Comms {
o.reply({ err });
return;
}
o.reply(result.css.toString());
o.reply(result);
});
});
}

View File

@ -25,7 +25,7 @@
<button @click="update">Update</button>
<div class="flex-spacer"></div>
<div id="chkboxLiveUpdate">
<input type="checkbox" @click="toggleLiveUpdate" :checked="liveUpdate" /><span>Live Update</span>
<label><input type="checkbox" @click="toggleLiveUpdate" v-model="liveUpdate" /><span>Live Update</span></label>
</div>
</div>
</div>
@ -130,8 +130,7 @@
return this.$refs.mycm;
}
},
mounted() {
this.codemirror.on('keyup', this.cmOnKeyUp);
created() {
BDIpc.on('set-scss', (_, data) => {
if (data.error) {
console.log(data.error);
@ -141,14 +140,24 @@
this.setScss(data.scss);
});
BDIpc.sendToDiscord('get-scss');
BDIpc.on('scss-error', (_, err) => {
this.error = err;
this.$forceUpdate();
if (err)
console.error('SCSS parse error:', err);
});
BDIpc.on('set-liveupdate', (e, liveUpdate) => this.liveUpdate = liveUpdate);
},
mounted() {
this.codemirror.on('keyup', this.cmOnKeyUp);
BDIpc.sendToDiscord('get-scss');
BDIpc.sendToDiscord('get-liveupdate');
},
watch: {
liveUpdate(liveUpdate) {
BDIpc.sendToDiscord('set-liveupdate', liveUpdate);
}
},
methods: {
save() {

View File

@ -38,11 +38,10 @@
width: 25px;
font-size: 12px;
font-weight: 600;
/*background: #263238;*/
background: #36393f;
color: #bac9d2;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
transition: background-color .2s ease;
transition: background-color .2s ease, color .2s ease;
cursor: default;
border: 0;
height: 25px;

View File

@ -3,7 +3,7 @@
background: #292b2f;
border-top: 1px solid hsla(218,5%,47%,.3);
color: #d84040;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
font-family: monospace;
white-space: pre-wrap;
font-size: 12px;
}
@ -30,7 +30,7 @@
background: #36393f;
color: #bac9d2;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
transition: background-color .2s ease;
transition: background-color .2s ease, color .2s ease;
cursor: pointer;
border: 0;
margin-right: 4px;
@ -47,6 +47,10 @@
line-height: 22px;
flex: 0 0 auto;
label {
cursor: pointer;
}
input[type="checkbox"] {
margin: 0 6px 0 0;
cursor: pointer;
@ -57,7 +61,6 @@
font-weight: 500;
color: #bac9d2;
font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
cursor: default;
}
}
}

1829
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,8 +26,9 @@
"electron": "^1.6.15",
"electron-rebuild": "^1.7.3",
"eslint": "^4.16.0",
"eslint-plugin-vue": "^4.2.0",
"eslint-plugin-vue": "^4.3.0",
"file-type": "^7.6.0",
"filewatcher": "^3.0.1",
"gulp": "^3.9.1",
"gulp-babel": "^7.0.0",
"gulp-plumber": "^1.2.0",

View File

@ -0,0 +1,6 @@
// Import this file in the custom CSS editor with
// @import '{path-to-betterdiscord}/tests/example-scss-file.scss';
// and when it is changed your custom CSS will be recompiled
.bd-settings {
height: 50%;
}

View File

@ -2,3 +2,20 @@ module.exports.default = {
template: "<div 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;\">Test custom setting {{ setting.id }}. This is from component.js in the plugin/theme's directory. (It can use functions.)</div>",
props: ['setting', 'change']
};
const component = {
template: "<div 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;\">Test custom setting {{ setting.id }}. Also in component.js. It extends the CustomSetting class. <button class=\"bd-button bd-button-primary\" style=\"display: inline-block; margin-left: 10px;\" @click=\"change(1)\">Set value to 1</button> <button class=\"bd-button bd-button-primary\" style=\"display: inline-block; margin-left: 10px;\" @click=\"change(2)\">Set value to 2</button></div>",
props: ['setting', 'change']
};
module.exports.CustomSetting = function (CustomSetting) {
return class extends CustomSetting {
get component() {
return component;
}
get debug() {
return true;
}
}
};

View File

@ -24,7 +24,7 @@
"text": "Test settings array",
"hint": "Just a test. Inline should be left as false here in most cases. (Only set it to true if there's only one setting otherwise it takes up too much space. Or you could put it in a drawer.)",
"inline": false,
"min": 1,
"min": 0,
"max": 5,
"settings": [
{
@ -237,6 +237,13 @@
"value": false,
"function": "getSettingsComponentHTMLElement",
"debug": true
},
{
"id": "custom-6",
"type": "custom",
"value": false,
"class_file": "component.js",
"class": "CustomSetting"
}
]
},

View File

@ -1,10 +1,16 @@
module.exports = (Plugin, Api, Vendor, Dependencies) => {
const { $, moment, _ } = Vendor;
const { Events, Logger } = Api;
const { Events, Logger, InternalSettings, CssUtils } = Api;
return class extends Plugin {
onStart() {
get api() {
return Api;
}
async onStart() {
await this.injectStyles();
Events.subscribe('TEST_EVENT', this.eventTest);
Logger.log('onStart');
@ -16,7 +22,24 @@ module.exports = (Plugin, Api, Vendor, Dependencies) => {
console.log('Received plugin settings update:', event);
});
Logger.log(`Internal setting "core/default/test-setting" value: ${Api.Settings.get('core', 'default', 'test-setting')}`);
this.settings.on('setting-updated', event => {
Logger.log(`Setting ${event.category.id}/${event.setting.id} changed from ${event.old_value} to ${event.value}:`, event);
});
// this.settings.categories.find(c => c.id === 'default').settings.find(s => s.id === 'default-5')
this.settings.getSetting('default', 'default-0').on('setting-updated', async event => {
Logger.log(`Some feature ${event.value ? 'enabled' : 'disabled'}`);
});
this.settings.on('settings-updated', async event => {
await this.injectStyles();
Logger.log('Settings updated:', event, 'Waiting before saving complete...');
await new Promise(resolve => setTimeout(resolve, 5000));
Logger.log('Done');
});
Logger.log(`Internal setting "core/default/test-setting" value: ${InternalSettings.get('core', 'default', 'test-setting')}`);
Events.subscribe('setting-updated', event => {
console.log('Received internal setting update:', event);
});
@ -26,7 +49,24 @@ module.exports = (Plugin, Api, Vendor, Dependencies) => {
return true;
}
async injectStyles() {
const scss = await CssUtils.getConfigAsSCSS() + `.layer-kosS71 .guilds-wrapper + * {
&::before {
content: 'Example plugin stuff (test radio setting #{$default-5} selected)';
display: block;
padding: 10px 40px;
color: #eee;
background-color: #202225;
text-align: center;
font-size: 14px;
}
}`;
Logger.log('Plugin SCSS:', scss);
await CssUtils.injectSass(scss);
}
onStop() {
CssUtils.deleteAllStyles();
Events.unsubscribeAll();
Logger.log('onStop');
console.log(this.showSettingsModal());