update to master

This commit is contained in:
Zack Rauen 2018-03-11 22:38:03 -04:00
commit 6917724700
168 changed files with 43307 additions and 2321 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ Installers/**/*/packages
dist/
user.config.json
tests/data
/tests/themes/SimplerFlat

View File

@ -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>

View File

@ -0,0 +1,8 @@
<template>
<span class="edited" v-tooltip="ets">(edited)</span>
</template>
<script>
export default {
props: ['ets']
}
</script>

View File

@ -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>

View File

@ -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;
}
});
}
}

View File

@ -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

View File

@ -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",

View File

@ -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();
}
}

View File

@ -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);

View File

@ -1,31 +0,0 @@
/**
* BetterDiscord Content Config Utility
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export default class ContentConfig {
constructor(data) {
this.data = data;
}
map(cb) {
return this.data.map(cb);
}
strip() {
return this.map(cat => ({
category: cat.category,
settings: cat.settings.map(setting => ({
id: setting.id, value: setting.value
}))
}));
}
}

View File

@ -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

View File

@ -8,19 +8,33 @@
* LICENSE file in the root directory of this source tree.
*/
import { ClientIPC } from 'common';
import { FileUtils, ClientLogger as Logger, ClientIPC } from 'common';
import Settings from './settings';
import { DOM } from 'ui';
import filewatcher from 'filewatcher';
import path from 'path';
import electron from 'electron';
/**
* Custom css editor communications
*/
export default class {
export default new class {
constructor() {
this._scss = '';
this._css = '';
this._error = undefined;
this.editor_bounds = undefined;
this._files = undefined;
this._filewatcher = undefined;
this._watchfiles = undefined;
this.compiling = false;
}
/**
* Init css editor
*/
static init() {
init() {
ClientIPC.on('bd-get-scss', () => this.sendToEditor('set-scss', { scss: this.scss }));
ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss));
ClientIPC.on('bd-save-csseditor-bounds', (e, bounds) => this.saveEditorBounds(bounds));
@ -29,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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
/**

View File

@ -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);
}
}

View File

@ -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)) }
}

View File

@ -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 }

View File

@ -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()
]);
}

View File

@ -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';

View File

@ -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];
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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 }

View File

@ -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);
}
}

View File

@ -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];
}
}
}
}

View File

@ -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, '\\\'') : ''}'`;
}
}

View File

@ -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)
});
}
}

View File

@ -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 {

View File

@ -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);
}
}

View File

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

View File

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

View File

@ -0,0 +1,35 @@
/**
* BetterDiscord Multiple Choice Option Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Utils } from 'common';
export default class MultipleChoiceOption {
constructor(args) {
this.args = args.args || args;
}
get id() {
return this.args.id || this.value;
}
get text() {
return this.args.text;
}
get value() {
return this.args.value;
}
clone() {
return new MultipleChoiceOption(Utils.deepclone(this.args));
}
}

View File

@ -0,0 +1,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`};
}
}

View File

@ -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); }
}

View File

@ -0,0 +1,77 @@
/**
* BetterDiscord Settings Scheme Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Utils, ClientLogger as Logger } from 'common';
export default class SettingsScheme {
constructor(args) {
this.args = args.args || args;
this.args.settings = this.settings.map(({ category, settings }) => ({
category, settings: settings.map(({ id, value }) => ({
id, value
}))
}));
Object.freeze(this);
}
get id() {
return this.args.id;
}
get icon_url() {
return this.args.icon_url;
}
get name() {
return this.args.name;
}
get hint() {
return this.args.hint;
}
get settings() {
return this.args.settings || [];
}
isActive(set) {
for (let schemeCategory of this.settings) {
const category = set.categories.find(c => c.category === schemeCategory.category);
if (!category) {
Logger.warn('SettingsScheme', `Category ${schemeCategory.category} does not exist`);
return false;
}
for (let schemeSetting of schemeCategory.settings) {
const setting = category.settings.find(s => s.id === schemeSetting.id);
if (!setting) {
Logger.warn('SettingsScheme', `Setting ${schemeCategory.category}/${schemeSetting.id} does not exist`);
return false;
}
if (!Utils.compare(setting.value, schemeSetting.value)) return false;
}
}
return true;
}
applyTo(set) {
return set.merge({ settings: this.settings });
}
clone() {
return new SettingsScheme(Utils.deepclone(this.args));
}
}

View File

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

View File

@ -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(', ') + ',' : '()';
}
}

View File

@ -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); }
}

View File

@ -0,0 +1,30 @@
/**
* BetterDiscord Boolean Setting Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Setting from './basesetting';
export default class BoolSetting extends Setting {
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return false;
}
/**
* Whether the setting should take the full width of the settings panel.
* This is always false for boolean settings.
*/
get fullwidth() {
return false;
}
}

View File

@ -0,0 +1,30 @@
/**
* BetterDiscord Colour Setting Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Setting from './basesetting';
export default class ColourSetting extends Setting {
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return 'rgba(0, 0, 0, 0)';
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String|Promise}
*/
toSCSS() {
return this.value;
}
}

View File

@ -0,0 +1,80 @@
/**
* BetterDiscord Custom Setting Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Setting from './basesetting';
import SettingsCategory from '../settingscategory';
import SettingsScheme from '../settingsscheme';
import path from 'path';
export default class CustomSetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
if (this.args.class_file && this.path)
this.setClass(this.args.class_file, this.args.class);
}
/**
* The file to load the custom setting from.
*/
get file() {
return this.args.file;
}
/**
* The name of a function on the plugin's main object that will be called to get a Vue component or a HTML element.
*/
get function() {
return this.args.function;
}
/**
* The name of an export of {file}, or a Vue component.
*/
get component() {
return this.args.component;
}
/**
* Whether to show a debug view under the custom setting's component.
*/
get debug() {
return this.args.debug || false;
}
/**
* Sets the path of the plugin/theme this setting is part of.
* Used by settings of type "array", "custom" and "file".
* @param {String} contentPath The plugin/theme's directory path
*/
setContentPath(_path) {
this.args.path = _path;
if (this.args.class_file)
this.setClass(this.args.class_file, this.args.class);
}
/**
* Replaces the custom setting's prototype with a new one that extends CustomSetting.
* @param {String} classFile The path of a file relative to the plugin/theme's directory that will be required
* @param {String} classExport The name of a property of the file's exports that will be used (optional)
*/
setClass(class_file, class_export) {
const component = window.require(path.join(this.path, class_file));
const setting_class = class_export ? component[class_export](CustomSetting) : component.default ? component.default(CustomSetting) : component(CustomSetting);
if (!(setting_class.prototype instanceof CustomSetting))
throw {message: 'Custom setting class function returned a class that doesn\'t extend from CustomSetting.'};
this.__proto__ = setting_class.prototype;
}
}

View File

@ -0,0 +1,63 @@
/**
* BetterDiscord Dropdown Setting Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Setting from './basesetting';
import MultipleChoiceOption from '../multiplechoiceoption';
export default class DropdownSetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
this.args.options = this.options.map(option => new MultipleChoiceOption(option));
}
/**
* The current value.
*/
get value() {
const selected = this.selected_option;
if (selected) return selected.value;
return this.args.value;
}
set value(value) {
const selected = this.options.find(option => option.value === value);
if (selected) this.setValue(selected.id);
else this.setValue(value);
}
/**
* An array of MultipleChoiceOption objects.
*/
get options() {
return this.args.options || [];
}
/**
* The currently selected option.
*/
get selected_option() {
return this.options.find(option => option.id === this.args.value);
}
set selected_option(selected_option) {
this.args.value = selected_option.id;
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String}
*/
toSCSS() {
return this.value;
}
}

View File

@ -0,0 +1,61 @@
/**
* BetterDiscord File Setting Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { ThemeManager } from 'modules';
import { FileUtils, ClientIPC } from 'common';
import Setting from './basesetting';
import path from 'path';
export default class FileSetting extends Setting {
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return [];
}
/**
* An object that will be passed to electron.dialog.showOpenDialog.
*/
get dialogOptions() {
return this.args.dialogOptions || {};
}
/**
* Opens the file selection dialog and sets this file setting's value to an array of selected file paths.
* @return {Promise}
*/
async openDialog() {
if (this.disabled) return;
const filenames = await ClientIPC.send('bd-native-open', this.dialogOptions);
if (filenames)
this.value = filenames;
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String|Promise}
*/
async toSCSS() {
if (!this.value || !this.value.length) return '()';
const files = [];
for (let filepath of this.value) {
const buffer = await FileUtils.readFileBuffer(path.resolve(this.path, filepath));
const type = await FileUtils.getFileType(buffer);
files.push(`(data: ${ThemeManager.toSCSSString(buffer.toString('base64'))}, type: ${ThemeManager.toSCSSString(type.mime)}, url: ${ThemeManager.toSCSSString(await FileUtils.toDataURI(buffer, type.mime))})`);
}
return files.length ? files.join(', ') : '()';
}
}

View File

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

View File

@ -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;
}
}

View File

@ -0,0 +1,63 @@
/**
* BetterDiscord Radio Setting Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Setting from './basesetting';
import MultipleChoiceOption from '../multiplechoiceoption';
export default class RadioSetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
this.args.options = this.options.map(option => new MultipleChoiceOption(option));
}
/**
* The current value.
*/
get value() {
const selected = this.selected_option;
if (selected) return selected.value;
return this.args.value;
}
set value(value) {
const selected = this.options.find(option => option.value === value);
if (selected) this.setValue(selected.id);
else this.setValue(value);
}
/**
* An array of MultipleChoiceOption objects.
*/
get options() {
return this.args.options || [];
}
/**
* The currently selected option.
*/
get selected_option() {
return this.options.find(option => option.id === this.args.value);
}
set selected_option(selected_option) {
this.args.value = selected_option.id;
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String}
*/
toSCSS() {
return this.value;
}
}

View File

@ -0,0 +1,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;
}
}

View File

@ -0,0 +1,37 @@
/**
* BetterDiscord String Setting Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Setting from './basesetting';
export default class StringSetting extends Setting {
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return '';
}
/**
* Whether the setting should take the full width of the settings panel.
* This is always true when multiline is true.
*/
get fullwidth() {
return this.args.fullwidth && !this.multiline;
}
/**
* Whether to display a multiline text area instead of a single line text input.
*/
get multiline() {
return this.args.multiline || false;
}
}

View File

@ -2,3 +2,4 @@ export * from './socketstructs/channel';
export * from './socketstructs/generic';
export * from './socketstructs/guild';
export * from './socketstructs/message';
export * from './socketstructs/user';

View File

@ -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 { }

View File

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

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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;
}
}
}

View File

@ -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';

View File

@ -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 {
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;
}
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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;

View File

@ -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';

View File

@ -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) }
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -8,8 +8,6 @@
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
padding: 10px 10px 10px 0;
margin: 10px 0;
@include scrollbar;
}

View File

@ -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;
}
}
}

View File

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

View File

@ -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';

View File

@ -0,0 +1,3 @@
.edit-message .markup.mutable {
display: none;
}

View File

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

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}
}

View File

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

View File

@ -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;
}
}

View File

@ -1,3 +1,4 @@
@import './main.scss';
@import './sidebar.scss';
@import './content.scss';
@import './settingswrap.scss';

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -41,10 +41,11 @@
&:hover,
&.active {
background: $colbdblue;
color: #fff;
}
&.active {
color: #FFF;
cursor: default;
}
}
}

286
client/src/ui/automanip.js Normal file
View File

@ -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
);
}
}

61
client/src/ui/bdmenu.js Normal file
View File

@ -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 };

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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();
});

View File

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

View File

@ -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>

View File

@ -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