update to master
This commit is contained in:
commit
6917724700
|
@ -23,3 +23,4 @@ Installers/**/*/packages
|
|||
dist/
|
||||
user.config.json
|
||||
tests/data
|
||||
/tests/themes/SimplerFlat
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* BetterDiscord Autocomplete Component
|
||||
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
|
||||
* All rights reserved.
|
||||
* https://betterdiscord.net
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
<template>
|
||||
<div class="bd-autocomplete">
|
||||
<div class="bd-autocomplete-inner">
|
||||
<div class="bd-autocompleteRow">
|
||||
<div class="bd-autocompleteSelector">
|
||||
<div class="bd-autocompleteTitle">
|
||||
Emotes Matching:
|
||||
<strong>Kappa</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import EmoteModule from '../../../builtin/Emotemodule.js';
|
||||
export default {
|
||||
props: ['title', 'emotes'],
|
||||
beforeMount() {
|
||||
console.log(EmoteModule);
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<span class="edited" v-tooltip="ets">(edited)</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['ets']
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<span class="bd-emotewrapper" v-tooltip="name">
|
||||
<img class="bd-emote" :src="src" :alt="name"/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
favourite: false
|
||||
}
|
||||
},
|
||||
props: ['src', 'name'],
|
||||
methods: {
|
||||
},
|
||||
beforeMount() {
|
||||
// Check favourite state
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.bd-emotewrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.bd-emotewrapper img {
|
||||
max-height: 32px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* BetterDiscord Emote Module
|
||||
* 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 { FileUtils } from 'common';
|
||||
import { Events, Globals } from 'modules';
|
||||
import { DOM, VueInjector } from 'ui';
|
||||
import EmoteComponent from './EmoteComponent.vue';
|
||||
let emotes = null;
|
||||
|
||||
export default class {
|
||||
|
||||
static async observe() {
|
||||
const dataPath = Globals.getObject('paths').find(path => path.id === 'data').path;
|
||||
try {
|
||||
emotes = await FileUtils.readJsonFromFile(dataPath + '/emotes.json');
|
||||
Events.on('ui:mutable:.markup',
|
||||
markup => {
|
||||
if (!emotes) return;
|
||||
this.injectEmotes(markup);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
static injectEmotes(node) {
|
||||
if (!/:[\w]+:/gmi.test(node.textContent)) return node;
|
||||
const childNodes = [...node.childNodes];
|
||||
const newNode = document.createElement('div');
|
||||
newNode.className = node.className;
|
||||
newNode.classList.add('hasEmotes');
|
||||
|
||||
for (const [cni, cn] of childNodes.entries()) {
|
||||
if (cn.nodeType !== Node.TEXT_NODE) {
|
||||
newNode.appendChild(cn);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { nodeValue } = cn;
|
||||
const words = nodeValue.split(/([^\s]+)([\s]|$)/g);
|
||||
|
||||
if (!words.some(word => word.startsWith(':') && word.endsWith(':'))) {
|
||||
newNode.appendChild(cn);
|
||||
continue;
|
||||
}
|
||||
let text = null;
|
||||
for (const [wi, word] of words.entries()) {
|
||||
let isEmote = null;
|
||||
if (word.startsWith(':') && word.endsWith(':')) {
|
||||
isEmote = this.isEmote(word);
|
||||
}
|
||||
|
||||
if (isEmote) {
|
||||
if (text !== null) {
|
||||
newNode.appendChild(document.createTextNode(text));
|
||||
text = null;
|
||||
}
|
||||
|
||||
const emoteRoot = document.createElement('span');
|
||||
newNode.appendChild(emoteRoot);
|
||||
VueInjector.inject(
|
||||
emoteRoot,
|
||||
DOM.createElement('span'),
|
||||
{ EmoteComponent },
|
||||
`<EmoteComponent src="${isEmote.src}" name="${isEmote.name}"/>`,
|
||||
true
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (text === null) {
|
||||
text = word;
|
||||
} else {
|
||||
text += word;
|
||||
}
|
||||
|
||||
if (wi === words.length - 1) {
|
||||
newNode.appendChild(document.createTextNode(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
node.replaceWith(newNode);
|
||||
}
|
||||
|
||||
static isEmote(word) {
|
||||
if (!emotes) return null;
|
||||
const name = word.replace(/:/g, '');
|
||||
const emote = emotes.find(emote => emote.id === name);
|
||||
if (!emote) return null;
|
||||
let { id, value } = emote;
|
||||
if (value.id) value = value.id;
|
||||
const uri = emote.type === 2 ? 'https://cdn.betterttv.net/emote/:id/1x' : emote.type === 1 ? 'https://cdn.frankerfacez.com/emoticon/:id/1' : 'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0';
|
||||
return { name, src: uri.replace(':id', value) };
|
||||
}
|
||||
|
||||
static filterTest() {
|
||||
const re = new RegExp('Kappa', 'i');
|
||||
const filtered = emotes.filter(emote => re.test(emote.id));
|
||||
return filtered.slice(0, 10);
|
||||
}
|
||||
|
||||
static filter(regex, limit) {
|
||||
let index = 0;
|
||||
return emotes.filter(emote => {
|
||||
if (index >= limit) return false;
|
||||
if (regex.test(emote.id)) {
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default as EmoteModule } from './EmoteModule';
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -30,11 +30,18 @@
|
|||
"hint": "Disconnect from voice server when Discord closes",
|
||||
"value": false,
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"id": "menu-keybind",
|
||||
"type": "keybind",
|
||||
"text": "Menu keybind",
|
||||
"value": "mod+b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Advanced",
|
||||
"category": "advanced",
|
||||
"category_name": "Advanced",
|
||||
"type": "drawer",
|
||||
"settings": [
|
||||
{
|
||||
|
@ -44,6 +51,13 @@
|
|||
"hint": "BetterDiscord developer mode",
|
||||
"value": false,
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"id": "ignore-content-manager-errors",
|
||||
"type": "bool",
|
||||
"text": "Ignore content manager errors",
|
||||
"hint": "Only when starting Discord. It gets annoying when you're reloading Discord often and have plugins that are meant to fail.",
|
||||
"value": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -75,6 +89,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",
|
||||
|
|
|
@ -10,41 +10,58 @@
|
|||
|
||||
import { DOM, BdUI, Modals } from 'ui';
|
||||
import BdCss from './styles/index.scss';
|
||||
import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, DiscordApi } from 'modules';
|
||||
import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database, DiscordApi } from 'modules';
|
||||
import { ClientLogger as Logger, ClientIPC } from 'common';
|
||||
import { EmoteModule } from 'builtin';
|
||||
const ignoreExternal = false;
|
||||
|
||||
class BetterDiscord {
|
||||
|
||||
constructor() {
|
||||
window.discordApi = DiscordApi;
|
||||
window.bddb = Database;
|
||||
window.bdglobals = Globals;
|
||||
window.ClientIPC = ClientIPC;
|
||||
window.css = CssEditor;
|
||||
window.pm = PluginManager;
|
||||
window.tm = ThemeManager;
|
||||
window.events = Events;
|
||||
window.wpm = WebpackModules;
|
||||
window.bdsettings = Settings;
|
||||
window.bdmodals = Modals;
|
||||
window.bdlogs = Logger;
|
||||
window.emotes = EmoteModule;
|
||||
window.dom = DOM;
|
||||
|
||||
DOM.injectStyle(BdCss, 'bdmain');
|
||||
Events.on('global-ready', this.globalReady.bind(this));
|
||||
}
|
||||
|
||||
async init() {
|
||||
await Settings.loadSettings();
|
||||
await ModuleManager.initModules();
|
||||
await ExtModuleManager.loadAllModules(true);
|
||||
await PluginManager.loadAllPlugins(true);
|
||||
await ThemeManager.loadAllThemes(true);
|
||||
Modals.showContentManagerErrors();
|
||||
Events.emit('ready');
|
||||
Events.emit('discord-ready');
|
||||
try {
|
||||
await Database.init();
|
||||
await Settings.loadSettings();
|
||||
await ModuleManager.initModules();
|
||||
Modals.showContentManagerErrors();
|
||||
if (!ignoreExternal) {
|
||||
await ExtModuleManager.loadAllModules(true);
|
||||
await PluginManager.loadAllPlugins(true);
|
||||
await ThemeManager.loadAllThemes(true);
|
||||
}
|
||||
if (!Settings.get('core', 'advanced', 'ignore-content-manager-errors'))
|
||||
Modals.showContentManagerErrors();
|
||||
Events.emit('ready');
|
||||
Events.emit('discord-ready');
|
||||
EmoteModule.observe();
|
||||
} catch (err) {
|
||||
Logger.err('main', ['FAILED TO LOAD!', err]);
|
||||
}
|
||||
}
|
||||
|
||||
globalReady() {
|
||||
BdUI.initUiEvents();
|
||||
this.vueInstance = BdUI.injectUi();
|
||||
(async () => {
|
||||
this.init();
|
||||
})();
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/**
|
||||
* BetterDiscord Content Base
|
||||
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
|
||||
* All rights reserved.
|
||||
* https://betterdiscord.net
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { Utils, FileUtils, ClientLogger as Logger, AsyncEventEmitter } from 'common';
|
||||
import { Modals } from 'ui';
|
||||
import Database from './database';
|
||||
|
||||
export default class Content {
|
||||
|
||||
constructor(internals) {
|
||||
Utils.deepfreeze(internals.info);
|
||||
Object.freeze(internals.paths);
|
||||
|
||||
this.__internals = internals;
|
||||
|
||||
this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
|
||||
this.settings.on('settings-updated', event => this.events.emit('settings-updated', event));
|
||||
this.settings.on('settings-updated', event => this.__settingsUpdated(event));
|
||||
|
||||
// Add hooks
|
||||
if (this.onstart) this.on('start', event => this.onstart(event));
|
||||
if (this.onStart) this.on('start', event => this.onStart(event));
|
||||
if (this.onstop) this.on('stop', event => this.onstop(event));
|
||||
if (this.onStop) this.on('stop', event => this.onStop(event));
|
||||
if (this.onunload) this.on('unload', event => this.onunload(event));
|
||||
if (this.onUnload) this.on('unload', event => this.onUnload(event));
|
||||
if (this.settingUpdated) this.on('setting-updated', event => this.settingUpdated(event));
|
||||
if (this.settingsUpdated) this.on('settings-updated', event => this.settingsUpdated(event));
|
||||
}
|
||||
|
||||
get type() { return undefined }
|
||||
get configs() { return this.__internals.configs }
|
||||
get info() { return this.__internals.info }
|
||||
get paths() { return this.__internals.paths }
|
||||
get main() { return this.__internals.main }
|
||||
get defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
get configSchemes() { return this.configs.schemes }
|
||||
get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
|
||||
get name() { return this.info.name }
|
||||
get icon() { return this.info.icon }
|
||||
get description() { return this.info.description }
|
||||
get authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
get contentPath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return this.userConfig.enabled }
|
||||
get settings() { return this.userConfig.config }
|
||||
get config() { return this.settings.categories }
|
||||
get data() { return this.userConfig.data || (this.userConfig.data = {}) }
|
||||
get events() { return this.EventEmitter || (this.EventEmitter = new AsyncEventEmitter()) }
|
||||
|
||||
/**
|
||||
* Opens a settings modal for this content.
|
||||
*/
|
||||
showSettingsModal() {
|
||||
return Modals.contentSettings(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this content has any settings.
|
||||
*/
|
||||
get hasSettings() {
|
||||
return !!this.settings.findSetting(() => true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the content's current configuration.
|
||||
*/
|
||||
async saveConfiguration() {
|
||||
try {
|
||||
/*
|
||||
await FileUtils.writeFile(`${this.contentPath}/user.config.json`, JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
config: this.settings.strip().settings,
|
||||
data: this.data
|
||||
}));
|
||||
*/
|
||||
Database.insertOrUpdate({ type: 'contentconfig', $or: [{ id: this.id }, { name: this.name }] }, {
|
||||
type: 'contentconfig',
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
enabled: this.enabled,
|
||||
config: this.settings.strip().settings,
|
||||
data: this.data
|
||||
});
|
||||
this.settings.setSaved();
|
||||
} catch (err) {
|
||||
Logger.err(this.name, ['Failed to save configuration', err]);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when settings are updated.
|
||||
* This can be overridden by other content types.
|
||||
*/
|
||||
__settingsUpdated(event) {
|
||||
return this.saveConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the content.
|
||||
* @param {Boolean} save Whether to save the new enabled state
|
||||
* @return {Promise}
|
||||
*/
|
||||
async enable(save = true) {
|
||||
if (this.enabled) return;
|
||||
await this.emit('enable');
|
||||
await this.emit('start');
|
||||
|
||||
this.userConfig.enabled = true;
|
||||
if (save) await this.saveConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the content.
|
||||
* @param {Boolean} save Whether to save the new enabled state
|
||||
* @return {Promise}
|
||||
*/
|
||||
async disable(save = true) {
|
||||
if (!this.enabled) return;
|
||||
await this.emit('stop');
|
||||
await this.emit('disable');
|
||||
|
||||
this.userConfig.enabled = false;
|
||||
if (save) await this.saveConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener.
|
||||
* @param {String} event The event to add the listener to
|
||||
* @param {Function} callback The function to call when the event is emitted
|
||||
*/
|
||||
on(...args) {
|
||||
return this.events.on(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an event listener.
|
||||
* @param {String} event The event to remove the listener from
|
||||
* @param {Function} callback The bound callback (optional)
|
||||
*/
|
||||
off(...args) {
|
||||
return this.events.removeListener(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener that removes itself when called, therefore only being called once.
|
||||
* @param {String} event The event to add the listener to
|
||||
* @param {Function} callback The function to call when the event is emitted
|
||||
* @return {Promise|undefined}
|
||||
*/
|
||||
once(...args) {
|
||||
return this.events.once(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event.
|
||||
* @param {String} event The event to emit
|
||||
* @param {Any} data Data to be passed to listeners
|
||||
* @return {Promise|undefined}
|
||||
*/
|
||||
emit(...args) {
|
||||
return this.events.emit(...args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.freeze(Content.prototype);
|
|
@ -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
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
|
@ -8,15 +8,18 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Content from './content';
|
||||
import Globals from './globals';
|
||||
import { FileUtils, ClientLogger as Logger } from 'common';
|
||||
import path from 'path';
|
||||
import Database from './database';
|
||||
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
|
||||
import { Events } from 'modules';
|
||||
import { ErrorEvent } from 'structs';
|
||||
import { SettingsSet, ErrorEvent } from 'structs';
|
||||
import { Modals } from 'ui';
|
||||
import path from 'path';
|
||||
import Combokeys from 'combokeys';
|
||||
|
||||
/**
|
||||
* Base class for external content managing
|
||||
* Base class for managing external content
|
||||
*/
|
||||
export default class {
|
||||
|
||||
|
@ -54,6 +57,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) {
|
||||
|
@ -85,8 +92,9 @@ export default class {
|
|||
|
||||
/**
|
||||
* Refresh locally stored content
|
||||
* @param {bool} suppressErrors Suppress any errors that occur during loading of content
|
||||
*/
|
||||
static async refreshContent() {
|
||||
static async refreshContent(suppressErrors = false) {
|
||||
if (!this.localContent.length) return this.loadAllContent();
|
||||
|
||||
try {
|
||||
|
@ -97,23 +105,53 @@ 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);
|
||||
} catch (err) {
|
||||
//We don't want every plugin/theme to fail loading when one does
|
||||
// We don't want every plugin/theme to fail loading when one does
|
||||
this.errors.push(new ErrorEvent({
|
||||
module: this.moduleName,
|
||||
message: `Failed to load ${dir}`,
|
||||
err
|
||||
}));
|
||||
|
||||
Logger.err(this.moduleName, err);
|
||||
}
|
||||
}
|
||||
|
||||
for (let content of this.localContent) {
|
||||
if (directories.includes(content.dirName)) continue;
|
||||
//Plugin/theme was deleted manually, stop it and remove any reference
|
||||
this.unloadContent(content);
|
||||
|
||||
try {
|
||||
// Plugin/theme was deleted manually, stop it and remove any reference
|
||||
await this.unloadContent(content);
|
||||
} catch (err) {
|
||||
this.errors.push(new ErrorEvent({
|
||||
module: this.moduleName,
|
||||
message: `Failed to unload ${content.dirName}`,
|
||||
err
|
||||
}));
|
||||
|
||||
Logger.err(this.moduleName, err);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.errors.length && !suppressErrors) {
|
||||
Modals.error({
|
||||
header: `${this.moduleName} - ${this.errors.length} ${this.contentType}${this.errors.length !== 1 ? 's' : ''} failed to load`,
|
||||
module: this.moduleName,
|
||||
type: 'err',
|
||||
content: this.errors
|
||||
});
|
||||
this._errors = [];
|
||||
}
|
||||
|
||||
return this.localContent;
|
||||
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
|
@ -141,48 +179,61 @@ 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;
|
||||
userConfig.config = readConfig.defaultConfig.map(config => {
|
||||
const userSet = readUserConfig.config.find(c => c.category === config.category);
|
||||
// return userSet || config;
|
||||
if (!userSet) return config;
|
||||
|
||||
config.settings = config.settings.map(setting => {
|
||||
const userSetting = userSet.settings.find(s => s.id === setting.id);
|
||||
if (!userSetting) return setting;
|
||||
|
||||
setting.value = userSetting.value;
|
||||
return setting;
|
||||
});
|
||||
return config;
|
||||
});
|
||||
userConfig.css = readUserConfig.css || null;
|
||||
// userConfig.config = readUserConfig.config;
|
||||
//const readUserConfig = await this.readUserConfig(contentPath);
|
||||
const readUserConfig = await Database.find({ type: 'contentconfig', name: readConfig.info.name });
|
||||
if (readUserConfig.length) {
|
||||
userConfig.enabled = readUserConfig[0].enabled || false;
|
||||
// await userConfig.config.merge({ settings: readUserConfig.config });
|
||||
// userConfig.config.setSaved();
|
||||
// userConfig.config = userConfig.config.clone({ settings: readUserConfig.config });
|
||||
userConfig.config = readUserConfig[0].config;
|
||||
userConfig.data = readUserConfig[0].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)) {
|
||||
// This will load custom settings
|
||||
// Setting the content's path on only the live config (and not the default config) ensures that custom settings will not be loaded on the default settings
|
||||
setting.setContentPath(contentPath);
|
||||
}
|
||||
|
||||
Utils.deepfreeze(defaultConfig, object => object instanceof Combokeys);
|
||||
|
||||
const configs = {
|
||||
defaultConfig: readConfig.defaultConfig,
|
||||
defaultConfig,
|
||||
schemes: userConfig.schemes,
|
||||
userConfig
|
||||
}
|
||||
};
|
||||
|
||||
const paths = {
|
||||
contentPath,
|
||||
dirName,
|
||||
mainPath
|
||||
}
|
||||
};
|
||||
|
||||
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions);
|
||||
if (!content) return undefined;
|
||||
if (!reload && this.getContentById(content.id))
|
||||
throw {message: `A ${this.contentType} with the ID ${content.id} already exists.`};
|
||||
|
||||
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies);
|
||||
if (reload) this.localContent[index] = content;
|
||||
else this.localContent.push(content);
|
||||
return content;
|
||||
|
@ -192,6 +243,45 @@ export default class {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload content
|
||||
* @param {any} content Content to unload
|
||||
* @param {bool} reload Whether to reload the content after
|
||||
*/
|
||||
static async unloadContent(content, reload) {
|
||||
content = this.findContent(content);
|
||||
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
|
||||
|
||||
try {
|
||||
await content.disable(false);
|
||||
await content.emit('unload', reload);
|
||||
|
||||
const index = this.getContentIndex(content);
|
||||
|
||||
delete window.require.cache[window.require.resolve(content.paths.mainPath)];
|
||||
|
||||
if (reload) {
|
||||
const newcontent = await this.preloadContent(content.dirName, true, index);
|
||||
if (newcontent.enabled) {
|
||||
newcontent.userConfig.enabled = false;
|
||||
newcontent.start(false);
|
||||
}
|
||||
return newcontent;
|
||||
} else this.localContent.splice(index, 1);
|
||||
} catch (err) {
|
||||
Logger.err(this.moduleName, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload content
|
||||
* @param {any} content Content to reload
|
||||
*/
|
||||
static reloadContent(content) {
|
||||
return this.unloadContent(content, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read content config file
|
||||
* @param {any} configPath Config file path
|
||||
|
@ -211,25 +301,40 @@ export default class {
|
|||
}
|
||||
|
||||
/**
|
||||
* Wildcard content finder
|
||||
* @param {any} wild Content name | id | path | dirname
|
||||
* Checks if the passed object is an instance of this content type.
|
||||
* @param {any} content Object to check
|
||||
*/
|
||||
//TODO make this nicer
|
||||
static findContent(wild) {
|
||||
let content = this.getContentByName(wild);
|
||||
if (content) return content;
|
||||
content = this.getContentById(wild);
|
||||
if (content) return content;
|
||||
content = this.getContentByPath(wild);
|
||||
if (content) return content;
|
||||
return this.getContentByDirName(wild);
|
||||
static isThisContent(content) {
|
||||
return content instanceof Content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first content where calling {function} returns true.
|
||||
* @param {Function} function A function to call to filter content
|
||||
*/
|
||||
static find(f) {
|
||||
return this.localContent.find(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wildcard content finder
|
||||
* @param {any} wild Content ID / directory name / path / name
|
||||
* @param {bool} nonunique Allow searching attributes that may not be unique
|
||||
*/
|
||||
static findContent(wild, nonunique) {
|
||||
if (this.isThisContent(wild)) return wild;
|
||||
let content;
|
||||
content = this.getContentById(wild); if (content) return content;
|
||||
content = this.getContentByDirName(wild); if (content) return content;
|
||||
content = this.getContentByPath(wild); if (content) return content;
|
||||
content = this.getContentByName(wild); if (content && nonunique) return content;
|
||||
}
|
||||
|
||||
static getContentIndex(content) { return this.localContent.findIndex(c => c === content) }
|
||||
static getContentByName(name) { return this.localContent.find(c => c.name === name) }
|
||||
static getContentById(id) { return this.localContent.find(c => c.id === id) }
|
||||
static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
|
||||
static getContentByDirName(dirName) { return this.localContent.find(c => c.dirName === dirName) }
|
||||
static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
|
||||
static getContentByName(name) { return this.localContent.find(c => c.name === name) }
|
||||
|
||||
/**
|
||||
* Wait for content to load
|
||||
|
|
|
@ -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,41 +43,63 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update css in client
|
||||
* @param {String} scss scss to compile
|
||||
* @param {bool} sendSource send to css editor instance
|
||||
* @param {bool} sendSource send to css editor instance
|
||||
*/
|
||||
static updateScss(scss, sendSource) {
|
||||
async updateScss(scss, sendSource) {
|
||||
if (sendSource)
|
||||
this.sendToEditor('set-scss', { scss });
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
if (!scss) {
|
||||
this._scss = this.css = '';
|
||||
this.sendToEditor('scss-error', null);
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -71,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();
|
||||
}
|
||||
|
@ -80,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
|
||||
* @param {any} channel
|
||||
* 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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Current uncompiled scss
|
||||
* Opens an SCSS file in a system editor
|
||||
*/
|
||||
static get scss() {
|
||||
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
|
||||
*/
|
||||
get scss() {
|
||||
return this._scss || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* BetterDiscord Database Module
|
||||
* 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 { ClientIPC } from 'bdipc';
|
||||
|
||||
export default class {
|
||||
|
||||
static async init() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static async insertOrUpdate(args, data) {
|
||||
try {
|
||||
return ClientIPC.send('bd-dba', { action: 'update', args, data });
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static async find(args) {
|
||||
try {
|
||||
return ClientIPC.send('bd-dba', { action: 'find', args });
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,10 +11,9 @@
|
|||
import EventListener from './eventlistener';
|
||||
import { Utils } from 'common';
|
||||
import Events from './events';
|
||||
import WebpackModules from './webpackmodules';
|
||||
|
||||
import {
|
||||
MESSAGE_CREATE
|
||||
} from '../structs/socketstructs';
|
||||
import * as SocketStructs from '../structs/socketstructs';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -23,6 +22,11 @@ import {
|
|||
*/
|
||||
export default class extends EventListener {
|
||||
|
||||
init() {
|
||||
console.log(SocketStructs);
|
||||
this.hook();
|
||||
}
|
||||
|
||||
bindings() {
|
||||
this.hook = this.hook.bind(this);
|
||||
}
|
||||
|
@ -33,9 +37,19 @@ export default class extends EventListener {
|
|||
];
|
||||
}
|
||||
|
||||
hook() {}
|
||||
hook() {
|
||||
const self = this;
|
||||
const orig = this.eventsModule.prototype.emit;
|
||||
this.eventsModule.prototype.emit = function (...args) {
|
||||
orig.call(this, ...args);
|
||||
self.wsc = this;
|
||||
self.emit(...args);
|
||||
}
|
||||
}
|
||||
|
||||
get eventsModule() {}
|
||||
get eventsModule() {
|
||||
return WebpackModules.getModuleByPrototypes(['setMaxListeners', 'emit']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord emit overload
|
||||
|
@ -56,36 +70,14 @@ export default class extends EventListener {
|
|||
* @param {any} d Event Args
|
||||
*/
|
||||
dispatch(e, d) {
|
||||
|
||||
Events.emit('raw-event', { type: e, data: d });
|
||||
|
||||
switch (e) {
|
||||
case this.actions.READ:
|
||||
Events.emit('discord-ready');
|
||||
break;
|
||||
case this.actions.RESUMED:
|
||||
Events.emit('discord-resumed');
|
||||
break;
|
||||
case this.actions.TYPING_START:
|
||||
Events.emit('discord-event', {
|
||||
type: e,
|
||||
channelId: d.channel_id,
|
||||
userId: d.user_id
|
||||
});
|
||||
break;
|
||||
case this.actions.MESSAGE_CREATE:
|
||||
Events.emit('discord-event', { type: e, data: new MESSAGE_CREATE(d) });
|
||||
break;
|
||||
case 'k':
|
||||
Events.emit('discord-event', {
|
||||
|
||||
});
|
||||
break;
|
||||
case this.actions.ACTIVITY_START:
|
||||
Events.emit('discord-event', this.construct(e, d));
|
||||
break;
|
||||
|
||||
if (e === this.actions.READY || e === this.actions.RESUMED) {
|
||||
Events.emit(e, d);
|
||||
return;
|
||||
}
|
||||
if (!Object.keys(SocketStructs).includes(e)) return;
|
||||
const evt = new SocketStructs[e](d);
|
||||
Events.emit(`discord:${e}`, evt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* BetterDiscord Events Wrapper Module
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const eventemitters = new WeakMap();
|
||||
|
||||
export default class EventsWrapper {
|
||||
constructor(eventemitter) {
|
||||
eventemitters.set(this, 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
|
||||
});
|
||||
eventemitters.get(this).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;
|
||||
eventemitters.get(this).off(event, this.eventSubs[index].callback);
|
||||
this.eventSubs.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeAll() {
|
||||
for (let event of this.eventSubs) {
|
||||
eventemitters.get(this).off(event.event, event.callback);
|
||||
}
|
||||
this.eventSubs.splice(0, this.eventSubs.length);
|
||||
}
|
||||
}
|
|
@ -8,51 +8,15 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import Content from './content';
|
||||
|
||||
class ExtModuleEvents {
|
||||
constructor(extmodule) {
|
||||
this.extmodule = extmodule;
|
||||
this.emitter = new EventEmitter();
|
||||
}
|
||||
export default class ExtModule extends Content {
|
||||
|
||||
on(eventname, callback) {
|
||||
this.emitter.on(eventname, callback);
|
||||
}
|
||||
|
||||
off(eventname, callback) {
|
||||
this.emitter.removeListener(eventname, callback);
|
||||
}
|
||||
|
||||
emit(...args) {
|
||||
this.emitter.emit(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default class ExtModule {
|
||||
|
||||
constructor(pluginInternals) {
|
||||
this.__pluginInternals = pluginInternals;
|
||||
constructor(internals) {
|
||||
super(internals);
|
||||
this.__require = window.require(this.paths.mainPath);
|
||||
this.hasSettings = false;
|
||||
}
|
||||
|
||||
get type() { return 'module' }
|
||||
get configs() { return this.__pluginInternals.configs }
|
||||
get info() { return this.__pluginInternals.info }
|
||||
get icon() { return this.info.icon }
|
||||
get paths() { return this.__pluginInternals.paths }
|
||||
get main() { return this.__pluginInternals.main }
|
||||
get defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
get id() { return this.info.id || this.info.name.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
|
||||
get name() { return this.info.name }
|
||||
get authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
get pluginPath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return true }
|
||||
get config() { return this.userConfig.config || [] }
|
||||
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new ExtModuleEvents(this)) }
|
||||
|
||||
}
|
||||
|
|
|
@ -39,7 +39,19 @@ export default class extends ContentManager {
|
|||
|
||||
static get loadContent() { return this.loadModule }
|
||||
static async loadModule(paths, configs, info, main) {
|
||||
return new ExtModule({ configs, info, main, paths: { contentPath: paths.contentPath, dirName: paths.dirName, mainPath: paths.mainPath } });
|
||||
return new ExtModule({
|
||||
configs, info, main,
|
||||
paths: {
|
||||
contentPath: paths.contentPath,
|
||||
dirName: paths.dirName,
|
||||
mainPath: paths.mainPath
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static get isExtModule() { return this.isThisContent }
|
||||
static isThisContent(module) {
|
||||
return module instanceof ExtModule;
|
||||
}
|
||||
|
||||
static get findModule() { return this.findContent }
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
import { Events, SocketProxy, EventHook, CssEditor } from 'modules';
|
||||
import { ProfileBadges } from 'ui';
|
||||
import Updater from './updater';
|
||||
|
||||
export default class {
|
||||
|
||||
|
@ -20,7 +21,8 @@ export default class {
|
|||
new ProfileBadges(),
|
||||
new SocketProxy(),
|
||||
new EventHook(),
|
||||
CssEditor
|
||||
CssEditor,
|
||||
new Updater()
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,4 +11,7 @@ export { default as ModuleManager } from './modulemanager';
|
|||
export { default as EventListener } from './eventlistener';
|
||||
export { default as SocketProxy } from './socketproxy';
|
||||
export { default as EventHook } from './eventhook';
|
||||
export { default as Permissions } from './permissionmanager';
|
||||
export { default as Database } from './database';
|
||||
export { default as EventsWrapper } from './eventswrapper';
|
||||
export { default as DiscordApi } from './discordapi';
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* BetterDiscord Permission Manager
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const PermissionMap = {
|
||||
IDENTIFY: {
|
||||
HEADER: 'Access your account information',
|
||||
BODY: 'Allows :NAME: to read your account information (excluding user token).'
|
||||
},
|
||||
READ_MESSAGES: {
|
||||
HEADER: 'Read all messages',
|
||||
BODY: 'Allows :NAME: to read all messages accessible through your Discord account.'
|
||||
},
|
||||
SEND_MESSAGES: {
|
||||
HEADER: 'Send messages',
|
||||
BODY: 'Allows :NAME: to send messages on your behalf.'
|
||||
},
|
||||
DELETE_MESSAGES: {
|
||||
HEADER: 'Delete messages',
|
||||
BODY: 'Allows :NAME: to delete messages on your behalf.'
|
||||
},
|
||||
EDIT_MESSAGES: {
|
||||
HEADER: 'Edit messages',
|
||||
BODY: 'Allows :NAME: to edit messages on your behalf.'
|
||||
},
|
||||
JOIN_SERVERS: {
|
||||
HEADER: 'Join servers for you',
|
||||
BODY: 'Allows :NAME: to join servers on your behalf.'
|
||||
}
|
||||
}
|
||||
|
||||
export default class {
|
||||
|
||||
static permissionText(permission) {
|
||||
return PermissionMap[permission];
|
||||
}
|
||||
|
||||
}
|
|
@ -8,138 +8,22 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { FileUtils } from 'common';
|
||||
import { Modals } from 'ui';
|
||||
import { EventEmitter } from 'events';
|
||||
import ContentConfig from './contentconfig';
|
||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
||||
import PluginManager from './pluginmanager';
|
||||
import Content from './content';
|
||||
|
||||
class PluginEvents {
|
||||
constructor(plugin) {
|
||||
this.plugin = plugin;
|
||||
this.emitter = new EventEmitter();
|
||||
}
|
||||
|
||||
on(eventname, callback) {
|
||||
this.emitter.on(eventname, callback);
|
||||
}
|
||||
|
||||
off(eventname, callback) {
|
||||
this.emitter.removeListener(eventname, callback);
|
||||
}
|
||||
|
||||
emit(...args) {
|
||||
this.emitter.emit(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Plugin {
|
||||
|
||||
constructor(pluginInternals) {
|
||||
this.__pluginInternals = pluginInternals;
|
||||
this.saveSettings = this.saveSettings.bind(this);
|
||||
this.hasSettings = this.pluginConfig && this.pluginConfig.length > 0;
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
}
|
||||
export default class Plugin extends Content {
|
||||
|
||||
get type() { return 'plugin' }
|
||||
get configs() { return this.__pluginInternals.configs }
|
||||
get info() { return this.__pluginInternals.info }
|
||||
get icon() { return this.info.icon }
|
||||
get paths() { return this.__pluginInternals.paths }
|
||||
get main() { return this.__pluginInternals.main }
|
||||
get defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
get id() { return this.info.id || this.name.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-') }
|
||||
get name() { return this.info.name }
|
||||
get authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
get pluginPath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return this.userConfig.enabled }
|
||||
get config() { return this.userConfig.config || [] }
|
||||
|
||||
// Don't use - these will eventually be removed!
|
||||
get pluginPath() { return this.contentPath }
|
||||
get pluginConfig() { return this.config }
|
||||
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.pluginConfig) {
|
||||
if (category_id && category.category !== category_id) return;
|
||||
for (let setting of category.settings) {
|
||||
if (setting.id !== setting_id) return;
|
||||
return setting.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
get start() { return this.enable }
|
||||
get stop() { return this.disable }
|
||||
|
||||
showSettingsModal() {
|
||||
return Modals.pluginSettings(this);
|
||||
}
|
||||
|
||||
async saveSettings(newSettings) {
|
||||
const updatedSettings = [];
|
||||
|
||||
for (let newCategory of newSettings) {
|
||||
const category = this.pluginConfig.find(c => c.category === newCategory.category);
|
||||
for (let newSetting of newCategory.settings) {
|
||||
const setting = category.settings.find(s => s.id === newSetting.id);
|
||||
if (setting.value === newSetting.value) continue;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async saveConfiguration() {
|
||||
window.testConfig = new ContentConfig(this.pluginConfig);
|
||||
try {
|
||||
const config = new ContentConfig(this.pluginConfig).strip();
|
||||
await FileUtils.writeFile(`${this.pluginPath}/user.config.json`, JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
config
|
||||
}));
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.onstart && !this.onstart()) return false;
|
||||
if (this.onStart && !this.onStart()) return false;
|
||||
|
||||
if (!this.enabled) {
|
||||
this.userConfig.enabled = true;
|
||||
this.saveConfiguration();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.onstop && !this.onstop()) return false;
|
||||
if (this.onStop && !this.onStop()) return false;
|
||||
|
||||
this.userConfig.enabled = false;
|
||||
this.saveConfiguration();
|
||||
return true;
|
||||
unload() {
|
||||
PluginManager.unloadPlugin(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,101 +8,30 @@
|
|||
* 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 EventsWrapper from './eventswrapper';
|
||||
import WebpackModules from './webpackmodules';
|
||||
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
|
||||
import { BdMenuItems, Modals, DOM } from 'ui';
|
||||
import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue';
|
||||
|
||||
export default class PluginApi {
|
||||
|
||||
constructor(pluginInfo) {
|
||||
this.pluginInfo = pluginInfo;
|
||||
this.Events = new EventsWrapper(Events);
|
||||
this._menuItems = undefined;
|
||||
this._injectedStyles = undefined;
|
||||
this._modalStack = undefined;
|
||||
}
|
||||
|
||||
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),
|
||||
err: this.loggerErr.bind(this),
|
||||
warn: this.loggerWarn.bind(this),
|
||||
info: this.loggerInfo.bind(this),
|
||||
dbg: this.loggerDbg.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
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() {
|
||||
return {
|
||||
subscribe: this.eventSubscribe.bind(this),
|
||||
unsubscribe: this.eventUnsubscribe.bind(this),
|
||||
unsubscribeAll: this.eventUnsubscribeAll.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
getSetting(set, category, setting) {
|
||||
return Settings.get(set, category, setting);
|
||||
}
|
||||
get Settings() {
|
||||
return {
|
||||
get: this.getSetting.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
async getPlugin(plugin_id) {
|
||||
// This should require extra permissions
|
||||
return await PluginManager.waitForPlugin(plugin_id);
|
||||
}
|
||||
getPlugins(plugin_id) {
|
||||
return PluginManager.localContent.map(plugin => plugin.id);
|
||||
}
|
||||
get Plugins() {
|
||||
return {
|
||||
getPlugin: this.getPlugin.bind(this),
|
||||
getPlugins: this.getPlugins.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
async getTheme(theme_id) {
|
||||
// This should require extra permissions
|
||||
return await ThemeManager.waitForContent(theme_id);
|
||||
}
|
||||
getThemes(plugin_id) {
|
||||
return ThemeManager.localContent.map(theme => theme.id);
|
||||
}
|
||||
get Themes() {
|
||||
return {
|
||||
getTheme: this.getTheme.bind(this),
|
||||
getThemes: this.getThemes.bind(this)
|
||||
};
|
||||
get plugin() {
|
||||
return PluginManager.getPluginById(this.pluginInfo.id || this.pluginInfo.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-'));
|
||||
}
|
||||
|
||||
async bridge(plugin_id) {
|
||||
|
@ -117,4 +46,331 @@ export default class PluginApi {
|
|||
return null;
|
||||
}
|
||||
|
||||
get Api() { return this }
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
|
||||
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),
|
||||
err: this.loggerErr.bind(this),
|
||||
warn: this.loggerWarn.bind(this),
|
||||
info: this.loggerInfo.bind(this),
|
||||
dbg: this.loggerDbg.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
get Utils() {
|
||||
return {
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
|
||||
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 {
|
||||
createSet: this.createSettingsSet.bind(this),
|
||||
createCategory: this.createSettingsCategory.bind(this),
|
||||
createSetting: this.createSetting.bind(this),
|
||||
createScheme: this.createSettingsScheme.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* InternalSettings
|
||||
*/
|
||||
|
||||
getInternalSetting(set, category, setting) {
|
||||
return Settings.get(set, category, setting);
|
||||
}
|
||||
get InternalSettings() {
|
||||
return {
|
||||
get: this.getInternalSetting.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* BdMenu
|
||||
*/
|
||||
|
||||
get BdMenu() {
|
||||
return {
|
||||
BdMenuItems: this.BdMenuItems
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* BdMenuItems
|
||||
*/
|
||||
|
||||
get menuItems() {
|
||||
return this._menuItems || (this._menuItems = []);
|
||||
}
|
||||
addMenuItem(item) {
|
||||
return BdMenuItems.add(item);
|
||||
}
|
||||
addMenuSettingsSet(category, set, text) {
|
||||
const item = BdMenuItems.addSettingsSet(category, set, text);
|
||||
return this.menuItems.push(item);
|
||||
}
|
||||
addMenuVueComponent(category, text, component) {
|
||||
const item = BdMenuItems.addVueComponent(category, text, component);
|
||||
return this.menuItems.push(item);
|
||||
}
|
||||
removeMenuItem(item) {
|
||||
BdMenuItems.remove(item);
|
||||
Utils.removeFromArray(this.menuItems, item);
|
||||
}
|
||||
removeAllMenuItems() {
|
||||
for (let item of this.menuItems)
|
||||
BdMenuItems.remove(item);
|
||||
}
|
||||
get BdMenuItems() {
|
||||
return Object.defineProperty({
|
||||
add: this.addMenuItem.bind(this),
|
||||
addSettingsSet: this.addMenuSettingsSet.bind(this),
|
||||
addVueComponent: this.addMenuVueComponent.bind(this),
|
||||
remove: this.removeMenuItem.bind(this),
|
||||
removeAll: this.removeAllMenuItems.bind(this)
|
||||
}, 'items', {
|
||||
get: () => this.menuItems
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CssUtils
|
||||
*/
|
||||
|
||||
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.plugin.id}-${id}`;
|
||||
this.injectedStyles.push(id);
|
||||
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.plugin.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)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Modals
|
||||
*/
|
||||
|
||||
get modalStack() {
|
||||
return this._modalStack || (this._modalStack = []);
|
||||
}
|
||||
get baseModalComponent() {
|
||||
return Modals.baseComponent;
|
||||
}
|
||||
addModal(_modal, component) {
|
||||
const modal = Modals.add(_modal, component);
|
||||
modal.on('close', () => {
|
||||
let index;
|
||||
while ((index = this.modalStack.findIndex(m => m === modal)) > -1)
|
||||
this.modalStack.splice(index, 1);
|
||||
});
|
||||
this.modalStack.push(modal);
|
||||
return modal;
|
||||
}
|
||||
closeModal(modal, force) {
|
||||
return Modals.close(modal, force);
|
||||
}
|
||||
closeAllModals(force) {
|
||||
const promises = [];
|
||||
for (let modal of this.modalStack)
|
||||
promises.push(modal.close(force));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
closeLastModal(force) {
|
||||
if (!this.modalStack.length) return;
|
||||
return this.modalStack[this.modalStack.length - 1].close(force);
|
||||
}
|
||||
basicModal(title, text) {
|
||||
return this.addModal(Modals.basic(title, text));
|
||||
}
|
||||
settingsModal(settingsset, headertext, options) {
|
||||
return this.addModal(Modals.settings(settingsset, headertext, options));
|
||||
}
|
||||
get Modals() {
|
||||
return Object.defineProperties({
|
||||
add: this.addModal.bind(this),
|
||||
close: this.closeModal.bind(this),
|
||||
closeAll: this.closeAllModals.bind(this),
|
||||
closeLast: this.closeLastModal.bind(this),
|
||||
basic: this.basicModal.bind(this),
|
||||
settings: this.settingsModal.bind(this)
|
||||
}, {
|
||||
stack: {
|
||||
get: () => this.modalStack
|
||||
},
|
||||
baseComponent: {
|
||||
get: () => this.baseModalComponent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugins
|
||||
*/
|
||||
|
||||
async getPlugin(plugin_id) {
|
||||
// This should require extra permissions
|
||||
return await PluginManager.waitForPlugin(plugin_id);
|
||||
}
|
||||
listPlugins() {
|
||||
return PluginManager.localContent.map(plugin => plugin.id);
|
||||
}
|
||||
get Plugins() {
|
||||
return {
|
||||
getPlugin: this.getPlugin.bind(this),
|
||||
listPlugins: this.listPlugins.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Themes
|
||||
*/
|
||||
|
||||
async getTheme(theme_id) {
|
||||
// This should require extra permissions
|
||||
return await ThemeManager.waitForContent(theme_id);
|
||||
}
|
||||
listThemes() {
|
||||
return ThemeManager.localContent.map(theme => theme.id);
|
||||
}
|
||||
get Themes() {
|
||||
return {
|
||||
getTheme: this.getTheme.bind(this),
|
||||
listThemes: this.listThemes.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ExtModules
|
||||
*/
|
||||
|
||||
async getModule(module_id) {
|
||||
// This should require extra permissions
|
||||
return await ExtModuleManager.waitForContent(module_id);
|
||||
}
|
||||
listModules() {
|
||||
return ExtModuleManager.localContent.map(module => module.id);
|
||||
}
|
||||
get ExtModules() {
|
||||
return {
|
||||
getModule: this.getModule.bind(this),
|
||||
listModules: this.listModules.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* WebpackModules
|
||||
*/
|
||||
|
||||
get webpackRequire() {
|
||||
return WebpackModules.require;
|
||||
}
|
||||
getWebpackModule(filter, first = true) {
|
||||
return WebpackModules.getModule(filter, first);
|
||||
}
|
||||
getWebpackModuleByName(name, fallback) {
|
||||
return WebpackModules.getModuleByName(name, fallback);
|
||||
}
|
||||
getWebpackModuleByRegex(regex, first = true) {
|
||||
return WebpackModules.getModuleByRegex(regex, first);
|
||||
}
|
||||
getWebpackModuleByProperties(props, first = true) {
|
||||
return WebpackModules.getModuleByProps(props, first);
|
||||
}
|
||||
getWebpackModuleByPrototypeFields(props, first = true) {
|
||||
return WebpackModules.getModuleByPrototypes(props, first);
|
||||
}
|
||||
get WebpackModules() {
|
||||
return Object.defineProperty({
|
||||
getModule: this.getWebpackModule.bind(this),
|
||||
getModuleByName: this.getWebpackModuleByName.bind(this),
|
||||
getModuleByDisplayName: this.getWebpackModuleByName.bind(this),
|
||||
getModuleByRegex: this.getWebpackModuleByRegex.bind(this),
|
||||
getModuleByProperties: this.getWebpackModuleByProperties.bind(this),
|
||||
getModuleByPrototypeFields: this.getWebpackModuleByPrototypeFields.bind(this)
|
||||
}, 'require', {
|
||||
get: () => this.webpackRequire
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Stop plugins from modifying the plugin API for all plugins
|
||||
// Plugins can still modify their own plugin API object
|
||||
Object.freeze(PluginApi);
|
||||
Object.freeze(PluginApi.prototype);
|
||||
|
|
|
@ -14,7 +14,9 @@ import Plugin from './plugin';
|
|||
import PluginApi from './pluginapi';
|
||||
import Vendor from './vendor';
|
||||
import { ClientLogger as Logger } from 'common';
|
||||
import { Events } from 'modules';
|
||||
import { Events, Permissions } from 'modules';
|
||||
import { Modals } from 'ui';
|
||||
import { ErrorEvent } from 'structs';
|
||||
|
||||
export default class extends ContentManager {
|
||||
|
||||
|
@ -35,17 +37,53 @@ export default class extends ContentManager {
|
|||
}
|
||||
|
||||
static async loadAllPlugins(suppressErrors) {
|
||||
const loadAll = await this.loadAllContent(suppressErrors);
|
||||
this.localPlugins.forEach(plugin => {
|
||||
if (plugin.enabled) plugin.start();
|
||||
});
|
||||
this.loaded = false;
|
||||
const loadAll = await this.loadAllContent(true);
|
||||
this.loaded = true;
|
||||
for (let plugin of this.localPlugins) {
|
||||
if (!plugin.enabled) continue;
|
||||
plugin.userConfig.enabled = false;
|
||||
|
||||
try {
|
||||
plugin.start(false);
|
||||
} catch (err) {
|
||||
// Disable the plugin but don't save it - the next time BetterDiscord is started the plugin will attempt to start again
|
||||
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;
|
||||
}
|
||||
static get refreshPlugins() { return this.refreshContent }
|
||||
|
||||
static get loadContent() { return this.loadPlugin }
|
||||
static async loadPlugin(paths, configs, info, main, dependencies) {
|
||||
static async loadPlugin(paths, configs, info, main, dependencies, permissions) {
|
||||
if (permissions && permissions.length > 0) {
|
||||
for (let perm of permissions) {
|
||||
console.log(`Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
|
||||
}
|
||||
try {
|
||||
const allowed = await Modals.permissions(`${info.name} wants to:`, info.name, permissions).promise;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const deps = [];
|
||||
if (dependencies) {
|
||||
|
@ -61,36 +99,27 @@ export default class extends ContentManager {
|
|||
}
|
||||
|
||||
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor, deps);
|
||||
const instance = new plugin({ configs, info, main, paths: { contentPath: paths.contentPath, dirName: paths.dirName, mainPath: paths.mainPath } });
|
||||
if (!(plugin.prototype instanceof Plugin))
|
||||
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};
|
||||
|
||||
const instance = new plugin({
|
||||
configs, info, main,
|
||||
paths: {
|
||||
contentPath: paths.contentPath,
|
||||
dirName: paths.dirName,
|
||||
mainPath: paths.mainPath
|
||||
}
|
||||
});
|
||||
|
||||
if (instance.enabled && this.loaded) {
|
||||
instance.userConfig.enabled = false;
|
||||
instance.start(false);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
static get unloadContent() { return this.unloadPlugin }
|
||||
static async unloadPlugin(plugin) {
|
||||
try {
|
||||
if (plugin.enabled) plugin.stop();
|
||||
const { pluginPath } = plugin;
|
||||
const index = this.getPluginIndex(plugin);
|
||||
|
||||
delete window.require.cache[window.require.resolve(pluginPath)];
|
||||
this.localPlugins.splice(index, 1);
|
||||
} catch (err) {
|
||||
//This might fail but we don't have any other option at this point
|
||||
Logger.err('PluginManager', err);
|
||||
}
|
||||
}
|
||||
|
||||
static async reloadPlugin(plugin) {
|
||||
const _plugin = plugin instanceof Plugin ? plugin : this.findPlugin(plugin);
|
||||
if (!_plugin) throw { 'message': 'Attempted to reload a plugin that is not loaded?' };
|
||||
if (!_plugin.stop()) throw { 'message': 'Plugin failed to stop!' };
|
||||
const index = this.getPluginIndex(_plugin);
|
||||
const { pluginPath, dirName } = _plugin;
|
||||
|
||||
delete window.require.cache[window.require.resolve(pluginPath)];
|
||||
|
||||
return this.preloadContent(dirName, true, index);
|
||||
}
|
||||
static get unloadPlugin() { return this.unloadContent }
|
||||
static get reloadPlugin() { return this.reloadContent }
|
||||
|
||||
static stopPlugin(name) {
|
||||
const plugin = name instanceof Plugin ? name : this.getPluginByName(name);
|
||||
|
@ -112,6 +141,11 @@ export default class extends ContentManager {
|
|||
return true; //Return true anyways since plugin doesn't exist
|
||||
}
|
||||
|
||||
static get isPlugin() { return this.isThisContent }
|
||||
static isThisContent(plugin) {
|
||||
return plugin instanceof Plugin;
|
||||
}
|
||||
|
||||
static get findPlugin() { return this.findContent }
|
||||
static get getPluginIndex() { return this.getContentIndex }
|
||||
static get getPluginByName() { return this.getContentByName }
|
||||
|
|
|
@ -12,39 +12,47 @@ import defaultSettings from '../data/user.settings.default';
|
|||
import Globals from './globals';
|
||||
import CssEditor from './csseditor';
|
||||
import Events from './events';
|
||||
import { FileUtils, ClientLogger as Logger } from 'common';
|
||||
import { SettingUpdatedEvent } from 'structs';
|
||||
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
|
||||
import { SettingsSet, SettingUpdatedEvent } from 'structs';
|
||||
import path from 'path';
|
||||
|
||||
export default class {
|
||||
static async loadSettings() {
|
||||
export default new class Settings {
|
||||
constructor() {
|
||||
this.settings = defaultSettings.map(_set => {
|
||||
const set = new SettingsSet(_set);
|
||||
|
||||
set.on('setting-updated', event => {
|
||||
const { category, setting, value, old_value } = event;
|
||||
Logger.log('Settings', `${set.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`);
|
||||
Events.emit('setting-updated', event);
|
||||
Events.emit(`setting-updated-${set.id}_${category.id}_${setting.id}`, event);
|
||||
});
|
||||
|
||||
set.on('settings-updated', async (event) => {
|
||||
await this.saveSettings();
|
||||
Events.emit('settings-updated', event);
|
||||
});
|
||||
|
||||
return set;
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
for (let set of this.settings) {
|
||||
const newSet = settings.find(s => s.id === set.id);
|
||||
if (!newSet) continue;
|
||||
set.merge(newSet);
|
||||
set.setSaved();
|
||||
}
|
||||
|
||||
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 +61,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,71 +79,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 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 (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;
|
||||
get getSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,146 +8,96 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import Content from './content';
|
||||
import Settings from './settings';
|
||||
import ThemeManager from './thememanager';
|
||||
import { EventEmitter } from 'events';
|
||||
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
|
||||
import { DOM, Modals } from 'ui';
|
||||
import { FileUtils, ClientIPC } from 'common';
|
||||
import ContentConfig from './contentconfig';
|
||||
import { DOM } from 'ui';
|
||||
import { FileUtils, ClientIPC, ClientLogger as Logger } from 'common';
|
||||
import filewatcher from 'filewatcher';
|
||||
|
||||
class ThemeEvents {
|
||||
constructor(theme) {
|
||||
this.theme = theme;
|
||||
this.emitter = new EventEmitter();
|
||||
export default class Theme extends Content {
|
||||
|
||||
constructor(internals) {
|
||||
super(internals);
|
||||
|
||||
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 = [];
|
||||
});
|
||||
}
|
||||
|
||||
on(eventname, callback) {
|
||||
this.emitter.on(eventname, callback);
|
||||
}
|
||||
get type() { return 'theme' }
|
||||
get css() { return this.data.css }
|
||||
|
||||
off(eventname, callback) {
|
||||
this.emitter.removeListener(eventname, callback);
|
||||
}
|
||||
|
||||
emit(...args) {
|
||||
this.emitter.emit(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Theme {
|
||||
|
||||
constructor(themeInternals) {
|
||||
this.__themeInternals = themeInternals;
|
||||
this.hasSettings = this.themeConfig && this.themeConfig.length > 0;
|
||||
this.saveSettings = this.saveSettings.bind(this);
|
||||
this.enable = this.enable.bind(this);
|
||||
this.disable = this.disable.bind(this);
|
||||
}
|
||||
|
||||
get configs() { return this.__themeInternals.configs }
|
||||
get info() { return this.__themeInternals.info }
|
||||
get icon() { return this.info.icon }
|
||||
get paths() { return this.__themeInternals.paths }
|
||||
get main() { return this.__themeInternals.main }
|
||||
get defaultConfig() { return this.configs.defaultConfig }
|
||||
get userConfig() { return this.configs.userConfig }
|
||||
get id() { return this.info.id || this.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/\s+/g, '-') }
|
||||
get name() { return this.info.name }
|
||||
get authors() { return this.info.authors }
|
||||
get version() { return this.info.version }
|
||||
get themePath() { return this.paths.contentPath }
|
||||
get dirName() { return this.paths.dirName }
|
||||
get enabled() { return this.userConfig.enabled }
|
||||
get config() { return this.userConfig.config || [] }
|
||||
// Don't use - these will eventually be removed!
|
||||
get themePath() { return this.contentPath }
|
||||
get themeConfig() { return this.config }
|
||||
get css() { return this.userConfig.css }
|
||||
get events() { return this.EventEmitter ? this.EventEmitter : (this.EventEmitter = new ThemeEvents(this)) }
|
||||
|
||||
showSettingsModal() {
|
||||
return Modals.themeSettings(this);
|
||||
/**
|
||||
* Called when settings are updated.
|
||||
* This can be overridden by other content types.
|
||||
*/
|
||||
__settingsUpdated(event) {
|
||||
return this.recompile();
|
||||
}
|
||||
|
||||
async saveSettings(newSettings) {
|
||||
const updatedSettings = [];
|
||||
|
||||
for (let newCategory of newSettings) {
|
||||
const category = this.themeConfig.find(c => c.category === newCategory.category);
|
||||
for (let newSetting of newCategory.settings) {
|
||||
const setting = category.settings.find(s => s.id === newSetting.id);
|
||||
if (setting.value === newSetting.value) continue;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// As the theme's configuration has changed it needs recompiling
|
||||
// When the compiled CSS has been save 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.themeConfig).strip();
|
||||
await FileUtils.writeFile(`${this.themePath}/user.config.json`, JSON.stringify({
|
||||
enabled: this.enabled,
|
||||
config,
|
||||
css: this.css
|
||||
}));
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (!this.enabled) {
|
||||
this.userConfig.enabled = true;
|
||||
this.saveConfiguration();
|
||||
}
|
||||
/**
|
||||
* This is called when the theme is enabled.
|
||||
*/
|
||||
async onstart() {
|
||||
if (!this.css) await this.recompile();
|
||||
DOM.injectTheme(this.css, this.id);
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.userConfig.enabled = false;
|
||||
this.saveConfiguration();
|
||||
/**
|
||||
* This is called when the theme is disabled.
|
||||
*/
|
||||
onstop() {
|
||||
DOM.deleteTheme(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the theme and returns an object containing the CSS and an array of files that were included.
|
||||
* @return {Promise}
|
||||
*/
|
||||
async compile() {
|
||||
console.log('Compiling CSS');
|
||||
|
||||
let css = '';
|
||||
if (this.info.type === 'sass') {
|
||||
css = await ClientIPC.send('bd-compileSass', {
|
||||
data: ThemeManager.getConfigAsSCSS(this.themeConfig),
|
||||
const config = await ThemeManager.getConfigAsSCSS(this.settings);
|
||||
|
||||
const result = await ClientIPC.send('bd-compileSass', {
|
||||
data: config,
|
||||
path: this.paths.mainPath.replace(/\\/g, '/')
|
||||
});
|
||||
console.log(css);
|
||||
} else {
|
||||
css = await FileUtils.readFile(this.paths.mainPath);
|
||||
}
|
||||
|
||||
return css;
|
||||
Logger.log(this.name, ['Finished compiling theme', new class Info {
|
||||
get SCSS_variables() { console.log(config); }
|
||||
get Compiled_SCSS() { console.log(result.css.toString()); }
|
||||
get Result() { console.log(result); }
|
||||
}]);
|
||||
|
||||
return {
|
||||
css: result.css.toString(),
|
||||
files: result.stats.includedFiles
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
css: await FileUtils.readFile(this.paths.mainPath)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the theme and updates and saves the CSS and the list of include files.
|
||||
* @return {Promise}
|
||||
*/
|
||||
async recompile() {
|
||||
const 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();
|
||||
|
||||
|
@ -157,4 +107,67 @@ export default class Theme {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of files that are imported in the theme's SCSS.
|
||||
* @return {Array} Files being watched
|
||||
*/
|
||||
get files() {
|
||||
return this.data.files || (this.data.files = []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all files that are imported in the theme's SCSS.
|
||||
* @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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
|
||||
import ContentManager from './contentmanager';
|
||||
import Theme from './theme';
|
||||
import { FileUtils } from 'common';
|
||||
import path from 'path';
|
||||
|
||||
export default class ThemeManager extends ContentManager {
|
||||
|
||||
|
@ -29,9 +31,8 @@ export default class ThemeManager extends ContentManager {
|
|||
return 'themes';
|
||||
}
|
||||
|
||||
static get loadAllThemes() {
|
||||
return this.loadAllContent;
|
||||
}
|
||||
static get loadAllThemes() { return this.loadAllContent }
|
||||
static get refreshThemes() { return this.refreshContent }
|
||||
|
||||
static get loadContent() { return this.loadTheme }
|
||||
static async loadTheme(paths, configs, info, main) {
|
||||
|
@ -44,14 +45,22 @@ export default class ThemeManager extends ContentManager {
|
|||
mainPath: paths.mainPath
|
||||
}
|
||||
});
|
||||
if (!instance.css) instance.recompile();
|
||||
else if (instance.enabled) instance.enable();
|
||||
if (instance.enabled) {
|
||||
instance.userConfig.enabled = false;
|
||||
instance.enable();
|
||||
}
|
||||
return instance;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
static get unloadTheme() { return this.unloadContent }
|
||||
static async reloadTheme(theme) {
|
||||
theme = await this.reloadContent(theme);
|
||||
theme.recompile();
|
||||
}
|
||||
|
||||
static enableTheme(theme) {
|
||||
theme.enable();
|
||||
}
|
||||
|
@ -60,40 +69,48 @@ export default class ThemeManager extends ContentManager {
|
|||
theme.disable();
|
||||
}
|
||||
|
||||
static reloadTheme(theme) {
|
||||
theme.recompile();
|
||||
static get isTheme() { return this.isThisContent }
|
||||
static isThisContent(theme) {
|
||||
return theme instanceof Theme;
|
||||
}
|
||||
|
||||
static 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) {
|
||||
variables.push(this.parseSetting(setting));
|
||||
const setting_scss = await this.parseSetting(setting);
|
||||
if (setting_scss) variables.push(`$${setting_scss[0]}: ${setting_scss[1]};`);
|
||||
}
|
||||
}
|
||||
|
||||
return variables.join('\n');
|
||||
}
|
||||
|
||||
static parseSetting(setting) {
|
||||
static async getConfigAsSCSSMap(settingsset) {
|
||||
const variables = [];
|
||||
|
||||
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]})`);
|
||||
}
|
||||
}
|
||||
|
||||
return '(' + variables.join(', ') + ')';
|
||||
}
|
||||
|
||||
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 === 'slider') {
|
||||
return `$${name}: ${value * setting.multi || 1};`;
|
||||
}
|
||||
if (scss) return [name, scss];
|
||||
}
|
||||
|
||||
if (type === 'dropdown' || type === 'radio') {
|
||||
return `$${name}: ${setting.options.find(opt => opt.id === value).value};`;
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean' || typeof value === 'number') {
|
||||
return `$${name}: ${value};`;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return `$${name}: ${setting.scss_raw ? value : `'${setting.value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}'`};`;
|
||||
}
|
||||
static toSCSSString(value) {
|
||||
if (typeof value !== 'string' && value.toString) value = value.toString();
|
||||
return `'${typeof value === 'string' ? value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') : ''}'`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* BetterDiscord Updater Module
|
||||
* 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 Events from './events';
|
||||
import Globals from './globals';
|
||||
import { $ } from 'vendor';
|
||||
import { ClientLogger as Logger } from 'common';
|
||||
|
||||
export default class {
|
||||
|
||||
constructor() {
|
||||
window.updater = this;
|
||||
this.updatesAvailable = false;
|
||||
this.init = this.init.bind(this);
|
||||
this.checkForUpdates = this.checkForUpdates.bind(this);
|
||||
}
|
||||
|
||||
get interval() {
|
||||
return 60 * 1000 * 30;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.updateInterval = setInterval(this.checkForUpdates, this.interval);
|
||||
}
|
||||
|
||||
update() {
|
||||
// TODO
|
||||
this.updatesAvailable = false;
|
||||
Events.emit('update-check-end');
|
||||
}
|
||||
|
||||
checkForUpdates() {
|
||||
if (this.updatesAvailable) return;
|
||||
Events.emit('update-check-start');
|
||||
Logger.info('Updater', 'Checking for updates');
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: 'https://rawgit.com/JsSucks/BetterDiscordApp/master/package.json',
|
||||
cache: false,
|
||||
success: e => {
|
||||
try {
|
||||
Events.emit('update-check-end');
|
||||
Logger.info('Updater',
|
||||
`Latest Version: ${e.version} - Current Version: ${Globals.getObject('version')}`);
|
||||
if (e.version !== Globals.getObject('version')) {
|
||||
this.updatesAvailable = true;
|
||||
Events.emit('updates-available');
|
||||
}
|
||||
} catch (err) {
|
||||
Events.emit('update-check-fail', err);
|
||||
}
|
||||
},
|
||||
fail: e => Events.emit('update-check-fail', e)
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -12,7 +12,7 @@ import WebpackModules from './webpackmodules';
|
|||
import jQuery from 'jquery';
|
||||
import lodash from 'lodash';
|
||||
|
||||
export { jQuery, jQuery as $ };
|
||||
export { jQuery as $ };
|
||||
|
||||
export default class {
|
||||
|
||||
|
|
|
@ -86,7 +86,6 @@ const KnownModules = {
|
|||
UserActivityStore: Filters.byProperties(['getActivity']),
|
||||
UserNameResolver: Filters.byProperties(['getName']),
|
||||
|
||||
|
||||
/* Emoji Store and Utils */
|
||||
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
|
||||
EmojiUtils: Filters.byProperties(['diversitySurrogate']),
|
||||
|
@ -97,7 +96,6 @@ const KnownModules = {
|
|||
InviteResolver: Filters.byProperties(['findInvite']),
|
||||
InviteActions: Filters.byProperties(['acceptInvite']),
|
||||
|
||||
|
||||
/* Discord Objects & Utils */
|
||||
DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]),
|
||||
Permissions: Filters.byProperties(['getHighestRole']),
|
||||
|
@ -122,7 +120,6 @@ const KnownModules = {
|
|||
ExperimentsManager: Filters.byProperties(['isDeveloper']),
|
||||
CurrentExperiment: Filters.byProperties(['getExperimentId']),
|
||||
|
||||
|
||||
/* Images, Avatars and Utils */
|
||||
ImageResolver: Filters.byProperties(["getUserAvatarURL"]),
|
||||
ImageUtils: Filters.byProperties(['getSizedImageSrc']),
|
||||
|
@ -176,7 +173,6 @@ const KnownModules = {
|
|||
URLParser: Filters.byProperties(['Url', 'parse']),
|
||||
ExtraURLs: Filters.byProperties(['getArticleURL']),
|
||||
|
||||
|
||||
/* DOM/React Components */
|
||||
/* ==================== */
|
||||
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
|
||||
|
@ -201,60 +197,114 @@ const KnownModules = {
|
|||
ExternalLink: Filters.byCode(/\.trusted\b/)
|
||||
};
|
||||
|
||||
export default class {
|
||||
/* Synchronous */
|
||||
export default class WebpackModules {
|
||||
|
||||
/**
|
||||
* Finds a module using a filter function.
|
||||
* @param {Function} filter A function to use to filter modules
|
||||
* @param {Boolean} first Whether to return only the first matching module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModule(filter, first = true) {
|
||||
const modules = this.getAllModules();
|
||||
const rm = [];
|
||||
for (let index in modules) {
|
||||
if (!modules.hasOwnProperty(index)) continue;
|
||||
const module = modules[index];
|
||||
const { exports } = module;
|
||||
let foundModule = null;
|
||||
|
||||
if (!exports) continue;
|
||||
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
|
||||
if (filter(exports)) foundModule = exports;
|
||||
if (!foundModule) continue;
|
||||
if (first) return foundModule;
|
||||
rm.push(foundModule);
|
||||
}
|
||||
return first || rm.length == 0 ? undefined : rm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module by it's name.
|
||||
* @param {String} name The name of the module
|
||||
* @param {Function} fallback A function to use to filter modules if not finding a known module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByName(name, fallback) {
|
||||
if (Cache.hasOwnProperty(name)) return Cache[name];
|
||||
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
|
||||
if (!fallback) return null;
|
||||
return Cache[name] = this.getModule(fallback, true);
|
||||
if (!fallback) return undefined;
|
||||
const module = this.getModule(fallback, true);
|
||||
return module ? Cache[name] = module : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module by it's display name.
|
||||
* @param {String} name The display name of the module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByDisplayName(name) {
|
||||
return this.getModule(Filters.byDisplayName(name), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module using it's code.
|
||||
* @param {RegEx} regex A regular expression to use to filter modules
|
||||
* @param {Boolean} first Whether to return the only the first matching module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByRegex(regex, first = true) {
|
||||
return this.getModule(Filters.byCode(regex), first);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module using properties on it's prototype.
|
||||
* @param {Array} props Properties to use to filter modules
|
||||
* @param {Boolean} first Whether to return only the first matching module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByPrototypes(prototypes, first = true) {
|
||||
return this.getModule(Filters.byPrototypeFields(prototypes), first);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module using it's own properties.
|
||||
* @param {Array} props Properties to use to filter modules
|
||||
* @param {Boolean} first Whether to return only the first matching module
|
||||
* @return {Any}
|
||||
*/
|
||||
static getModuleByProps(props, first = true) {
|
||||
return this.getModule(Filters.byProperties(props), first);
|
||||
}
|
||||
|
||||
static getModule(filter, first = true) {
|
||||
const modules = this.getAllModules();
|
||||
const rm = [];
|
||||
for (let index in modules) {
|
||||
if (!modules.hasOwnProperty(index)) continue;
|
||||
const module = modules[index];
|
||||
const { exports } = module;
|
||||
let foundModule = null;
|
||||
|
||||
if (!exports) continue;
|
||||
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
|
||||
if (filter(exports)) foundModule = exports;
|
||||
if (!foundModule) continue;
|
||||
if (first) return foundModule;
|
||||
rm.push(foundModule);
|
||||
}
|
||||
return first || rm.length == 0 ? null : rm;
|
||||
}
|
||||
|
||||
static getAllModules() {
|
||||
/**
|
||||
* Discord's __webpack_require__ function.
|
||||
*/
|
||||
static get require() {
|
||||
if (this._require) return this._require;
|
||||
const id = 'bd-webpackmodules';
|
||||
const __webpack_require__ = window['webpackJsonp'](
|
||||
[],
|
||||
{
|
||||
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
|
||||
},
|
||||
[id]).default;
|
||||
const __webpack_require__ = window['webpackJsonp']([], {
|
||||
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
|
||||
}, [id]).default;
|
||||
delete __webpack_require__.m[id];
|
||||
delete __webpack_require__.c[id];
|
||||
return __webpack_require__.c;
|
||||
return this._require = __webpack_require__;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all loaded modules.
|
||||
* @return {Array}
|
||||
*/
|
||||
static getAllModules() {
|
||||
return this.require.c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of known modules.
|
||||
* @return {Array}
|
||||
*/
|
||||
static listKnownModules() {
|
||||
return Object.keys(KnownModules);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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';
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* 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 KeybindSetting from './types/keybind';
|
||||
import FileSetting from './types/file';
|
||||
import ArraySetting from './types/array';
|
||||
import CustomSetting from './types/custom';
|
||||
|
||||
export default class Setting {
|
||||
|
||||
constructor(args, ...merge) {
|
||||
args = args.args || args;
|
||||
|
||||
if (args.type === 'color') args.type = 'colour';
|
||||
|
||||
if (args.type === 'bool') return new BoolSetting(args, ...merge);
|
||||
else if (args.type === 'text') return new StringSetting(args, ...merge);
|
||||
else if (args.type === 'number') return new NumberSetting(args, ...merge);
|
||||
else if (args.type === 'dropdown') return new DropdownSetting(args, ...merge);
|
||||
else if (args.type === 'radio') return new RadioSetting(args, ...merge);
|
||||
else if (args.type === 'slider') return new SliderSetting(args, ...merge);
|
||||
else if (args.type === 'colour') return new ColourSetting(args, ...merge);
|
||||
else if (args.type === 'keybind') return new KeybindSetting(args, ...merge);
|
||||
else if (args.type === 'file') return new FileSetting(args, ...merge);
|
||||
else if (args.type === 'array') return new ArraySetting(args, ...merge);
|
||||
else if (args.type === 'custom') return new CustomSetting(args, ...merge);
|
||||
else throw {message: `Setting type ${args.type} unknown`};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
/**
|
||||
* 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 BaseSetting from './types/basesetting';
|
||||
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);
|
||||
}
|
||||
|
||||
this.__settingUpdated = this.__settingUpdated.bind(this);
|
||||
this.__settingsUpdated = this.__settingsUpdated.bind(this);
|
||||
|
||||
for (let setting of this.settings) {
|
||||
setting.on('setting-updated', this.__settingUpdated);
|
||||
setting.on('settings-updated', this.__settingsUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Category ID
|
||||
*/
|
||||
get id() {
|
||||
return this.args.id || this.args.category;
|
||||
}
|
||||
|
||||
get category() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Category name
|
||||
*/
|
||||
get name() {
|
||||
return this.args.name || 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting event listeners.
|
||||
* This only exists for use by the constructor and settingscategory.addSetting.
|
||||
*/
|
||||
__settingUpdated({ setting, value, old_value }) {
|
||||
return this.emit('setting-updated', new SettingUpdatedEvent({
|
||||
category: this, category_id: this.id,
|
||||
setting, setting_id: setting.id,
|
||||
value, old_value
|
||||
}));
|
||||
}
|
||||
|
||||
__settingsUpdated({ updatedSettings }) {
|
||||
return this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||
updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
|
||||
category: this, category_id: this.id
|
||||
}, updatedSetting)))
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically adds a setting to this category.
|
||||
* @param {Setting} category The setting to add to this category
|
||||
* @param {Number} index The index to add the setting at (optional)
|
||||
* @return {Promise}
|
||||
*/
|
||||
async addSetting(setting, index) {
|
||||
if (this.settings.find(s => s === setting)) return;
|
||||
|
||||
if (!(setting instanceof BaseSetting))
|
||||
setting = new Setting(setting);
|
||||
|
||||
if (this.getSetting(setting.id))
|
||||
throw {message: 'A setting with this ID already exists.'};
|
||||
|
||||
setting.on('setting-updated', this.__settingUpdated);
|
||||
setting.on('settings-updated', this.__settingsUpdated);
|
||||
|
||||
if (index === undefined) index = this.settings.length;
|
||||
this.settings.splice(index, 0, setting);
|
||||
|
||||
const event = {
|
||||
category: this, category_id: this.id,
|
||||
setting, setting_id: setting.id,
|
||||
at_index: index
|
||||
};
|
||||
|
||||
await setting.emit('added-to', event);
|
||||
await this.emit('added-setting', event);
|
||||
return setting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically removes a setting from this category.
|
||||
* @param {Setting} setting The setting to remove from this category
|
||||
* @return {Promise}
|
||||
*/
|
||||
async removeSetting(setting) {
|
||||
setting.off('setting-updated', this.__settingUpdated);
|
||||
setting.off('settings-updated', this.__settingsUpdated);
|
||||
|
||||
let index;
|
||||
while ((index = this.settings.findIndex(s => s === setting)) > -1) {
|
||||
this.settings.splice(index, 0);
|
||||
}
|
||||
|
||||
const event = {
|
||||
set: this, set_id: this.id,
|
||||
category: this, category_id: this.id,
|
||||
from_index: index
|
||||
};
|
||||
|
||||
await setting.emit('removed-from', event);
|
||||
await this.emit('removed-category', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.find(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); }
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,457 @@
|
|||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
this.__settingUpdated = this.__settingUpdated.bind(this);
|
||||
this.__settingsUpdated = this.__settingsUpdated.bind(this);
|
||||
this.__addedSetting = this.__addedSetting.bind(this);
|
||||
this.__removedSetting = this.__removedSetting.bind(this);
|
||||
|
||||
for (let category of this.categories) {
|
||||
category.on('setting-updated', this.__settingUpdated);
|
||||
category.on('settings-updated', this.__settingsUpdated);
|
||||
category.on('added-setting', this.__addedSetting);
|
||||
category.on('removed-setting', this.__removedSetting);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Category event listeners.
|
||||
* These only exists for use by the constructor and settingsset.addCategory.
|
||||
*/
|
||||
__settingUpdated({ category, setting, value, old_value }) {
|
||||
return 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
|
||||
}));
|
||||
}
|
||||
|
||||
__settingsUpdated({ updatedSettings }) {
|
||||
return this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||
updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({
|
||||
set: this, set_id: this.id
|
||||
}, updatedSetting)))
|
||||
}));
|
||||
}
|
||||
|
||||
__addedSetting({ category, setting, at_index }) {
|
||||
return this.emit('added-setting', {
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
setting, setting_id: setting.id,
|
||||
at_index
|
||||
});
|
||||
}
|
||||
|
||||
__removedSetting({ category, setting, from_index }) {
|
||||
return this.emit('removed-setting', {
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
setting, setting_id: setting.id,
|
||||
from_index
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically adds a category to this set.
|
||||
* @param {SettingsCategory} category The category to add to this set
|
||||
* @param {Number} index The index to add the category at (optional)
|
||||
* @return {Promise}
|
||||
*/
|
||||
async addCategory(category, index) {
|
||||
if (this.categories.find(c => c === category)) return;
|
||||
|
||||
if (!(category instanceof SettingsCategory))
|
||||
category = new SettingsCategory(category);
|
||||
|
||||
if (this.getCategory(category.id))
|
||||
throw {message: 'A category with this ID already exists.'};
|
||||
|
||||
category.on('setting-updated', this.__settingUpdated);
|
||||
category.on('settings-updated', this.__settingsUpdated);
|
||||
category.on('added-setting', this.__addedSetting);
|
||||
category.on('removed-setting', this.__removedSetting);
|
||||
|
||||
if (index === undefined) index = this.categories.length;
|
||||
this.categories.splice(index, 0, category);
|
||||
|
||||
const event = {
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
at_index: index
|
||||
};
|
||||
|
||||
await category.emit('added-to', event);
|
||||
await this.emit('added-category', event);
|
||||
return category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically removes a category from this set.
|
||||
* @param {SettingsCategory} category The category to remove from this set
|
||||
* @return {Promise}
|
||||
*/
|
||||
async removeCategory(category) {
|
||||
category.off('setting-updated', this.__settingUpdated);
|
||||
category.off('settings-updated', this.__settingsUpdated);
|
||||
category.off('added-setting', this.__addedSetting);
|
||||
category.off('removed-setting', this.__removedSetting);
|
||||
|
||||
let index;
|
||||
while ((index = this.categories.findIndex(c => c === category)) > -1) {
|
||||
this.categories.splice(index, 0);
|
||||
}
|
||||
|
||||
const event = {
|
||||
set: this, set_id: this.id,
|
||||
category, category_id: category.id,
|
||||
from_index: index
|
||||
};
|
||||
|
||||
await category.emit('removed-from', event);
|
||||
await this.emit('removed-category', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically adds a scheme to this set.
|
||||
* @param {SettingsScheme} scheme The scheme to add to this set
|
||||
* @param {Number} index The index to add the scheme at (optional)
|
||||
* @return {Promise}
|
||||
*/
|
||||
async addScheme(scheme, index) {
|
||||
if (this.schemes.find(c => c === scheme)) return;
|
||||
|
||||
if (!(scheme instanceof SettingsScheme))
|
||||
scheme = new SettingsScheme(scheme);
|
||||
|
||||
if (this.schemes.find(s => s.id === scheme.id))
|
||||
throw {message: 'A scheme with this ID already exists.'};
|
||||
|
||||
if (index === undefined) index = this.schemes.length;
|
||||
this.schemes.splice(index, 0, scheme);
|
||||
|
||||
await this.emit('added-scheme', {
|
||||
set: this, set_id: this.id,
|
||||
scheme, scheme_id: scheme.id,
|
||||
at_index: index
|
||||
});
|
||||
return scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically removes a scheme from this set.
|
||||
* @param {SettingsScheme} scheme The scheme to remove from this set
|
||||
* @return {Promise}
|
||||
*/
|
||||
async removeScheme(scheme) {
|
||||
let index;
|
||||
while ((index = this.schemes.findIndex(s => s === scheme)) > -1) {
|
||||
this.schemes.splice(index, 0);
|
||||
}
|
||||
|
||||
await this.emit('removed-scheme', {
|
||||
set: this, set_id: this.id,
|
||||
scheme, scheme_id: scheme.id,
|
||||
from_index: index
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.find(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); }
|
||||
|
||||
}
|
|
@ -0,0 +1,273 @@
|
|||
/**
|
||||
* 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';
|
||||
import { SettingsUpdatedEvent } from 'structs';
|
||||
|
||||
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._setValue(this.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async addItem(_item) {
|
||||
const item = this.createItem(_item);
|
||||
this.args.items.push(item);
|
||||
await this.updateValue();
|
||||
|
||||
await this.emit('item-added', { item });
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a set from this array setting.
|
||||
* This ignores the minimum value.
|
||||
* @param {SettingsSet} item The set to remove
|
||||
* @return {Promise}
|
||||
*/
|
||||
async removeItem(item) {
|
||||
this.args.items = this.items.filter(i => i !== item);
|
||||
await this.updateValue();
|
||||
|
||||
await this.emit('item-removed', { item });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (item instanceof SettingsSet)
|
||||
return item;
|
||||
|
||||
const set = new SettingsSet({
|
||||
id: item ? item.args ? item.args.id : item.id : Math.random(),
|
||||
settings: Utils.deepclone(this.settings),
|
||||
schemes: this.schemes
|
||||
}, item ? item.args || item : undefined);
|
||||
|
||||
set.setSaved();
|
||||
set.on('settings-updated', async event => {
|
||||
await this.emit('item-updated', { item: set, event, updatedSettings: event.updatedSettings });
|
||||
if (event.args.updating_array !== this) await this.updateValue();
|
||||
});
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to be called after the value changes.
|
||||
* This can be overridden by other settings types.
|
||||
* This function is used when the value needs to be updated synchronously (basically just in the constructor - so there won't be any events to emit anyway).
|
||||
* @param {SettingUpdatedEvent} updatedSetting
|
||||
*/
|
||||
setValueHookSync(updatedSetting) {
|
||||
this.args.items = updatedSetting.value ? updatedSetting.value.map(item => this.createItem(item)) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to be called after the value changes.
|
||||
* This can be overridden by other settings types.
|
||||
* @param {SettingUpdatedEvent} updatedSetting
|
||||
*/
|
||||
async setValueHook(updatedSetting) {
|
||||
const newItems = [];
|
||||
let error;
|
||||
|
||||
for (let newItem of updatedSetting.value) {
|
||||
try {
|
||||
const item = this.items.find(i => i.id && i.id === newItem.id);
|
||||
|
||||
if (item) {
|
||||
// Merge the new item into the original item
|
||||
newItems.push(item);
|
||||
const updatedSettings = await item.merge(newItem, false);
|
||||
if (!updatedSettings.length) continue;
|
||||
|
||||
const event = new SettingsUpdatedEvent({
|
||||
updatedSettings,
|
||||
updating_array: this
|
||||
});
|
||||
|
||||
await item.emit('settings-updated', event);
|
||||
// await this.emit('item-updated', { item, event, updatedSettings });
|
||||
} else {
|
||||
// Add a new item
|
||||
const item = this.createItem(newItem);
|
||||
newItems.push(item);
|
||||
await this.emit('item-added', { item });
|
||||
}
|
||||
} catch (e) { error = e; }
|
||||
}
|
||||
|
||||
for (let item of this.items) {
|
||||
if (newItems.includes(item)) continue;
|
||||
|
||||
try {
|
||||
// Item removed
|
||||
await this.emit('item-removed', { item });
|
||||
} catch (e) { error = e; }
|
||||
}
|
||||
|
||||
this.args.items = newItems;
|
||||
|
||||
// We can't throw anything before the items array is updated, otherwise the array setting would be in an inconsistent state where the values in this.items wouldn't match the values in this.value
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
// emit(...args) {
|
||||
// console.log('Emitting event', args[0], 'with data', args[1]);
|
||||
// return this.emitter.emit(...args);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Updates the value of this array setting.
|
||||
* This only exists for use by array settings.
|
||||
* @return {Promise}
|
||||
*/
|
||||
getValue() {
|
||||
return this.items.map(item => {
|
||||
if (!item) return;
|
||||
item.setSaved();
|
||||
return item.strip();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the value of this array setting.
|
||||
* This only exists for use by array settings.
|
||||
* @return {Promise}
|
||||
*/
|
||||
updateValue(emit_multi = true, emit = true) {
|
||||
return this.setValue(this.getValue(), 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(', ') + ',' : '()';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
/**
|
||||
* 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, hook = true) {
|
||||
const value = newSetting.args ? newSetting.args.value : newSetting.value;
|
||||
return this._setValue(value, hook);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges another setting into this setting.
|
||||
* @param {SettingsSetting} newSetting The setting to merge into this setting
|
||||
* @return {Promise}
|
||||
*/
|
||||
async merge(newSetting, emit_multi = true, emit = true) {
|
||||
const updatedSettings = this._merge(newSetting, false);
|
||||
if (!updatedSettings.length) return [];
|
||||
|
||||
await this.setValueHook(updatedSettings[0]);
|
||||
|
||||
if (emit)
|
||||
await this.emit('setting-updated', updatedSettings[0]);
|
||||
|
||||
if (emit_multi)
|
||||
await this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||
updatedSettings
|
||||
}));
|
||||
|
||||
return updatedSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of this setting.
|
||||
* This only exists for use by the constructor and SettingsCategory.
|
||||
*/
|
||||
_setValue(value, hook = 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 = new SettingUpdatedEvent({
|
||||
setting: this, setting_id: this.id,
|
||||
value, old_value
|
||||
});
|
||||
|
||||
if (hook)
|
||||
this.setValueHookSync(updatedSetting);
|
||||
|
||||
return [updatedSetting];
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to be called after the value changes.
|
||||
* This can be overridden by other settings types.
|
||||
* @param {SettingUpdatedEvent} updatedSetting
|
||||
*/
|
||||
async setValueHook(updatedSetting) {}
|
||||
setValueHookSync(updatedSetting) {}
|
||||
|
||||
/**
|
||||
* Sets the value of this setting.
|
||||
* @param {Any} value The new value of this setting
|
||||
* @return {Promise}
|
||||
*/
|
||||
async setValue(value, emit_multi = true, emit = true) {
|
||||
const updatedSettings = this._setValue(value, false);
|
||||
if (!updatedSettings.length) return [];
|
||||
|
||||
await this.setValueHook(updatedSettings[0]);
|
||||
|
||||
if (emit)
|
||||
await this.emit('setting-updated', updatedSettings[0]);
|
||||
|
||||
if (emit_multi)
|
||||
await this.emit('settings-updated', new SettingsUpdatedEvent({
|
||||
updatedSettings
|
||||
}));
|
||||
|
||||
return updatedSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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); }
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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(', ') : '()';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* BetterDiscord Keybind 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 Combokeys from 'combokeys';
|
||||
|
||||
export default class KeybindSetting extends Setting {
|
||||
|
||||
constructor(args, ...merge) {
|
||||
super(args, ...merge);
|
||||
|
||||
this.combokeys = new Combokeys(document);
|
||||
this.combokeys.bind(this.value, event => this.emit('keybind-activated', event));
|
||||
}
|
||||
|
||||
setValueHook() {
|
||||
this.combokeys.reset();
|
||||
this.combokeys.bind(this.value, event => this.emit('keybind-activated', event));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* The current value.
|
||||
*/
|
||||
get value() {
|
||||
return this.args.value * this.multi;
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
this.setValue(value / this.multi);
|
||||
}
|
||||
|
||||
/**
|
||||
* A number to multiply the value by.
|
||||
*/
|
||||
get multi() {
|
||||
return this.args.multi || 1;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* 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 current value.
|
||||
*/
|
||||
get value() {
|
||||
return this.args.value * this.multi;
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
this.setValue(value / this.multi);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* A number to multiply the value by.
|
||||
*/
|
||||
get multi() {
|
||||
return this.args.multi || 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object mapping points on the slider to labels.
|
||||
*/
|
||||
get points() {
|
||||
return this.args.points;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -2,3 +2,4 @@ export * from './socketstructs/channel';
|
|||
export * from './socketstructs/generic';
|
||||
export * from './socketstructs/guild';
|
||||
export * from './socketstructs/message';
|
||||
export * from './socketstructs/user';
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
*/
|
||||
|
||||
import DiscordEvent from './discordevent';
|
||||
import { Reflection } from 'ui';
|
||||
|
||||
export class MESSAGE_CREATE extends DiscordEvent {
|
||||
get author() { return this.args.author }
|
||||
|
@ -26,6 +27,14 @@ export class MESSAGE_CREATE extends DiscordEvent {
|
|||
get timestamp() { return this.args.timestamp }
|
||||
get tts() { return this.args.tts }
|
||||
get type() { return this.args.type }
|
||||
get element() {
|
||||
const find = document.querySelector(`[message-id="${this.id}"]`);
|
||||
if (find) return find;
|
||||
const messages = document.querySelectorAll('.message');
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (Reflection(lastMessage).prop('message.id') === this.id) return lastMessage;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export class MESSAGE_UPDATE extends MESSAGE_CREATE { }
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from './events/index';
|
||||
export * from './settings/index';
|
||||
|
|
|
@ -18,13 +18,41 @@
|
|||
}
|
||||
|
||||
.bd-profile-badge-developer,
|
||||
.bd-profile-badge-contributor {
|
||||
.bd-profile-badge-contributor,
|
||||
.bd-message-badge-developer,
|
||||
.bd-message-badge-contributor {
|
||||
background-image: $logoSmallBw;
|
||||
width: 16px;
|
||||
filter: brightness(10);
|
||||
|
||||
cursor: pointer;
|
||||
.theme-light [class*="topSectionNormal-"] & {
|
||||
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjYmJiYmJiIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg==');
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-message-badges-wrap {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
height: 11px;
|
||||
.bd-message-badge-developer,
|
||||
.bd-message-badge-contributor {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.member-username .bd-message-badges-wrap {
|
||||
display: inline-block;
|
||||
height: 17px;
|
||||
width: 14px;
|
||||
|
||||
.bd-message-badge-developer,
|
||||
.bd-message-badge-contributor {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
background-position: center;
|
||||
background-size: 12px 12px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
|
@ -13,9 +13,9 @@
|
|||
top: 27px;
|
||||
}
|
||||
|
||||
.platform-linux & {
|
||||
top: 0;
|
||||
}
|
||||
.platform-linux & {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.bd-settings-button-btn {
|
||||
background-image: $logoSmallBw;
|
||||
|
@ -38,13 +38,17 @@
|
|||
&.bd-loading {
|
||||
animation: bd-settings-button-pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
&.bd-updates {
|
||||
filter: hue-rotate(250deg) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.bd-active {
|
||||
background: transparent;
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
z-index: 3001;
|
||||
|
||||
.bd-settings-button-btn {
|
||||
background-image: $logoBigBw;
|
||||
|
@ -57,6 +61,11 @@
|
|||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&.bd-active,
|
||||
&.bd-animating {
|
||||
z-index: 3001;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bd-settings-button-pulse {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
.bd-pluginsview,
|
||||
.bd-themesview {
|
||||
.bd-online-ph {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
@import './button.scss';
|
||||
@import './sidebarview.scss';
|
||||
@import './plugins.scss';
|
||||
@import './contentview.scss';
|
||||
@import './card.scss';
|
||||
@import './tooltips.scss';
|
||||
@import './plugin-settings-modal.scss';
|
||||
@import './settings-schemes.scss';
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
/*.bd-pluginsView {
|
||||
.bd-button {
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
border-bottom: 2px solid #2b2d31;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
display: block;
|
||||
font-size: 1.17em;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-weight: bold;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.material-design-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.bd-active {
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
border-bottom: 2px solid #3e82e5;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-spinner-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
.bd-spinner-2 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
.bd-pluginsView {
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
.bd-settings-schemes {
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid rgba(114, 118, 126, 0.3);
|
||||
|
||||
.bd-settings-schemes-container {
|
||||
margin-right: -15px;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-settings-scheme {
|
||||
display: inline-block;
|
||||
width: calc(33.3% - 15px);
|
||||
margin: 0 15px 15px 0;
|
||||
cursor: pointer;
|
||||
vertical-align: top;
|
||||
|
||||
.bd-settings-modal & {
|
||||
width: calc(50% - 15px);
|
||||
|
||||
.bd-settings-scheme-icon {
|
||||
height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-settings-scheme-icon {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
border: 2px solid $coldimwhite;
|
||||
border-radius: 3px;
|
||||
transition: border-color 0.2s ease;
|
||||
margin-bottom: 15px;
|
||||
background: center / cover no-repeat #2f3136;
|
||||
}
|
||||
|
||||
.bd-settings-scheme-name {
|
||||
font-weight: 500;
|
||||
color: #f6f6f7;
|
||||
}
|
||||
|
||||
.bd-settings-scheme-hint {
|
||||
flex: 1 1 auto;
|
||||
color: #72767d;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.bd-settings-scheme-icon {
|
||||
border-color: lighten($coldimwhite, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
&.bd-active {
|
||||
cursor: default;
|
||||
|
||||
.bd-settings-scheme-icon {
|
||||
border-color: $colbdblue;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -111,8 +111,7 @@
|
|||
.platform-darwin & {
|
||||
top: 0px;
|
||||
|
||||
.bd-sidebar-view .bd-sidebar-region,
|
||||
.bd-sidebar-view .bd-content-region {
|
||||
.bd-sidebar-view .bd-sidebar-region {
|
||||
padding-top: 22px;
|
||||
}
|
||||
}
|
||||
|
@ -120,19 +119,38 @@
|
|||
.platform-linux & {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-settings .bd-sidebar-view.bd-stop .bd-content-region {
|
||||
z-index: 3003;
|
||||
}
|
||||
.bd-sidebar-view.bd-stop .bd-content-region {
|
||||
z-index: 3003;
|
||||
}
|
||||
|
||||
.bd-backdrop {
|
||||
z-index: 3003;
|
||||
.bd-sidebar-view.active {
|
||||
.bd-content-region {
|
||||
transition: transform 0.4s ease-in-out, opacity 0.2s ease;
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) .bd-sidebar-view.active,
|
||||
&.bd-settings-out .bd-sidebar-view.active {
|
||||
.bd-content-region {
|
||||
transform: translate(-600px, 0%);
|
||||
opacity: 0;
|
||||
width: 590px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) .bd-sidebar-view.active {
|
||||
.bd-content-region {
|
||||
transform: translate(-600px, 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-sidebar .bd-settings-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: 0;
|
||||
|
||||
.platform-darwin & {
|
||||
top: 22px;
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
.bd-autocomplete {
|
||||
border-radius: 5px 5px 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
z-index: 3;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background-color: #2f3136;
|
||||
|
||||
.bd-autocomplete-inner {
|
||||
padding-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
|
||||
.bd-autocompleteRow {
|
||||
padding: 0 8px;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
|
||||
.bd-autocompleteSelector {
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
|
||||
&.bd-selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.bd-selected {
|
||||
background-color: #36393f;
|
||||
}
|
||||
|
||||
.bd-autocompleteTitle {
|
||||
color: #72767d;
|
||||
padding: 4px 0;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
color: #fff;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-autocompleteField {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
color: #f6f6f7;
|
||||
min-height: 16px;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-box-orient: horizontal;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
-webkit-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
-webkit-box-align: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
min-width: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-left: 8px;
|
||||
color: #f6f6f7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -79,50 +79,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-tabheader {
|
||||
.bd-button {
|
||||
background: transparent;
|
||||
border-bottom: 2px solid rgba(114, 118, 126, 0.3);
|
||||
|
||||
h3 {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
display: block;
|
||||
font-size: 1.17em;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-weight: bold;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.bd-material-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
.material-design-icon,
|
||||
.bd-material-design-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
fill: #FFF;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #2d2f34;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.bd-active {
|
||||
background: transparent;
|
||||
border-bottom: 2px solid $colbdblue;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,14 @@
|
|||
.bd-drawer {
|
||||
border-bottom: 1px solid rgba(114, 118, 126, 0.3);
|
||||
margin-bottom: 15px;
|
||||
|
||||
.bd-settings-category > & {
|
||||
border-bottom: 1px solid rgba(114, 118, 126, 0.3);
|
||||
|
||||
.bd-drawer-contents > .bd-form-item:last-child > .bd-form-divider:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-form-header {
|
||||
margin-top: 0;
|
||||
cursor: pointer;
|
||||
|
@ -31,24 +38,26 @@
|
|||
}
|
||||
|
||||
.bd-drawer-contents {
|
||||
// margin-top is set in JavaScript when the drawer is animating
|
||||
// It still needs to be set here for it to animate
|
||||
transition: transform 0.2s ease, margin-top 0.2s ease, opacity 0.2s ease;
|
||||
transform: scaleY(0) translateY(0%);
|
||||
margin-top: -50%;
|
||||
margin-top: -100%;
|
||||
opacity: 0;
|
||||
|
||||
> .bd-form-item:last-child > .bd-form-divider:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
margin-top: 15px;
|
||||
&.bd-animating {
|
||||
> .bd-drawer-contents-wrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.bd-drawer-open {
|
||||
.bd-drawer-open-button {
|
||||
> .bd-drawer-header .bd-drawer-open-button {
|
||||
.bd-chevron-1 {
|
||||
svg {
|
||||
transform: rotate(90deg)
|
||||
|
@ -63,10 +72,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.bd-drawer-contents {
|
||||
> .bd-drawer-contents-wrap > .bd-drawer-contents {
|
||||
transform: scaleY(1) translateY(0%);
|
||||
margin-top: 0%;
|
||||
opacity: 1;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,14 +43,17 @@
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bd-form-item-changed .bd-form-divider {
|
||||
background: $colok;
|
||||
}
|
||||
|
||||
|
||||
.bd-form-divider {
|
||||
height: 1px;
|
||||
margin: 15px 0;
|
||||
background: hsla(218,5%,47%,.3);
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
.bd-form-item-changed > & {
|
||||
background: $colok;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-form-warning {
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
.bd-form-settingsarray {
|
||||
.bd-button.bd-button-primary {
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.bd-settingsarray-items {
|
||||
margin-top: 15px;
|
||||
|
||||
.bd-settingsarray-item {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
|
||||
.bd-settingsarray-item-marker {
|
||||
flex: 0 0 auto;
|
||||
min-width: 15px;
|
||||
margin-right: 5px;
|
||||
color: #aaa;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.bd-settingsarray-item-contents {
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.bd-settings-panel {
|
||||
.bd-settings-categories:last-child .bd-form-item:last-child .bd-form-divider {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bd-settings-category:only-child > div > .bd-form-item.bd-form-item-noheader:only-child {
|
||||
.bd-form-textinput,
|
||||
.bd-form-numberinput {
|
||||
+ .bd-form-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-settingsarray-item-hint {
|
||||
color: #aaa;
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
word-wrap: break-word;
|
||||
|
||||
max-width: 385px;
|
||||
}
|
||||
|
||||
.bd-settingsarray-item-controls {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.bd-settingsarray-open,
|
||||
.bd-settingsarray-remove {
|
||||
margin-left: 5px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
fill: #ccc;
|
||||
|
||||
&:hover {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child .bd-settings-categories:last-child .bd-form-item:last-child .bd-form-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.bd-form-settingsarray-inline .bd-settingsarray-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
.bd-colourpicker-swatch {
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid hsla(0,0%,100%,.1);
|
||||
}
|
||||
|
||||
.bd-form-colourpicker {
|
||||
position: relative;
|
||||
|
||||
.vc-chrome {
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
top: 35px;
|
||||
border-radius: 3px;
|
||||
z-index: 9001;
|
||||
|
||||
.vc-chrome-body {
|
||||
background: #36393e;
|
||||
|
||||
.vc-chrome-fields-wrap {
|
||||
.vc-input__input {
|
||||
background: #1e2124;
|
||||
color: #FFF;
|
||||
box-shadow: inset 0 0 0 1px #000;
|
||||
}
|
||||
|
||||
.vc-chrome-toggle-icon-highlight {
|
||||
background: #1e2124;
|
||||
}
|
||||
|
||||
.vc-chrome-toggle-btn svg path {
|
||||
fill: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,19 @@
|
|||
.bd-form-dropdown {
|
||||
h3 + .bd-dropdown {
|
||||
width: 180px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.bd-form-item-fullwidth & {
|
||||
.bd-dropdown {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-dropdown {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.bd-dropdown-current {
|
||||
color: #f6f6f7;
|
||||
|
@ -10,7 +24,6 @@
|
|||
cursor: default;
|
||||
outline: none;
|
||||
transition: border .15s ease;
|
||||
width: 180px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
|
@ -54,6 +67,7 @@
|
|||
|
||||
.bd-dropdown-options {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
top: calc(100% - 2.5px);
|
||||
width: 100%;
|
||||
max-height: 180px;
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
@import './main.scss';
|
||||
@import './switches.scss';
|
||||
@import './text.scss';
|
||||
@import './files.scss';
|
||||
@import './dropdowns.scss';
|
||||
@import './radios.scss';
|
||||
@import './sliders.scss';
|
||||
@import './switches.scss';
|
||||
@import './colourpickers.scss';
|
||||
@import './keybinds.scss';
|
||||
@import './arrays.scss';
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
.bd-keybind {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
// width: 180px;
|
||||
margin-top: 10px;
|
||||
background-color: rgba(0,0,0,.1);
|
||||
border: 1px solid rgba(0,0,0,.3);
|
||||
transition: border .15s ease;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
min-height: 40px;
|
||||
|
||||
.bd-keybind-selected {
|
||||
flex: 1 1 auto;
|
||||
color: #f6f6f7;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.bd-keybind-unset {
|
||||
.bd-keybind-selected {
|
||||
color: hsla(240,6%,97%,.3);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-button {
|
||||
border-radius: 2px;
|
||||
margin: -4px -4px -4px 10px;
|
||||
padding: 2px 20px;
|
||||
transition: background-color .2s ease-in-out, color .2s ease-in-out;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.bd-active {
|
||||
border-color: $colerr;
|
||||
animation: bd-keybind-pulse 1s infinite;
|
||||
|
||||
.bd-button {
|
||||
color: $colerr;
|
||||
background-color: rgba($colerr, .3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bd-keybind-pulse {
|
||||
0% { box-shadow: 0 0 6px rgba(240,71,71,.3) }
|
||||
50% { box-shadow: 0 0 10px rgba(240,71,71,.6) }
|
||||
100% { box-shadow: 0 0 6px rgba(240,71,71,.3) }
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
.bd-setting-switch,
|
||||
.bd-form-textinput,
|
||||
.bd-form-textarea,
|
||||
.bd-form-fileinput,
|
||||
.bd-form-numberinput,
|
||||
.bd-form-dropdown,
|
||||
.bd-form-radio,
|
||||
.bd-form-numberinput,
|
||||
.bd-form-slider,
|
||||
.bd-setting-switch {
|
||||
.bd-form-colourpicker,
|
||||
.bd-form-keybind,
|
||||
.bd-form-fileinput,
|
||||
.bd-form-settingsarray {
|
||||
.bd-title {
|
||||
display: flex;
|
||||
|
||||
|
@ -21,10 +24,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);
|
||||
}
|
||||
|
@ -37,3 +38,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-form-customsetting {
|
||||
&.bd-form-customsetting-debug + .bd-form-divider {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> .bd-drawer {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
.bd-form-radio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.bd-form-radio-details {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.bd-form-radio-group {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 10px;
|
||||
.bd-radio-group {
|
||||
width: 180px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.bd-form-item-fullwidth & {
|
||||
flex-direction: column;
|
||||
|
||||
.bd-radio-group {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,6 +54,8 @@
|
|||
.bd-radio-text {
|
||||
flex: 1 1 auto;
|
||||
color: white;
|
||||
word-wrap: break-word;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
|
|
|
@ -1,3 +1,20 @@
|
|||
.bd-form-slider {
|
||||
h3 + .bd-slider {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.bd-form-item-fullwidth & {
|
||||
.bd-title {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h3 + .bd-slider {
|
||||
margin-top: 15px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-slider {
|
||||
min-height: 24px;
|
||||
min-width: 180px;
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
.bd-form-textinput,
|
||||
.bd-form-numberinput {
|
||||
h3 {
|
||||
+ .bd-textinput-wrapper,
|
||||
+ .bd-number {
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-textinput-wrapper,
|
||||
.bd-number {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
background: transparent;
|
||||
|
@ -10,17 +22,40 @@
|
|||
line-height: 24px;
|
||||
font-size: 100%;
|
||||
font-weight: 500;
|
||||
width: 180px;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
border-color: $colbdblue;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-form-item-fullwidth & {
|
||||
.bd-title {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bd-textinput-wrapper,
|
||||
.bd-number {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.bd-hint {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-textinput-wrapper,
|
||||
.bd-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bd-form-textarea {
|
||||
.bd-form-textarea-wrap {
|
||||
.bd-form-textarea-wrap,
|
||||
textarea.bd-textarea {
|
||||
margin-top: 15px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
|
@ -28,6 +63,7 @@
|
|||
color: #b9bbbe;
|
||||
overflow-y: scroll;
|
||||
max-height: 140px;
|
||||
transition: border-color .2s ease, color .2s ease;
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
|
@ -37,9 +73,24 @@
|
|||
@include scrollbar;
|
||||
}
|
||||
|
||||
div[contenteditable] {
|
||||
div[contenteditable],
|
||||
textarea {
|
||||
padding: 11px;
|
||||
cursor: text;
|
||||
min-height: 45px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
background: transparent;
|
||||
border: none;
|
||||
resize: none;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
box-sizing: border-box;
|
||||
overflow-y: visible;
|
||||
max-height: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
@import './spinners/index.scss';
|
||||
@import './scrollable.scss';
|
||||
@import './buttons.scss';
|
||||
@import './tabs.scss';
|
||||
@import './forms.scss';
|
||||
@import './forms/index.scss';
|
||||
@import './material-buttons.scss';
|
||||
@import './drawers.scss';
|
||||
@import './preformatted.scss';
|
||||
@import './refreshbtn.scss';
|
||||
@import './autocomplete.scss';
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
.bd-pre-wrap {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
border-radius: 3px;
|
||||
color: #b9bbbe;
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
overflow-y: auto;
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
border-color: #040405;
|
||||
}
|
||||
|
||||
@include scrollbar;
|
||||
}
|
||||
|
||||
.bd-pre {
|
||||
padding: 11px;
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
@keyframes bd-refresh-circle-anim-out {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
stroke-dasharray: 36.5, 200;
|
||||
stroke-dashoffset: -7.5;
|
||||
}
|
||||
|
||||
80% {
|
||||
stroke-dasharray: 36.5, 200;
|
||||
stroke-dashoffset: -44;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
stroke-dasharray: 36.5, 200;
|
||||
stroke-dashoffset: -44;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bd-refresh-circle-anim-in {
|
||||
0% {
|
||||
transform: rotate(-360deg);
|
||||
stroke-dasharray: 0, 200;
|
||||
stroke-dashoffset: -7.5;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
stroke-dasharray: 36.5, 200;
|
||||
stroke-dashoffset: -7.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bd-refresh-arrow-rotate {
|
||||
from {
|
||||
transform: rotate(-680deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.bd-refresh-button {
|
||||
cursor: pointer;
|
||||
height: 31px;
|
||||
width: 31px;
|
||||
display: block;
|
||||
|
||||
svg,
|
||||
svg * {
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 19px;
|
||||
width: 19px;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.bd-svg-refresh {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.bd-refreshed {
|
||||
.bd-svg-refresh {
|
||||
transition: .5s cubic-bezier(.4,0,0,1) .2s;
|
||||
}
|
||||
|
||||
.bd-svg-cancel-line:nth-of-type(1) {
|
||||
transform-origin: bottom;
|
||||
transition: .5s cubic-bezier(0,0,0,1) .1s, transform-origin 1ms;
|
||||
}
|
||||
|
||||
.bd-svg-cancel-line:nth-of-type(2) {
|
||||
transform-origin: left;
|
||||
transition: .5s cubic-bezier(0,0,0,1), transform-origin 1ms;
|
||||
}
|
||||
|
||||
.bd-svg-circle {
|
||||
stroke-dasharray: 36.5, 200;
|
||||
stroke-dashoffset: -7.5;
|
||||
transform-origin: center;
|
||||
animation: bd-refresh-circle-anim-in 1s cubic-bezier(.4,0,0,1);
|
||||
}
|
||||
|
||||
.bd-svg-arrow {
|
||||
transform-origin: center;
|
||||
animation: bd-refresh-arrow-rotate 1s cubic-bezier(.4,0,0,1);
|
||||
}
|
||||
}
|
||||
|
||||
&.bd-refreshing {
|
||||
.bd-svg-refresh {
|
||||
transition: .5s cubic-bezier(.4,0,0,1);
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.bd-svg-cancel-line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bd-svg-cancel-line:nth-of-type(1) {
|
||||
transform-origin: top;
|
||||
transform: scale(1,1);
|
||||
transition: .5s cubic-bezier(0,0,0,1) .4s, transform-origin 1ms;
|
||||
}
|
||||
|
||||
.bd-svg-cancel-line:nth-of-type(2) {
|
||||
transform-origin: right;
|
||||
transform: scale(1,1);
|
||||
transition: .5s cubic-bezier(0,0,0,1) .3s, transform-origin 1ms;
|
||||
}
|
||||
|
||||
.bd-svg-circle {
|
||||
stroke-dasharray: 36.5, 200;
|
||||
stroke-dashoffset: -44;
|
||||
transform-origin: center;
|
||||
animation: bd-refresh-circle-anim-out 1s cubic-bezier(.4,0,0,1);
|
||||
}
|
||||
|
||||
.bd-svg-arrow {
|
||||
transform-origin: center;
|
||||
transform: rotate(360deg);
|
||||
transition: 1s cubic-bezier(.4,0,0,1);
|
||||
}
|
||||
|
||||
.bd-svg-arrow-triangle {
|
||||
transition: .5s ease;
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bd-svg-cancel {
|
||||
transform: rotate(-45deg);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.bd-svg-cancel-line {
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
.bd-svg-cancel-line:nth-of-type(1) {
|
||||
transform: scale(1,0);
|
||||
}
|
||||
|
||||
.bd-svg-cancel-line:nth-of-type(2) {
|
||||
transform: scale(0,1);
|
||||
}
|
||||
|
||||
.bd-svg-circle {
|
||||
stroke-dasharray: 36.5, 200;
|
||||
stroke-dashoffset: -7.5;
|
||||
}
|
||||
|
||||
.bd-svg-arrow {
|
||||
transform-origin: center;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.bd-svg-arrow-triangle {
|
||||
transform-origin: 17.1px 6.9px;
|
||||
transition: .8s ease .2s;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
|
@ -8,8 +8,6 @@
|
|||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 10px 10px 10px 0;
|
||||
margin: 10px 0;
|
||||
|
||||
@include scrollbar;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
.bd-tabbar {
|
||||
flex: 0 0 auto;
|
||||
margin-right: -15px;
|
||||
display: flex;
|
||||
|
||||
.bd-button {
|
||||
background: transparent;
|
||||
border-bottom: 2px solid rgba(114, 118, 126, 0.3);
|
||||
cursor: pointer;
|
||||
margin-right: 15px;
|
||||
padding: 15px 0;
|
||||
color: #87909c;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease, border-bottom-color 0.2s ease;
|
||||
flex: 0 0;
|
||||
display: flex;
|
||||
|
||||
h3 {
|
||||
flex: 0 0;
|
||||
}
|
||||
|
||||
.bd-material-button {
|
||||
margin: -1px 0 -1px 4px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.bd-active {
|
||||
background: transparent;
|
||||
border-bottom-color: $colbdblue;
|
||||
color: #fff;
|
||||
|
||||
.bd-material-design-icon {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.bd-active {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.bd-settingswrap & {
|
||||
min-width: 150px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-material-button-wrap {
|
||||
margin-right: 15px;
|
||||
flex: 0 0;
|
||||
padding: 17px 0 18px;
|
||||
|
||||
.bd-material-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-button,
|
||||
.bd-material-button-wrap {
|
||||
.bd-material-button {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0;
|
||||
cursor: pointer;
|
||||
|
||||
.material-design-icon,
|
||||
.bd-material-design-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
fill: #87909c;
|
||||
transition: fill 0.2s ease;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
|
||||
.bd-material-design-icon {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-settingswrap-header & {
|
||||
// margin-top: -17px;
|
||||
margin-bottom: -20px;
|
||||
|
||||
.bd-button {
|
||||
font-size: 16px;
|
||||
// padding: 18px 0 17px;
|
||||
}
|
||||
|
||||
.bd-material-button {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -7,6 +7,8 @@
|
|||
@import './bdsettings/index.scss';
|
||||
@import './generic/index.scss';
|
||||
@import './modals/index.scss';
|
||||
@import './profilebadges.scss';
|
||||
@import './badges.scss';
|
||||
|
||||
@import './discordoverrides.scss';
|
||||
@import './helpers.scss';
|
||||
@import './misc.scss';
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.edit-message .markup.mutable {
|
||||
display: none;
|
||||
}
|
|
@ -5,3 +5,5 @@
|
|||
|
||||
@import './basic-modal.scss';
|
||||
@import './error-modal.scss';
|
||||
@import './settings-modal.scss';
|
||||
@import './permission-modal.scss';
|
||||
|
|
|
@ -85,12 +85,16 @@
|
|||
.bd-modal-tip {
|
||||
flex-grow: 1;
|
||||
line-height: 26px;
|
||||
color: #FFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bd-button {
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
|
||||
+ .bd-button {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
.bd-perm-scope {
|
||||
display: flex;
|
||||
|
||||
.bd-perm-allow {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
.bd-perm-check {
|
||||
-webkit-box-sizing: border-box;
|
||||
background: hsla(0,0%,100%,.2);
|
||||
border-radius: 18px;
|
||||
box-sizing: border-box;
|
||||
height: 36px;
|
||||
margin-right: 20px;
|
||||
margin-top: 14px;
|
||||
padding: 2px;
|
||||
width: 36px;
|
||||
|
||||
.bd-perm-check-inner {
|
||||
background-color: #43b581;
|
||||
border: 2px solid #35383c;
|
||||
border-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-perm-inner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 45px;
|
||||
padding: 13px 0;
|
||||
border-bottom: 1px solid hsla(0,0%,100%,.1);
|
||||
|
||||
.bd-perm-name {
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.bd-perm-desc {
|
||||
color: hsla(0,0%,100%,.2);
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
.bd-plugin-settings-modal {
|
||||
.bd-settings-modal {
|
||||
.bd-modal .bd-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.bd-plugin-settings-body {
|
||||
.bd-settings-modal-body {
|
||||
padding: 0 15px;
|
||||
margin: 0 0 74px;
|
||||
|
|
@ -11,28 +11,6 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.bd-settingsWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
.bd-scroller-wrap {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.bd-settingsWrap-header {
|
||||
color: $colbdblue;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 100%;
|
||||
outline: 0;
|
||||
padding: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
> div:not(.active) {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
|
@ -50,8 +28,4 @@
|
|||
.animating {
|
||||
animation: bd-fade-out .4s forwards;
|
||||
}
|
||||
|
||||
.bd-settingsWrap {
|
||||
padding: 20px 15px 15px 15px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import './main.scss';
|
||||
@import './sidebar.scss';
|
||||
@import './content.scss';
|
||||
@import './settingswrap.scss';
|
||||
|
|
|
@ -20,14 +20,8 @@
|
|||
max-width: 310px;
|
||||
min-width: 310px;
|
||||
|
||||
.bd-settingsWrap {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
-webkit-box-flex: 1;
|
||||
flex: 1;
|
||||
min-height: 1px;
|
||||
box-sizing: border-box;
|
||||
padding: 80px 15px 15px 15px;
|
||||
.bd-scroller {
|
||||
padding: 10px 10px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,6 +31,7 @@
|
|||
background: #36393e;
|
||||
box-shadow: 0 0 4px #202225;
|
||||
backface-visibility: hidden;
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
&.bd-stop {
|
||||
|
@ -44,10 +39,4 @@
|
|||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.bd-content-region {
|
||||
animation: bd-slidein .6s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
.bd-sidebar-region {
|
||||
.bd-settingswrap {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
-webkit-box-flex: 1;
|
||||
flex: 1;
|
||||
min-height: 1px;
|
||||
box-sizing: border-box;
|
||||
padding: 90px 15px 0 15px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-content-region {
|
||||
.bd-settingswrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
> .bd-scroller-wrap {
|
||||
flex-grow: 1;
|
||||
|
||||
> .bd-scroller {
|
||||
overflow-y: scroll;
|
||||
|
||||
.platform-darwin .bd-settings & {
|
||||
padding-top: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-settingswrap-header {
|
||||
outline: 0;
|
||||
padding: 0;
|
||||
margin: 30px 20px 20px;
|
||||
vertical-align: baseline;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.bd-settingswrap-header-text {
|
||||
color: $colbdblue;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-settingswrap-contents {
|
||||
padding: 0 20px;
|
||||
margin-bottom: 84px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,10 +41,11 @@
|
|||
&:hover,
|
||||
&.active {
|
||||
background: $colbdblue;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #FFF;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,286 @@
|
|||
/**
|
||||
* BetterDiscord Automated DOM Manipulations
|
||||
* 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 { Events, WebpackModules, EventListener } from 'modules';
|
||||
import Reflection from './reflection';
|
||||
import DOM from './dom';
|
||||
import VueInjector from './vueinjector';
|
||||
import EditedTimeStamp from './components/common/EditedTimestamp.vue';
|
||||
import Autocomplete from './components/common/Autocomplete.vue';
|
||||
|
||||
class TempApi {
|
||||
static get currentGuildId() {
|
||||
try {
|
||||
return WebpackModules.getModuleByName('SelectedGuildStore').getGuildId();
|
||||
} catch (err) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
static get currentChannelId() {
|
||||
try {
|
||||
return WebpackModules.getModuleByName('SelectedChannelStore').getChannelId();
|
||||
} catch (err) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
static get currentUserId() {
|
||||
try {
|
||||
return WebpackModules.getModuleByName('UserStore').getCurrentUser().id;
|
||||
} catch (err) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class extends EventListener {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const messageFilter = function (m) {
|
||||
return m.addedNodes && m.addedNodes.length && m.addedNodes[0].classList && m.addedNodes[0].classList.contains('message-group');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('loading-more-manip', messageFilter, mutations => {
|
||||
this.setIds();
|
||||
this.makeMutable();
|
||||
Events.emit('ui:laodedmore', mutations.map(m => m.addedNodes[0]));
|
||||
}, 'filter');
|
||||
|
||||
const userFilter = function (m) {
|
||||
return m.addedNodes && m.addedNodes.length && m.addedNodes[0].classList && m.addedNodes[0].classList.contains('member');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('loading-more-users-manip', userFilter, mutations => {
|
||||
this.setUserIds();
|
||||
Events.emit('ui:loadedmoreusers', mutations.map(m => m.addedNodes[0]));
|
||||
}, 'filter');
|
||||
|
||||
const channelFilter = function(m) {
|
||||
return m.addedNodes &&
|
||||
m.addedNodes.length &&
|
||||
m.addedNodes[0].className &&
|
||||
m.addedNodes[0].className.includes('container');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('loading-more-channels-manip', channelFilter, mutations => {
|
||||
this.setChannelIds();
|
||||
Events.emit('ui:loadedmorechannels', mutations.map(m => m.addedNodes[0]));
|
||||
}, 'filter');
|
||||
|
||||
const popoutFilter = function(m) {
|
||||
return m.addedNodes &&
|
||||
m.addedNodes.length &&
|
||||
m.addedNodes[0].className &&
|
||||
m.addedNodes[0].className.includes('popout');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('userpopout-manip', popoutFilter, mutations => {
|
||||
const userPopout = document.querySelector('[class*=userPopout]');
|
||||
if (!userPopout) return;
|
||||
const user = Reflection(userPopout).prop('user');
|
||||
if (!user) return;
|
||||
userPopout.setAttribute('data-user-id', user.id);
|
||||
if (user.id === TempApi.currentUserId) userPopout.setAttribute('data-currentuser', true);
|
||||
}, 'filter');
|
||||
|
||||
const modalFilter = function(m) {
|
||||
return m.addedNodes &&
|
||||
m.addedNodes.length &&
|
||||
m.addedNodes[0].className &&
|
||||
m.addedNodes[0].className.includes('modal');
|
||||
}
|
||||
|
||||
DOM.observer.subscribe('modal-manip', modalFilter, mutations => {
|
||||
const userModal = document.querySelector('[class*=modal] > [class*=inner]');
|
||||
if (!userModal) return;
|
||||
const user = Reflection(userModal).prop('user');
|
||||
if (!user) return;
|
||||
const modal = userModal.closest('[class*=modal]');
|
||||
if (!modal) return;
|
||||
modal.setAttribute('data-user-id', user.id);
|
||||
if (user.id === TempApi.currentUserId) modal.setAttribute('data-currentuser', true);
|
||||
});
|
||||
}
|
||||
|
||||
bindings() {
|
||||
this.manipAll = this.manipAll.bind(this);
|
||||
this.markupInjector = this.markupInjector.bind(this);
|
||||
this.setIds = this.setIds.bind(this);
|
||||
this.setMessageIds = this.setMessageIds.bind(this);
|
||||
this.setUserIds = this.setUserIds.bind(this);
|
||||
}
|
||||
|
||||
get eventBindings() {
|
||||
return [
|
||||
{ id: 'server-switch', callback: this.manipAll },
|
||||
{ id: 'channel-switch', callback: this.manipAll },
|
||||
{ id: 'discord:MESSAGE_CREATE', callback: this.markupInjector },
|
||||
{ id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector },
|
||||
{ id: 'gkh:keyup', callback: this.injectAutocomplete }
|
||||
];
|
||||
}
|
||||
|
||||
manipAll() {
|
||||
try {
|
||||
this.appMount.setAttribute('guild-id', TempApi.currentGuildId);
|
||||
this.appMount.setAttribute('channel-id', TempApi.currentChannelId);
|
||||
this.setIds();
|
||||
this.makeMutable();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
markupInjector(e) {
|
||||
if (!e.element) return;
|
||||
this.setId(e.element);
|
||||
const markup = e.element.querySelector('.markup:not(.mutable)');
|
||||
if (markup) this.injectMarkup(markup, this.cloneMarkup(markup), false);
|
||||
}
|
||||
|
||||
getEts(node) {
|
||||
try {
|
||||
const reh = Object.keys(node).find(k => k.startsWith('__reactInternalInstance'));
|
||||
return node[reh].memoizedProps.children[node[reh].memoizedProps.children.length - 1].props.text;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
makeMutable() {
|
||||
for (const el of document.querySelectorAll('.markup:not(.mutable)')) {
|
||||
this.injectMarkup(el, this.cloneMarkup(el), false);
|
||||
}
|
||||
}
|
||||
|
||||
cloneMarkup(node) {
|
||||
const childNodes = [...node.childNodes];
|
||||
const clone = document.createElement('div');
|
||||
clone.className = 'markup mutable';
|
||||
const ets = this.getEts(node);
|
||||
for (const [cni, cn] of childNodes.entries()) {
|
||||
if (cn.nodeType !== Node.TEXT_NODE) {
|
||||
if (cn.className.includes('edited')) continue;
|
||||
}
|
||||
clone.appendChild(cn.cloneNode(true));
|
||||
}
|
||||
return { clone, ets }
|
||||
}
|
||||
|
||||
injectMarkup(sibling, markup, reinject) {
|
||||
if (sibling.className && sibling.className.includes('mutable')) return; // Ignore trying to make mutable again
|
||||
let cc = null;
|
||||
for (const cn of sibling.parentElement.childNodes) {
|
||||
if (cn.className && cn.className.includes('mutable')) cc = cn;
|
||||
}
|
||||
if (cc) sibling.parentElement.removeChild(cc);
|
||||
if (markup === true) markup = this.cloneMarkup(sibling);
|
||||
|
||||
sibling.parentElement.insertBefore(markup.clone, sibling);
|
||||
sibling.classList.add('shadow');
|
||||
sibling.style.display = 'none';
|
||||
if (markup.ets) {
|
||||
const etsRoot = document.createElement('span');
|
||||
markup.clone.appendChild(etsRoot);
|
||||
VueInjector.inject(
|
||||
etsRoot,
|
||||
DOM.createElement('span', null, 'test'),
|
||||
{ EditedTimeStamp },
|
||||
`<EditedTimeStamp ets="${markup.ets}"/>`,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
Events.emit('ui:mutable:.markup', markup.clone);
|
||||
}
|
||||
|
||||
setIds() {
|
||||
this.setMessageIds();
|
||||
this.setUserIds();
|
||||
this.setChannelIds();
|
||||
}
|
||||
|
||||
setMessageIds() {
|
||||
for (let msg of document.querySelectorAll('.message')) {
|
||||
this.setId(msg);
|
||||
}
|
||||
}
|
||||
|
||||
setUserIds() {
|
||||
for (let user of document.querySelectorAll('.channel-members-wrap .member')) {
|
||||
this.setUserId(user);
|
||||
}
|
||||
}
|
||||
|
||||
setChannelIds() {
|
||||
for (let channel of document.querySelectorAll('[class*=channels] [class*=containerDefault]')) {
|
||||
this.setChannelId(channel);
|
||||
}
|
||||
}
|
||||
|
||||
setId(msg) {
|
||||
if (msg.hasAttribute('message-id')) return;
|
||||
const messageid = Reflection(msg).prop('message.id');
|
||||
const authorid = Reflection(msg).prop('message.author.id');
|
||||
if (!messageid || !authorid) {
|
||||
const msgGroup = msg.closest('.message-group');
|
||||
if (!msgGroup) return;
|
||||
const userTest = Reflection(msgGroup).prop('user');
|
||||
if (!userTest) return;
|
||||
msgGroup.setAttribute('data-author-id', userTest.id);
|
||||
if (userTest.id === TempApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
|
||||
return;
|
||||
}
|
||||
msg.setAttribute('data-message-id', messageid);
|
||||
const msgGroup = msg.closest('.message-group');
|
||||
if (!msgGroup) return;
|
||||
msgGroup.setAttribute('data-author-id', authorid);
|
||||
if (authorid === TempApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
|
||||
}
|
||||
|
||||
setUserId(user) {
|
||||
if (user.hasAttribute('data-user-id')) return;
|
||||
const userid = Reflection(user).prop('user.id');
|
||||
if (!userid) return;
|
||||
user.setAttribute('data-user-id', userid);
|
||||
const currentUser = userid === TempApi.currentUserId;
|
||||
if (currentUser) user.setAttribute('data-currentuser', true);
|
||||
Events.emit('ui:useridset', user);
|
||||
}
|
||||
|
||||
setChannelId(channel) {
|
||||
if (channel.hasAttribute('data-channel-id')) return;
|
||||
const channelObj = Reflection(channel).prop('channel');
|
||||
if (!channelObj) return;
|
||||
channel.setAttribute('data-channel-id', channelObj.id);
|
||||
if (channelObj.nsfw) channel.setAttribute('data-channel-nsfw', true);
|
||||
if (channelObj.type && channelObj.type === 2) channel.setAttribute('data-channel-voice', true);
|
||||
}
|
||||
|
||||
get appMount() {
|
||||
return document.getElementById('app-mount');
|
||||
}
|
||||
|
||||
injectAutocomplete(e) {
|
||||
if (document.querySelector('.bd-autocomplete')) return;
|
||||
if (!e.target.closest('[class*=channelTextArea]')) return;
|
||||
const root = document.createElement('span');
|
||||
const parent = document.querySelector('[class*="channelTextArea"] > [class*="inner"]');
|
||||
if (!parent) return;
|
||||
parent.append(root);
|
||||
VueInjector.inject(
|
||||
root,
|
||||
DOM.createElement('span'),
|
||||
{ Autocomplete },
|
||||
`<Autocomplete initial="${e.target.value}"/>`,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* BetterDiscord Menu Module
|
||||
* 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';
|
||||
|
||||
let items = 0;
|
||||
|
||||
const BdMenuItems = new class {
|
||||
|
||||
constructor() {
|
||||
window.bdmenu = this;
|
||||
|
||||
this.items = [];
|
||||
|
||||
this.addSettingsSet('Internal', 'core', 'Core');
|
||||
this.addSettingsSet('Internal', 'ui', 'UI');
|
||||
this.addSettingsSet('Internal', 'emotes', 'Emotes');
|
||||
|
||||
this.add({category: 'Internal', contentid: 'css', text: 'CSS Editor'});
|
||||
this.add({category: 'External', contentid: 'plugins', text: 'Plugins'});
|
||||
this.add({category: 'External', contentid: 'themes', text: 'Themes'});
|
||||
}
|
||||
|
||||
add(item) {
|
||||
item.id = items++;
|
||||
item.contentid = item.contentid || (items++ + '');
|
||||
item.active = false;
|
||||
item.hidden = item.hidden || false;
|
||||
item._type = item._type || 'button';
|
||||
|
||||
this.items.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
addSettingsSet(category, set, text) {
|
||||
return this.add({
|
||||
category, set,
|
||||
text: text || set.text
|
||||
});
|
||||
}
|
||||
|
||||
addVueComponent(category, text, component) {
|
||||
return this.add({
|
||||
category, text, component
|
||||
});
|
||||
}
|
||||
|
||||
remove(item) {
|
||||
Utils.removeFromArray(this.items, item);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { BdMenuItems };
|
|
@ -14,18 +14,89 @@ import { BdSettingsWrapper } from './components';
|
|||
import BdModals from './components/bd/BdModals.vue';
|
||||
import { Events, WebpackModules } from 'modules';
|
||||
import { Utils } from 'common';
|
||||
import AutoManip from './automanip';
|
||||
import { remote } from 'electron';
|
||||
|
||||
class TempApi {
|
||||
static get currentGuild() {
|
||||
try {
|
||||
const currentGuildId = WebpackModules.getModuleByName('SelectedGuildStore').getGuildId();
|
||||
return WebpackModules.getModuleByName('GuildStore').getGuild(currentGuildId);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
static get currentChannel() {
|
||||
try {
|
||||
const currentChannelId = WebpackModules.getModuleByName('SelectedChannelStore').getChannelId();
|
||||
return WebpackModules.getModuleByName('ChannelStore').getChannel(currentChannelId);
|
||||
} catch (err) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
static get currentUserId() {
|
||||
try {
|
||||
return WebpackModules.getModuleByName('UserStore').getCurrentUser().id;
|
||||
} catch (err) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class {
|
||||
|
||||
static initUiEvents() {
|
||||
this.pathCache = {
|
||||
isDm: null,
|
||||
server: TempApi.currentGuild,
|
||||
channel: TempApi.currentChannel
|
||||
};
|
||||
window.addEventListener('keyup', e => Events.emit('gkh:keyup', e));
|
||||
this.autoManip = new AutoManip();
|
||||
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);
|
||||
|
||||
const ehookInterval = setInterval(() => {
|
||||
if (!remote.BrowserWindow.getFocusedWindow()) return;
|
||||
clearInterval(ehookInterval);
|
||||
remote.BrowserWindow.getFocusedWindow().webContents.on('did-navigate-in-page', (e, url, isMainFrame) => {
|
||||
const { currentGuild, currentChannel } = TempApi;
|
||||
if (!this.pathCache.server) {
|
||||
Events.emit('server-switch', { 'server': currentGuild, 'channel': currentChannel });
|
||||
this.pathCache.server = currentGuild;
|
||||
this.pathCache.channel = currentChannel;
|
||||
return;
|
||||
}
|
||||
if (!this.pathCache.channel) {
|
||||
Events.emit('channel-switch', currentChannel);
|
||||
this.pathCache.server = currentGuild;
|
||||
this.pathCache.channel = currentChannel;
|
||||
return;
|
||||
}
|
||||
if (currentGuild &&
|
||||
currentGuild.id &&
|
||||
this.pathCache.server &&
|
||||
this.pathCache.server.id !== currentGuild.id) {
|
||||
Events.emit('server-switch', { 'server': currentGuild, 'channel': currentChannel });
|
||||
this.pathCache.server = currentGuild;
|
||||
this.pathCache.channel = currentChannel;
|
||||
return;
|
||||
}
|
||||
if (currentChannel &&
|
||||
currentChannel.id &&
|
||||
this.pathCache.channel &&
|
||||
this.pathCache.channel.id !== currentChannel.id) Events.emit('channel-switch', currentChannel);
|
||||
|
||||
|
||||
this.pathCache.server = currentGuild;
|
||||
this.pathCache.channel = currentChannel;
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
@ -53,4 +124,5 @@ export default class {
|
|||
|
||||
return vueInstance;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,18 +9,21 @@
|
|||
*/
|
||||
|
||||
<template>
|
||||
<div class="bd-settings" :class="{active: active}" @keyup="close">
|
||||
<SidebarView :contentVisible="this.activeIndex >= 0" :animating="this.animating" :class="{'bd-stop': !first}">
|
||||
<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">
|
||||
<MiClose size="17"/>
|
||||
<span class="bd-x-text">ESC</span>
|
||||
</div>
|
||||
<SidebarItem v-for="item in sidebarItems" :item="item" :key="item.id" :onClick="itemOnClick" />
|
||||
<template v-for="(category, text) in sidebar">
|
||||
<SidebarItem :item="{text, type: 'header'}" />
|
||||
<SidebarItem v-for="item in category" :item="item" :key="item.id" :onClick="itemOnClick" />
|
||||
</template>
|
||||
</Sidebar>
|
||||
<div slot="sidebarfooter" class="bd-info">
|
||||
<span class="bd-vtext">v2.0.0a by Jiiks/JsSucks</span>
|
||||
<div @click="openGithub" v-tooltip="'Github'" class="bd-material-button">
|
||||
<div @click="openGithub" v-tooltip="'GitHub'" class="bd-material-button">
|
||||
<MiGithubCircle size="16" />
|
||||
</div>
|
||||
<div @click="openTwitter" v-tooltip="'@Jiiksi'" class="bd-material-button">
|
||||
|
@ -31,19 +34,21 @@
|
|||
</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)}">
|
||||
<SettingsWrapper :headertext="set.headertext">
|
||||
<SettingsPanel :settings="set.settings" :change="(c, s, v) => changeSetting(set.id, c, s, v)" />
|
||||
<div v-for="item in sidebarItems" v-if="activeContent(item.contentid) || animatingContent(item.contentid)" :class="{active: activeContent(item.contentid), animating: animatingContent(item.contentid)}">
|
||||
<template v-if="item.component">
|
||||
<component :is="item.component" :SettingsWrapper="SettingsWrapper" />
|
||||
</template>
|
||||
|
||||
<SettingsWrapper v-if="typeof item.set === 'string'" :headertext="Settings.getSet(item.set).headertext">
|
||||
<SettingsPanel :settings="Settings.getSet(item.set)" :schemes="Settings.getSet(item.set).schemes" />
|
||||
</SettingsWrapper>
|
||||
</div>
|
||||
<div v-if="activeContent('css') || animatingContent('css')" :class="{active: activeContent('css'), animating: animatingContent('css')}">
|
||||
<CssEditorView />
|
||||
</div>
|
||||
<div v-if="activeContent('plugins') || animatingContent('plugins')" :class="{active: activeContent('plugins'), animating: animatingContent('plugins')}">
|
||||
<PluginsView />
|
||||
</div>
|
||||
<div v-if="activeContent('themes') || animatingContent('themes')" :class="{active: activeContent('themes'), animating: animatingContent('themes')}">
|
||||
<ThemesView />
|
||||
<SettingsWrapper v-else-if="item.set" :headertext="item.set.headertext">
|
||||
<SettingsPanel :settings="item.set" :schemes="item.set.schemes" />
|
||||
</SettingsWrapper>
|
||||
|
||||
<CssEditorView v-if="item.contentid === 'css'" />
|
||||
<PluginsView v-if="item.contentid === 'plugins'" />
|
||||
<ThemesView v-if="item.contentid === 'themes'" />
|
||||
</div>
|
||||
</ContentColumn>
|
||||
</SidebarView>
|
||||
|
@ -53,31 +58,22 @@
|
|||
// Imports
|
||||
import { shell } from 'electron';
|
||||
import { Settings } from 'modules';
|
||||
import { BdMenuItems } from 'ui';
|
||||
import { SidebarView, Sidebar, SidebarItem, ContentColumn } from './sidebar';
|
||||
import { SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView } from './bd';
|
||||
import { SvgX, MiGithubCircle, MiWeb, MiClose, MiTwitterCircle } from './common';
|
||||
|
||||
// Constants
|
||||
const sidebarItems = [
|
||||
{ text: 'Internal', _type: 'header' },
|
||||
{ id: 0, contentid: "core", text: 'Core', active: false, _type: 'button' },
|
||||
{ id: 1, contentid: "ui", text: 'UI', active: false, _type: 'button' },
|
||||
{ id: 2, contentid: "emotes", text: 'Emotes', active: false, _type: 'button' },
|
||||
{ id: 3, contentid: "css", text: 'CSS Editor', active: false, _type: 'button' },
|
||||
{ text: 'External', _type: 'header' },
|
||||
{ id: 4, contentid: "plugins", text: 'Plugins', active: false, _type: 'button' },
|
||||
{ id: 5, contentid: "themes", text: 'Themes', active: false, _type: 'button' }
|
||||
];
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
sidebarItems,
|
||||
BdMenuItems,
|
||||
activeIndex: -1,
|
||||
lastActiveIndex: -1,
|
||||
animating: false,
|
||||
first: true,
|
||||
settings: Settings.getSettings
|
||||
Settings,
|
||||
timeout: null,
|
||||
SettingsWrapper
|
||||
}
|
||||
},
|
||||
props: ['active', 'close'],
|
||||
|
@ -86,6 +82,20 @@
|
|||
SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView,
|
||||
MiGithubCircle, MiWeb, MiClose, MiTwitterCircle
|
||||
},
|
||||
computed: {
|
||||
sidebarItems() {
|
||||
return this.BdMenuItems.items;
|
||||
},
|
||||
sidebar() {
|
||||
const categories = {};
|
||||
for (let item of this.sidebarItems) {
|
||||
if (item.hidden) continue;
|
||||
const category = categories[item.category] || (categories[item.category] = []);
|
||||
category.push(item);
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
itemOnClick(id) {
|
||||
if (this.animating || id === this.activeIndex) return;
|
||||
|
@ -95,25 +105,34 @@
|
|||
this.lastActiveIndex = this.activeIndex;
|
||||
this.activeIndex = id;
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.first = false;
|
||||
this.animating = false;
|
||||
this.lastActiveIndex = -1;
|
||||
this.timeout = null;
|
||||
}, 400);
|
||||
},
|
||||
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;
|
||||
},
|
||||
changeSetting(set_id, category_id, setting_id, value) {
|
||||
Settings.setSetting(set_id, category_id, setting_id, value);
|
||||
Settings.saveSettings();
|
||||
closeContent() {
|
||||
if (this.activeIndex >= 0) this.sidebarItems.find(item => item.id === this.activeIndex).active = false;
|
||||
this.first = true;
|
||||
this.lastActiveIndex = this.activeIndex;
|
||||
this.activeIndex = -1;
|
||||
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.animating = false;
|
||||
this.lastActiveIndex = -1;
|
||||
this.timeout = null;
|
||||
}, 400);
|
||||
},
|
||||
openGithub() {
|
||||
shell.openExternal('https://github.com/JsSucks/BetterDiscordApp');
|
||||
|
@ -124,6 +143,12 @@
|
|||
openTwitter() {
|
||||
shell.openExternal('https://twitter.com/Jiiksi');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
active(active) {
|
||||
if (active) return;
|
||||
this.closeContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -10,22 +10,28 @@
|
|||
|
||||
<template>
|
||||
<div class="bd-settings-wrapper" :class="[{active: active}, 'platform-' + this.platform]">
|
||||
<div class="bd-settings-button" :class="{'bd-active': active}" @click="showSettings">
|
||||
<div class="bd-settings-button-btn" :class="[{'bd-loading': !loaded}]"></div>
|
||||
<div class="bd-settings-button" :class="{'bd-active': active, 'bd-animating': animating}" @click="showSettings">
|
||||
<div v-if="updating === 0" v-tooltip.right="'Checking for updates'" class="bd-settings-button-btn bd-loading"></div>
|
||||
<div v-else-if="updating === 2" v-tooltip.right="'Updates available!'" class="bd-settings-button-btn bd-updates"></div>
|
||||
<div v-else class="bd-settings-button-btn" :class="[{'bd-loading': !loaded}]"></div>
|
||||
</div>
|
||||
<BdSettings :active="active" :close="hideSettings" />
|
||||
<BdSettings ref="settings" :active="active" :close="hideSettings" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// Imports
|
||||
import { Events } from 'modules';
|
||||
import { Events, Settings } from 'modules';
|
||||
import { Modals } from 'ui';
|
||||
import BdSettings from './BdSettings.vue';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
updating: false,
|
||||
active: false,
|
||||
animating: false,
|
||||
timeout: null,
|
||||
platform: global.process.platform
|
||||
}
|
||||
},
|
||||
|
@ -40,16 +46,31 @@
|
|||
hideSettings() { this.active = false },
|
||||
toggleSettings() { this.active = !this.active },
|
||||
keyupListener(e) {
|
||||
if (document.getElementsByClassName('bd-backdrop').length) return;
|
||||
if (this.active && e.which === 27) return this.hideSettings();
|
||||
if (!e.metaKey && !e.ctrlKey || e.key !== 'b') return;
|
||||
this.toggleSettings();
|
||||
if (Modals.stack.length || !this.active || e.which !== 27) return;
|
||||
if (this.$refs.settings.activeIndex !== -1) this.$refs.settings.closeContent();
|
||||
else this.hideSettings();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
active(active) {
|
||||
this.animating = true;
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.animating = false;
|
||||
this.timeout = null;
|
||||
}, 400);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
Events.on('ready', e => this.loaded = true);
|
||||
Events.on('update-check-start', e => this.updating = 0);
|
||||
Events.on('update-check-end', e => this.updating = 1);
|
||||
Events.on('updates-available', e => this.updating = 2);
|
||||
window.addEventListener('keyup', this.keyupListener);
|
||||
|
||||
const menuKeybind = Settings.getSetting('core', 'default', 'menu-keybind');
|
||||
menuKeybind.on('keybind-activated', () => this.active = !this.active);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('keyup', this.keyupListener);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* BetterDiscord BD Message Badge Component
|
||||
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
|
||||
* All rights reserved.
|
||||
* https://betterdiscord.net
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
<template>
|
||||
<div class="bd-message-badges-wrap">
|
||||
<div v-if="developer == 'true'" v-tooltip="'BetterDiscord Developer'" class="bd-message-badge bd-message-badge-developer" @click="onClick"></div>
|
||||
<div v-else-if="webdev == 'true'" v-tooltip="'BetterDiscord Web Developer'" class="bd-message-badge bd-message-badge-developer" @click="onClick"></div>
|
||||
<div v-else-if="contributor == 'true'" v-tooltip="'BetterDiscord Contributor'" class="bd-message-badge bd-message-badge-contributor" @click="onClick"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// Imports
|
||||
import { shell } from 'electron';
|
||||
|
||||
export default {
|
||||
props: ['webdev', 'developer', 'contributor', 'hasBadges'],
|
||||
methods: {
|
||||
onClick() {
|
||||
if (this.developer) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp');
|
||||
if (this.webdev) return shell.openExternal('https://betterdiscord.net');
|
||||
if (this.contributor) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp/graphs/contributors');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -10,11 +10,11 @@
|
|||
|
||||
<template>
|
||||
<div class="bd-modals-container">
|
||||
<div v-for="(modal, index) in modals.stack" :key="`bd-modal-${index}`">
|
||||
<div class="bd-backdrop" :class="{'bd-backdrop-out': modal.closing}" :style="{opacity: index === 0 ? undefined : 0}"></div>
|
||||
<div v-for="(modal, index) in modals.stack" :key="`bd-modal-${modal.id}`">
|
||||
<div class="bd-backdrop" :class="{'bd-backdrop-out': closing}" :style="{opacity: index === 0 ? undefined : 0}"></div>
|
||||
<div class="bd-modal-wrap" :style="{transform: `scale(${downscale(index + 1, 0.2)})`, opacity: downscale(index + 1, 1)}">
|
||||
<div class="bd-modal-close-area" @click="closeModal(modal)"></div>
|
||||
<component :is="modal.component" />
|
||||
<keep-alive><component :is="modal.component" /></keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,8 +37,12 @@
|
|||
eventListener: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
closing() {
|
||||
return !this.modals.stack.find(m => !m.closing);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
console.log(this);
|
||||
Events.on('bd-refresh-modals', this.eventListener = () => {
|
||||
this.$forceUpdate();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,20 +10,12 @@
|
|||
|
||||
<template>
|
||||
<Card :item="plugin">
|
||||
<SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :checked="plugin.enabled" :change="() => plugin.enabled ? plugin.stop() : plugin.start()" />
|
||||
<SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :checked="plugin.enabled" :change="togglePlugin" />
|
||||
<ButtonGroup slot="controls">
|
||||
<Button v-tooltip="'Settings'" v-if="plugin.hasSettings" :onClick="() => showSettings(plugin)">
|
||||
<MiSettings size="18" />
|
||||
</Button>
|
||||
<Button v-tooltip="'Reload'" :onClick="() => reloadPlugin(plugin)">
|
||||
<MiRefresh size="18" />
|
||||
</Button>
|
||||
<Button v-tooltip="'Edit'" :onClick="editPlugin">
|
||||
<MiPencil size="18" />
|
||||
</Button>
|
||||
<Button v-tooltip="'Uninstall'" type="err">
|
||||
<MiDelete size="18" />
|
||||
</Button>
|
||||
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="plugin.hasSettings" :onClick="e => showSettings(e.shiftKey)"><MiSettings size="18" /></Button>
|
||||
<Button v-tooltip="'Reload'" :onClick="reloadPlugin"><MiRefresh size="18" /></Button>
|
||||
<Button v-tooltip="'Edit'" :onClick="editPlugin"><MiPencil size="18" /></Button>
|
||||
<Button v-tooltip="'Uninstall (shift + click to unload)'" :onClick="e => deletePlugin(e.shiftKey)" type="err"><MiDelete size="18" /></Button>
|
||||
</ButtonGroup>
|
||||
</Card>
|
||||
</template>
|
||||
|
@ -39,7 +31,7 @@
|
|||
settingsOpen: false
|
||||
}
|
||||
},
|
||||
props: ['plugin', 'togglePlugin', 'reloadPlugin', 'showSettings'],
|
||||
props: ['plugin', 'togglePlugin', 'reloadPlugin', 'deletePlugin', 'showSettings'],
|
||||
components: {
|
||||
Card, Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension
|
||||
},
|
||||
|
@ -52,6 +44,5 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -10,26 +10,24 @@
|
|||
|
||||
<template>
|
||||
<SettingsWrapper headertext="Plugins">
|
||||
<div class="bd-flex bd-flex-col bd-pluginsView">
|
||||
<div class="bd-flex bd-tabheader">
|
||||
<div class="bd-flex-grow bd-button" :class="{'bd-active': local}" @click="showLocal">
|
||||
<h3>Local</h3>
|
||||
<div class="bd-material-button" @click="refreshLocal">
|
||||
<MiRefresh />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bd-flex-grow bd-button" :class="{'bd-active': !local}" @click="showOnline">
|
||||
<h3>Online</h3>
|
||||
<div class="bd-material-button">
|
||||
<MiRefresh />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bd-tabbar" slot="header">
|
||||
<div class="bd-button" :class="{'bd-active': local}" @click="showLocal">
|
||||
<h3>Installed</h3>
|
||||
<RefreshBtn v-if="local" :onClick="refreshLocal" />
|
||||
</div>
|
||||
<div class="bd-button" :class="{'bd-active': !local}" @click="showOnline">
|
||||
<h3>Browse</h3>
|
||||
<RefreshBtn v-if="!local" :onClick="refreshOnline" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" :reloadPlugin="reloadPlugin" :showSettings="showSettings" />
|
||||
<PluginCard v-for="plugin in localPlugins" :plugin="plugin" :key="plugin.id" :togglePlugin="() => togglePlugin(plugin)" :reloadPlugin="() => reloadPlugin(plugin)" :deletePlugin="unload => deletePlugin(plugin, unload)" :showSettings="dont_clone => showSettings(plugin, dont_clone)" />
|
||||
</div>
|
||||
<div v-if="!local" class="bd-spinner-container">
|
||||
<div class="bd-spinner-2"></div>
|
||||
<div v-if="!local" class="bd-online-ph">
|
||||
<h3>Coming Soon</h3>
|
||||
<a href="https://v2.betterdiscord.net/plugins" target="_new">Website Browser</a>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsWrapper>
|
||||
|
@ -42,6 +40,7 @@
|
|||
import { SettingsWrapper } from './';
|
||||
import PluginCard from './PluginCard.vue';
|
||||
import { MiRefresh } from '../common';
|
||||
import RefreshBtn from '../common/RefreshBtn.vue';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -52,7 +51,8 @@
|
|||
},
|
||||
components: {
|
||||
SettingsWrapper, PluginCard,
|
||||
MiRefresh
|
||||
MiRefresh,
|
||||
RefreshBtn
|
||||
},
|
||||
methods: {
|
||||
showLocal() {
|
||||
|
@ -61,35 +61,39 @@
|
|||
showOnline() {
|
||||
this.local = false;
|
||||
},
|
||||
refreshLocal() {
|
||||
(async () => {
|
||||
await PluginManager.refreshPlugins();
|
||||
})();
|
||||
async refreshLocal() {
|
||||
await PluginManager.refreshPlugins();
|
||||
},
|
||||
togglePlugin(plugin) {
|
||||
async refreshOnline() {
|
||||
|
||||
},
|
||||
async togglePlugin(plugin) {
|
||||
// TODO Display error if plugin fails to start/stop
|
||||
try {
|
||||
if (plugin.enabled) {
|
||||
PluginManager.stopPlugin(plugin);
|
||||
} else {
|
||||
PluginManager.startPlugin(plugin);
|
||||
}
|
||||
await plugin.enabled ? PluginManager.stopPlugin(plugin) : PluginManager.startPlugin(plugin);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
reloadPlugin(plugin) {
|
||||
(async () => {
|
||||
try {
|
||||
await PluginManager.reloadPlugin(plugin);
|
||||
this.$forceUpdate();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
})();
|
||||
async reloadPlugin(plugin) {
|
||||
try {
|
||||
await PluginManager.reloadPlugin(plugin);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
showSettings(plugin) {
|
||||
return Modals.pluginSettings(plugin);
|
||||
async deletePlugin(plugin, unload) {
|
||||
try {
|
||||
if (unload) await PluginManager.unloadPlugin(plugin);
|
||||
else await PluginManager.deletePlugin(plugin);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
showSettings(plugin, dont_clone) {
|
||||
return Modals.contentSettings(plugin, null, {
|
||||
dont_clone
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue