Comments and fix tooltip arrow positioning

This commit is contained in:
Samuel Elliott 2018-03-20 23:24:31 +00:00
parent 994faf94d6
commit b4bd9e9c7b
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
43 changed files with 706 additions and 417 deletions

View File

@ -91,9 +91,9 @@ export default class Content {
config: this.settings.strip().settings,
data: this.data
});
this.settings.setSaved();
this.settings.setSaved();
} catch (err) {
Logger.err(this.name, ['Failed to save configuration', err]);
Logger.err(this.name, ['Failed to save configuration', err]);
throw err;
}
}

View File

@ -16,6 +16,12 @@ export default class {
return true;
}
/**
* Inserts or updates data in the database.
* @param {Object} args The record to find
* @param {Object} data The new record
* @return {Promise}
*/
static async insertOrUpdate(args, data) {
try {
return ClientIPC.send('bd-dba', { action: 'update', args, data });
@ -24,6 +30,11 @@ export default class {
}
}
/**
* Finds data in the database.
* @param {Object} args The record to find
* @return {Promise}
*/
static async find(args) {
try {
return ClientIPC.send('bd-dba', { action: 'find', args });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,9 @@ import Updater from './updater';
*/
export default class {
/**
* An array of modules.
*/
static get modules() {
return this._modules ? this._modules : (this._modules = [
new ProfileBadges(),
@ -28,6 +31,10 @@ export default class {
]);
}
/**
* Initializes all modules.
* @return {Promise}
*/
static async initModules() {
for (let module of this.modules) {
try {

View File

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

View File

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

View File

@ -76,12 +76,12 @@ export default class extends ContentManager {
static async loadPlugin(paths, configs, info, main, dependencies, permissions) {
if (permissions && permissions.length > 0) {
for (let perm of permissions) {
console.log(`Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
Logger.log(this.moduleName, `Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
}
try {
const allowed = await Modals.permissions(`${info.name} wants to:`, info.name, permissions).promise;
} catch (err) {
return null;
return;
}
}
@ -98,7 +98,7 @@ export default class extends ContentManager {
}
}
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor, deps);
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info, paths.contentPath), Vendor, deps);
if (!(plugin.prototype instanceof Plugin))
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,9 @@ export default class {
this.checkForUpdates = this.checkForUpdates.bind(this);
}
/**
* The interval to wait before checking for updates.
*/
get interval() {
return 60 * 1000 * 30;
}
@ -30,34 +33,51 @@ export default class {
this.updateInterval = setInterval(this.checkForUpdates, this.interval);
}
/**
* Installs an update.
* TODO
*/
update() {
// TODO
this.updatesAvailable = false;
Events.emit('update-check-end');
}
/**
* Checks for updates.
* @return {Promise}
*/
checkForUpdates() {
if (this.updatesAvailable) return;
Events.emit('update-check-start');
Logger.info('Updater', 'Checking for updates');
$.ajax({
type: 'GET',
url: 'https://rawgit.com/JsSucks/BetterDiscordApp/master/package.json',
cache: false,
success: e => {
try {
Events.emit('update-check-end');
Logger.info('Updater',
`Latest Version: ${e.version} - Current Version: ${Globals.version}`);
if (e.version !== Globals.version) {
this.updatesAvailable = true;
Events.emit('updates-available');
return new Promise((resolve, reject) => {
if (this.updatesAvailable) return resolve(true);
Events.emit('update-check-start');
Logger.info('Updater', 'Checking for updates');
$.ajax({
type: 'GET',
url: 'https://rawgit.com/JsSucks/BetterDiscordApp/master/package.json',
cache: false,
success: e => {
try {
Events.emit('update-check-end');
Logger.info('Updater', `Latest Version: ${e.version} - Current Version: ${Globals.getObject('version')}`);
if (e.version !== Globals.getObject('version')) {
this.updatesAvailable = true;
Events.emit('updates-available');
resolve(true);
}
resolve(false);
} catch (err) {
Events.emit('update-check-fail', err);
reject(err);
}
} catch (err) {
},
fail: err => {
Events.emit('update-check-fail', err);
reject(err);
}
},
fail: e => Events.emit('update-check-fail', e)
});
});
}

View File

@ -16,22 +16,21 @@ export { jQuery as $ };
export default class {
static get jQuery() {
return jQuery;
}
/**
* jQuery
*/
static get jQuery() { return jQuery }
static get $() { return this.jQuery }
static get $() {
return this.jQuery;
}
static get lodash() {
return lodash;
}
static get _() {
return this.lodash;
}
/**
* Lodash
*/
static get lodash() { return lodash }
static get _() { return this.lodash }
/**
* Moment
*/
static get moment() {
return WebpackModules.getModuleByName('Moment');
}

View File

@ -51,6 +51,8 @@ const KnownModules = {
React: Filters.byProperties(['createElement', 'cloneElement']),
ReactDOM: Filters.byProperties(['render', 'findDOMNode']),
Events: Filters.byPrototypeFields(['setMaxListeners', 'emit']),
/* Guild Info, Stores, and Utilities */
GuildStore: Filters.byProperties(['getGuild']),
SortedGuildStore: Filters.byProperties(['getSortedGuilds']),
@ -207,37 +209,37 @@ const KnownModules = {
export class WebpackModules {
/**
* Finds a module using a filter function.
* @param {Function} filter A function to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
static getModule(filter, first = true) {
const modules = this.getAllModules();
const rm = [];
for (let index in modules) {
if (!modules.hasOwnProperty(index)) continue;
const module = modules[index];
const { exports } = module;
let foundModule = null;
/**
* Finds a module using a filter function.
* @param {Function} filter A function to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
static getModule(filter, first = true) {
const modules = this.getAllModules();
const rm = [];
for (let index in modules) {
if (!modules.hasOwnProperty(index)) continue;
const module = modules[index];
const { exports } = module;
let foundModule = null;
if (!exports) continue;
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
if (filter(exports)) foundModule = exports;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
return first || rm.length == 0 ? undefined : rm;
}
if (!exports) continue;
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
if (filter(exports)) foundModule = exports;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
return first || rm.length == 0 ? undefined : rm;
}
/**
* Finds a module by it's name.
* @param {String} name The name of the module
* @param {Function} fallback A function to use to filter modules if not finding a known module
* @return {Any}
*/
/**
* Finds a module by it's name.
* @param {String} name The name of the module
* @param {Function} fallback A function to use to filter modules if not finding a known module
* @return {Any}
*/
static getModuleByName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name];
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
@ -246,48 +248,48 @@ export class WebpackModules {
return module ? Cache[name] = module : undefined;
}
/**
* Finds a module by it's display name.
* @param {String} name The display name of the module
* @return {Any}
*/
/**
* Finds a module by it's display name.
* @param {String} name The display name of the module
* @return {Any}
*/
static getModuleByDisplayName(name) {
return this.getModule(Filters.byDisplayName(name), true);
}
/**
* Finds a module using it's code.
* @param {RegEx} regex A regular expression to use to filter modules
* @param {Boolean} first Whether to return the only the first matching module
* @return {Any}
*/
/**
* Finds a module using it's code.
* @param {RegEx} regex A regular expression to use to filter modules
* @param {Boolean} first Whether to return the only the first matching module
* @return {Any}
*/
static getModuleByRegex(regex, first = true) {
return this.getModule(Filters.byCode(regex), first);
}
/**
* Finds a module using properties on it's prototype.
* @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
/**
* Finds a module using properties on it's prototype.
* @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
static getModuleByPrototypes(prototypes, first = true) {
return this.getModule(Filters.byPrototypeFields(prototypes), first);
}
/**
* Finds a module using it's own properties.
* @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
/**
* Finds a module using it's own properties.
* @param {Array} props Properties to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @return {Any}
*/
static getModuleByProps(props, first = true) {
return this.getModule(Filters.byProperties(props), first);
}
/**
* Discord's __webpack_require__ function.
*/
/**
* Discord's __webpack_require__ function.
*/
static get require() {
if (this._require) return this._require;
const id = 'bd-webpackmodules';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,17 +20,17 @@ bd-tooltips {
word-wrap: break-word;
z-index: 9001;
margin-bottom: 10px;
}
.bd-tooltip:after {
border: 5px solid transparent;
content: " ";
height: 0;
pointer-events: none;
width: 0;
border-top-color: #000;
left: 50%;
margin-left: -5px;
position: absolute;
top: 100%;
.bd-tooltip-arrow {
border: 5px solid transparent;
content: " ";
height: 0;
pointer-events: none;
width: 0;
border-top-color: #000;
left: 50%;
margin-left: -5px;
position: absolute;
top: 100%;
}
}

View File

@ -8,37 +8,14 @@
* LICENSE file in the root directory of this source tree.
*/
import { Events, WebpackModules, EventListener, ReactComponents, Renderer } from 'modules';
import { Events, WebpackModules, EventListener, DiscordApi, ReactComponents, Renderer } from 'modules';
import { ClientLogger as Logger } from 'common';
import Reflection from './reflection';
import DOM from './dom';
import VueInjector from './vueinjector';
import EditedTimeStamp from './components/common/EditedTimestamp.vue';
import Autocomplete from './components/common/Autocomplete.vue';
class TempApi {
static get currentGuildId() {
try {
return WebpackModules.getModuleByName('SelectedGuildStore').getGuildId();
} catch (err) {
return 0;
}
}
static get currentChannelId() {
try {
return WebpackModules.getModuleByName('SelectedChannelStore').getChannelId();
} catch (err) {
return 0;
}
}
static get currentUserId() {
try {
return WebpackModules.getModuleByName('UserStore').getCurrentUser().id;
} catch (err) {
return 0;
}
}
}
export default class extends EventListener {
constructor(args) {
@ -54,22 +31,19 @@ export default class extends EventListener {
}
get eventBindings() {
return [{ id: 'gkh:keyup', callback: this.injectAutocomplete }];
/*
return [
{ id: 'server-switch', callback: this.manipAll },
{ id: 'channel-switch', callback: this.manipAll },
{ id: 'discord:MESSAGE_CREATE', callback: this.markupInjector },
{ id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector },
// { id: 'server-switch', callback: this.manipAll },
// { id: 'channel-switch', callback: this.manipAll },
// { id: 'discord:MESSAGE_CREATE', callback: this.markupInjector },
// { id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector },
{ id: 'gkh:keyup', callback: this.injectAutocomplete }
];
*/
}
manipAll() {
try {
this.appMount.setAttribute('guild-id', TempApi.currentGuildId);
this.appMount.setAttribute('channel-id', TempApi.currentChannelId);
this.appMount.setAttribute('guild-id', DiscordApi.currentGuild.id);
this.appMount.setAttribute('channel-id', DiscordApi.currentChannel.id);
this.setIds();
this.makeMutable();
} catch (err) {
@ -172,14 +146,14 @@ export default class extends EventListener {
const userTest = Reflection(msgGroup).prop('user');
if (!userTest) return;
msgGroup.setAttribute('data-author-id', userTest.id);
if (userTest.id === TempApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
if (userTest.id === DiscordApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
return;
}
msg.setAttribute('data-message-id', messageid);
const msgGroup = msg.closest('.message-group');
if (!msgGroup) return;
msgGroup.setAttribute('data-author-id', authorid);
if (authorid === TempApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
if (authorid === DiscordApi.currentUser.id) msgGroup.setAttribute('data-currentuser', true);
}
setUserId(user) {
@ -187,7 +161,7 @@ export default class extends EventListener {
const userid = Reflection(user).prop('user.id');
if (!userid) return;
user.setAttribute('data-user-id', userid);
const currentUser = userid === TempApi.currentUserId;
const currentUser = userid === DiscordApi.currentUser.id;
if (currentUser) user.setAttribute('data-currentuser', true);
Events.emit('ui:useridset', user);
}
@ -218,4 +192,5 @@ export default class extends EventListener {
template: '<Autocomplete :initial="initial" />'
});
}
}

View File

@ -28,7 +28,14 @@ const BdMenuItems = new class {
this.add({category: 'External', contentid: 'themes', text: 'Themes'});
}
/**
* Adds an item to the menu.
* @param {Object} item The item to add to the menu
* @return {Object}
*/
add(item) {
if (this.items.includes(item)) return item;
item.id = items++;
item.contentid = item.contentid || (items++ + '');
item.active = false;
@ -39,6 +46,13 @@ const BdMenuItems = new class {
return item;
}
/**
* Adds a settings set to the menu.
* @param {String} category The category to display this item under
* @param {SettingsSet} set The settings set to display when this item is active
* @param {String} text The text to display in the menu (optional)
* @return {Object} The item that was added
*/
addSettingsSet(category, set, text) {
return this.add({
category, set,
@ -46,12 +60,23 @@ const BdMenuItems = new class {
});
}
/**
* Adds a Vue component to the menu.
* @param {String} category The category to display this item under
* @param {String} text The text to display in the menu
* @param {Object} component The Vue component to display when this item is active
* @return {Object} The item that was added
*/
addVueComponent(category, text, component) {
return this.add({
category, text, component
});
}
/**
* Removes an item from the menu.
* @param {Object} item The item to remove from the menu
*/
remove(item) {
Utils.removeFromArray(this.items, item);
}

View File

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

View File

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

View File

@ -131,7 +131,7 @@
this.timeout = setTimeout(() => {
this.animating = false;
this.lastActiveIndex = -1;
this.timeout = null;
this.timeout = null;
}, 400);
},
openGithub() {

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@
<script>
import { shell } from 'electron';
import { ClientIPC } from 'common';
import { ClientIPC, ClientLogger as Logger } from 'common';
import Combokeys from 'combokeys';
import CombokeysRecord from 'combokeys/plugins/record';
@ -65,7 +65,7 @@
this.active = false;
this.recordingValue = undefined;
this.setting.value = sequence.join(' ');
console.log('keypress', sequence);
Logger.log('Keybind', ['Recorded sequence', sequence]);
},
getDisplayString(value) {
if (!value) return;

View File

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

View File

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

View File

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

View File

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

View File

@ -12,17 +12,14 @@ import Vue from './vue';
export default class {
static inject(root, bdnode, components, template, replaceRoot) {
if(!replaceRoot) bdnode.appendTo(root);
return new Vue({
el: replaceRoot ? root : bdnode.element,
components,
template
});
}
static _inject(root, options, bdnode) {
/**
* 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 to
* @return {Vue}
*/
static inject(root, options, bdnode) {
if(bdnode) bdnode.appendTo(root);
const vue = new Vue(options);

View File

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