Merge pull request #184 from samuelthomas2774/refactor

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

View File

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

34
.gitignore vendored
View File

@ -1,27 +1,11 @@
.idea/* # Generated files
*.name
devjs/.idea/devjs.iml
*.bak
*.bak.*
*.xpi
Firefox/data/js/jquery-2.1.4.min.js
*.dev.*
/nbproject/private/
node_modules node_modules
.sass-cache dist
/*.jiiks etc
Installers/dotNet/bin/
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
release release
tests/log.txt
# User data
tests/data
user.config.json

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -402,7 +402,7 @@ export default class DiscordApi {
static get currentChannel() { static get currentChannel() {
const channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId()); const channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId());
return channel.isPrivate ? new PrivateChannel(channel) : new GuildChannel(channel); if (channel) return channel.isPrivate() ? new PrivateChannel(channel) : new GuildChannel(channel);
} }
static get currentUser() { static get currentUser() {

View File

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

View File

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

View File

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

View File

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

View File

@ -8,17 +8,16 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* /**
Base Module that every non-static module should extend * Base Module that every non-static module should extend
*/ */
export default class Module { export default class Module {
constructor(args) { constructor(args) {
this.__ = { this.__ = {
state: args || {}, state: args || {},
args args
} };
this.setState = this.setState.bind(this); this.setState = this.setState.bind(this);
this.initialize(); this.initialize();
} }
@ -38,7 +37,6 @@ export default class Module {
set args(t) { } set args(t) { }
get args() { return this.__.args; } get args() { return this.__.args; }
set state(state) { return this.__.state = state; } set state(state) { return this.__.state = state; }
get state() { return this.__.state; } get state() { return this.__.state; }

View File

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

View File

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

View File

@ -12,7 +12,9 @@ import { WebpackModules } from './webpackmodules';
import { ClientLogger as Logger, Utils } from 'common'; import { ClientLogger as Logger, Utils } from 'common';
export class Patcher { export class Patcher {
static get patches() { return this._patches || (this._patches = {}) } static get patches() { return this._patches || (this._patches = {}) }
static getPatchesByCaller(id) { static getPatchesByCaller(id) {
const patches = []; const patches = [];
for (const patch in this.patches) { for (const patch in this.patches) {
@ -22,16 +24,21 @@ export class Patcher {
} }
return patches; return patches;
} }
static unpatchAll(patches) { static unpatchAll(patches) {
if (typeof patches === 'string')
patches = this.getPatchesByCaller(patches);
for (const patch of patches) { for (const patch of patches) {
for (const child of patch.children) { for (const child of patch.children) {
child.unpatch(); child.unpatch();
} }
} }
} }
static resolveModule(module) { static resolveModule(module) {
if (module instanceof Function || (module instanceof Object && !(module instanceof Array))) return module; if (module instanceof Function || (module instanceof Object && !(module instanceof Array))) return module;
if ('string' === typeof module) return WebpackModules.getModuleByName(module); if (typeof module === 'string') return WebpackModules.getModuleByName(module);
if (module instanceof Array) return WebpackModules.getModuleByProps(module); if (module instanceof Array) return WebpackModules.getModuleByProps(module);
return null; return null;
} }
@ -99,10 +106,12 @@ export class Patcher {
static before() { return this.pushChildPatch(...arguments, 'before') } static before() { return this.pushChildPatch(...arguments, 'before') }
static after() { return this.pushChildPatch(...arguments, 'after') } static after() { return this.pushChildPatch(...arguments, 'after') }
static instead() { return this.pushChildPatch(...arguments, 'instead') } static instead() { return this.pushChildPatch(...arguments, 'instead') }
static pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, type = 'after') { static pushChildPatch(caller, unresolvedModule, functionName, callback, displayName, type = 'after') {
const module = this.resolveModule(unresolvedModule); const module = this.resolveModule(unresolvedModule);
if (!module || !module[functionName] || !(module[functionName] instanceof Function)) return null; if (!module || !module[functionName] || !(module[functionName] instanceof Function)) return null;
displayName = 'string' === typeof unresolvedModule ? unresolvedModule : displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name; displayName = typeof unresolvedModule === 'string' ? unresolvedModule :
displayName || module.displayName || module.name || module.constructor.displayName || module.constructor.name;
const patchId = `${displayName}:${functionName}:${caller}`; const patchId = `${displayName}:${functionName}:${caller}`;
const patch = this.patches[patchId] || this.pushPatch(caller, patchId, module, functionName); const patch = this.patches[patchId] || this.pushPatch(caller, patchId, module, functionName);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,70 @@
/**
* BetterDiscord Updater View Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<SettingsWrapper headertext="Updates">
<div class="bd-flex bd-flex-col bd-updaterview">
<div v-if="error" class="bd-form-item">
<h5 style="margin-bottom: 10px;">Error installing updates</h5>
<div class="bd-err bd-pre-wrap"><div class="bd-pre">{{ error.formatted }}</div></div>
<div class="bd-form-divider"></div>
</div>
<template v-if="updatesAvailable">
<p>Version {{ newVersion }} is available. You are currently running version {{ currentVersion }}.</p>
<FormButton :onClick="install" :loading="updating">Install</FormButton>
</template>
<template v-else>
<p>You're all up to date!</p>
</template>
</div>
</SettingsWrapper>
</template>
<script>
import { Globals, Updater } from 'modules';
import { ClientLogger as Logger } from 'common';
import SettingsWrapper from './SettingsWrapper.vue';
import { FormButton } from '../common';
export default {
data() {
return {
currentVersion: Globals.version,
updating: false,
updater: Updater
};
},
components: {
SettingsWrapper,
FormButton
},
computed: {
updatesAvailable() {
return this.updater.updatesAvailable;
},
newVersion() {
return this.updater.latestVersion;
},
error() {
return this.updater.error;
}
},
methods: {
async install() {
this.updating = true;
try {
await this.updater.update();
} catch (err) {}
this.updating = false;
}
}
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
/** /**
* BetterDiscord VUE Injector Module * BetterDiscord Vue Injector Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved. * All rights reserved.
* https://betterdiscord.net * https://betterdiscord.net
@ -12,14 +12,20 @@ import Vue from './vue';
export default class { 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({ const vue = new Vue(options);
el: replaceRoot ? root : bdnode.element,
components, vue.$mount(bdnode ? bdnode.element : root);
template return vue;
});
} }
} }

View File

@ -1,6 +1,5 @@
const const path = require('path');
path = require('path'), const webpack = require('webpack');
webpack = require('webpack');
const jsLoader = { const jsLoader = {
test: /\.(js|jsx)$/, test: /\.(js|jsx)$/,
@ -9,18 +8,18 @@ const jsLoader = {
query: { query: {
presets: ['react'] presets: ['react']
} }
} };
const vueLoader = { const vueLoader = {
test: /\.(vue)$/, test: /\.(vue)$/,
loader: 'vue-loader' loader: 'vue-loader'
} };
const scssLoader = { const scssLoader = {
test: /\.scss$/, test: /\.scss$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: ['css-loader', 'sass-loader'] loader: ['css-loader', 'sass-loader']
} };
module.exports = { module.exports = {
entry: './src/index.js', entry: './src/index.js',
@ -32,9 +31,11 @@ module.exports = {
loaders: [jsLoader, vueLoader, scssLoader] loaders: [jsLoader, vueLoader, scssLoader]
}, },
externals: { externals: {
'electron': 'window.require("electron")', electron: 'window.require("electron")',
'fs': 'window.require("fs")', fs: 'window.require("fs")',
'path': 'window.require("path")' path: 'window.require("path")',
node_utils: 'window.require("util")',
sparkplug: 'require("../../core/dist/sparkplug")'
}, },
resolve: { resolve: {
alias: { alias: {
@ -49,15 +50,10 @@ module.exports = {
path.resolve('src', 'structs'), path.resolve('src', 'structs'),
path.resolve('src', 'builtin') path.resolve('src', 'builtin')
] ]
}
/* resolve: {
alias: {
'momentjs': 'vendor/moment.min.js'
}, },
modules: [ node: {
path.resolve('./node_modules'), process: false,
path.resolve(__dirname, '..'), __filename: false,
path.resolve(__dirname, '..', 'node_modules') __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. * LICENSE file in the root directory of this source tree.
*/ */
const { ipcRenderer, ipcMain } = require('electron'); import { ipcRenderer } from 'electron';
export class ClientIPC { const callbacks = new WeakMap();
static on(channel, cb) {
ipcRenderer.on(channel, (event, message) => cb(event, message)); const ClientIPC = new class ClientIPC {
constructor() {
this.on('ping', () => 'pong', true);
} }
static async send(channel, message) { /**
* Adds an IPC event listener.
* @param {String} channel The channel to listen on
* @param {Function} callback A function that will be called when a message is received
* @param {Boolean} reply Whether to automatically reply to the message with the callback's return value
* @return {Promise}
*/
on(channel, callback, reply) {
channel = channel.startsWith('bd-') ? channel : `bd-${channel}`; channel = channel.startsWith('bd-') ? channel : `bd-${channel}`;
const __eid = Date.now().toString();
ipcRenderer.send(channel, Object.assign(message ? message : {}, { __eid })); const boundCallback = async (event, args) => {
return new Promise((resolve, reject) => { const ipcevent = new BDIpcEvent(event, args);
ipcRenderer.once(__eid, (event, arg) => { try {
if (arg.err) { const r = callback(ipcevent, ipcevent.message);
return reject(arg.err); if (reply) ipcevent.reply(await r);
} } catch (err) {
resolve(arg); 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 { class BDIpcEvent {
constructor(event, args) { constructor(event, args) {
this.args = args; this.args = args;
this.ipcEvent = event; this.ipcEvent = event;
this.replied = false;
} }
bindings() { bindings() {
this.send = this.send.bind(this);
this.reply = this.reply.bind(this); this.reply = this.reply.bind(this);
} }
send(message) { /**
this.ipcEvent.sender.send(this.args.__eid, message); * Sends a message back to the message's sender.
* @param {Any} message Data to send to this message's sender
*/
get send() { return this.reply }
reply(message, error) {
if (this.replied)
throw {message: 'This message has already been replied to.'};
this.replied = true;
return ClientIPC.send(this.eid, message, error);
} }
reply(message) { reject(err) {
this.send(message); return this.reply(err, true);
}
} }
export class CoreIPC { get message() {
static on(channel, cb) { return this.args.message;
ipcMain.on(channel, (event, args) => cb(new BDIpcEvent(event, args))); }
get error() {
return this.args.error;
}
get eid() {
return this.args.eid;
} }
} }

View File

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

View File

@ -9,47 +9,10 @@
*/ */
import { Vendor } from 'modules'; import { Vendor } from 'modules';
import { FileUtils } from './utils';
import node_utils from 'node_utils';
const logs = []; export const logLevels = {
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 {
'log': 'log', 'log': 'log',
'warn': 'warn', 'warn': 'warn',
'err': 'error', 'err': 'error',
@ -58,10 +21,48 @@ export class ClientLogger {
'dbg': 'debug', 'dbg': 'debug',
'info': 'info' '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) { static parseLevel(level) {
return this.levels.hasOwnProperty(level) ? this.levels[level] : 'log'; return logLevels.hasOwnProperty(level) ? logLevels[level] : 'log';
}
static get timestamp() {
return Vendor.moment().format('DD/MM/YY hh:mm:ss');
} }
} }
export const ClientLogger = new Logger();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,17 +8,16 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
/* /**
Base Module that every non-static module should extend * Base Module that every non-static module should extend.
*/ */
class Module { class Module {
constructor(args) { constructor(args) {
this.__ = { this.__ = {
state: args, state: args,
args args
} };
this.init(); this.init();
} }

View File

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

View File

@ -1,5 +1,17 @@
/**
* BetterDiscord Sparkplug
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* This file is evaluated in the renderer process!
*/
(() => { (() => {
if (window.__bd && window.__bd.ignited) return; if (module.exports.bd) return;
console.log('[BetterDiscord|Sparkplug]'); console.log('[BetterDiscord|Sparkplug]');
@ -7,12 +19,12 @@
if (!ls) console.warn('[BetterDiscord|Sparkplug] Failed to hook localStorage :('); if (!ls) console.warn('[BetterDiscord|Sparkplug] Failed to hook localStorage :(');
const wsOrig = window.WebSocket; const wsOrig = window.WebSocket;
window.__bd = { const bd = module.exports.bd = {
localStorage: ls, localStorage: ls,
wsHook: null, wsHook: null,
wsOrig, wsOrig,
ignited: true ignited: true
} };
class WSHook extends window.WebSocket { class WSHook extends window.WebSocket {
@ -25,14 +37,14 @@
console.info(`[BetterDiscord|WebSocket Proxy] new WebSocket detected, url: ${url}`); console.info(`[BetterDiscord|WebSocket Proxy] new WebSocket detected, url: ${url}`);
if (!url.includes('gateway.discord.gg')) return; if (!url.includes('gateway.discord.gg')) return;
if (window.__bd.setWS) { if (bd.setWS) {
window.__bd.setWS(this); bd.setWS(this);
console.info(`[BetterDiscord|WebSocket Proxy] WebSocket sent to instance`); console.info(`[BetterDiscord|WebSocket Proxy] WebSocket sent to instance`);
return; return;
} }
console.info(`[BetterDiscord|WebSocket Proxy] WebSocket stored to __bd['wsHook']`); console.info(`[BetterDiscord|WebSocket Proxy] WebSocket stored to bd.wsHook`);
window.__bd.wsHook = this; bd.wsHook = this;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,16 +2,17 @@ const
path = require('path'), path = require('path'),
webpack = require('webpack'), webpack = require('webpack'),
HtmlWebpackPlugin = require('html-webpack-plugin'); HtmlWebpackPlugin = require('html-webpack-plugin');
const vueLoader = { const vueLoader = {
test: /\.(vue)$/, test: /\.(vue)$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: 'vue-loader' loader: 'vue-loader'
} };
const scssLoader = { const scssLoader = {
test: /\.(css|scss)$/, test: /\.(css|scss)$/,
loader: ['css-loader', 'sass-loader'] loader: ['css-loader', 'sass-loader']
} };
module.exports = { module.exports = {
entry: './src/index.js', entry: './src/index.js',

1732
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/Jiiks/BetterDiscordApp.git" "url": "https://github.com/JsSucks/BetterDiscordApp.git"
}, },
"private": false, "private": false,
"dependencies": { "dependencies": {
@ -38,6 +38,7 @@
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-babel": "^7.0.0", "gulp-babel": "^7.0.0",
"gulp-copy": "^1.1.0", "gulp-copy": "^1.1.0",
"gulp-inject-string": "^1.1.1",
"gulp-merge": "^0.1.1", "gulp-merge": "^0.1.1",
"gulp-npm-copy-deps": "^1.0.2", "gulp-npm-copy-deps": "^1.0.2",
"gulp-plumber": "^1.2.0", "gulp-plumber": "^1.2.0",
@ -46,11 +47,10 @@
"html-webpack-plugin": "^3.0.6", "html-webpack-plugin": "^3.0.6",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"nedb": "^1.8.0",
"node-gyp": "^3.6.2", "node-gyp": "^3.6.2",
"node-sass": "^4.7.2",
"pump": "^2.0.0", "pump": "^2.0.0",
"sass-loader": "^6.0.6", "sass-loader": "^6.0.6",
"uglifyjs-webpack-plugin": "^1.2.4",
"v-tooltip": "^2.0.0-rc.30", "v-tooltip": "^2.0.0-rc.30",
"vue": "^2.5.13", "vue": "^2.5.13",
"vue-codemirror": "^4.0.3", "vue-codemirror": "^4.0.3",
@ -73,6 +73,7 @@
"lint": "eslint -f unix client/src core/src csseditor/src", "lint": "eslint -f unix client/src core/src csseditor/src",
"test": "npm run build && npm run lint", "test": "npm run build && npm run lint",
"build_node-sass": "node scripts/build-node-sass.js", "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 "PLATFORM=win32"
set "ARCH=ia32" set "ARCH=ia32"
set "VER=53" set "VER=53"
set "VENDOR_PATH=..\node_modules\node-sass\vendor" set "VENDOR_PATH=.\node_modules\node-sass\vendor"
set "BUILD_PATH=..\node_modules\node-sass\build\Release\binding.node" set "BUILD_PATH=.\node_modules\node-sass\build\Release\binding.node"
echo Building %PLATFORM%-%ARCH% bindings 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 ( if exist %VENDOR_PATH%\%PLATFORM%-%ARCH%-%VER%\binding.node (
echo Deleting old %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" set "ARCH=x64"
echo Building %PLATFORM%-%ARCH% bindings 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 ( if exist %VENDOR_PATH%\%PLATFORM%-%ARCH%-%VER%\binding.node (
echo Deleting old %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