Merge pull request #184 from samuelthomas2774/refactor

Refactor and comment
This commit is contained in:
Alexei Stukov 2018-03-25 09:52:32 -02:00 committed by GitHub
commit a4ceb8bd2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
121 changed files with 3414 additions and 3248 deletions

View File

@ -2,6 +2,7 @@ root = true
[*] [*]
end_of_line = lf end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
indent_style = space indent_style = space

2
.gitattributes vendored
View File

@ -1 +1 @@
*.sh text=auto *.sh text=auto

34
.gitignore vendored
View File

@ -1,27 +1,11 @@
.idea/* # Generated files
*.name
devjs/.idea/devjs.iml
*.bak
*.bak.*
*.xpi
Firefox/data/js/jquery-2.1.4.min.js
*.dev.*
/nbproject/private/
node_modules node_modules
.sass-cache dist
/*.jiiks etc
Installers/dotNet/bin/ release
Installers/dotNet/packages/
Installers/dotNet/dlls/ tests/log.txt
v2/dist/vendor/
v2/lib/static.js # User data
**/*.suo
Installers/**/*/bin
Installers/**/*/obj
Installers/**/*/packages
.vs
dist/
user.config.json
tests/data tests/data
/tests/themes/SimplerFlat user.config.json
release

View File

@ -3,4 +3,4 @@ node_js:
- stable - stable
branches: branches:
only: only:
- master - master

View File

@ -6,4 +6,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,3 +1,4 @@
# BetterDiscordApp [![Travis][build-badge]][build] # BetterDiscordApp [![Travis][build-badge]][build]
[build-badge]: https://img.shields.io/travis/JsSucks/BetterDiscordApp/master.svg [build-badge]: https://img.shields.io/travis/JsSucks/BetterDiscordApp/master.svg
[build]: https://travis-ci.org/JsSucks/BetterDiscordApp [build]: https://travis-ci.org/JsSucks/BetterDiscordApp

View File

@ -5,21 +5,19 @@
"version": "2.0.0b", "version": "2.0.0b",
"homepage": "https://betterdiscord.net", "homepage": "https://betterdiscord.net",
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "dist/betterdiscord.client.js",
"contributors": [ "contributors": [
"Jiiks", "Jiiks",
"Pohky" "Pohky"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git" "url": "https://github.com/JsSucks/BetterDiscordApp.git"
}, },
"private": false, "private": false,
"devDependencies": {
},
"scripts": { "scripts": {
"build": "webpack --progress --colors", "build": "webpack --progress --colors",
"watch": "webpack --progress --colors --watch" "watch": "webpack --progress --colors --watch",
"release": "webpack --progress --colors --config=webpack.production.config.js"
} }
} }

View File

@ -9,11 +9,10 @@
data() { data() {
return { return {
favourite: false favourite: false
} };
}, },
props: ['src', 'name'], props: ['src', 'name'],
methods: { methods: {},
},
beforeMount() { beforeMount() {
// Check favourite state // Check favourite state
} }

View File

@ -7,26 +7,34 @@
* This source code is licensed under the MIT license found in the * 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.
*/ */
import { FileUtils, ClientLogger as Logger } from 'common';
import { Events, Globals, WebpackModules, ReactComponents, MonkeyPatch } from 'modules'; import { Events, Globals, WebpackModules, ReactComponents, MonkeyPatch } from 'modules';
import { DOM, VueInjector, Reflection } from 'ui'; import { DOM, VueInjector, Reflection } from 'ui';
import { FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import EmoteComponent from './EmoteComponent.vue'; import EmoteComponent from './EmoteComponent.vue';
let emotes = null; let emotes = null;
const emotesEnabled = true; const emotesEnabled = true;
export default class { export default class {
static get searchCache() { static get searchCache() {
return this._searchCache || (this._searchCache = {}); return this._searchCache || (this._searchCache = {});
} }
static get emoteDb() { static get emoteDb() {
return emotes; return emotes;
} }
static get React() { static get React() {
return WebpackModules.getModuleByName('React'); return WebpackModules.getModuleByName('React');
} }
static get ReactDOM() { static get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM'); return WebpackModules.getModuleByName('ReactDOM');
} }
static processMarkup(markup) { static processMarkup(markup) {
if (!emotesEnabled) return markup; // TODO Get it from setttings if (!emotesEnabled) return markup; // TODO Get it from setttings
const newMarkup = []; const newMarkup = [];
@ -92,13 +100,20 @@ export default class {
} }
static async observe() { static async observe() {
const dataPath = Globals.getObject('paths').find(path => path.id === 'data').path; const dataPath = Globals.getPath('data');
try {
emotes = await FileUtils.readJsonFromFile(path.join(dataPath, 'emotes.json'));
} catch (err) {
Logger.err('EmoteModule', [`Failed to load emote data. Make sure you've downloaded the emote data and placed it in ${dataPath}:`, err]);
return;
}
try { try {
emotes = await FileUtils.readJsonFromFile(dataPath + '/emotes.json');
const Message = await ReactComponents.getComponent('Message'); const Message = await ReactComponents.getComponent('Message');
this.unpatchRender = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('render', (component, args, retVal) => { this.unpatchRender = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('render', (component, args, retVal) => {
try { try {
const markup = this.findByProp(retVal, 'className', 'markup'); // First child has all the actual text content, second is the edited timestamp // First child has all the actual text content, second is the edited timestamp
const markup = this.findByProp(retVal, 'className', 'markup');
if (!markup) return; if (!markup) return;
markup.children[0] = this.processMarkup(markup.children[0]); markup.children[0] = this.processMarkup(markup.children[0]);
} catch (err) { } catch (err) {
@ -131,12 +146,11 @@ export default class {
} }
const { bdemoteName, bdemoteSrc } = root.dataset; const { bdemoteName, bdemoteSrc } = root.dataset;
if (!bdemoteName || !bdemoteSrc) return; if (!bdemoteName || !bdemoteSrc) return;
VueInjector.inject( VueInjector.inject(root, {
root, components: { EmoteComponent },
DOM.createElement('span'), data: { src: bdemoteSrc, name: bdemoteName },
{ EmoteComponent }, template: '<EmoteComponent :src="src" :name="name" />'
`<EmoteComponent src="${bdemoteSrc}" name="${bdemoteName}"/>` }, DOM.createElement('span'));
);
root.classList.add('bd-is-emote'); root.classList.add('bd-is-emote');
} }
@ -179,4 +193,5 @@ export default class {
} }
}); });
} }
} }

View File

@ -0,0 +1,65 @@
[
{
"__user": "Jiiks#5000",
"id": "81388395867156480",
"developer": true,
"webdev": true,
"contributor": true
},
{
"__user": "Pohky#0156",
"id": "98003542823944192",
"developer": true,
"webdev": false,
"contributor": true
},
{
"__user": "Hammock#3110",
"id": "138850472541814784",
"developer": false,
"webdev": true,
"contributor": true
},
{
"__user": "Zerebos#7790",
"id": "249746236008169473",
"developer": true,
"webdev": false,
"contributor": true
},
{
"__user": "Pierce#1337",
"id": "125367412370440192",
"developer": true,
"webdev": false,
"contributor": true
},
{
"__user": "Samuel Elliott#2764",
"id": "284056145272766465",
"developer": true,
"webdev": false,
"contributor": true
},
{
"__user": "Lilian Tedone#6223",
"id": "184021060562321419",
"developer": false,
"webdev": false,
"contributor": true
},
{
"__user": "samfun123#8972",
"id": "76052829285916672",
"developer": false,
"webdev": false,
"contributor": true
},
{
"__user": "samogot#4379",
"id": "171005991272316937",
"developer": false,
"webdev": false,
"contributor": true
}
]

View File

@ -51,6 +51,12 @@
"hint": "Adds some of BetterDiscord's internal modules to `global._bd`.", "hint": "Adds some of BetterDiscord's internal modules to `global._bd`.",
"value": false "value": false
}, },
{
"id": "debugger-keybind",
"type": "keybind",
"text": "Debugger keybind",
"hint": "When this keybind is activated the developer tools will be opened and Discord will be paused."
},
{ {
"id": "ignore-content-manager-errors", "id": "ignore-content-manager-errors",
"type": "bool", "type": "bool",
@ -91,7 +97,6 @@
{ {
"id": "css", "id": "css",
"text": "CSS Editor", "text": "CSS Editor",
"hidden": true,
"settings": [ "settings": [
{ {
"category": "default", "category": "default",

View File

@ -10,39 +10,32 @@
import { DOM, BdUI, Modals, Reflection } from 'ui'; import { DOM, BdUI, Modals, Reflection } from 'ui';
import BdCss from './styles/index.scss'; import BdCss from './styles/index.scss';
import { Patcher, MonkeyPatch, Vendor, Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database, ReactComponents, ReactAutoPatcher, DiscordApi } from 'modules'; import { Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, WebpackModules, Patcher, MonkeyPatch, ReactComponents, ReactAutoPatcher, DiscordApi } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils } from 'common'; import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
import { EmoteModule } from 'builtin'; import { EmoteModule } from 'builtin';
import electron from 'electron';
import path from 'path';
const tests = typeof PRODUCTION === 'undefined';
const ignoreExternal = false; const ignoreExternal = false;
const DEV = true;
class BetterDiscord { class BetterDiscord {
constructor() { constructor() {
Logger.file = tests ? path.resolve(__dirname, '..', '..', 'tests', 'log.txt') : path.join(__dirname, 'log.txt');
Logger.log('main', 'BetterDiscord starting');
this._bd = { this._bd = {
DOM, DOM, BdUI, Modals, Reflection,
BdUI,
Modals, Events, CssEditor, Globals, Settings, Database, Updater,
Reflection, ModuleManager, PluginManager, ThemeManager, ExtModuleManager,
Patcher,
MonkeyPatch,
Vendor, Vendor,
Events,
CssEditor, WebpackModules, Patcher, MonkeyPatch, ReactComponents, DiscordApi,
Globals, EmoteModule,
ExtModuleManager,
PluginManager, Logger, ClientIPC, Utils
ThemeManager,
ModuleManager,
WebpackModules,
Settings,
Database,
ReactComponents,
DiscordApi,
Logger,
ClientIPC,
Utils,
EmoteModule
}; };
const developermode = Settings.getSetting('core', 'advanced', 'developer-mode'); const developermode = Settings.getSetting('core', 'advanced', 'developer-mode');
@ -52,6 +45,14 @@ class BetterDiscord {
else if (window._bd) delete window._bd; else if (window._bd) delete window._bd;
}); });
const debuggerkeybind = Settings.getSetting('core', 'advanced', 'debugger-keybind');
debuggerkeybind.on('keybind-activated', () => {
const currentWindow = electron.remote.getCurrentWindow();
if (currentWindow.isDevToolsOpened()) return eval('debugger;');
currentWindow.openDevTools();
setTimeout(() => eval('debugger;'), 1000);
});
DOM.injectStyle(BdCss, 'bdmain'); DOM.injectStyle(BdCss, 'bdmain');
this.globalReady = this.globalReady.bind(this); this.globalReady = this.globalReady.bind(this);
Events.on('global-ready', this.globalReady); Events.on('global-ready', this.globalReady);
@ -63,14 +64,16 @@ class BetterDiscord {
await Database.init(); await Database.init();
await Settings.loadSettings(); await Settings.loadSettings();
await ModuleManager.initModules(); await ModuleManager.initModules();
Modals.showContentManagerErrors();
if (!ignoreExternal) { if (!ignoreExternal) {
await ExtModuleManager.loadAllModules(true); await ExtModuleManager.loadAllModules(true);
await PluginManager.loadAllPlugins(true); await PluginManager.loadAllPlugins(true);
await ThemeManager.loadAllThemes(true); await ThemeManager.loadAllThemes(true);
} }
if (!Settings.get('core', 'advanced', 'ignore-content-manager-errors')) if (!Settings.get('core', 'advanced', 'ignore-content-manager-errors'))
Modals.showContentManagerErrors(); Modals.showContentManagerErrors();
Events.emit('ready'); Events.emit('ready');
Events.emit('discord-ready'); Events.emit('discord-ready');
EmoteModule.observe(); EmoteModule.observe();
@ -90,7 +93,7 @@ class BetterDiscord {
if (window.BetterDiscord) { if (window.BetterDiscord) {
Logger.log('main', 'Attempting to inject again?'); Logger.log('main', 'Attempting to inject again?');
} else { } else {
let instance = null; let instance;
Events.on('autopatcher', () => instance = new BetterDiscord()); Events.on('autopatcher', () => instance = new BetterDiscord());
ReactAutoPatcher.autoPatch().then(() => Events.emit('autopatcher')); ReactAutoPatcher.autoPatch().then(() => Events.emit('autopatcher'));
} }

View File

@ -59,6 +59,7 @@ export default class Content {
/** /**
* Opens a settings modal for this content. * Opens a settings modal for this content.
* @return {Modal}
*/ */
showSettingsModal() { showSettingsModal() {
return Modals.contentSettings(this); return Modals.contentSettings(this);
@ -73,27 +74,20 @@ export default class Content {
/** /**
* Saves the content's current configuration. * Saves the content's current configuration.
* @return {Promise}
*/ */
async saveConfiguration() { async saveConfiguration() {
try { try {
/* Database.insertOrUpdate({ type: `${this.type}-config`, id: this.id }, {
await FileUtils.writeFile(`${this.contentPath}/user.config.json`, JSON.stringify({ type: `${this.type}-config`,
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, id: this.id,
name: this.name,
enabled: this.enabled, enabled: this.enabled,
config: this.settings.strip().settings, config: this.settings.strip().settings,
data: this.data data: this.data
}); });
this.settings.setSaved(); this.settings.setSaved();
} catch (err) { } catch (err) {
Logger.err(this.name, ['Failed to save configuration', err]); Logger.err(this.name, ['Failed to save configuration', err]);
throw err; throw err;
} }
} }
@ -143,15 +137,6 @@ export default class Content {
return this.events.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. * 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 {String} event The event to add the listener to
@ -162,6 +147,15 @@ export default class Content {
return this.events.once(...args); return this.events.once(...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);
}
/** /**
* Emits an event. * Emits an event.
* @param {String} event The event to emit * @param {String} event The event to emit

View File

@ -24,31 +24,52 @@ import Combokeys from 'combokeys';
export default class { export default class {
/** /**
* Any errors that happened * Any errors that happened.
* returns {Array} * @return {Array}
*/ */
static get errors() { static get errors() {
return this._errors || (this._errors = []); return this._errors || (this._errors = []);
} }
/** /**
* Locallly stored content * Locally stored content.
* returns {Array} * @return {Array}
*/ */
static get localContent() { static get localContent() {
return this._localContent ? this._localContent : (this._localContent = []); return this._localContent ? this._localContent : (this._localContent = []);
} }
/** /**
* Local path for content * The type of content this content manager manages.
* returns {String} */
static get contentType() {
return undefined;
}
/**
* The name of this content manager.
*/
static get moduleName() {
return undefined;
}
/**
* The path used to store this content manager's content.
*/
static get pathId() {
return undefined;
}
/**
* Local path for content.
* @return {String}
*/ */
static get contentPath() { static get contentPath() {
return Globals.getPath(this.pathId); return Globals.getPath(this.pathId);
} }
/** /**
* Load all locally stored content * Load all locally stored content.
* @param {bool} suppressErrors Suppress any errors that occur during loading of content * @param {bool} suppressErrors Suppress any errors that occur during loading of content
*/ */
static async loadAllContent(suppressErrors = false) { static async loadAllContent(suppressErrors = false) {
@ -83,8 +104,6 @@ export default class {
}); });
this._errors = []; this._errors = [];
} }
return this.localContent;
} catch (err) { } catch (err) {
throw err; throw err;
} }
@ -102,7 +121,7 @@ export default class {
const directories = await FileUtils.listDirectory(this.contentPath); const directories = await FileUtils.listDirectory(this.contentPath);
for (let dir of directories) { for (let dir of directories) {
// If content is already loaded this should resolve. // If content is already loaded this should resolve
if (this.getContentByDirName(dir)) continue; if (this.getContentByDirName(dir)) continue;
try { try {
@ -150,8 +169,6 @@ export default class {
}); });
this._errors = []; this._errors = [];
} }
return this.localContent;
} catch (err) { } catch (err) {
throw err; throw err;
} }
@ -169,15 +186,12 @@ export default class {
await FileUtils.directoryExists(contentPath); await FileUtils.directoryExists(contentPath);
if (!reload) { if (!reload && this.getContentByPath(contentPath))
const loaded = this.localContent.find(content => content.contentPath === contentPath); throw { 'message': `Attempted to load already loaded user content: ${path}` };
if (loaded) {
throw { 'message': `Attempted to load already loaded user content: ${path}` };
}
}
const readConfig = await this.readConfig(contentPath); const configPath = path.resolve(contentPath, 'config.json');
const mainPath = path.join(contentPath, readConfig.main); const readConfig = await FileUtils.readJsonFromFile(configPath);
const mainPath = path.join(contentPath, readConfig.main || 'index.js');
const defaultConfig = new SettingsSet({ const defaultConfig = new SettingsSet({
settings: readConfig.defaultConfig, settings: readConfig.defaultConfig,
@ -191,19 +205,16 @@ export default class {
}; };
try { try {
//const readUserConfig = await this.readUserConfig(contentPath); const id = readConfig.info.id || readConfig.info.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
const readUserConfig = await Database.find({ type: 'contentconfig', name: readConfig.info.name }); const readUserConfig = await Database.find({ type: `${this.contentType}-config`, id });
if (readUserConfig.length) { if (readUserConfig.length) {
userConfig.enabled = readUserConfig[0].enabled || false; 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.config = readUserConfig[0].config;
userConfig.data = readUserConfig[0].data || {}; 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*/ } catch (err) {
Logger.info(this.moduleName, `Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`); // 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.err(this.moduleName, err); Logger.warn(this.moduleName, [`Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`, err]);
} }
userConfig.config = defaultConfig.clone({ settings: userConfig.config }); userConfig.config = defaultConfig.clone({ settings: userConfig.config });
@ -244,9 +255,10 @@ export default class {
} }
/** /**
* Unload content * Unload content.
* @param {any} content Content to unload * @param {Content|String} content Content to unload
* @param {bool} reload Whether to reload the content after * @param {Boolean} reload Whether to reload the content after
* @return {Content}
*/ */
static async unloadContent(content, reload) { static async unloadContent(content, reload) {
content = this.findContent(content); content = this.findContent(content);
@ -275,34 +287,18 @@ export default class {
} }
/** /**
* Reload content * Reload content.
* @param {any} content Content to reload * @param {Content|String} content Content to reload
* @return {Content}
*/ */
static reloadContent(content) { static reloadContent(content) {
return this.unloadContent(content, true); return this.unloadContent(content, true);
} }
/**
* Read content config file
* @param {any} configPath Config file path
*/
static async readConfig(configPath) {
configPath = path.resolve(configPath, 'config.json');
return FileUtils.readJsonFromFile(configPath);
}
/**
* Read content user config file
* @param {any} configPath User config file path
*/
static async readUserConfig(configPath) {
configPath = path.resolve(configPath, 'user.config.json');
return FileUtils.readJsonFromFile(configPath);
}
/** /**
* Checks if the passed object is an instance of this content type. * Checks if the passed object is an instance of this content type.
* @param {any} content Object to check * @param {Any} content Object to check
* @return {Boolean}
*/ */
static isThisContent(content) { static isThisContent(content) {
return content instanceof Content; return content instanceof Content;
@ -318,8 +314,9 @@ export default class {
/** /**
* Wildcard content finder * Wildcard content finder
* @param {any} wild Content ID / directory name / path / name * @param {String} wild Content ID / directory name / path / name
* @param {bool} nonunique Allow searching attributes that may not be unique * @param {Boolean} nonunique Allow searching attributes that may not be unique
* @return {Content}
*/ */
static findContent(wild, nonunique) { static findContent(wild, nonunique) {
if (this.isThisContent(wild)) return wild; if (this.isThisContent(wild)) return wild;
@ -338,7 +335,8 @@ export default class {
/** /**
* Wait for content to load * Wait for content to load
* @param {any} content_id * @param {String} content_id
* @return {Promise}
*/ */
static waitForContent(content_id) { static waitForContent(content_id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -32,24 +32,24 @@ export default new class {
} }
/** /**
* Init css editor * Init css editor.
*/ */
init() { init() {
ClientIPC.on('bd-get-scss', () => this.sendToEditor('set-scss', { scss: this.scss })); ClientIPC.on('bd-get-scss', () => this.scss, true);
ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss)); ClientIPC.on('bd-update-scss', (e, scss) => this.updateScss(scss));
ClientIPC.on('bd-save-csseditor-bounds', (e, bounds) => this.saveEditorBounds(bounds)); ClientIPC.on('bd-save-csseditor-bounds', (e, bounds) => this.saveEditorBounds(bounds));
ClientIPC.on('bd-save-scss', async (e, scss) => { ClientIPC.on('bd-save-scss', async (e, scss) => {
await this.updateScss(scss); await this.updateScss(scss);
await this.save(); await this.save();
}); }, true);
this.liveupdate = Settings.getSetting('css', 'default', 'live-update'); this.liveupdate = Settings.getSetting('css', 'default', 'live-update');
this.liveupdate.on('setting-updated', event => { this.liveupdate.on('setting-updated', event => {
this.sendToEditor('set-liveupdate', event.value); this.sendToEditor('set-liveupdate', event.value);
}); });
ClientIPC.on('bd-get-liveupdate', () => this.sendToEditor('set-liveupdate', this.liveupdate.value)); ClientIPC.on('bd-get-liveupdate', () => this.liveupdate.value, true);
ClientIPC.on('bd-set-liveupdate', (e, value) => this.liveupdate.value = value); ClientIPC.on('bd-set-liveupdate', (e, value) => this.liveupdate.value = value);
this.watchfilessetting = Settings.getSetting('css', 'default', 'watch-files'); this.watchfilessetting = Settings.getSetting('css', 'default', 'watch-files');
@ -60,20 +60,20 @@ export default new class {
} }
/** /**
* Show css editor, flashes if already visible * Show css editor, flashes if already visible.
*/ */
async show() { async show() {
await ClientIPC.send('openCssEditor', this.editor_bounds); await ClientIPC.send('openCssEditor', this.editor_bounds);
} }
/** /**
* Update css in client * Update css in client.
* @param {String} scss scss to compile * @param {String} scss SCSS to compile
* @param {bool} sendSource send to css editor instance * @param {bool} sendSource Whether to send to css editor instance
*/ */
async updateScss(scss, sendSource) { async updateScss(scss, sendSource) {
if (sendSource) if (sendSource)
this.sendToEditor('set-scss', { scss }); this.sendToEditor('set-scss', scss);
if (!scss) { if (!scss) {
this._scss = this.css = ''; this._scss = this.css = '';
@ -97,24 +97,26 @@ export default new class {
} }
/** /**
* Save css to file * Save css to file.
* @return {Promise}
*/ */
async save() { save() {
Settings.saveSettings(); return Settings.saveSettings();
} }
/** /**
* Save current editor bounds * Save current editor bounds.
* @param {Rectangle} bounds editor bounds * @param {Rectangle} bounds Editor bounds
* @return {Promise}
*/ */
saveEditorBounds(bounds) { saveEditorBounds(bounds) {
this.editor_bounds = bounds; this.editor_bounds = bounds;
Settings.saveSettings(); return Settings.saveSettings();
} }
/** /**
* Send scss to core for compilation * Send SCSS to core for compilation.
* @param {String} scss scss string * @param {String} scss SCSS string
*/ */
async compile(scss) { async compile(scss) {
return await ClientIPC.send('bd-compileSass', { return await ClientIPC.send('bd-compileSass', {
@ -124,7 +126,7 @@ export default new class {
} }
/** /**
* Recompile the current SCSS * Recompile the current SCSS.
* @return {Promise} * @return {Promise}
*/ */
async recompile() { async recompile() {
@ -132,16 +134,18 @@ export default new class {
} }
/** /**
* Send data to open editor * Send data to open editor.
* @param {any} channel * @param {String} channel
* @param {any} data * @param {Any} data
* @return {Promise}
*/ */
async sendToEditor(channel, data) { async sendToEditor(channel, data) {
return await ClientIPC.send('sendToCssEditor', { channel, data }); return ClientIPC.sendToCssEditor(channel, data);
} }
/** /**
* Opens an SCSS file in a system editor * Opens an SCSS file in a system editor.
* @return {Promise}
*/ */
async openSystemEditor() { async openSystemEditor() {
try { try {
@ -160,7 +164,8 @@ export default new class {
throw {message: 'Failed to open system editor.'}; throw {message: 'Failed to open system editor.'};
} }
/** Set current state /**
* Set current state
* @param {String} scss Current uncompiled SCSS * @param {String} scss Current uncompiled SCSS
* @param {String} css Current compiled CSS * @param {String} css Current compiled CSS
* @param {String} files Files imported in the SCSS * @param {String} files Files imported in the SCSS
@ -168,36 +173,35 @@ export default new class {
*/ */
setState(scss, css, files, err) { setState(scss, css, files, err) {
this._scss = scss; this._scss = scss;
this.sendToEditor('set-scss', { scss }); this.sendToEditor('set-scss', scss);
this.css = css; this.css = css;
this.files = files; this.files = files;
this.error = err; this.error = err;
} }
/** /**
* Current uncompiled scss * Current uncompiled scss.
*/ */
get scss() { get scss() {
return this._scss || ''; return this._scss || '';
} }
/** /**
* Set current scss * Set current scss.
*/ */
set scss(scss) { set scss(scss) {
this.updateScss(scss, true); this.updateScss(scss, true);
} }
/** /**
* Current compiled css * Current compiled css.
*/ */
get css() { get css() {
return this._css || ''; return this._css || '';
} }
/** /**
* Inject compiled css to head * Inject compiled css to head.
* {DOM}
*/ */
set css(css) { set css(css) {
this._css = css; this._css = css;
@ -205,15 +209,14 @@ export default new class {
} }
/** /**
* Current error * Current error.
*/ */
get error() { get error() {
return this._error || undefined; return this._error || undefined;
} }
/** /**
* Set current error * Set current error.
* {DOM}
*/ */
set error(err) { set error(err) {
this._error = err; this._error = err;
@ -293,7 +296,7 @@ export default new class {
/** /**
* Checks if the system editor's file exists. * Checks if the system editor's file exists.
* @return {Boolean} * @return {Promise}
*/ */
async fileExists() { async fileExists() {
try { try {

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import { ClientIPC } from 'bdipc'; import { ClientIPC } from 'common';
export default class { export default class {
@ -16,6 +16,12 @@ export default class {
return true; return true;
} }
/**
* Inserts or updates data in the database.
* @param {Object} args The record to find
* @param {Object} data The new record
* @return {Promise}
*/
static async insertOrUpdate(args, data) { static async insertOrUpdate(args, data) {
try { try {
return ClientIPC.send('bd-dba', { action: 'update', args, data }); return ClientIPC.send('bd-dba', { action: 'update', args, data });
@ -24,6 +30,11 @@ export default class {
} }
} }
/**
* Finds data in the database.
* @param {Object} args The record to find
* @return {Promise}
*/
static async find(args) { static async find(args) {
try { try {
return ClientIPC.send('bd-dba', { action: 'find', args }); return ClientIPC.send('bd-dba', { action: 'find', args });
@ -31,4 +42,5 @@ export default class {
throw err; throw err;
} }
} }
} }

View File

@ -402,7 +402,7 @@ export default class DiscordApi {
static get currentChannel() { static get currentChannel() {
const channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId()); const channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId());
return channel.isPrivate ? new PrivateChannel(channel) : new GuildChannel(channel); if (channel) return channel.isPrivate() ? new PrivateChannel(channel) : new GuildChannel(channel);
} }
static get currentUser() { static get currentUser() {
@ -415,5 +415,5 @@ export default class DiscordApi {
for (const id of friends) returnUsers.push(User.fromId(id)); for (const id of friends) returnUsers.push(User.fromId(id));
return returnUsers; return returnUsers;
} }
} }

View File

@ -1,5 +1,5 @@
/** /**
* BetterDiscord WebpackModules Module * BetterDiscord Event Hook
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved. * All rights reserved.
* https://betterdiscord.net * https://betterdiscord.net
@ -8,14 +8,13 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import EventListener from './eventlistener'; import { Utils, ClientLogger as Logger } from 'common';
import { Utils } from 'common';
import Events from './events';
import { WebpackModules } from './webpackmodules'; import { WebpackModules } from './webpackmodules';
import Events from './events';
import EventListener from './eventlistener';
import * as SocketStructs from '../structs/socketstructs'; import * as SocketStructs from '../structs/socketstructs';
/** /**
* Discord socket event hook * Discord socket event hook
* @extends {EventListener} * @extends {EventListener}
@ -23,7 +22,7 @@ import * as SocketStructs from '../structs/socketstructs';
export default class extends EventListener { export default class extends EventListener {
init() { init() {
console.log(SocketStructs); Logger.log('EventHook', SocketStructs);
this.hook(); this.hook();
} }
@ -44,16 +43,16 @@ export default class extends EventListener {
orig.call(this, ...args); orig.call(this, ...args);
self.wsc = this; self.wsc = this;
self.emit(...args); self.emit(...args);
} };
} }
get eventsModule() { get eventsModule() {
return WebpackModules.getModuleByPrototypes(['setMaxListeners', 'emit']); return WebpackModules.getModuleByName('Events');
} }
/** /**
* Discord emit overload * Discord emit overload
* @param {any} e * @param {any} event
* @param {any} action * @param {any} action
* @param {any} data * @param {any} data
*/ */
@ -66,8 +65,8 @@ export default class extends EventListener {
/** /**
* Emit callback * Emit callback
* @param {any} e Event Action * @param {any} event Event
* @param {any} d Event Args * @param {any} data Event data
*/ */
dispatch(e, d) { dispatch(e, d) {
Events.emit('raw-event', { type: e, data: d }); Events.emit('raw-event', { type: e, data: d });
@ -143,7 +142,7 @@ export default class extends EventListener {
LFG_LISTING_CREATE: 'LFG_LISTING_CREATE', // No groups here LFG_LISTING_CREATE: 'LFG_LISTING_CREATE', // No groups here
LFG_LISTING_DELETE: 'LFG_LISTING_DELETE', // Thank you LFG_LISTING_DELETE: 'LFG_LISTING_DELETE', // Thank you
BRAINTREE_POPUP_BRIDGE_CALLBACK: 'BRAINTREE_POPUP_BRIDGE_CALLBACK' // What BRAINTREE_POPUP_BRIDGE_CALLBACK: 'BRAINTREE_POPUP_BRIDGE_CALLBACK' // What
} };
} }
} }

View File

@ -12,14 +12,39 @@ import { EventEmitter } from 'events';
const emitter = new EventEmitter(); const emitter = new EventEmitter();
export default class { export default class {
static on(eventName, callBack) {
emitter.on(eventName, callBack); /**
* Adds an event listener.
* @param {String} event The event to listen for
* @param {Function} callback The function to call when the event is emitted
*/
static on(event, callback) {
emitter.on(event, callback);
} }
static off(eventName, callBack) { /**
emitter.removeListener(eventName, callBack); * Adds an event listener that is only called once.
* @param {String} event The event to listen for
* @param {Function} callback The function to call when the event is emitted
*/
static once(event, callback) {
emitter.once(event, callback);
} }
/**
* Removes an event listener.
* @param {String} event The event to remove
* @param {Function} callback The listener to remove
*/
static off(event, callback) {
emitter.removeListener(event, callback);
}
/**
* Emits an event
* @param {String} event The event to emit
* @param {Any} ...data Data to pass to the event listeners
*/
static emit(...args) { static emit(...args) {
emitter.emit(...args); emitter.emit(...args);
} }

View File

@ -11,7 +11,8 @@
const eventemitters = new WeakMap(); const eventemitters = new WeakMap();
export default class EventsWrapper { export default class EventsWrapper {
constructor(eventemitter) {
constructor(eventemitter, bind) {
eventemitters.set(this, eventemitter); eventemitters.set(this, eventemitter);
} }
@ -19,26 +20,33 @@ export default class EventsWrapper {
return this._eventSubs || (this._eventSubs = []); return this._eventSubs || (this._eventSubs = []);
} }
get on() { return this.subscribe }
subscribe(event, callback) { subscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return; if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
this.eventSubs.push({ const boundCallback = () => callback.apply(this.bind, arguments);
event, this.eventSubs.push({ event, callback, boundCallback });
callback eventemitters.get(this).on(event, boundCallback);
});
eventemitters.get(this).on(event, callback);
} }
once(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
const boundCallback = () => this.off(event, callback) && callback.apply(this.bind, arguments);
this.eventSubs.push({ event, callback, boundCallback });
eventemitters.get(this).on(event, boundCallback);
}
get off() { return this.unsubscribe }
unsubscribe(event, callback) { unsubscribe(event, callback) {
for (let index of this.eventSubs) { for (let index of this.eventSubs) {
if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) return; if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) continue;
eventemitters.get(this).off(event, this.eventSubs[index].callback); eventemitters.get(this).off(event, this.eventSubs[index].boundCallback);
this.eventSubs.splice(index, 1); this.eventSubs.splice(index, 1);
} }
} }
unsubscribeAll() { unsubscribeAll() {
for (let event of this.eventSubs) { for (let event of this.eventSubs) {
eventemitters.get(this).off(event.event, event.callback); eventemitters.get(this).off(event.event, event.boundCallback);
} }
this.eventSubs.splice(0, this.eventSubs.length); this.eventSubs.splice(0, this.eventSubs.length);
} }

View File

@ -8,9 +8,10 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import sparkplug from 'sparkplug';
import { ClientIPC } from 'common';
import Module from './module'; import Module from './module';
import Events from './events'; import Events from './events';
import { ClientIPC } from 'bdipc';
export default new class extends Module { export default new class extends Module {
@ -28,28 +29,24 @@ export default new class extends Module {
this.getObject = this.getObject.bind(this); this.getObject = this.getObject.bind(this);
} }
first() { async first() {
(async() => { const config = await ClientIPC.send('getConfig');
const config = await ClientIPC.send('getConfig'); this.setState({ config });
this.setState(config);
// This is for Discord to stop error reporting :3 // This is for Discord to stop error reporting :3
window.BetterDiscord = { window.BetterDiscord = {
'version': config.version, version: config.version,
'v': config.version v: config.version
}; };
window.jQuery = {}; window.jQuery = {};
if (window.__bd) { if (sparkplug.bd) {
this.setState(window.__bd); this.setState({ bd: sparkplug.bd });
window.__bd = { sparkplug.bd.setWS = this.setWS;
setWS: this.setWS }
}
}
Events.emit('global-ready'); Events.emit('global-ready');
Events.emit('socket-created', this.state.wsHook); Events.emit('socket-created', this.state.wsHook);
})();
} }
setWS(wSocket) { setWS(wSocket) {
@ -60,19 +57,43 @@ export default new class extends Module {
} }
getObject(name) { getObject(name) {
return this.state[name]; return this.config[name] || this.bd[name];
}
get bd() {
return this.state.bd;
}
get localStorage() {
return this.bd.localStorage;
}
get webSocket() {
return this.bd.wsHook;
}
get WebSocket() {
return this.bd.wsOrig;
}
get ignited() {
return this.bd.ignited;
}
get config() {
return this.state.config;
}
get paths() {
return this.config.paths;
} }
getPath(id) { getPath(id) {
return this.state.paths.find(path => path.id === id).path; return this.paths.find(path => path.id === id).path;
} }
static get paths() { get version() {
return this.state.paths; return this.config.version;
}
static get version() {
return this.state.version;
} }
} }

View File

@ -5,20 +5,19 @@
* https://github.com/JsSucks - https://betterdiscord.net * https://github.com/JsSucks - https://betterdiscord.net
* *
* This source code is licensed under the MIT license found in the * 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.
*/
/*
Base Module that every non-static module should extend
*/ */
/**
* Base Module that every non-static module should extend
*/
export default class Module { export default class Module {
constructor(args) { constructor(args) {
this.__ = { this.__ = {
state: args || {}, state: args || {},
args args
} };
this.setState = this.setState.bind(this); this.setState = this.setState.bind(this);
this.initialize(); this.initialize();
} }
@ -38,7 +37,6 @@ export default class Module {
set args(t) { } set args(t) { }
get args() { return this.__.args; } get args() { return this.__.args; }
set state(state) { return this.__.state = state; } set state(state) { return this.__.state = state; }
get state() { return this.__.state; } get state() { return this.__.state; }

View File

@ -8,30 +8,39 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/*Module Manager initializes all modules when everything is ready*/ import { ClientLogger as Logger } from 'common';
import { Events, SocketProxy, EventHook, CssEditor } from 'modules'; import { Events, SocketProxy, EventHook, CssEditor } from 'modules';
import { ProfileBadges } from 'ui'; import { ProfileBadges } from 'ui';
import Updater from './updater'; import Updater from './updater';
/**
* Module Manager initializes all modules when everything is ready
*/
export default class { export default class {
/**
* An array of modules.
*/
static get modules() { static get modules() {
return this._modules ? this._modules : (this._modules = [ return this._modules ? this._modules : (this._modules = [
new ProfileBadges(), new ProfileBadges(),
new SocketProxy(), new SocketProxy(),
new EventHook(), new EventHook(),
CssEditor, CssEditor,
new Updater() Updater
]); ]);
} }
/**
* Initializes all modules.
* @return {Promise}
*/
static async initModules() { static async initModules() {
for (let module of this.modules) { for (let module of this.modules) {
try { try {
if (module.init && module.init instanceof Function) module.init(); if (module.init && module.init instanceof Function) module.init();
} catch (err) { } catch (err) {
console.log(`Failed to initialize module: ${err}`); Logger.err('Module Manager', ['Failed to initialize module:', err]);
} }
} }
return true; return true;

View File

@ -1,19 +1,23 @@
export { default as Events } from './events'; export { default as Events } from './events';
export { default as Settings } from './settings';
export { default as CssEditor } from './csseditor'; export { default as CssEditor } from './csseditor';
export { default as ExtModuleManager } from './extmodulemanager'; export { default as Globals } from './globals';
export { default as Settings } from './settings';
export { default as Database } from './database';
export { default as Updater } from './updater';
export { default as ModuleManager } from './modulemanager';
export { default as PluginManager } from './pluginmanager'; export { default as PluginManager } from './pluginmanager';
export { default as ThemeManager } from './thememanager'; export { default as ThemeManager } from './thememanager';
export { default as Globals } from './globals'; export { default as ExtModuleManager } from './extmodulemanager';
export { default as Permissions } from './permissionmanager';
export { default as EventsWrapper } from './eventswrapper';
export { default as Vendor } from './vendor'; export { default as Vendor } from './vendor';
export * from './webpackmodules'; export * from './webpackmodules';
export { default as ModuleManager } from './modulemanager'; export * from './patcher';
export * from './reactcomponents';
export { default as EventListener } from './eventlistener'; export { default as EventListener } from './eventlistener';
export { default as SocketProxy } from './socketproxy'; export { default as SocketProxy } from './socketproxy';
export { default as EventHook } from './eventhook'; 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'; export { default as DiscordApi } from './discordapi';
export * from './patcher';
export * from './reactcomponents';

View File

@ -5,14 +5,16 @@
* https://github.com/JsSucks - https://betterdiscord.net * https://github.com/JsSucks - https://betterdiscord.net
* *
* This source code is licensed under the MIT license found in the * 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.
*/ */
import { WebpackModules } from './webpackmodules'; import { WebpackModules } from './webpackmodules';
import { ClientLogger as Logger, Utils } from 'common'; import { ClientLogger as Logger, Utils } from 'common';
export class Patcher { export class Patcher {
static get patches() { return this._patches || (this._patches = {}) } static get patches() { return this._patches || (this._patches = {}) }
static getPatchesByCaller(id) { static getPatchesByCaller(id) {
const patches = []; const patches = [];
for (const patch in this.patches) { for (const patch in this.patches) {
@ -22,16 +24,21 @@ export class Patcher {
} }
return patches; return patches;
} }
static unpatchAll(patches) { static unpatchAll(patches) {
if (typeof patches === 'string')
patches = this.getPatchesByCaller(patches);
for (const patch of patches) { for (const patch of patches) {
for (const child of patch.children) { for (const child of patch.children) {
child.unpatch(); child.unpatch();
} }
} }
} }
static resolveModule(module) { static resolveModule(module) {
if (module instanceof Function || (module instanceof Object && !(module instanceof Array))) return module; if (module instanceof Function || (module instanceof Object && !(module instanceof Array))) return module;
if ('string' === typeof module) return WebpackModules.getModuleByName(module); if (typeof module === 'string') return WebpackModules.getModuleByName(module);
if (module instanceof Array) return WebpackModules.getModuleByProps(module); if (module instanceof Array) return WebpackModules.getModuleByProps(module);
return null; return null;
} }
@ -99,10 +106,12 @@ export class Patcher {
static before() { return this.pushChildPatch(...arguments, 'before') } static before() { return this.pushChildPatch(...arguments, 'before') }
static after() { return this.pushChildPatch(...arguments, 'after') } static after() { return this.pushChildPatch(...arguments, 'after') }
static instead() { return this.pushChildPatch(...arguments, 'instead') } static instead() { return this.pushChildPatch(...arguments, 'instead') }
static pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, type = 'after') { static pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, type = 'after') {
const module = this.resolveModule(unresolvedModule); const module = this.resolveModule(unresolvedModule);
if (!module || !module[functionName] || !(module[functionName] instanceof Function)) return null; if (!module || !module[functionName] || !(module[functionName] instanceof Function)) return null;
displayName = 'string' === typeof unresolvedModule ? unresolvedModule : displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name; displayName = typeof unresolvedModule === 'string' ? unresolvedModule :
displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
const patchId = `${displayName}:${functionName}:${caller}`; const patchId = `${displayName}:${functionName}:${caller}`;
const patch = this.patches[patchId] || this.pushPatch(caller, patchId, module, functionName); const patch = this.patches[patchId] || this.pushPatch(caller, patchId, module, functionName);

View File

@ -15,15 +15,15 @@ export default class Plugin extends Content {
get type() { return 'plugin' } get type() { return 'plugin' }
// Don't use - these will eventually be removed!
get pluginPath() { return this.contentPath }
get pluginConfig() { return this.config }
get start() { return this.enable } get start() { return this.enable }
get stop() { return this.disable } get stop() { return this.disable }
reload() {
return PluginManager.reloadPlugin(this);
}
unload() { unload() {
PluginManager.unloadPlugin(this); return PluginManager.unloadPlugin(this);
} }
} }

View File

@ -1,5 +1,5 @@
/** /**
* BetterDiscord Plugin Api * BetterDiscord Plugin API
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved. * All rights reserved.
* https://betterdiscord.net * https://betterdiscord.net
@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import { Utils, ClientLogger as Logger, ClientIPC } from 'common'; import { Utils, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
import Settings from './settings'; import Settings from './settings';
import ExtModuleManager from './extmodulemanager'; import ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager'; import PluginManager from './pluginmanager';
@ -20,31 +20,24 @@ import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs'
import { BdMenuItems, Modals, DOM, Reflection } from 'ui'; import { BdMenuItems, Modals, DOM, Reflection } from 'ui';
import DiscordApi from './discordapi'; import DiscordApi from './discordapi';
import { ReactComponents } from './reactcomponents'; import { ReactComponents } from './reactcomponents';
import { MonkeyPatch } from './patcher'; import { Patcher, MonkeyPatch } from './patcher';
export default class PluginApi { export default class PluginApi {
constructor(pluginInfo) { constructor(pluginInfo, pluginPath) {
this.pluginInfo = pluginInfo; this.pluginInfo = pluginInfo;
this.pluginPath = pluginPath;
this.Events = new EventsWrapper(Events); this.Events = new EventsWrapper(Events);
Utils.defineSoftGetter(this.Events, 'bind', () => this.plugin);
this._menuItems = undefined; this._menuItems = undefined;
this._injectedStyles = undefined; this._injectedStyles = undefined;
this._modalStack = undefined; this._modalStack = undefined;
} }
get Discord() {
return DiscordApi;
}
get ReactComponents() {
return ReactComponents;
}
get Reflection() {
return Reflection;
}
get MonkeyPatch() {
return module => MonkeyPatch(this.pluginInfo.id, module);
}
get plugin() { get plugin() {
return PluginManager.getPluginById(this.pluginInfo.id || this.pluginInfo.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-')); return PluginManager.getPluginByPath(this.pluginPath);
} }
async bridge(plugin_id) { async bridge(plugin_id) {
@ -61,15 +54,18 @@ export default class PluginApi {
get Api() { return this } get Api() { return this }
get AsyncEventEmitter() { return AsyncEventEmitter }
get EventsWrapper() { return EventsWrapper }
/** /**
* Logger * Logger
*/ */
loggerLog(...message) { Logger.log(this.pluginInfo.name, message) } loggerLog(...message) { Logger.log(this.plugin.name, message) }
loggerErr(...message) { Logger.err(this.pluginInfo.name, message) } loggerErr(...message) { Logger.err(this.plugin.name, message) }
loggerWarn(...message) { Logger.warn(this.pluginInfo.name, message) } loggerWarn(...message) { Logger.warn(this.plugin.name, message) }
loggerInfo(...message) { Logger.info(this.pluginInfo.name, message) } loggerInfo(...message) { Logger.info(this.plugin.name, message) }
loggerDbg(...message) { Logger.dbg(this.pluginInfo.name, message) } loggerDbg(...message) { Logger.dbg(this.plugin.name, message) }
get Logger() { get Logger() {
return { return {
log: this.loggerLog.bind(this), log: this.loggerLog.bind(this),
@ -381,6 +377,53 @@ export default class PluginApi {
}); });
} }
/**
* DiscordApi
*/
get Discord() {
return DiscordApi;
}
get ReactComponents() {
return ReactComponents;
}
get Reflection() {
return Reflection;
}
/**
* Patcher
*/
get patches() {
return Patcher.getPatchesByCaller(this.plugin.id);
}
patchBefore(...args) { return this.pushChildPatch(...args, 'before') }
patchAfter(...args) { return this.pushChildPatch(...args, 'after') }
patchInstead(...args) { return this.pushChildPatch(...args, 'instead') }
pushChildPatch(...args) {
return Patcher.pushChildPatch(this.plugin.id, ...args);
}
unpatchAll(patches) {
return Patcher.unpatchAll(patches || this.plugin.id);
}
get Patcher() {
return Object.defineProperty({
before: this.patchBefore.bind(this),
after: this.patchAfter.bind(this),
instead: this.patchInstead.bind(this),
pushChildPatch: this.pushChildPatch.bind(this),
unpatchAll: this.unpatchAll.bind(this),
}, 'patches', {
get: () => this.patches
});
}
get monkeyPatch() {
return module => MonkeyPatch(this.plugin.id, module);
}
} }
// Stop plugins from modifying the plugin API for all plugins // Stop plugins from modifying the plugin API for all plugins

View File

@ -76,12 +76,12 @@ export default class extends ContentManager {
static async loadPlugin(paths, configs, info, main, dependencies, permissions) { static async loadPlugin(paths, configs, info, main, dependencies, permissions) {
if (permissions && permissions.length > 0) { if (permissions && permissions.length > 0) {
for (let perm of permissions) { for (let perm of permissions) {
console.log(`Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`); Logger.log(this.moduleName, `Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
} }
try { try {
const allowed = await Modals.permissions(`${info.name} wants to:`, info.name, permissions).promise; const allowed = await Modals.permissions(`${info.name} wants to:`, info.name, permissions).promise;
} catch (err) { } catch (err) {
return null; return;
} }
} }
@ -90,15 +90,13 @@ export default class extends ContentManager {
for (const [key, value] of Object.entries(dependencies)) { for (const [key, value] of Object.entries(dependencies)) {
const extModule = ExtModuleManager.findModule(key); const extModule = ExtModuleManager.findModule(key);
if (!extModule) { if (!extModule) {
throw { throw {message: `Dependency ${key}:${value} is not loaded.`};
'message': `Dependency: ${key}:${value} is not loaded`
};
} }
deps[key] = extModule.__require; deps[key] = extModule.__require;
} }
} }
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor, deps); const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info, paths.contentPath), Vendor, deps);
if (!(plugin.prototype instanceof Plugin)) if (!(plugin.prototype instanceof Plugin))
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`}; throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};
@ -121,24 +119,24 @@ export default class extends ContentManager {
static get unloadPlugin() { return this.unloadContent } static get unloadPlugin() { return this.unloadContent }
static get reloadPlugin() { return this.reloadContent } static get reloadPlugin() { return this.reloadContent }
static stopPlugin(name) { /**
const plugin = name instanceof Plugin ? name : this.getPluginByName(name); * Stops a plugin.
try { * @param {Plugin|String} plugin
if (plugin) return plugin.stop(); * @return {Promise}
} catch (err) { */
// Logger.err('PluginManager', err); static stopPlugin(plugin) {
} plugin = this.isPlugin(plugin) ? plugin : this.getPluginById(plugin);
return true; //Return true anyways since plugin doesn't exist return plugin.stop();
} }
static startPlugin(name) { /**
const plugin = name instanceof Plugin ? name : this.getPluginByName(name); * Starts a plugin.
try { * @param {Plugin|String} plugin
if (plugin) return plugin.start(); * @return {Promise}
} catch (err) { */
// Logger.err('PluginManager', err); static startPlugin(plugin) {
} plugin = this.isPlugin(plugin) ? plugin : this.getPluginById(plugin);
return true; //Return true anyways since plugin doesn't exist return plugin.start();
} }
static get isPlugin() { return this.isThisContent } static get isPlugin() { return this.isThisContent }

View File

@ -6,7 +6,7 @@
* https://github.com/JsSucks - https://betterdiscord.net * https://github.com/JsSucks - https://betterdiscord.net
* *
* This source code is licensed under the MIT license found in the * 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.
*/ */
import { MonkeyPatch, Patcher } from './patcher'; import { MonkeyPatch, Patcher } from './patcher';
@ -19,6 +19,7 @@ class Helpers {
static get plannedActions() { static get plannedActions() {
return this._plannedActions || (this._plannedActions = new Map()); return this._plannedActions || (this._plannedActions = new Map());
} }
static recursiveArray(parent, key, count = 1) { static recursiveArray(parent, key, count = 1) {
let index = 0; let index = 0;
function* innerCall(parent, key) { function* innerCall(parent, key) {
@ -34,13 +35,15 @@ class Helpers {
return innerCall(parent, key); return innerCall(parent, key);
} }
static recursiveArrayCount(parent, key) { static recursiveArrayCount(parent, key) {
let count = 0; let count = 0;
// eslint-disable-next-line no-empty-pattern // eslint-disable-next-line no-empty-pattern
for (let { } of this.recursiveArray(parent, key)) for (let {} of this.recursiveArray(parent, key))
++count; ++count;
return this.recursiveArray(parent, key, count); return this.recursiveArray(parent, key, count);
} }
static get recursiveChildren() { static get recursiveChildren() {
return function* (parent, key, index = 0, count = 1) { return function* (parent, key, index = 0, count = 1) {
const item = parent[key]; const item = parent[key];
@ -52,12 +55,14 @@ class Helpers {
} }
} }
} }
static returnFirst(iterator, process) { static returnFirst(iterator, process) {
for (let child of iterator) { for (let child of iterator) {
const retVal = process(child); const retVal = process(child);
if (retVal !== undefined) return retVal; if (retVal !== undefined) return retVal;
} }
} }
static getFirstChild(rootParent, rootKey, selector) { static getFirstChild(rootParent, rootKey, selector) {
const getDirectChild = (item, selector) => { const getDirectChild = (item, selector) => {
if (item && item.props && item.props.children) { if (item && item.props && item.props.children) {
@ -116,11 +121,13 @@ class Helpers {
}; };
return this.returnFirst(this.recursiveChildren(rootParent, rootKey), checkFilter.bind(null, selector)) || {}; return this.returnFirst(this.recursiveChildren(rootParent, rootKey), checkFilter.bind(null, selector)) || {};
} }
static parseSelector(selector) { static parseSelector(selector) {
if (selector.startsWith('.')) return { className: selector.substr(1) } if (selector.startsWith('.')) return { className: selector.substr(1) }
if (selector.startsWith('#')) return { id: selector.substr(1) } if (selector.startsWith('#')) return { id: selector.substr(1) }
return {} return {}
} }
static findByProp(obj, what, value) { static findByProp(obj, what, value) {
if (obj.hasOwnProperty(what) && obj[what] === value) return obj; if (obj.hasOwnProperty(what) && obj[what] === value) return obj;
if (obj.props && !obj.children) return this.findByProp(obj.props, what, value); if (obj.props && !obj.children) return this.findByProp(obj.props, what, value);
@ -132,6 +139,7 @@ class Helpers {
} }
return null; return null;
} }
static findProp(obj, what) { static findProp(obj, what) {
if (obj.hasOwnProperty(what)) return obj[what]; if (obj.hasOwnProperty(what)) return obj[what];
if (obj.props && !obj.children) return this.findProp(obj.props, what); if (obj.props && !obj.children) return this.findProp(obj.props, what);
@ -144,6 +152,7 @@ class Helpers {
} }
return null; return null;
} }
static get ReactDOM() { static get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM'); return WebpackModules.getModuleByName('ReactDOM');
} }
@ -155,12 +164,15 @@ class ReactComponent {
this._component = component; this._component = component;
this._retVal = retVal; this._retVal = retVal;
} }
get id() { get id() {
return this._id; return this._id;
} }
get component() { get component() {
return this._component; return this._component;
} }
get retVal() { get retVal() {
return this._retVal; return this._retVal;
} }

View File

@ -8,22 +8,23 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import defaultSettings from '../data/user.settings.default';
import Globals from './globals';
import CssEditor from './csseditor';
import Events from './events';
import { Utils, FileUtils, ClientLogger as Logger } from 'common'; import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { SettingsSet, SettingUpdatedEvent } from 'structs'; import { SettingsSet, SettingUpdatedEvent } from 'structs';
import path from 'path'; import path from 'path';
import Globals from './globals';
import CssEditor from './csseditor';
import Events from './events';
import defaultSettings from '../data/user.settings.default';
export default new class Settings { export default new class Settings {
constructor() { constructor() {
this.settings = defaultSettings.map(_set => { this.settings = defaultSettings.map(_set => {
const set = new SettingsSet(_set); const set = new SettingsSet(_set);
set.on('setting-updated', event => { set.on('setting-updated', event => {
const { category, setting, value, old_value } = event; const { category, setting, value, old_value } = event;
Logger.log('Settings', `${set.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`); 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', event);
Events.emit(`setting-updated-${set.id}_${category.id}_${setting.id}`, event); Events.emit(`setting-updated-${set.id}_${category.id}_${setting.id}`, event);
}); });
@ -37,6 +38,9 @@ export default new class Settings {
}); });
} }
/**
* Loads BetterDiscord's settings.
*/
async loadSettings() { async loadSettings() {
try { try {
await FileUtils.ensureDirectory(this.dataPath); await FileUtils.ensureDirectory(this.dataPath);
@ -48,7 +52,7 @@ export default new class Settings {
for (let set of this.settings) { for (let set of this.settings) {
const newSet = settings.find(s => s.id === set.id); const newSet = settings.find(s => s.id === set.id);
if (!newSet) continue; if (!newSet) continue;
set.merge(newSet); await set.merge(newSet);
set.setSaved(); set.setSaved();
} }
@ -57,10 +61,13 @@ export default new class Settings {
} catch (err) { } catch (err) {
// There was an error loading settings // There was an error loading settings
// This probably means that the user doesn't have any settings yet // This probably means that the user doesn't have any settings yet
Logger.err('Settings', err); Logger.warn('Settings', ['Failed to load internal settings', err]);
} }
} }
/**
* Saves BetterDiscord's settings including CSS editor data.
*/
async saveSettings() { async saveSettings() {
try { try {
await FileUtils.ensureDirectory(this.dataPath); await FileUtils.ensureDirectory(this.dataPath);
@ -72,15 +79,10 @@ export default new class Settings {
css: CssEditor.css, css: CssEditor.css,
css_editor_files: CssEditor.files, css_editor_files: CssEditor.files,
scss_error: CssEditor.error, scss_error: CssEditor.error,
css_editor_bounds: { css_editor_bounds: CssEditor.editor_bounds
width: CssEditor.editor_bounds.width,
height: CssEditor.editor_bounds.height,
x: CssEditor.editor_bounds.x,
y: CssEditor.editor_bounds.y
}
}); });
for (let set of this.getSettings) { for (let set of this.settings) {
set.setSaved(); set.setSaved();
} }
} catch (err) { } catch (err) {
@ -90,8 +92,13 @@ export default new class Settings {
} }
} }
/**
* Finds one of BetterDiscord's settings sets.
* @param {String} set_id The ID of the set to find
* @return {SettingsSet}
*/
getSet(set_id) { getSet(set_id) {
return this.getSettings.find(s => s.id === set_id); return this.settings.find(s => s.id === set_id);
} }
get core() { return this.getSet('core') } get core() { return this.getSet('core') }
@ -100,39 +107,46 @@ export default new class Settings {
get css() { return this.getSet('css') } get css() { return this.getSet('css') }
get security() { return this.getSet('security') } get security() { return this.getSet('security') }
/**
* Finds a category in one of BetterDiscord's settings sets.
* @param {String} set_id The ID of the set to look in
* @param {String} category_id The ID of the category to find
* @return {SettingsCategory}
*/
getCategory(set_id, category_id) { getCategory(set_id, category_id) {
const set = this.getSet(set_id); const set = this.getSet(set_id);
return set ? set.getCategory(category_id) : undefined; return set ? set.getCategory(category_id) : undefined;
} }
/**
* Finds a setting in one of BetterDiscord's settings sets.
* @param {String} set_id The ID of the set to look in
* @param {String} category_id The ID of the category to look in
* @param {String} setting_id The ID of the setting to find
* @return {Setting}
*/
getSetting(set_id, category_id, setting_id) { getSetting(set_id, category_id, setting_id) {
const set = this.getSet(set_id); const set = this.getSet(set_id);
return set ? set.getSetting(category_id, setting_id) : undefined; return set ? set.getSetting(category_id, setting_id) : undefined;
} }
/**
* Returns a setting's value in one of BetterDiscord's settings sets.
* @param {String} set_id The ID of the set to look in
* @param {String} category_id The ID of the category to look in
* @param {String} setting_id The ID of the setting to find
* @return {Any}
*/
get(set_id, category_id, setting_id) { get(set_id, category_id, setting_id) {
const set = this.getSet(set_id); const set = this.getSet(set_id);
return set ? set.get(category_id, setting_id) : undefined; return set ? set.get(category_id, setting_id) : undefined;
} }
async mergeSettings(set_id, newSettings) { /**
const set = this.getSet(set_id); * The path to store user data in.
if (!set) return; */
return await set.merge(newSettings);
}
setSetting(set_id, category_id, setting_id, value) {
const setting = this.getSetting(set_id, category_id, setting_id);
if (!setting) throw {message: `Tried to set ${set_id}/${category_id}/${setting_id}, which doesn't exist`};
setting.value = value;
}
get getSettings() {
return this.settings;
}
get dataPath() { get dataPath() {
return Globals.getPath('data'); return Globals.getPath('data');
} }
} }

View File

@ -8,6 +8,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import { ClientLogger as Logger } from 'common';
import EventListener from './eventlistener'; import EventListener from './eventlistener';
export default class SocketProxy extends EventListener { export default class SocketProxy extends EventListener {
@ -19,7 +20,7 @@ export default class SocketProxy extends EventListener {
get eventBindings() { get eventBindings() {
return [ return [
{ id: 'socket-created', 'callback': this.socketCreated } { id: 'socket-created', callback: this.socketCreated }
]; ];
} }
@ -29,11 +30,11 @@ export default class SocketProxy extends EventListener {
socketCreated(socket) { socketCreated(socket) {
this.activeSocket = socket; this.activeSocket = socket;
// socket.addEventListener('message', this.onMessage); // socket.addEventListener('message', this.onMessage);
} }
onMessage(e) { onMessage(e) {
console.info(e); Logger.info('SocketProxy', e);
} }
} }

View File

@ -31,10 +31,6 @@ export default class Theme extends Content {
get type() { return 'theme' } get type() { return 'theme' }
get css() { return this.data.css } get css() { return this.data.css }
// Don't use - these will eventually be removed!
get themePath() { return this.contentPath }
get themeConfig() { return this.config }
/** /**
* Called when settings are updated. * Called when settings are updated.
* This can be overridden by other content types. * This can be overridden by other content types.
@ -63,7 +59,7 @@ export default class Theme extends Content {
* @return {Promise} * @return {Promise}
*/ */
async compile() { async compile() {
console.log('Compiling CSS'); Logger.log(this.name, 'Compiling CSS');
if (this.info.type === 'sass') { if (this.info.type === 'sass') {
const config = await ThemeManager.getConfigAsSCSS(this.settings); const config = await ThemeManager.getConfigAsSCSS(this.settings);
@ -76,7 +72,7 @@ export default class Theme extends Content {
Logger.log(this.name, ['Finished compiling theme', new class Info { Logger.log(this.name, ['Finished compiling theme', new class Info {
get SCSS_variables() { console.log(config); } get SCSS_variables() { console.log(config); }
get Compiled_SCSS() { console.log(result.css.toString()); } get Compiled_SCSS() { console.log(result.css.toString()); }
get Result() { console.log(result); } get Result() { console.log(result); }
}]); }]);
return { return {
@ -121,6 +117,7 @@ export default class Theme extends Content {
*/ */
set files(files) { set files(files) {
this.data.files = files; this.data.files = files;
if (Settings.get('css', 'default', 'watch-files')) if (Settings.get('css', 'default', 'watch-files'))
this.watchfiles = files; this.watchfiles = files;
} }

View File

@ -62,11 +62,11 @@ export default class ThemeManager extends ContentManager {
} }
static enableTheme(theme) { static enableTheme(theme) {
theme.enable(); return theme.enable();
} }
static disableTheme(theme) { static disableTheme(theme) {
theme.disable(); return theme.disable();
} }
static get isTheme() { return this.isThisContent } static get isTheme() { return this.isThisContent }
@ -74,6 +74,11 @@ export default class ThemeManager extends ContentManager {
return theme instanceof Theme; return theme instanceof Theme;
} }
/**
* Returns a representation of a settings set's values in SCSS.
* @param {SettingsSet} settingsset The set to convert to SCSS
* @return {Promise}
*/
static async getConfigAsSCSS(settingsset) { static async getConfigAsSCSS(settingsset) {
const variables = []; const variables = [];
@ -87,6 +92,11 @@ export default class ThemeManager extends ContentManager {
return variables.join('\n'); return variables.join('\n');
} }
/**
* Returns a representation of a settings set's values as an SCSS map.
* @param {SettingsSet} settingsset The set to convert to an SCSS map
* @return {Promise}
*/
static async getConfigAsSCSSMap(settingsset) { static async getConfigAsSCSSMap(settingsset) {
const variables = []; const variables = [];
@ -100,6 +110,11 @@ export default class ThemeManager extends ContentManager {
return '(' + variables.join(', ') + ')'; return '(' + variables.join(', ') + ')';
} }
/**
* Returns a setting's name and value as a string that can be included in SCSS.
* @param {Setting} setting The setting to convert to SCSS
* @return {Promise}
*/
static async parseSetting(setting) { static async parseSetting(setting) {
const { type, id, value } = setting; const { type, id, value } = setting;
const name = id.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-'); const name = id.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
@ -108,6 +123,11 @@ export default class ThemeManager extends ContentManager {
if (scss) return [name, scss]; if (scss) return [name, scss];
} }
/**
* Escapes a string so it can be included in SCSS.
* @param {String} value The string to escape
* @return {String}
*/
static toSCSSString(value) { static toSCSSString(value) {
if (typeof value !== 'string' && value.toString) value = value.toString(); if (typeof value !== 'string' && value.toString) value = value.toString();
return `'${typeof value === 'string' ? value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') : ''}'`; return `'${typeof value === 'string' ? value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') : ''}'`;

View File

@ -13,15 +13,20 @@ import Globals from './globals';
import { $ } from 'vendor'; import { $ } from 'vendor';
import { ClientLogger as Logger } from 'common'; import { ClientLogger as Logger } from 'common';
export default class { export default new class {
constructor() { constructor() {
window.updater = this;
this.updatesAvailable = false; this.updatesAvailable = false;
this.latestVersion = undefined;
this.error = undefined;
this.init = this.init.bind(this); this.init = this.init.bind(this);
this.checkForUpdates = this.checkForUpdates.bind(this); this.checkForUpdates = this.checkForUpdates.bind(this);
} }
/**
* The interval to wait before checking for updates.
*/
get interval() { get interval() {
return 60 * 1000 * 30; return 60 * 1000 * 30;
} }
@ -30,34 +35,61 @@ export default class {
this.updateInterval = setInterval(this.checkForUpdates, this.interval); this.updateInterval = setInterval(this.checkForUpdates, this.interval);
} }
update() { /**
// TODO * Installs an update.
this.updatesAvailable = false; * TODO
Events.emit('update-check-end'); */
async update() {
try {
await new Promise(resolve => setTimeout(resolve, 5000));
this.updatesAvailable = false;
this.latestVersion = Globals.version;
Events.emit('update-check-end');
} catch (err) {
this.error = err;
this.checkForUpdates();
throw err;
}
} }
/**
* Checks for updates.
* @return {Promise}
*/
checkForUpdates() { checkForUpdates() {
if (this.updatesAvailable) return; return new Promise((resolve, reject) => {
Events.emit('update-check-start'); if (this.updatesAvailable) return resolve(true);
Logger.info('Updater', 'Checking for updates'); Events.emit('update-check-start');
$.ajax({ Logger.info('Updater', 'Checking for updates');
type: 'GET',
url: 'https://rawgit.com/JsSucks/BetterDiscordApp/master/package.json', $.ajax({
cache: false, type: 'GET',
success: e => { url: 'https://rawgit.com/JsSucks/BetterDiscordApp/master/package.json',
try { cache: false,
Events.emit('update-check-end'); success: e => {
Logger.info('Updater', try {
`Latest Version: ${e.version} - Current Version: ${Globals.version}`); this.latestVersion = e.version;
if (e.version !== Globals.version) { Events.emit('update-check-end');
this.updatesAvailable = true; Logger.info('Updater', `Latest Version: ${e.version} - Current Version: ${Globals.version}`);
Events.emit('updates-available');
if (this.latestVersion !== Globals.version) {
this.updatesAvailable = true;
Events.emit('updates-available');
resolve(true);
}
resolve(false);
} catch (err) {
Events.emit('update-check-fail', err);
reject(err);
} }
} catch (err) { },
fail: err => {
Events.emit('update-check-fail', err); Events.emit('update-check-fail', err);
reject(err);
} }
}, });
fail: e => Events.emit('update-check-fail', e)
}); });
} }

View File

@ -11,29 +11,36 @@
import { WebpackModules } from './webpackmodules'; import { WebpackModules } from './webpackmodules';
import jQuery from 'jquery'; import jQuery from 'jquery';
import lodash from 'lodash'; import lodash from 'lodash';
import Vue from 'vue';
export { jQuery as $ }; export { jQuery as $ };
export default class { export default class {
static get jQuery() { /**
return jQuery; * jQuery
} */
static get jQuery() { return jQuery }
static get $() { return this.jQuery }
static get $() { /**
return this.jQuery; * Lodash
} */
static get lodash() { return lodash }
static get lodash() { static get _() { return this.lodash }
return lodash;
}
static get _() {
return this.lodash;
}
/**
* Moment
*/
static get moment() { static get moment() {
return WebpackModules.getModuleByName('Moment'); return WebpackModules.getModuleByName('Moment');
} }
/**
* Vue
*/
static get Vue() {
return Vue;
}
} }

View File

@ -51,6 +51,8 @@ const KnownModules = {
React: Filters.byProperties(['createElement', 'cloneElement']), React: Filters.byProperties(['createElement', 'cloneElement']),
ReactDOM: Filters.byProperties(['render', 'findDOMNode']), ReactDOM: Filters.byProperties(['render', 'findDOMNode']),
Events: Filters.byPrototypeFields(['setMaxListeners', 'emit']),
/* Guild Info, Stores, and Utilities */ /* Guild Info, Stores, and Utilities */
GuildStore: Filters.byProperties(['getGuild']), GuildStore: Filters.byProperties(['getGuild']),
SortedGuildStore: Filters.byProperties(['getSortedGuilds']), SortedGuildStore: Filters.byProperties(['getSortedGuilds']),
@ -89,7 +91,6 @@ const KnownModules = {
UserActivityStore: Filters.byProperties(['getActivity']), UserActivityStore: Filters.byProperties(['getActivity']),
UserNameResolver: Filters.byProperties(['getName']), UserNameResolver: Filters.byProperties(['getName']),
/* Emoji Store and Utils */ /* Emoji Store and Utils */
EmojiInfo: Filters.byProperties(['isEmojiDisabled']), EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
EmojiUtils: Filters.byProperties(['getGuildEmoji']), EmojiUtils: Filters.byProperties(['getGuildEmoji']),
@ -100,7 +101,6 @@ const KnownModules = {
InviteResolver: Filters.byProperties(['findInvite']), InviteResolver: Filters.byProperties(['findInvite']),
InviteActions: Filters.byProperties(['acceptInvite']), InviteActions: Filters.byProperties(['acceptInvite']),
/* Discord Objects & Utils */ /* Discord Objects & Utils */
DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]), DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]),
Permissions: Filters.byProperties(['getHighestRole']), Permissions: Filters.byProperties(['getHighestRole']),
@ -126,7 +126,6 @@ const KnownModules = {
ExperimentsManager: Filters.byProperties(['isDeveloper']), ExperimentsManager: Filters.byProperties(['isDeveloper']),
CurrentExperiment: Filters.byProperties(['getExperimentId']), CurrentExperiment: Filters.byProperties(['getExperimentId']),
/* Images, Avatars and Utils */ /* Images, Avatars and Utils */
ImageResolver: Filters.byProperties(["getUserAvatarURL"]), ImageResolver: Filters.byProperties(["getUserAvatarURL"]),
ImageUtils: Filters.byProperties(['getSizedImageSrc']), ImageUtils: Filters.byProperties(['getSizedImageSrc']),
@ -180,7 +179,6 @@ const KnownModules = {
URLParser: Filters.byProperties(['Url', 'parse']), URLParser: Filters.byProperties(['Url', 'parse']),
ExtraURLs: Filters.byProperties(['getArticleURL']), ExtraURLs: Filters.byProperties(['getArticleURL']),
/* DOM/React Components */ /* DOM/React Components */
/* ==================== */ /* ==================== */
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']), UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
@ -207,37 +205,37 @@ const KnownModules = {
export class WebpackModules { export class WebpackModules {
/** /**
* Finds a module using a filter function. * Finds a module using a filter function.
* @param {Function} filter A function to use to filter modules * @param {Function} filter A function to use to filter modules
* @param {Boolean} first Whether to return only the first matching module * @param {Boolean} first Whether to return only the first matching module
* @return {Any} * @return {Any}
*/ */
static getModule(filter, first = true) { static getModule(filter, first = true) {
const modules = this.getAllModules(); const modules = this.getAllModules();
const rm = []; const rm = [];
for (let index in modules) { for (let index in modules) {
if (!modules.hasOwnProperty(index)) continue; if (!modules.hasOwnProperty(index)) continue;
const module = modules[index]; const module = modules[index];
const { exports } = module; const { exports } = module;
let foundModule = null; let foundModule = null;
if (!exports) continue; if (!exports) continue;
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default; if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
if (filter(exports)) foundModule = exports; if (filter(exports)) foundModule = exports;
if (!foundModule) continue; if (!foundModule) continue;
if (first) return foundModule; if (first) return foundModule;
rm.push(foundModule); rm.push(foundModule);
} }
return first || rm.length == 0 ? undefined : rm; return first || rm.length == 0 ? undefined : rm;
} }
/** /**
* Finds a module by it's name. * Finds a module by it's name.
* @param {String} name The name of the module * @param {String} name The name of the module
* @param {Function} fallback A function to use to filter modules if not finding a known module * @param {Function} fallback A function to use to filter modules if not finding a known module
* @return {Any} * @return {Any}
*/ */
static getModuleByName(name, fallback) { static getModuleByName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name]; if (Cache.hasOwnProperty(name)) return Cache[name];
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name]; if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
@ -246,48 +244,48 @@ export class WebpackModules {
return module ? Cache[name] = module : undefined; return module ? Cache[name] = module : undefined;
} }
/** /**
* Finds a module by it's display name. * Finds a module by it's display name.
* @param {String} name The display name of the module * @param {String} name The display name of the module
* @return {Any} * @return {Any}
*/ */
static getModuleByDisplayName(name) { static getModuleByDisplayName(name) {
return this.getModule(Filters.byDisplayName(name), true); return this.getModule(Filters.byDisplayName(name), true);
} }
/** /**
* Finds a module using it's code. * Finds a module using it's code.
* @param {RegEx} regex A regular expression to use to filter modules * @param {RegEx} regex A regular expression to use to filter modules
* @param {Boolean} first Whether to return the only the first matching module * @param {Boolean} first Whether to return the only the first matching module
* @return {Any} * @return {Any}
*/ */
static getModuleByRegex(regex, first = true) { static getModuleByRegex(regex, first = true) {
return this.getModule(Filters.byCode(regex), first); return this.getModule(Filters.byCode(regex), first);
} }
/** /**
* Finds a module using properties on it's prototype. * Finds a module using properties on it's prototype.
* @param {Array} props Properties to use to filter modules * @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module * @param {Boolean} first Whether to return only the first matching module
* @return {Any} * @return {Any}
*/ */
static getModuleByPrototypes(prototypes, first = true) { static getModuleByPrototypes(prototypes, first = true) {
return this.getModule(Filters.byPrototypeFields(prototypes), first); return this.getModule(Filters.byPrototypeFields(prototypes), first);
} }
/** /**
* Finds a module using it's own properties. * Finds a module using it's own properties.
* @param {Array} props Properties to use to filter modules * @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module * @param {Boolean} first Whether to return only the first matching module
* @return {Any} * @return {Any}
*/ */
static getModuleByProps(props, first = true) { static getModuleByProps(props, first = true) {
return this.getModule(Filters.byProperties(props), first); return this.getModule(Filters.byProperties(props), first);
} }
/** /**
* Discord's __webpack_require__ function. * Discord's __webpack_require__ function.
*/ */
static get require() { static get require() {
if (this._require) return this._require; if (this._require) return this._require;
const id = 'bd-webpackmodules'; const id = 'bd-webpackmodules';

View File

@ -1,3 +0,0 @@
export default class {
}

View File

@ -17,22 +17,37 @@ export default class ErrorEvent extends Event {
this.showStack = false; // For error modal this.showStack = false; // For error modal
} }
/**
* The module the error occured in.
*/
get module() { get module() {
return this.args.module; return this.args.module;
} }
/**
* A message describing the error.
*/
get message() { get message() {
return this.args.message; return this.args.message;
} }
/**
* The original error object.
*/
get err() { get err() {
return this.args.err; return this.args.err;
} }
/**
* A trace showing which functions were called when the error occured.
*/
get stackTrace() { get stackTrace() {
return this.err.stack; return this.err.stack;
} }
/**
* The type of event.
*/
get __eventType() { get __eventType() {
return 'error'; return 'error';
} }

View File

@ -17,14 +17,23 @@ export default class Event {
}; };
} }
/**
* An object containing information about the event.
*/
get event() { get event() {
return this.__eventInfo; return this.__eventInfo;
} }
/**
* The first argument that was passed to the constructor, which contains information about the event.
*/
get args() { get args() {
return this.event.args[0]; return this.event.args[0];
} }
get __eventType() { return null; } /**
* The type of event.
*/
get __eventType() { return undefined; }
} }

View File

@ -12,10 +12,16 @@ import Event from './event';
export default class SettingsUpdatedEvent extends Event { export default class SettingsUpdatedEvent extends Event {
/**
* An array of SettingUpdated events.
*/
get updatedSettings() { get updatedSettings() {
return this.args.updatedSettings; return this.args.updatedSettings;
} }
/**
* The type of event.
*/
get __eventType() { get __eventType() {
return 'settings-updated'; return 'settings-updated';
} }

View File

@ -12,38 +12,65 @@ import Event from './event';
export default class SettingUpdatedEvent extends Event { export default class SettingUpdatedEvent extends Event {
/**
* The set containing the setting that was updated.
*/
get set() { get set() {
return this.args.set; return this.args.set;
} }
/**
* The ID of the set containing the setting that was updated.
*/
get set_id() { get set_id() {
return this.args.set.id; return this.set.id;
} }
/**
* The category containing the setting that was updated.
*/
get category() { get category() {
return this.args.category; return this.args.category;
} }
/**
* The ID of the category containing the setting that was updated.
*/
get category_id() { get category_id() {
return this.args.category.id; return this.category.id;
} }
/**
* The setting that was updated.
*/
get setting() { get setting() {
return this.args.setting; return this.args.setting;
} }
/**
* The ID of the setting that was updated.
*/
get setting_id() { get setting_id() {
return this.args.setting.id; return this.setting.id;
} }
/**
* The setting's new value.
*/
get value() { get value() {
return this.args.value; return this.args.value;
} }
/**
* The setting's old value.
*/
get old_value() { get old_value() {
return this.args.old_value; return this.args.old_value;
} }
/**
* The type of event.
*/
get __eventType() { get __eventType() {
return 'setting-updated'; return 'setting-updated';
} }

View File

@ -2,3 +2,4 @@ export { default as SettingsSet } from './settingsset';
export { default as SettingsCategory } from './settingscategory'; export { default as SettingsCategory } from './settingscategory';
export { default as Setting } from './setting'; export { default as Setting } from './setting';
export { default as SettingsScheme } from './settingsscheme'; export { default as SettingsScheme } from './settingsscheme';
export * from './types';

View File

@ -14,22 +14,29 @@ export default class MultipleChoiceOption {
constructor(args) { constructor(args) {
this.args = args.args || args; this.args = args.args || args;
Object.freeze(this);
} }
/**
* This option's ID.
*/
get id() { get id() {
return this.args.id || this.value; return this.args.id || this.value;
} }
/**
* A string describing this option.
*/
get text() { get text() {
return this.args.text; return this.args.text;
} }
/**
* The value to return when this option is active.
*/
get value() { get value() {
return this.args.value; return this.args.value;
} }
clone() {
return new MultipleChoiceOption(Utils.deepclone(this.args));
}
} }

View File

@ -15,35 +15,49 @@ export default class SettingsScheme {
constructor(args) { constructor(args) {
this.args = args.args || 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); Object.freeze(this);
} }
/**
* The scheme's ID.
*/
get id() { get id() {
return this.args.id; return this.args.id;
} }
/**
* The URL of the scheme's icon. This should be a base64 encoded data URI.
*/
get icon_url() { get icon_url() {
return this.args.icon_url; return this.args.icon_url;
} }
/**
* The scheme's name.
*/
get name() { get name() {
return this.args.name; return this.args.name;
} }
/**
* A string to be displayed under the scheme.
*/
get hint() { get hint() {
return this.args.hint; return this.args.hint;
} }
/**
* An array of stripped settings categories this scheme manages.
*/
get settings() { get settings() {
return this.args.settings || []; return this.args.settings || [];
} }
/**
* Checks if this scheme's values are currently applied to a set.
* @param {SettingsSet} set The set to check
* @return {Boolean}
*/
isActive(set) { isActive(set) {
for (let schemeCategory of this.settings) { for (let schemeCategory of this.settings) {
const category = set.categories.find(c => c.category === schemeCategory.category); const category = set.categories.find(c => c.category === schemeCategory.category);
@ -66,12 +80,13 @@ export default class SettingsScheme {
return true; return true;
} }
/**
* Applies this scheme's values to a set.
* @param {SettingsSet} set The set to merge this scheme's values into
* @return {Promise}
*/
applyTo(set) { applyTo(set) {
return set.merge({ settings: this.settings }); return set.merge(this);
}
clone() {
return new SettingsScheme(Utils.deepclone(this.args));
} }
} }

View File

@ -446,7 +446,7 @@ export default class SettingsSet {
text: this.text, text: this.text,
headertext: this.headertext, headertext: this.headertext,
settings: this.categories.map(category => category.clone()), settings: this.categories.map(category => category.clone()),
schemes: this.schemes.map(scheme => scheme.clone()) schemes: this.schemes
}, ...merge); }, ...merge);
} }

View File

@ -0,0 +1,12 @@
export { default as BoolSetting } from './bool';
export { default as StringSetting } from './text';
export { default as NumberSetting } from './number';
export { default as DropdownSetting } from './dropdown';
export { default as RadioSetting } from './radio';
export { default as SliderSetting } from './slider';
export { default as ColourSetting } from './colour';
export { default as KeybindSetting } from './keybind';
export { default as FileSetting } from './file';
export { default as GuildSetting } from './guild';
export { default as ArraySetting } from './array';
export { default as CustomSetting } from './custom';

View File

@ -11,18 +11,42 @@
import Setting from './basesetting'; import Setting from './basesetting';
import Combokeys from 'combokeys'; import Combokeys from 'combokeys';
let keybindsPaused = false;
export default class KeybindSetting extends Setting { export default class KeybindSetting extends Setting {
constructor(args, ...merge) { constructor(args, ...merge) {
super(args, ...merge); super(args, ...merge);
this.__keybind_activated = this.__keybind_activated.bind(this);
this.combokeys = new Combokeys(document); this.combokeys = new Combokeys(document);
this.combokeys.bind(this.value, event => this.emit('keybind-activated', event)); this.combokeys.bind(this.value, this.__keybind_activated);
}
/**
* The value to use when the setting doesn't have a value.
*/
get defaultValue() {
return '';
} }
setValueHook() { setValueHook() {
this.combokeys.reset(); this.combokeys.reset();
this.combokeys.bind(this.value, event => this.emit('keybind-activated', event)); this.combokeys.bind(this.value, this.__keybind_activated);
}
__keybind_activated(event) {
if (KeybindSetting.paused) return;
this.emit('keybind-activated', event);
}
static get paused() {
return keybindsPaused;
}
static set paused(paused) {
keybindsPaused = paused;
} }
} }

View File

@ -25,16 +25,21 @@
width: 16px; width: 16px;
filter: brightness(10); filter: brightness(10);
cursor: pointer; cursor: pointer;
.theme-light [class*="topSectionNormal-"] & { }
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjYmJiYmJiIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg==');
filter: none; .theme-light [class*="topSectionNormal-"] .bd-profile-badge-developer,
} .theme-light [class*="topSectionNormal-"] .bd-profile-badge-contributor,
.theme-light .bd-message-badge-developer,
.theme-light .bd-message-badge-contributor {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjYmJiYmJiIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg==');
filter: none;
} }
.bd-message-badges-wrap { .bd-message-badges-wrap {
display: inline-block; display: inline-block;
margin-left: 6px; margin-left: 6px;
height: 11px; height: 11px;
.bd-message-badge-developer, .bd-message-badge-developer,
.bd-message-badge-contributor { .bd-message-badge-contributor {
width: 12px; width: 12px;

View File

@ -52,12 +52,12 @@
.bd-settings-button-btn { .bd-settings-button-btn {
background-image: $logoBigBw; background-image: $logoBigBw;
background-size: 100% 100%;
filter: none; filter: none;
opacity: 1; opacity: 1;
width: 130px; width: 130px;
height: 80px; height: 43px;
background-size: 100% 100%; margin: 18px 0 17px 20px;
margin-left: 20px;
cursor: default; cursor: default;
} }
} }

View File

@ -4,3 +4,4 @@
@import './card.scss'; @import './card.scss';
@import './tooltips.scss'; @import './tooltips.scss';
@import './settings-schemes.scss'; @import './settings-schemes.scss';
@import './updater.scss';

View File

@ -19,18 +19,53 @@ bd-tooltips {
position: absolute; position: absolute;
word-wrap: break-word; word-wrap: break-word;
z-index: 9001; z-index: 9001;
margin-bottom: 10px;
}
.bd-tooltip:after { .bd-tooltip-arrow {
border: 5px solid transparent; border: 5px solid transparent;
content: " "; content: " ";
height: 0; height: 0;
pointer-events: none; pointer-events: none;
width: 0; width: 0;
border-top-color: #000; position: absolute;
left: 50%; }
margin-left: -5px;
position: absolute; &[x-placement^="top"] {
top: 100%; margin-bottom: 10px;
.bd-tooltip-arrow {
margin-left: -5px;
border-top-color: #000;
bottom: -10px;
}
}
&[x-placement^="bottom"] {
margin-top: 10px;
.bd-tooltip-arrow {
border-width: 0 5px 5px 5px;
top: -5px;
border-bottom-color: #000;
}
}
&[x-placement^="right"] {
margin-left: 10px;
.bd-tooltip-arrow {
border-width: 5px 5px 5px 0;
left: -5px;
border-right-color: #000;
}
}
&[x-placement^="left"] {
margin-right: 10px;
.bd-tooltip-arrow {
border-width: 5px 0 5px 5px;
right: -5px;
border-left-color: #000;
}
}
} }

View File

@ -0,0 +1,6 @@
.bd-updaterview {
p {
margin: 0 0 10px;
color: #ffffff;
}
}

View File

@ -8,37 +8,14 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import { Events, WebpackModules, EventListener, ReactComponents, Renderer } from 'modules'; import { Events, WebpackModules, EventListener, DiscordApi, ReactComponents, Renderer } from 'modules';
import { ClientLogger as Logger } from 'common';
import Reflection from './reflection'; import Reflection from './reflection';
import DOM from './dom'; import DOM from './dom';
import VueInjector from './vueinjector'; import VueInjector from './vueinjector';
import EditedTimeStamp from './components/common/EditedTimestamp.vue'; import EditedTimeStamp from './components/common/EditedTimestamp.vue';
import Autocomplete from './components/common/Autocomplete.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 { export default class extends EventListener {
constructor(args) { constructor(args) {
@ -54,22 +31,19 @@ export default class extends EventListener {
} }
get eventBindings() { get eventBindings() {
return [{ id: 'gkh:keyup', callback: this.injectAutocomplete }];
/*
return [ return [
{ id: 'server-switch', callback: this.manipAll }, // { id: 'server-switch', callback: this.manipAll },
{ id: 'channel-switch', callback: this.manipAll }, // { id: 'channel-switch', callback: this.manipAll },
{ id: 'discord:MESSAGE_CREATE', callback: this.markupInjector }, // { id: 'discord:MESSAGE_CREATE', callback: this.markupInjector },
{ id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector }, // { id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector },
{ id: 'gkh:keyup', callback: this.injectAutocomplete } { id: 'gkh:keyup', callback: this.injectAutocomplete }
]; ];
*/
} }
manipAll() { manipAll() {
try { try {
this.appMount.setAttribute('guild-id', TempApi.currentGuildId); this.appMount.setAttribute('guild-id', DiscordApi.currentGuild.id);
this.appMount.setAttribute('channel-id', TempApi.currentChannelId); this.appMount.setAttribute('channel-id', DiscordApi.currentChannel.id);
this.setIds(); this.setIds();
this.makeMutable(); this.makeMutable();
} catch (err) { } catch (err) {
@ -128,13 +102,11 @@ export default class extends EventListener {
if (markup.ets) { if (markup.ets) {
const etsRoot = document.createElement('span'); const etsRoot = document.createElement('span');
markup.clone.appendChild(etsRoot); markup.clone.appendChild(etsRoot);
VueInjector.inject( VueInjector.inject(etsRoot, {
etsRoot, components: { EditedTimeStamp },
DOM.createElement('span', null, 'test'), data: { ets: markup.ets },
{ EditedTimeStamp }, template: '<EditedTimeStamp :ets="ets" />'
`<EditedTimeStamp ets="${markup.ets}"/>`, });
true
);
} }
Events.emit('ui:mutable:.markup', markup.clone); Events.emit('ui:mutable:.markup', markup.clone);
@ -174,14 +146,14 @@ export default class extends EventListener {
const userTest = Reflection(msgGroup).prop('user'); const userTest = Reflection(msgGroup).prop('user');
if (!userTest) return; if (!userTest) return;
msgGroup.setAttribute('data-author-id', userTest.id); msgGroup.setAttribute('data-author-id', userTest.id);
if (userTest.id === TempApi.currentUserId) msgGroup.setAttribute('data-currentuser', true); if (userTest.id === DiscordApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
return; return;
} }
msg.setAttribute('data-message-id', messageid); msg.setAttribute('data-message-id', messageid);
const msgGroup = msg.closest('.message-group'); const msgGroup = msg.closest('.message-group');
if (!msgGroup) return; if (!msgGroup) return;
msgGroup.setAttribute('data-author-id', authorid); msgGroup.setAttribute('data-author-id', authorid);
if (authorid === TempApi.currentUserId) msgGroup.setAttribute('data-currentuser', true); if (authorid === DiscordApi.currentUser.id) msgGroup.setAttribute('data-currentuser', true);
} }
setUserId(user) { setUserId(user) {
@ -189,7 +161,7 @@ export default class extends EventListener {
const userid = Reflection(user).prop('user.id'); const userid = Reflection(user).prop('user.id');
if (!userid) return; if (!userid) return;
user.setAttribute('data-user-id', userid); user.setAttribute('data-user-id', userid);
const currentUser = userid === TempApi.currentUserId; const currentUser = userid === DiscordApi.currentUser.id;
if (currentUser) user.setAttribute('data-currentuser', true); if (currentUser) user.setAttribute('data-currentuser', true);
Events.emit('ui:useridset', user); Events.emit('ui:useridset', user);
} }
@ -214,12 +186,11 @@ export default class extends EventListener {
const parent = document.querySelector('[class*="channelTextArea"] > [class*="inner"]'); const parent = document.querySelector('[class*="channelTextArea"] > [class*="inner"]');
if (!parent) return; if (!parent) return;
parent.append(root); parent.append(root);
VueInjector.inject( VueInjector.inject(root, {
root, components: { Autocomplete },
DOM.createElement('span'), data: { initial: e.target.value },
{ Autocomplete }, template: '<Autocomplete :initial="initial" />'
`<Autocomplete initial="${e.target.value}"/>`, });
true
);
} }
} }

View File

@ -8,17 +8,22 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import { Events } from 'modules';
import { Utils } from 'common'; import { Utils } from 'common';
let items = 0; let items = 0;
const BdMenuItems = new class { export const BdMenuItems = new class {
constructor() { constructor() {
window.bdmenu = this; window.bdmenu = this;
this.items = []; this.items = [];
const updater = this.add({category: 'Updates', contentid: 'updater', text: 'Updates available!', hidden: true});
Events.on('update-check-end', () => updater.hidden = true);
Events.on('updates-available', () => updater.hidden = false);
this.addSettingsSet('Internal', 'core', 'Core'); this.addSettingsSet('Internal', 'core', 'Core');
this.addSettingsSet('Internal', 'ui', 'UI'); this.addSettingsSet('Internal', 'ui', 'UI');
this.addSettingsSet('Internal', 'emotes', 'Emotes'); this.addSettingsSet('Internal', 'emotes', 'Emotes');
@ -28,7 +33,14 @@ const BdMenuItems = new class {
this.add({category: 'External', contentid: 'themes', text: 'Themes'}); this.add({category: 'External', contentid: 'themes', text: 'Themes'});
} }
/**
* Adds an item to the menu.
* @param {Object} item The item to add to the menu
* @return {Object}
*/
add(item) { add(item) {
if (this.items.includes(item)) return item;
item.id = items++; item.id = items++;
item.contentid = item.contentid || (items++ + ''); item.contentid = item.contentid || (items++ + '');
item.active = false; item.active = false;
@ -39,6 +51,13 @@ const BdMenuItems = new class {
return item; return item;
} }
/**
* Adds a settings set to the menu.
* @param {String} category The category to display this item under
* @param {SettingsSet} set The settings set to display when this item is active
* @param {String} text The text to display in the menu (optional)
* @return {Object} The item that was added
*/
addSettingsSet(category, set, text) { addSettingsSet(category, set, text) {
return this.add({ return this.add({
category, set, category, set,
@ -46,16 +65,25 @@ const BdMenuItems = new class {
}); });
} }
/**
* Adds a Vue component to the menu.
* @param {String} category The category to display this item under
* @param {String} text The text to display in the menu
* @param {Object} component The Vue component to display when this item is active
* @return {Object} The item that was added
*/
addVueComponent(category, text, component) { addVueComponent(category, text, component) {
return this.add({ return this.add({
category, text, component category, text, component
}); });
} }
/**
* Removes an item from the menu.
* @param {Object} item The item to remove from the menu
*/
remove(item) { remove(item) {
Utils.removeFromArray(this.items, item); Utils.removeFromArray(this.items, item);
} }
}; };
export { BdMenuItems };

View File

@ -8,48 +8,21 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import { Events, WebpackModules, DiscordApi } from 'modules';
import { Utils } from 'common';
import { remote } from 'electron';
import DOM from './dom'; import DOM from './dom';
import Vue from './vue'; import Vue from './vue';
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 AutoManip from './automanip';
import { remote } from 'electron'; import { BdSettingsWrapper, BdModals } from './components';
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 { export default class {
static initUiEvents() { static initUiEvents() {
this.pathCache = { this.pathCache = {
isDm: null, isDm: null,
server: TempApi.currentGuild, server: DiscordApi.currentGuild,
channel: TempApi.currentChannel channel: DiscordApi.currentChannel
}; };
window.addEventListener('keyup', e => Events.emit('gkh:keyup', e)); window.addEventListener('keyup', e => Events.emit('gkh:keyup', e));
this.autoManip = new AutoManip(); this.autoManip = new AutoManip();
@ -67,9 +40,9 @@ export default class {
if (!remote.BrowserWindow.getFocusedWindow()) return; if (!remote.BrowserWindow.getFocusedWindow()) return;
clearInterval(ehookInterval); clearInterval(ehookInterval);
remote.BrowserWindow.getFocusedWindow().webContents.on('did-navigate-in-page', (e, url, isMainFrame) => { remote.BrowserWindow.getFocusedWindow().webContents.on('did-navigate-in-page', (e, url, isMainFrame) => {
const { currentGuild, currentChannel } = TempApi; const { currentGuild, currentChannel } = DiscordApi;
if (!this.pathCache.server) { if (!this.pathCache.server) {
Events.emit('server-switch', { 'server': currentGuild, 'channel': currentChannel }); Events.emit('server-switch', { server: currentGuild, channel: currentChannel });
this.pathCache.server = currentGuild; this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel; this.pathCache.channel = currentChannel;
return; return;
@ -84,7 +57,7 @@ export default class {
currentGuild.id && currentGuild.id &&
this.pathCache.server && this.pathCache.server &&
this.pathCache.server.id !== currentGuild.id) { this.pathCache.server.id !== currentGuild.id) {
Events.emit('server-switch', { 'server': currentGuild, 'channel': currentChannel }); Events.emit('server-switch', { server: currentGuild, channel: currentChannel });
this.pathCache.server = currentGuild; this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel; this.pathCache.channel = currentChannel;
return; return;
@ -110,19 +83,19 @@ export default class {
DOM.createElement('div', null, 'bd-modals').appendTo(DOM.bdModals); DOM.createElement('div', null, 'bd-modals').appendTo(DOM.bdModals);
DOM.createElement('bd-tooltips').appendTo(DOM.bdBody); DOM.createElement('bd-tooltips').appendTo(DOM.bdBody);
const modals = new Vue({ this.modals = new Vue({
el: '#bd-modals', el: '#bd-modals',
components: { BdModals }, components: { BdModals },
template: '<BdModals/>' template: '<BdModals />'
}); });
const vueInstance = new Vue({ this.vueInstance = new Vue({
el: '#bd-settings', el: '#bd-settings',
components: { BdSettingsWrapper }, components: { BdSettingsWrapper },
template: '<BdSettingsWrapper/>' template: '<BdSettingsWrapper />'
}); });
return vueInstance; return this.vueInstance;
} }
} }

View File

@ -19,13 +19,14 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script>
// Imports // Imports
import { Events } from 'modules'; import { Events } from 'modules';
import { Modals } from 'ui'; import { Modals } from 'ui';
import { Modal } from '../common'; import { Modal } from './common';
import { MiError } from '../common/MaterialIcon'; import { MiError } from './common/MaterialIcon';
import ErrorModal from './modals/ErrorModal.vue'; import ErrorModal from './bd/modals/ErrorModal.vue';
export default { export default {
components: { components: {

View File

@ -49,6 +49,7 @@
<CssEditorView v-if="item.contentid === 'css'" /> <CssEditorView v-if="item.contentid === 'css'" />
<PluginsView v-if="item.contentid === 'plugins'" /> <PluginsView v-if="item.contentid === 'plugins'" />
<ThemesView v-if="item.contentid === 'themes'" /> <ThemesView v-if="item.contentid === 'themes'" />
<UpdaterView v-if="item.contentid === 'updater'" />
</div> </div>
</ContentColumn> </ContentColumn>
</SidebarView> </SidebarView>
@ -60,7 +61,7 @@
import { Settings } from 'modules'; import { Settings } from 'modules';
import { BdMenuItems } from 'ui'; import { BdMenuItems } from 'ui';
import { SidebarView, Sidebar, SidebarItem, ContentColumn } from './sidebar'; import { SidebarView, Sidebar, SidebarItem, ContentColumn } from './sidebar';
import { SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView } from './bd'; import { SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView, UpdaterView } from './bd';
import { SvgX, MiGithubCircle, MiWeb, MiClose, MiTwitterCircle } from './common'; import { SvgX, MiGithubCircle, MiWeb, MiClose, MiTwitterCircle } from './common';
export default { export default {
@ -79,7 +80,7 @@
props: ['active', 'close'], props: ['active', 'close'],
components: { components: {
SidebarView, Sidebar, SidebarItem, ContentColumn, SidebarView, Sidebar, SidebarItem, ContentColumn,
SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView, SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView, UpdaterView,
MiGithubCircle, MiWeb, MiClose, MiTwitterCircle MiGithubCircle, MiWeb, MiClose, MiTwitterCircle
}, },
computed: { computed: {
@ -131,7 +132,7 @@
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
this.animating = false; this.animating = false;
this.lastActiveIndex = -1; this.lastActiveIndex = -1;
this.timeout = null; this.timeout = null;
}, 400); }, 400);
}, },
openGithub() { openGithub() {

View File

@ -9,11 +9,11 @@
*/ */
<template> <template>
<div :class="{'bd-profile-badges-wrap': hasBadges == 'false'}"> <div :class="{'bd-profile-badges-wrap': !hasBadges}">
<div class="bd-profile-badges"> <div class="bd-profile-badges">
<div v-if="developer == 'true'" v-tooltip="'BetterDiscord Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="onClick"></div> <div v-if="developer" v-tooltip="'BetterDiscord Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="onClick"></div>
<div v-else-if="webdev == 'true'" v-tooltip="'BetterDiscord Web Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="onClick"></div> <div v-else-if="webdev" v-tooltip="'BetterDiscord Web Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="onClick"></div>
<div v-else-if="contributor == 'true'" v-tooltip="'BetterDiscord Contributor'" class="bd-profile-badge bd-profile-badge-contributor" @click="onClick"></div> <div v-else-if="contributor" v-tooltip="'BetterDiscord Contributor'" class="bd-profile-badge bd-profile-badge-contributor" @click="onClick"></div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -10,9 +10,9 @@
<template> <template>
<div class="bd-message-badges-wrap"> <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-if="developer" 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="webdev" 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 v-else-if="contributor" v-tooltip="'BetterDiscord Contributor'" class="bd-message-badge bd-message-badge-contributor" @click="onClick"></div>
</div> </div>
</template> </template>
<script> <script>

View File

@ -11,9 +11,9 @@
<template> <template>
<SettingsWrapper headertext="CSS Editor"> <SettingsWrapper headertext="CSS Editor">
<div class="bd-css-editor"> <div class="bd-css-editor">
<div v-if="CssEditor.error" class="bd-form-item"> <div v-if="error" class="bd-form-item">
<h5 style="margin-bottom: 10px;">Compiler error</h5> <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-err bd-pre-wrap"><div class="bd-pre">{{ error.formatted }}</div></div>
<div class="bd-form-divider"></div> <div class="bd-form-divider"></div>
</div> </div>

View File

@ -21,16 +21,12 @@
</template> </template>
<script> <script>
// Imports // Imports
import { ClientLogger as Logger } from 'common';
import { shell } from 'electron'; import { shell } from 'electron';
import Card from './Card.vue'; import Card from './Card.vue';
import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension } from '../common'; import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension } from '../common';
export default { export default {
data() {
return {
settingsOpen: false
}
},
props: ['plugin', 'togglePlugin', 'reloadPlugin', 'deletePlugin', 'showSettings'], props: ['plugin', 'togglePlugin', 'reloadPlugin', 'deletePlugin', 'showSettings'],
components: { components: {
Card, Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension Card, Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension
@ -38,9 +34,9 @@
methods: { methods: {
editPlugin() { editPlugin() {
try { try {
shell.openItem(this.plugin.pluginPath); shell.openItem(this.plugin.contentPath);
} catch (err) { } catch (err) {
console.log(err); Logger.err('PluginCard', [`Error opening plugin directory ${this.plugin.contentPath}:`, err]);
} }
} }
} }

View File

@ -37,17 +37,19 @@
// Imports // Imports
import { PluginManager } from 'modules'; import { PluginManager } from 'modules';
import { Modals } from 'ui'; import { Modals } from 'ui';
import { SettingsWrapper } from './'; import { ClientLogger as Logger } from 'common';
import PluginCard from './PluginCard.vue';
import { MiRefresh } from '../common'; import { MiRefresh } from '../common';
import SettingsWrapper from './SettingsWrapper.vue';
import PluginCard from './PluginCard.vue';
import RefreshBtn from '../common/RefreshBtn.vue'; import RefreshBtn from '../common/RefreshBtn.vue';
export default { export default {
data() { data() {
return { return {
PluginManager,
local: true, local: true,
localPlugins: PluginManager.localPlugins localPlugins: PluginManager.localPlugins
} };
}, },
components: { components: {
SettingsWrapper, PluginCard, SettingsWrapper, PluginCard,
@ -62,32 +64,32 @@
this.local = false; this.local = false;
}, },
async refreshLocal() { async refreshLocal() {
await PluginManager.refreshPlugins(); await this.PluginManager.refreshPlugins();
}, },
async refreshOnline() { async refreshOnline() {
// TODO
}, },
async togglePlugin(plugin) { async togglePlugin(plugin) {
// TODO Display error if plugin fails to start/stop // TODO: display error if plugin fails to start/stop
const enabled = plugin.enabled;
try { try {
await plugin.enabled ? PluginManager.stopPlugin(plugin) : PluginManager.startPlugin(plugin); await enabled ? this.PluginManager.stopPlugin(plugin) : this.PluginManager.startPlugin(plugin);
} catch (err) { } catch (err) {
console.log(err); Logger.err('PluginsView', [`Error ${enabled ? 'stopp' : 'start'}ing plugin ${plugin.name}:`, err]);
} }
}, },
async reloadPlugin(plugin) { async reloadPlugin(plugin) {
try { try {
await PluginManager.reloadPlugin(plugin); await this.PluginManager.reloadPlugin(plugin);
} catch (err) { } catch (err) {
console.log(err); Logger.err('PluginsView', [`Error reloading plugin ${plugin.name}:`, err]);
} }
}, },
async deletePlugin(plugin, unload) { async deletePlugin(plugin, unload) {
try { try {
if (unload) await PluginManager.unloadPlugin(plugin); await unload ? this.PluginManager.unloadPlugin(plugin) : this.PluginManager.deletePlugin(plugin);
else await PluginManager.deletePlugin(plugin);
} catch (err) { } catch (err) {
console.error(err); Logger.err('PluginsView', [`Error ${unload ? 'unload' : 'delet'}ing plugin ${plugin.name}:`, err]);
} }
}, },
showSettings(plugin, dont_clone) { showSettings(plugin, dont_clone) {

View File

@ -26,11 +26,6 @@
import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension } from '../common'; import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension } from '../common';
export default { export default {
data() {
return {
settingsOpen: false
}
},
props: ['theme', 'toggleTheme', 'reloadTheme', 'deleteTheme', 'showSettings'], props: ['theme', 'toggleTheme', 'reloadTheme', 'deleteTheme', 'showSettings'],
components: { components: {
Card, Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension Card, Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension
@ -40,7 +35,7 @@
try { try {
shell.openItem(this.theme.themePath); shell.openItem(this.theme.themePath);
} catch (err) { } catch (err) {
console.log(err); Logger.err('ThemeCard', [`Error opening theme directory ${this.theme.contentPath}:`, err]);
} }
} }
} }

View File

@ -37,17 +37,19 @@
// Imports // Imports
import { ThemeManager } from 'modules'; import { ThemeManager } from 'modules';
import { Modals } from 'ui'; import { Modals } from 'ui';
import { SettingsWrapper } from './'; import { ClientLogger as Logger } from 'common';
import { MiRefresh } from '../common'; import { MiRefresh } from '../common';
import SettingsWrapper from './SettingsWrapper.vue';
import ThemeCard from './ThemeCard.vue'; import ThemeCard from './ThemeCard.vue';
import RefreshBtn from '../common/RefreshBtn.vue'; import RefreshBtn from '../common/RefreshBtn.vue';
export default { export default {
data() { data() {
return { return {
ThemeManager,
local: true, local: true,
localThemes: ThemeManager.localThemes localThemes: ThemeManager.localThemes
} };
}, },
components: { components: {
SettingsWrapper, ThemeCard, SettingsWrapper, ThemeCard,
@ -62,33 +64,31 @@
this.local = false; this.local = false;
}, },
async refreshLocal() { async refreshLocal() {
await ThemeManager.refreshThemes(); await this.ThemeManager.refreshThemes();
}, },
async refreshOnline() { async refreshOnline() {
// TODO
}, },
async toggleTheme(theme) { async toggleTheme(theme) {
// TODO Display error if theme fails to enable/disable // TODO: display error if theme fails to enable/disable
try { try {
await theme.enabled ? ThemeManager.disableTheme(theme) : ThemeManager.enableTheme(theme); await theme.enabled ? this.ThemeManager.disableTheme(theme) : this.ThemeManager.enableTheme(theme);
} catch (err) { } catch (err) {
console.log(err); Logger.err('ThemesView', [`Error ${enabled ? 'stopp' : 'start'}ing theme ${theme.name}:`, err]);
} }
}, },
async reloadTheme(theme, reload) { async reloadTheme(theme, reload) {
try { try {
if (reload) await ThemeManager.reloadTheme(theme); await reload ? this.ThemeManager.reloadTheme(theme) : theme.recompile();
else await theme.recompile();
} catch (err) { } catch (err) {
console.log(err); Logger.err('ThemesView', [`Error ${reload ? 'reload' : 'recompil'}ing theme ${theme.name}:`, err]);
} }
}, },
async deleteTheme(theme, unload) { async deleteTheme(theme, unload) {
try { try {
if (unload) await ThemeManager.unloadTheme(theme); await unload ? this.ThemeManager.unloadTheme(theme) : this.ThemeManager.deleteTheme(theme);
else await ThemeManager.deleteTheme(theme);
} catch (err) { } catch (err) {
console.error(err); Logger.err('ThemesView', [`Error ${unload ? 'unload' : 'delet'}ing theme ${theme.name}:`, err]);
} }
}, },
showSettings(theme, dont_clone) { showSettings(theme, dont_clone) {

View File

@ -0,0 +1,70 @@
/**
* BetterDiscord Updater 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="Updates">
<div class="bd-flex bd-flex-col bd-updaterview">
<div v-if="error" class="bd-form-item">
<h5 style="margin-bottom: 10px;">Error installing updates</h5>
<div class="bd-err bd-pre-wrap"><div class="bd-pre">{{ error.formatted }}</div></div>
<div class="bd-form-divider"></div>
</div>
<template v-if="updatesAvailable">
<p>Version {{ newVersion }} is available. You are currently running version {{ currentVersion }}.</p>
<FormButton :onClick="install" :loading="updating">Install</FormButton>
</template>
<template v-else>
<p>You're all up to date!</p>
</template>
</div>
</SettingsWrapper>
</template>
<script>
import { Globals, Updater } from 'modules';
import { ClientLogger as Logger } from 'common';
import SettingsWrapper from './SettingsWrapper.vue';
import { FormButton } from '../common';
export default {
data() {
return {
currentVersion: Globals.version,
updating: false,
updater: Updater
};
},
components: {
SettingsWrapper,
FormButton
},
computed: {
updatesAvailable() {
return this.updater.updatesAvailable;
},
newVersion() {
return this.updater.latestVersion;
},
error() {
return this.updater.error;
}
},
methods: {
async install() {
this.updating = true;
try {
await this.updater.update();
} catch (err) {}
this.updating = false;
}
}
}
</script>

View File

@ -3,5 +3,6 @@ export { default as SettingsPanel } from './SettingsPanel.vue';
export { default as CssEditorView } from './CssEditor.vue'; export { default as CssEditorView } from './CssEditor.vue';
export { default as PluginsView } from './PluginsView.vue'; export { default as PluginsView } from './PluginsView.vue';
export { default as ThemesView } from './ThemesView.vue'; export { default as ThemesView } from './ThemesView.vue';
export { default as UpdaterView } from './UpdaterView.vue';
export { default as BdBadge } from './BdBadge.vue'; export { default as BdBadge } from './BdBadge.vue';
export { default as BdMessageBadge } from './BdMessageBadge.vue'; export { default as BdMessageBadge } from './BdMessageBadge.vue';

View File

@ -24,8 +24,9 @@
</template> </template>
<script> <script>
import { KeybindSetting } from 'structs';
import { ClientIPC, ClientLogger as Logger } from 'common';
import { shell } from 'electron'; import { shell } from 'electron';
import { ClientIPC } from 'common';
import Combokeys from 'combokeys'; import Combokeys from 'combokeys';
import CombokeysRecord from 'combokeys/plugins/record'; import CombokeysRecord from 'combokeys/plugins/record';
@ -49,6 +50,7 @@
}, },
watch: { watch: {
active(active) { active(active) {
KeybindSetting.paused = active;
if (active) combokeys.record(this.recorded); if (active) combokeys.record(this.recorded);
} }
}, },
@ -65,7 +67,7 @@
this.active = false; this.active = false;
this.recordingValue = undefined; this.recordingValue = undefined;
this.setting.value = sequence.join(' '); this.setting.value = sequence.join(' ');
console.log('keypress', sequence); Logger.log('Keybind', ['Recorded sequence', sequence]);
}, },
getDisplayString(value) { getDisplayString(value) {
if (!value) return; if (!value) return;

View File

@ -34,6 +34,7 @@
import { EmoteModule } from 'builtin'; import { EmoteModule } from 'builtin';
import { Events } from 'modules'; import { Events } from 'modules';
import { DOM } from 'ui'; import { DOM } from 'ui';
export default { export default {
data() { data() {
return { return {
@ -44,7 +45,7 @@
open: false, open: false,
selectedIndex: 0, selectedIndex: 0,
sterm: '' sterm: ''
} };
}, },
props: ['initial'], props: ['initial'],
beforeMount() { beforeMount() {

View File

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

View File

@ -186,4 +186,5 @@ export default class DOM {
node.setAttribute(attribute.name, attribute.value); node.setAttribute(attribute.name, attribute.value);
} }
} }
} }

View File

@ -209,7 +209,7 @@ export default class Modals {
ThemeManager._errors = []; ThemeManager._errors = [];
} }
return modal; return modal;
} }
} }

View File

@ -12,6 +12,7 @@ import { EventListener } from 'modules';
import DOM from './dom'; import DOM from './dom';
import { BdBadge, BdMessageBadge } from './components/bd'; import { BdBadge, BdMessageBadge } from './components/bd';
import VueInjector from './vueinjector'; import VueInjector from './vueinjector';
import contributors from '../data/contributors';
export default class extends EventListener { export default class extends EventListener {
@ -55,43 +56,39 @@ export default class extends EventListener {
if (msgGroup.dataset.hasBadges) return; if (msgGroup.dataset.hasBadges) return;
msgGroup.setAttribute('data-has-badges', true); msgGroup.setAttribute('data-has-badges', true);
if (!msgGroup.dataset.authorId) return; if (!msgGroup.dataset.authorId) return;
const c = this.contributors.find(c => c.id === msgGroup.dataset.authorId); const c = contributors.find(c => c.id === msgGroup.dataset.authorId);
if (!c) return; if (!c) return;
const root = document.createElement('span'); const root = document.createElement('span');
const wrapperParent = msgGroup.querySelector('.username-wrapper').parentElement; const usernameWrapper = msgGroup.querySelector('.username-wrapper');
if (!usernameWrapper) return;
const wrapperParent = usernameWrapper.parentElement;
if (!wrapperParent || wrapperParent.children.length < 2) return; if (!wrapperParent || wrapperParent.children.length < 2) return;
wrapperParent.insertBefore(root, wrapperParent.children[1]); wrapperParent.insertBefore(root, wrapperParent.children[1]);
const { developer, contributor, webdev } = c; VueInjector.inject(root, {
VueInjector.inject( components: { BdMessageBadge },
root, data: { c },
DOM.createElement('div', null, 'bdmessagebadges'), template: '<BdMessageBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />'
{ BdMessageBadge }, });
`<BdMessageBadge developer="${developer}" webdev="${webdev}" contributor="${contributor}"/>`,
true
);
} }
userlistBadge(e) { userlistBadge(e) {
const c = this.contributors.find(c => c.id === e.dataset.userId); const c = contributors.find(c => c.id === e.dataset.userId);
if (!c) return; if (!c) return;
const memberUsername = e.querySelector('.member-username'); const memberUsername = e.querySelector('.member-username');
if (!memberUsername) return; if (!memberUsername) return;
const root = document.createElement('span'); const root = document.createElement('span');
memberUsername.append(root); memberUsername.append(root);
const { developer, contributor, webdev } = c; VueInjector.inject(root, {
VueInjector.inject( components: { BdMessageBadge },
root, data: { c },
DOM.createElement('div', null, 'bdmessagebadges'), template: '<BdMessageBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />'
{ BdMessageBadge }, });
`<BdMessageBadge developer="${developer}" webdev="${webdev}" contributor="${contributor}"/>`,
true
);
} }
inject(userid) { inject(userid) {
const c = this.contributors.find(c => c.id === userid); const c = contributors.find(c => c.id === userid);
if (!c) return; if (!c) return;
setTimeout(() => { setTimeout(() => {
let hasBadges = false; let hasBadges = false;
let root = document.querySelector('[class*="profileBadges"]'); let root = document.querySelector('[class*="profileBadges"]');
@ -101,29 +98,16 @@ export default class extends EventListener {
root = document.querySelector('[class*="headerInfo"]'); root = document.querySelector('[class*="headerInfo"]');
} }
const { developer, contributor, webdev } = c; VueInjector.inject(root, {
components: { BdBadge },
VueInjector.inject( data: { hasBadges, c },
root, template: '<BdBadge :hasBadges="hasBadges" :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />',
DOM.createElement('div', null, 'bdprofilebadges'), }, DOM.createElement('div', null, 'bdprofilebadges'));
{ BdBadge },
`<BdBadge hasBadges="${hasBadges}" developer="${developer}" webdev="${webdev}" contributor="${contributor}"/>`
);
}, 400); }, 400);
} }
get contributors() { get contributors() {
return [ return contributors;
{ 'id': '81388395867156480', 'webdev': true, 'developer': true, 'contributor': true }, // Jiiks
{ 'id': '98003542823944192', 'webdev': false, 'developer': true, 'contributor': true }, // Pohky
{ 'id': '138850472541814784', 'webdev': true, 'developer': false, 'contributor': true }, // Hammock
{ 'id': '249746236008169473', 'webdev': false, 'developer': true, 'contributor': true }, // Zerebos
{ 'id': '125367412370440192', 'webdev': false, 'developer': true, 'contributor': true }, // Pierce
{ 'id': '284056145272766465', 'webdev': false, 'developer': false, 'contributor': true }, // Samuel Elliott
{ 'id': '184021060562321419', 'webdev': false, 'developer': false, 'contributor': true }, // Lilian Tedone
{ 'id': '76052829285916672', 'webdev': false, 'developer': false, 'contributor': true }, // samfun123
{ 'id': '171005991272316937', 'webdev': false, 'developer': false, 'contributor': true }, // samogot
];
} }
} }

View File

@ -16,8 +16,9 @@ Vue.use(VTooltip, {
defaultContainer: 'bd-tooltips', defaultContainer: 'bd-tooltips',
defaultClass: 'bd-tooltip', defaultClass: 'bd-tooltip',
defaultTargetClass: 'bd-has-tooltip', defaultTargetClass: 'bd-has-tooltip',
defaultArrowSelector: '.bd-tooltip-arrow',
defaultInnerSelector: '.bd-tooltip-inner', defaultInnerSelector: '.bd-tooltip-inner',
defaultTemplate: '<div class="bd-tooltip"><span class="bd-tooltip-inner"></span></div>', defaultTemplate: '<div class="bd-tooltip"><div class="bd-tooltip-arrow"></div><span class="bd-tooltip-inner"></span></div>',
defaultBoundariesElement: DOM.getElement('#app-mount') defaultBoundariesElement: DOM.getElement('#app-mount')
}); });

View File

@ -1,5 +1,5 @@
/** /**
* BetterDiscord VUE Injector Module * BetterDiscord Vue Injector Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved. * All rights reserved.
* https://betterdiscord.net * https://betterdiscord.net
@ -11,15 +11,21 @@
import Vue from './vue'; import Vue from './vue';
export default class { export default class {
static inject(root, bdnode, components, template, replaceRoot) {
if(!replaceRoot) bdnode.appendTo(root);
return new Vue({ /**
el: replaceRoot ? root : bdnode.element, * Creates a new Vue object and mounts it in the passed element.
components, * @param {HTMLElement} root The element to mount the new Vue object at
template * @param {Object} options Options to pass to Vue
}); * @param {BdNode} bdnode The element to append
* @return {Vue}
*/
static inject(root, options, bdnode) {
if(bdnode) bdnode.appendTo(root);
const vue = new Vue(options);
vue.$mount(bdnode ? bdnode.element : root);
return vue;
} }
} }

View File

@ -1,6 +1,5 @@
const const path = require('path');
path = require('path'), const webpack = require('webpack');
webpack = require('webpack');
const jsLoader = { const jsLoader = {
test: /\.(js|jsx)$/, test: /\.(js|jsx)$/,
@ -9,18 +8,18 @@ const jsLoader = {
query: { query: {
presets: ['react'] presets: ['react']
} }
} };
const vueLoader = { const vueLoader = {
test: /\.(vue)$/, test: /\.(vue)$/,
loader: 'vue-loader' loader: 'vue-loader'
} };
const scssLoader = { const scssLoader = {
test: /\.scss$/, test: /\.scss$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: ['css-loader', 'sass-loader'] loader: ['css-loader', 'sass-loader']
} };
module.exports = { module.exports = {
entry: './src/index.js', entry: './src/index.js',
@ -32,9 +31,11 @@ module.exports = {
loaders: [jsLoader, vueLoader, scssLoader] loaders: [jsLoader, vueLoader, scssLoader]
}, },
externals: { externals: {
'electron': 'window.require("electron")', electron: 'window.require("electron")',
'fs': 'window.require("fs")', fs: 'window.require("fs")',
'path': 'window.require("path")' path: 'window.require("path")',
node_utils: 'window.require("util")',
sparkplug: 'require("../../core/dist/sparkplug")'
}, },
resolve: { resolve: {
alias: { alias: {
@ -49,15 +50,10 @@ module.exports = {
path.resolve('src', 'structs'), path.resolve('src', 'structs'),
path.resolve('src', 'builtin') path.resolve('src', 'builtin')
] ]
},
node: {
process: false,
__filename: false,
__dirname: false
} }
/* resolve: {
alias: {
'momentjs': 'vendor/moment.min.js'
},
modules: [
path.resolve('./node_modules'),
path.resolve(__dirname, '..'),
path.resolve(__dirname, '..', 'node_modules')
]
}*/
}; };

View File

@ -0,0 +1,66 @@
const path = require('path');
const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const jsLoader = {
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['react']
}
};
const vueLoader = {
test: /\.(vue)$/,
loader: 'vue-loader'
};
const scssLoader = {
test: /\.scss$/,
exclude: /node_modules/,
loader: ['css-loader', 'sass-loader']
};
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'betterdiscord.client-release.js'
},
module: {
loaders: [jsLoader, vueLoader, scssLoader]
},
externals: {
electron: 'window.require("electron")',
fs: 'window.require("fs")',
path: 'window.require("path")',
node_utils: 'window.require("util")',
sparkplug: 'require("./sparkplug")'
},
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'),
path.resolve('src', 'plugins'),
path.resolve('src', 'structs'),
path.resolve('src', 'builtin')
]
},
node: {
process: false,
__filename: false,
__dirname: false
},
plugins: [
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true)
}),
new UglifyJsPlugin()
]
};

View File

@ -8,51 +8,155 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
const { ipcRenderer, ipcMain } = require('electron'); import { ipcRenderer } from 'electron';
export class ClientIPC { const callbacks = new WeakMap();
static on(channel, cb) {
ipcRenderer.on(channel, (event, message) => cb(event, message)); const ClientIPC = new class ClientIPC {
constructor() {
this.on('ping', () => 'pong', true);
} }
static async send(channel, message) { /**
* Adds an IPC event listener.
* @param {String} channel The channel to listen on
* @param {Function} callback A function that will be called when a message is received
* @param {Boolean} reply Whether to automatically reply to the message with the callback's return value
* @return {Promise}
*/
on(channel, callback, reply) {
channel = channel.startsWith('bd-') ? channel : `bd-${channel}`; channel = channel.startsWith('bd-') ? channel : `bd-${channel}`;
const __eid = Date.now().toString();
ipcRenderer.send(channel, Object.assign(message ? message : {}, { __eid })); const boundCallback = async (event, args) => {
const ipcevent = new BDIpcEvent(event, args);
try {
const r = callback(ipcevent, ipcevent.message);
if (reply) ipcevent.reply(await r);
} catch (err) {
console.error('Error in IPC callback:', err);
if (reply) ipcevent.reject(err);
}
};
callbacks.set(callback, boundCallback);
ipcRenderer.on(channel, boundCallback);
}
off(channel, callback) {
ipcRenderer.removeListener(channel, callbacks.get(callback));
}
/**
* Sends a message to the main process and returns a promise that is resolved when the main process replies.
* @param {String} channel The channel to send a message to
* @param {Any} message Data to send to the main process
* @param {Boolean} error Whether to mark the message as an error
* @return {Promise}
*/
async send(channel, message, error) {
channel = channel.startsWith('bd-') ? channel : `bd-${channel}`;
const eid = 'bd-' + Date.now().toString();
ipcRenderer.send(channel, { eid, message, error });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ipcRenderer.once(__eid, (event, arg) => { ipcRenderer.once(eid, (event, arg) => {
if (arg.err) { if (arg.error) reject(arg.message);
return reject(arg.err); else resolve(arg.message);
}
resolve(arg);
}); });
}); });
} }
/**
* Sends a message to the Discord window and returns a promise that is resolved when it replies.
* @param {String} channel The channel to send a message to
* @param {Any} message Data to send to the renderer process
* @return {Promise}
*/
sendToDiscord(channel, message) {
return this.send('bd-sendToDiscord', {
channel, message
});
}
/**
* Sends a message to the CSS editor window and returns a promise that is resolved when it replies.
* @param {String} channel The channel to send a message to
* @param {Any} message Data to send to the CSS editor window
* @return {Promise}
*/
sendToCssEditor(channel, message) {
return this.send('bd-sendToCssEditor', {
channel, message
});
}
ping() {
return this.send('ping');
}
getConfig() {
return this.send('getConfig');
}
showOpenDialog(options) {
return this.send('native-open', options);
}
compileSass(options) {
return this.send('compileSass', options);
}
dba(command) {
return this.send('dba', command);
}
} }
export default ClientIPC;
/**
* An IPC event.
*/
class BDIpcEvent { class BDIpcEvent {
constructor(event, args) { constructor(event, args) {
this.args = args; this.args = args;
this.ipcEvent = event; this.ipcEvent = event;
this.replied = false;
} }
bindings() { bindings() {
this.send = this.send.bind(this);
this.reply = this.reply.bind(this); this.reply = this.reply.bind(this);
} }
send(message) { /**
this.ipcEvent.sender.send(this.args.__eid, message); * Sends a message back to the message's sender.
* @param {Any} message Data to send to this message's sender
*/
get send() { return this.reply }
reply(message, error) {
if (this.replied)
throw {message: 'This message has already been replied to.'};
this.replied = true;
return ClientIPC.send(this.eid, message, error);
} }
reply(message) { reject(err) {
this.send(message); return this.reply(err, true);
} }
}
export class CoreIPC { get message() {
static on(channel, cb) { return this.args.message;
ipcMain.on(channel, (event, args) => cb(new BDIpcEvent(event, args))); }
get error() {
return this.args.error;
}
get eid() {
return this.args.eid;
} }
} }

View File

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

View File

@ -9,34 +9,45 @@
*/ */
import { Vendor } from 'modules'; import { Vendor } from 'modules';
import { FileUtils } from './utils';
import node_utils from 'node_utils';
const logs = []; export const logLevels = {
'log': 'log',
'warn': 'warn',
'err': 'error',
'error': 'error',
'debug': 'debug',
'dbg': 'debug',
'info': 'info'
};
export class ClientLogger { export default class Logger {
static err(module, message) { this.log(module, message, 'err'); } constructor(file) {
static warn(module, message) { this.log(module, message, 'warn'); } this.logs = [];
static info(module, message) { this.log(module, message, 'info'); } this.file = file;
static dbg(module, message) { this.log(module, message, 'dbg'); } }
static log(module, message, level = 'log') { err(module, message) { this.log(module, message, 'err'); }
level = this.parseLevel(level); warn(module, message) { this.log(module, message, 'warn'); }
if (typeof message === 'object' && !(message instanceof Array)) { info(module, message) { this.log(module, message, 'info'); }
console[level]('[%cBetter%cDiscord:%s]', 'color: #3E82E5', '', `${module}${level === 'debug' ? '|DBG' : ''}`, message); dbg(module, message) { this.log(module, message, 'dbg'); }
let message_string = message.toString();
if (message_string === '[object Object]')
message_string += ' ' + JSON.stringify(message, null, 4);
logs.push(`${level.toUpperCase()} : [${Vendor.moment().format('DD/MM/YY hh:mm:ss')}|${module}] ${message_string}${message_string === '[object Object]' ? ' ' + JSON.stringify(message, null, 4) : ''}`); log(module, message, level = 'log') {
return; level = Logger.parseLevel(level);
}
message = typeof message === 'object' && message instanceof Array ? message : [message]; message = typeof message === 'object' && message instanceof Array ? message : [message];
console[level]('[%cBetter%cDiscord:%s]', 'color: #3E82E5', '', `${module}${level === 'debug' ? '|DBG' : ''}`, ...message); console[level]('[%cBetter%cDiscord:%s]', 'color: #3E82E5', '', `${module}${level === 'debug' ? '|DBG' : ''}`, ...message);
logs.push(`${level.toUpperCase()} : [${Vendor.moment().format('DD/MM/YY hh:mm:ss')}|${module}] ${message.join(' ')}`);
const message_string = message.map(m => typeof m === 'string' ? m : node_utils.inspect(m)).join(' ');
this.logs.push(`${level.toUpperCase()} : [${Logger.timestamp}|${module}] ${message_string}`);
if (this.file)
FileUtils.appendToFile(this.file, `${level.toUpperCase()} : [${Logger.timestamp}|${module}] ${message_string}\n`);
} }
static logError(err) { logError(err) {
if (!err.module && !err.message) { if (!err.module && !err.message) {
console.log(err); console.log(err);
return; return;
@ -44,24 +55,14 @@ export class ClientLogger {
this.err(err.module, err.message); this.err(err.module, err.message);
} }
static get logs() {
return logs;
}
static get levels() {
return {
'log': 'log',
'warn': 'warn',
'err': 'error',
'error': 'error',
'debug': 'debug',
'dbg': 'debug',
'info': 'info'
};
}
static parseLevel(level) { static parseLevel(level) {
return this.levels.hasOwnProperty(level) ? this.levels[level] : 'log'; return logLevels.hasOwnProperty(level) ? logLevels[level] : 'log';
}
static get timestamp() {
return Vendor.moment().format('DD/MM/YY hh:mm:ss');
} }
} }
export const ClientLogger = new Logger();

View File

@ -8,19 +8,13 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
const
path = require('path'),
fs = require('fs'),
_ = require('lodash');
import { PatchedFunction, Patch } from './monkeypatch'; import { PatchedFunction, Patch } from './monkeypatch';
import { Vendor } from 'modules'; import path from 'path';
import fs from 'fs';
import _ from 'lodash';
import filetype from 'file-type'; import filetype from 'file-type';
export class Utils { export class Utils {
static isArrowFunction(fn) {
return !fn.toString().startsWith('function');
}
static overload(fn, cb) { static overload(fn, cb) {
const orig = fn; const orig = fn;
return function (...args) { return function (...args) {
@ -31,6 +25,10 @@ export class Utils {
/** /**
* Monkey-patches an object's method. * Monkey-patches an object's method.
* @param {Object} object The object containing the function to monkey patch
* @param {String} methodName The name of the method to monkey patch
* @param {Object|String|Function} options Options to pass to the Patch constructor
* @param {Function} function If {options} is either "before" or "after", this function will be used as that hook
*/ */
static monkeyPatch(object, methodName, options, f) { static monkeyPatch(object, methodName, options, f) {
const patchedFunction = new PatchedFunction(object, methodName); const patchedFunction = new PatchedFunction(object, methodName);
@ -41,12 +39,31 @@ export class Utils {
/** /**
* Monkey-patches an object's method and returns a promise that will be resolved with the data object when the method is called. * Monkey-patches an object's method and returns a promise that will be resolved with the data object when the method is called.
* You will have to call data.callOriginalMethod() if it wants the original method to be called. * This can only be used to get the arguments and return data. If you want to change anything, call Utils.monkeyPatch with the once option set to true.
*/ */
static monkeyPatchOnce(object, methodName) { static monkeyPatchOnce(object, methodName) {
return new Promise((resolve, reject) => {
this.monkeyPatch(object, methodName, 'after', data => {
data.patch.cancel();
resolve(data);
});
});
}
/**
* Monkey-patches an object's method and returns a promise that will be resolved with the data object when the method is called.
* You will have to call data.callOriginalMethod() if you wants the original method to be called.
*/
static monkeyPatchAsync(object, methodName, callback) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.monkeyPatch(object, methodName, data => { this.monkeyPatch(object, methodName, data => {
data.patch.cancel(); data.patch.cancel();
data.promise = data.return = callback ? Promise.all(callback.call(global, data, ...data.arguments)) : new Promise((resolve, reject) => {
data.resolve = resolve;
data.reject = reject;
});
resolve(data); resolve(data);
}); });
}); });
@ -81,15 +98,20 @@ export class Utils {
}; };
const patch = this.monkeyPatch(what, methodName, { const patch = this.monkeyPatch(what, methodName, {
before: before ? compatible_function(before) : undefined, before: !instead && before ? compatible_function(before) : undefined,
instead: instead ? compatible_function(instead) : undefined, instead: instead ? compatible_function(instead) : undefined,
after: after ? compatible_function(after) : undefined, after: !instead && after ? compatible_function(after) : undefined,
once once
}); });
return cancelPatch; return cancelPatch;
} }
/**
* Attempts to parse a string as JSON.
* @param {String} json The string to parse
* @return {Any}
*/
static async tryParseJson(jsonString) { static async tryParseJson(jsonString) {
try { try {
return JSON.parse(jsonString); return JSON.parse(jsonString);
@ -101,6 +123,11 @@ export class Utils {
} }
} }
/**
* Returns a new object with normalised keys.
* @param {Object} object
* @return {Object}
*/
static toCamelCase(o) { static toCamelCase(o) {
const camelCased = {}; const camelCased = {};
_.forEach(o, (value, key) => { _.forEach(o, (value, key) => {
@ -112,17 +139,20 @@ export class Utils {
return camelCased; return camelCased;
} }
static compare(value1, value2) { /**
* Checks if two or more values contain the same data.
* @param {Any} ...value The value to compare
* @return {Boolean}
*/
static compare(value1, value2, ...values) {
// Check to see if value1 and value2 contain the same data // Check to see if value1 and value2 contain the same data
if (typeof value1 !== typeof value2) return false; if (typeof value1 !== typeof value2) return false;
if (value1 === null && value2 === null) return true; if (value1 === null && value2 === null) return true;
if (value1 === null || value2 === null) return false; if (value1 === null || value2 === null) return false;
if (typeof value1 === 'object' || typeof value1 === 'array') { if (typeof value1 === 'object') {
// Loop through the object and check if everything's the same // Loop through the object and check if everything's the same
let value1array = typeof value1 === 'array' ? value1 : Object.keys(value1); if (Object.keys(value1).length !== Object.keys(value2).length) return false;
let value2array = typeof value2 === 'array' ? value2 : Object.keys(value2);
if (value1array.length !== value2array.length) return false;
for (let key in value1) { for (let key in value1) {
if (!this.compare(value1[key], value2[key])) return false; if (!this.compare(value1[key], value2[key])) return false;
@ -130,9 +160,20 @@ export class Utils {
} else if (value1 !== value2) return false; } else if (value1 !== value2) return false;
// value1 and value2 contain the same data // value1 and value2 contain the same data
// Check any more values
for (let value3 of values) {
if (!this.compare(value1, value3))
return false;
}
return true; return true;
} }
/**
* Clones an object and all it's properties.
* @param {Any} value The value to clone
* @return {Any} The cloned value
*/
static deepclone(value) { static deepclone(value) {
if (typeof value === 'object') { if (typeof value === 'object') {
if (value instanceof Array) return value.map(i => this.deepclone(i)); if (value instanceof Array) return value.map(i => this.deepclone(i));
@ -149,6 +190,11 @@ export class Utils {
return value; return value;
} }
/**
* Freezes an object and all it's properties.
* @param {Any} object The object to freeze
* @param {Function} exclude A function to filter object that shouldn't be frozen
*/
static deepfreeze(object, exclude) { static deepfreeze(object, exclude) {
if (exclude && exclude(object)) return; if (exclude && exclude(object)) return;
@ -165,38 +211,57 @@ export class Utils {
return object; return object;
} }
static filterArray(array, filter) { /**
const indexes = []; * Removes an item from an array. This differs from Array.prototype.filter as it mutates the original array instead of creating a new one.
for (let index in array) { * @param {Array} array The array to filter
if (!filter(array[index], index)) * @param {Any} item The item to remove from the array
indexes.push(index); * @return {Array}
} */
for (let i in indexes)
array.splice(indexes[i] - i, 1);
return array;
}
static removeFromArray(array, item) { static removeFromArray(array, item) {
let index; let index;
while ((index = array.indexOf(item)) > -1) while ((index = array.indexOf(item)) > -1)
array.splice(index, 1); array.splice(index, 1);
return array; return array;
} }
/**
* Defines a property with a getter that can be changed like a normal property.
* @param {Object} object The object to define a property on
* @param {String} property The property to define
* @param {Function} getter The property's getter
* @return {Object}
*/
static defineSoftGetter(object, property, get) {
return Object.defineProperty(object, property, {
get,
set: value => Object.defineProperty(object, property, {
value,
writable: true,
configurable: true,
enumerable: true
}),
configurable: true,
enumerable: true
});
}
} }
export class FileUtils { export class FileUtils {
/**
* Checks if a file exists and is a file.
* @param {String} path The file's path
* @return {Promise}
*/
static async fileExists(path) { static async fileExists(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => { fs.stat(path, (err, stats) => {
if (err) return reject({ if (err) return reject({
'message': `No such file or directory: ${err.path}`, message: `No such file or directory: ${err.path}`,
err err
}); });
if (!stats.isFile()) return reject({ if (!stats.isFile()) return reject({
'message': `Not a file: ${path}`, message: `Not a file: ${path}`,
stats stats
}); });
@ -205,16 +270,21 @@ export class FileUtils {
}); });
} }
/**
* Checks if a directory exists and is a directory.
* @param {String} path The directory's path
* @return {Promise}
*/
static async directoryExists(path) { static async directoryExists(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => { fs.stat(path, (err, stats) => {
if (err) return reject({ if (err) return reject({
'message': `Directory does not exist: ${path}`, message: `Directory does not exist: ${path}`,
err err
}); });
if (!stats.isDirectory()) return reject({ if (!stats.isDirectory()) return reject({
'message': `Not a directory: ${path}`, message: `Not a directory: ${path}`,
stats stats
}); });
@ -223,18 +293,25 @@ export class FileUtils {
}); });
} }
/**
* Creates a directory.
* @param {String} path The directory's path
* @return {Promise}
*/
static async createDirectory(path) { static async createDirectory(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.mkdir(path, err => { fs.mkdir(path, err => {
if (err) { if (err) reject(err);
if (err.code === 'EEXIST') return resolve(); else resolve();
else return reject(err);
}
resolve();
}); });
}); });
} }
/**
* Checks if a directory exists and creates it if it doesn't.
* @param {String} path The directory's path
* @return {Promise}
*/
static async ensureDirectory(path) { static async ensureDirectory(path) {
try { try {
await this.directoryExists(path); await this.directoryExists(path);
@ -249,17 +326,22 @@ export class FileUtils {
} }
} }
/**
* Returns the contents of a file.
* @param {String} path The file's path
* @return {Promise}
*/
static async readFile(path) { static async readFile(path) {
try { try {
await this.fileExists(path); await this.fileExists(path);
} catch (err) { } catch (err) {
throw (err); throw err;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readFile(path, 'utf-8', (err, data) => { fs.readFile(path, 'utf-8', (err, data) => {
if (err) reject({ if (err) return reject({
'message': `Could not read file: ${path}`, message: `Could not read file: ${path}`,
err err
}); });
@ -268,24 +350,62 @@ export class FileUtils {
}); });
} }
/**
* Returns the contents of a file.
* @param {String} path The file's path
* @param {Object} options Additional options to pass to fs.readFile
* @return {Promise}
*/
static async readFileBuffer(path, options) { static async readFileBuffer(path, options) {
try {
await this.fileExists(path);
} catch (err) {
throw err;
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readFile(path, options || {}, (err, data) => { fs.readFile(path, options || {}, (err, data) => {
if (err) return reject(err); if (err) reject(err);
resolve(data); else resolve(data);
}); });
}); });
} }
/**
* Writes to a file.
* @param {String} path The file's path
* @param {String} data The file's new contents
* @return {Promise}
*/
static async writeFile(path, data) { static async writeFile(path, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile(path, data, err => { fs.writeFile(path, data, err => {
if (err) return reject(err); if (err) reject(err);
resolve(); else resolve();
}); });
}); });
} }
/**
* Writes to the end of a file.
* @param {String} path The file's path
* @param {String} data The data to append to the file
* @return {Promise}
*/
static async appendToFile(path, data) {
return new Promise((resolve, reject) => {
fs.appendFile(path, data, err => {
if (err) reject(err);
else resolve();
});
});
}
/**
* Returns the contents of a file parsed as JSON.
* @param {String} path The file's path
* @return {Promise}
*/
static async readJsonFromFile(path) { static async readJsonFromFile(path) {
let readFile; let readFile;
try { try {
@ -295,41 +415,57 @@ export class FileUtils {
} }
try { try {
const parsed = await Utils.tryParseJson(readFile); return await Utils.tryParseJson(readFile);
return parsed;
} catch (err) { } catch (err) {
throw (Object.assign(err, { path })); throw Object.assign(err, { path });
} }
} }
/**
* Writes to a file as JSON.
* @param {String} path The file's path
* @param {Any} data The file's new contents
* @return {Promise}
*/
static async writeJsonToFile(path, json) { static async writeJsonToFile(path, json) {
return this.writeFile(path, JSON.stringify(json)); return this.writeFile(path, JSON.stringify(json));
} }
/**
* Returns an array of items in a directory.
* @param {String} path The directory's path
* @return {Promise}
*/
static async listDirectory(path) { static async listDirectory(path) {
try { await this.directoryExists(path);
await this.directoryExists(path); return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => { fs.readdir(path, (err, files) => {
fs.readdir(path, (err, files) => { if (err) reject(err);
if (err) return reject(err); else resolve(files);
resolve(files);
});
}); });
} catch (err) { });
throw err;
}
} }
static async readDir(path) { static async readDir(path) {
return this.listDirectory(path); return this.listDirectory(path);
} }
/**
* Returns a file or buffer's MIME type and typical file extension.
* @param {String|Buffer} buffer A buffer or the path of a file
* @return {Promise}
*/
static async getFileType(buffer) { static async getFileType(buffer) {
if (typeof buffer === 'string') buffer = await this.readFileBuffer(buffer); if (typeof buffer === 'string') buffer = await this.readFileBuffer(buffer);
return filetype(buffer); return filetype(buffer);
} }
/**
* Returns a file's contents as a data URI.
* @param {String} path The directory's path
* @return {Promise}
*/
static async toDataURI(buffer, type) { static async toDataURI(buffer, type) {
if (typeof buffer === 'string') buffer = await this.readFileBuffer(buffer); if (typeof buffer === 'string') buffer = await this.readFileBuffer(buffer);
if (!type) type = this.getFileType(buffer).mime; if (!type) type = this.getFileType(buffer).mime;

View File

@ -7,4 +7,4 @@
"debug": true "debug": true
}] }]
] ]
} }

View File

@ -5,7 +5,7 @@ const
plumber = require('gulp-plumber'), plumber = require('gulp-plumber'),
watch = require('gulp-watch'); watch = require('gulp-watch');
const task_babel = function () { const task_build = function () {
return pump([ return pump([
gulp.src('src/**/*js'), gulp.src('src/**/*js'),
plumber(), plumber(),
@ -14,7 +14,7 @@ const task_babel = function () {
]); ]);
} }
const watch_babel = function () { const task_watch = function () {
return pump([ return pump([
watch('src/**/*js'), watch('src/**/*js'),
plumber(), plumber(),
@ -23,5 +23,5 @@ const watch_babel = function () {
]); ]);
} }
gulp.task('build', task_babel); gulp.task('build', task_build);
gulp.task('watch', watch_babel); gulp.task('watch', task_watch);

View File

@ -1 +0,0 @@
module.exports = require('./main.js');

View File

@ -5,21 +5,18 @@
"version": "2.0.0b", "version": "2.0.0b",
"homepage": "https://betterdiscord.net", "homepage": "https://betterdiscord.net",
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "dist/main.js",
"contributors": [ "contributors": [
"Jiiks", "Jiiks",
"Pohky" "Pohky"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git" "url": "https://github.com/JsSucks/BetterDiscordApp.git"
}, },
"private": false, "private": false,
"devDependencies": {
},
"scripts": { "scripts": {
"build": "gulp build", "build": "gulp build",
"watch": "gulp watch" "watch": "gulp watch"
} }
} }

View File

@ -10,48 +10,47 @@
const path = require('path'); const path = require('path');
const sass = require('node-sass'); const sass = require('node-sass');
const { FileUtils, BDIpc, Config, WindowUtils, CSSEditor, Database } = require('./modules');
const { BrowserWindow, dialog } = require('electron'); const { BrowserWindow, dialog } = require('electron');
const tests = true; const { FileUtils, BDIpc, Config, WindowUtils, CSSEditor, Database } = require('./modules');
const _basePath = __dirname;
const tests = typeof PRODUCTION === 'undefined';
const _basePath = tests ? path.resolve(__dirname, '..', '..') : __dirname;
const _baseDataPath = tests ? path.resolve(_basePath, 'tests') : _basePath;
const sparkplug = path.resolve(__dirname, 'sparkplug.js');
const _clientScript = tests const _clientScript = tests
? path.resolve(__dirname, '..', '..', 'client', 'dist', 'betterdiscord.client.js') ? path.resolve(_basePath, 'client', 'dist', 'betterdiscord.client.js')
: path.resolve(__dirname, 'betterdiscord.client.js'); : path.resolve(_basePath, 'betterdiscord.client.js');
const _dataPath = tests
? path.resolve(__dirname, '..', '..', 'tests', 'data')
: path.resolve(__dirname, 'data');
const _extPath = tests
? path.resolve(__dirname, '..', '..', 'tests', 'ext')
: path.resolve(__dirname, 'ext');
const _pluginPath = path.resolve(_extPath, 'plugins');
const _themePath = path.resolve(_extPath, 'themes');
const _modulePath = path.resolve(_extPath, 'modules');
const _cssEditorPath = tests const _cssEditorPath = tests
? path.resolve(__dirname, '..', '..', 'csseditor', 'dist') ? path.resolve(__dirname, '..', '..', 'csseditor', 'dist')
: path.resolve(__dirname, 'csseditor'); : path.resolve(__dirname, 'csseditor');
const _dataPath = path.resolve(_baseDataPath, 'data');
const _extPath = path.resolve(_baseDataPath, 'ext');
const _pluginPath = path.resolve(_extPath, 'plugins');
const _themePath = path.resolve(_extPath, 'themes');
const _modulePath = path.resolve(_extPath, 'modules');
const version = require(path.resolve(_basePath, 'package.json')).version;
const paths = [ const paths = [
{ id: 'base', path: _basePath.replace(/\\/g, '/') }, { id: 'base', path: _basePath },
{ id: 'cs', path: _clientScript.replace(/\\/g, '/') }, { id: 'cs', path: _clientScript },
{ id: 'data', path: _dataPath.replace(/\\/g, '/') }, { id: 'data', path: _dataPath },
{ id: 'ext', path: _extPath.replace(/\\/g, '/') }, { id: 'ext', path: _extPath },
{ id: 'plugins', path: _pluginPath.replace(/\\/g, '/') }, { id: 'plugins', path: _pluginPath },
{ id: 'themes', path: _themePath.replace(/\\/g, '/') }, { id: 'themes', path: _themePath },
{ id: 'modules', path: _modulePath.replace(/\\/g, '/') }, { id: 'modules', path: _modulePath },
{ id: 'csseditor', path: _cssEditorPath.replace(/\\/g, '/') } { id: 'csseditor', path: _cssEditorPath }
]; ];
const sparkplug = path.resolve(__dirname, 'sparkplug.js').replace(/\\/g, '/');
const Common = {};
const globals = { const globals = {
version: '2.0.0a', version,
paths paths
} };
const dbInstance = new Database(paths.find(path => path.id === 'data').path);
class Comms { class Comms {
@ -61,67 +60,48 @@ class Comms {
} }
initListeners() { initListeners() {
BDIpc.on('bd-getConfig', o => { BDIpc.on('ping', () => 'pong', true);
o.reply(Common.Config.config);
});
BDIpc.on('bd-sendToDiscord', event => this.bd.windowUtils.send(event.args.channel, event.args.message)); BDIpc.on('bd-getConfig', () => this.bd.config.config, true);
BDIpc.on('bd-openCssEditor', o => this.bd.csseditor.openEditor(o)); BDIpc.on('bd-sendToDiscord', (event, m) => this.sendToDiscord(m.channel, m.message), true);
// BDIpc.on('bd-setScss', o => this.bd.csseditor.setSCSS(o.args.scss));
BDIpc.on('bd-sendToCssEditor', o => this.bd.csseditor.send(o.args.channel, o.args.data));
BDIpc.on('bd-readFile', this.readFile); BDIpc.on('bd-openCssEditor', (event, options) => this.bd.csseditor.openEditor(options), true);
BDIpc.on('bd-readJson', o => this.readFile(o, true)); BDIpc.on('bd-sendToCssEditor', (event, m) => this.sendToCssEditor(m.channel, m.message), true);
BDIpc.on('bd-native-open', o => { BDIpc.on('bd-native-open', (event, options) => {
dialog.showOpenDialog(BrowserWindow.fromWebContents(o.ipcEvent.sender), o.args, filenames => { dialog.showOpenDialog(BrowserWindow.fromWebContents(event.ipcEvent.sender), options, filenames => {
o.reply(filenames); event.reply(filenames);
}); });
}); });
BDIpc.on('bd-compileSass', o => { BDIpc.on('bd-compileSass', (event, options) => {
if (!o.args.path && !o.args.data) return o.reply(''); if (typeof options.path === 'string' && typeof options.data === 'string') {
if (typeof o.args.path === 'string' && typeof o.args.data === 'string') { options.data = `${options.data} @import '${options.path.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}';`;
o.args.data = `${o.args.data} @import '${o.args.path.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}';`; options.path = undefined;
o.args.path = undefined;
} }
sass.render(o.args, (err, result) => { sass.render(options, (err, result) => {
if (err) { if (err) event.reject(err);
o.reply({ err }); else event.reply(result);
return;
}
o.reply(result);
}); });
}); });
BDIpc.on('bd-dba', o => { BDIpc.on('bd-dba', (event, options) => this.bd.dbInstance.exec(options), true);
(async () => {
try {
const ret = await dbInstance.exec(o.args);
o.reply(ret);
} catch (err) {
o.reply({err});
}
})();
});
}
async readFile(o, json) {
const { path } = o.args;
try {
const readFile = json ? await FileUtils.readJsonFromFile(path) : await FileUtils.readFile(path);
o.reply(readFile);
} catch (err) {
o.reply(err);
}
} }
async send(channel, message) { async send(channel, message) {
BDIpc.send(channel, message); BDIpc.send(channel, message);
} }
async sendToDiscord(channel, message) {
return this.bd.windowUtils.send(channel, message);
}
async sendToCssEditor(channel, message) {
return this.bd.csseditor.send(channel, message);
}
} }
class BetterDiscord { class BetterDiscord {
@ -135,23 +115,32 @@ class BetterDiscord {
this.injectScripts = this.injectScripts.bind(this); this.injectScripts = this.injectScripts.bind(this);
this.ignite = this.ignite.bind(this); this.ignite = this.ignite.bind(this);
Common.Config = new Config(globals);
this.config = new Config(args || globals);
this.dbInstance = new Database(this.config.getPath('data'));
this.comms = new Comms(this); this.comms = new Comms(this);
this.init(); this.init();
} }
async init() { async init() {
const window = await this.waitForWindow(); await this.waitForWindowUtils();
this.windowUtils = new WindowUtils({ window });
await FileUtils.ensureDirectory(paths.find(path => path.id === 'ext').path); if (!tests) {
const basePath = this.config.getPath('base');
const files = await FileUtils.listDirectory(basePath);
const latestCs = FileUtils.resolveLatest(files, file => file.endsWith('.js') && file.startsWith('client.'), file => file.replace('client.', '').replace('.js', ''), 'client.', '.js');
this.config.getPath('cs', true).path = path.resolve(basePath, latestCs);
}
this.csseditor = new CSSEditor(this, paths.find(path => path.id === 'csseditor').path); await FileUtils.ensureDirectory(this.config.getPath('ext'));
this.windowUtils.events('did-get-response-details', () => this.ignite(this.windowUtils.window)); this.csseditor = new CSSEditor(this, this.config.getPath('csseditor'));
this.windowUtils.events('did-finish-load', e => this.injectScripts(true));
this.windowUtils.events('did-navigate-in-page', (event, url, isMainFrame) => { this.windowUtils.on('did-get-response-details', () => this.ignite());
this.windowUtils.on('did-finish-load', () => this.injectScripts(true));
this.windowUtils.on('did-navigate-in-page', (event, url, isMainFrame) => {
this.windowUtils.send('did-navigate-in-page', { event, url, isMainFrame }); this.windowUtils.send('did-navigate-in-page', { event, url, isMainFrame });
}); });
@ -166,10 +155,8 @@ class BetterDiscord {
const defer = setInterval(() => { const defer = setInterval(() => {
const windows = BrowserWindow.getAllWindows(); const windows = BrowserWindow.getAllWindows();
if (windows.length > 0) { for (let window of windows) {
windows.forEach(window => { if (window) BetterDiscord.ignite(window);
self.ignite(window);
});
} }
if (windows.length === 1 && windows[0].webContents.getURL().includes('discordapp.com')) { if (windows.length === 1 && windows[0].webContents.getURL().includes('discordapp.com')) {
@ -180,23 +167,40 @@ class BetterDiscord {
}); });
} }
ignite(window) { async waitForWindowUtils() {
//Hook things that Discord removes from global. These will be removed again in the client script if (this.windowUtils) return this.windowUtils;
window.webContents.executeJavaScript(`require("${sparkplug}");`); const window = await this.waitForWindow();
return this.windowUtils = new WindowUtils({ window });
} }
get window() {
return this.windowUtils ? this.windowUtils.window : undefined;
}
/**
* Hooks things that Discord removes from global. These will be removed again in the client script.
*/
ignite() {
return BetterDiscord.ignite(this.window);
}
/**
* Hooks things that Discord removes from global. These will be removed again in the client script.
* @param {BrowserWindow} window The window to inject the sparkplug script into
*/
static ignite(window) {
return WindowUtils.injectScript(window, sparkplug);
}
/**
* Injects the client script into the main window.
* @param {Boolean} reload Whether the main window was reloaded
*/
async injectScripts(reload = false) { async injectScripts(reload = false) {
console.log(`RELOAD? ${reload}`); console.log(`RELOAD? ${reload}`);
if (!tests) { return this.windowUtils.injectScript(this.config.getPath('cs'));
const files = await FileUtils.listDirectory(paths.find(path => path.id === 'base').path);
const latestCs = FileUtils.resolveLatest(files, file => file.endsWith('.js') && file.startsWith('client.'), file => file.replace('client.', '').replace('.js', ''), 'client.', '.js');
paths.find(path => path.id === 'cs').path = path.resolve(paths.find(path => path.id === 'base').path, latestCs).replace(/\\/g, '/');
}
this.windowUtils.injectScript(paths.find(path => path.id === 'cs').path);
} }
get fileUtils() { return FileUtils; }
} }
module.exports = { module.exports = {

View File

@ -5,39 +5,117 @@
* https://github.com/JsSucks - https://betterdiscord.net * https://github.com/JsSucks - https://betterdiscord.net
* *
* This source code is licensed under the MIT license found in the * 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 { Module } = require('./modulebase');
const { ipcMain } = require('electron'); const { ipcMain } = require('electron');
const { Module } = require('./modulebase');
const callbacks = new WeakMap();
/**
* The IPC module used in the main process.
*/
class BDIpc {
/**
* Adds an IPC event listener.
* @param {String} channel The channel to listen on
* @param {Function} callback A function that will be called when a message is received
* @param {Boolean} reply Whether to automatically reply to the message with the callback's return value
* @return {Promise}
*/
static on(channel, callback, reply) {
channel = channel.startsWith('bd-') ? channel : `bd-${channel}`;
const boundCallback = async (event, args) => {
const ipcevent = new BDIpcEvent(event, args);
try {
const r = callback(ipcevent, ipcevent.message);
if (reply) ipcevent.reply(await r);
} catch (err) {
console.error('Error in IPC callback:', err);
if (reply) ipcevent.reject(err);
}
};
callbacks.set(callback, boundCallback);
ipcMain.on(channel, boundCallback);
}
static off(channel, callback) {
ipcMain.removeListener(channel, callbacks.get(callback));
}
/**
* Sends a message to the main process and returns a promise that is resolved when the main process replies.
* @param {BrowserWindow} window The window to send a message to
* @param {String} channel The channel to send a message to
* @param {Any} message Data to send to the main process
* @param {Boolean} error Whether to mark the message as an error
* @return {Promise}
*/
static send(window, channel, message, error) {
channel = channel.startsWith('bd-') ? channel : `bd-${channel}`;
const eid = 'bd-' + Date.now().toString();
window.send(channel, { eid, message, error });
return new Promise((resolve, reject) => {
ipcMain.once(eid, (event, arg) => {
if (arg.error) reject(arg.message);
else resolve(arg.message);
});
});
}
static ping(window) {
return this.send(window, 'ping');
}
}
class BDIpcEvent extends Module { class BDIpcEvent extends Module {
constructor(event, args) { constructor(event, args) {
super(args); super(args);
this.ipcEvent = event; this.ipcEvent = event;
this.replied = false;
} }
bindings() { bindings() {
this.send = this.send.bind(this);
this.reply = this.reply.bind(this); this.reply = this.reply.bind(this);
} }
send(message) { /**
this.ipcEvent.sender.send(this.args.__eid, message); * Sends a message back to the message's sender.
* @param {Any} message Data to send to this message's sender
*/
reply(message, error) {
if (this.replied)
throw {message: 'This message has already been replied to.'};
this.replied = true;
return BDIpc.send(this.ipcEvent.sender, this.eid, message, error);
} }
reply(message) { reject(err) {
this.send(message); return this.reply(err, true);
}
get message() {
return this.args.message;
}
get error() {
return this.args.error;
}
get eid() {
return this.args.eid;
} }
} }
class BDIpc { module.exports = { BDIpc };
static on(channel, cb) {
ipcMain.on(channel, (event, args) => cb(new BDIpcEvent(event, args)));
}
}
module.exports = { BDIpc };

View File

@ -5,7 +5,7 @@
* https://github.com/JsSucks - https://betterdiscord.net * https://github.com/JsSucks - https://betterdiscord.net
* *
* This source code is licensed under the MIT license found in the * 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 { Module } = require('./modulebase'); const { Module } = require('./modulebase');
@ -20,13 +20,18 @@ class Config extends Module {
return this.args.paths; return this.args.paths;
} }
getPath(id, full) {
const path = this.paths.find(path => path.id === id);
return full ? path : path.path;
}
get config() { get config() {
return { return {
'version': this.version, version: this.version,
'paths': this.paths paths: this.paths
}; };
} }
} }
module.exports = { Config }; module.exports = { Config };

View File

@ -13,6 +13,7 @@ const { BrowserWindow } = require('electron');
const { Module } = require('./modulebase'); const { Module } = require('./modulebase');
const { WindowUtils } = require('./utils'); const { WindowUtils } = require('./utils');
const { BDIpc } = require('./bdipc');
class CSSEditor extends Module { class CSSEditor extends Module {
@ -22,57 +23,64 @@ class CSSEditor extends Module {
this.bd = bd; this.bd = bd;
} }
openEditor(o) { /**
if (this.editor) { * Opens an editor.
if (this.editor.isFocused()) return; * @return {Promise}
*/
openEditor(options) {
return new Promise((resolve, reject) => {
if (this.editor) {
if (this.editor.isFocused()) return;
this.editor.focus(); this.editor.focus();
this.editor.flashFrame(true); this.editor.flashFrame(true);
o.reply(true); return resolve(true);
return;
}
const options = this.options;
for (let option in o.args) {
if (o.args.hasOwnProperty(option)) {
options[option] = o.args[option];
} }
}
this.editor = new BrowserWindow(options); options = Object.assign({}, this.options, options);
this.editor.loadURL('about:blank');
this.editor.setSheetOffset(33);
this.editorUtils = new WindowUtils({ window: this.editor });
this.editor.on('close', () => { this.editor = new BrowserWindow(options);
this.bd.windowUtils.send('bd-save-csseditor-bounds', this.editor.getBounds()); this.editor.loadURL('about:blank');
this.editor = null; this.editor.setSheetOffset(33);
}); this.editorUtils = new WindowUtils({ window: this.editor });
this.editor.once('ready-to-show', () => { this.editor.on('close', () => {
this.editor.show(); this.bd.windowUtils.send('bd-save-csseditor-bounds', this.editor.getBounds());
}); this.editor = null;
});
this.editor.webContents.on('did-finish-load', () => { this.editor.once('ready-to-show', () => {
this.editorUtils.injectScript(path.join(this.editorPath, 'csseditor.js')); this.editor.show();
o.reply(true); });
});
} this.editor.webContents.on('did-finish-load', () => {
this.editorUtils.injectScript(path.join(this.editorPath, 'csseditor.js'));
setSCSS(scss) { resolve(true);
this.send('set-scss', scss); });
})
} }
/**
* Sends data to the editor.
* @param {String} channel
* @param {Any} data
*/
send(channel, data) { send(channel, data) {
if (!this.editor) return; if (!this.editor) throw {message: 'The CSS editor is not open.'};
this.editor.webContents.send(channel, data); return BDIpc.send(this.editor, channel, data);
} }
/**
* Sets the CSS editor's always on top flag.
*/
set alwaysOnTop(state) { set alwaysOnTop(state) {
if (!this.editor) return; if (!this.editor) return;
this.editor.setAlwaysOnTop(state); this.editor.setAlwaysOnTop(state);
} }
/**
* Default options to pass to BrowserWindow.
*/
get options() { get options() {
return { return {
width: 800, width: 800,
@ -81,7 +89,7 @@ class CSSEditor extends Module {
frame: false frame: false
}; };
} }
} }
module.exports = { CSSEditor }; module.exports = { CSSEditor };

View File

@ -10,7 +10,7 @@
const Datastore = require('nedb'); const Datastore = require('nedb');
class Database { class Database {
constructor(dbPath) { constructor(dbPath) {
this.exec = this.exec.bind(this); this.exec = this.exec.bind(this);

View File

@ -5,20 +5,19 @@
* https://github.com/JsSucks - https://betterdiscord.net * https://github.com/JsSucks - https://betterdiscord.net
* *
* This source code is licensed under the MIT license found in the * 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.
*/
/*
Base Module that every non-static module should extend
*/ */
/**
* Base Module that every non-static module should extend.
*/
class Module { class Module {
constructor(args) { constructor(args) {
this.__ = { this.__ = {
state: args, state: args,
args args
} };
this.init(); this.init();
} }
@ -33,4 +32,4 @@ class Module {
} }
module.exports = { Module }; module.exports = { Module };

View File

@ -10,43 +10,36 @@
// TODO Use common // TODO Use common
const const path = require('path');
path = require('path'), const fs = require('fs');
fs = require('fs');
const { Module } = require('./modulebase'); const { Module } = require('./modulebase');
const { BDIpc } = require('./bdipc');
class Utils { class Utils {
static async tryParseJson(jsonString) { static async tryParseJson(jsonString) {
try { try {
return JSON.parse(jsonString); return JSON.parse(jsonString);
}catch(err) { } catch (err) {
throw ({ throw ({
'message': 'Failed to parse json', message: 'Failed to parse json',
err err
}); });
} }
} }
static get timestamp() {
return 'Timestamp';
}
} }
class FileUtils { class FileUtils {
static async fileExists(path) { static async fileExists(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => { fs.stat(path, (err, stats) => {
if(err) return reject({ if(err) return reject({
'message': `No such file or directory: ${err.path}`, message: `No such file or directory: ${err.path}`,
err err
}); });
if(!stats.isFile()) return reject({ if(!stats.isFile()) return reject({
'message': `Not a file: ${path}`, message: `Not a file: ${path}`,
stats stats
}); });
@ -59,12 +52,12 @@ class FileUtils {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => { fs.stat(path, (err, stats) => {
if(err) return reject({ if(err) return reject({
'message': `Directory does not exist: ${path}`, message: `Directory does not exist: ${path}`,
err err
}); });
if(!stats.isDirectory()) return reject({ if(!stats.isDirectory()) return reject({
'message': `Not a directory: ${path}`, message: `Not a directory: ${path}`,
stats stats
}); });
@ -76,14 +69,14 @@ class FileUtils {
static async readFile(path) { static async readFile(path) {
try { try {
await this.fileExists(path); await this.fileExists(path);
} catch(err) { } catch (err) {
throw(err); throw err;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readFile(path, 'utf-8', (err, data) => { fs.readFile(path, 'utf-8', (err, data) => {
if(err) reject({ if(err) return reject({
'message': `Could not read file: ${path}`, message: `Could not read file: ${path}`,
err err
}); });
@ -97,14 +90,13 @@ class FileUtils {
try { try {
readFile = await this.readFile(path); readFile = await this.readFile(path);
} catch(err) { } catch(err) {
throw(err); throw err;
} }
try { try {
const parsed = await Utils.tryParseJson(readFile); return await Utils.tryParseJson(readFile);
return parsed; } catch (err) {
} catch(err) { throw Object.assign(err, { path });
throw(Object.assign(err, { path }));
} }
} }
@ -113,8 +105,8 @@ class FileUtils {
await this.directoryExists(path); await this.directoryExists(path);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readdir(path, (err, files) => { fs.readdir(path, (err, files) => {
if (err) return reject(err); if (err) reject(err);
resolve(files); else resolve(files);
}); });
}); });
} catch (err) { } catch (err) {
@ -145,11 +137,8 @@ class FileUtils {
static async createDirectory(path) { static async createDirectory(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.mkdir(path, err => { fs.mkdir(path, err => {
if (err) { if (err) reject(err);
if (err.code === 'EEXIST') return resolve(); else resolve();
else return reject(err);
}
resolve();
}); });
}); });
} }
@ -170,7 +159,6 @@ class FileUtils {
} }
class WindowUtils extends Module { class WindowUtils extends Module {
bindings() { bindings() {
this.openDevTools = this.openDevTools.bind(this); this.openDevTools = this.openDevTools.bind(this);
this.executeJavascript = this.executeJavascript.bind(this); this.executeJavascript = this.executeJavascript.bind(this);
@ -190,28 +178,32 @@ class WindowUtils extends Module {
} }
executeJavascript(script) { executeJavascript(script) {
this.webContents.executeJavaScript(script); return this.webContents.executeJavaScript(script);
} }
injectScript(fpath, variable) { injectScript(fpath, variable) {
console.log(`Injecting: ${fpath}`); return WindowUtils.injectScript(this.window, fpath, variable);
}
static injectScript(window, fpath, variable) {
window = window.webContents || window;
if (!window) return;
// console.log(`Injecting: ${fpath} to`, window);
const escaped_path = fpath.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); const escaped_path = fpath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const escaped_variable = variable ? variable.replace(/\\/g, '\\\\').replace(/"/g, '\\"') : null; const escaped_variable = variable ? variable.replace(/\\/g, '\\\\').replace(/"/g, '\\"') : null;
if (variable) this.executeJavascript(`window["${escaped_variable}"] = require("${escaped_path}");`); if (variable) return window.executeJavaScript(`window["${escaped_variable}"] = require("${escaped_path}");`);
else this.executeJavascript(`require("${escaped_path}");`); else return window.executeJavaScript(`require("${escaped_path}");`);
} }
events(event, callback) { on(event, callback) {
this.webContents.on(event, callback); this.webContents.on(event, callback);
} }
send(channel, message) { send(channel, message) {
channel = channel.startsWith('bd-') ? channel : `bd-${channel}`; return BDIpc.send(this.window, channel, message);
this.webContents.send(channel, message);
} }
} }
module.exports = { module.exports = {

View File

@ -1,5 +1,17 @@
/**
* BetterDiscord Sparkplug
* 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.
*
* This file is evaluated in the renderer process!
*/
(() => { (() => {
if (window.__bd && window.__bd.ignited) return; if (module.exports.bd) return;
console.log('[BetterDiscord|Sparkplug]'); console.log('[BetterDiscord|Sparkplug]');
@ -7,12 +19,12 @@
if (!ls) console.warn('[BetterDiscord|Sparkplug] Failed to hook localStorage :('); if (!ls) console.warn('[BetterDiscord|Sparkplug] Failed to hook localStorage :(');
const wsOrig = window.WebSocket; const wsOrig = window.WebSocket;
window.__bd = { const bd = module.exports.bd = {
localStorage: ls, localStorage: ls,
wsHook: null, wsHook: null,
wsOrig, wsOrig,
ignited: true ignited: true
} };
class WSHook extends window.WebSocket { class WSHook extends window.WebSocket {
@ -25,14 +37,14 @@
console.info(`[BetterDiscord|WebSocket Proxy] new WebSocket detected, url: ${url}`); console.info(`[BetterDiscord|WebSocket Proxy] new WebSocket detected, url: ${url}`);
if (!url.includes('gateway.discord.gg')) return; if (!url.includes('gateway.discord.gg')) return;
if (window.__bd.setWS) { if (bd.setWS) {
window.__bd.setWS(this); bd.setWS(this);
console.info(`[BetterDiscord|WebSocket Proxy] WebSocket sent to instance`); console.info(`[BetterDiscord|WebSocket Proxy] WebSocket sent to instance`);
return; return;
} }
console.info(`[BetterDiscord|WebSocket Proxy] WebSocket stored to __bd['wsHook']`); console.info(`[BetterDiscord|WebSocket Proxy] WebSocket stored to bd.wsHook`);
window.__bd.wsHook = this; bd.wsHook = this;
} }
} }

View File

@ -5,21 +5,19 @@
"version": "0.4.0", "version": "0.4.0",
"homepage": "https://betterdiscord.net", "homepage": "https://betterdiscord.net",
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "dist/csseditor.js",
"contributors": [ "contributors": [
"Jiiks", "Jiiks",
"Pohky" "Pohky"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git" "url": "https://github.com/JsSucks/BetterDiscordApp.git"
}, },
"private": false, "private": false,
"devDependencies": {
},
"scripts": { "scripts": {
"build": "webpack --progress --colors", "build": "webpack --progress --colors",
"watch": "webpack --progress --colors --watch" "watch": "webpack --progress --colors --watch",
"release": "webpack --progress --colors --config=webpack.production.config.js"
} }
} }

View File

@ -1,28 +0,0 @@
const { ipcRenderer } = window.require('electron');
export default class {
static on(channel, cb) {
ipcRenderer.on(channel, (event, args) => cb(event, args));
}
static async send(channel, message) {
const __eid = Date.now().toString();
ipcRenderer.send(
channel.startsWith('bd-') ? channel: `bd-${channel}`,
message === undefined ? { __eid } : Object.assign(message, { __eid })
);
return new Promise((resolve, reject) => {
ipcRenderer.once(__eid, (event, arg) => {
if (arg.err) return reject(arg);
resolve(arg);
});
});
}
static sendToDiscord(channel, message) {
this.send('bd-sendToDiscord', { channel, message });
}
}

View File

@ -33,18 +33,18 @@
</template> </template>
<script> <script>
import '../../node_modules/codemirror/addon/scroll/simplescrollbars.js'; import ClientIPC from 'bdipc';
import '../../node_modules/codemirror/mode/css/css.js';
import '../../node_modules/codemirror/addon/hint/css-hint.js';
import '../../node_modules/codemirror/addon/search/search.js';
import '../../node_modules/codemirror/addon/search/searchcursor.js';
import '../../node_modules/codemirror/addon/search/jump-to-line.js';
import '../../node_modules/codemirror/addon/dialog/dialog.js';
import '../../node_modules/codemirror/addon/hint/show-hint.js';
import BDIpc from './BDIpc'; import { remote } from 'electron';
const { remote } = window.require('electron'); import 'codemirror/addon/scroll/simplescrollbars.js';
import 'codemirror/mode/css/css.js';
import 'codemirror/addon/hint/css-hint.js';
import 'codemirror/addon/search/search.js';
import 'codemirror/addon/search/searchcursor.js';
import 'codemirror/addon/search/jump-to-line.js';
import 'codemirror/addon/dialog/dialog.js';
import 'codemirror/addon/hint/show-hint.js';
const ExcludedIntelliSenseTriggerKeys = { const ExcludedIntelliSenseTriggerKeys = {
'8': 'backspace', '8': 'backspace',
@ -131,42 +131,38 @@
} }
}, },
created() { created() {
BDIpc.on('set-scss', (_, data) => { ClientIPC.on('set-scss', (_, scss) => this.setScss(scss));
if (data.error) {
console.log(data.error);
return;
}
console.log(data);
this.setScss(data.scss);
});
BDIpc.on('scss-error', (_, err) => { ClientIPC.on('scss-error', (_, err) => {
this.error = err; this.error = err;
this.$forceUpdate(); this.$forceUpdate();
if (err) if (err)
console.error('SCSS parse error:', err); console.error('SCSS parse error:', err);
}); });
BDIpc.on('set-liveupdate', (e, liveUpdate) => this.liveUpdate = liveUpdate); ClientIPC.on('set-liveupdate', (e, liveUpdate) => this.liveUpdate = liveUpdate);
}, },
mounted() { mounted() {
this.codemirror.on('keyup', this.cmOnKeyUp); this.codemirror.on('keyup', this.cmOnKeyUp);
BDIpc.sendToDiscord('get-scss');
BDIpc.sendToDiscord('get-liveupdate'); (async () => {
this.setScss(await ClientIPC.sendToDiscord('get-scss'));
this.liveUpdate = await ClientIPC.sendToDiscord('get-liveupdate');
})();
}, },
watch: { watch: {
liveUpdate(liveUpdate) { liveUpdate(liveUpdate) {
BDIpc.sendToDiscord('set-liveupdate', liveUpdate); ClientIPC.sendToDiscord('set-liveupdate', liveUpdate);
} }
}, },
methods: { methods: {
save() { save() {
const scss = this.codemirror.getValue(); const scss = this.codemirror.getValue();
BDIpc.sendToDiscord('save-scss', scss); ClientIPC.sendToDiscord('save-scss', scss);
}, },
update() { update() {
const scss = this.codemirror.getValue(); const scss = this.codemirror.getValue();
BDIpc.sendToDiscord('update-scss', scss); ClientIPC.sendToDiscord('update-scss', scss);
}, },
toggleaot() { toggleaot() {
this.alwaysOnTop = !this.alwaysOnTop; this.alwaysOnTop = !this.alwaysOnTop;
@ -180,7 +176,7 @@
this.codemirror.setValue(scss || ''); this.codemirror.setValue(scss || '');
}, },
cmOnChange(value) { cmOnChange(value) {
if(this.liveUpdate) BDIpc.sendToDiscord('update-scss', value); if(this.liveUpdate) ClientIPC.sendToDiscord('update-scss', value);
}, },
cmOnKeyUp(editor, event) { cmOnKeyUp(editor, event) {
if (event.ctrlKey) return; if (event.ctrlKey) return;

View File

@ -1,16 +1,16 @@
const const path = require('path');
path = require('path'), const webpack = require('webpack');
webpack = require('webpack');
const vueLoader = { const vueLoader = {
test: /\.(vue)$/, test: /\.(vue)$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: 'vue-loader' loader: 'vue-loader'
} };
const scssLoader = { const scssLoader = {
test: /\.(css|scss)$/, test: /\.(css|scss)$/,
loader: ['css-loader', 'sass-loader'] loader: ['css-loader', 'sass-loader']
} };
module.exports = { module.exports = {
entry: './src/index.js', entry: './src/index.js',
@ -21,9 +21,17 @@ module.exports = {
module: { module: {
loaders: [vueLoader, scssLoader] loaders: [vueLoader, scssLoader]
}, },
externals: {
electron: 'window.require("electron")',
fs: 'window.require("fs")'
},
resolve: { resolve: {
alias: { alias: {
vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js') vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js')
} },
modules: [
path.resolve('..', 'node_modules'),
path.resolve('..', 'common', 'modules')
]
} }
}; };

View File

@ -0,0 +1,44 @@
const path = require('path');
const webpack = require('webpack');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const vueLoader = {
test: /\.(vue)$/,
exclude: /node_modules/,
loader: 'vue-loader'
};
const scssLoader = {
test: /\.(css|scss)$/,
loader: ['css-loader', 'sass-loader']
};
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'csseditor-release.js'
},
module: {
loaders: [vueLoader, scssLoader]
},
externals: {
electron: 'window.require("electron")',
fs: 'window.require("fs")'
},
resolve: {
alias: {
vue$: path.resolve('..', 'node_modules', 'vue', 'dist', 'vue.esm.js')
},
modules: [
path.resolve('..', 'node_modules'),
path.resolve('..', 'common', 'modules')
]
},
plugins: [
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true)
}),
new UglifyJsPlugin()
]
};

View File

@ -6,64 +6,75 @@ const
merge = require('gulp-merge'), merge = require('gulp-merge'),
copy = require('gulp-copy'), copy = require('gulp-copy'),
rename = require('gulp-rename'), rename = require('gulp-rename'),
inject = require('gulp-inject-string'),
copydeps = require('gulp-npm-copy-deps'); copydeps = require('gulp-npm-copy-deps');
const mainpkg = require('./package.json');
const corepkg = require('./core/package.json'); const corepkg = require('./core/package.json');
const clientpkg = require('./client/package.json'); const clientpkg = require('./client/package.json');
const editorpkg = require('./csseditor/package.json'); const editorpkg = require('./csseditor/package.json');
const releasepkg = function() {
delete mainpkg.main;
delete mainpkg.devDependencies;
delete mainpkg.scripts;
return fs.writeFileSync('./release/package.json', JSON.stringify(mainpkg, null, 2));
};
const client = function() { const client = function() {
return pump([ return pump([
gulp.src('./client/dist/*.client.js'), gulp.src('./client/dist/*.client-release.js'),
rename(`client.${clientpkg.version}.js`), rename(`client.${clientpkg.version}.js`),
gulp.dest('./release') gulp.dest('./release')
]); ]);
} };
const core = function() { const core = function() {
return pump([
gulp.src('./core/dist/modules/**/*'),
copy('release/', { prefix: 2 })
]);
}
const core2 = function() {
return pump([ return pump([
gulp.src('./core/dist/main.js'), gulp.src('./core/dist/main.js'),
inject.after("'use strict';\n", 'const PRODUCTION = true;\n'),
rename(`core.${corepkg.version}.js`), rename(`core.${corepkg.version}.js`),
gulp.dest('./release') gulp.dest('./release')
]); ]);
} };
const core3 = function() {
return fs.writeFileSync('./release/index.js', `module.exports = require('./core.${corepkg.version}.js');`);
}
const sparkplug = function() { const sparkplug = function() {
return pump([ return pump([
gulp.src('./core/dist/sparkplug.js'), gulp.src('./core/dist/sparkplug.js'),
gulp.dest('./release') gulp.dest('./release')
]); ]);
} };
const core_modules = function() {
return pump([
gulp.src('./core/dist/modules/**/*'),
copy('release/', { prefix: 2 })
]);
};
const index = function() {
return fs.writeFileSync('./release/index.js', `module.exports = require('./core.${corepkg.version}.js');`);
};
const cssEditor = function() { const cssEditor = function() {
return pump([ return pump([
gulp.src('./csseditor/dist/**/*'), gulp.src('./csseditor/dist/csseditor-release.js'),
rename('csseditor.js'),
copy('release/csseditor', { prefix: 2 }) copy('release/csseditor', { prefix: 2 })
]); ]);
} };
const deps = function() { const deps = function() {
return copydeps('./', './release'); return copydeps('./', './release');
} };
const bindings = function() { const node_sass_bindings = function() {
return pump([ return pump([
gulp.src('./other/node_sass_bindings/**/*'), gulp.src('./other/node_sass_bindings/**/*'),
copy('release/node_modules/node-sass/vendor', { prefix: 2 }) copy('release/node_modules/node-sass/vendor', { prefix: 2 })
]); ]);
} };
gulp.task('release', function () { gulp.task('release', function () {
del(['./release/**/*']).then(() => merge(client(), core(), core2(), core3(), sparkplug(), cssEditor(), deps(), bindings())); del(['./release/**/*']).then(() => merge(releasepkg(), client(), core(), sparkplug(), core_modules(), index(), cssEditor(), deps(), node_sass_bindings()));
}); });

View File

@ -5,19 +5,16 @@
"version": "2.0.0", "version": "2.0.0",
"homepage": "https://betterdiscord.net", "homepage": "https://betterdiscord.net",
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "dist/installer.js",
"contributors": [ "contributors": [
"Jiiks", "Jiiks",
"Pohky" "Pohky"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git" "url": "https://github.com/JsSucks/BetterDiscordApp.git"
}, },
"private": false, "private": false,
"devDependencies": {
},
"scripts": { "scripts": {
"build": "webpack --progress --colors", "build": "webpack --progress --colors",
"watch": "webpack --progress --colors --watch" "watch": "webpack --progress --colors --watch"

Some files were not shown because too many files have changed in this diff Show More