Merge branch 'master' into dom-attributes

This commit is contained in:
Alexei Stukov 2018-03-09 12:26:50 +02:00 committed by GitHub
commit ef19f4723e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 625 additions and 156 deletions

View File

@ -30,6 +30,12 @@
"hint": "Disconnect from voice server when Discord closes",
"value": false,
"disabled": true
},
{
"id": "menu-keybind",
"type": "keybind",
"text": "Menu keybind",
"value": "mod+b"
}
]
},
@ -45,6 +51,13 @@
"hint": "BetterDiscord developer mode",
"value": false,
"disabled": true
},
{
"id": "ignore-content-manager-errors",
"type": "bool",
"text": "Ignore content manager errors",
"hint": "Only when starting Discord. It gets annoying when you're reloading Discord often and have plugins that are meant to fail.",
"value": false
}
]
}

View File

@ -13,7 +13,7 @@ import BdCss from './styles/index.scss';
import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database } from 'modules';
import { ClientLogger as Logger, ClientIPC } from 'common';
import { EmoteModule } from 'builtin';
const ignoreExternal = true;
const ignoreExternal = false;
class BetterDiscord {
@ -32,6 +32,7 @@ class BetterDiscord {
window.emotes = EmoteModule;
window.dom = DOM;
EmoteModule.observe();
DOM.injectStyle(BdCss, 'bdmain');
Events.on('global-ready', this.globalReady.bind(this));
}
@ -41,25 +42,25 @@ class BetterDiscord {
await Database.init();
await Settings.loadSettings();
await ModuleManager.initModules();
Modals.showContentManagerErrors();
if (!ignoreExternal) {
await ExtModuleManager.loadAllModules(true);
await PluginManager.loadAllPlugins(true);
await ThemeManager.loadAllThemes(true);
}
Modals.showContentManagerErrors();
if (!Settings.get('core', 'advanced', 'ignore-content-manager-errors'))
Modals.showContentManagerErrors();
Events.emit('ready');
Events.emit('discord-ready');
} catch (err) {
console.log('FAILED TO LOAD!', err);
Logger.err('main', ['FAILED TO LOAD!', err]);
}
}
globalReady() {
BdUI.initUiEvents();
this.vueInstance = BdUI.injectUi();
(async () => {
this.init();
})();
this.init();
}
}

View File

@ -15,6 +15,9 @@ import Database from './database';
export default class Content {
constructor(internals) {
Utils.deepfreeze(internals.info);
Object.freeze(internals.paths);
this.__internals = internals;
this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
@ -170,3 +173,5 @@ export default class Content {
}
}
Object.freeze(Content.prototype);

View File

@ -10,15 +10,16 @@
import Content from './content';
import Globals from './globals';
import Database from './database';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import { Events } from 'modules';
import { SettingsSet, ErrorEvent } from 'structs';
import { Modals } from 'ui';
import Database from './database';
import path from 'path';
import Combokeys from 'combokeys';
/**
* Base class for external content managing
* Base class for managing external content
*/
export default class {
@ -209,10 +210,12 @@ export default class {
userConfig.config.setSaved();
for (let setting of userConfig.config.findSettings(() => true)) {
// This will load custom settings
// Setting the content's path on only the live config (and not the default config) ensures that custom settings will not be loaded on the default settings
setting.setContentPath(contentPath);
}
Utils.deepfreeze(defaultConfig);
Utils.deepfreeze(defaultConfig, object => object instanceof Combokeys);
const configs = {
defaultConfig,

View File

@ -0,0 +1,45 @@
/**
* BetterDiscord Events Wrapper Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const eventemitters = new WeakMap();
export default class EventsWrapper {
constructor(eventemitter) {
eventemitters.set(this, eventemitter);
}
get eventSubs() {
return this._eventSubs || (this._eventSubs = []);
}
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);
}
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);
this.eventSubs.splice(index, 1);
}
}
unsubscribeAll() {
for (let event of this.eventSubs) {
eventemitters.get(this).off(event.event, event.callback);
}
this.eventSubs.splice(0, this.eventSubs.length);
}
}

View File

@ -13,3 +13,4 @@ export { default as SocketProxy } from './socketproxy';
export { default as EventHook } from './eventhook';
export { default as Permissions } from './permissionmanager';
export { default as Database } from './database';
export { default as EventsWrapper } from './eventswrapper';

View File

@ -11,27 +11,27 @@
const PermissionMap = {
IDENTIFY: {
HEADER: 'Access your account information',
BODY: 'Allows :NAME: to read your account information(excluding user token)'
BODY: 'Allows :NAME: to read your account information (excluding user token).'
},
READ_MESSAGES: {
HEADER: 'Read all messages',
BODY: 'Allows :NAME: to read all messages accessible through your Discord account'
BODY: 'Allows :NAME: to read all messages accessible through your Discord account.'
},
SEND_MESSAGES: {
HEADER: 'Send messages',
BODY: 'Allows :NAME: to send messages on your behalf'
BODY: 'Allows :NAME: to send messages on your behalf.'
},
DELETE_MESSAGES: {
HEADER: 'Delete messages',
BODY: 'Allows :NAME: to delete messages on your behalf'
BODY: 'Allows :NAME: to delete messages on your behalf.'
},
EDIT_MESSAGES: {
HEADER: 'Edit messages',
BODY: 'Allows :NAME: to edit messages on your behalf'
BODY: 'Allows :NAME: to edit messages on your behalf.'
},
JOIN_SERVERS: {
HEADER: 'Join servers for you',
BODY: 'Allows :NAME: to join servers on your behalf'
BODY: 'Allows :NAME: to join servers on your behalf.'
}
}

View File

@ -14,50 +14,20 @@ import ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager';
import ThemeManager from './thememanager';
import Events from './events';
import EventsWrapper from './eventswrapper';
import WebpackModules from './webpackmodules';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { Modals, DOM } from 'ui';
import { BdMenuItems, Modals, DOM } from 'ui';
import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue';
class EventsWrapper {
constructor(eventemitter) {
this.__eventemitter = eventemitter;
}
get eventSubs() {
return this._eventSubs || (this._eventSubs = []);
}
subscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
this.eventSubs.push({
event,
callback
});
this.__eventemitter.on(event, callback);
}
unsubscribe(event, callback) {
for (let index of this.eventSubs) {
if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) return;
this.__eventemitter.off(event, this.eventSubs[index].callback);
this.eventSubs.splice(index, 1);
}
}
unsubscribeAll() {
for (let event of this.eventSubs) {
this.__eventemitter.off(event.event, event.callback);
}
this._eventSubs = [];
}
}
export default class PluginApi {
constructor(pluginInfo) {
this.pluginInfo = pluginInfo;
this.Events = new EventsWrapper(Events);
this._menuItems = undefined;
this._injectedStyles = undefined;
this._modalStack = undefined;
}
get plugin() {
@ -153,6 +123,54 @@ export default class PluginApi {
};
}
/**
* BdMenu
*/
get BdMenu() {
return {
BdMenuItems: this.BdMenuItems
};
}
/**
* BdMenuItems
*/
get menuItems() {
return this._menuItems || (this._menuItems = []);
}
addMenuItem(item) {
return BdMenuItems.add(item);
}
addMenuSettingsSet(category, set, text) {
const item = BdMenuItems.addSettingsSet(category, set, text);
return this.menuItems.push(item);
}
addMenuVueComponent(category, text, component) {
const item = BdMenuItems.addVueComponent(category, text, component);
return this.menuItems.push(item);
}
removeMenuItem(item) {
BdMenuItems.remove(item);
Utils.removeFromArray(this.menuItems, item);
}
removeAllMenuItems() {
for (let item of this.menuItems)
BdMenuItems.remove(item);
}
get BdMenuItems() {
return Object.defineProperty({
add: this.addMenuItem.bind(this),
addSettingsSet: this.addMenuSettingsSet.bind(this),
addVueComponent: this.addMenuVueComponent.bind(this),
remove: this.removeMenuItem.bind(this),
removeAll: this.removeAllMenuItems.bind(this)
}, 'items', {
get: () => this.menuItems
});
}
/**
* CssUtils
*/
@ -172,8 +190,8 @@ export default class PluginApi {
injectStyle(id, css) {
if (id && !css) css = id, id = undefined;
this.deleteStyle(id);
const styleid = `plugin-${this.getPlugin().id}-${id}`;
this.injectedStyles.push(styleid);
const styleid = `plugin-${this.plugin.id}-${id}`;
this.injectedStyles.push(id);
DOM.injectStyle(css, styleid);
}
async injectSass(id, scss, options) {
@ -183,7 +201,7 @@ export default class PluginApi {
this.injectStyle(id, css, options);
}
deleteStyle(id) {
const styleid = `plugin-${this.getPlugin().id}-${id}`;
const styleid = `plugin-${this.plugin.id}-${id}`;
this.injectedStyles.splice(this.injectedStyles.indexOf(styleid), 1);
DOM.deleteStyle(styleid);
}
@ -216,7 +234,6 @@ export default class PluginApi {
}
addModal(_modal, component) {
const modal = Modals.add(_modal, component);
modal.close = force => this.closeModal(modal, force);
modal.on('close', () => {
let index;
while ((index = this.modalStack.findIndex(m => m === modal)) > -1)
@ -245,16 +262,20 @@ export default class PluginApi {
return this.addModal(Modals.settings(settingsset, headertext, options));
}
get Modals() {
return Object.defineProperty(Object.defineProperty({
return Object.defineProperties({
add: this.addModal.bind(this),
close: this.closeModal.bind(this),
closeAll: this.closeAllModals.bind(this),
closeLast: this.closeLastModal.bind(this),
basic: this.basicModal.bind(this),
settings: this.settingsModal.bind(this)
}, 'stack', {
get: () => this.modalStack
}), 'baseComponent', {
get: () => this.baseModalComponent
}, {
stack: {
get: () => this.modalStack
},
baseComponent: {
get: () => this.baseModalComponent
}
});
}
@ -348,3 +369,8 @@ export default class PluginApi {
}
}
// Stop plugins from modifying the plugin API for all plugins
// Plugins can still modify their own plugin API object
Object.freeze(PluginApi);
Object.freeze(PluginApi.prototype);

View File

@ -99,6 +99,9 @@ export default class extends ContentManager {
}
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor, deps);
if (!(plugin.prototype instanceof Plugin))
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};
const instance = new plugin({
configs, info, main,
paths: {

View File

@ -18,7 +18,23 @@ import path from 'path';
export default new class Settings {
constructor() {
this.settings = [];
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}`);
Events.emit('setting-updated', event);
Events.emit(`setting-updated-${set.id}_${category.id}_${setting.id}`, event);
});
set.on('settings-updated', async (event) => {
await this.saveSettings();
Events.emit('settings-updated', event);
});
return set;
});
}
async loadSettings() {
@ -29,22 +45,12 @@ export default new class Settings {
const user_config = await FileUtils.readJsonFromFile(settingsPath);
const { settings, scss, css, css_editor_files, scss_error, css_editor_bounds } = user_config;
this.settings = defaultSettings.map(set => {
const newSet = new SettingsSet(set);
newSet.merge(settings.find(s => s.id === newSet.id));
newSet.setSaved();
newSet.on('setting-updated', event => {
const { category, setting, value, old_value } = event;
Logger.log('Settings', `${newSet.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`);
Events.emit('setting-updated', event);
Events.emit(`setting-updated-${newSet.id}_${category.id}_${setting.id}`, event);
});
newSet.on('settings-updated', async (event) => {
await this.saveSettings();
Events.emit('settings-updated', event);
});
return newSet;
});
for (let set of this.settings) {
const newSet = settings.find(s => s.id === set.id);
if (!newSet) continue;
set.merge(newSet);
set.setSaved();
}
CssEditor.setState(scss, css, css_editor_files, scss_error);
CssEditor.editor_bounds = css_editor_bounds || {};

View File

@ -46,7 +46,8 @@ export default class Theme extends Content {
/**
* This is called when the theme is enabled.
*/
onstart() {
async onstart() {
if (!this.css) await this.recompile();
DOM.injectTheme(this.css, this.id);
}

View File

@ -45,8 +45,10 @@ export default class ThemeManager extends ContentManager {
mainPath: paths.mainPath
}
});
if (!instance.css) instance.recompile();
else if (instance.enabled) instance.enable();
if (instance.enabled) {
instance.userConfig.enabled = false;
instance.enable();
}
return instance;
} catch (err) {
throw err;

View File

@ -17,27 +17,29 @@ import DropdownSetting from './types/dropdown';
import RadioSetting from './types/radio';
import SliderSetting from './types/slider';
import ColourSetting from './types/colour';
import KeybindSetting from './types/keybind';
import FileSetting from './types/file';
import ArraySetting from './types/array';
import CustomSetting from './types/custom';
export default class Setting {
constructor(args) {
constructor(args, ...merge) {
args = args.args || args;
if (args.type === 'color') args.type = 'colour';
if (args.type === 'bool') return new BoolSetting(args);
else if (args.type === 'text') return new StringSetting(args);
else if (args.type === 'number') return new NumberSetting(args);
else if (args.type === 'dropdown') return new DropdownSetting(args);
else if (args.type === 'radio') return new RadioSetting(args);
else if (args.type === 'slider') return new SliderSetting(args);
else if (args.type === 'colour') return new ColourSetting(args);
else if (args.type === 'file') return new FileSetting(args);
else if (args.type === 'array') return new ArraySetting(args);
else if (args.type === 'custom') return new CustomSetting(args);
if (args.type === 'bool') return new BoolSetting(args, ...merge);
else if (args.type === 'text') return new StringSetting(args, ...merge);
else if (args.type === 'number') return new NumberSetting(args, ...merge);
else if (args.type === 'dropdown') return new DropdownSetting(args, ...merge);
else if (args.type === 'radio') return new RadioSetting(args, ...merge);
else if (args.type === 'slider') return new SliderSetting(args, ...merge);
else if (args.type === 'colour') return new ColourSetting(args, ...merge);
else if (args.type === 'keybind') return new KeybindSetting(args, ...merge);
else if (args.type === 'file') return new FileSetting(args, ...merge);
else if (args.type === 'array') return new ArraySetting(args, ...merge);
else if (args.type === 'custom') return new CustomSetting(args, ...merge);
else throw {message: `Setting type ${args.type} unknown`};
}

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord Keybind Setting Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import Setting from './basesetting';
import Combokeys from 'combokeys';
export default class KeybindSetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
this.combokeys = new Combokeys(document);
this.combokeys.bind(this.value, event => this.emit('keybind-activated', event));
}
setValueHook() {
this.combokeys.reset();
this.combokeys.bind(this.value, event => this.emit('keybind-activated', event));
}
}

View File

@ -1,9 +1,10 @@
@import './main.scss';
@import './switches.scss';
@import './text.scss';
@import './files.scss';
@import './dropdowns.scss';
@import './radios.scss';
@import './sliders.scss';
@import './switches.scss';
@import './colourpickers.scss';
@import './keybinds.scss';
@import './arrays.scss';
@import './colourpicker.scss';

View File

@ -0,0 +1,52 @@
.bd-keybind {
padding: 10px;
display: flex;
// width: 180px;
margin-top: 10px;
background-color: rgba(0,0,0,.1);
border: 1px solid rgba(0,0,0,.3);
transition: border .15s ease;
border-radius: 3px;
box-sizing: border-box;
min-height: 40px;
.bd-keybind-selected {
flex: 1 1 auto;
color: #f6f6f7;
font-size: 14px;
}
&.bd-keybind-unset {
.bd-keybind-selected {
color: hsla(240,6%,97%,.3);
font-weight: 600;
}
}
.bd-button {
border-radius: 2px;
margin: -4px -4px -4px 10px;
padding: 2px 20px;
transition: background-color .2s ease-in-out, color .2s ease-in-out;
font-size: 14px;
font-weight: 500;
flex: 0 0 auto;
cursor: pointer;
}
&.bd-active {
border-color: $colerr;
animation: bd-keybind-pulse 1s infinite;
.bd-button {
color: $colerr;
background-color: rgba($colerr, .3);
}
}
}
@keyframes bd-keybind-pulse {
0% { box-shadow: 0 0 6px rgba(240,71,71,.3) }
50% { box-shadow: 0 0 10px rgba(240,71,71,.6) }
100% { box-shadow: 0 0 6px rgba(240,71,71,.3) }
}

View File

@ -1,12 +1,13 @@
.bd-setting-switch,
.bd-form-textinput,
.bd-form-textarea,
.bd-form-fileinput,
.bd-form-numberinput,
.bd-form-dropdown,
.bd-form-radio,
.bd-form-numberinput,
.bd-form-slider,
.bd-form-colourpicker,
.bd-setting-switch,
.bd-form-keybind,
.bd-form-fileinput,
.bd-form-settingsarray {
.bd-title {
display: flex;

61
client/src/ui/bdmenu.js Normal file
View File

@ -0,0 +1,61 @@
/**
* BetterDiscord Menu Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Utils } from 'common';
let items = 0;
const BdMenuItems = new class {
constructor() {
window.bdmenu = this;
this.items = [];
this.addSettingsSet('Internal', 'core', 'Core');
this.addSettingsSet('Internal', 'ui', 'UI');
this.addSettingsSet('Internal', 'emotes', 'Emotes');
this.add({category: 'Internal', contentid: 'css', text: 'CSS Editor'});
this.add({category: 'External', contentid: 'plugins', text: 'Plugins'});
this.add({category: 'External', contentid: 'themes', text: 'Themes'});
}
add(item) {
item.id = items++;
item.contentid = item.contentid || (items++ + '');
item.active = false;
item.hidden = item.hidden || false;
item._type = item._type || 'button';
this.items.push(item);
return item;
}
addSettingsSet(category, set, text) {
return this.add({
category, set,
text: text || set.text
});
}
addVueComponent(category, text, component) {
return this.add({
category, text, component
});
}
remove(item) {
Utils.removeFromArray(this.items, item);
}
};
export { BdMenuItems };

View File

@ -123,4 +123,5 @@ export default class {
return vueInstance;
}
}

View File

@ -16,7 +16,10 @@
<MiClose size="17"/>
<span class="bd-x-text">ESC</span>
</div>
<SidebarItem v-for="item in sidebarItems" :item="item" :key="item.id" :onClick="itemOnClick" />
<template v-for="(category, text) in sidebar">
<SidebarItem :item="{text, type: 'header'}" />
<SidebarItem v-for="item in category" :item="item" :key="item.id" :onClick="itemOnClick" />
</template>
</Sidebar>
<div slot="sidebarfooter" class="bd-info">
<span class="bd-vtext">v2.0.0a by Jiiks/JsSucks</span>
@ -31,19 +34,21 @@
</div>
</div>
<ContentColumn slot="content">
<div v-for="set in Settings.settings" v-if="!set.hidden && activeContent(set.id) || animatingContent(set.id)" :class="{active: activeContent(set.id), animating: animatingContent(set.id)}">
<SettingsWrapper :headertext="set.headertext">
<SettingsPanel :settings="set" :schemes="set.schemes" />
<div v-for="item in sidebarItems" v-if="activeContent(item.contentid) || animatingContent(item.contentid)" :class="{active: activeContent(item.contentid), animating: animatingContent(item.contentid)}">
<template v-if="item.component">
<component :is="item.component" :SettingsWrapper="SettingsWrapper" />
</template>
<SettingsWrapper v-if="typeof item.set === 'string'" :headertext="Settings.getSet(item.set).headertext">
<SettingsPanel :settings="Settings.getSet(item.set)" :schemes="Settings.getSet(item.set).schemes" />
</SettingsWrapper>
</div>
<div v-if="activeContent('css') || animatingContent('css')" :class="{active: activeContent('css'), animating: animatingContent('css')}">
<CssEditorView />
</div>
<div v-if="activeContent('plugins') || animatingContent('plugins')" :class="{active: activeContent('plugins'), animating: animatingContent('plugins')}">
<PluginsView />
</div>
<div v-if="activeContent('themes') || animatingContent('themes')" :class="{active: activeContent('themes'), animating: animatingContent('themes')}">
<ThemesView />
<SettingsWrapper v-else-if="item.set" :headertext="item.set.headertext">
<SettingsPanel :settings="item.set" :schemes="item.set.schemes" />
</SettingsWrapper>
<CssEditorView v-if="item.contentid === 'css'" />
<PluginsView v-if="item.contentid === 'plugins'" />
<ThemesView v-if="item.contentid === 'themes'" />
</div>
</ContentColumn>
</SidebarView>
@ -53,32 +58,22 @@
// Imports
import { shell } from 'electron';
import { Settings } from 'modules';
import { BdMenuItems } from 'ui';
import { SidebarView, Sidebar, SidebarItem, ContentColumn } from './sidebar';
import { SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView } from './bd';
import { SvgX, MiGithubCircle, MiWeb, MiClose, MiTwitterCircle } from './common';
// Constants
const sidebarItems = [
{ text: 'Internal', _type: 'header' },
{ id: 0, contentid: "core", text: 'Core', active: false, _type: 'button' },
{ id: 1, contentid: "ui", text: 'UI', active: false, _type: 'button' },
{ id: 2, contentid: "emotes", text: 'Emotes', active: false, _type: 'button' },
{ id: 3, contentid: "css", text: 'CSS Editor', active: false, _type: 'button' },
{ text: 'External', _type: 'header' },
{ id: 4, contentid: "plugins", text: 'Plugins', active: false, _type: 'button' },
{ id: 5, contentid: "themes", text: 'Themes', active: false, _type: 'button' }
];
export default {
data() {
return {
sidebarItems,
BdMenuItems,
activeIndex: -1,
lastActiveIndex: -1,
animating: false,
first: true,
Settings,
timeout: null
timeout: null,
SettingsWrapper
}
},
props: ['active', 'close'],
@ -87,6 +82,20 @@
SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView,
MiGithubCircle, MiWeb, MiClose, MiTwitterCircle
},
computed: {
sidebarItems() {
return this.BdMenuItems.items;
},
sidebar() {
const categories = {};
for (let item of this.sidebarItems) {
if (item.hidden) continue;
const category = categories[item.category] || (categories[item.category] = []);
category.push(item);
}
return categories;
}
},
methods: {
itemOnClick(id) {
if (this.animating || id === this.activeIndex) return;

View File

@ -20,7 +20,8 @@
</template>
<script>
// Imports
import { Events } from 'modules';
import { Events, Settings } from 'modules';
import { Modals } from 'ui';
import BdSettings from './BdSettings.vue';
export default {
@ -45,11 +46,9 @@
hideSettings() { this.active = false },
toggleSettings() { this.active = !this.active },
keyupListener(e) {
if (document.getElementsByClassName('bd-backdrop').length) return;
if (this.$refs.settings.activeIndex !== -1 && e.which === 27) return this.$refs.settings.closeContent();
if (this.active && e.which === 27) return this.hideSettings();
if (!e.metaKey && !e.ctrlKey || e.key !== 'b') return;
this.toggleSettings();
if (Modals.stack.length || !this.active || e.which !== 27) return;
if (this.$refs.settings.activeIndex !== -1) this.$refs.settings.closeContent();
else this.hideSettings();
e.stopImmediatePropagation();
}
},
@ -69,6 +68,9 @@
Events.on('update-check-end', e => this.updating = 1);
Events.on('updates-available', e => this.updating = 2);
window.addEventListener('keyup', this.keyupListener);
const menuKeybind = Settings.getSetting('core', 'default', 'menu-keybind');
menuKeybind.on('keybind-activated', () => this.active = !this.active);
},
destroyed() {
window.removeEventListener('keyup', this.keyupListener);

View File

@ -71,7 +71,6 @@
// TODO Display error if plugin fails to start/stop
try {
await plugin.enabled ? PluginManager.stopPlugin(plugin) : PluginManager.startPlugin(plugin);
this.$forceUpdate();
} catch (err) {
console.log(err);
}
@ -79,7 +78,6 @@
async reloadPlugin(plugin) {
try {
await PluginManager.reloadPlugin(plugin);
this.$forceUpdate();
} catch (err) {
console.log(err);
}
@ -88,7 +86,6 @@
try {
if (unload) await PluginManager.unloadPlugin(plugin);
else await PluginManager.deletePlugin(plugin);
this.$forceUpdate();
} catch (err) {
console.error(err);
}

View File

@ -71,7 +71,6 @@
// TODO Display error if theme fails to enable/disable
try {
await theme.enabled ? ThemeManager.disableTheme(theme) : ThemeManager.enableTheme(theme);
this.$forceUpdate();
} catch (err) {
console.log(err);
}
@ -80,7 +79,6 @@
try {
if (reload) await ThemeManager.reloadTheme(theme);
else await theme.recompile();
this.$forceUpdate();
} catch (err) {
console.log(err);
}
@ -89,7 +87,6 @@
try {
if (unload) await ThemeManager.unloadTheme(theme);
else await ThemeManager.deleteTheme(theme);
this.$forceUpdate();
} catch (err) {
console.error(err);
}

View File

@ -0,0 +1,78 @@
/**
* BetterDiscord Setting Keybind Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-form-keybind">
<div class="bd-form-keybind-details">
<div class="bd-title">
<h3>{{ setting.text }}</h3>
</div>
<div class="bd-hint">{{ setting.hint }}</div>
</div>
<div class="bd-keybind" :class="{'bd-active': active, 'bd-disabled': setting.disabled, 'bd-keybind-unset': !setting.value}">
<div class="bd-keybind-selected">{{ selected || 'No Keybind Set' }}</div>
<button class="bd-button" v-tooltip="`Click to record a new keybind sequence${setting.value ? ' (shift + click to delete the sequence)' : ''}`" @click="$event.shiftKey ? deleteKeybind() : toggleActive(); $event.target.blur()">{{ active ? 'Stop Recording' : setting.value ? 'Edit Keybind' : 'Record Keybind' }}</button>
</div>
</div>
</template>
<script>
import { shell } from 'electron';
import { ClientIPC } from 'common';
import Combokeys from 'combokeys';
import CombokeysRecord from 'combokeys/plugins/record';
const combokeys = new Combokeys(document);
CombokeysRecord(combokeys);
const process = window.require('process');
const modifierKey = process.platform === 'darwin' ? 'meta' : 'ctrl';
export default {
props: ['setting'],
data() {
return {
active: false
};
},
computed: {
selected() {
return this.getDisplayString(this.setting.value);
}
},
watch: {
active(active) {
if (active) combokeys.record(this.recorded);
}
},
methods: {
toggleActive() {
if (this.setting.disabled) return;
this.active = !this.active;
},
deleteKeybind() {
this.setting.value = '';
},
recorded(sequence) {
if (!this.active) return;
this.active = false;
this.recordingValue = undefined;
this.setting.value = sequence.join(' ');
console.log('keypress', sequence);
},
getDisplayString(value) {
if (!value) return;
return value.split(' ').map(pattern => {
return pattern.toUpperCase().replace(/\+/g, ' + ').replace(/mod/gi, modifierKey).replace(/meta/gi, 'Cmd').replace(/ctrl/gi, 'Ctrl').replace(/alt/gi, 'Alt').replace(/shift/gi, 'Shift');
}).join(', ');
}
}
}
</script>

View File

@ -10,17 +10,18 @@
<template>
<div class="bd-form-item" :class="{'bd-form-item-changed': setting.changed, 'bd-disabled': disabled, 'bd-form-item-noheader': !setting.text, 'bd-form-item-fullwidth': setting.fullwidth}">
<BoolSetting v-if="setting.type === 'bool'" :setting="setting" :change="change"/>
<DropdownSetting v-if="setting.type === 'dropdown'" :setting="setting" :change="change"/>
<NumberSetting v-if="setting.type === 'number'" :setting="setting" :change="change"/>
<RadioSetting v-if="setting.type === 'radio'" :setting="setting" :change="change"/>
<StringSetting v-if="setting.type === 'text' && !setting.multiline" :setting="setting" :change="change"/>
<MultilineTextSetting v-if="setting.type === 'text' && setting.multiline" :setting="setting" :change="change"/>
<SliderSetting v-if="setting.type === 'slider'" :setting="setting" :change="change"/>
<FileSetting v-if="setting.type === 'file'" :setting="setting" :change="change"/>
<BoolSetting v-if="setting.type === 'bool'" :setting="setting" :change="change" />
<DropdownSetting v-if="setting.type === 'dropdown'" :setting="setting" :change="change" />
<NumberSetting v-if="setting.type === 'number'" :setting="setting" :change="change" />
<RadioSetting v-if="setting.type === 'radio'" :setting="setting" :change="change" />
<StringSetting v-if="setting.type === 'text' && !setting.multiline" :setting="setting" :change="change" />
<MultilineTextSetting v-if="setting.type === 'text' && setting.multiline" :setting="setting" />
<SliderSetting v-if="setting.type === 'slider'" :setting="setting" :change="change" />
<ColourSetting v-if="setting.type === 'colour'" :setting="setting" :change="change" />
<KeybindSetting v-if="setting.type === 'keybind'" :setting="setting" />
<FileSetting v-if="setting.type === 'file'" :setting="setting" :change="change" />
<ArraySetting v-if="setting.type === 'array'" :setting="setting" :change="change" />
<CustomSetting v-if="setting.type === 'custom'" :setting="setting" :change="change" />
<ColourSetting v-if="setting.type === 'colour'" :setting="setting" :change="change"/>
<div class="bd-form-divider"></div>
</div>
</template>
@ -33,10 +34,11 @@
import StringSetting from './String.vue';
import MultilineTextSetting from './Multiline.vue';
import SliderSetting from './Slider.vue';
import ColourSetting from './Colour.vue';
import KeybindSetting from './Keybind.vue';
import FileSetting from './File.vue';
import ArraySetting from './Array.vue';
import CustomSetting from './Custom.vue';
import ColourSetting from './Colour.vue';
export default {
props: [
@ -50,10 +52,11 @@
StringSetting,
MultilineTextSetting,
SliderSetting,
ColourSetting,
KeybindSetting,
FileSetting,
ArraySetting,
CustomSetting,
ColourSetting
CustomSetting
},
computed: {
changed() {

View File

@ -1,6 +1,7 @@
export { default as DOM } from './dom';
export { default as BdUI } from './bdui';
export { default as VueInjector } from './vueinjector';
export * from './bdmenu';
export { default as Modals } from './modals';
export { default as ProfileBadges } from './profilebadges';
export { default as Reflection } from './reflection';

View File

@ -10,10 +10,15 @@
import { ClientLogger as Logger } from './logger';
const patchedFunctions = new WeakMap();
export class PatchedFunction {
constructor(object, methodName, replaceOriginal = true) {
if (object[methodName].__monkeyPatch)
return object[methodName].__monkeyPatch;
if (patchedFunctions.has(object[methodName])) {
const patchedFunction = patchedFunctions.get(object[methodName]);
if (replaceOriginal) patchedFunction.replaceOriginal();
return patchedFunction;
}
this.object = object;
this.methodName = methodName;
@ -25,7 +30,9 @@ export class PatchedFunction {
this.replace = function(...args) {
patchedFunction.call(this, arguments);
};
this.replace.__monkeyPatch = this;
patchedFunctions.set(object[methodName], this);
patchedFunctions.set(this.replace, this);
if (replaceOriginal)
this.replaceOriginal();

View File

@ -146,12 +146,14 @@ export class Utils {
return value;
}
static deepfreeze(object) {
static deepfreeze(object, exclude) {
if (exclude && exclude(object)) return;
if (typeof object === 'object' && object !== null) {
const properties = Object.getOwnPropertyNames(object);
for (let property of properties) {
this.deepfreeze(object[property]);
this.deepfreeze(object[property], exclude);
}
Object.freeze(object);
@ -159,6 +161,13 @@ export class Utils {
return object;
}
static removeFromArray(array, item) {
let index;
while ((index = array.indexOf(item)) > -1)
array.splice(index, 1);
return array;
}
}
export class FileUtils {

66
package-lock.json generated
View File

@ -1323,6 +1323,15 @@
"integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
"dev": true
},
"binary-search-tree": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/binary-search-tree/-/binary-search-tree-0.2.5.tgz",
"integrity": "sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=",
"dev": true,
"requires": {
"underscore": "1.4.4"
}
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
@ -2023,6 +2032,12 @@
"delayed-stream": "1.0.0"
}
},
"combokeys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/combokeys/-/combokeys-3.0.0.tgz",
"integrity": "sha1-lVxZo5Wa9A0mhGq2/DxoJEjnVy4=",
"dev": true
},
"commander": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
@ -5652,6 +5667,12 @@
"integrity": "sha1-YSKJv7PCIOGGpYEYYY1b6MG6sCE=",
"dev": true
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
"dev": true
},
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@ -6293,6 +6314,15 @@
"type-check": "0.3.2"
}
},
"lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
"dev": true,
"requires": {
"immediate": "3.0.6"
}
},
"liftoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz",
@ -6347,6 +6377,15 @@
"json5": "0.5.1"
}
},
"localforage": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.6.0.tgz",
"integrity": "sha1-iwBZvus4dcSBJChsp/2/I9UrjJc=",
"dev": true,
"requires": {
"lie": "3.1.1"
}
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
@ -6979,6 +7018,27 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true
},
"nedb": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/nedb/-/nedb-1.8.0.tgz",
"integrity": "sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=",
"dev": true,
"requires": {
"async": "0.2.10",
"binary-search-tree": "0.2.5",
"localforage": "1.6.0",
"mkdirp": "0.5.1",
"underscore": "1.4.4"
},
"dependencies": {
"async": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=",
"dev": true
}
}
},
"neo-async": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.0.tgz",
@ -10569,6 +10629,12 @@
"integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=",
"dev": true
},
"underscore": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
"integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=",
"dev": true
},
"union-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",

View File

@ -22,6 +22,7 @@
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"codemirror": "^5.23.0",
"combokeys": "^3.0.0",
"css-loader": "^0.28.9",
"electron": "^1.6.15",
"electron-rebuild": "^1.7.3",

View File

@ -77,6 +77,13 @@
]
}
]
},
{
"id": "keybind-1",
"type": "keybind",
"value": "mod+.",
"text": "Test Keybind Setting 1",
"hint": "Test Keybind Setting Hint 1"
}
]
}

View File

@ -1,5 +1,7 @@
module.exports = (Plugin, { Logger, Settings }) => class extends Plugin {
module.exports = (Plugin, { Logger, Settings, Modals, BdMenu: { BdMenuItems }, Api }) => class extends Plugin {
async onstart() {
this.keybindEvent = this.keybindEvent.bind(this);
// Some array event examples
const arraySetting = this.settings.getSetting('default', 'array-1');
Logger.log('Array setting', arraySetting);
@ -7,8 +9,15 @@ module.exports = (Plugin, { Logger, Settings }) => class extends Plugin {
arraySetting.on('item-updated', event => Logger.log('Item', event.item, 'of the array setting was updated', event));
arraySetting.on('item-removed', event => Logger.log('Item', event.item, 'removed from the array setting'));
// Create a new settings set and show it in a modal
const set = Settings.createSet({});
// Keybind setting examples
const keybindSetting = this.settings.getSetting('default', 'keybind-1');
Logger.log('Keybind setting', keybindSetting);
keybindSetting.on('keybind-activated', this.keybindEvent);
// Create a new settings set and add it to the menu
const set = Settings.createSet({
text: this.name
});
const category = await set.addCategory({ id: 'default' });
const setting = await category.addSetting({
@ -33,6 +42,37 @@ module.exports = (Plugin, { Logger, Settings }) => class extends Plugin {
set.setSaved();
})
set.showModal('Custom settings panel');
const setting2 = await category.addSetting({
id: 'setting-2',
type: 'text',
text: 'Enter some text',
fullwidth: true
});
setting2.on('setting-updated', event => Logger.log('Setting 2 was changed to', event.value));
this.menuItem = BdMenuItems.addSettingsSet('Plugins', set, 'Plugin 4');
this.menuItem2 = BdMenuItems.addVueComponent('Plugins', 'Also Plugin 4', {
template: `<component :is="SettingsWrapper" :headertext="plugin.name + ' custom menu panel'">
<p style="margin-top: 0; color: #f6f6f7;">Test</p>
</component>`,
props: ['SettingsWrapper'],
data() { return {
Api, plugin: Api.plugin
}; }
});
}
onstop() {
const keybindSetting = this.settings.getSetting('default', 'keybind-1');
keybindSetting.off('keybind-activated', this.keybindEvent);
BdMenuItems.removeAll();
}
keybindEvent(event) {
Logger.log('Keybind pressed', event);
Modals.basic('Example Plugin 4', 'Test keybind activated.');
}
};