Merge pull request #58 from JsSucks/refactor

Client refactor
This commit is contained in:
Alexei Stukov 2018-01-31 13:52:56 +02:00 committed by GitHub
commit 363c7e9f7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 1708 additions and 1467 deletions

View File

@ -35,7 +35,7 @@
},
{
"id": "ui",
"text": "UI",
"text": "UI",
"settings": [
]
@ -50,6 +50,6 @@
"text": "Security",
"settings": [
]
]
}
]

View File

@ -1,54 +1,40 @@
/**
* BetterDiscord Client Core
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
* 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.
*/
'use strict';
const styles = require('./styles/index.scss');
const { Global, Logger, Utils, PluginManager, BDIpc, WebpackModules, SocketProxy, Events, Vendor } = require('./modules');
//const { UI } = require('./modules/ui/index.jsx');
import { DOM, BdUI } from 'ui';
import BdCss from './styles/index.scss';
import { Events, CssEditor, Globals, PluginManager, ThemeManager } from 'modules';
class BetterDiscord {
constructor() {
window.bdUtils = Utils;
window.wpm = WebpackModules;
Events.on('global-ready', e => {
const { UI } = require('./modules/ui/vueui.js');
this.ui = new UI();
this.init();
});
//Inject styles to head for now
const style = document.createElement('style');
style.id = 'bd-main';
style.type = 'text/css';
style.appendChild(document.createTextNode(styles));
document.head.appendChild(style);
this.init();
window.pom = PluginManager;
DOM.injectStyle(BdCss, 'bdmain');
Events.on('global-ready', this.globalReady.bind(this));
}
async init() {
try {
await PluginManager.loadAllPlugins();
} catch (err) {
}
await PluginManager.loadAllPlugins();
await ThemeManager.loadAllThemes();
Events.emit('ready');
}
globalReady() {
this.vueInstance = BdUI.injectUi();
(async () => {
this.init();
})();
}
}
if (window.BetterDiscord) {
Logger.log('main', 'Attempting to inject again?');
} else {
let bdInstance = new BetterDiscord();
window.BetterDiscord = {'vendor': Vendor};
}
}

View File

@ -0,0 +1,155 @@
/**
* BetterDiscord Content Manager 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 Globals from './globals';
import { FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
export default class {
static get localContent() {
return this._localContent ? this._localContent : (this._localContent = []);
}
static get contentPath() {
return this._contentPath ? this._contentPath : (this._contentPath = Globals.getObject('paths').find(path => path.id === this.pathId).path);
}
static async loadAllContent() {
try {
await FileUtils.ensureDirectory(this.contentPath);
const directories = await FileUtils.listDirectory(this.contentPath);
for (let dir of directories) {
try {
await this.preloadContent(dir);
} catch (err) {
//We don't want every plugin/theme to fail loading when one does
Logger.err(this.moduleName, err);
}
}
return this.localContent;
} catch (err) {
throw err;
}
}
static async refreshContent() {
if (!this.localContent.length) return this.loadAllContent();
try {
await FileUtils.ensureDirectory(this.contentPath);
const directories = await FileUtils.listDirectory(this.contentPath);
for (let dir of directories) {
// If content is already loaded this should resolve.
if (this.getContentByDirName(dir)) continue;
try {
// Load if not
await this.preloadContent(dir);
} catch (err) {
//We don't want every plugin/theme to fail loading when one does
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);
}
return this.localContent;
} catch (err) {
throw err;
}
}
static async preloadContent(dirName, reload = false, index) {
try {
const contentPath = path.join(this.contentPath, dirName);
await FileUtils.directoryExists(contentPath);
if (!reload) {
const loaded = this.localContent.find(content => content.contentPath === contentPath);
if (loaded) {
throw { 'message': `Attempted to load already loaded user content: ${path}` };
}
}
const readConfig = await this.readConfig(contentPath);
const mainPath = path.join(contentPath, readConfig.main);
const userConfig = {
enabled: false,
config: readConfig.defaultConfig
};
try {
const readUserConfig = await this.readUserConfig(contentPath);
userConfig.config = userConfig.defaultConfig.map(config => {
const userSet = readUserConfig.config.find(c => c.id === config.id);
return userSet || config;
});
} 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*/ }
const configs = {
defaultConfig: readConfig.defaultConfig,
userConfig
}
const paths = {
contentPath,
dirName,
mainPath
}
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main);
if (reload) this.localContent[index] = content;
else this.localContent.push(content);
return content;
} catch (err) {
throw err;
}
}
static async readConfig(configPath) {
configPath = path.resolve(configPath, 'config.json');
return FileUtils.readJsonFromFile(configPath);
}
static async readUserConfig(configPath) {
configPath = path.resolve(configPath, 'user.config.json');
return FileUtils.readJsonFromFile(configPath);
}
//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 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) }
}

View File

@ -1,29 +0,0 @@
/**
* BetterDiscord Client IPC Module
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - 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 { ipcRenderer } = require('electron');
class BDIpc {
static on(channel, cb) {
ipcRenderer.on(channel, (event, message) => cb(event, message));
}
static async send(channel, message) {
channel = channel.startsWith('bd-') ? channel : `bd-${channel}`;
const __eid = Date.now().toString();
ipcRenderer.send(channel, Object.assign(message ? message : {}, { __eid }));
return new Promise((resolve, reject) => {
ipcRenderer.once(__eid, (event, arg) => resolve(arg));
});
}
}
module.exports = { BDIpc };

View File

@ -1,34 +0,0 @@
/**
* BetterDiscord CSS Editor
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - 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 { Module } = require('./modulebase');
const { BDIpc } = require('./bdipc');
const $ = require('jquery');
class CssEditor extends Module {
setInitialState() {
this.state = {
css: ''
}
this.customcss = $('<style id="customcss">').appendTo("head");
window.cssEditor = this;
BDIpc.on("bd-update-css", (_, css) => this.customcss.text(css));
BDIpc.on("bd-save-css", (_, css) => this.setState({css}));
}
show() {
BDIpc.send('openCssEditor', {}).then(() => BDIpc.send('setCss', {css: this.state.css}));
}
}
const _instance = new CssEditor();
module.exports = { 'CssEditor': _instance }

View File

@ -1,60 +0,0 @@
/**
* BetterDiscord Discord Socket Proxy
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - 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 { Events } = require('./events');
const { Module } = require('./modulebase');
const { Global } = require('./global');
const { Utils } = require('./utils');
class SocketProxy extends Module {
events() {
Events.on('socket-created', this.socketCreated);
}
bindings() {
this.socketCreated = this.socketCreated.bind(this);
this.onmessage = this.onmessage.bind(this);
}
socketCreated() {
const wsHook = Global.getObject('wsHook');
wsHook.addEventListener('message', this.onmessage);
}
onmessage(e) {
console.log(e);
//TODO fix unpacking
const unpacked = this.erlpack.unpack(e.data);
console.log(unpacked);
}
get erlpack() {
if (this._erlpack) return this._erlpack;
try {
this._erlpack = window.require('erlpack');
} catch (err) {
console.log(err);
try {
this._erlpack = window.require('discord_erlpack');
} catch (err) {
console.log(err);
}
}
return this._erlpack;
}
}
const _instance = new SocketProxy();
module.exports = { 'SocketProxy': _instance }

View File

@ -1,70 +0,0 @@
/**
* BetterDiscord Client Globals
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - 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 { Module } = require('./modulebase');
const { Events } = require('./events');
const { BDIpc } = require('./bdipc');
const { WebpackModules } = require('./webpackmodules');
class Global extends Module {
constructor(args) {
super(args);
this.first();
window.gl = this;
}
bindings() {
this.first = this.first.bind(this);
this.setWS = this.setWS.bind(this);
this.getObject = this.getObject.bind(this);
}
first() {
(async () => {
const config = await BDIpc.send('getConfig');
this.setState(config);
/* const getReact = await WebpackModules.getModuleByProps(('createElement', 'cloneElement'));
this.React = getReact[0].exports;
window.React = this.React;
const getReactDom = await WebpackModules.getModuleByProps(('render', 'findDOMNode'));
this.reactDOM = getReactDom[0].exports;*/
// this.setState(Object.assign(config, { React, reactDOM }));
Events.emit('global-ready');
})();
if (window.__bd) {
this.setState(window.__bd);
window.__bd = {
setWS: this.setWS
}
Events.emit('socket-created');
}
}
setWS(wSocket) {
const state = this.state;
state.wsHook = wSocket;
this.setState(state);
Events.emit('socket-created');
}
getObject(name) {
return this.state[name];
}
getLoadedModule(name) {
return this[name];
}
}
const _instance = new Global();
module.exports = { 'Global': _instance }

View File

@ -1,17 +0,0 @@
/**
* BetterDiscord Plugin Base Class
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - 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.
*/
class Plugin {
constructor() {}
}
module.exports = { Plugin }

View File

@ -1,275 +0,0 @@
/**
* BetterDiscord Plugin Manager
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - 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 { Module } = require('./modulebase');
const { FileUtils, Logger } = require('./utils');
const { Global } = require('./global');
const path = window.require('path');
class Plugin {
constructor(pluginInternals) {
this.__pluginInternals = pluginInternals;
this.hasSettings = this.pluginConfig && this.pluginConfig.length > 0;
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
}
get configs() { return this.__pluginInternals.configs }
get info() { return this.__pluginInternals.info }
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 name() { return this.info.name }
get authors() { return this.info.authors }
get version() { return this.info.version }
get pluginPath() { return this.paths.pluginPath }
get dirName() { return this.paths.dirName }
get enabled() { return this.userConfig.enabled }
get pluginConfig() { return this.userConfig.pluginConfig }
start() {
if (this.onStart) {
const started = this.onStart();
if (started) {
return this.userConfig.enabled = true;
}
return false;
}
return this.userConfig.enabled = true; //Assume plugin started since it doesn't have onStart
}
stop() {
if (this.onStop) {
const stopped = this.onStop();
if (stopped) {
this.userConfig.enabled = false;
return true;
}
return false;
}
this.userConfig.enabled = false;
return true; //Assume plugin stopped since it doesn't have onStop
}
}
class PluginManager extends Module {
setInitialState() {
window.pm = this;
this.setState({
plugins: []
});
}
get plugins() {
return this.state.plugins;
}
get pluginsPath() {
return Global.getObject('paths').find(path => path.id === 'plugins').path;
}
async loadAllPlugins() {
try {
const directories = await FileUtils.readDir(this.pluginsPath);
for (let dir of directories) {
try {
await this.loadPlugin(dir);
} catch (err) {
//We don't want every plugin to fail loading when one does
Logger.err('PluginManager', err);
}
}
return this.plugins;
} catch (err) {
throw err;
}
}
async refreshPlugins() {
if (this.plugins.length <= 0) return this.loadAllPlugins();
try {
const directories = await FileUtils.readDir(this.pluginsPath);
for (let dir of directories) {
//If a plugin is already loaded this should resolve.
if (this.getPluginByDirName(dir)) continue;
try {
//Load the plugin if not
await this.loadPlugin(dir);
} catch (err) {
//We don't want every plugin to fail loading when one does
Logger.err('PluginManager', err);
}
}
for (let plugin of this.plugins) {
if (directories.includes(plugin.dirName)) continue;
//Plugin was deleted manually, stop it and remove any reference
try {
if (plugin.enabled) plugin.stop();
const { pluginPath } = plugin;
const index = this.getPluginIndex(plugin);
delete window.require.cache[window.require.resolve(pluginPath)];
this.plugins.splice(index, 1);
} catch (err) {
//This might fail but we don't have any other option at this point
Logger.err('PluginManager', err);
}
}
} catch (err) {
throw err;
}
}
async loadPlugin(pluginPath, reload = false, index) {
const { plugins } = this.state;
const dirName = pluginPath;
try {
pluginPath = path.join(this.pluginsPath, pluginPath);
// Make sure this is a directory
await FileUtils.directoryExists(pluginPath);
if (!reload) {
const loaded = plugins.find(plugin => plugin.pluginPath === pluginPath);
if (loaded) {
throw { 'message': 'Attempted to load an already loaded plugin' };
}
}
const readConfig = await this.readConfig(pluginPath);
const mainPath = path.join(pluginPath, readConfig.main);
const userConfigPath = path.join(pluginPath, 'user.config.json');
const userConfig = {
enabled: false,
pluginConfig: readConfig.defaultConfig
};
try {
const readUserConfig = await FileUtils.readJsonFromFile(userConfigPath);
//userConfig = Object.assign({}, userConfig, readUserConfig);
userConfig.pluginConfig = readConfig.defaultConfig.map(config => {
const userSet = readUserConfig.pluginConfig.find(c => c.id === config.id);
return userSet || config;
});
} 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*/}
const configs = {
defaultConfig: readConfig.defaultConfig,
userConfig
};
const plugin = window.require(mainPath)(Plugin, {}, {});
const instance = new plugin({configs, info: readConfig.info, main: readConfig.main, paths: { pluginPath, dirName }});
if (instance.enabled) instance.start();
if (reload) plugins[index] = instance;
else plugins.push(instance);
this.setState(plugins);
return instance;
} catch (err) {
throw err;
}
}
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)];
// this.plugins.splice(index, 1);
return this.loadPlugin(dirName, true, index);
}
//TODO make this nicer
findPlugin(wild) {
let plugin = this.getPluginByName(wild);
if (plugin) return plugin;
plugin = this.getPluginById(wild);
if (plugin) return plugin;
plugin = this.getPluginByPath(wild);
if (plugin) return plugin;
return this.getPluginByDirName(wild);
}
getPluginIndex(plugin) { return this.plugins.findIndex(p => p === plugin) }
getPluginByName(name) { return this.plugins.find(p => p.name === name) }
getPluginById(id) { return this.plugins.find(p => p.id === id) }
getPluginByPath(path) { return this.plugins.find(p => p.pluginPath === path) }
getPluginByDirName(dirName) { return this.plugins.find(p => p.dirName === dirName) }
stopPlugin(name) {
const plugin = name instanceof Plugin ? name : this.getPluginByName(name);
try {
if (plugin) return plugin.stop();
} catch (err) {
Logger.err('PluginManager', err);
}
return true; //Return true anyways since plugin doesn't exist
}
startPlugin(name) {
const plugin = name instanceof Plugin ? name : this.getPluginByName(name);
try {
if (plugin) return plugin.start();
} catch (err) {
Logger.err('PluginManager', err);
}
return true; //Return true anyways since plugin doesn't exist
}
async readConfig(path) {
path = `${path}/config.json`;
return FileUtils.readJsonFromFile(path);
}
}
const _instance = new PluginManager();
async function pluginManager(pluginName) {
try {
//Load test plugin
const plugin = await _instance.loadPlugin(pluginName);
//Attempt to load the same plugin again
const plugin2 = await _instance.loadPlugin(pluginName);
return true;
} catch (err) {
console.log(`Failed to load plugin! ${err.message}`);
}
try {
//Reload test plugin
const reloadedPlugin = await _instance.reloadPlugin('Example Plugin');
} catch (err) {
console.log(`Failed to reload plugin! ${err.message}`);
}
}
if (window.bdTests) window.bdTests.pluginManager = pluginManager;
else window.bdTests = { pluginManager };
module.exports = { PluginManager: _instance, Plugin };

View File

@ -1,11 +0,0 @@
const defaultSettings = require('../../data/user.settings.default');
class Settings {
static get getSettings() {
return defaultSettings;
}
}
module.exports = { Settings };

View File

@ -0,0 +1,26 @@
/**
* BetterDiscord CSS Editor 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 'common';
import { DOM } from 'ui';
export default class {
static async show() {
const t = await ClientIPC.send('openCssEditor', {});
ClientIPC.send('setCss', { css: DOM.getStyleCss('bd-customcss') });
ClientIPC.on('bd-update-css', this.updateCss);
}
static updateCss(e, css) {
DOM.injectStyle(css, 'bd-customcss');
}
}

View File

@ -1,19 +1,17 @@
/**
* BetterDiscord Events
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* BetterDiscord Events Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
* 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.
* LICENSE file in the root directory of this source tree.
*/
const { EventEmitter } = require('events');
import { EventEmitter } from 'events';
const emitter = new EventEmitter();
class Events {
export default class {
static on(eventName, callBack) {
emitter.on(eventName, callBack);
}
@ -25,7 +23,4 @@ class Events {
static emit(...args) {
emitter.emit(...args);
}
}
module.exports = { Events }

View File

@ -0,0 +1,63 @@
/**
* BetterDiscord Globals 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 Module from './module';
import Events from './events';
import { ClientIPC } from 'bdipc';
export default new class extends Module {
constructor(args) {
super(args);
this.first();
}
bindings() {
this.first = this.first.bind(this);
this.setWS = this.setWS.bind(this);
this.getObject = this.getObject.bind(this);
}
first() {
(async() => {
const config = await ClientIPC.send('getConfig');
this.setState(config);
// This is for Discord to stop error reporting :3
window.BetterDiscord = {
'version': config.version,
'v': config.version
};
window.jQuery = {};
Events.emit('global-ready');
})();
if (window.__bd) {
this.setState(window.__bd);
window.__bd = {
setWS: this.setWS
}
Events.emit('socket-created', this.state.wsHook);
}
}
setWS(wSocket) {
const state = this.state;
state.wsHook = wSocket;
this.setState(state);
Events.emit('socket-created');
}
getObject(name) {
return this.state[name];
}
}

View File

@ -1,11 +0,0 @@
export { Global } from './core/global';
export { Logger, Utils, FileUtils } from './core/utils';
export { PluginManager, Plugin } from './core/pluginmanager';
export { Pluging } from './core/plugin';
export { BDIpc } from './core/bdipc';
export { WebpackModules } from './core/webpackmodules';
export { Events } from './core/events';
export { SocketProxy } from './core/discordsocket';
export { CssEditor } from './core/csseditor';
export { Settings } from './core/settings';
export { Vendor } from './core/vendor';

View File

@ -12,7 +12,7 @@
Base Module that every non-static module should extend
*/
class Module {
export default class Module {
constructor(args) {
this.__ = {
@ -43,5 +43,3 @@ class Module {
get state() { return this.__.state; }
}
module.exports = { Module };

View File

@ -0,0 +1,7 @@
export { default as Events } from './events';
export { default as Settings } from './settings';
export { default as CssEditor } from './csseditor';
export { default as PluginManager } from './pluginmanager';
export { default as ThemeManager } from './thememanager';
export { default as Globals } from './globals';
export { default as Vendor } from './vendor';

View File

@ -0,0 +1,58 @@
/**
* BetterDiscord Plugin 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.
*/
export default class {
constructor(pluginInternals) {
this.__pluginInternals = pluginInternals;
this.hasSettings = this.pluginConfig && this.pluginConfig.length > 0;
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
}
get configs() { return this.__pluginInternals.configs }
get info() { return this.__pluginInternals.info }
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 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 pluginConfig() { return this.userConfig.config }
start() {
if (this.onStart) {
const started = this.onStart();
if (started) {
return this.userConfig.enabled = true;
}
return false;
}
return this.userConfig.enabled = true; //Assume plugin started since it doesn't have onStart
}
stop() {
if (this.onStop) {
const stopped = this.onStop();
if (stopped) {
this.userConfig.enabled = false;
return true;
}
return false;
}
this.userConfig.enabled = false;
return true; //Assume plugin stopped since it doesn't have onStop
}
}

View File

@ -0,0 +1,94 @@
/**
* BetterDiscord Plugin Manager 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 ContentManager from './contentmanager';
import Plugin from './plugin';
export default class extends ContentManager {
static get localPlugins() {
return this.localContent;
}
static get moduleName() {
return 'PluginManager';
}
static get pathId() {
return 'plugins';
}
static get loadAllPlugins() { return this.loadAllContent }
static get refreshPlugins() { return this.refreshContent }
static get loadContent() { return this.loadPlugin }
static async loadPlugin(paths, configs, info, main) {
const plugin = window.require(paths.mainPath)(Plugin, {}, {});
const instance = new plugin({ configs, info, main, paths: { contentPath: paths.contentPath, dirName: paths.dirName } });
if (instance.enabled) instance.start();
return instance;
}
static get unloadContent() { return this.unloadPlugin }
static async unloadPlugin(plugin) {
try {
if (plugin.enabled) plugin.stop();
const { pluginPath } = plugin;
const index = this.getPluginIndex(plugin);
delete window.require.cache[window.require.resolve(pluginPath)];
this.localPlugins.splice(index, 1);
} catch (err) {
//This might fail but we don't have any other option at this point
Logger.err('PluginManager', err);
}
}
static async reloadPlugin(plugin) {
const _plugin = plugin instanceof Plugin ? plugin : this.findPlugin(plugin);
if (!_plugin) throw { 'message': 'Attempted to reload a plugin that is not loaded?' };
if (!_plugin.stop()) throw { 'message': 'Plugin failed to stop!' };
const index = this.getPluginIndex(_plugin);
const { pluginPath, dirName } = _plugin;
delete window.require.cache[window.require.resolve(pluginPath)];
return this.preloadContent(dirName, true, index);
}
static stopPlugin(name) {
const plugin = name instanceof Plugin ? name : this.getPluginByName(name);
try {
if (plugin) return plugin.stop();
} catch (err) {
// Logger.err('PluginManager', err);
}
return true; //Return true anyways since plugin doesn't exist
}
static startPlugin(name) {
const plugin = name instanceof Plugin ? name : this.getPluginByName(name);
try {
if (plugin) return plugin.start();
} catch (err) {
// Logger.err('PluginManager', err);
}
return true; //Return true anyways since plugin doesn't exist
}
static get findPlugin() { return this.findContent }
static get getPluginIndex() { return this.getContentIndex }
static get getPluginByName() { return this.getContentByName }
static get getPluginById() { return this.getContentById }
static get getPluginByPath() { return this.getContentByPath }
static get getPluginByDirName() { return this.getContentByDirName }
}

View File

@ -0,0 +1,17 @@
/**
* BetterDiscord Settings 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 defaultSettings from '../data/user.settings.default';
export default class {
static get getSettings() {
return defaultSettings;
}
}

View File

@ -0,0 +1,27 @@
/**
* BetterDiscord Theme Manager 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 ContentManager from './contentmanager';
export default class extends ContentManager {
static get localThemes() {
return this.localContent;
}
static get pathId() {
return 'themes';
}
static get loadAllThemes() {
return this.loadAllContent;
}
}

View File

@ -1,96 +0,0 @@
<template src="./templates/BdSettings.html"></template>
<script>
const { Settings } = require('../../');
/*Imports*/
import { SidebarView, Sidebar, SidebarItem, ContentColumn } from './sidebar';
import { CoreSettings, UISettings, EmoteSettings, PluginsView, CssEditorView } from './bd';
const components = { SidebarView, Sidebar, SidebarItem, ContentColumn, CoreSettings, UISettings, EmoteSettings, PluginsView, CssEditorView };
/*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' }
];
/*Methods*/
function itemOnClick(id) {
if (this.animating || id === this.activeIndex) return;
if (this.activeIndex >= 0) this.sidebarItems.find(item => item.id === this.activeIndex).active = false;
this.sidebarItems.find(item => item.id === id).active = true;
this.animating = true;
this.lastActiveIndex = this.activeIndex;
this.activeIndex = id;
if (this.first) {
this.first = false;
}
setTimeout(() => {
this.animating = false;
this.lastActiveIndex = -1;
}, 400);
}
function animatingContent(s) {
const item = this.sidebarItems.find(item => item.contentid === s);
if (!item) return false;
return item.id === this.lastActiveIndex;
}
function activeContent(s) {
const item = this.sidebarItems.find(item => item.contentid === s);
if (!item) return false;
return item.id === this.activeIndex;
}
function enableSetting(cat, id) {
switch (cat) {
case 'core':
return this.coreSettings.find(setting => setting.id === id).enabled = true;
}
}
function disableSetting(cat, id) {
switch (cat) {
case 'core':
return this.coreSettings.find(setting => setting.id === id).enabled = false;
}
}
const methods = { itemOnClick, animatingContent, activeContent, enableSetting, disableSetting };
export default {
components,
props: ['active', 'close'],
methods,
data() {
return {
sidebarItems,
activeIndex: -1,
lastActiveIndex: -1,
animating: false,
first: true,
settings: Settings.getSettings
}
},
computed: {
coreSettings: function () {
return this.settings.find(settingset => settingset.id === 'core').settings;
}
},
updated: function () {
if (this.active) return;
this.activeIndex = this.lastActiveIndex = -1;
this.sidebarItems.forEach(item => item.active = false);
}
}
</script>

View File

@ -1,54 +0,0 @@
<template src="./templates/BdSettingsWrapper.html"></template>
<script>
const { Events } = require('../../');
/*Imports*/
import BdSettings from './BdSettings.vue';
const components = { BdSettings };
/*Methods*/
function showSettings() {
if (!this.loaded) return;
this.active = true;
}
function hideSettings() {
this.active = false;
}
const methods = { showSettings, hideSettings };
let globalKeyListener;
export default {
components,
methods,
data() {
return {
loaded: false,
active: false,
platform: global.process.platform
}
},
created: function () {
Events.on('ready', e => {
this.loaded = true;
});
window.addEventListener('keyup', globalKeyListener = e => {
if (this.active && e.which === 27) {
this.hideSettings();
return;
}
if (!e.metaKey && !e.ctrlKey || e.key !== 'b') return;
!this.active ? this.showSettings() : this.hideSettings();
e.stopImmediatePropagation();
});
},
destroyed: function () {
if (globalKeyListener) window.removeEventListener('keyup', globalKeyListener);
}
}
</script>

View File

@ -1,29 +0,0 @@
<template>
<SettingsWrapper headertext="Core Settings">
<div class="bd-form-item" v-for="setting in settings" :key="setting.id">
<SettingSwitch key="setting.id" :setting="setting" :onClick="settingOnClick" :disabled="setting.disabled"/>
<div class="bd-form-divider"/>
</div>
</SettingsWrapper>
</template>
<script>
/*Imports*/
import { SettingsWrapper } from './';
import { SettingSwitch } from '../generic';
const components = { SettingsWrapper, SettingSwitch };
/*Methods*/
function settingOnClick(setting) {
if (setting.enabled) return this.disableSetting('core', setting.id);
this.enableSetting('core', setting.id);
}
const methods = { settingOnClick };
export default {
components,
methods,
props: ['settings', 'enableSetting', 'disableSetting']
}
</script>

View File

@ -1,32 +0,0 @@
<template src="./templates/CssEditor.html"></template>
<script>
const { CssEditor } = require('../../../');
/*Imports*/
import { SettingsWrapper } from './';
import { SettingSwitch, FormButton } from '../generic';
const components = { SettingsWrapper, SettingSwitch, FormButton };
function openInternalEditor() {
CssEditor.show();
}
function settingClicked() {
this.dummySetting.checked = !this.dummySetting.checked;
}
export default {
components,
methods: { openInternalEditor, settingClicked },
data() {
return {
dummySetting: {
title: "Live Update",
hint: "Automatically update client css when saved.",
checked: true
}
}
}
}
</script>

View File

@ -1,14 +0,0 @@
<template>
<SettingsWrapper headertext="Emote Settings">
</SettingsWrapper>
</template>
<script>
/*Imports*/
import { SettingsWrapper } from './';
const components = { SettingsWrapper };
export default {
components
}
</script>

View File

@ -1,37 +0,0 @@
<template src="./templates/PluginCard.html"></template>
<script>
/*Imports*/
import { Button, ButtonGroup, SettingSwitch } from '../generic';
import MiSettings from 'vue-material-design-icons/settings.vue';
import MiReload from 'vue-material-design-icons/refresh.vue';
import MiEdit from 'vue-material-design-icons/pencil.vue';
import MiDelete from 'vue-material-design-icons/delete.vue';
const components = { MiSettings, Button, ButtonGroup, SettingSwitch, MiReload, MiEdit, MiDelete };
/*Methods*/
function showSettings() {
this.settingsOpen = true;
}
function editPlugin() {
try {
window.require('electron').shell.openItem(this.plugin.pluginPath);
} catch (err) {
console.log(err);
}
}
const methods = { editPlugin };
export default {
props: ['plugin', 'togglePlugin', 'reloadPlugin', 'showSettings'],
components,
name: "PluginCard",
methods,
data() {
return {
'settingsOpen': false
}
}
}
</script>

View File

@ -1,77 +0,0 @@
<template src="./templates/PluginSettingsModal.html"></template>
<script>
/*Imports*/
import { Modal } from '../generic';
const components = { Modal };
/*Methods*/
function textInputKd(e) {
console.log(e);
this.changed = true;
}
const methods = { textInputKd };
export default {
props: ['plugin'],
data() {
return {
'changed': false
}
},
components,
methods
}
</script>
<style>
.bd-modal-inner {
position: relative;
}
.bd-modal-footer .footer-alert {
opacity: 0;
transform: translateY(100%);
height: 30px;
display: flex;
background: rgba(32, 34, 37, 0.9);
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
box-shadow: 0 2px 10px 0 rgba(0,0,0,.2);
padding: 10px 10px 10px 16px;
border-radius: 5px;
transition: all .3s ease-in-out;
}
.bd-modal-footer .footer-alert.bd-active {
opacity: 1;
transform: none;
}
.bd-modal-footer .footer-alert .footer-alert-text {
color: #FFF;
flex: 1 1 auto;
font-weight: 500;
}
.bd-modal-footer .footer-alert {
height: 30px;
color: #FFF;
}
.bd-modal-footer .footer-alert .footer-alert-text {
line-height: 30px;
}
.bd-modal-footer .footer-alert .bd-button {
width: 120px;
border-radius: 3px;
}
.bd-modal-footer .footer-alert .bd-reset-button {
height: 30px;
line-height: 30px;
margin-right: 10px;
}
</style>

View File

@ -1,68 +0,0 @@
<template src="./templates/PluginsView.html"></template>
<script>
const { PluginManager, Plugin } = require('../../../');
/*Imports*/
import { SettingsWrapper } from './';
import PluginSettingsModal from './PluginSettingsModal.vue';
import PluginCard from './PluginCard.vue';
import Refresh from 'vue-material-design-icons/refresh.vue';
const components = { SettingsWrapper, PluginCard, Refresh, PluginSettingsModal };
/*Variables*/
/*Methods*/
async function refreshLocalPlugins() {
try {
await PluginManager.refreshPlugins();
this.$forceUpdate();
} catch (err) {
}
}
function showLocal() {
this.local = true;
}
function showOnline() {
this.local = false;
}
//TODO Display error if plugin fails to start/stop
function togglePlugin(plugin) {
if (plugin.enabled) {
this.pluginManager.stopPlugin(plugin);
} else {
this.pluginManager.startPlugin(plugin);
}
}
async function reloadPlugin(plugin) {
await this.pluginManager.reloadPlugin(plugin.name);
this.$forceUpdate();
}
function showSettings(plugin) {
this.settingsOpen = plugin;
}
const methods = { showLocal, showOnline, refreshLocalPlugins, togglePlugin, reloadPlugin, showSettings };
export default {
components,
data() {
return {
local: true,
pluginManager: PluginManager,
settingsOpen: null
}
},
computed: {
localPlugins: function () {
return this.pluginManager.plugins;
}
},
methods
}
</script>

View File

@ -1,11 +0,0 @@
<template src="./templates/SettingsWrapper.html"></template>
<script>
/*Imports*/
import { ScrollerWrap } from '../generic';
const components = { ScrollerWrap };
export default {
components,
props: ['headertext']
}
</script>

View File

@ -1,14 +0,0 @@
<template>
<SettingsWrapper headertext="UI Settings">
</SettingsWrapper>
</template>
<script>
/*Imports*/
import { SettingsWrapper } from './';
const components = { SettingsWrapper };
export default {
components
}
</script>

View File

@ -1,33 +0,0 @@
<SettingsWrapper headertext="CSS Editor">
<div class="bd-css-editor">
<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>
</div>
<div class="bd-form-divider"></div>
<SettingSwitch :setting="dummySetting" :onClick="settingClicked"/>
<div class="bd-form-item">
<h5>System Editor</h5>
<FormButton>
Open
</FormButton>
</div>
<div class="bd-form-divider"></div>
<FormButton :onClick="settingClicked">
Enabled
</FormButton>
<FormButton :disabled="true">
<span>Disabled</span>
</FormButton>
<FormButton :loading="true"/>
</div>
</SettingsWrapper>

View File

@ -1,34 +0,0 @@
<div class="bd-plugin-card">
<div class="bd-plugin-header">
<span v-tooltip="'wat'">{{plugin.name}}</span>
<div class="bd-flex-spacer"/>
<label class="bd-switch-wrapper" @click="togglePlugin(plugin)">
<input type="checkbox" class="bd-switch-checkbox" />
<div class="bd-switch" :class="{'bd-checked': plugin.enabled}" />
</label>
</div>
<div class="bd-plugin-body">
<div class="bd-plugin-description">{{plugin.description}}</div>
<div class="bd-plugin-footer">
<div class="bd-plugin-extra">v{{plugin.version}} by {{plugin.authors.join(', ').replace(/,(?!.*,)/gmi, ' and')}}</div>
<div class="bd-controls">
<ButtonGroup>
<Button v-tooltip="'Settings'" v-if="plugin.hasSettings" :onClick="() => showSettings(plugin)">
<MiSettings/>
</Button>
<Button v-tooltip="'Reload'" :onClick="() => reloadPlugin(plugin)">
<MiReload/>
</Button>
<Button v-tooltip="'Edit'" :onClick="editPlugin">
<MiEdit/>
</Button>
<Button v-tooltip="'Uninstall'" type="err">
<MiDelete/>
</Button>
</ButtonGroup>
</div>
</div>
</div>
</div>

View File

@ -1,38 +0,0 @@
<Modal :headerText="plugin.name + ' Settings'" :close="() => { }">
<div slot="body" v-for="setting in plugin.pluginConfig" class="bd-plugin-settings-body">
<div class="bd-form-item">
<div v-if="setting.type === 'bool'" class="bd-setting-switch">
<div class="bd-title">
<h3>{{setting.text}}</h3>
<label class="bd-switch-wrapper">
<input type="checkbox" class="bd-switch-checkbox" />
<div class="bd-switch" :class="{'bd-checked': setting.value}" />
</label>
</div>
<div class="bd-hint">{{setting.hint}}</div>
</div>
<div v-else-if="setting.type === 'text'" class="bd-form-textinput">
<div class="bd-title">
<h3>{{setting.text}}</h3>
<div class="bd-textinput-wrapper">
<input type="text" v-model="setting.value" @keyup.stop @keydown="textInputKd"/>
</div>
</div>
<div class="bd-hint">{{setting.hint}}</div>
</div>
<div class="bd-form-divider"></div>
</div>
</div>
<div slot="footer">
<div class="footer-alert" :class="{'bd-active': changed}">
<div class="footer-alert-text">Unsaved changes</div>
<div class="bd-reset-button">Reset</div>
<div class="bd-button bd-ok">Save Changes</div>
</div>
</div>
</Modal>

View File

@ -1,29 +0,0 @@
<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="refreshLocalPlugins">
<refresh/>
</div>
</div>
<div class="bd-flex-grow bd-button" :class="{'bd-active': !local}" @click="showOnline">
<h3>Online</h3>
<div class="bd-material-button">
<refresh/>
</div>
</div>
</div>
<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"/>
</div>
<div v-if="!local" class="bd-spinner-container">
<div class="bd-spinner-2"></div>
</div>
</div>
<div v-if="settingsOpen !== null" class="bd-backdrop" @click="settingsOpen = null"></div>
<div v-if="settingsOpen !== null" class="bd-modal">
<PluginSettingsModal :plugin="settingsOpen"/>
</div>
</SettingsWrapper>

View File

@ -1,8 +0,0 @@
<div class="bd-settingsWrap">
<div class="bd-settingsWrap-header">{{headertext}}</div>
<ScrollerWrap>
<div class="bd-scroller">
<slot/>
</div>
</ScrollerWrap>
</div>

View File

@ -1,9 +0,0 @@
<template>
<div class="bd-button-group">
<slot/>
</div>
</template>
<script>
export default {
}
</script>

View File

@ -1,11 +0,0 @@
<template>
<div class="bd-form-button bd-button" :class="{'bd-disabled': disabled}" @click="!disabled && !loading ? onClick() : null">
<div v-if="loading" class="bd-spinner-7"></div>
<slot v-else />
</div>
</template>
<script>
export default {
props: ['loading', 'disabled', 'type', 'onClick']
}
</script>

View File

@ -1,10 +0,0 @@
<template>
<div class="bd-scroller-wrap" :class="{'bd-dark': dark}">
<slot />
</div>
</template>
<script>
export default {
props: ['dark']
}
</script>

View File

@ -1,6 +0,0 @@
<template src="./templates/Button.html"></template>
<script>
export default {
props: ['item', 'onClick']
}
</script>

View File

@ -1,2 +0,0 @@
<template src="./templates/ContentColumn.html"></template>
<script> export default { }</script>

View File

@ -1,6 +0,0 @@
<template src="./templates/Header.html"></template>
<script>
export default {
props: ['item']
}
</script>

View File

@ -1,11 +0,0 @@
<template src="./templates/Item.html"></template>
<script>
/*Imports*/
import { SidebarHeader, SidebarButton } from './';
const components = { SidebarHeader, SidebarButton };
export default {
components,
props: ['item', 'onClick']
}
</script>

View File

@ -1,2 +0,0 @@
<template src="./templates/Sidebar.html"></template>
<script> export default { }</script>

View File

@ -1,11 +0,0 @@
<template src="./templates/View.html"></template>
<script>
/*Imports*/
import { ScrollerWrap } from '../generic';
const components = { ScrollerWrap };
export default {
components,
props: ['contentVisible', 'animating']
}
</script>

View File

@ -1 +0,0 @@
<div class="bd-item" :class="{active: item.active}" @click="onClick(item.id)">{{item.text}}</div>

View File

@ -1,3 +0,0 @@
<div class="bd-content bd-content-column">
<slot />
</div>

View File

@ -1 +0,0 @@
<div class='bd-header'>{{item.text}}</div>

View File

@ -1,2 +0,0 @@
<SidebarButton v-if="item._type == 'button'" :item="item" :onClick="onClick" />
<SidebarHeader v-else-if="item._type == 'header'" :item="item" />

View File

@ -1,3 +0,0 @@
<div class="bd-sidebar bd-scroller">
<slot/>
</div>

View File

@ -1,12 +0,0 @@
<div class="bd-sidebar-view" :class="{active: contentVisible, animating: animating}">
<div class="bd-sidebar-region">
<div class="bd-settingsWrap">
<ScrollerWrap dark="true">
<slot name="sidebar"/>
</ScrollerWrap>
</div>
</div>
<div class="bd-content-region">
<slot name="content"/>
</div>
</div>

View File

@ -1,32 +0,0 @@
<div class="bd-settings" :class="{active: active}" @keyup="close">
<SidebarView :contentVisible="this.activeIndex >= 0" :animating="this.animating">
<Sidebar slot="sidebar">
<div class="bd-settings-x" @click="close">
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 12 12"><g fill="none" fill-rule="evenodd"><path d="M0 0h12v12H0"></path><path class="fill" fill="#dcddde" d="M9.5 3.205L8.795 2.5 6 5.295 3.205 2.5l-.705.705L5.295 6 2.5 8.795l.705.705L6 6.705 8.795 9.5l.705-.705L6.705 6"></path></g></svg>
<span>ESC</span>
</div>
<SidebarItem v-for="item in sidebarItems" :item="item" :key="item.id" :onClick="itemOnClick" />
<div class="bd-info">
<span>v2.0.0a by Jiiks/JsSucks</span>
</div>
</Sidebar>
<ContentColumn slot="content">
<div v-if="activeContent('core') || animatingContent('core')" :class="{active: activeContent('core'), animating: animatingContent('core')}">
<CoreSettings :settings="coreSettings" :enableSetting="enableSetting" :disableSetting="disableSetting"/>
</div>
<div v-if="activeContent('ui') || animatingContent('ui')" :class="{active: activeContent('ui'), animating: animatingContent('ui')}">
<UISettings />
</div>
<div v-if="activeContent('css') || animatingContent('css')" :class="{active: activeContent('css'), animating: animatingContent('css')}">
<CssEditorView />
</div>
<div v-if="activeContent('emotes') || animatingContent('emotes')" :class="{active: activeContent('emotes'), animating: animatingContent('emotes')}">
<EmoteSettings />
</div>
<div v-if="activeContent('plugins') || animatingContent('plugins')" :class="{active: activeContent('plugins'), animating: animatingContent('plugins')}">
<PluginsView />
</div>
</ContentColumn>
</SidebarView>
</div>

View File

@ -1,6 +0,0 @@
<div class="bd-settings-wrapper" :class="[{active: active}, 'platform-' + this.platform]">
<div class="bd-settings-button" :class="{active: active}" @click="showSettings">
<div class="bd-settings-button-btn" :class="[{'bd-loading': !loaded}]"></div>
</div>
<BdSettings :active="active" :close="hideSettings"/>
</div>

View File

@ -1,35 +0,0 @@
/**
* BetterDiscord Client Renderer
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - 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 { WebpackModules } = require('../');
class Renderer {
static async render(component, root) {
if (!this.React) this.React = await this.getReact();
if (!this.reactDom) this.reactDom = await this.getReactDom();
const React = this.React;
window.React = React;
this.reactDom.render(component, root);
}
static async getReact() {
const getReact = await WebpackModules.getModuleByProps(('createElement', 'cloneElement'));
return getReact[0].exports;
}
static async getReactDom() {
const getReactDom = await WebpackModules.getModuleByProps(('render', 'findDOMNode'));
return getReactDom[0].exports;
}
}
module.exports = { Renderer };

View File

@ -1,45 +0,0 @@
/**
* BetterDiscord Client UI Module
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - 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 $ = require('jquery');
const Vue = require('vue').default;
const VTooltip = require('v-tooltip');
const BdSettingsWrapper = (require('./components/BdSettingsWrapper.vue')).default;
class UI {
constructor() {
$('body').append($('<bdbody/>')
.append($('<div/>', {
id: 'bd-settings'
})).append($('<bdtooltips/>')));
Vue.use(VTooltip, {
defaultContainer: 'bdtooltips',
defaultClass: 'bd-tooltip',
defaultTargetClass: 'bd-has-tooltip',
defaultInnerSelector: '.bd-tooltip-inner',
defaultTemplate: '<div class="bd-tooltip"><span class="bd-tooltip-inner"></span></div>'
});
this.vueInstance = new Vue({
el: '#bd-settings',
components: { BdSettingsWrapper },
template: '<BdSettingsWrapper/>'
});
}
}
module.exports = { UI }

View File

@ -1,24 +1,24 @@
/**
* BetterDiscord Client Utils Module
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* BetterDiscord Vendor Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
* 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 {WebpackModules} = require('./webpackmodules');
const jQuery = require('jquery');
import WebpackModules from './webpackmodules';
import jQuery from 'jquery';
class Vendor {
export default class {
static get jQuery() {
return jQuery;
}
static get $() {
return jQuery;
return this.jQuery;
}
static get moment() {
@ -26,5 +26,3 @@ class Vendor {
}
}
module.exports = {Vendor};

View File

@ -1,13 +1,51 @@
/**
* BetterDiscord Client WebpackModules Module
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* BetterDiscord WebpackModules Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
* 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.
* LICENSE file in the root directory of this source tree.
*/
const { Filters } = require('./utils');
class Filters {
static byProperties(props, selector = m => m) {
return module => {
const component = selector(module);
if (!component) return false;
return props.every(property => component[property] !== undefined);
};
}
static byPrototypeFields(fields, selector = m => m) {
return module => {
const component = selector(module);
if (!component) return false;
if (!component.prototype) return false;
return fields.every(field => component.prototype[field] !== undefined);
};
}
static byCode(search, selector = m => m) {
return module => {
const method = selector(module);
if (!method) return false;
return method.toString().search(search) !== -1;
};
}
static byDisplayName(name) {
return module => {
return module && module.displayName === name;
};
}
static combine(...filters) {
return module => {
return filters.every(filter => filter(module));
};
}
}
const KnownModules = {
React: Filters.byProperties(['createElement', 'cloneElement']),
@ -163,11 +201,7 @@ const KnownModules = {
ExternalLink: Filters.byCode(/\.trusted\b/)
};
const Cache = {};
class WebpackModules {
export default class {
/* Synchronous */
static getModuleByName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name];
@ -224,5 +258,3 @@ class WebpackModules {
return __webpack_require__.c;
}
}
module.exports = { WebpackModules };

36
client/src/ui/bdui.js Normal file
View File

@ -0,0 +1,36 @@
/**
* BetterDiscord Client UI 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 Dom from './dom';
import Vue from 'vue';
import VTooltip from 'v-tooltip';
import { BdSettingsWrapper } from './components';
export default class {
static injectUi() {
Vue.use(VTooltip, {
defaultContainer: 'bdtooltips',
defaultClass: 'bd-tooltip',
defaultTargetClass: 'bd-has-tooltip',
defaultInnerSelector: '.bd-tooltip-inner',
defaultTemplate: '<div class="bd-tooltip"><span class="bd-tooltip-inner"></span></div>'
});
Dom.createElement('div', null, 'bd-settings').appendTo(Dom.bdBody);
const vueInstance = new Vue({
el: '#bd-settings',
components: { BdSettingsWrapper },
template: '<BdSettingsWrapper/>'
});
return vueInstance;
}
}

View File

@ -0,0 +1,127 @@
/**
* BetterDiscord Settings 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-settings" :class="{active: active}" @keyup="close">
<SidebarView :contentVisible="this.activeIndex >= 0" :animating="this.animating">
<Sidebar slot="sidebar">
<div class="bd-settings-x" @click="close">
<SvgX size="17"/>
<span>ESC</span>
</div>
<SidebarItem v-for="item in sidebarItems" :item="item" :key="item.id" :onClick="itemOnClick" />
<div class="bd-info">
<span>v2.0.0a by Jiiks/JsSucks</span>
</div>
</Sidebar>
<ContentColumn slot="content">
<div v-if="activeContent('core') || animatingContent('core')" :class="{active: activeContent('core'), animating: animatingContent('core')}">
<CoreSettings :settings="coreSettings" :enableSetting="enableSetting" :disableSetting="disableSetting" />
</div>
<div v-if="activeContent('ui') || animatingContent('ui')" :class="{active: activeContent('ui'), animating: animatingContent('ui')}">
<UISettings />
</div>
<div v-if="activeContent('css') || animatingContent('css')" :class="{active: activeContent('css'), animating: animatingContent('css')}">
<CssEditorView />
</div>
<div v-if="activeContent('emotes') || animatingContent('emotes')" :class="{active: activeContent('emotes'), animating: animatingContent('emotes')}">
<EmoteSettings />
</div>
<div v-if="activeContent('plugins') || animatingContent('plugins')" :class="{active: activeContent('plugins'), animating: animatingContent('plugins')}">
<PluginsView />
</div>
</ContentColumn>
</SidebarView>
</div>
</template>
<script>
// Imports
import { Settings } from 'modules';
import { SidebarView, Sidebar, SidebarItem, ContentColumn } from './sidebar';
import { CoreSettings, UISettings, EmoteSettings, CssEditorView, PluginsView } from './bd';
import { SvgX } 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,
activeIndex: -1,
lastActiveIndex: -1,
animating: false,
first: true,
settings: Settings.getSettings
}
},
props: ['active', 'close'],
computed: {
coreSettings() {
return this.settings.find(settingset => settingset.id === 'core').settings;
}
},
components: {
SidebarView, Sidebar, SidebarItem, ContentColumn,
CoreSettings, UISettings, EmoteSettings, CssEditorView, PluginsView,
SvgX
},
methods: {
itemOnClick(id) {
if (this.animating || id === this.activeIndex) return;
if (this.activeIndex >= 0) this.sidebarItems.find(item => item.id === this.activeIndex).active = false;
this.sidebarItems.find(item => item.id === id).active = true;
this.animating = true;
this.lastActiveIndex = this.activeIndex;
this.activeIndex = id;
if (this.first) {
this.first = false;
}
setTimeout(() => {
this.animating = false;
this.lastActiveIndex = -1;
}, 400);
},
activeContent(s) {
const item = this.sidebarItems.find(item => item.contentid === s);
if (!item) return false;
return item.id === this.activeIndex;
},
animatingContent(s) {
const item = this.sidebarItems.find(item => item.contentid === s);
if (!item) return false;
return item.id === this.lastActiveIndex;
},
enableSetting(cat, id) {
switch (cat) {
case 'core':
return this.coreSettings.find(setting => setting.id === id).enabled = true;
}
},
disableSetting(cat, id) {
switch (cat) {
case 'core':
return this.coreSettings.find(setting => setting.id === id).enabled = false;
}
}
}
}
</script>

View File

@ -0,0 +1,57 @@
/**
* BetterDiscord Settings Wrapper 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-settings-wrapper" :class="[{active: active}, 'platform-' + this.platform]">
<div class="bd-settings-button" :class="{active: active}" @click="showSettings">
<div class="bd-settings-button-btn" :class="[{'bd-loading': !loaded}]"></div>
</div>
<BdSettings :active="active" :close="hideSettings" />
</div>
</template>
<script>
// Imports
import { Events } from 'modules';
import BdSettings from './BdSettings.vue';
export default {
data() {
return {
loaded: false,
active: false,
platform: global.process.platform
}
},
components: {
BdSettings
},
methods: {
showSettings() {
if (!this.loaded) return;
this.active = true;
},
hideSettings() { this.active = false },
toggleSettings() { this.active = !this.active },
keyupListener(e) {
if (this.active && e.which === 27) return this.hideSettings();
if (!e.metaKey && !e.ctrlKey || e.key !== 'b') return;
this.toggleSettings();
e.stopImmediatePropagation();
}
},
created() {
Events.on('ready', e => this.loaded = true);
window.addEventListener('keyup', this.keyupListener);
},
destroyed() {
window.removeEventListener('keyup', this.keyupListener);
}
}
</script>

View File

@ -0,0 +1,45 @@
/**
* BetterDiscord Core Settings 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>
<SettingsWrapper headertext="Core Settings">
<div class="bd-form-item" v-for="setting in settings" :key="setting.id">
<SettingSwitch key="setting.id" :setting="setting" :onClick="settingOnClick" :disabled="setting.disabled" />
<div class="bd-form-divider" />
</div>
</SettingsWrapper>
</template>
<script>
// Imports
import { SettingsWrapper } from './';
import { SettingSwitch } from '../common';
/*Methods*/
function settingOnClick(setting) {
if (setting.enabled) return this.disableSetting('core', setting.id);
this.enableSetting('core', setting.id);
}
const methods = { settingOnClick };
export default {
props: ['settings', 'enableSetting', 'disableSetting'],
components: {
SettingsWrapper, SettingSwitch
},
methods: {
settingOnClick(setting) {
if (setting.enabled) return this.disableSetting('core', setting.id);
this.enableSetting('core', setting.id);
}
}
}
</script>

View File

@ -0,0 +1,75 @@
/**
* BetterDiscord Css Editor 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>
<SettingsWrapper headertext="CSS Editor">
<div class="bd-css-editor">
<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>
</div>
<div class="bd-form-divider"></div>
<SettingSwitch :setting="dummySetting" :onClick="settingClicked" />
<div class="bd-form-item">
<h5>System Editor</h5>
<FormButton>
Open
</FormButton>
</div>
<div class="bd-form-divider"></div>
<FormButton :onClick="settingClicked">
Enabled
</FormButton>
<FormButton :disabled="true">
<span>Disabled</span>
</FormButton>
<FormButton :loading="true" />
</div>
</SettingsWrapper>
</template>
<script>
// Imports
import { CssEditor } from 'modules';
import { SettingsWrapper } from './';
import { SettingSwitch, FormButton } from '../common';
export default {
components: {
SettingsWrapper,
SettingSwitch, FormButton
},
methods: {
openInternalEditor() {
CssEditor.show();
},
settingClicked() {
}
},
data() {
return {
dummySetting: {
title: "Live Update",
hint: "Automatically update client css when saved.",
checked: true
}
}
}
}
</script>

View File

@ -0,0 +1,27 @@
/**
* BetterDiscord Emote Settings 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>
<SettingsWrapper headertext="Emote Settings">
</SettingsWrapper>
</template>
<script>
// Imports
import { SettingsWrapper } from './';
import { SettingSwitch } from '../common';
export default {
components: {
SettingsWrapper,
SettingSwitch
}
}
</script>

View File

@ -0,0 +1,76 @@
/**
* BetterDiscord Plugin Card Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-plugin-card">
<div class="bd-plugin-header">
<span v-tooltip="'wat'">{{plugin.name}}</span>
<div class="bd-flex-spacer" />
<label class="bd-switch-wrapper" @click="() => { togglePlugin(plugin); this.$forceUpdate(); }">
<input type="checkbox" class="bd-switch-checkbox" />
<div class="bd-switch" :class="{'bd-checked': plugin.enabled}" />
</label>
</div>
<div class="bd-plugin-body">
<div class="bd-plugin-description">{{plugin.description}}</div>
<div class="bd-plugin-footer">
<div class="bd-plugin-extra">v{{plugin.version}} by {{plugin.authors.join(', ').replace(/,(?!.*,)/gmi, ' and')}}</div>
<div class="bd-controls">
<ButtonGroup>
<Button v-tooltip="'Settings'" v-if="plugin.hasSettings" :onClick="() => showSettings(plugin)">
<MiSettings />
</Button>
<Button v-tooltip="'Reload'" :onClick="() => reloadPlugin(plugin)">
<MiReload />
</Button>
<Button v-tooltip="'Edit'" :onClick="editPlugin">
<MiEdit />
</Button>
<Button v-tooltip="'Uninstall'" type="err">
<MiDelete />
</Button>
</ButtonGroup>
</div>
</div>
</div>
</div>
</template>
<script>
// Imports
import { shell } from 'electron';
import { Button, ButtonGroup, SettingSwitch } from '../common';
import MiSettings from 'vue-material-design-icons/settings.vue';
import MiReload from 'vue-material-design-icons/refresh.vue';
import MiEdit from 'vue-material-design-icons/pencil.vue';
import MiDelete from 'vue-material-design-icons/delete.vue';
export default {
data() {
return {
settingsOpen: false
}
},
props: ['plugin', 'togglePlugin', 'reloadPlugin', 'showSettings'],
components: {
Button, ButtonGroup, SettingSwitch,
MiSettings, MiReload, MiEdit, MiDelete
},
methods: {
editPlugin() {
try {
shell.openItem(this.plugin.pluginPath);
} catch (err) {
console.log(err);
}
}
}
}
</script>

View File

@ -0,0 +1,71 @@
/**
* BetterDiscord Plugin Settings Modal Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<Modal :headerText="plugin.name + ' Settings'" :close="() => { }">
<div slot="body" v-for="setting in plugin.pluginConfig" class="bd-plugin-settings-body">
<div class="bd-form-item">
<div v-if="setting.type === 'bool'" class="bd-setting-switch">
<div class="bd-title">
<h3>{{setting.text}}</h3>
<label class="bd-switch-wrapper">
<input type="checkbox" class="bd-switch-checkbox" />
<div class="bd-switch" :class="{'bd-checked': setting.value}" />
</label>
</div>
<div class="bd-hint">{{setting.hint}}</div>
</div>
<div v-else-if="setting.type === 'text'" class="bd-form-textinput">
<div class="bd-title">
<h3>{{setting.text}}</h3>
<div class="bd-textinput-wrapper">
<input type="text" v-model="setting.value" @keyup.stop @keydown="textInputKd" />
</div>
</div>
<div class="bd-hint">{{setting.hint}}</div>
</div>
<div class="bd-form-divider"></div>
</div>
</div>
<div slot="footer">
<div class="footer-alert" :class="{'bd-active': changed}">
<div class="footer-alert-text">Unsaved changes</div>
<div class="bd-reset-button">Reset</div>
<div class="bd-button bd-ok">Save Changes</div>
</div>
</div>
</Modal>
</template>
<script>
// Imports
import { Modal } from '../common';
export default {
props: ['plugin'],
data() {
return {
'changed': false
}
},
components: {
Modal
},
methods: {
textInputKd(e) {
this.changed = true;
}
}
}
</script>

View File

@ -0,0 +1,101 @@
/**
* BetterDiscord Plugins View 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>
<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">
<refresh />
</div>
</div>
<div class="bd-flex-grow bd-button" :class="{'bd-active': !local}" @click="showOnline">
<h3>Online</h3>
<div class="bd-material-button">
<refresh />
</div>
</div>
</div>
<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" />
</div>
<div v-if="!local" class="bd-spinner-container">
<div class="bd-spinner-2"></div>
</div>
</div>
<div v-if="settingsOpen !== null" class="bd-backdrop" @click="settingsOpen = null"></div>
<div v-if="settingsOpen !== null" class="bd-modal">
<PluginSettingsModal :plugin="settingsOpen" />
</div>
</SettingsWrapper>
</template>
<script>
// Imports
import { PluginManager } from 'modules';
import { SettingsWrapper } from './';
import PluginCard from './PluginCard.vue';
import PluginSettingsModal from './PluginSettingsModal.vue';
import Refresh from 'vue-material-design-icons/refresh.vue';
export default {
data() {
return {
local: true,
settingsOpen: null,
localPlugins: PluginManager.localPlugins
}
},
components: {
SettingsWrapper, PluginCard, PluginSettingsModal, Refresh
},
methods: {
showLocal() {
this.local = true;
},
showOnline() {
this.local = false;
},
refreshLocal() {
(async () => {
await PluginManager.refreshPlugins();
})();
},
togglePlugin(plugin) {
// TODO Display error if plugin fails to start/stop
try {
if (plugin.enabled) {
PluginManager.stopPlugin(plugin);
} else {
PluginManager.startPlugin(plugin);
}
} catch (err) {
console.log(err);
}
},
reloadPlugin(plugin) {
(async () => {
try {
await PluginManager.reloadPlugin(plugin);
this.$forceUpdate();
} catch (err) {
console.log(err);
}
})();
},
showSettings(plugin) {
this.settingsOpen = plugin;
}
}
}
</script>

View File

@ -0,0 +1,29 @@
/**
* BetterDiscord Settings Wrapper 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-settingsWrap">
<div class="bd-settingsWrap-header">{{headertext}}</div>
<ScrollerWrap>
<slot />
</ScrollerWrap>
</div>
</template>
<script>
// Imports
import { ScrollerWrap } from '../common';
export default {
props: ['headertext'],
components: {
ScrollerWrap
}
}
</script>

View File

@ -0,0 +1,27 @@
/**
* BetterDiscord UI Settings 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>
<SettingsWrapper headertext="UI Settings">
</SettingsWrapper>
</template>
<script>
// Imports
import { SettingsWrapper } from './';
import { SettingSwitch } from '../common';
export default {
components: {
SettingsWrapper,
SettingSwitch
}
}
</script>

View File

@ -2,7 +2,5 @@ export { default as SettingsWrapper } from './SettingsWrapper.vue';
export { default as CoreSettings } from './CoreSettings.vue';
export { default as UISettings } from './UISettings.vue';
export { default as EmoteSettings } from './EmoteSettings.vue';
export { default as PluginsView } from './PluginsView.vue';
export { default as PluginCard } from './PluginCard.vue';
export { default as CssEditorView } from './CssEditor.vue';
export { default as PluginSettingsModal } from './PluginSettingsModal.vue';
export { default as PluginsView } from './PluginsView.vue';

View File

@ -1,3 +1,13 @@
/**
* BetterDiscord Button 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-button" :class="[{'bd-disabled': disabled}, {'bd-err': type === 'err'}]" @click="!disabled && !loading ? onClick() : null">
<div v-if="loading" class="bd-spinner-7"></div>
@ -8,4 +18,4 @@
export default {
props: ['loading', 'disabled', 'type', 'onClick', 'type']
}
</script>
</script>

View File

@ -0,0 +1,19 @@
/**
* BetterDiscord Button Group 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-button-group">
<slot />
</div>
</template>
<script>
export default {
}
</script>

View File

@ -0,0 +1,21 @@
/**
* BetterDiscord Form Button 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-form-button bd-button" :class="{'bd-disabled': disabled}" @click="!disabled && !loading ? onClick() : null">
<div v-if="loading" class="bd-spinner-7"></div>
<slot v-else />
</div>
</template>
<script>
export default {
props: ['loading', 'disabled', 'type', 'onClick']
}
</script>

View File

@ -1,10 +1,20 @@
/**
* BetterDiscord ¨Modal Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-modal">
<div class="bd-modal-inner">
<div class="bd-modal-header">
<span>{{headerText}}</span>
<div class="bd-modal-x" @click="close">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 12 12"><g fill="none" fill-rule="evenodd"><path d="M0 0h12v12H0"></path><path class="fill" fill="#dcddde" d="M9.5 3.205L8.795 2.5 6 5.295 3.205 2.5l-.705.705L5.295 6 2.5 8.795l.705.705L6 6.705 8.795 9.5l.705-.705L6.705 6"></path></g></svg>
<SvgX size="18"/>
</div>
</div>
<div class="bd-modal-body">
@ -18,7 +28,13 @@
</template>
<script>
// Imports
import SvgX from './SvgX.vue';
export default {
props: ['headerText', 'close']
props: ['headerText', 'close'],
components: {
SvgX
}
}
</script>
</script>

View File

@ -0,0 +1,22 @@
/**
* BetterDiscord Scroller Wrap 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-scroller-wrap" :class="{'bd-dark': dark}">
<div class="bd-scroller">
<slot/>
</div>
</div>
</template>
<script>
export default {
props: ['dark']
}
</script>

View File

@ -1,3 +1,13 @@
/**
* BetterDiscord Setting Switch 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-setting-switch" :class="{'bd-disabled': disabled}">
<div class="bd-title">
@ -14,4 +24,4 @@
export default {
props: ['setting', 'onClick', 'disabled']
}
</script>
</script>

View File

@ -0,0 +1,13 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size" viewBox="0 0 12 12">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h12v12H0"></path>
<path class="fill" fill="#dcddde" d="M9.5 3.205L8.795 2.5 6 5.295 3.205 2.5l-.705.705L5.295 6 2.5 8.795l.705.705L6 6.705 8.795 9.5l.705-.705L6.705 6"></path>
</g>
</svg>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -3,4 +3,5 @@ export { default as SettingSwitch } from './SettingSwitch.vue';
export { default as FormButton } from './FormButton.vue';
export { default as ButtonGroup } from './ButtonGroup.vue';
export { default as Button } from './Button.vue';
export { default as Modal } from './Modal.vue';
export { default as Modal } from './Modal.vue';
export { default as SvgX } from './SvgX.vue';

View File

@ -0,0 +1,2 @@
export { default as BdSettingsWrapper } from './BdSettingsWrapper.vue';
export { default as BdSettings } from './BdSettings.vue';

View File

@ -0,0 +1,20 @@
/**
* BetterDiscord Sidebar Button 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-item" :class="{active: item.active}" @click="onClick(item.id)">
{{item.text}}
</div>
</template>
<script>
export default {
props: ['item', 'onClick']
}
</script>

View File

@ -0,0 +1,18 @@
/**
* BetterDiscord Sidebar Content Column 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-content bd-content-column">
<slot/>
</div>
</template>
<script>
export default {}
</script>

View File

@ -0,0 +1,20 @@
/**
* BetterDiscord Sidebar Header 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-header'>
{{item.text}}
</div>
</template>
<script>
export default {
props: ['item']
}
</script>

View File

@ -0,0 +1,26 @@
/**
* BetterDiscord Sidebar Item 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>
<SidebarButton v-if="item._type === 'button'" :item="item" :onClick="onClick"/>
<SidebarHeader v-else :item="item" />
</template>
<script>
// Imports
import { SidebarHeader, SidebarButton } from './';
export default {
props: ['item', 'onClick'],
components: {
SidebarHeader,
SidebarButton
}
}
</script>

View File

@ -0,0 +1,20 @@
/**
* BetterDiscord Sidebar 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-sidebar bd-scroller">
<slot/>
</div>
</template>
<script>
export default {
}
</script>

View File

@ -0,0 +1,35 @@
/**
* BetterDiscord Sidebar View 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-sidebar-view" :class="{active: contentVisible, animating: animating}">
<div class="bd-sidebar-region">
<div class="bd-settingsWrap">
<ScrollerWrap dark="true">
<slot name="sidebar" />
</ScrollerWrap>
</div>
</div>
<div class="bd-content-region">
<slot name="content" />
</div>
</div>
</template>
<script>
// Imports
import { ScrollerWrap } from '../common';
export default {
props: ['contentVisible', 'animating'],
components: {
ScrollerWrap
}
}
</script>

96
client/src/ui/dom.js Normal file
View File

@ -0,0 +1,96 @@
/**
* BetterDiscord Client DOM 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.
*/
class BdNode {
constructor(tag, className, id) {
this.element = document.createElement(tag);
if (className) this.element.className = className;
if (id) this.element.id = id;
}
appendTo(e) {
const el = DOM.getElement(e);
if (!el) return null;
el.append(this.element);
return this.element;
}
prependTo(e) {
const el = DOM.getElement(e);
if (!el) return null;
el.prepend(this.element);
return this.element;
}
}
class DOM {
static get bdHead() {
return this.getElement('bd-head') || this.createElement('bd-head').appendTo('head');
}
static get bdBody() {
return this.getElement('bd-body') || this.createElement('bd-body').appendTo('body');
}
static get bdStyles() {
return this.getElement('bd-styles') || this.createElement('bd-styles').appendTo(this.bdHead);
}
static get bdThemes() {
return this.getElement('bd-themes') || this.createElement('bd-themes').appendTo(this.bdHead);
}
static get bdTooltips() {
return this.getElement('bd-tooltips') || this.createElement('bd-tooltips').appendTo(this.bdBody);
}
static getElement(e) {
if (e instanceof BdNode) return e.element;
if (e instanceof window.Node) return e;
if ('string' !== typeof e) return null;
return document.querySelector(e);
}
static createElement(tag = 'div', className = null, id = null) {
return new BdNode(tag, className, id);
}
static deleteStyle(id) {
const exists = this.getElement(`bd-styles > #${id}`);
if (exists) exists.remove();
}
static injectStyle(css, id) {
this.deleteStyle(id);
this.bdStyles.append(this.createStyle(css, id));
}
static getStyleCss(id) {
const exists = this.getElement(`bd-styles > #${id}`);
return exists ? exists.textContent : '';
}
static deleteTheme(id) {
const exists = this.getElement(`bd-themes > #${id}`);
if (exists) exists.remove();
}
static injectTheme(css, id) {
this.deleteTheme(id);
this.bdThemes.append(this.createStyle(css, id));
}
static createStyle(css, id) {
const style = document.createElement('style');
style.id = id;
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
return style;
}
}
export default DOM;

2
client/src/ui/ui.js Normal file
View File

@ -0,0 +1,2 @@
export { default as DOM } from './dom';
export { default as BdUI } from './bdui';

View File

@ -32,12 +32,20 @@ module.exports = {
loaders: [jsLoader, vueLoader, scssLoader]
},
externals: {
'electron': 'window.require("electron")'
'electron': 'window.require("electron")',
'fs': 'window.require("fs")',
'path': 'window.require("path")'
},
resolve: {
alias: {
vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js')
}
},
modules: [
path.resolve('..', 'node_modules'),
path.resolve('..', 'common', 'modules'),
path.resolve('src', 'modules'),
path.resolve('src', 'ui')
]
}
/* resolve: {
alias: {

53
common/modules/bdipc.js Normal file
View File

@ -0,0 +1,53 @@
/**
* BetterDiscord IPC 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 { ipcRenderer, ipcMain } = require('electron');
export class ClientIPC {
static on(channel, cb) {
ipcRenderer.on(channel, (event, message) => cb(event, message));
}
static async send(channel, message) {
channel = channel.startsWith('bd-') ? channel : `bd-${channel}`;
const __eid = Date.now().toString();
ipcRenderer.send(channel, Object.assign(message ? message : {}, { __eid }));
return new Promise((resolve, reject) => {
ipcRenderer.once(__eid, (event, arg) => resolve(arg));
});
}
}
class BDIpcEvent {
constructor(event, args) {
this.args = args;
this.ipcEvent = event;
}
bindings() {
this.send = this.send.bind(this);
this.reply = this.reply.bind(this);
}
send(message) {
this.ipcEvent.sender.send(this.args.__eid, message);
}
reply(message) {
this.send(message);
}
}
export class CoreIPC {
static on(channel, cb) {
ipcMain.on(channel, (event, args) => cb(new BDIpcEvent(event, args)));
}
}

2
common/modules/common.js Normal file
View File

@ -0,0 +1,2 @@
export { ClientIPC } from './bdipc';
export * from './utils';

View File

@ -1,69 +1,23 @@
/**
* BetterDiscord Client Utils Module
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* BetterDiscord Utils Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
* 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 { Module } = require('./modulebase');
const { Vendor } = require('./vendor');
const fs = window.require('fs');
const path = window.require('path');
const
path = require('path'),
fs = require('fs');
const logs = [];
class Logger {
static err(module, message) { this.log(module, message, 'err'); }
static warn(module, message) { this.log(module, message, 'warn'); }
static info(module, message) { this.log(module, message, 'info'); }
static dbg(module, message) { this.log(module, message, 'dbg'); }
static log(module, message, level = 'log') {
message = message.message || message;
if (typeof message === 'object') {
//TODO object handler for logs
console.log(message);
return;
}
level = this.parseLevel(level);
console[level]('[%cBetter%cDiscord:%s] %s', 'color: #3E82E5', '', `${module}${level === 'debug' ? '|DBG' : ''}`, message);
logs.push(`[${Vendor.moment().format('DD/MM/YY hh:mm:ss')}|${module}|${level}] ${message}`);
window.bdlogs = logs;
}
static logError(err) {
if (!err.module && !err.message) {
console.log(err);
return;
}
this.err(err.module, err.message);
}
static get levels() {
return {
'log': 'log',
'warn': 'warn',
'err': 'error',
'error': 'error',
'debug': 'debug',
'dbg': 'debug',
'info': 'info'
};
}
static parseLevel(level) {
return this.levels.hasOwnProperty(level) ? this.levels[level] : 'log';
}
}
class Utils {
import { Vendor } from 'modules';
export class Utils {
static overload(fn, cb) {
const orig = fn;
return function(...args) {
return function (...args) {
orig(...args);
cb(...args);
}
@ -79,11 +33,9 @@ class Utils {
});
}
}
}
class FileUtils {
export class FileUtils {
static async fileExists(path) {
return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => {
@ -120,6 +72,32 @@ class FileUtils {
});
}
static async createDirectory(path) {
return new Promise((resolve, reject) => {
fs.mkdir(path, err => {
if (err) {
if (err.code === 'EEXIST') return resolve();
else return reject(err);
}
resolve();
});
});
}
static async ensureDirectory(path) {
try {
await this.directoryExists(path);
return true;
} catch (err) {
try {
await this.createDirectory(path);
return true;
} catch (err) {
throw err;
}
}
}
static async readFile(path) {
try {
await this.fileExists(path);
@ -168,7 +146,7 @@ class FileUtils {
return this.writeFile(path, JSON.stringify(json));
}
static async readDir(path) {
static async listDirectory(path) {
try {
await this.directoryExists(path);
return new Promise((resolve, reject) => {
@ -181,45 +159,53 @@ class FileUtils {
throw err;
}
}
}
class Filters {
static byProperties(props, selector = m => m) {
return module => {
const component = selector(module);
if (!component) return false;
return props.every(property => component[property] !== undefined);
};
}
static byPrototypeFields(fields, selector = m => m) {
return module => {
const component = selector(module);
if (!component) return false;
if (!component.prototype) return false;
return fields.every(field => component.prototype[field] !== undefined);
};
}
static byCode(search, selector = m => m) {
return module => {
const method = selector(module);
if (!method) return false;
return method.toString().search(search) !== -1;
};
}
static byDisplayName(name) {
return module => {
return module && module.displayName === name;
};
}
static combine(...filters) {
return module => {
return filters.every(filter => filter(module));
};
static async readDir(path) {
return this.listDirectory(path);
}
}
module.exports = { Logger, Utils, FileUtils, Filters };
const logs = [];
export class ClientLogger {
static err(module, message) { this.log(module, message, 'err'); }
static warn(module, message) { this.log(module, message, 'warn'); }
static info(module, message) { this.log(module, message, 'info'); }
static dbg(module, message) { this.log(module, message, 'dbg'); }
static log(module, message, level = 'log') {
message = message.message || message;
if (typeof message === 'object') {
//TODO object handler for logs
console.log(message);
return;
}
level = this.parseLevel(level);
console[level]('[%cBetter%cDiscord:%s] %s', 'color: #3E82E5', '', `${module}${level === 'debug' ? '|DBG' : ''}`, message);
logs.push(`${level.toUpperCase()} : [${Vendor.moment().format('DD/MM/YY hh:mm:ss')}|${module}] ${message}`);
window.bdlogs = logs;
}
static logError(err) {
if (!err.module && !err.message) {
console.log(err);
return;
}
this.err(err.module, err.message);
}
static get levels() {
return {
'log': 'log',
'warn': 'warn',
'err': 'error',
'error': 'error',
'debug': 'debug',
'dbg': 'debug',
'info': 'info'
};
}
static parseLevel(level) {
return this.levels.hasOwnProperty(level) ? this.levels[level] : 'log';
}
}

View File

@ -11,7 +11,12 @@ module.exports = (Plugin, Api, Vendor) => {
}
onStart() {
console.log('On Start!');
console.log('Example Plugin 1 onStart');
return true;
}
onStop() {
console.log('Example Plugin 1 onStop');
return true;
}
}