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
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space

34
.gitignore vendored
View File

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

View File

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

View File

@ -5,21 +5,19 @@
"version": "2.0.0b",
"homepage": "https://betterdiscord.net",
"license": "MIT",
"main": "index.js",
"main": "dist/betterdiscord.client.js",
"contributors": [
"Jiiks",
"Pohky"
],
"repository": {
"type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git"
"url": "https://github.com/JsSucks/BetterDiscordApp.git"
},
"private": false,
"devDependencies": {
},
"scripts": {
"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() {
return {
favourite: false
}
};
},
props: ['src', 'name'],
methods: {
},
methods: {},
beforeMount() {
// Check favourite state
}

View File

@ -7,26 +7,34 @@
* This source code is licensed under the MIT license found in the
* 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 { DOM, VueInjector, Reflection } from 'ui';
import { FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import EmoteComponent from './EmoteComponent.vue';
let emotes = null;
const emotesEnabled = true;
export default class {
static get searchCache() {
return this._searchCache || (this._searchCache = {});
}
static get emoteDb() {
return emotes;
}
static get React() {
return WebpackModules.getModuleByName('React');
}
static get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM');
}
static processMarkup(markup) {
if (!emotesEnabled) return markup; // TODO Get it from setttings
const newMarkup = [];
@ -92,13 +100,20 @@ export default class {
}
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 {
emotes = await FileUtils.readJsonFromFile(dataPath + '/emotes.json');
const Message = await ReactComponents.getComponent('Message');
this.unpatchRender = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('render', (component, args, retVal) => {
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;
markup.children[0] = this.processMarkup(markup.children[0]);
} catch (err) {
@ -131,12 +146,11 @@ export default class {
}
const { bdemoteName, bdemoteSrc } = root.dataset;
if (!bdemoteName || !bdemoteSrc) return;
VueInjector.inject(
root,
DOM.createElement('span'),
{ EmoteComponent },
`<EmoteComponent src="${bdemoteSrc}" name="${bdemoteName}"/>`
);
VueInjector.inject(root, {
components: { EmoteComponent },
data: { src: bdemoteSrc, name: bdemoteName },
template: '<EmoteComponent :src="src" :name="name" />'
}, DOM.createElement('span'));
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`.",
"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",
"type": "bool",
@ -91,7 +97,6 @@
{
"id": "css",
"text": "CSS Editor",
"hidden": true,
"settings": [
{
"category": "default",

View File

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

View File

@ -59,6 +59,7 @@ export default class Content {
/**
* Opens a settings modal for this content.
* @return {Modal}
*/
showSettingsModal() {
return Modals.contentSettings(this);
@ -73,20 +74,13 @@ export default class Content {
/**
* Saves the content's current configuration.
* @return {Promise}
*/
async saveConfiguration() {
try {
/*
await FileUtils.writeFile(`${this.contentPath}/user.config.json`, JSON.stringify({
enabled: this.enabled,
config: this.settings.strip().settings,
data: this.data
}));
*/
Database.insertOrUpdate({ type: 'contentconfig', $or: [{ id: this.id }, { name: this.name }] }, {
type: 'contentconfig',
Database.insertOrUpdate({ type: `${this.type}-config`, id: this.id }, {
type: `${this.type}-config`,
id: this.id,
name: this.name,
enabled: this.enabled,
config: this.settings.strip().settings,
data: this.data
@ -143,15 +137,6 @@ export default class Content {
return this.events.on(...args);
}
/**
* Removes an event listener.
* @param {String} event The event to remove the listener from
* @param {Function} callback The bound callback (optional)
*/
off(...args) {
return this.events.removeListener(...args);
}
/**
* Adds an event listener that removes itself when called, therefore only being called once.
* @param {String} event The event to add the listener to
@ -162,6 +147,15 @@ export default class Content {
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.
* @param {String} event The event to emit

View File

@ -24,31 +24,52 @@ import Combokeys from 'combokeys';
export default class {
/**
* Any errors that happened
* returns {Array}
* Any errors that happened.
* @return {Array}
*/
static get errors() {
return this._errors || (this._errors = []);
}
/**
* Locallly stored content
* returns {Array}
* Locally stored content.
* @return {Array}
*/
static get localContent() {
return this._localContent ? this._localContent : (this._localContent = []);
}
/**
* Local path for content
* returns {String}
* The type of content this content manager manages.
*/
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() {
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
*/
static async loadAllContent(suppressErrors = false) {
@ -83,8 +104,6 @@ export default class {
});
this._errors = [];
}
return this.localContent;
} catch (err) {
throw err;
}
@ -102,7 +121,7 @@ export default class {
const directories = await FileUtils.listDirectory(this.contentPath);
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;
try {
@ -150,8 +169,6 @@ export default class {
});
this._errors = [];
}
return this.localContent;
} catch (err) {
throw err;
}
@ -169,15 +186,12 @@ export default class {
await FileUtils.directoryExists(contentPath);
if (!reload) {
const loaded = this.localContent.find(content => content.contentPath === contentPath);
if (loaded) {
if (!reload && this.getContentByPath(contentPath))
throw { 'message': `Attempted to load already loaded user content: ${path}` };
}
}
const readConfig = await this.readConfig(contentPath);
const mainPath = path.join(contentPath, readConfig.main);
const configPath = path.resolve(contentPath, 'config.json');
const readConfig = await FileUtils.readJsonFromFile(configPath);
const mainPath = path.join(contentPath, readConfig.main || 'index.js');
const defaultConfig = new SettingsSet({
settings: readConfig.defaultConfig,
@ -191,19 +205,16 @@ export default class {
};
try {
//const readUserConfig = await this.readUserConfig(contentPath);
const readUserConfig = await Database.find({ type: 'contentconfig', name: readConfig.info.name });
const id = readConfig.info.id || readConfig.info.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
const readUserConfig = await Database.find({ type: `${this.contentType}-config`, id });
if (readUserConfig.length) {
userConfig.enabled = readUserConfig[0].enabled || false;
// await userConfig.config.merge({ settings: readUserConfig.config });
// userConfig.config.setSaved();
// userConfig.config = userConfig.config.clone({ settings: readUserConfig.config });
userConfig.config = readUserConfig[0].config;
userConfig.data = readUserConfig[0].data || {};
}
} catch (err) { /*We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config*/
Logger.info(this.moduleName, `Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`);
Logger.err(this.moduleName, err);
} catch (err) {
// We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config
Logger.warn(this.moduleName, [`Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`, err]);
}
userConfig.config = defaultConfig.clone({ settings: userConfig.config });
@ -244,9 +255,10 @@ export default class {
}
/**
* Unload content
* @param {any} content Content to unload
* @param {bool} reload Whether to reload the content after
* Unload content.
* @param {Content|String} content Content to unload
* @param {Boolean} reload Whether to reload the content after
* @return {Content}
*/
static async unloadContent(content, reload) {
content = this.findContent(content);
@ -275,34 +287,18 @@ export default class {
}
/**
* Reload content
* @param {any} content Content to reload
* Reload content.
* @param {Content|String} content Content to reload
* @return {Content}
*/
static reloadContent(content) {
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.
* @param {any} content Object to check
* @param {Any} content Object to check
* @return {Boolean}
*/
static isThisContent(content) {
return content instanceof Content;
@ -318,8 +314,9 @@ export default class {
/**
* Wildcard content finder
* @param {any} wild Content ID / directory name / path / name
* @param {bool} nonunique Allow searching attributes that may not be unique
* @param {String} wild Content ID / directory name / path / name
* @param {Boolean} nonunique Allow searching attributes that may not be unique
* @return {Content}
*/
static findContent(wild, nonunique) {
if (this.isThisContent(wild)) return wild;
@ -338,7 +335,8 @@ export default class {
/**
* Wait for content to load
* @param {any} content_id
* @param {String} content_id
* @return {Promise}
*/
static waitForContent(content_id) {
return new Promise((resolve, reject) => {

View File

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

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { ClientIPC } from 'bdipc';
import { ClientIPC } from 'common';
export default class {
@ -16,6 +16,12 @@ export default class {
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) {
try {
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) {
try {
return ClientIPC.send('bd-dba', { action: 'find', args });
@ -31,4 +42,5 @@ export default class {
throw err;
}
}
}

View File

@ -402,7 +402,7 @@ export default class DiscordApi {
static get currentChannel() {
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() {

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
* All rights reserved.
* https://betterdiscord.net
@ -8,14 +8,13 @@
* LICENSE file in the root directory of this source tree.
*/
import EventListener from './eventlistener';
import { Utils } from 'common';
import Events from './events';
import { Utils, ClientLogger as Logger } from 'common';
import { WebpackModules } from './webpackmodules';
import Events from './events';
import EventListener from './eventlistener';
import * as SocketStructs from '../structs/socketstructs';
/**
* Discord socket event hook
* @extends {EventListener}
@ -23,7 +22,7 @@ import * as SocketStructs from '../structs/socketstructs';
export default class extends EventListener {
init() {
console.log(SocketStructs);
Logger.log('EventHook', SocketStructs);
this.hook();
}
@ -44,16 +43,16 @@ export default class extends EventListener {
orig.call(this, ...args);
self.wsc = this;
self.emit(...args);
}
};
}
get eventsModule() {
return WebpackModules.getModuleByPrototypes(['setMaxListeners', 'emit']);
return WebpackModules.getModuleByName('Events');
}
/**
* Discord emit overload
* @param {any} e
* @param {any} event
* @param {any} action
* @param {any} data
*/
@ -66,8 +65,8 @@ export default class extends EventListener {
/**
* Emit callback
* @param {any} e Event Action
* @param {any} d Event Args
* @param {any} event Event
* @param {any} data Event data
*/
dispatch(e, 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_DELETE: 'LFG_LISTING_DELETE', // Thank you
BRAINTREE_POPUP_BRIDGE_CALLBACK: 'BRAINTREE_POPUP_BRIDGE_CALLBACK' // What
}
};
}
}

View File

@ -12,14 +12,39 @@ import { EventEmitter } from 'events';
const emitter = new EventEmitter();
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) {
emitter.emit(...args);
}

View File

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

View File

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

View File

@ -8,17 +8,16 @@
* 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 {
constructor(args) {
this.__ = {
state: args || {},
args
}
};
this.setState = this.setState.bind(this);
this.initialize();
}
@ -38,7 +37,6 @@ export default class Module {
set args(t) { }
get args() { return this.__.args; }
set state(state) { return this.__.state = state; }
get state() { return this.__.state; }

View File

@ -8,30 +8,39 @@
* 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 { ProfileBadges } from 'ui';
import Updater from './updater';
/**
* Module Manager initializes all modules when everything is ready
*/
export default class {
/**
* An array of modules.
*/
static get modules() {
return this._modules ? this._modules : (this._modules = [
new ProfileBadges(),
new SocketProxy(),
new EventHook(),
CssEditor,
new Updater()
Updater
]);
}
/**
* Initializes all modules.
* @return {Promise}
*/
static async initModules() {
for (let module of this.modules) {
try {
if (module.init && module.init instanceof Function) module.init();
} catch (err) {
console.log(`Failed to initialize module: ${err}`);
Logger.err('Module Manager', ['Failed to initialize module:', err]);
}
}
return true;

View File

@ -1,19 +1,23 @@
export { default as Events } from './events';
export { default as Settings } from './settings';
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 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 * from './webpackmodules';
export { default as ModuleManager } from './modulemanager';
export * from './patcher';
export * from './reactcomponents';
export { default as EventListener } from './eventlistener';
export { default as SocketProxy } from './socketproxy';
export { default as EventHook } from './eventhook';
export { default as Permissions } from './permissionmanager';
export { default as Database } from './database';
export { default as EventsWrapper } from './eventswrapper';
export { default as DiscordApi } from './discordapi';
export * from './patcher';
export * from './reactcomponents';

View File

@ -12,7 +12,9 @@ import { WebpackModules } from './webpackmodules';
import { ClientLogger as Logger, Utils } from 'common';
export class Patcher {
static get patches() { return this._patches || (this._patches = {}) }
static getPatchesByCaller(id) {
const patches = [];
for (const patch in this.patches) {
@ -22,16 +24,21 @@ export class Patcher {
}
return patches;
}
static unpatchAll(patches) {
if (typeof patches === 'string')
patches = this.getPatchesByCaller(patches);
for (const patch of patches) {
for (const child of patch.children) {
child.unpatch();
}
}
}
static resolveModule(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);
return null;
}
@ -99,10 +106,12 @@ export class Patcher {
static before() { return this.pushChildPatch(...arguments, 'before') }
static after() { return this.pushChildPatch(...arguments, 'after') }
static instead() { return this.pushChildPatch(...arguments, 'instead') }
static pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, type = 'after') {
const module = this.resolveModule(unresolvedModule);
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 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' }
// Don't use - these will eventually be removed!
get pluginPath() { return this.contentPath }
get pluginConfig() { return this.config }
get start() { return this.enable }
get stop() { return this.disable }
reload() {
return PluginManager.reloadPlugin(this);
}
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
* All rights reserved.
* https://betterdiscord.net
@ -8,7 +8,7 @@
* 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 ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager';
@ -20,31 +20,24 @@ import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs'
import { BdMenuItems, Modals, DOM, Reflection } from 'ui';
import DiscordApi from './discordapi';
import { ReactComponents } from './reactcomponents';
import { MonkeyPatch } from './patcher';
import { Patcher, MonkeyPatch } from './patcher';
export default class PluginApi {
constructor(pluginInfo) {
constructor(pluginInfo, pluginPath) {
this.pluginInfo = pluginInfo;
this.pluginPath = pluginPath;
this.Events = new EventsWrapper(Events);
Utils.defineSoftGetter(this.Events, 'bind', () => this.plugin);
this._menuItems = undefined;
this._injectedStyles = 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() {
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) {
@ -61,15 +54,18 @@ export default class PluginApi {
get Api() { return this }
get AsyncEventEmitter() { return AsyncEventEmitter }
get EventsWrapper() { return EventsWrapper }
/**
* Logger
*/
loggerLog(...message) { Logger.log(this.pluginInfo.name, message) }
loggerErr(...message) { Logger.err(this.pluginInfo.name, message) }
loggerWarn(...message) { Logger.warn(this.pluginInfo.name, message) }
loggerInfo(...message) { Logger.info(this.pluginInfo.name, message) }
loggerDbg(...message) { Logger.dbg(this.pluginInfo.name, message) }
loggerLog(...message) { Logger.log(this.plugin.name, message) }
loggerErr(...message) { Logger.err(this.plugin.name, message) }
loggerWarn(...message) { Logger.warn(this.plugin.name, message) }
loggerInfo(...message) { Logger.info(this.plugin.name, message) }
loggerDbg(...message) { Logger.dbg(this.plugin.name, message) }
get Logger() {
return {
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

View File

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

View File

@ -19,6 +19,7 @@ class Helpers {
static get plannedActions() {
return this._plannedActions || (this._plannedActions = new Map());
}
static recursiveArray(parent, key, count = 1) {
let index = 0;
function* innerCall(parent, key) {
@ -34,6 +35,7 @@ class Helpers {
return innerCall(parent, key);
}
static recursiveArrayCount(parent, key) {
let count = 0;
// eslint-disable-next-line no-empty-pattern
@ -41,6 +43,7 @@ class Helpers {
++count;
return this.recursiveArray(parent, key, count);
}
static get recursiveChildren() {
return function* (parent, key, index = 0, count = 1) {
const item = parent[key];
@ -52,12 +55,14 @@ class Helpers {
}
}
}
static returnFirst(iterator, process) {
for (let child of iterator) {
const retVal = process(child);
if (retVal !== undefined) return retVal;
}
}
static getFirstChild(rootParent, rootKey, selector) {
const getDirectChild = (item, selector) => {
if (item && item.props && item.props.children) {
@ -116,11 +121,13 @@ class Helpers {
};
return this.returnFirst(this.recursiveChildren(rootParent, rootKey), checkFilter.bind(null, selector)) || {};
}
static parseSelector(selector) {
if (selector.startsWith('.')) return { className: selector.substr(1) }
if (selector.startsWith('#')) return { id: selector.substr(1) }
return {}
}
static findByProp(obj, what, value) {
if (obj.hasOwnProperty(what) && obj[what] === value) return obj;
if (obj.props && !obj.children) return this.findByProp(obj.props, what, value);
@ -132,6 +139,7 @@ class Helpers {
}
return null;
}
static findProp(obj, what) {
if (obj.hasOwnProperty(what)) return obj[what];
if (obj.props && !obj.children) return this.findProp(obj.props, what);
@ -144,6 +152,7 @@ class Helpers {
}
return null;
}
static get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM');
}
@ -155,12 +164,15 @@ class ReactComponent {
this._component = component;
this._retVal = retVal;
}
get id() {
return this._id;
}
get component() {
return this._component;
}
get retVal() {
return this._retVal;
}

View File

@ -8,22 +8,23 @@
* 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 { SettingsSet, SettingUpdatedEvent } from 'structs';
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 {
constructor() {
this.settings = defaultSettings.map(_set => {
const set = new SettingsSet(_set);
set.on('setting-updated', event => {
const { category, setting, value, old_value } = event;
Logger.log('Settings', `${set.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`);
Logger.log('Settings', [`${set.id}/${category.id}/${setting.id} was changed from`, old_value, 'to', value]);
Events.emit('setting-updated', event);
Events.emit(`setting-updated-${set.id}_${category.id}_${setting.id}`, event);
});
@ -37,6 +38,9 @@ export default new class Settings {
});
}
/**
* Loads BetterDiscord's settings.
*/
async loadSettings() {
try {
await FileUtils.ensureDirectory(this.dataPath);
@ -48,7 +52,7 @@ export default new class Settings {
for (let set of this.settings) {
const newSet = settings.find(s => s.id === set.id);
if (!newSet) continue;
set.merge(newSet);
await set.merge(newSet);
set.setSaved();
}
@ -57,10 +61,13 @@ export default new class Settings {
} catch (err) {
// There was an error loading settings
// 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() {
try {
await FileUtils.ensureDirectory(this.dataPath);
@ -72,15 +79,10 @@ export default new class Settings {
css: CssEditor.css,
css_editor_files: CssEditor.files,
scss_error: CssEditor.error,
css_editor_bounds: {
width: CssEditor.editor_bounds.width,
height: CssEditor.editor_bounds.height,
x: CssEditor.editor_bounds.x,
y: CssEditor.editor_bounds.y
}
css_editor_bounds: CssEditor.editor_bounds
});
for (let set of this.getSettings) {
for (let set of this.settings) {
set.setSaved();
}
} 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) {
return this.getSettings.find(s => s.id === set_id);
return this.settings.find(s => s.id === set_id);
}
get core() { return this.getSet('core') }
@ -100,39 +107,46 @@ export default new class Settings {
get css() { return this.getSet('css') }
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) {
const set = this.getSet(set_id);
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) {
const set = this.getSet(set_id);
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) {
const set = this.getSet(set_id);
return set ? set.get(category_id, setting_id) : undefined;
}
async mergeSettings(set_id, newSettings) {
const set = this.getSet(set_id);
if (!set) return;
return 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;
}
/**
* The path to store user data in.
*/
get dataPath() {
return Globals.getPath('data');
}
}

View File

@ -8,6 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { ClientLogger as Logger } from 'common';
import EventListener from './eventlistener';
export default class SocketProxy extends EventListener {
@ -19,7 +20,7 @@ export default class SocketProxy extends EventListener {
get eventBindings() {
return [
{ id: 'socket-created', 'callback': this.socketCreated }
{ id: 'socket-created', callback: this.socketCreated }
];
}
@ -33,7 +34,7 @@ export default class SocketProxy extends EventListener {
}
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 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.
* This can be overridden by other content types.
@ -63,7 +59,7 @@ export default class Theme extends Content {
* @return {Promise}
*/
async compile() {
console.log('Compiling CSS');
Logger.log(this.name, 'Compiling CSS');
if (this.info.type === 'sass') {
const config = await ThemeManager.getConfigAsSCSS(this.settings);
@ -121,6 +117,7 @@ export default class Theme extends Content {
*/
set files(files) {
this.data.files = files;
if (Settings.get('css', 'default', 'watch-files'))
this.watchfiles = files;
}

View File

@ -62,11 +62,11 @@ export default class ThemeManager extends ContentManager {
}
static enableTheme(theme) {
theme.enable();
return theme.enable();
}
static disableTheme(theme) {
theme.disable();
return theme.disable();
}
static get isTheme() { return this.isThisContent }
@ -74,6 +74,11 @@ export default class ThemeManager extends ContentManager {
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) {
const variables = [];
@ -87,6 +92,11 @@ export default class ThemeManager extends ContentManager {
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) {
const variables = [];
@ -100,6 +110,11 @@ export default class ThemeManager extends ContentManager {
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) {
const { type, id, value } = setting;
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];
}
/**
* Escapes a string so it can be included in SCSS.
* @param {String} value The string to escape
* @return {String}
*/
static toSCSSString(value) {
if (typeof value !== 'string' && value.toString) value = value.toString();
return `'${typeof value === 'string' ? value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') : ''}'`;

View File

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

View File

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

View File

@ -51,6 +51,8 @@ const KnownModules = {
React: Filters.byProperties(['createElement', 'cloneElement']),
ReactDOM: Filters.byProperties(['render', 'findDOMNode']),
Events: Filters.byPrototypeFields(['setMaxListeners', 'emit']),
/* Guild Info, Stores, and Utilities */
GuildStore: Filters.byProperties(['getGuild']),
SortedGuildStore: Filters.byProperties(['getSortedGuilds']),
@ -89,7 +91,6 @@ const KnownModules = {
UserActivityStore: Filters.byProperties(['getActivity']),
UserNameResolver: Filters.byProperties(['getName']),
/* Emoji Store and Utils */
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
EmojiUtils: Filters.byProperties(['getGuildEmoji']),
@ -100,7 +101,6 @@ const KnownModules = {
InviteResolver: Filters.byProperties(['findInvite']),
InviteActions: Filters.byProperties(['acceptInvite']),
/* Discord Objects & Utils */
DiscordConstants: Filters.byProperties(["Permissions", "ActivityTypes", "StatusTypes"]),
Permissions: Filters.byProperties(['getHighestRole']),
@ -126,7 +126,6 @@ const KnownModules = {
ExperimentsManager: Filters.byProperties(['isDeveloper']),
CurrentExperiment: Filters.byProperties(['getExperimentId']),
/* Images, Avatars and Utils */
ImageResolver: Filters.byProperties(["getUserAvatarURL"]),
ImageUtils: Filters.byProperties(['getSizedImageSrc']),
@ -180,7 +179,6 @@ const KnownModules = {
URLParser: Filters.byProperties(['Url', 'parse']),
ExtraURLs: Filters.byProperties(['getArticleURL']),
/* DOM/React Components */
/* ==================== */
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),

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
}
/**
* The module the error occured in.
*/
get module() {
return this.args.module;
}
/**
* A message describing the error.
*/
get message() {
return this.args.message;
}
/**
* The original error object.
*/
get err() {
return this.args.err;
}
/**
* A trace showing which functions were called when the error occured.
*/
get stackTrace() {
return this.err.stack;
}
/**
* The type of event.
*/
get __eventType() {
return 'error';
}

View File

@ -17,14 +17,23 @@ export default class Event {
};
}
/**
* An object containing information about the event.
*/
get event() {
return this.__eventInfo;
}
/**
* The first argument that was passed to the constructor, which contains information about the event.
*/
get args() {
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 {
/**
* An array of SettingUpdated events.
*/
get updatedSettings() {
return this.args.updatedSettings;
}
/**
* The type of event.
*/
get __eventType() {
return 'settings-updated';
}

View File

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

View File

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

View File

@ -14,22 +14,29 @@ export default class MultipleChoiceOption {
constructor(args) {
this.args = args.args || args;
Object.freeze(this);
}
/**
* This option's ID.
*/
get id() {
return this.args.id || this.value;
}
/**
* A string describing this option.
*/
get text() {
return this.args.text;
}
/**
* The value to return when this option is active.
*/
get 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) {
this.args = args.args || args;
this.args.settings = this.settings.map(({ category, settings }) => ({
category, settings: settings.map(({ id, value }) => ({
id, value
}))
}));
Object.freeze(this);
}
/**
* The scheme's ID.
*/
get id() {
return this.args.id;
}
/**
* The URL of the scheme's icon. This should be a base64 encoded data URI.
*/
get icon_url() {
return this.args.icon_url;
}
/**
* The scheme's name.
*/
get name() {
return this.args.name;
}
/**
* A string to be displayed under the scheme.
*/
get hint() {
return this.args.hint;
}
/**
* An array of stripped settings categories this scheme manages.
*/
get 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) {
for (let schemeCategory of this.settings) {
const category = set.categories.find(c => c.category === schemeCategory.category);
@ -66,12 +80,13 @@ export default class SettingsScheme {
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) {
return set.merge({ settings: this.settings });
}
clone() {
return new SettingsScheme(Utils.deepclone(this.args));
return set.merge(this);
}
}

View File

@ -446,7 +446,7 @@ export default class SettingsSet {
text: this.text,
headertext: this.headertext,
settings: this.categories.map(category => category.clone()),
schemes: this.schemes.map(scheme => scheme.clone())
schemes: this.schemes
}, ...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 Combokeys from 'combokeys';
let keybindsPaused = false;
export default class KeybindSetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
this.__keybind_activated = this.__keybind_activated.bind(this);
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() {
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;
filter: brightness(10);
cursor: pointer;
.theme-light [class*="topSectionNormal-"] & {
}
.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 {
display: inline-block;
margin-left: 6px;
height: 11px;
.bd-message-badge-developer,
.bd-message-badge-contributor {
width: 12px;

View File

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

View File

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

View File

@ -19,18 +19,53 @@ bd-tooltips {
position: absolute;
word-wrap: break-word;
z-index: 9001;
margin-bottom: 10px;
}
.bd-tooltip:after {
.bd-tooltip-arrow {
border: 5px solid transparent;
content: " ";
height: 0;
pointer-events: none;
width: 0;
border-top-color: #000;
left: 50%;
margin-left: -5px;
position: absolute;
top: 100%;
}
&[x-placement^="top"] {
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.
*/
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 DOM from './dom';
import VueInjector from './vueinjector';
import EditedTimeStamp from './components/common/EditedTimestamp.vue';
import Autocomplete from './components/common/Autocomplete.vue';
class TempApi {
static get currentGuildId() {
try {
return WebpackModules.getModuleByName('SelectedGuildStore').getGuildId();
} catch (err) {
return 0;
}
}
static get currentChannelId() {
try {
return WebpackModules.getModuleByName('SelectedChannelStore').getChannelId();
} catch (err) {
return 0;
}
}
static get currentUserId() {
try {
return WebpackModules.getModuleByName('UserStore').getCurrentUser().id;
} catch (err) {
return 0;
}
}
}
export default class extends EventListener {
constructor(args) {
@ -54,22 +31,19 @@ export default class extends EventListener {
}
get eventBindings() {
return [{ id: 'gkh:keyup', callback: this.injectAutocomplete }];
/*
return [
{ id: 'server-switch', callback: this.manipAll },
{ id: 'channel-switch', callback: this.manipAll },
{ id: 'discord:MESSAGE_CREATE', callback: this.markupInjector },
{ id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector },
// { id: 'server-switch', callback: this.manipAll },
// { id: 'channel-switch', callback: this.manipAll },
// { id: 'discord:MESSAGE_CREATE', callback: this.markupInjector },
// { id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector },
{ id: 'gkh:keyup', callback: this.injectAutocomplete }
];
*/
}
manipAll() {
try {
this.appMount.setAttribute('guild-id', TempApi.currentGuildId);
this.appMount.setAttribute('channel-id', TempApi.currentChannelId);
this.appMount.setAttribute('guild-id', DiscordApi.currentGuild.id);
this.appMount.setAttribute('channel-id', DiscordApi.currentChannel.id);
this.setIds();
this.makeMutable();
} catch (err) {
@ -128,13 +102,11 @@ export default class extends EventListener {
if (markup.ets) {
const etsRoot = document.createElement('span');
markup.clone.appendChild(etsRoot);
VueInjector.inject(
etsRoot,
DOM.createElement('span', null, 'test'),
{ EditedTimeStamp },
`<EditedTimeStamp ets="${markup.ets}"/>`,
true
);
VueInjector.inject(etsRoot, {
components: { EditedTimeStamp },
data: { ets: markup.ets },
template: '<EditedTimeStamp :ets="ets" />'
});
}
Events.emit('ui:mutable:.markup', markup.clone);
@ -174,14 +146,14 @@ export default class extends EventListener {
const userTest = Reflection(msgGroup).prop('user');
if (!userTest) return;
msgGroup.setAttribute('data-author-id', userTest.id);
if (userTest.id === TempApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
if (userTest.id === DiscordApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
return;
}
msg.setAttribute('data-message-id', messageid);
const msgGroup = msg.closest('.message-group');
if (!msgGroup) return;
msgGroup.setAttribute('data-author-id', authorid);
if (authorid === TempApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
if (authorid === DiscordApi.currentUser.id) msgGroup.setAttribute('data-currentuser', true);
}
setUserId(user) {
@ -189,7 +161,7 @@ export default class extends EventListener {
const userid = Reflection(user).prop('user.id');
if (!userid) return;
user.setAttribute('data-user-id', userid);
const currentUser = userid === TempApi.currentUserId;
const currentUser = userid === DiscordApi.currentUser.id;
if (currentUser) user.setAttribute('data-currentuser', true);
Events.emit('ui:useridset', user);
}
@ -214,12 +186,11 @@ export default class extends EventListener {
const parent = document.querySelector('[class*="channelTextArea"] > [class*="inner"]');
if (!parent) return;
parent.append(root);
VueInjector.inject(
root,
DOM.createElement('span'),
{ Autocomplete },
`<Autocomplete initial="${e.target.value}"/>`,
true
);
VueInjector.inject(root, {
components: { Autocomplete },
data: { initial: e.target.value },
template: '<Autocomplete :initial="initial" />'
});
}
}

View File

@ -8,17 +8,22 @@
* LICENSE file in the root directory of this source tree.
*/
import { Events } from 'modules';
import { Utils } from 'common';
let items = 0;
const BdMenuItems = new class {
export const BdMenuItems = new class {
constructor() {
window.bdmenu = this;
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', 'ui', 'UI');
this.addSettingsSet('Internal', 'emotes', 'Emotes');
@ -28,7 +33,14 @@ const BdMenuItems = new class {
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) {
if (this.items.includes(item)) return item;
item.id = items++;
item.contentid = item.contentid || (items++ + '');
item.active = false;
@ -39,6 +51,13 @@ const BdMenuItems = new class {
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) {
return this.add({
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) {
return this.add({
category, text, component
});
}
/**
* Removes an item from the menu.
* @param {Object} item The item to remove from the menu
*/
remove(item) {
Utils.removeFromArray(this.items, item);
}
};
export { BdMenuItems };

View File

@ -8,48 +8,21 @@
* 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 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 { remote } from 'electron';
class TempApi {
static get currentGuild() {
try {
const currentGuildId = WebpackModules.getModuleByName('SelectedGuildStore').getGuildId();
return WebpackModules.getModuleByName('GuildStore').getGuild(currentGuildId);
} catch (err) {
return null;
}
}
static get currentChannel() {
try {
const currentChannelId = WebpackModules.getModuleByName('SelectedChannelStore').getChannelId();
return WebpackModules.getModuleByName('ChannelStore').getChannel(currentChannelId);
} catch (err) {
return 0;
}
}
static get currentUserId() {
try {
return WebpackModules.getModuleByName('UserStore').getCurrentUser().id;
} catch (err) {
return 0;
}
}
}
import { BdSettingsWrapper, BdModals } from './components';
export default class {
static initUiEvents() {
this.pathCache = {
isDm: null,
server: TempApi.currentGuild,
channel: TempApi.currentChannel
server: DiscordApi.currentGuild,
channel: DiscordApi.currentChannel
};
window.addEventListener('keyup', e => Events.emit('gkh:keyup', e));
this.autoManip = new AutoManip();
@ -67,9 +40,9 @@ export default class {
if (!remote.BrowserWindow.getFocusedWindow()) return;
clearInterval(ehookInterval);
remote.BrowserWindow.getFocusedWindow().webContents.on('did-navigate-in-page', (e, url, isMainFrame) => {
const { currentGuild, currentChannel } = TempApi;
const { currentGuild, currentChannel } = DiscordApi;
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.channel = currentChannel;
return;
@ -84,7 +57,7 @@ export default class {
currentGuild.id &&
this.pathCache.server &&
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.channel = currentChannel;
return;
@ -110,19 +83,19 @@ export default class {
DOM.createElement('div', null, 'bd-modals').appendTo(DOM.bdModals);
DOM.createElement('bd-tooltips').appendTo(DOM.bdBody);
const modals = new Vue({
this.modals = new Vue({
el: '#bd-modals',
components: { BdModals },
template: '<BdModals />'
});
const vueInstance = new Vue({
this.vueInstance = new Vue({
el: '#bd-settings',
components: { BdSettingsWrapper },
template: '<BdSettingsWrapper />'
});
return vueInstance;
return this.vueInstance;
}
}

View File

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

View File

@ -49,6 +49,7 @@
<CssEditorView v-if="item.contentid === 'css'" />
<PluginsView v-if="item.contentid === 'plugins'" />
<ThemesView v-if="item.contentid === 'themes'" />
<UpdaterView v-if="item.contentid === 'updater'" />
</div>
</ContentColumn>
</SidebarView>
@ -60,7 +61,7 @@
import { Settings } from 'modules';
import { BdMenuItems } from 'ui';
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';
export default {
@ -79,7 +80,7 @@
props: ['active', 'close'],
components: {
SidebarView, Sidebar, SidebarItem, ContentColumn,
SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView,
SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView, UpdaterView,
MiGithubCircle, MiWeb, MiClose, MiTwitterCircle
},
computed: {

View File

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

View File

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

View File

@ -11,9 +11,9 @@
<template>
<SettingsWrapper headertext="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>
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1,3 @@
export { default as BdSettingsWrapper } from './BdSettingsWrapper.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);
}
}
}

View File

@ -12,6 +12,7 @@ import { EventListener } from 'modules';
import DOM from './dom';
import { BdBadge, BdMessageBadge } from './components/bd';
import VueInjector from './vueinjector';
import contributors from '../data/contributors';
export default class extends EventListener {
@ -55,41 +56,37 @@ export default class extends EventListener {
if (msgGroup.dataset.hasBadges) return;
msgGroup.setAttribute('data-has-badges', true);
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;
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;
wrapperParent.insertBefore(root, wrapperParent.children[1]);
const { developer, contributor, webdev } = c;
VueInjector.inject(
root,
DOM.createElement('div', null, 'bdmessagebadges'),
{ BdMessageBadge },
`<BdMessageBadge developer="${developer}" webdev="${webdev}" contributor="${contributor}"/>`,
true
);
VueInjector.inject(root, {
components: { BdMessageBadge },
data: { c },
template: '<BdMessageBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />'
});
}
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;
const memberUsername = e.querySelector('.member-username');
if (!memberUsername) return;
const root = document.createElement('span');
memberUsername.append(root);
const { developer, contributor, webdev } = c;
VueInjector.inject(
root,
DOM.createElement('div', null, 'bdmessagebadges'),
{ BdMessageBadge },
`<BdMessageBadge developer="${developer}" webdev="${webdev}" contributor="${contributor}"/>`,
true
);
VueInjector.inject(root, {
components: { BdMessageBadge },
data: { c },
template: '<BdMessageBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />'
});
}
inject(userid) {
const c = this.contributors.find(c => c.id === userid);
const c = contributors.find(c => c.id === userid);
if (!c) return;
setTimeout(() => {
@ -101,29 +98,16 @@ export default class extends EventListener {
root = document.querySelector('[class*="headerInfo"]');
}
const { developer, contributor, webdev } = c;
VueInjector.inject(
root,
DOM.createElement('div', null, 'bdprofilebadges'),
{ BdBadge },
`<BdBadge hasBadges="${hasBadges}" developer="${developer}" webdev="${webdev}" contributor="${contributor}"/>`
);
VueInjector.inject(root, {
components: { BdBadge },
data: { hasBadges, c },
template: '<BdBadge :hasBadges="hasBadges" :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />',
}, DOM.createElement('div', null, 'bdprofilebadges'));
}, 400);
}
get contributors() {
return [
{ '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
];
return contributors;
}
}

View File

@ -16,8 +16,9 @@ Vue.use(VTooltip, {
defaultContainer: 'bd-tooltips',
defaultClass: 'bd-tooltip',
defaultTargetClass: 'bd-has-tooltip',
defaultArrowSelector: '.bd-tooltip-arrow',
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')
});

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
* All rights reserved.
* https://betterdiscord.net
@ -12,14 +12,20 @@ import Vue from './vue';
export default class {
static inject(root, bdnode, components, template, replaceRoot) {
if(!replaceRoot) bdnode.appendTo(root);
/**
* Creates a new Vue object and mounts it in the passed element.
* @param {HTMLElement} root The element to mount the new Vue object at
* @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);
return new Vue({
el: replaceRoot ? root : bdnode.element,
components,
template
});
const vue = new Vue(options);
vue.$mount(bdnode ? bdnode.element : root);
return vue;
}
}

View File

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

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.
*/
const { ipcRenderer, ipcMain } = require('electron');
import { ipcRenderer } from 'electron';
export class ClientIPC {
static on(channel, cb) {
ipcRenderer.on(channel, (event, message) => cb(event, message));
const callbacks = new WeakMap();
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}`;
const __eid = Date.now().toString();
ipcRenderer.send(channel, Object.assign(message ? message : {}, { __eid }));
return new Promise((resolve, reject) => {
ipcRenderer.once(__eid, (event, arg) => {
if (arg.err) {
return reject(arg.err);
}
resolve(arg);
});
});
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) => {
ipcRenderer.once(eid, (event, arg) => {
if (arg.error) reject(arg.message);
else resolve(arg.message);
});
});
}
/**
* 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 {
constructor(event, args) {
this.args = args;
this.ipcEvent = event;
this.replied = false;
}
bindings() {
this.send = this.send.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) {
this.send(message);
}
reject(err) {
return this.reply(err, true);
}
export class CoreIPC {
static on(channel, cb) {
ipcMain.on(channel, (event, args) => cb(new BDIpcEvent(event, args)));
get message() {
return this.args.message;
}
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 { ClientLogger } from './logger';
export { default as AsyncEventEmitter } from './async-eventemitter';

View File

@ -9,47 +9,10 @@
*/
import { Vendor } from 'modules';
import { FileUtils } from './utils';
import node_utils from 'node_utils';
const logs = [];
export class ClientLogger {
static err(module, message) { this.log(module, message, 'err'); }
static warn(module, message) { this.log(module, message, 'warn'); }
static info(module, message) { this.log(module, message, 'info'); }
static dbg(module, message) { this.log(module, message, 'dbg'); }
static log(module, message, level = 'log') {
level = this.parseLevel(level);
if (typeof message === 'object' && !(message instanceof Array)) {
console[level]('[%cBetter%cDiscord:%s]', 'color: #3E82E5', '', `${module}${level === 'debug' ? '|DBG' : ''}`, message);
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) : ''}`);
return;
}
message = typeof message === 'object' && message instanceof Array ? message : [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(' ')}`);
}
static logError(err) {
if (!err.module && !err.message) {
console.log(err);
return;
}
this.err(err.module, err.message);
}
static get logs() {
return logs;
}
static get levels() {
return {
export const logLevels = {
'log': 'log',
'warn': 'warn',
'err': 'error',
@ -58,10 +21,48 @@ export class ClientLogger {
'dbg': 'debug',
'info': 'info'
};
export default class Logger {
constructor(file) {
this.logs = [];
this.file = file;
}
err(module, message) { this.log(module, message, 'err'); }
warn(module, message) { this.log(module, message, 'warn'); }
info(module, message) { this.log(module, message, 'info'); }
dbg(module, message) { this.log(module, message, 'dbg'); }
log(module, message, level = 'log') {
level = Logger.parseLevel(level);
message = typeof message === 'object' && message instanceof Array ? message : [message];
console[level]('[%cBetter%cDiscord:%s]', 'color: #3E82E5', '', `${module}${level === 'debug' ? '|DBG' : ''}`, ...message);
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`);
}
logError(err) {
if (!err.module && !err.message) {
console.log(err);
return;
}
this.err(err.module, err.message);
}
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.
*/
const
path = require('path'),
fs = require('fs'),
_ = require('lodash');
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';
export class Utils {
static isArrowFunction(fn) {
return !fn.toString().startsWith('function');
}
static overload(fn, cb) {
const orig = fn;
return function (...args) {
@ -31,6 +25,10 @@ export class Utils {
/**
* 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) {
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.
* 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) {
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) => {
this.monkeyPatch(object, methodName, data => {
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);
});
});
@ -81,15 +98,20 @@ export class Utils {
};
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,
after: after ? compatible_function(after) : undefined,
after: !instead && after ? compatible_function(after) : undefined,
once
});
return cancelPatch;
}
/**
* Attempts to parse a string as JSON.
* @param {String} json The string to parse
* @return {Any}
*/
static async tryParseJson(jsonString) {
try {
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) {
const camelCased = {};
_.forEach(o, (value, key) => {
@ -112,17 +139,20 @@ export class Utils {
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
if (typeof value1 !== typeof value2) return false;
if (value1 === null && value2 === null) return true;
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
let value1array = typeof value1 === 'array' ? value1 : Object.keys(value1);
let value2array = typeof value2 === 'array' ? value2 : Object.keys(value2);
if (value1array.length !== value2array.length) return false;
if (Object.keys(value1).length !== Object.keys(value2).length) return false;
for (let key in value1) {
if (!this.compare(value1[key], value2[key])) return false;
@ -130,9 +160,20 @@ export class Utils {
} else if (value1 !== value2) return false;
// 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;
}
/**
* Clones an object and all it's properties.
* @param {Any} value The value to clone
* @return {Any} The cloned value
*/
static deepclone(value) {
if (typeof value === 'object') {
if (value instanceof Array) return value.map(i => this.deepclone(i));
@ -149,6 +190,11 @@ export class Utils {
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) {
if (exclude && exclude(object)) return;
@ -165,38 +211,57 @@ export class Utils {
return object;
}
static filterArray(array, filter) {
const indexes = [];
for (let index in array) {
if (!filter(array[index], index))
indexes.push(index);
}
for (let i in indexes)
array.splice(indexes[i] - i, 1);
return array;
}
/**
* Removes an item from an array. This differs from Array.prototype.filter as it mutates the original array instead of creating a new one.
* @param {Array} array The array to filter
* @param {Any} item The item to remove from the array
* @return {Array}
*/
static removeFromArray(array, item) {
let index;
while ((index = array.indexOf(item)) > -1)
array.splice(index, 1);
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 {
/**
* Checks if a file exists and is a file.
* @param {String} path The file's path
* @return {Promise}
*/
static async fileExists(path) {
return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => {
if (err) return reject({
'message': `No such file or directory: ${err.path}`,
message: `No such file or directory: ${err.path}`,
err
});
if (!stats.isFile()) return reject({
'message': `Not a file: ${path}`,
message: `Not a file: ${path}`,
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) {
return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => {
if (err) return reject({
'message': `Directory does not exist: ${path}`,
message: `Directory does not exist: ${path}`,
err
});
if (!stats.isDirectory()) return reject({
'message': `Not a directory: ${path}`,
message: `Not a directory: ${path}`,
stats
});
@ -223,18 +293,25 @@ export class FileUtils {
});
}
/**
* Creates a directory.
* @param {String} path The directory's path
* @return {Promise}
*/
static async createDirectory(path) {
return new Promise((resolve, reject) => {
fs.mkdir(path, err => {
if (err) {
if (err.code === 'EEXIST') return resolve();
else return reject(err);
}
resolve();
if (err) reject(err);
else 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) {
try {
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) {
try {
await this.fileExists(path);
} catch (err) {
throw (err);
throw err;
}
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf-8', (err, data) => {
if (err) reject({
'message': `Could not read file: ${path}`,
if (err) return reject({
message: `Could not read file: ${path}`,
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) {
try {
await this.fileExists(path);
} catch (err) {
throw err;
}
return new Promise((resolve, reject) => {
fs.readFile(path, options || {}, (err, data) => {
if (err) return reject(err);
resolve(data);
if (err) reject(err);
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) {
return new Promise((resolve, reject) => {
fs.writeFile(path, data, err => {
if (err) return reject(err);
resolve();
if (err) reject(err);
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) {
let readFile;
try {
@ -295,41 +415,57 @@ export class FileUtils {
}
try {
const parsed = await Utils.tryParseJson(readFile);
return parsed;
return await Utils.tryParseJson(readFile);
} 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) {
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) {
try {
await this.directoryExists(path);
return new Promise((resolve, reject) => {
fs.readdir(path, (err, files) => {
if (err) return reject(err);
resolve(files);
if (err) reject(err);
else resolve(files);
});
});
} catch (err) {
throw err;
}
}
static async readDir(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) {
if (typeof buffer === 'string') buffer = await this.readFileBuffer(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) {
if (typeof buffer === 'string') buffer = await this.readFileBuffer(buffer);
if (!type) type = this.getFileType(buffer).mime;

View File

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

View File

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

View File

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

View File

@ -10,48 +10,47 @@
const path = require('path');
const sass = require('node-sass');
const { FileUtils, BDIpc, Config, WindowUtils, CSSEditor, Database } = require('./modules');
const { BrowserWindow, dialog } = require('electron');
const tests = true;
const _basePath = __dirname;
const { FileUtils, BDIpc, Config, WindowUtils, CSSEditor, Database } = require('./modules');
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
? path.resolve(__dirname, '..', '..', 'client', 'dist', 'betterdiscord.client.js')
: path.resolve(__dirname, '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');
? path.resolve(_basePath, 'client', 'dist', 'betterdiscord.client.js')
: path.resolve(_basePath, 'betterdiscord.client.js');
const _cssEditorPath = tests
? path.resolve(__dirname, '..', '..', 'csseditor', 'dist')
: 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 = [
{ id: 'base', path: _basePath.replace(/\\/g, '/') },
{ id: 'cs', path: _clientScript.replace(/\\/g, '/') },
{ id: 'data', path: _dataPath.replace(/\\/g, '/') },
{ id: 'ext', path: _extPath.replace(/\\/g, '/') },
{ id: 'plugins', path: _pluginPath.replace(/\\/g, '/') },
{ id: 'themes', path: _themePath.replace(/\\/g, '/') },
{ id: 'modules', path: _modulePath.replace(/\\/g, '/') },
{ id: 'csseditor', path: _cssEditorPath.replace(/\\/g, '/') }
{ id: 'base', path: _basePath },
{ id: 'cs', path: _clientScript },
{ id: 'data', path: _dataPath },
{ id: 'ext', path: _extPath },
{ id: 'plugins', path: _pluginPath },
{ id: 'themes', path: _themePath },
{ id: 'modules', path: _modulePath },
{ id: 'csseditor', path: _cssEditorPath }
];
const sparkplug = path.resolve(__dirname, 'sparkplug.js').replace(/\\/g, '/');
const Common = {};
const globals = {
version: '2.0.0a',
version,
paths
}
const dbInstance = new Database(paths.find(path => path.id === 'data').path);
};
class Comms {
@ -61,67 +60,48 @@ class Comms {
}
initListeners() {
BDIpc.on('bd-getConfig', o => {
o.reply(Common.Config.config);
});
BDIpc.on('ping', () => 'pong', true);
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-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-sendToDiscord', (event, m) => this.sendToDiscord(m.channel, m.message), true);
BDIpc.on('bd-readFile', this.readFile);
BDIpc.on('bd-readJson', o => this.readFile(o, true));
BDIpc.on('bd-openCssEditor', (event, options) => this.bd.csseditor.openEditor(options), true);
BDIpc.on('bd-sendToCssEditor', (event, m) => this.sendToCssEditor(m.channel, m.message), true);
BDIpc.on('bd-native-open', o => {
dialog.showOpenDialog(BrowserWindow.fromWebContents(o.ipcEvent.sender), o.args, filenames => {
o.reply(filenames);
BDIpc.on('bd-native-open', (event, options) => {
dialog.showOpenDialog(BrowserWindow.fromWebContents(event.ipcEvent.sender), options, filenames => {
event.reply(filenames);
});
});
BDIpc.on('bd-compileSass', o => {
if (!o.args.path && !o.args.data) return o.reply('');
if (typeof o.args.path === 'string' && typeof o.args.data === 'string') {
o.args.data = `${o.args.data} @import '${o.args.path.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}';`;
o.args.path = undefined;
BDIpc.on('bd-compileSass', (event, options) => {
if (typeof options.path === 'string' && typeof options.data === 'string') {
options.data = `${options.data} @import '${options.path.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}';`;
options.path = undefined;
}
sass.render(o.args, (err, result) => {
if (err) {
o.reply({ err });
return;
}
o.reply(result);
sass.render(options, (err, result) => {
if (err) event.reject(err);
else event.reply(result);
});
});
BDIpc.on('bd-dba', o => {
(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);
}
BDIpc.on('bd-dba', (event, options) => this.bd.dbInstance.exec(options), true);
}
async 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 {
@ -135,23 +115,32 @@ class BetterDiscord {
this.injectScripts = this.injectScripts.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.init();
}
async init() {
const window = await this.waitForWindow();
this.windowUtils = new WindowUtils({ window });
await this.waitForWindowUtils();
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.windowUtils.events('did-finish-load', e => this.injectScripts(true));
this.csseditor = new CSSEditor(this, this.config.getPath('csseditor'));
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 });
});
@ -166,10 +155,8 @@ class BetterDiscord {
const defer = setInterval(() => {
const windows = BrowserWindow.getAllWindows();
if (windows.length > 0) {
windows.forEach(window => {
self.ignite(window);
});
for (let window of windows) {
if (window) BetterDiscord.ignite(window);
}
if (windows.length === 1 && windows[0].webContents.getURL().includes('discordapp.com')) {
@ -180,22 +167,39 @@ class BetterDiscord {
});
}
ignite(window) {
//Hook things that Discord removes from global. These will be removed again in the client script
window.webContents.executeJavaScript(`require("${sparkplug}");`);
async waitForWindowUtils() {
if (this.windowUtils) return this.windowUtils;
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) {
console.log(`RELOAD? ${reload}`);
if (!tests) {
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, '/');
return this.windowUtils.injectScript(this.config.getPath('cs'));
}
this.windowUtils.injectScript(paths.find(path => path.id === 'cs').path);
}
get fileUtils() { return FileUtils; }
}

View File

@ -7,37 +7,115 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const { Module } = require('./modulebase');
const { 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 {
constructor(event, args) {
super(args);
this.ipcEvent = event;
this.replied = false;
}
bindings() {
this.send = this.send.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) {
this.send(message);
reject(err) {
return this.reply(err, true);
}
get message() {
return this.args.message;
}
class BDIpc {
static on(channel, cb) {
ipcMain.on(channel, (event, args) => cb(new BDIpcEvent(event, args)));
get error() {
return this.args.error;
}
get eid() {
return this.args.eid;
}
}
module.exports = { BDIpc };

View File

@ -20,10 +20,15 @@ class Config extends Module {
return this.args.paths;
}
getPath(id, full) {
const path = this.paths.find(path => path.id === id);
return full ? path : path.path;
}
get config() {
return {
'version': this.version,
'paths': this.paths
version: this.version,
paths: this.paths
};
}

View File

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

View File

@ -8,17 +8,16 @@
* 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 {
constructor(args) {
this.__ = {
state: args,
args
}
};
this.init();
}

View File

@ -10,43 +10,36 @@
// TODO Use common
const
path = require('path'),
fs = require('fs');
const path = require('path');
const fs = require('fs');
const { Module } = require('./modulebase');
const { BDIpc } = require('./bdipc');
class Utils {
static async tryParseJson(jsonString) {
try {
return JSON.parse(jsonString);
} catch (err) {
throw ({
'message': 'Failed to parse json',
message: 'Failed to parse json',
err
});
}
}
static get timestamp() {
return 'Timestamp';
}
}
class FileUtils {
static async fileExists(path) {
return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => {
if(err) return reject({
'message': `No such file or directory: ${err.path}`,
message: `No such file or directory: ${err.path}`,
err
});
if(!stats.isFile()) return reject({
'message': `Not a file: ${path}`,
message: `Not a file: ${path}`,
stats
});
@ -59,12 +52,12 @@ class FileUtils {
return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => {
if(err) return reject({
'message': `Directory does not exist: ${path}`,
message: `Directory does not exist: ${path}`,
err
});
if(!stats.isDirectory()) return reject({
'message': `Not a directory: ${path}`,
message: `Not a directory: ${path}`,
stats
});
@ -77,13 +70,13 @@ class FileUtils {
try {
await this.fileExists(path);
} catch (err) {
throw(err);
throw err;
}
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf-8', (err, data) => {
if(err) reject({
'message': `Could not read file: ${path}`,
if(err) return reject({
message: `Could not read file: ${path}`,
err
});
@ -97,14 +90,13 @@ class FileUtils {
try {
readFile = await this.readFile(path);
} catch(err) {
throw(err);
throw err;
}
try {
const parsed = await Utils.tryParseJson(readFile);
return parsed;
return await Utils.tryParseJson(readFile);
} catch (err) {
throw(Object.assign(err, { path }));
throw Object.assign(err, { path });
}
}
@ -113,8 +105,8 @@ class FileUtils {
await this.directoryExists(path);
return new Promise((resolve, reject) => {
fs.readdir(path, (err, files) => {
if (err) return reject(err);
resolve(files);
if (err) reject(err);
else resolve(files);
});
});
} catch (err) {
@ -145,11 +137,8 @@ class FileUtils {
static async createDirectory(path) {
return new Promise((resolve, reject) => {
fs.mkdir(path, err => {
if (err) {
if (err.code === 'EEXIST') return resolve();
else return reject(err);
}
resolve();
if (err) reject(err);
else resolve();
});
});
}
@ -170,7 +159,6 @@ class FileUtils {
}
class WindowUtils extends Module {
bindings() {
this.openDevTools = this.openDevTools.bind(this);
this.executeJavascript = this.executeJavascript.bind(this);
@ -190,28 +178,32 @@ class WindowUtils extends Module {
}
executeJavascript(script) {
this.webContents.executeJavaScript(script);
return this.webContents.executeJavaScript(script);
}
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_variable = variable ? variable.replace(/\\/g, '\\\\').replace(/"/g, '\\"') : null;
if (variable) this.executeJavascript(`window["${escaped_variable}"] = require("${escaped_path}");`);
else this.executeJavascript(`require("${escaped_path}");`);
if (variable) return window.executeJavaScript(`window["${escaped_variable}"] = require("${escaped_path}");`);
else return window.executeJavaScript(`require("${escaped_path}");`);
}
events(event, callback) {
on(event, callback) {
this.webContents.on(event, callback);
}
send(channel, message) {
channel = channel.startsWith('bd-') ? channel : `bd-${channel}`;
this.webContents.send(channel, message);
return BDIpc.send(this.window, channel, message);
}
}
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]');
@ -7,12 +19,12 @@
if (!ls) console.warn('[BetterDiscord|Sparkplug] Failed to hook localStorage :(');
const wsOrig = window.WebSocket;
window.__bd = {
const bd = module.exports.bd = {
localStorage: ls,
wsHook: null,
wsOrig,
ignited: true
}
};
class WSHook extends window.WebSocket {
@ -25,14 +37,14 @@
console.info(`[BetterDiscord|WebSocket Proxy] new WebSocket detected, url: ${url}`);
if (!url.includes('gateway.discord.gg')) return;
if (window.__bd.setWS) {
window.__bd.setWS(this);
if (bd.setWS) {
bd.setWS(this);
console.info(`[BetterDiscord|WebSocket Proxy] WebSocket sent to instance`);
return;
}
console.info(`[BetterDiscord|WebSocket Proxy] WebSocket stored to __bd['wsHook']`);
window.__bd.wsHook = this;
console.info(`[BetterDiscord|WebSocket Proxy] WebSocket stored to bd.wsHook`);
bd.wsHook = this;
}
}

View File

@ -5,21 +5,19 @@
"version": "0.4.0",
"homepage": "https://betterdiscord.net",
"license": "MIT",
"main": "index.js",
"main": "dist/csseditor.js",
"contributors": [
"Jiiks",
"Pohky"
],
"repository": {
"type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git"
"url": "https://github.com/JsSucks/BetterDiscordApp.git"
},
"private": false,
"devDependencies": {
},
"scripts": {
"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>
<script>
import '../../node_modules/codemirror/addon/scroll/simplescrollbars.js';
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 ClientIPC from 'bdipc';
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 = {
'8': 'backspace',
@ -131,42 +131,38 @@
}
},
created() {
BDIpc.on('set-scss', (_, data) => {
if (data.error) {
console.log(data.error);
return;
}
console.log(data);
this.setScss(data.scss);
});
ClientIPC.on('set-scss', (_, scss) => this.setScss(scss));
BDIpc.on('scss-error', (_, err) => {
ClientIPC.on('scss-error', (_, err) => {
this.error = err;
this.$forceUpdate();
if (err)
console.error('SCSS parse error:', err);
});
BDIpc.on('set-liveupdate', (e, liveUpdate) => this.liveUpdate = liveUpdate);
ClientIPC.on('set-liveupdate', (e, liveUpdate) => this.liveUpdate = liveUpdate);
},
mounted() {
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: {
liveUpdate(liveUpdate) {
BDIpc.sendToDiscord('set-liveupdate', liveUpdate);
ClientIPC.sendToDiscord('set-liveupdate', liveUpdate);
}
},
methods: {
save() {
const scss = this.codemirror.getValue();
BDIpc.sendToDiscord('save-scss', scss);
ClientIPC.sendToDiscord('save-scss', scss);
},
update() {
const scss = this.codemirror.getValue();
BDIpc.sendToDiscord('update-scss', scss);
ClientIPC.sendToDiscord('update-scss', scss);
},
toggleaot() {
this.alwaysOnTop = !this.alwaysOnTop;
@ -180,7 +176,7 @@
this.codemirror.setValue(scss || '');
},
cmOnChange(value) {
if(this.liveUpdate) BDIpc.sendToDiscord('update-scss', value);
if(this.liveUpdate) ClientIPC.sendToDiscord('update-scss', value);
},
cmOnKeyUp(editor, event) {
if (event.ctrlKey) return;

View File

@ -1,16 +1,16 @@
const
path = require('path'),
webpack = require('webpack');
const path = require('path');
const webpack = require('webpack');
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',
@ -21,9 +21,17 @@ module.exports = {
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')
]
}
};

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'),
copy = require('gulp-copy'),
rename = require('gulp-rename'),
inject = require('gulp-inject-string'),
copydeps = require('gulp-npm-copy-deps');
const mainpkg = require('./package.json');
const corepkg = require('./core/package.json');
const clientpkg = require('./client/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() {
return pump([
gulp.src('./client/dist/*.client.js'),
gulp.src('./client/dist/*.client-release.js'),
rename(`client.${clientpkg.version}.js`),
gulp.dest('./release')
]);
}
};
const core = function() {
return pump([
gulp.src('./core/dist/modules/**/*'),
copy('release/', { prefix: 2 })
]);
}
const core2 = function() {
return pump([
gulp.src('./core/dist/main.js'),
inject.after("'use strict';\n", 'const PRODUCTION = true;\n'),
rename(`core.${corepkg.version}.js`),
gulp.dest('./release')
]);
}
const core3 = function() {
return fs.writeFileSync('./release/index.js', `module.exports = require('./core.${corepkg.version}.js');`);
}
};
const sparkplug = function() {
return pump([
gulp.src('./core/dist/sparkplug.js'),
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() {
return pump([
gulp.src('./csseditor/dist/**/*'),
gulp.src('./csseditor/dist/csseditor-release.js'),
rename('csseditor.js'),
copy('release/csseditor', { prefix: 2 })
]);
}
};
const deps = function() {
return copydeps('./', './release');
}
};
const bindings = function() {
const node_sass_bindings = function() {
return pump([
gulp.src('./other/node_sass_bindings/**/*'),
copy('release/node_modules/node-sass/vendor', { prefix: 2 })
]);
}
};
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",
"homepage": "https://betterdiscord.net",
"license": "MIT",
"main": "index.js",
"main": "dist/installer.js",
"contributors": [
"Jiiks",
"Pohky"
],
"repository": {
"type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git"
"url": "https://github.com/JsSucks/BetterDiscordApp.git"
},
"private": false,
"devDependencies": {
},
"scripts": {
"build": "webpack --progress --colors",
"watch": "webpack --progress --colors --watch"

View File

@ -2,16 +2,17 @@ const
path = require('path'),
webpack = require('webpack'),
HtmlWebpackPlugin = require('html-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',

1732
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
],
"repository": {
"type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git"
"url": "https://github.com/JsSucks/BetterDiscordApp.git"
},
"private": false,
"dependencies": {
@ -38,6 +38,7 @@
"gulp": "^3.9.1",
"gulp-babel": "^7.0.0",
"gulp-copy": "^1.1.0",
"gulp-inject-string": "^1.1.1",
"gulp-merge": "^0.1.1",
"gulp-npm-copy-deps": "^1.0.2",
"gulp-plumber": "^1.2.0",
@ -46,11 +47,10 @@
"html-webpack-plugin": "^3.0.6",
"jquery": "^3.2.1",
"lodash": "^4.17.4",
"nedb": "^1.8.0",
"node-gyp": "^3.6.2",
"node-sass": "^4.7.2",
"pump": "^2.0.0",
"sass-loader": "^6.0.6",
"uglifyjs-webpack-plugin": "^1.2.4",
"v-tooltip": "^2.0.0-rc.30",
"vue": "^2.5.13",
"vue-codemirror": "^4.0.3",
@ -73,6 +73,7 @@
"lint": "eslint -f unix client/src core/src csseditor/src",
"test": "npm run build && npm run lint",
"build_node-sass": "node scripts/build-node-sass.js",
"release": "npm run lint && npm run build && gulp release"
"build_release": "npm run release --prefix client && npm run build --prefix core && npm run release --prefix csseditor && npm run build --prefix installer",
"release": "npm run lint && npm run build_release && gulp release"
}
}

View File

@ -4,11 +4,11 @@ set "ELECTRON=1.6.15"
set "PLATFORM=win32"
set "ARCH=ia32"
set "VER=53"
set "VENDOR_PATH=..\node_modules\node-sass\vendor"
set "BUILD_PATH=..\node_modules\node-sass\build\Release\binding.node"
set "VENDOR_PATH=.\node_modules\node-sass\vendor"
set "BUILD_PATH=.\node_modules\node-sass\build\Release\binding.node"
echo Building %PLATFORM%-%ARCH% bindings
call ../node_modules/.bin/electron-rebuild -v=%ELECTRON% -a=%ARCH% -m ../node_modules/node-sass
call ./node_modules/.bin/electron-rebuild -v=%ELECTRON% -a=%ARCH% -m ./node_modules/node-sass
if exist %VENDOR_PATH%\%PLATFORM%-%ARCH%-%VER%\binding.node (
echo Deleting old %VENDOR_PATH%\%PLATFORM%-%ARCH%-%VER%\binding.node
@ -30,7 +30,7 @@ if not exist %BUILD_PATH% (
set "ARCH=x64"
echo Building %PLATFORM%-%ARCH% bindings
call ../node_modules/.bin/electron-rebuild -v=%ELECTRON% -a=%ARCH% -m ../node_modules/node-sass
call ./node_modules/.bin/electron-rebuild -v=%ELECTRON% -a=%ARCH% -m ./node_modules/node-sass
if exist %VENDOR_PATH%\%PLATFORM%-%ARCH%-%VER%\binding.node (
echo Deleting old %VENDOR_PATH%\%PLATFORM%-%ARCH%-%VER%\binding.node

View File

@ -1,4 +0,0 @@
{
"version": "0.3.2",
"paths": []
}

View File

@ -1,50 +0,0 @@
<html>
<head>
<title>CSS Editor</title>
<link rel="stylesheet" href="../../node_modules/codemirror/lib/codemirror.css" />
<link rel="stylesheet" href="../../node_modules/codemirror/theme/material.css" />
<link rel="stylesheet" href="../../node_modules/codemirror/addon/scroll/simplescrollbars.css" />
<link rel="stylesheet" href="../../node_modules/codemirror/addon/dialog/dialog.css" />
<link rel="stylesheet" href="../../node_modules/codemirror/addon/hint/show-hint.css" />
<link rel="stylesheet" href="./main.css" />
</head>
<body>
<div class="container">
<div class="titlebar">
<div class="draggable"></div>
<div class="icon">
<div class="inner"></div>
</div>
<div class="title">CSS Editor</div>
<div class="flex-spacer"></div>
<div class="controls">
<button title="Toggle always on top" id="toggleaot">P</button>
<button title="Close CSS Editor" id="closeeditor">X</button>
</div>
</div>
<div id="spinner"><div class="valign">Loading Please Wait...</div></div>
<div class="editor" id="editor">
</div>
<div class="tools">
<div class="flex-row">
<button id="btnSave">Save</button>
<button id="btnUpdate">Update</button>
<div class="flex-spacer"></div>
<div id="chkboxLiveUpdate"><input type="checkbox"><span>Live Update</span></div>
</div>
</div>
</div>
<script>const $ = require('../../node_modules/jquery/dist/jquery.min.js');</script>
<script src="../../node_modules/codemirror/lib/codemirror.js"></script>
<script src="../../node_modules/codemirror/mode/css/css.js"></script>
<script src="../../node_modules/codemirror/addon/scroll/simplescrollbars.js"></script>
<script src="../../node_modules/codemirror/addon/search/search.js"></script>
<script src="../../node_modules/codemirror/addon/search/searchcursor.js"></script>
<script src="../../node_modules/codemirror/addon/search/jump-to-line.js"></script>
<script src="../../node_modules/codemirror/addon/dialog/dialog.js"></script>
<script src="../../node_modules/codemirror/addon/hint/show-hint.js"></script>
<script src="../../node_modules/codemirror/addon/hint/css-hint.js"></script>
<script src="./main.js"></script>
</body>
</html>

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