Merge pull request #187 from samuelthomas2774/tweaks-and-emote-favourites

Tweaks and favourite emotes
This commit is contained in:
Alexei Stukov 2018-04-02 16:25:45 -02:00 committed by GitHub
commit bbd12a0381
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 1804 additions and 1006 deletions

View File

@ -1,33 +0,0 @@
/**
* BetterDiscord Autocomplete 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-autocomplete">
<div class="bd-autocomplete-inner">
<div class="bd-autocompleteRow">
<div class="bd-autocompleteSelector">
<div class="bd-autocompleteTitle">
Emotes Matching:
<strong>Kappa</strong>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import EmoteModule from '../../../builtin/Emotemodule.js';
export default {
props: ['title', 'emotes'],
beforeMount() {
console.log(EmoteModule);
}
}
</script>

View File

@ -1,8 +0,0 @@
<template>
<span class="edited" v-tooltip="ets">(edited)</span>
</template>
<script>
export default {
props: ['ets']
}
</script>

View File

@ -1,20 +1,38 @@
<template>
<span class="bd-emotewrapper" v-tooltip="name">
<img class="bd-emote" :src="src" :alt="`;${name};`"/>
<span class="bd-emotewrapper" :class="{'bd-emote-favourite': favourite, 'bd-emote-no-wrapper': !hasWrapper}" v-tooltip="name" :data-emote-name="name">
<img class="bd-emote" :src="src" :alt="`;${name};`" />
<div class="bd-emote-favourite-button" :class="{'bd-active': favourite}" @click="toggleFavourite">
<MiStar :size="16" />
</div>
</span>
</template>
<script>
import { ClientLogger as Logger } from 'common';
import EmoteModule from './EmoteModule';
import { MiStar } from '../ui/components/common';
export default {
components: {
MiStar
},
props: ['src', 'name', 'hasWrapper'],
data() {
return {
favourite: false
EmoteModule
};
},
props: ['src', 'name'],
methods: {},
beforeMount() {
// Check favourite state
computed: {
favourite() {
return EmoteModule.isFavourite(this.name);
}
},
methods: {
async toggleFavourite() {
await EmoteModule.setFavourite(this.name, !this.favourite);
Logger.log('EmoteComponent', `Set emote ${this.name} as ${this.favourite ? '' : 'un'}favourite`);
}
}
}
</script>

View File

@ -8,42 +8,101 @@
* LICENSE file in the root directory of this source tree.
*/
import { Events, Globals, WebpackModules, ReactComponents, MonkeyPatch } from 'modules';
import { Events, Settings, Globals, WebpackModules, ReactComponents, MonkeyPatch } from 'modules';
import { DOM, VueInjector, Reflection } from 'ui';
import { FileUtils, ClientLogger as Logger } from 'common';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import EmoteComponent from './EmoteComponent.vue';
let emotes = null;
const emotesEnabled = true;
const enforceWrapperFrom = (new Date('2018-05-01')).valueOf();
export default class {
export default new class EmoteModule {
static get searchCache() {
constructor() {
this.emotes = new Map();
this.favourite_emotes = [];
}
async init() {
this.enabledSetting = Settings.getSetting('emotes', 'default', 'enable');
this.enabledSetting.on('setting-updated', event => {
// Rerender all messages (or if we're disabling emotes, those that have emotes)
for (const message of document.querySelectorAll(event.value ? '.message' : '.bd-emote-outer')) {
Reflection(event.value ? message : message.closest('.message')).forceUpdate();
}
});
const dataPath = Globals.getPath('data');
try {
const emotes = await FileUtils.readJsonFromFile(path.join(dataPath, 'emotes.json'));
for (let emote of emotes) {
const uri = emote.type === 2 ? 'https://cdn.betterttv.net/emote/:id/1x' : emote.type === 1 ? 'https://cdn.frankerfacez.com/emoticon/:id/1' : 'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0';
emote.name = emote.id;
emote.src = uri.replace(':id', emote.value.id || emote.value);
this.emotes.set(emote.id, emote);
}
} catch (err) {
Logger.err('EmoteModule', [`Failed to load emote data. Make sure you've downloaded the emote data and placed it in ${dataPath}:`, err]);
return;
}
try {
await this.observe();
} catch (err) {
Logger.err('EmoteModule', ['Error patching Message', err]);
}
}
/**
* Sets an emote as favourite.
* @param {String} emote The name of the emote
* @param {Boolean} favourite The new favourite state
* @param {Boolean} save Whether to save settings
* @return {Promise}
*/
setFavourite(emote, favourite, save = true) {
if (favourite && !this.favourite_emotes.includes(emote)) this.favourite_emotes.push(emote);
if (!favourite) Utils.removeFromArray(this.favourite_emotes, emote);
if (save) return Settings.saveSettings();
}
addFavourite(emote, save = true) {
return this.setFavourite(emote, true, save);
}
removeFavourite(emote, save = true) {
return this.setFavourite(emote, false, save);
}
isFavourite(emote) {
return this.favourite_emotes.includes(emote);
}
get searchCache() {
return this._searchCache || (this._searchCache = {});
}
static get emoteDb() {
return emotes;
}
static get React() {
get React() {
return WebpackModules.getModuleByName('React');
}
static get ReactDOM() {
get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM');
}
static processMarkup(markup) {
if (!emotesEnabled) return markup; // TODO Get it from setttings
processMarkup(markup, timestamp) {
if (!this.enabledSetting.value) return markup;
timestamp = timestamp.valueOf();
const allowNoWrapper = timestamp < enforceWrapperFrom;
const newMarkup = [];
for (const child of markup) {
if ('string' !== typeof child) {
if (typeof child !== 'string') {
newMarkup.push(child);
continue;
}
if (!this.testWord(child)) {
if (!this.testWord(child) && !allowNoWrapper) {
newMarkup.push(child);
continue;
}
@ -51,13 +110,18 @@ export default class {
if (!words) continue;
let text = null;
for (const [wordIndex, word] of words.entries()) {
const isEmote = this.isEmote(word);
if (isEmote) {
const emote = this.getEmote(word);
if (emote) {
if (text !== null) {
newMarkup.push(text);
text = null;
}
newMarkup.push(this.React.createElement('span', { className: 'bd-emote-outer', 'data-bdemote-name': isEmote.name, 'data-bdemote-src': isEmote.src }));
newMarkup.push(this.React.createElement('span', {
className: 'bd-emote-outer',
'data-bdemote-name': emote.name,
'data-bdemote-src': emote.src,
'data-has-wrapper': /;[\w]+;/gmi.test(word)
}));
continue;
}
if (text === null) {
@ -73,13 +137,13 @@ export default class {
return newMarkup;
}
static testWord(word) {
if (!/;[\w]+;/gmi.test(word)) return false;
return true;
testWord(word) {
return !/;[\w]+;/gmi.test(word);
}
static injectAll() {
if (!emotesEnabled) return;
injectAll() {
if (!this.enabledSetting.value) return;
const all = document.getElementsByClassName('bd-emote-outer');
for (const ec of all) {
if (ec.children.length) continue;
@ -87,7 +151,7 @@ export default class {
}
}
static findByProp(obj, what, value) {
findByProp(obj, what, value) {
if (obj.hasOwnProperty(what) && obj[what] === value) return obj;
if (obj.props && !obj.children) return this.findByProp(obj.props, what, value);
if (!obj.children || !obj.children.length) return null;
@ -99,99 +163,81 @@ export default class {
return null;
}
static async observe() {
const dataPath = Globals.getPath('data');
try {
emotes = await FileUtils.readJsonFromFile(path.join(dataPath, 'emotes.json'));
} catch (err) {
Logger.err('EmoteModule', [`Failed to load emote data. Make sure you've downloaded the emote data and placed it in ${dataPath}:`, err]);
return;
}
try {
const Message = await ReactComponents.getComponent('Message');
this.unpatchRender = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('render', (component, args, retVal) => {
try {
// First child has all the actual text content, second is the edited timestamp
const markup = this.findByProp(retVal, 'className', 'markup');
if (!markup) return;
markup.children[0] = this.processMarkup(markup.children[0]);
} catch (err) {
Logger.err('EmoteModule', err);
}
});
for (const message of document.querySelectorAll('.message')) {
Reflection(message).forceUpdate();
async observe() {
const Message = await ReactComponents.getComponent('Message');
this.unpatchRender = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('render', (component, args, retVal) => {
try {
// First child has all the actual text content, second is the edited timestamp
const markup = this.findByProp(retVal, 'className', 'markup');
if (!markup) return;
markup.children[0] = this.processMarkup(markup.children[0], component.props.message.editedTimestamp || component.props.message.timestamp);
} catch (err) {
Logger.err('EmoteModule', err);
}
this.injectAll();
this.unpatchMount = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidMount', component => {
const element = this.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectEmotes(element);
});
this.unpatchUpdate = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidUpdate', component => {
const element = this.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectEmotes(element);
});
} catch (err) {
Logger.err('EmoteModule', err);
});
for (const message of document.querySelectorAll('.message')) {
Reflection(message).forceUpdate();
}
this.injectAll();
this.unpatchMount = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidMount', component => {
const element = this.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectEmotes(element);
});
this.unpatchUpdate = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidUpdate', component => {
const element = this.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectEmotes(element);
});
}
static injectEmote(root) {
if (!emotesEnabled) return;
injectEmote(root) {
if (!this.enabledSetting.value) return;
while (root.firstChild) {
root.removeChild(root.firstChild);
}
const { bdemoteName, bdemoteSrc } = root.dataset;
const { bdemoteName, bdemoteSrc, hasWrapper } = root.dataset;
if (!bdemoteName || !bdemoteSrc) return;
VueInjector.inject(root, {
components: { EmoteComponent },
data: { src: bdemoteSrc, name: bdemoteName },
template: '<EmoteComponent :src="src" :name="name" />'
data: { src: bdemoteSrc, name: bdemoteName, hasWrapper },
template: '<EmoteComponent :src="src" :name="name" :hasWrapper="hasWrapper" />'
}, DOM.createElement('span'));
root.classList.add('bd-is-emote');
}
static injectEmotes(element) {
if (!emotesEnabled || !element) return;
injectEmotes(element) {
if (!this.enabledSetting.value || !element) return;
for (const beo of element.getElementsByClassName('bd-emote-outer')) this.injectEmote(beo);
}
static isEmote(word) {
if (!emotes) return null;
getEmote(word) {
const name = word.replace(/;/g, '');
const emote = emotes.find(emote => emote.id === name);
if (!emote) return null;
let { id, value } = emote;
if (value.id) value = value.id;
const uri = emote.type === 2 ? 'https://cdn.betterttv.net/emote/:id/1x' : emote.type === 1 ? 'https://cdn.frankerfacez.com/emoticon/:id/1' : 'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0';
return { name, src: uri.replace(':id', value) };
return this.emotes.get(name);
}
static filterTest() {
const re = new RegExp('Kappa', 'i');
const filtered = emotes.filter(emote => re.test(emote.id));
return filtered.slice(0, 10);
}
static filter(regex, limit, start = 0) {
filter(regex, limit, start = 0) {
const key = `${regex}:${limit}:${start}`;
if (this.searchCache.hasOwnProperty(key)) return this.searchCache[key];
let index = 0;
let startIndex = 0;
return this.searchCache[key] = emotes.filter(emote => {
if (index >= limit) return false;
const matching = this.searchCache[key] = [];
for (let emote of this.emotes.values()) {
if (index >= limit) break;
if (regex.test(emote.id)) {
if (startIndex < start) {
startIndex++;
return false;
continue;
}
index++;
return true;
matching.push(emote);
}
});
}
return matching;
}
}

View File

@ -5,7 +5,7 @@
"headertext": "Core Settings",
"settings": [
{
"category": "default",
"id": "default",
"settings": [
{
"id": "test-setting",
@ -40,8 +40,8 @@
]
},
{
"category": "advanced",
"category_name": "Advanced",
"id": "advanced",
"name": "Advanced",
"type": "drawer",
"settings": [
{
@ -74,7 +74,7 @@
"headertext": "UI Settings",
"settings": [
{
"category": "default",
"id": "default",
"settings": [
{
"id": "hide-button",
@ -92,14 +92,26 @@
"id": "emotes",
"text": "Emotes",
"headertext": "Emote Settings",
"settings": []
"settings": [
{
"id": "default",
"settings": [
{
"id": "enable",
"type": "bool",
"text": "Enable emotes",
"value": true
}
]
}
]
},
{
"id": "css",
"text": "CSS Editor",
"settings": [
{
"category": "default",
"id": "default",
"settings": [
{
"id": "live-update",
@ -123,8 +135,6 @@
"id": "security",
"text": "Security",
"headertext": "Security Settings",
"settings": [
]
"settings": []
}
]

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { DOM, BdUI, Modals, Reflection } from 'ui';
import { DOM, BdUI, BdMenu, Modals, Reflection } from 'ui';
import BdCss from './styles/index.scss';
import { Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, WebpackModules, Patcher, MonkeyPatch, ReactComponents, ReactAutoPatcher, DiscordApi } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
@ -26,7 +26,7 @@ class BetterDiscord {
Logger.log('main', 'BetterDiscord starting');
this._bd = {
DOM, BdUI, Modals, Reflection,
DOM, BdUI, BdMenu, Modals, Reflection,
Events, CssEditor, Globals, Settings, Database, Updater,
ModuleManager, PluginManager, ThemeManager, ExtModuleManager,
@ -76,7 +76,7 @@ class BetterDiscord {
Events.emit('ready');
Events.emit('discord-ready');
EmoteModule.observe();
EmoteModule.init();
} catch (err) {
Logger.err('main', ['FAILED TO LOAD!', err]);
}

View File

@ -12,16 +12,22 @@ import { Utils, FileUtils, ClientLogger as Logger, AsyncEventEmitter } from 'com
import { Modals } from 'ui';
import Database from './database';
export default class Content {
export default class Content extends AsyncEventEmitter {
constructor(internals) {
super();
internals.loaded = Date.now();
internals.started = undefined;
internals.stopped = undefined;
Utils.deepfreeze(internals.info);
Object.freeze(internals.paths);
this.__internals = internals;
this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
this.settings.on('settings-updated', event => this.events.emit('settings-updated', event));
this.settings.on('setting-updated', event => this.emit('setting-updated', event));
this.settings.on('settings-updated', event => this.emit('settings-updated', event));
this.settings.on('settings-updated', event => this.__settingsUpdated(event));
// Add hooks
@ -49,13 +55,15 @@ export default class Content {
get description() { return this.info.description }
get authors() { return this.info.authors }
get version() { return this.info.version }
get loadedTimestamp() { return this.__internals.loaded }
get startedTimestamp() { return this.__internals.started }
get stoppedTimestamp() { return this.__internals.stopped }
get contentPath() { return this.paths.contentPath }
get dirName() { return this.paths.dirName }
get enabled() { return this.userConfig.enabled }
get settings() { return this.userConfig.config }
get config() { return this.settings.categories }
get data() { return this.userConfig.data || (this.userConfig.data = {}) }
get events() { return this.EventEmitter || (this.EventEmitter = new AsyncEventEmitter()) }
/**
* Opens a settings modal for this content.
@ -110,6 +118,8 @@ export default class Content {
await this.emit('enable');
await this.emit('start');
this.__internals.started = Date.now();
this.__internals.stopped = undefined;
this.userConfig.enabled = true;
if (save) await this.saveConfiguration();
}
@ -124,48 +134,12 @@ export default class Content {
await this.emit('stop');
await this.emit('disable');
this.__internals.started = undefined;
this.__internals.stopped = Date.now();
this.userConfig.enabled = false;
if (save) await this.saveConfiguration();
}
/**
* Adds an event listener.
* @param {String} event The event to add the listener to
* @param {Function} callback The function to call when the event is emitted
*/
on(...args) {
return this.events.on(...args);
}
/**
* Adds an event listener that removes itself when called, therefore only being called once.
* @param {String} event The event to add the listener to
* @param {Function} callback The function to call when the event is emitted
* @return {Promise|undefined}
*/
once(...args) {
return this.events.once(...args);
}
/**
* Removes an event listener.
* @param {String} event The event to remove the listener from
* @param {Function} callback The bound callback (optional)
*/
off(...args) {
return this.events.removeListener(...args);
}
/**
* Emits an event.
* @param {String} event The event to emit
* @param {Any} data Data to be passed to listeners
* @return {Promise|undefined}
*/
emit(...args) {
return this.events.emit(...args);
}
}
Object.freeze(Content.prototype);

View File

@ -245,10 +245,9 @@ export default class {
if (!reload && this.getContentById(content.id))
throw {message: `A ${this.contentType} with the ID ${content.id} already exists.`};
if (reload) this.localContent[index] = content;
if (reload) this.localContent.splice(index, 1, content);
else this.localContent.push(content);
return content;
} catch (err) {
throw err;
}

View File

@ -23,14 +23,14 @@ export default class EventsWrapper {
get on() { return this.subscribe }
subscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
const boundCallback = () => callback.apply(this.bind, arguments);
const boundCallback = (...args) => callback.apply(this.bind, args);
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);
const boundCallback = (...args) => this.off(event, callback) && callback.apply(this.bind, args);
this.eventSubs.push({ event, callback, boundCallback });
eventemitters.get(this).on(event, boundCallback);
}
@ -50,4 +50,5 @@ export default class EventsWrapper {
}
this.eventSubs.splice(0, this.eventSubs.length);
}
}

View File

@ -17,6 +17,7 @@ export { default as Vendor } from './vendor';
export * from './webpackmodules';
export * from './patcher';
export * from './reactcomponents';
export { default as Module } from './module';
export { default as EventListener } from './eventlistener';
export { default as SocketProxy } from './socketproxy';
export { default as EventHook } from './eventhook';

View File

@ -45,7 +45,7 @@ export class Patcher {
static overrideFn(patch) {
return function () {
let retVal = null;
let retVal = undefined;
if (!patch.children) return patch.originalFunction.apply(this, arguments);
for (const superPatch of patch.children.filter(c => c.type === 'before')) {
try {

View File

@ -8,6 +8,9 @@
* LICENSE file in the root directory of this source tree.
*/
import { EmoteModule } from 'builtin';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { BdMenu, Modals, DOM, Reflection } from 'ui';
import { Utils, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
import Settings from './settings';
import ExtModuleManager from './extmodulemanager';
@ -16,8 +19,6 @@ import ThemeManager from './thememanager';
import Events from './events';
import EventsWrapper from './eventswrapper';
import { WebpackModules } from './webpackmodules';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { BdMenuItems, Modals, DOM, Reflection } from 'ui';
import DiscordApi from './discordapi';
import { ReactComponents } from './reactcomponents';
import { Patcher, MonkeyPatch } from './patcher';
@ -61,18 +62,15 @@ export default class PluginApi {
* Logger
*/
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),
err: this.loggerErr.bind(this),
warn: this.loggerWarn.bind(this),
info: this.loggerInfo.bind(this),
dbg: this.loggerDbg.bind(this)
log: (...message) => Logger.log(this.plugin.name, message),
error: (...message) => Logger.err(this.plugin.name, message),
err: (...message) => Logger.err(this.plugin.name, message),
warn: (...message) => Logger.warn(this.plugin.name, message),
info: (...message) => Logger.info(this.plugin.name, message),
debug: (...message) => Logger.dbg(this.plugin.name, message),
dbg: (...message) => Logger.dbg(this.plugin.name, message)
};
}
@ -82,15 +80,15 @@ export default class PluginApi {
get Utils() {
return {
overload: () => Utils.overload.apply(Utils, arguments),
monkeyPatch: () => Utils.monkeyPatch.apply(Utils, arguments),
monkeyPatchOnce: () => Utils.monkeyPatchOnce.apply(Utils, arguments),
compatibleMonkeyPatch: () => Utils.monkeyPatchOnce.apply(Utils, arguments),
tryParseJson: () => Utils.tryParseJson.apply(Utils, arguments),
toCamelCase: () => Utils.toCamelCase.apply(Utils, arguments),
compare: () => Utils.compare.apply(Utils, arguments),
deepclone: () => Utils.deepclone.apply(Utils, arguments),
deepfreeze: () => Utils.deepfreeze.apply(Utils, arguments)
overload: (...args) => Utils.overload.apply(Utils, args),
tryParseJson: (...args) => Utils.tryParseJson.apply(Utils, args),
toCamelCase: (...args) => Utils.toCamelCase.apply(Utils, args),
compare: (...args) => Utils.compare.apply(Utils, args),
deepclone: (...args) => Utils.deepclone.apply(Utils, args),
deepfreeze: (...args) => Utils.deepfreeze.apply(Utils, args),
removeFromArray: (...args) => Utils.removeFromArray.apply(Utils, args),
defineSoftGetter: (...args) => Utils.defineSoftGetter.apply(Utils, args),
until: (...args) => Utils.until.apply(Utils, args)
};
}
@ -138,6 +136,9 @@ export default class PluginApi {
get BdMenu() {
return {
open: BdMenu.open.bind(BdMenu),
close: BdMenu.close.bind(BdMenu),
items: this.BdMenuItems,
BdMenuItems: this.BdMenuItems
};
}
@ -150,23 +151,23 @@ export default class PluginApi {
return this._menuItems || (this._menuItems = []);
}
addMenuItem(item) {
return BdMenuItems.add(item);
return BdMenu.items.add(item);
}
addMenuSettingsSet(category, set, text) {
const item = BdMenuItems.addSettingsSet(category, set, text);
const item = BdMenu.items.addSettingsSet(category, set, text);
return this.menuItems.push(item);
}
addMenuVueComponent(category, text, component) {
const item = BdMenuItems.addVueComponent(category, text, component);
const item = BdMenu.items.addVueComponent(category, text, component);
return this.menuItems.push(item);
}
removeMenuItem(item) {
BdMenuItems.remove(item);
BdMenu.items.remove(item);
Utils.removeFromArray(this.menuItems, item);
}
removeAllMenuItems() {
for (let item of this.menuItems)
BdMenuItems.remove(item);
BdMenu.items.remove(item);
}
get BdMenuItems() {
return Object.defineProperty({
@ -288,6 +289,52 @@ export default class PluginApi {
});
}
/**
* Emotes
*/
get emotes() {
return EmoteModule.emotes;
}
get favourite_emotes() {
return EmoteModule.favourite_emotes;
}
setFavouriteEmote(emote, favourite) {
return EmoteModule.setFavourite(emote, favourite);
}
addFavouriteEmote(emote) {
return EmoteModule.addFavourite(emote);
}
removeFavouriteEmote(emote) {
return EmoteModule.addFavourite(emote);
}
isFavouriteEmote(emote) {
return EmoteModule.isFavourite(emote);
}
getEmote(emote) {
return EmoteModule.getEmote(emote);
}
filterEmotes(regex, limit, start = 0) {
return EmoteModule.filterEmotes(regex, limit, start);
}
get Emotes() {
return Object.defineProperties({
setFavourite: this.setFavouriteEmote.bind(this),
addFavourite: this.addFavouriteEmote.bind(this),
removeFavourite: this.removeFavouriteEmote.bind(this),
isFavourite: this.isFavouriteEmote.bind(this),
getEmote: this.getEmote.bind(this),
filter: this.filterEmotes.bind(this)
}, {
emotes: {
get: () => this.emotes
},
favourite_emotes: {
get: () => this.favourite_emotes
}
});
}
/**
* Plugins
*/
@ -355,14 +402,23 @@ export default class PluginApi {
getWebpackModuleByName(name, fallback) {
return WebpackModules.getModuleByName(name, fallback);
}
getWebpackModuleByRegex(regex, first = true) {
return WebpackModules.getModuleByRegex(regex, first);
getWebpackModuleByRegex(regex) {
return WebpackModules.getModuleByRegex(regex, true);
}
getWebpackModuleByProperties(props, first = true) {
return WebpackModules.getModuleByProps(props, first);
getWebpackModulesByRegex(regex) {
return WebpackModules.getModuleByRegex(regex, false);
}
getWebpackModuleByPrototypeFields(props, first = true) {
return WebpackModules.getModuleByPrototypes(props, first);
getWebpackModuleByProperties(...props) {
return WebpackModules.getModuleByProps(props, true);
}
getWebpackModuleByPrototypeFields(...props) {
return WebpackModules.getModuleByPrototypes(props, true);
}
getWebpackModulesByProperties(...props) {
return WebpackModules.getModuleByProps(props, false);
}
getWebpackModulesByPrototypeFields(...props) {
return WebpackModules.getModuleByPrototypes(props, false);
}
get WebpackModules() {
return Object.defineProperty({
@ -370,8 +426,11 @@ export default class PluginApi {
getModuleByName: this.getWebpackModuleByName.bind(this),
getModuleByDisplayName: this.getWebpackModuleByName.bind(this),
getModuleByRegex: this.getWebpackModuleByRegex.bind(this),
getModulesByRegex: this.getWebpackModulesByRegex.bind(this),
getModuleByProperties: this.getWebpackModuleByProperties.bind(this),
getModuleByPrototypeFields: this.getWebpackModuleByPrototypeFields.bind(this)
getModuleByPrototypeFields: this.getWebpackModuleByPrototypeFields.bind(this),
getModulesByProperties: this.getWebpackModulesByProperties.bind(this),
getModulesByPrototypeFields: this.getWebpackModulesByPrototypeFields.bind(this)
}, 'require', {
get: () => this.webpackRequire
});
@ -416,12 +475,13 @@ export default class PluginApi {
instead: this.patchInstead.bind(this),
pushChildPatch: this.pushChildPatch.bind(this),
unpatchAll: this.unpatchAll.bind(this),
monkeyPatch: this.monkeyPatch.bind(this)
}, 'patches', {
get: () => this.patches
});
}
get monkeyPatch() {
return module => MonkeyPatch(this.plugin.id, module);
return m => MonkeyPatch(this.plugin.id, m);
}
}

View File

@ -9,11 +9,12 @@
* LICENSE file in the root directory of this source tree.
*/
import { EmoteModule } from 'builtin';
import { Reflection } from 'ui';
import { ClientLogger as Logger } from 'common';
import { MonkeyPatch, Patcher } from './patcher';
import { WebpackModules, Filters } from './webpackmodules';
import DiscordApi from './discordapi';
import { EmoteModule } from 'builtin';
import { Reflection } from 'ui';
class Helpers {
static get plannedActions() {
@ -153,11 +154,17 @@ class Helpers {
return null;
}
static get React() {
return WebpackModules.getModuleByName('React');
}
static get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM');
}
}
export { Helpers as ReactHelpers };
class ReactComponent {
constructor(id, component, retVal) {
this._id = id;
@ -209,7 +216,7 @@ export class ReactComponents {
if (important) {
const importantInterval = setInterval(() => {
if (this.components.find(c => c.id === name)) {
console.info(`Important component ${name} already found`);
Logger.info('ReactComponents', `Important component ${name} already found`);
clearInterval(importantInterval);
return;
}
@ -218,11 +225,11 @@ export class ReactComponents {
const reflect = Reflection(select);
if (!reflect.component) {
clearInterval(importantInterval);
console.error(`FAILED TO GET IMPORTANT COMPONENT ${name} WITH REFLECTION FROM`, select);
Logger.error('ReactComponents', [`FAILED TO GET IMPORTANT COMPONENT ${name} WITH REFLECTION FROM`, select]);
return;
}
if (!reflect.component.displayName) reflect.component.displayName = name;
console.info(`Found important component ${name} with reflection.`);
Logger.info('ReactComponents', [`Found important component ${name} with reflection`, reflect]);
this.push(reflect.component);
clearInterval(importantInterval);
}, 50);
@ -233,7 +240,7 @@ export class ReactComponents {
listeners: []
});
return new Promise(resolve => {
this.listeners.find(l => l.id === name).listeners.push(c => resolve(c));
this.listeners.find(l => l.id === name).listeners.push(resolve);
});
}
@ -319,9 +326,10 @@ export class ReactAutoPatcher {
}
static async patchChannelMember() {
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', { selector: '.member.member-status' });
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', { selector: '.member-2FrNV0' });
this.unpatchChannelMemberRender = MonkeyPatch('BD:ReactComponents', this.ChannelMember.component.prototype).after('render', (component, args, retVal) => {
if (!retVal.props || !retVal.props.children || !retVal.props.children.length) return;
// Logger.log('ReactComponents', ['Rendering ChannelMember', component, args, retVal]);
if (!retVal.props || !retVal.props.children) return;
const user = Helpers.findProp(component, 'user');
if (!user) return;
retVal.props['data-user-id'] = user.id;
@ -359,7 +367,7 @@ export class ReactAutoPatcher {
}
static forceUpdate() {
for (const e of document.querySelectorAll('.message,.message-group,.guild,.containerDefault-7RImuF,.channel-members .member')) {
for (const e of document.querySelectorAll('.message, .message-group, .guild, .containerDefault-7RImuF, .channel-members .member-2FrNV0')) {
Reflection(e).forceUpdate();
}
}

View File

@ -8,8 +8,9 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { EmoteModule } from 'builtin';
import { SettingsSet, SettingUpdatedEvent } from 'structs';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import Globals from './globals';
import CssEditor from './csseditor';
@ -47,7 +48,7 @@ export default new class Settings {
const settingsPath = path.resolve(this.dataPath, 'user.settings.json');
const user_config = await FileUtils.readJsonFromFile(settingsPath);
const { settings, scss, css, css_editor_files, scss_error, css_editor_bounds } = user_config;
const { settings, scss, css, css_editor_files, scss_error, css_editor_bounds, favourite_emotes } = user_config;
for (let set of this.settings) {
const newSet = settings.find(s => s.id === set.id);
@ -58,6 +59,7 @@ export default new class Settings {
CssEditor.setState(scss, css, css_editor_files, scss_error);
CssEditor.editor_bounds = css_editor_bounds || {};
EmoteModule.favourite_emotes = favourite_emotes;
} catch (err) {
// There was an error loading settings
// This probably means that the user doesn't have any settings yet
@ -79,7 +81,8 @@ export default new class Settings {
css: CssEditor.css,
css_editor_files: CssEditor.files,
scss_error: CssEditor.error,
css_editor_bounds: CssEditor.editor_bounds
css_editor_bounds: CssEditor.editor_bounds,
favourite_emotes: EmoteModule.favourite_emotes
});
for (let set of this.settings) {

View File

@ -13,11 +13,18 @@ import BaseSetting from './types/basesetting';
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
export default class SettingsCategory {
export default class SettingsCategory extends AsyncEventEmitter {
constructor(args, ...merge) {
this.emitter = new AsyncEventEmitter();
this.args = args.args || args;
super();
if (typeof args === 'string')
args = {id: args};
this.args = args.args || args || {};
this.args.id = this.args.id || this.args.category || 'default';
this.args.name = this.args.name || this.args.category_name || this.id;
this.type = this.args.type;
this.args.settings = this.settings.map(setting => new Setting(setting));
@ -56,6 +63,10 @@ export default class SettingsCategory {
return this.name;
}
set name(value) {
this.args.name = value;
}
/**
* Category type
* Currently either "drawer", "static", or undefined.
@ -64,6 +75,13 @@ export default class SettingsCategory {
return this.args.type;
}
set type(value) {
if (!value) this.args.type = undefined;
else if (value === 'drawer' || value === 'static')
this.args.type = value;
else throw {message: `Invalid category type ${value}`};
}
/**
* An array of settings in this category.
*/
@ -276,8 +294,4 @@ export default class SettingsCategory {
}, ...merge);
}
on(...args) { return this.emitter.on(...args); }
off(...args) { return this.emitter.removeListener(...args); }
emit(...args) { return this.emitter.emit(...args); }
}

View File

@ -14,11 +14,18 @@ import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
import { Modals } from 'ui';
export default class SettingsSet {
export default class SettingsSet extends AsyncEventEmitter {
constructor(args, ...merge) {
this.emitter = new AsyncEventEmitter();
this.args = args.args || args;
super();
if (typeof args === 'string')
args = {id: args};
this.args = args.args || args || {};
this.args.id = this.args.id || undefined;
this.args.text = this.args.text || undefined;
this.args.headertext = this.args.headertext || undefined;
this.args.categories = this.categories.map(category => new SettingsCategory(category));
this.args.schemes = this.schemes.map(scheme => new SettingsScheme(scheme));
@ -54,6 +61,10 @@ export default class SettingsSet {
return this.args.text;
}
set text(value) {
this.args.text = value;
}
/**
* Text to be displayed with the set.
*/
@ -65,14 +76,6 @@ export default class SettingsSet {
this.args.headertext = headertext;
}
/**
* Whether this set should be displayed.
* Currently only used in the settings menu.
*/
get hidden() {
return this.args.hidden || false;
}
/**
* An array of SettingsCategory objects in this set.
*/
@ -450,8 +453,4 @@ export default class SettingsSet {
}, ...merge);
}
on(...args) { return this.emitter.on(...args); }
off(...args) { return this.emitter.removeListener(...args); }
emit(...args) { return this.emitter.emit(...args); }
}

View File

@ -12,11 +12,19 @@ import { ThemeManager } from 'modules';
import { Utils, AsyncEventEmitter } from 'common';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
export default class Setting {
export default class Setting extends AsyncEventEmitter {
constructor(args, ...merge) {
super();
this.args = args.args || args;
this.args.id = this.args.id || 'default';
this.args.text = this.args.text || undefined;
this.args.hint = this.args.hint || undefined;
this.args.path = this.args.path || undefined;
this.args.disabled = !!this.args.disabled;
this.args.fullwidth = !!this.args.fullwidth;
if (!this.args.hasOwnProperty('value'))
this.args.value = this.defaultValue;
if (!this.args.hasOwnProperty('saved_value'))
@ -26,7 +34,6 @@ export default class Setting {
this._merge(newSetting);
}
this.emitter = new AsyncEventEmitter();
this.changed = !Utils.compare(this.args.value, this.args.saved_value);
}
@ -70,6 +77,10 @@ export default class Setting {
return this.args.text;
}
set text(value) {
this.args.text = value;
}
/**
* Text to be displayed with the setting.
*/
@ -77,9 +88,14 @@ export default class Setting {
return this.args.hint;
}
set hint(value) {
this.args.hint = value;
}
/**
* The path of the plugin/theme this setting is part of.
* Used by settings of type "array", "custom" and "file".
* Use set/category/setting.setContentPath to change.
*/
get path() {
return this.args.path;
@ -93,6 +109,10 @@ export default class Setting {
return this.args.disabled || false;
}
set disabled(value) {
this.args.disabled = !!value;
}
/**
* Whether the setting should take the full width of the settings panel.
* This is only customisable in some setting types.
@ -101,6 +121,10 @@ export default class Setting {
return this.args.fullwidth || false;
}
set fullwidth(value) {
this.args.fullwidth = !!value;
}
/**
* Merges a setting into this setting without emitting events (and therefore synchronously).
* This only exists for use by the constructor and SettingsCategory.
@ -234,8 +258,4 @@ export default class Setting {
}
}
on(...args) { return this.emitter.on(...args); }
off(...args) { return this.emitter.removeListener(...args); }
emit(...args) { return this.emitter.emit(...args); }
}

View File

@ -1,7 +1,11 @@
.bd-profile-badges-wrap {
display: flex;
display: inline-flex;
margin-top: 8px;
flex: 1 1 auto;
+ [class*="profileBadges"] {
display: inline-flex;
}
}
.bd-profile-badges {

View File

@ -5,7 +5,6 @@
width: 70px;
height: 48px;
left: 0;
background: #202225;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 2px 0 rgba(0, 0, 0, 0.06);
opacity: 1;
@ -24,7 +23,6 @@
background-position: center;
width: 100%;
height: 100%;
z-index: 3001;
cursor: pointer;
filter: grayscale(100%);
opacity: 0.5;
@ -57,7 +55,7 @@
opacity: 1;
width: 130px;
height: 43px;
margin: 18px 0 17px 20px;
margin: 18px 0 17px 25px;
cursor: default;
}
}

View File

@ -4,13 +4,13 @@
flex-grow: 1;
background: transparent;
border-bottom: 1px solid rgba(114, 118, 126, 0.3);
padding: 10px 5px;
min-height: 150px;
color: #b9bbbe;
margin-top: 10px;
margin-top: 20px;
padding-bottom: 20px;
.bd-card-header {
padding-bottom: 5px;
margin-bottom: 15px;
display: flex;
flex-grow: 0;
font-weight: 700;
@ -37,20 +37,16 @@
.bd-card-description {
flex-grow: 1;
overflow-y: auto;
max-height: 60px;
min-height: 60px;
color: #8a8c90;
font-size: 12px;
font-weight: 600;
padding: 5px;
border-radius: 8px;
margin-top: 5px;
margin-bottom: 5px;
white-space: pre-wrap;
}
.bd-card-footer {
display: flex;
flex-grow: 1;
align-items: flex-end;
.bd-card-extra {
@ -61,9 +57,11 @@
}
.bd-controls {
margin-left: 15px;
.bd-button-group {
.bd-button {
fill: #FFF;
fill: #fff;
width: 30px;
height: 30px;
}
@ -82,3 +80,22 @@
}
}
}
.bd-content-author {
.bd-content-author-link {
cursor: pointer;
&:hover {
color: $coldimwhite;
}
}
}
.bd-content-author-links {
padding: 3px 5px;
.bd-popover-inner > :first-child {
display: flex;
flex-direction: row;
}
}

View File

@ -3,6 +3,7 @@
.bd-online-ph {
display: flex;
flex-direction: column;
margin: 10% 0;
h3 {
color: #fff;

View File

@ -8,6 +8,7 @@
transform: translateX(-100%) translateY(-100%);
opacity: 0;
transition: all .4s ease-in-out;
pointer-events: none;
&.active {
width: 900px;
@ -15,20 +16,10 @@
opacity: 1;
}
.bd-sidebar-view::after {
content: "";
height: 100%;
width: 310px;
background-color: #202225;
top: 100%;
display: block;
position: absolute;
}
.bd-settings-x {
position: absolute;
top: 18px;
left: 250px;
left: 255px;
border: 2px solid #6e6e6e;
border-radius: 50%;
width: 25px;
@ -39,7 +30,7 @@
cursor: pointer;
.platform-darwin & {
top: 40px;
top: 43px;
}
.bd-x-text {
@ -60,15 +51,11 @@
background-color: hsla(218,5%,47%,.3);
.bd-material-design-icon {
fill: #FFF;
fill: #fff;
}
}
}
.bd-sidebar-region .bd-scroller {
padding-top: 0;
}
.bd-info {
display: flex;
align-items: flex-end;
@ -94,7 +81,7 @@
&:hover {
.bd-material-design-icon {
fill: #FFF;
fill: #fff;
}
}
}
@ -103,11 +90,43 @@
fill: #414245;
&:hover {
fill: #FFF;
fill: #fff;
}
}
}
.bd-sidebar-view {
&::after {
content: "";
height: 100%;
width: 310px;
background-color: #202225;
top: 100%;
display: block;
position: absolute;
}
.bd-sidebar-region .bd-scroller {
padding-top: 0;
}
.bd-content-region {
width: 590px;
}
&.active {
.bd-content-region {
transition: transform 0.4s ease-in-out, opacity 0.2s ease;
transform: none;
opacity: 1;
}
}
&.bd-stop .bd-content-region {
z-index: 3003;
}
}
.platform-darwin & {
top: 0px;
@ -120,19 +139,7 @@
top: 0;
}
.bd-sidebar-view.bd-stop .bd-content-region {
z-index: 3003;
}
.bd-sidebar-view.active {
.bd-content-region {
transition: transform 0.4s ease-in-out, opacity 0.2s ease;
transform: none;
opacity: 1;
}
}
&:not(.active) .bd-sidebar-view.active,
&:not(.active) > .bd-sidebar-view.active,
&.bd-settings-out .bd-sidebar-view.active {
.bd-content-region {
transform: translate(-600px, 0%);

View File

@ -6,7 +6,8 @@ bd-tooltips {
z-index: 9001;
}
.bd-tooltip {
.bd-tooltip,
.bd-popover {
background-color: #000;
border-radius: 5px;
box-shadow: 0 2px 10px 0 rgba(0,0,0,.2);
@ -20,9 +21,13 @@ bd-tooltips {
word-wrap: break-word;
z-index: 9001;
.bd-tooltip-arrow {
&::after {
content: none;
}
.bd-tooltip-arrow,
.bd-popover-arrow {
border: 5px solid transparent;
content: " ";
height: 0;
pointer-events: none;
width: 0;
@ -32,7 +37,8 @@ bd-tooltips {
&[x-placement^="top"] {
margin-bottom: 10px;
.bd-tooltip-arrow {
.bd-tooltip-arrow,
.bd-popover-arrow {
margin-left: -5px;
border-top-color: #000;
bottom: -10px;
@ -42,7 +48,8 @@ bd-tooltips {
&[x-placement^="bottom"] {
margin-top: 10px;
.bd-tooltip-arrow {
.bd-tooltip-arrow,
.bd-popover-arrow {
border-width: 0 5px 5px 5px;
top: -5px;
border-bottom-color: #000;
@ -52,7 +59,8 @@ bd-tooltips {
&[x-placement^="right"] {
margin-left: 10px;
.bd-tooltip-arrow {
.bd-tooltip-arrow,
.bd-popover-arrow {
border-width: 5px 5px 5px 0;
left: -5px;
border-right-color: #000;
@ -62,10 +70,29 @@ bd-tooltips {
&[x-placement^="left"] {
margin-right: 10px;
.bd-tooltip-arrow {
.bd-tooltip-arrow,
.bd-popover-arrow {
border-width: 5px 0 5px 5px;
right: -5px;
border-left-color: #000;
}
}
.bd-material-button {
cursor: pointer;
&:hover {
.bd-material-design-icon {
fill: #fff;
}
}
}
.bd-material-design-icon {
fill: #414245;
&:hover {
fill: #fff;
}
}
}

View File

@ -17,3 +17,8 @@
top: 50px;
}
}
// Any layers need to be above the main layer (where the BD button is placed)
.layer-kosS71 + .layer-kosS71 {
z-index: 900;
}

View File

@ -1,20 +0,0 @@
.bd-emote-outer {
display: inline-block;
height: 32px;
}
.bd-emote-outer.bd-is-emote {
font-size: 0;
}
.bd-emotewrapper {
display: flex;
max-height: 32px;
img {
height: 32px;
margin: 0 .05em 0 .1em !important;
min-height: 22px;
min-width: 22px;
object-fit: contain;
vertical-align: -.4em;
max-height: 32px;
}
}

View File

@ -0,0 +1,52 @@
.bd-emote-outer {
display: inline-block;
height: 32px;
user-select: none;
.bd-is-emote {
font-size: 0;
}
}
.bd-emotewrapper {
display: flex;
max-height: 32px;
img {
height: 32px;
margin: 0 .05em 0 .1em !important;
min-height: 22px;
min-width: 22px;
object-fit: contain;
vertical-align: -.4em;
max-height: 32px;
}
.bd-emote-favourite-button {
display: none;
width: 16px;
height: 16px;
margin-left: -17px;
margin-right: 1px;
cursor: pointer;
opacity: .7;
transition: opacity .1s ease;
&:hover {
opacity: 1;
}
svg {
fill: $coldimwhite;
transition: fill .1s ease;
}
&.bd-active svg {
fill: #fff;
}
}
&:hover .bd-emote-favourite-button {
display: block;
}
}

View File

@ -14,24 +14,28 @@
padding-bottom: 8px;
white-space: nowrap;
.bd-autocompleteRow {
.bd-autocomplete-row {
padding: 0 8px;
font-size: 14px;
line-height: 16px;
.bd-autocompleteSelector {
.bd-autocomplete-selector {
border-radius: 3px;
padding: 8px;
&.bd-selectable {
cursor: pointer;
&:hover {
background-color: darken(#36393f, 5%);
}
&.bd-selected {
background-color: #36393f;
}
}
&.bd-selected {
background-color: #36393f;
}
.bd-autocompleteTitle {
.bd-autocomplete-title {
color: #72767d;
padding: 4px 0;
text-transform: uppercase;
@ -46,7 +50,7 @@
}
}
.bd-autocompleteField {
.bd-autocomplete-field {
display: flex;
flex: 1 1 auto;
color: #f6f6f7;

View File

@ -43,8 +43,6 @@
font-size: 12px;
}
.bd-form-divider {
height: 1px;
margin: 15px 0;

View File

@ -10,7 +10,7 @@
.vc-chrome {
position: absolute;
right: 60px;
right: 0;
top: 35px;
border-radius: 3px;
z-index: 9001;
@ -21,7 +21,7 @@
.vc-chrome-fields-wrap {
.vc-input__input {
background: #1e2124;
color: #FFF;
color: #fff;
box-shadow: inset 0 0 0 1px #000;
}
@ -30,9 +30,23 @@
}
.vc-chrome-toggle-btn svg path {
fill: #FFF;
fill: #fff;
}
}
}
&:not(.bd-hide) {
animation: bd-colourpicker-slidein 0.1s ease-in;
}
}
}
@keyframes bd-colourpicker-slidein {
0% {
right: 20px;
}
100% {
right: 0;
}
}

View File

@ -45,7 +45,18 @@
margin-top: 0;
}
> .bd-drawer {
> .bd-form-customsetting-value {
margin-bottom: 0;
.bd-form-textarea-details {
margin-top: 15px;
}
.bd-button {
margin-top: 10px;
padding: 5px 8px;
border-radius: 2px;
font-size: 14px;
}
}
}

View File

@ -12,4 +12,4 @@
@import './discordoverrides.scss';
@import './helpers.scss';
@import './misc.scss';
@import './emote.scss';
@import './emotes.scss';

View File

@ -21,3 +21,7 @@
.bd-hide {
display: none;
}
.bd-inline {
display: inline;
}

View File

@ -19,6 +19,7 @@
z-index: 5;
max-width: 310px;
min-width: 310px;
pointer-events: all;
.bd-scroller {
padding: 10px 10px 0 0;
@ -32,6 +33,7 @@
box-shadow: 0 0 4px #202225;
backface-visibility: hidden;
transition: transform 0.6s ease;
pointer-events: all;
}
&.bd-stop {

View File

@ -47,7 +47,7 @@ export default class extends EventListener {
this.setIds();
this.makeMutable();
} catch (err) {
console.log(err);
Logger.err('AutoManip', err);
}
}
@ -125,7 +125,7 @@ export default class extends EventListener {
}
setUserIds() {
for (let user of document.querySelectorAll('.channel-members-wrap .member')) {
for (let user of document.querySelectorAll('.channel-members-wrap .member, .channel-members-wrap .member-2FrNV0')) {
this.setUserId(user);
}
}

View File

@ -11,10 +11,25 @@
import { Events } from 'modules';
import { Utils } from 'common';
export default new class {
open(item) {
Events.emit('bd-open-menu', item);
}
close() {
Events.emit('bd-close-menu');
}
get items() {
return BdMenuItems;
}
}
let items = 0;
export const BdMenuItems = new class {
constructor() {
window.bdmenu = this;
@ -85,5 +100,4 @@ export const BdMenuItems = new class {
remove(item) {
Utils.removeFromArray(this.items, item);
}
};

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { Events, WebpackModules, DiscordApi } from 'modules';
import { Events, WebpackModules, DiscordApi, MonkeyPatch } from 'modules';
import { Utils } from 'common';
import { remote } from 'electron';
import DOM from './dom';
@ -24,49 +24,39 @@ export default class {
server: DiscordApi.currentGuild,
channel: DiscordApi.currentChannel
};
window.addEventListener('keyup', e => Events.emit('gkh:keyup', e));
this.autoManip = new AutoManip();
const defer = setInterval(() => {
if (!this.profilePopupModule) return;
clearInterval(defer);
/*Utils.monkeyPatch(this.profilePopupModule, 'open', 'after', (data, userid) => Events.emit('ui-event', {
event: 'profile-popup-open',
data: { userid }
}));*/
}, 100);
const ehookInterval = setInterval(() => {
if (!remote.BrowserWindow.getFocusedWindow()) return;
clearInterval(ehookInterval);
remote.BrowserWindow.getFocusedWindow().webContents.on('did-navigate-in-page', (e, url, isMainFrame) => {
const { currentGuild, currentChannel } = DiscordApi;
if (!this.pathCache.server) {
Events.emit('server-switch', { server: currentGuild, channel: currentChannel });
this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel;
return;
}
if (!this.pathCache.channel) {
Events.emit('channel-switch', currentChannel);
this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel;
return;
}
if (currentGuild &&
currentGuild.id &&
this.pathCache.server &&
this.pathCache.server.id !== currentGuild.id) {
if (currentGuild && currentGuild.id && this.pathCache.server && this.pathCache.server.id !== currentGuild.id) {
Events.emit('server-switch', { server: currentGuild, channel: currentChannel });
this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel;
return;
}
if (currentChannel &&
currentChannel.id &&
this.pathCache.channel &&
this.pathCache.channel.id !== currentChannel.id) Events.emit('channel-switch', currentChannel);
if (currentChannel && currentChannel.id && this.pathCache.channel && this.pathCache.channel.id !== currentChannel.id)
Events.emit('channel-switch', currentChannel);
this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel;
@ -74,10 +64,6 @@ export default class {
}, 100);
}
static get profilePopupModule() {
return WebpackModules.getModuleByProps(['fetchMutualFriends', 'setSection']);
}
static injectUi() {
DOM.createElement('div', null, 'bd-settings').appendTo(DOM.bdBody);
DOM.createElement('div', null, 'bd-modals').appendTo(DOM.bdModals);

View File

@ -9,16 +9,16 @@
*/
<template>
<div class="bd-settings" :class="{active: active, 'bd-settings-out': activeIndex === -1 && lastActiveIndex >= 0}" @keyup="close">
<div class="bd-settings" :class="{active: active, 'bd-settings-out': activeIndex === -1 && lastActiveIndex >= 0}" @keyup="$emit('close')">
<SidebarView :contentVisible="this.activeIndex >= 0 || this.lastActiveIndex >= 0" :animating="this.animating" :class="{'bd-stop': !first}">
<Sidebar slot="sidebar">
<div class="bd-settings-x" @click="close">
<div class="bd-settings-x" @click="$emit('close')">
<MiClose size="17"/>
<span class="bd-x-text">ESC</span>
</div>
<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" />
<SidebarItem v-for="item in category" :item="item" :key="item.id" @click="itemOnClick(item.id)" />
</template>
</Sidebar>
<div slot="sidebarfooter" class="bd-info">
@ -55,11 +55,12 @@
</SidebarView>
</div>
</template>
<script>
// Imports
import { shell } from 'electron';
import { Settings } from 'modules';
import { Events, Settings } from 'modules';
import { BdMenuItems } from 'ui';
import { shell } from 'electron';
import { SidebarView, Sidebar, SidebarItem, ContentColumn } from './sidebar';
import { SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView, UpdaterView } from './bd';
import { SvgX, MiGithubCircle, MiWeb, MiClose, MiTwitterCircle } from './common';
@ -75,9 +76,9 @@
Settings,
timeout: null,
SettingsWrapper
}
};
},
props: ['active', 'close'],
props: ['active'],
components: {
SidebarView, Sidebar, SidebarItem, ContentColumn,
SettingsWrapper, SettingsPanel, CssEditorView, PluginsView, ThemesView, UpdaterView,
@ -150,6 +151,9 @@
if (active) return;
this.closeContent();
}
},
created() {
Events.on('bd-open-menu', item => item && this.itemOnClick(this.sidebarItems.find(i => i === item || i.id === item || i.contentid === item || i.set === item).id));
}
}
</script>

View File

@ -10,14 +10,15 @@
<template>
<div class="bd-settings-wrapper" :class="[{active: active}, 'platform-' + this.platform]">
<div class="bd-settings-button" :class="{'bd-active': active, 'bd-animating': animating}" @click="showSettings">
<div class="bd-settings-button" :class="{'bd-active': active, 'bd-animating': animating}" @click="active = true">
<div v-if="updating === 0" v-tooltip.right="'Checking for updates'" class="bd-settings-button-btn bd-loading"></div>
<div v-else-if="updating === 2" v-tooltip.right="'Updates available!'" class="bd-settings-button-btn bd-updates"></div>
<div v-else class="bd-settings-button-btn" :class="[{'bd-loading': !loaded}]"></div>
</div>
<BdSettings ref="settings" :active="active" :close="hideSettings" />
<BdSettings ref="settings" :active="active" @close="active = false" />
</div>
</template>
<script>
// Imports
import { Events, Settings } from 'modules';
@ -33,27 +34,24 @@
animating: false,
timeout: null,
platform: global.process.platform
}
};
},
components: {
BdSettings
},
methods: {
showSettings() {
if (!this.loaded) return;
this.active = true;
},
hideSettings() { this.active = false },
toggleSettings() { this.active = !this.active },
keyupListener(e) {
if (Modals.stack.length || !this.active || e.which !== 27) return;
if (this.$refs.settings.activeIndex !== -1) this.$refs.settings.closeContent();
else this.hideSettings();
else this.active = false;
e.stopImmediatePropagation();
}
},
watch: {
active(active) {
if (active && !this.loaded)
return this.active = false;
this.animating = true;
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
@ -64,6 +62,8 @@
},
created() {
Events.on('ready', e => this.loaded = true);
Events.on('bd-open-menu', item => this.active = true);
Events.on('bd-close-menu', () => this.active = false);
Events.on('update-check-start', e => this.updating = 0);
Events.on('update-check-end', e => this.updating = 1);
Events.on('updates-available', e => this.updating = 2);

View File

@ -9,22 +9,23 @@
*/
<template>
<div :class="{'bd-profile-badges-wrap': !hasBadges}">
<div class="bd-profile-badges-wrap">
<div class="bd-profile-badges">
<div v-if="developer" v-tooltip="'BetterDiscord Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="onClick"></div>
<div v-else-if="webdev" v-tooltip="'BetterDiscord Web Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="onClick"></div>
<div v-else-if="contributor" v-tooltip="'BetterDiscord Contributor'" class="bd-profile-badge bd-profile-badge-contributor" @click="onClick"></div>
<div v-if="developer" v-tooltip="'BetterDiscord Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="click"></div>
<div v-else-if="webdev" v-tooltip="'BetterDiscord Web Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="click"></div>
<div v-else-if="contributor" v-tooltip="'BetterDiscord Contributor'" class="bd-profile-badge bd-profile-badge-contributor" @click="click"></div>
</div>
</div>
</template>
<script>
// Imports
import { shell } from 'electron';
export default {
props: ['webdev', 'developer', 'contributor', 'hasBadges'],
props: ['webdev', 'developer', 'contributor'],
methods: {
onClick() {
click() {
if (this.developer) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp');
if (this.webdev) return shell.openExternal('https://betterdiscord.net');
if (this.contributor) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp/graphs/contributors');

View File

@ -9,12 +9,13 @@
*/
<template>
<div class="bd-message-badges-wrap">
<div v-if="developer" v-tooltip="'BetterDiscord Developer'" class="bd-message-badge bd-message-badge-developer" @click="onClick"></div>
<div v-else-if="webdev" v-tooltip="'BetterDiscord Web Developer'" class="bd-message-badge bd-message-badge-developer" @click="onClick"></div>
<div v-else-if="contributor" v-tooltip="'BetterDiscord Contributor'" class="bd-message-badge bd-message-badge-contributor" @click="onClick"></div>
<div class="bd-message-badges-wrap" @click.stop>
<div v-if="developer" v-tooltip="'BetterDiscord Developer'" class="bd-message-badge bd-message-badge-developer" @click="click"></div>
<div v-else-if="webdev" v-tooltip="'BetterDiscord Web Developer'" class="bd-message-badge bd-message-badge-developer" @click="click"></div>
<div v-else-if="contributor" v-tooltip="'BetterDiscord Contributor'" class="bd-message-badge bd-message-badge-contributor" @click="click"></div>
</div>
</template>
<script>
// Imports
import { shell } from 'electron';
@ -22,7 +23,7 @@
export default {
props: ['webdev', 'developer', 'contributor', 'hasBadges'],
methods: {
onClick() {
click() {
if (this.developer) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp');
if (this.webdev) return shell.openExternal('https://betterdiscord.net');
if (this.contributor) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp/graphs/contributors');

View File

@ -21,21 +21,29 @@
<div class="bd-card-body">
<div class="bd-card-description">{{item.description}}</div>
<div class="bd-card-footer">
<div class="bd-card-extra">v{{item.version}} by {{item.authors.join(', ').replace(/,(?!.*,)/gmi, ' and')}}</div>
<div class="bd-card-extra">
v{{item.version}} by
<template v-for="(author, i) in item.authors">
<ContentAuthor :author="author" :after="i === item.authors.length - 1 ? '' : i === item.authors.length - 2 ? ' and' : ','" />
</template>
</div>
<div class="bd-controls">
<slot name="controls"/>
<slot name="controls" />
</div>
</div>
</div>
</div>
</template>
<script>
// Imports
import { MiExtension } from '../common';
import ContentAuthor from './ContentAuthor.vue';
export default {
props: ['item'],
components: {
ContentAuthor,
MiExtension
}
}

View File

@ -0,0 +1,68 @@
/**
* BetterDiscord Content Author 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>
<v-popover class="bd-content-author bd-inline" popoverClass="bd-popover bd-content-author-links" trigger="click" placement="top" :disabled="!hasLinks">
<!-- <template v-if="typeof author === 'string'">{{ author }}</template>
<a v-else-if="author.url" :href="author.url" @click="openLink">{{ author.name }}</a>
<a v-else-if="author.github_username" :href="'https://github.com/' + author.github_username" @click="openLink">{{ author.name }}</a>
<span v-else-if="author.discord_id" @click="openUserProfileModal(author.discord_id)">{{ author.name }}</span>
<template v-else>{{ author.name }}</template> -->
<span :class="{'bd-content-author-link': hasLinks}">{{ author.name || author }}</span><span v-text="after" @click.stop></span>
<template slot="popover">
<div v-if="author.discord_id" @click="openUserProfileModal(author.discord_id)" v-tooltip="author.discord_username || author.name" class="bd-material-button"><MiDiscord size="16" /></div>
<div v-if="author.github_username" @click="openGitHub" v-tooltip="author.github_username" class="bd-material-button"><MiGithubCircle size="16" /></div>
<div v-if="author.twitter_username" @click="openTwitter" v-tooltip="'@' + author.twitter_username" class="bd-material-button"><MiTwitterCircle size="16" /></div>
<div v-if="author.url" @click="openWebsite" v-tooltip="author.url" class="bd-material-button"><MiWeb size="16" /></div>
</template>
</v-popover>
</template>
<script>
// Imports
import { WebpackModules } from 'modules';
import { BdMenu } from 'ui';
import { shell } from 'electron';
import { MiGithubCircle, MiWeb, MiTwitterCircle, MiDiscord } from '../common';
export default {
props: ['author', 'after'],
components: {
MiGithubCircle, MiWeb, MiTwitterCircle, MiDiscord
},
computed: {
hasLinks() {
return this.author.discord_id || this.author.github_username || this.author.twitter_username || this.author.url;
}
},
methods: {
openLink(e) {
shell.openExternal(e.target.href);
e.preventDefault();
},
openUserProfileModal(discord_id) {
const UserProfileModal = WebpackModules.getModuleByProps(['fetchMutualFriends', 'setSection']);
UserProfileModal.open(discord_id);
BdMenu.close();
},
openGitHub() {
shell.openExternal(`https://github.com/${this.author.github_username}`);
},
openWebsite() {
shell.openExternal(this.author.url);
},
openTwitter() {
shell.openExternal(`https://twitter.com/${this.author.twitter_username}`);
}
}
}
</script>

View File

@ -19,7 +19,7 @@
<div class="bd-form-item">
<h5>Custom Editor</h5>
<FormButton v-if="internalEditorIsInstalled" :onClick="openInternalEditor">Open</FormButton>
<FormButton v-if="internalEditorIsInstalled" @click="openInternalEditor">Open</FormButton>
<template v-else>
<div class="bd-form-warning">
<div class="bd-text">Custom Editor is not installed!</div>
@ -32,7 +32,7 @@
<div class="bd-form-item">
<h5>System Editor</h5>
<FormButton :onClick="openSystemEditor">Open</FormButton>
<FormButton @click="openSystemEditor">Open</FormButton>
<p class="bd-hint">This will open {{ systemEditorPath }} in your system's default editor.</p>
</div>
<div class="bd-form-divider"></div>
@ -42,10 +42,7 @@
</div>
<SettingsPanel :settings="settingsset" />
<!-- <Setting :setting="liveUpdateSetting" />
<Setting :setting="watchFilesSetting" /> -->
<FormButton :onClick="recompile" :loading="compiling">Recompile</FormButton>
<FormButton @click="recompile" :loading="compiling">Recompile</FormButton>
</div>
</SettingsWrapper>
</template>

View File

@ -10,15 +10,16 @@
<template>
<Card :item="plugin">
<SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :checked="plugin.enabled" :change="togglePlugin" />
<SettingSwitch v-if="plugin.type === 'plugin'" slot="toggle" :value="plugin.enabled" @input="$emit('toggle-plugin')" />
<ButtonGroup slot="controls">
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="plugin.hasSettings" :onClick="e => showSettings(e.shiftKey)"><MiSettings size="18" /></Button>
<Button v-tooltip="'Reload'" :onClick="reloadPlugin"><MiRefresh size="18" /></Button>
<Button v-tooltip="'Edit'" :onClick="editPlugin"><MiPencil size="18" /></Button>
<Button v-tooltip="'Uninstall (shift + click to unload)'" :onClick="e => deletePlugin(e.shiftKey)" type="err"><MiDelete size="18" /></Button>
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="plugin.hasSettings" @click="$emit('show-settings', $event.shiftKey)"><MiSettings size="18" /></Button>
<Button v-tooltip="'Reload'" @click="$emit('reload-plugin')"><MiRefresh size="18" /></Button>
<Button v-tooltip="'Edit'" @click="editPlugin"><MiPencil size="18" /></Button>
<Button v-tooltip="'Uninstall (shift + click to unload)'" @click="$emit('delete-plugin', $event.shiftKey)" type="err"><MiDelete size="18" /></Button>
</ButtonGroup>
</Card>
</template>
<script>
// Imports
import { ClientLogger as Logger } from 'common';
@ -27,9 +28,10 @@
import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension } from '../common';
export default {
props: ['plugin', 'togglePlugin', 'reloadPlugin', 'deletePlugin', 'showSettings'],
props: ['plugin'],
components: {
Card, Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension
Card, Button, ButtonGroup, SettingSwitch,
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension
},
methods: {
editPlugin() {

View File

@ -13,17 +13,17 @@
<div class="bd-tabbar" slot="header">
<div class="bd-button" :class="{'bd-active': local}" @click="showLocal">
<h3>Installed</h3>
<RefreshBtn v-if="local" :onClick="refreshLocal" />
<RefreshBtn v-if="local" @click="refreshLocal" />
</div>
<div class="bd-button" :class="{'bd-active': !local}" @click="showOnline">
<h3>Browse</h3>
<RefreshBtn v-if="!local" :onClick="refreshOnline" />
<RefreshBtn v-if="!local" @click="refreshOnline" />
</div>
</div>
<div class="bd-flex bd-flex-col bd-pluginsview">
<div v-if="local" class="bd-flex bd-flex-grow bd-flex-col bd-plugins-container bd-local-plugins">
<PluginCard v-for="plugin in localPlugins" :plugin="plugin" :key="plugin.id" :togglePlugin="() => togglePlugin(plugin)" :reloadPlugin="() => reloadPlugin(plugin)" :deletePlugin="unload => deletePlugin(plugin, unload)" :showSettings="dont_clone => showSettings(plugin, dont_clone)" />
<PluginCard v-for="plugin in localPlugins" :plugin="plugin" :key="plugin.id" :data-plugin-id="plugin.id" @toggle-plugin="togglePlugin(plugin)" @reload-plugin="reloadPlugin(plugin)" @delete-plugin="unload => deletePlugin(plugin, unload)" @show-settings="dont_clone => showSettings(plugin, dont_clone)" />
</div>
<div v-if="!local" class="bd-online-ph">
<h3>Coming Soon</h3>

View File

@ -21,6 +21,7 @@
</ScrollerWrap>
</div>
</template>
<script>
// Imports
import { ScrollerWrap } from '../common';

View File

@ -10,15 +10,16 @@
<template>
<Card :item="theme">
<SettingSwitch slot="toggle" :checked="theme.enabled" :change="toggleTheme" />
<SettingSwitch slot="toggle" :value="theme.enabled" @input="$emit('toggle-theme')" />
<ButtonGroup slot="controls">
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="theme.hasSettings" :onClick="e => showSettings(e.shiftKey)"><MiSettings size="18" /></Button>
<Button v-tooltip="'Recompile (shift + click to reload)'" :onClick="e => reloadTheme(e.shiftKey)"><MiRefresh size="18" /></Button>
<Button v-tooltip="'Edit'" :onClick="editTheme"><MiPencil size="18" /></Button>
<Button v-tooltip="'Uninstall (shift + click to unload)'" :onClick="e => deleteTheme(e.shiftKey)" type="err"><MiDelete size="18" /></Button>
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="theme.hasSettings" @click="$emit('show-settings', $event.shiftKey)"><MiSettings size="18" /></Button>
<Button v-tooltip="'Recompile (shift + click to reload)'" @click="$emit('reload-theme', $event.shiftKey)"><MiRefresh size="18" /></Button>
<Button v-tooltip="'Edit'" @click="editTheme"><MiPencil size="18" /></Button>
<Button v-tooltip="'Uninstall (shift + click to unload)'" @click="$emit('delete-theme', $event.shiftKey)" type="err"><MiDelete size="18" /></Button>
</ButtonGroup>
</Card>
</template>
<script>
// Imports
import { shell } from 'electron';
@ -26,14 +27,15 @@
import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension } from '../common';
export default {
props: ['theme', 'toggleTheme', 'reloadTheme', 'deleteTheme', 'showSettings'],
props: ['theme'],
components: {
Card, Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension
Card, Button, ButtonGroup, SettingSwitch,
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension
},
methods: {
editTheme() {
try {
shell.openItem(this.theme.themePath);
shell.openItem(this.theme.contentPath);
} catch (err) {
Logger.err('ThemeCard', [`Error opening theme directory ${this.theme.contentPath}:`, err]);
}

View File

@ -13,17 +13,17 @@
<div class="bd-tabbar" slot="header">
<div class="bd-button" :class="{'bd-active': local}" @click="showLocal">
<h3>Installed</h3>
<RefreshBtn v-if="local" :onClick="refreshLocal"/>
<RefreshBtn v-if="local" @click="refreshLocal"/>
</div>
<div class="bd-button" :class="{'bd-active': !local}" @click="showOnline">
<h3>Browse</h3>
<RefreshBtn v-if="!local" :onClick="refreshOnline" />
<RefreshBtn v-if="!local" @click="refreshOnline" />
</div>
</div>
<div class="bd-flex bd-flex-col bd-themesview">
<div v-if="local" class="bd-flex bd-flex-grow bd-flex-col bd-themes-container bd-local-themes">
<ThemeCard v-for="theme in localThemes" :theme="theme" :key="theme.id" :toggleTheme="() => toggleTheme(theme)" :reloadTheme="reload => reloadTheme(theme, reload)" :showSettings="dont_clone => showSettings(theme, dont_clone)" :deleteTheme="unload => deleteTheme(theme, unload)" />
<ThemeCard v-for="theme in localThemes" :theme="theme" :key="theme.id" :data-theme-id="theme.id" @toggle-theme="toggleTheme(theme)" @reload-theme="reload => reloadTheme(theme, reload)" @show-settings="dont_clone => showSettings(theme, dont_clone)" @delete-theme="unload => deleteTheme(theme, unload)" />
</div>
<div v-if="!local" class="bd-online-ph">
<h3>Coming Soon</h3>

View File

@ -9,7 +9,7 @@
*/
<template>
<Modal :class="['bd-modal-basic', {'bd-modal-out': modal.closing}]" :headerText="modal.title" :close="modal.close">
<Modal :class="['bd-modal-basic', {'bd-modal-out': modal.closing}]" :headerText="modal.title" @close="modal.close">
<div slot="body" class="bd-modal-basic-body">{{ modal.text }}</div>
<div slot="footer" class="bd-modal-controls">
<div class="bd-flex-grow"></div>

View File

@ -9,7 +9,7 @@
*/
<template>
<Modal :class="['bd-modal-basic', {'bd-modal-out': modal.closing}]" :headerText="modal.title" :close="modal.close">
<Modal :class="['bd-modal-basic', {'bd-modal-out': modal.closing}]" :headerText="modal.title" @close="modal.close">
<div slot="body" class="bd-modal-basic-body">{{ modal.text }}</div>
<div slot="footer" class="bd-modal-controls">
<div class="bd-flex-grow"></div>

View File

@ -1,5 +1,5 @@
<template>
<Modal :headerText="modal.event.header" :close="modal.close"
<Modal :headerText="modal.event.header" @close="modal.close"
:class="[{'bd-err': modal.event.type && modal.event.type === 'err'}, {'bd-modal-out': modal.closing}]">
<MiError v-if="modal.event.type === 'err'" slot="icon" size="20"/>
<div slot="body">

View File

@ -9,7 +9,7 @@
*/
<template>
<Modal :class="['bd-modal-basic', {'bd-modal-out': modal.closing}]" :headerText="modal.title" :close="modal.close">
<Modal :class="['bd-modal-basic', {'bd-modal-out': modal.closing}]" :headerText="modal.title" @close="modal.close">
<div slot="body" class="bd-modal-basic-body">
<div v-for="(perm, i) in permissions" :key="`perm-${i}`" class="bd-perm-scope">
<div class="bd-perm-allow">

View File

@ -10,7 +10,7 @@
<template>
<div class="bd-settings-modal" :class="{'bd-edited': changed}">
<Modal :headerText="modal.headertext" :close="modal.close" :class="{'bd-modal-out': modal.closing}">
<Modal :class="{'bd-modal-out': modal.closing}" :headerText="modal.headertext" @close="modal.close">
<SettingsPanel :settings="settings" :schemes="modal.schemes" slot="body" class="bd-settings-modal-body" />
<div slot="footer" class="bd-footer-alert" :class="{'bd-active': changed || saving, 'bd-warn': warnclose}" :style="{pointerEvents: changed ? 'all' : 'none'}">
<div class="bd-footer-alert-text">Unsaved changes</div>
@ -23,9 +23,9 @@
</Modal>
</div>
</template>
<script>
// Imports
import Vue from 'vue';
import { Modal } from '../../common';
import SettingsPanel from '../SettingsPanel.vue';
import { Utils, ClientLogger as Logger } from 'common';
@ -54,10 +54,9 @@
if (this.saving) return;
this.saving = true;
try {
if (this.modal.saveSettings) await this.modal.saveSettings(this.settings);
else await this.modal.settings.merge(this.settings);
await this.modal.saveSettings ? this.modal.saveSettings(this.settings) : this.modal.settings.merge(this.settings);
} catch (err) {
// TODO Display error that settings failed to save
// TODO: display error that settings failed to save
Logger.err('SettingsModal', ['Failed to save settings:', err]);
}
this.saving = false;
@ -71,13 +70,13 @@
}
},
created() {
this.modal.beforeClose = force => {
this.modal.on('close', force => {
if (this.changed && !force) {
this.warnclose = true;
setTimeout(() => this.warnclose = false, 400);
throw {message: 'Settings have been changed'};
}
};
});
this.modal.settings.on('settings-updated', this.cloneSettings);
this.cloneSettings();

View File

@ -43,7 +43,7 @@
import SettingsPanel from '../SettingsPanel.vue';
export default {
props: ['setting', 'change'],
props: ['setting'],
components: {
MiSettings, MiOpenInNew, MiMinus
},

View File

@ -12,16 +12,17 @@
<div class="bd-setting-switch">
<div class="bd-title">
<h3>{{setting.text}}</h3>
<SettingSwitch :checked="setting.value" :change="change" />
<SettingSwitch v-model="setting.value" />
</div>
<div class="bd-hint">{{setting.hint}}</div>
</div>
</template>
<script>
import SettingSwitch from '../../common/SettingSwitch.vue';
export default {
props: ['setting', 'change'],
props: ['setting'],
components: {
SettingSwitch
}

View File

@ -9,74 +9,68 @@
*/
<template>
<div ref="root" class="bd-form-colourpicker">
<div class="bd-form-colourpicker">
<div class="bd-title">
<h3 v-if="setting.text">{{setting.text}}</h3>
<div class="bd-colourpicker-wrapper">
<button class="bd-colourpicker-swatch" :style="{backgroundColor: rgbaString}" @click="show"/>
<button ref="swatch" class="bd-colourpicker-swatch" :style="{backgroundColor: rgbaString}" @click="open = !open" />
</div>
</div>
<Picker ref="picker" v-model="colors" @input="pick" :class="{'bd-hidden': !open}" :style="{top: topOffset}"/>
<Picker ref="picker" v-model="colours" @input="pick" :class="{'bd-hide': !open}" :style="{top: topOffset}" />
<div class="bd-hint">{{setting.hint}}</div>
</div>
</template>
<script>
import { Chrome as Picker } from 'vue-color';
export default {
data() {
return {
open: false,
colors: '#FFF',
colours: '#fff',
topOffset: '35px'
}
},
components: {
Picker
},
props: ['setting', 'change'],
props: ['setting'],
computed: {
hex() {
if (!this.$refs.picker || !this.$refs.picker.val) return this.colors;
if (!this.$refs.picker || !this.$refs.picker.val) return this.colours;
return this.$refs.picker.val.hex;
},
rgba() {
if (!this.$refs.picker || !this.$refs.picker.val) return this.colors;
if (!this.$refs.picker || !this.$refs.picker.val) return this.colours;
return this.$refs.picker.val.rgba;
},
hsva() {
if (!this.$refs.picker || !this.$refs.picker.val) return this.colors;
if (!this.$refs.picker || !this.$refs.picker.val) return this.colours;
return this.$refs.picker.val.hsv;
},
hsla() {
if (!this.$refs.picker || !this.$refs.picker.val) return this.colors;
if (!this.$refs.picker || !this.$refs.picker.val) return this.colours;
return this.$refs.picker.val.hsl;
},
rgbaString() {
if ('string' === typeof this.colors) return this.colors;
const { rgba } = this.colors;
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;
if (typeof this.colours === 'string') return this.colours;
const { r, g, b, a } = this.colours.rgba;
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
},
methods: {
show() {
const offset = window.innerHeight - this.$refs.root.getBoundingClientRect().top - 340;
if (offset >= 0) {
this.topOffset = '35px';
} else {
this.topOffset = 35 + offset > 35 ? '35px' : `${35 + offset}px`;
}
this.open = true;
},
pick(c) {
this.change(this.rgbaString);
this.setting.value = this.rgbaString;
},
closePopup(e) {
if (!this.$refs.root.contains(e.target)) this.open = false;
if (!this.$refs.swatch.contains(e.target) && !this.$refs.picker.$el.contains(e.target))
this.open = false;
}
},
beforeMount() {
this.colors = this.setting.value;
window.addEventListener('click', this.closePopup);
this.colours = this.setting.value;
window.addEventListener('click', this.closePopup);
},
destroyed() {
window.removeEventListener('click', this.closePopup);
@ -84,14 +78,18 @@
watch: {
setting(newVal, oldVal) {
if (newVal.value === oldVal.value) return;
this.colors = newVal.value;
this.colours = newVal.value;
this.open = false;
},
open(open) {
if (!open) return;
const offset = window.innerHeight - this.$el.getBoundingClientRect().top - 340;
if (offset >= 0) {
this.topOffset = '35px';
} else {
this.topOffset = 35 + offset > 35 ? '35px' : `${35 + offset}px`;
}
}
}
}
</script>
<style>
.bd-hidden {
display: none;
}
</style>

View File

@ -11,8 +11,17 @@
<template>
<div class="bd-form-customsetting" :class="{'bd-form-customsetting-debug': setting.debug}">
<component :is="component" :setting="setting" :change="change"></component>
<Drawer class="bd-form-customsetting-value" label="Custom setting data" v-if="setting.debug">
<Drawer class="bd-form-customsetting-value bd-form-textarea" label="Custom setting data" v-if="setting.debug">
<pre class="bd-pre-wrap"><div class="bd-pre">{{ JSON.stringify(setting, null, 4) }}</div></pre>
<div class="bd-form-textarea-details">
<div class="bd-title">
<h3>Set value</h3>
</div>
<div class="bd-hint">Enter a JSON string to update the setting.</div>
</div>
<textarea ref="set_value_input" class="bd-textarea" @keyup.stop :value="JSON.stringify(setting.value, null, 4)"></textarea>
<Button @click="setting.value = JSON.parse($refs.set_value_input.value)">Update</Button>
</Drawer>
</div>
</template>
@ -21,12 +30,14 @@
import { PluginManager } from 'modules';
import SettingsPanel from '../SettingsPanel.vue';
import Drawer from '../../common/Drawer.vue';
import Button from '../../common/Button.vue';
import path from 'path';
export default {
props: ['setting', 'change'],
props: ['setting'],
components: {
Drawer
Drawer,
Button
},
computed: {
component() {
@ -34,6 +45,7 @@
const component = window.require(path.join(this.setting.path, this.setting.file));
return this.setting.component ? component[this.setting.component] : component.default ? component.default : component;
}
if (typeof this.setting.function === 'string') {
const plugin = PluginManager.getPluginByPath(this.setting.path);
if (!plugin) return;
@ -42,12 +54,14 @@
return this.componentFromHTMLElement(component, this.setting, this.change);
return component;
}
if (typeof this.setting.component === 'string') {
const plugin = PluginManager.getPluginByPath(this.setting.path);
if (!plugin) return;
const component = plugin[this.setting.component];
return component;
}
if (typeof this.setting.component === 'object') {
return this.setting.component;
}
@ -62,6 +76,10 @@
this.$el.appendChild(htmlelement);
}
};
},
change(value, ignore_disabled) {
if (this.disabled && !ignore_disabled) return;
this.setting.value = value;
}
}
}

View File

@ -12,17 +12,18 @@
<div class="bd-form-dropdown">
<div class="bd-title">
<h3 v-if="setting.text">{{setting.text}}</h3>
<Dropdown v-if="!setting.fullwidth" :options="setting.options" :selected="setting.value" :disabled="setting.disabled" :change="change" />
<Dropdown v-if="!setting.fullwidth" :options="setting.options" v-model="setting.value" :disabled="setting.disabled" />
</div>
<div class="bd-hint">{{setting.hint}}</div>
<Dropdown v-if="setting.fullwidth" :options="setting.options" :selected="setting.value" :disabled="setting.disabled" :change="change" />
<Dropdown v-if="setting.fullwidth" :options="setting.options" v-model="setting.value" :disabled="setting.disabled" />
</div>
</template>
<script>
import Dropdown from '../../common/Dropdown.vue';
export default {
props: ['setting', 'change'],
props: ['setting'],
components: {
Dropdown
}

View File

@ -16,12 +16,12 @@
</div>
<div class="bd-hint">{{ setting.hint }}</div>
<div class="bd-selected-files">
<div class="bd-selected-file" v-for="file_path in this.setting.value">
<div class="bd-selected-file" v-for="file_path in setting.value">
<!-- Maybe add a preview here later? -->
<!-- For now just show the selected file path -->
<span class="bd-file-path">{{ file_path }}</span>
<span class="bd-file-open" @click="() => openItem(file_path)"><MiOpenInNew /></span>
<span class="bd-file-remove" @click="() => removeItem(file_path)"><MiMinus /></span>
<span class="bd-file-remove" :class="{'bd-disabled': setting.disabled}" @click="() => removeItem(file_path)"><MiMinus /></span>
</div>
</div>
</div>
@ -34,7 +34,7 @@
import path from 'path';
export default {
props: ['setting', 'change'],
props: ['setting'],
components: {
MiOpenInNew, MiMinus
},
@ -44,13 +44,14 @@
const filenames = await ClientIPC.send('bd-native-open', this.setting.dialogOptions);
if (filenames)
this.change(filenames);
this.setting.value = filenames;
},
openItem(file_path) {
shell.openItem(path.resolve(this.setting.path, file_path));
},
removeItem(file_path) {
this.change(this.setting.value.filter(f => f !== file_path));
if (this.setting.disabled) return;
this.setting = this.setting.value.filter(f => f !== file_path);
}
}
}

View File

@ -27,13 +27,13 @@
import { KeybindSetting } from 'structs';
import { ClientIPC, ClientLogger as Logger } from 'common';
import { shell } from 'electron';
import process from 'process';
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 {

View File

@ -19,6 +19,7 @@
<textarea class="bd-textarea" ref="textarea" @keyup.stop v-model="setting.value" :disabled="setting.disabled"></textarea>
</div>
</template>
<script>
export default {
props: ['setting'],

View File

@ -13,29 +13,30 @@
<div class="bd-title">
<h3 v-if="setting.text">{{setting.text}}</h3>
<div class="bd-number">
<input type="number" :value="setting.value / setting.multi" :min="setting.min" :max="setting.max" :step="setting.step" @keyup.stop @input="input"/>
<input type="number" :value="setting.value / setting.multi" :min="setting.min" :max="setting.max" :step="setting.step" @keyup.stop @input="input" />
<div class="bd-number-spinner bd-flex bd-flex-col">
<div class="bd-arrow" @click="changeBy(true)"><div class="bd-up-arrow"></div></div>
<div class="bd-arrow" @click="changeBy(false)"><div class="bd-down-arrow"></div></div>
<div class="bd-arrow" @click="changeBy(1)"><div class="bd-up-arrow"></div></div>
<div class="bd-arrow" @click="changeBy(-1)"><div class="bd-down-arrow"></div></div>
</div>
</div>
</div>
<div class="bd-hint">{{setting.hint}}</div>
</div>
</template>
<script>
export default {
props: ['setting', 'change'],
props: ['setting'],
methods: {
input(e) {
let number = parseFloat(e.target.value)
if (Number.isNaN(number)) return;
this.change(number * this.setting.multi);
this.setting.value = number * this.setting.multi;
},
changeBy(positive) {
let step = this.setting.step == undefined ? 1 : this.settings.step;
this.change((this.setting.value + (positive ? step : -step)) * this.setting.multi);
changeBy(change) {
let step = this.setting.step === undefined ? 1 : this.settings.step;
this.setting.value = (this.setting.value + (change * step)) * this.setting.multi;
},
handleWheel() {} // No idea why this works but it does
},

View File

@ -16,14 +16,15 @@
</div>
<div class="bd-hint">{{setting.hint}}</div>
</div>
<RadioGroup :options="setting.options" :selected="setting.value" :disabled="setting.disabled" :change="change" />
<RadioGroup :options="setting.options" v-model="setting.value" :disabled="setting.disabled" />
</div>
</template>
<script>
import RadioGroup from '../../common/RadioGroup.vue';
export default {
props: ['setting', 'change'],
props: ['setting'],
components: {
RadioGroup
}

View File

@ -10,22 +10,23 @@
<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" />
<BoolSetting v-if="setting.type === 'bool'" :setting="setting" />
<StringSetting v-if="setting.type === 'text' && !setting.multiline" :setting="setting" />
<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" />
<NumberSetting v-if="setting.type === 'number'" :setting="setting" />
<DropdownSetting v-if="setting.type === 'dropdown'" :setting="setting" />
<RadioSetting v-if="setting.type === 'radio'" :setting="setting" />
<SliderSetting v-if="setting.type === 'slider'" :setting="setting" />
<ColourSetting v-if="setting.type === 'colour'" :setting="setting" />
<KeybindSetting v-if="setting.type === 'keybind'" :setting="setting" />
<FileSetting v-if="setting.type === 'file'" :setting="setting" :change="change" />
<FileSetting v-if="setting.type === 'file'" :setting="setting" />
<GuildSetting v-if="setting.type === 'guild'" :setting="setting" />
<ArraySetting v-if="setting.type === 'array'" :setting="setting" :change="change" />
<CustomSetting v-if="setting.type === 'custom'" :setting="setting" :change="change" />
<ArraySetting v-if="setting.type === 'array'" :setting="setting" />
<CustomSetting v-if="setting.type === 'custom'" :setting="setting" />
<div class="bd-form-divider"></div>
</div>
</template>
<script>
// Imports
import BoolSetting from './Bool.vue';
@ -68,12 +69,6 @@
disabled() {
return this.setting.disabled || false;
}
},
methods: {
change(value) {
if (this.disabled) return;
this.setting.value = value;
}
}
}
</script>

View File

@ -9,7 +9,7 @@
*/
<template>
<div class="bd-form-slider" @mouseenter="showTooltip" @mouseleave="hideTooltip">
<div class="bd-form-slider" @mouseenter="tooltip = true" @mouseleave="tooltip = false">
<div class="bd-title">
<h3>{{ setting.text }}</h3>
<div class="bd-slider">
@ -21,7 +21,7 @@
<div class="bd-slider-bar-filled" :style="{width: `${getPointPosition() * 100}%`}"></div>
</div>
<div class="bd-slider-thumb-wrap">
<div class="bd-slider-thumb" v-tooltip="{content: (value || '0') + setting.unit, show: toolTip, trigger: 'manual'}" :style="{left: `${getPointPosition() * 100}%`}"></div>
<div class="bd-slider-thumb" v-tooltip="{content: (value || '0') + setting.unit, show: tooltip, trigger: 'manual'}" :style="{left: `${getPointPosition() * 100}%`}"></div>
</div>
<input type="range" :value="value" :min="setting.min || 0" :max="setting.max || 100" :step="setting.step || 1" @keyup.stop @input="input" />
</div>
@ -30,13 +30,14 @@
<div class="bd-hint">{{ setting.hint }}</div>
</div>
</template>
<script>
export default {
props: ['setting', 'change'],
props: ['setting'],
data() {
return {
fillpercentage: 0,
toolTip: false
tooltip: false
};
},
computed: {
@ -48,16 +49,10 @@
input(e) {
let number = parseFloat(e.target.value);
if (Number.isNaN(number)) return;
this.change(number * this.setting.multi);
this.setting.value = number * this.setting.multi;
},
getPointPosition(value) {
return ((value || this.value) - (this.setting.min || 0)) / ((this.setting.max || 100) - (this.setting.min || 0));
},
showTooltip() {
this.toolTip = true;
},
hideTooltip() {
this.toolTip = false;
}
}
}

View File

@ -13,19 +13,15 @@
<div class="bd-title">
<h3 v-if="setting.text">{{setting.text}}</h3>
<div class="bd-textinput-wrapper">
<input type="text" :value="setting.value" @keyup.stop @input="input"/>
<input type="text" v-model="setting.value" @keyup.stop />
</div>
</div>
<div class="bd-hint">{{setting.hint}}</div>
</div>
</template>
<script>
export default {
props: ['setting', 'change'],
methods: {
input(e) {
this.change(e.target.value);
}
}
props: ['setting']
}
</script>

View File

@ -1,5 +1,5 @@
/**
* BetterDiscord Autocomplete Component
* BetterDiscord Emote Autocomplete Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
@ -11,17 +11,17 @@
<template>
<div class="bd-autocomplete">
<div v-if="emotes && emotes.length" class="bd-autocomplete-inner">
<div class="bd-autocompleteRow">
<div class="bd-autocompleteSelector">
<div class="bd-autocompleteTitle">
<div class="bd-autocomplete-row">
<div class="bd-autocomplete-selector">
<div class="bd-autocomplete-title">
Emotes Matching:
<strong>{{title}}</strong>
</div>
</div>
</div>
<div v-for="(emote, index) in emotes" class="bd-autocompleteRow" :key="index">
<div class="bd-autocompleteSelector bd-selectable" :class="{'bd-selected': index === selectedIndex}" @mouseover="() => { selected = emote.id }" @click="() => inject(emote)">
<div class="bd-autocompleteField">
<div v-for="(emote, index) in emotes" class="bd-autocomplete-row" :key="index">
<div class="bd-autocomplete-selector bd-selectable" :class="{'bd-selected': index === selectedIndex}" @mouseover="() => { selected = emote.id }" @click="() => inject(emote)">
<div class="bd-autocomplete-field">
<img :src="getEmoteSrc(emote)"/>
<div>{{emote.id}}</div>
</div>
@ -30,6 +30,7 @@
</div>
</div>
</template>
<script>
import { EmoteModule } from 'builtin';
import { Events } from 'modules';

View File

@ -9,13 +9,14 @@
*/
<template>
<div class="bd-button" :class="[{'bd-disabled': disabled}, {'bd-err': type === 'err'}]" @click="!disabled && !loading ? onClick($event) : null">
<div class="bd-button" :class="[{'bd-disabled': disabled}, {'bd-err': type === 'err'}]" @click="!disabled && !loading ? $emit('click', $event) : null">
<div v-if="loading" class="bd-spinner-7"></div>
<slot v-else />
</div>
</template>
<script>
export default {
props: ['loading', 'disabled', 'type', 'onClick', 'type']
props: ['loading', 'disabled', 'type']
}
</script>

View File

@ -13,7 +13,7 @@
<slot />
</div>
</template>
<script>
export default {
}
export default {}
</script>

View File

@ -17,13 +17,14 @@
</span>
</div>
<div class="bd-dropdown-options bd-flex bd-flex-col" ref="options" v-if="active">
<div class="bd-dropdown-option" v-for="option in options" :class="{'bd-dropdown-option-selected': selected === option.value}" @click="change(option.value); active = false">{{ option.text }}</div>
<div class="bd-dropdown-option" v-for="option in options" :class="{'bd-dropdown-option-selected': selected === option.value}" @click="select(option)">{{ option.text }}</div>
</div>
</div>
</template>
<script>
export default {
props: ['options', 'selected', 'disabled', 'change'],
props: ['options', 'value', 'disabled'],
data() {
return {
active: false
@ -33,10 +34,14 @@
getSelectedText() {
const selected_option = this.options.find(option => option.value === this.selected);
return selected_option ? selected_option.text : this.selected;
},
select(option) {
this.$emit('input', option.value);
this.active = false;
}
},
mounted() {
document.addEventListener("click", e => {
document.addEventListener('click', e => {
let options = this.$refs.options;
if (options && !options.contains(e.target) && options !== e.target) {
this.active = false;

View File

@ -1,30 +0,0 @@
/**
* BetterDiscord Error Modal 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-modal-error" :class="{'bd-open': content.showStack}">
<div class="bd-modal-error-title bd-flex">
<span class="bd-modal-title-text bd-flex-grow">{{content.message}}</span>
<span class="bd-modal-titlelink" v-if="content.showStack" @click="hideStack(content)">Hide Stacktrace</span>
<span class="bd-modal-titlelink" v-else @click="showStack(content)">Show Stacktrace</span>
</div>
<div class="bd-scroller-wrap">
<div class="bd-scroller">
<div class="bd-modal-error-body"><span>{{content.err.message}}</span>
{{content.stackTrace}}</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['content', 'hideStack', 'showStack']
}
</script>

View File

@ -9,13 +9,14 @@
*/
<template>
<div class="bd-form-button bd-button" :class="{'bd-disabled': disabled}" @click="!disabled && !loading ? onClick() : null">
<div class="bd-form-button bd-button" :class="{'bd-disabled': disabled}" @click="!disabled && !loading ? $emit('click', $event) : null">
<div v-if="loading" class="bd-spinner-7"></div>
<slot v-else />
</div>
</template>
<script>
export default {
props: ['loading', 'disabled', 'type', 'onClick']
props: ['loading', 'disabled', 'type']
}
</script>

View File

@ -12,3 +12,5 @@ export { default as MiPlus } from './materialicons/Plus.vue';
export { default as MiChevronDown } from './materialicons/ChevronDown.vue';
export { default as MiExtension } from './materialicons/Extension.vue';
export { default as MiError } from './materialicons/Error.vue';
export { default as MiDiscord } from './materialicons/Discord.vue';
export { default as MiStar } from './materialicons/Star.vue';

View File

@ -15,8 +15,8 @@
<div class="bd-modal-icon">
<slot name="icon" />
</div>
<span class="bd-modal-headertext">{{headerText}}</span>
<div class="bd-modal-x" @click="e => close(e.shiftKey, e)">
<span class="bd-modal-headertext">{{ headerText }}</span>
<div class="bd-modal-x" @click="$emit('close', $event.shiftKey, $event)">
<MiClose size="18" />
</div>
</div>
@ -39,7 +39,7 @@
import { MiClose } from './MaterialIcon';
export default {
props: ['headerText', 'close'],
props: ['headerText'],
components: {
MiClose
},
@ -56,9 +56,8 @@
},
methods: {
keyupListener(e) {
if (e.which === 27) {
this.close();
}
if (e.which === 27)
this.$emit('close', false, e);
}
}
}

View File

@ -10,16 +10,19 @@
<template>
<div class="bd-radio-group" :class="{'bd-disabled': disabled}">
<label class="bd-radio" v-for="option in options" :class="{'bd-radio-selected': selected === option.value}" @click="change(option.value)">
<label class="bd-radio" v-for="option in options" :class="{'bd-radio-selected': value === option.value}" @click="$emit('input', option.value)">
<div class="bd-radio-control-wrap">
<svg class="bd-radio-control" name="Checkmark" width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><polyline stroke="#3e82e5" stroke-width="2" points="3.5 9.5 7 13 15 5"></polyline></g></svg>
<svg class="bd-radio-control" name="Checkmark" width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd"><polyline stroke="#3e82e5" stroke-width="2" points="3.5 9.5 7 13 15 5"></polyline></g>
</svg>
</div>
<div class="bd-radio-text">{{option.text}}</div>
<div class="bd-radio-text">{{ option.text }}</div>
</label>
</div>
</template>
<script>
export default {
props: ['options', 'selected', 'disabled', 'change']
props: ['options', 'value', 'disabled']
}
</script>

View File

@ -1,11 +1,11 @@
/**
* BetterDiscord RefreshBtn
* BetterDiscord Refresh Button
* Copyright (c) 2018 Lilian Tedone http://beard-design.com/
* All rights reserved.
*/
<template>
<div class="bd-refresh-button" :class="{'bd-refreshed': state === 'refreshed', 'bd-refreshing': state === 'refreshing'}" @click="onClick">
<div class="bd-refresh-button" :class="{'bd-refreshed': state === 'refreshed', 'bd-refreshing': state === 'refreshing'}" @click="$emit('click')">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g class="bd-svg-refresh">
<g class="bd-svg-circle">
@ -24,8 +24,9 @@
</svg>
</div>
</template>
<script>
export default {
props: ['state', 'onClick']
props: ['state']
}
</script>

View File

@ -15,8 +15,9 @@
</div>
</div>
</template>
<script>
export default {
props: ['dark']
}
</script>
</script>

View File

@ -9,13 +9,14 @@
*/
<template>
<label class="bd-switch-wrapper" @click="() => change(!checked)">
<label class="bd-switch-wrapper" @click="$emit('input', !value)">
<input type="checkbox" class="bd-switch-checkbox" />
<div class="bd-switch" :class="{'bd-checked': checked}" />
<div class="bd-switch" :class="{'bd-checked': value}" />
</label>
</template>
<script>
export default {
props: ['checked', 'change']
props: ['value']
}
</script>

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord Material Design Icon
* 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.
*
* Material Design Icons
* Copyright (c) 2014 Google
* Apache 2.0 LICENSE
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
<template>
<span class="bd-material-design-icon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M 22,24L 16.75,19L 17.375,21L 4.5,21C 3.11929,21 2,19.8807 2,18.5L 2,3.5C 2,2.11929 3.11929,1 4.5,1L 19.5,1C 20.8807,1 22,2.11929 22,3.5L 22,24 Z M 12,6.8C 9.31508,6.8 7.4401,7.95052 7.4401,7.95052C 8.47135,7.02864 10.2682,6.48177 10.2682,6.48177L 10.0964,6.32552C 8.40885,6.35677 6.8776,7.52864 6.8776,7.52864C 5.15885,11.1224 5.26823,14.2161 5.26823,14.2161C 6.67448,16.0286 8.7526,15.9036 8.7526,15.9036L 9.45573,14.9974C 8.20572,14.7318 7.42448,13.6224 7.42448,13.6224C 7.42448,13.6224 9.29946,14.9 12,14.9C 14.7005,14.9 16.5755,13.6224 16.5755,13.6224C 16.5755,13.6224 15.7943,14.7318 14.5443,14.9974L 15.2474,15.9036C 15.2474,15.9036 17.3255,16.0286 18.7318,14.2161C 18.7318,14.2161 18.8411,11.1224 17.1224,7.52865C 17.1224,7.52865 15.5911,6.35677 13.9036,6.32552L 13.7318,6.48177C 13.7318,6.48177 15.5286,7.02865 16.5599,7.95052C 16.5599,7.95052 14.6849,6.80001 12,6.8 Z M 9.93143,10.5886C 10.5829,10.5886 11.1086,11.16 11.0971,11.8571C 11.0971,12.5543 10.5829,13.1257 9.93143,13.1257C 9.29143,13.1257 8.76571,12.5543 8.76571,11.8571C 8.76571,11.16 9.28,10.5886 9.93143,10.5886 Z M 14.1028,10.5886C 14.7543,10.5886 15.2686,11.16 15.2686,11.8572C 15.2686,12.5543 14.7543,13.1257 14.1028,13.1257C 13.4628,13.1257 12.9371,12.5543 12.9371,11.8572C 12.9371,11.16 13.4514,10.5886 14.1028,10.5886 Z " />
</svg>
</span>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord Material Design Icon
* 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.
*
* Material Design Icons
* Copyright (c) 2014 Google
* Apache 2.0 LICENSE
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
<template>
<span class="bd-material-design-icon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M 11.9994,17.2708L 18.1794,20.9978L 16.5444,13.9688L 21.9994,9.24277L 14.8084,8.62477L 11.9994,1.99777L 9.1904,8.62477L 1.9994,9.24277L 7.4544,13.9688L 5.8194,20.9978L 11.9994,17.2708 Z " />
</svg>
</span>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -9,12 +9,13 @@
*/
<template>
<div class="bd-item" :class="{active: item.active}" @click="onClick(item.id)">
<div class="bd-item" :class="{active: item.active}" @click="$emit('click', item.id)">
{{item.text}}
</div>
</template>
<script>
export default {
props: ['item', 'onClick']
props: ['item']
}
</script>

View File

@ -13,6 +13,7 @@
<slot/>
</div>
</template>
<script>
export default {}
</script>

View File

@ -13,6 +13,7 @@
{{item.text}}
</div>
</template>
<script>
export default {
props: ['item']

View File

@ -9,15 +9,16 @@
*/
<template>
<SidebarButton v-if="item._type === 'button'" :item="item" :onClick="onClick"/>
<SidebarButton v-if="item._type === 'button'" :item="item" @click="$emit('click', $event)" />
<SidebarHeader v-else :item="item" />
</template>
<script>
// Imports
import { SidebarHeader, SidebarButton } from './';
export default {
props: ['item', 'onClick'],
props: ['item'],
components: {
SidebarHeader,
SidebarButton

View File

@ -13,8 +13,7 @@
<slot/>
</div>
</template>
<script>
export default {
}
<script>
export default {}
</script>

View File

@ -23,6 +23,7 @@
</div>
</div>
</template>
<script>
// Imports
import { ScrollerWrap } from '../common';

View File

@ -8,106 +8,192 @@
* LICENSE file in the root directory of this source tree.
*/
import { EventListener } from 'modules';
import { Module, ReactComponents, ReactHelpers, MonkeyPatch, WebpackModules } from 'modules';
import { Reflection } from 'ui';
import { Utils, ClientLogger as Logger } from 'common';
import DOM from './dom';
import { BdBadge, BdMessageBadge } from './components/bd';
import VueInjector from './vueinjector';
import contributors from '../data/contributors';
export default class extends EventListener {
export default class extends Module {
bindings() {
this.uiEvent = this.uiEvent.bind(this);
this.messageBadge = this.messageBadge.bind(this);
this.badges = this.badges.bind(this);
this.userlistBadge = this.userlistBadge.bind(this);
}
get eventBindings() {
return [
{ id: 'discord:MESSAGE_CREATE', callback: this.messageBadge },
{ id: 'discord:MESSAGE_UPDATE', callback: this.messageBadge },
{ id: 'server-switch', callback: this.badges },
{ id: 'channel-switch', callback: this.badges },
{ id: 'ui:loadedmore', callback: this.badges },
{ id: 'ui:useridset', callback: this.userlistBadge },
{ id: 'ui-event', callback: this.uiEvent }
];
}
uiEvent(e) {
const { event, data } = e;
if (event !== 'profile-popup-open') return;
const { userid } = data;
if (!userid) return;
this.inject(userid);
}
badges() {
for (const messageGroup of document.querySelectorAll('.message-group')) {
this.messageBadge({ element: messageGroup });
}
}
messageBadge(e) {
if (!e.element) return;
const msgGroup = e.element.closest('.message-group');
if (msgGroup.dataset.hasBadges) return;
msgGroup.setAttribute('data-has-badges', true);
if (!msgGroup.dataset.authorId) return;
const c = contributors.find(c => c.id === msgGroup.dataset.authorId);
if (!c) return;
const root = document.createElement('span');
const usernameWrapper = msgGroup.querySelector('.username-wrapper');
if (!usernameWrapper) return;
const wrapperParent = usernameWrapper.parentElement;
if (!wrapperParent || wrapperParent.children.length < 2) return;
wrapperParent.insertBefore(root, wrapperParent.children[1]);
VueInjector.inject(root, {
components: { BdMessageBadge },
data: { c },
template: '<BdMessageBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />'
});
}
userlistBadge(e) {
const c = contributors.find(c => c.id === e.dataset.userId);
if (!c) return;
const memberUsername = e.querySelector('.member-username');
if (!memberUsername) return;
const root = document.createElement('span');
memberUsername.append(root);
VueInjector.inject(root, {
components: { BdMessageBadge },
data: { c },
template: '<BdMessageBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />'
});
}
inject(userid) {
const c = contributors.find(c => c.id === userid);
if (!c) return;
setTimeout(() => {
let hasBadges = false;
let root = document.querySelector('[class*="profileBadges"]');
if (root) {
hasBadges = true;
} else {
root = document.querySelector('[class*="headerInfo"]');
}
VueInjector.inject(root, {
components: { BdBadge },
data: { hasBadges, c },
template: '<BdBadge :hasBadges="hasBadges" :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />',
}, DOM.createElement('div', null, 'bdprofilebadges'));
}, 400);
init() {
this.patchMessage();
this.patchChannelMember();
this.patchNameTag();
this.patchUserProfileModals();
}
get contributors() {
return contributors;
}
/**
* Patches Message to use the extended NameTag.
* This is because NameTag is also used in places we don't really want any badges.
*/
async patchMessage() {
const Message = await ReactComponents.getComponent('Message');
this.unpatchMessageRender = MonkeyPatch('ProfileBadges', Message.component.prototype).after('render', (component, args, retVal) => {
if (!retVal.props || !retVal.props.children) return;
const message = ReactHelpers.findProp(component, 'message');
if (!message || !message.author) return;
const user = message.author;
const c = contributors.find(c => c.id === user.id);
if (!c) return;
const username = ReactHelpers.findByProp(retVal, 'type', 'h2');
if (!username) return;
username.props.children.splice(1, 0, ReactHelpers.React.createElement('span', {
className: 'bd-badge-outer',
'data-userid': user.id
}));
});
this.unpatchMessageMount = MonkeyPatch('ProfileBadges', Message.component.prototype).after('componentDidMount', component => {
const element = ReactHelpers.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectMessageBadges(element);
});
this.unpatchMessageUpdate = MonkeyPatch('ProfileBadges', Message.component.prototype).after('componentDidUpdate', component => {
const element = ReactHelpers.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectMessageBadges(element);
});
// Rerender all messages
for (const message of document.querySelectorAll('.message')) {
Reflection(message).forceUpdate();
}
}
/**
* Patches ChannelMember to use the extended NameTag.
* This is because NameTag is also used in places we don't really want any badges.
*/
async patchChannelMember() {
const ChannelMember = await ReactComponents.getComponent('ChannelMember');
this.unpatchChannelMemberRender = MonkeyPatch('ProfileBadges', ChannelMember.component.prototype).after('render', (component, args, retVal) => {
if (!retVal.props || !retVal.props.children) return;
const user = ReactHelpers.findProp(component, 'user');
if (!user) return;
const c = contributors.find(c => c.id === user.id);
if (!c) return;
const nameTag = retVal.props.children.props.children[1].props.children[0];
nameTag.type = this.PatchedNameTag || nameTag.type;
});
}
/**
* Creates an extended NameTag component that renders message badges.
*/
async patchNameTag() {
if (this.PatchedNameTag) return this.PatchedNameTag;
const ProfileBadges = this;
const NameTag = await ReactComponents.getComponent('NameTag', {selector: '.nameTag-26T3kW'});
this.PatchedNameTag = class extends NameTag.component {
render() {
const retVal = NameTag.component.prototype.render.call(this, arguments);
try {
if (!retVal.props || !retVal.props.children) return;
const user = ReactHelpers.findProp(this, 'user');
if (!user) return;
const c = contributors.find(c => c.id === user.id);
if (!c) return;
retVal.props.children.splice(1, 0, ReactHelpers.React.createElement('span', {
className: 'bd-badge-outer',
'data-userid': user.id
}));
} catch (err) {
Logger.err('ProfileBadges', ['Error thrown while rendering a NameTag', err]);
}
return retVal;
}
componentDidMount() {
const element = ReactHelpers.ReactDOM.findDOMNode(this);
if (!element) return;
ProfileBadges.injectMessageBadges(element);
}
componentDidUpdate() {
const element = ReactHelpers.ReactDOM.findDOMNode(this);
if (!element) return;
ProfileBadges.injectMessageBadges(element);
}
};
// Rerender all channel members
for (const channelMember of document.querySelectorAll('.member-2FrNV0')) {
Reflection(channelMember).forceUpdate();
}
return this.PatchedNameTag;
}
injectMessageBadges(element) {
for (const beo of element.getElementsByClassName('bd-badge-outer'))
this.injectMessageBadge(beo);
}
injectMessageBadge(root) {
while (root.firstChild) {
root.removeChild(root.firstChild);
}
const { userid } = root.dataset;
if (!userid) return;
const c = contributors.find(c => c.id === userid);
if (!c) return;
VueInjector.inject(root, {
components: { BdMessageBadge },
data: { c },
template: '<BdMessageBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />'
}, DOM.createElement('span'));
root.classList.add('bd-has-badge');
}
/**
* Patches UserProfileModals to inject profile badges into the modal once opened.
* TODO: just patch the modal component
*/
async patchUserProfileModals() {
const UserProfileModals = WebpackModules.getModuleByName('UserProfileModals');
MonkeyPatch('BdUI', UserProfileModals).after('open', async (context, [userid]) => {
const c = contributors.find(c => c.id === userid);
if (!c) return;
const root = await Utils.until(() => document.querySelector('[class*="headerInfo"]'));
const el = DOM.createElement('div', null, 'bdprofilebadges');
root.insertBefore(el.element, root.firstChild.nextSibling);
this.injectProfileBadge(userid, el.element);
});
}
injectProfileBadge(userid, root) {
const c = contributors.find(c => c.id === userid);
if (!c) return;
VueInjector.inject(root, {
components: { BdBadge },
data: { c },
template: '<BdBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />',
});
}
}

View File

@ -1,7 +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 BdMenu, BdMenuItems } from './bdmenu';
export { default as Modals } from './modals';
export { default as ProfileBadges } from './profilebadges';
export { default as Reflection } from './reflection';

View File

@ -19,7 +19,15 @@ Vue.use(VTooltip, {
defaultArrowSelector: '.bd-tooltip-arrow',
defaultInnerSelector: '.bd-tooltip-inner',
defaultTemplate: '<div class="bd-tooltip"><div class="bd-tooltip-arrow"></div><span class="bd-tooltip-inner"></span></div>',
defaultBoundariesElement: DOM.getElement('#app-mount')
defaultBoundariesElement: DOM.getElement('#app-mount'),
popover: {
defaultContainer: 'bd-tooltips',
defaultClass: 'bd-popover',
defaultWrapperClass: 'bd-popover-wrapper',
defaultInnerClass: 'bd-popover-inner',
defaultArrowClass: 'bd-popover-arrow',
defaultBoundariesElement: DOM.getElement('#app-mount')
}
});
export default Vue;

View File

@ -58,4 +58,11 @@ export default class AsyncEventEmitter extends EventEmitter {
});
}
/**
* Unbinds an event listener.
*/
off(event, callback) {
this.removeListener(event, callback);
}
}

View File

@ -1,169 +0,0 @@
/**
* BetterDiscord Monkeypatch
* 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 { ClientLogger as Logger } from './logger';
const patchedFunctions = new WeakMap();
export class PatchedFunction {
constructor(object, methodName, replaceOriginal = true) {
if (patchedFunctions.has(object[methodName])) {
const patchedFunction = patchedFunctions.get(object[methodName]);
if (replaceOriginal) patchedFunction.replaceOriginal();
return patchedFunction;
}
this.object = object;
this.methodName = methodName;
this.patches = [];
this.originalMethod = object[methodName];
this.replaced = false;
const patchedFunction = this;
this.replace = function(...args) {
patchedFunction.call(this, arguments);
};
patchedFunctions.set(object[methodName], this);
patchedFunctions.set(this.replace, this);
if (replaceOriginal)
this.replaceOriginal();
}
addPatch(patch) {
if (!this.patches.includes(patch))
this.patches.push(patch);
}
removePatch(patch, restoreOriginal = true) {
let i = 0;
while (this.patches[i]) {
if (this.patches[i] !== patch) i++;
else this.patches.splice(i, 1);
}
if (!this.patches.length && restoreOriginal)
this.restoreOriginal();
}
replaceOriginal() {
if (this.replaced) return;
this.object[this.methodName] = Object.assign(this.replace, this.object[this.methodName]);
this.replaced = true;
}
restoreOriginal() {
if (!this.replaced) return;
this.object[this.methodName] = Object.assign(this.object[this.methodName], this.replace);
this.replaced = false;
}
call(_this, args) {
const data = {
this: _this,
arguments: args,
return: undefined,
originalMethod: this.originalMethod,
callOriginalMethod: () => {
Logger.log('MonkeyPatch', [`Calling original method`, this, data]);
data.return = this.originalMethod.apply(data.this, data.arguments);
}
};
// Work through the patches calling each patch's hooks as if each patch had overridden the previous patch
for (let patch of this.patches) {
let callOriginalMethod = data.callOriginalMethod;
data.callOriginalMethod = () => {
const patch_data = Object.assign({}, data, {
callOriginalMethod, patch
});
patch.call(patch_data);
data.arguments = patch_data.arguments;
data.return = patch_data.return;
};
}
data.callOriginalMethod();
return data.return;
}
}
export class Patch {
constructor(patchedFunction, options, f) {
this.patchedFunction = patchedFunction;
if (options instanceof Function) {
f = options;
options = {
instead: data => {
f.call(this, data, ...data.arguments);
}
};
} else if (options === 'before') {
options = {
before: data => {
f.call(this, data, ...data.arguments);
}
};
} else if (options === 'after') {
options = {
after: data => {
f.call(this, data, ...data.arguments);
}
};
}
this.before = options.before || undefined;
this.instead = options.instead || undefined;
this.after = options.after || undefined;
this.once = options.once || false;
this.silent = options.silent || false;
this.suppressErrors = typeof options.suppressErrors === 'boolean' ? options.suppressErrors : true;
}
call(data) {
if (this.once)
this.cancel();
this.callBefore(data);
this.callInstead(data);
this.callAfter(data);
}
callBefore(data) {
if (this.before)
this.callHook('before', this.before, data);
}
callInstead(data) {
if (this.instead)
this.callHook('instead', this.instead, data);
else data.callOriginalMethod();
}
callAfter(data) {
if (this.after)
this.callHook('after', this.after, data);
}
callHook(hook, f, data) {
try {
f.call(this, data, ...data.arguments);
} catch (err) {
Logger.log('MonkeyPatch', [`Error thrown in ${hook} hook of`, this, '- :', err]);
if (!this.suppressErrors) throw err;
}
}
cancel() {
this.patchedFunction.removePatch(this);
}
}

View File

@ -8,7 +8,6 @@
* LICENSE file in the root directory of this source tree.
*/
import { PatchedFunction, Patch } from './monkeypatch';
import path from 'path';
import fs from 'fs';
import _ from 'lodash';
@ -23,90 +22,6 @@ 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);
const patch = new Patch(patchedFunction, options, f);
patchedFunction.addPatch(patch);
return patch;
}
/**
* Monkey-patches an object's method and returns a promise that will be resolved with the data object when the method is 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);
});
});
}
/**
* Monkeypatch function that is compatible with samogot's Lib Discord Internals.
* Don't use this for writing new plugins as it will eventually be removed!
*/
static compatibleMonkeyPatch(what, methodName, options) {
const { before, instead, after, once = false, silent = false } = options;
const cancelPatch = () => patch.cancel();
const compatible_function = _function => data => {
const compatible_data = {
thisObject: data.this,
methodArguments: data.arguments,
returnValue: data.return,
cancelPatch,
originalMethod: data.originalMethod,
callOriginalMethod: () => data.callOriginalMethod()
};
try {
_function(compatible_data);
data.arguments = compatible_data.methodArguments;
data.return = compatible_data.returnValue;
} catch (err) {
data.arguments = compatible_data.methodArguments;
data.return = compatible_data.returnValue;
throw err;
}
};
const patch = this.monkeyPatch(what, methodName, {
before: !instead && before ? compatible_function(before) : undefined,
instead: instead ? compatible_function(instead) : 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
@ -244,6 +159,17 @@ export class Utils {
enumerable: true
});
}
static async until(check, time = 0) {
let value, i;
do {
// Wait for the next tick
await new Promise(resolve => setTimeout(resolve, time));
value = check(i);
i++;
} while (!value);
return value;
}
}
export class FileUtils {

View File

@ -52,8 +52,19 @@ const globals = {
paths
};
class Comms {
class PatchedBrowserWindow extends BrowserWindow {
constructor(originalOptions) {
const options = Object.assign({}, originalOptions);
options.webPreferences = Object.assign({}, options.webPreferences);
// Make sure Node integration is enabled
options.webPreferences.nodeIntegration = true;
return new BrowserWindow(options);
}
}
class Comms {
constructor(bd) {
this.bd = bd;
this.initListeners();
@ -101,7 +112,6 @@ class Comms {
async sendToCssEditor(channel, message) {
return this.bd.csseditor.send(channel, message);
}
}
class BetterDiscord {
@ -201,8 +211,23 @@ class BetterDiscord {
return this.windowUtils.injectScript(this.config.getPath('cs'));
}
/**
* Patches Electron's BrowserWindow so all windows have Node integration enabled.
* This needs to be called only once before the main window is created (or BrowserWindow is put in a variable).
* Basically BetterDiscord needs to load before discord_desktop_core.
*/
static patchBrowserWindow() {
const electron_path = require.resolve('electron');
const browser_window_path = require.resolve(path.resolve(electron_path, '..', '..', 'browser-window.js'));
const browser_window_module = require.cache[browser_window_path];
browser_window_module.exports = PatchedBrowserWindow;
}
}
BetterDiscord.patchBrowserWindow();
module.exports = {
BetterDiscord
};

View File

@ -1,5 +1,6 @@
const
fs = require('fs'),
mkdirp = require('mkdirp'),
gulp = require('gulp'),
del = require('del'),
pump = require('pump'),
@ -18,6 +19,7 @@ const releasepkg = function() {
delete mainpkg.main;
delete mainpkg.devDependencies;
delete mainpkg.scripts;
mkdirp.sync('./release');
return fs.writeFileSync('./release/package.json', JSON.stringify(mainpkg, null, 2));
};

448
package-lock.json generated
View File

@ -5,9 +5,9 @@
"requires": true,
"dependencies": {
"@types/node": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz",
"integrity": "sha1-39MnWCoGwRTrbgRB+j1vqzXtrUg=",
"version": "7.0.58",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.58.tgz",
"integrity": "sha512-4LwjraUddrN+sJ2cL7v64w9fEWQ0zUlDdB8yqmFrWlavHkXxjxBSnZ4ofeW5SbHLpUE0Ve3XvijT/eQmylzasg==",
"dev": true
},
"abbrev": {
@ -269,6 +269,112 @@
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo="
},
"archiver": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz",
"integrity": "sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=",
"dev": true,
"requires": {
"archiver-utils": "1.3.0",
"async": "2.6.0",
"buffer-crc32": "0.2.13",
"glob": "7.1.2",
"lodash": "4.17.5",
"readable-stream": "2.3.5",
"tar-stream": "1.5.5",
"zip-stream": "1.2.0"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"readable-stream": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
}
}
},
"archiver-utils": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz",
"integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=",
"dev": true,
"requires": {
"glob": "7.1.2",
"graceful-fs": "4.1.11",
"lazystream": "1.0.0",
"lodash": "4.17.5",
"normalize-path": "2.1.1",
"readable-stream": "2.3.5"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"readable-stream": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
}
}
},
"archy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
@ -1311,6 +1417,54 @@
"underscore": "1.4.4"
}
},
"bl": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
"integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
"dev": true,
"requires": {
"readable-stream": "2.3.5",
"safe-buffer": "5.1.1"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"readable-stream": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
}
}
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
@ -1479,6 +1633,12 @@
}
}
},
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
"dev": true
},
"buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
@ -2092,6 +2252,56 @@
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
"dev": true
},
"compress-commons": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz",
"integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=",
"dev": true,
"requires": {
"buffer-crc32": "0.2.13",
"crc32-stream": "2.0.0",
"normalize-path": "2.1.1",
"readable-stream": "2.3.5"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"readable-stream": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
}
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -2235,6 +2445,60 @@
}
}
},
"crc": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz",
"integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ=",
"dev": true
},
"crc32-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz",
"integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=",
"dev": true,
"requires": {
"crc": "3.5.0",
"readable-stream": "2.3.5"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"readable-stream": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
}
}
},
"create-ecdh": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz",
@ -2820,12 +3084,12 @@
}
},
"electron": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2.tgz",
"integrity": "sha1-qBfNczwpcrPHzE93fK9uQkuIAU0=",
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.6.15.tgz",
"integrity": "sha1-w07FRIa39Jpm21jG8koJKEFPrqc=",
"dev": true,
"requires": {
"@types/node": "8.9.4",
"@types/node": "7.0.58",
"electron-download": "3.3.0",
"extract-zip": "1.6.6"
}
@ -2842,7 +3106,7 @@
"minimist": "1.2.0",
"nugget": "2.0.1",
"path-exists": "2.1.0",
"rc": "1.2.5",
"rc": "1.2.6",
"semver": "5.5.0",
"sumchecker": "1.3.1"
},
@ -6898,6 +7162,53 @@
"set-getter": "0.1.0"
}
},
"lazystream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
"integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
"dev": true,
"requires": {
"readable-stream": "2.3.5"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"readable-stream": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
}
}
},
"lcid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
@ -9671,9 +9982,9 @@
}
},
"rc": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz",
"integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.6.tgz",
"integrity": "sha1-6xiYnG1PTxYsOZ953dKfODVWgJI=",
"dev": true,
"requires": {
"deep-extend": "0.4.2",
@ -11164,6 +11475,71 @@
"inherits": "2.0.3"
}
},
"tar-stream": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.5.tgz",
"integrity": "sha512-mQdgLPc/Vjfr3VWqWbfxW8yQNiJCbAZ+Gf6GDu1Cy0bdb33ofyiNGBtAY96jHFhDuivCwgW1H9DgTON+INiXgg==",
"dev": true,
"requires": {
"bl": "1.2.2",
"end-of-stream": "1.4.1",
"readable-stream": "2.3.5",
"xtend": "4.0.1"
},
"dependencies": {
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
"dev": true,
"requires": {
"once": "1.4.0"
}
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"readable-stream": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"xtend": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
"dev": true
}
}
},
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -12592,6 +12968,56 @@
"requires": {
"fd-slicer": "1.0.1"
}
},
"zip-stream": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz",
"integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=",
"dev": true,
"requires": {
"archiver-utils": "1.3.0",
"compress-commons": "1.2.2",
"lodash": "4.17.5",
"readable-stream": "2.3.5"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
},
"readable-stream": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
}
}
}
}
}

View File

@ -20,6 +20,7 @@
"nedb": "^1.8.0"
},
"devDependencies": {
"archiver": "^2.1.1",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
@ -29,7 +30,7 @@
"combokeys": "^3.0.0",
"css-loader": "^0.28.9",
"del": "^3.0.0",
"electron": "^1.6.15",
"electron": "1.6.15",
"electron-rebuild": "^1.7.3",
"eslint": "^4.16.0",
"eslint-plugin-vue": "^4.3.0",
@ -47,6 +48,7 @@
"html-webpack-plugin": "^3.0.6",
"jquery": "^3.2.1",
"lodash": "^4.17.4",
"mkdirp": "^0.5.1",
"node-gyp": "^3.6.2",
"pump": "^2.0.0",
"sass-loader": "^6.0.6",
@ -73,7 +75,8 @@
"lint": "eslint -f unix client/src core/src csseditor/src",
"test": "npm run build && npm run lint",
"build_node-sass": "node scripts/build-node-sass.js",
"build_release": "npm run release --prefix client && npm run build --prefix core && npm run release --prefix csseditor && npm run build --prefix installer",
"release": "npm run lint && npm run build_release && gulp release"
"build_release": "npm run release --prefix client && npm run build --prefix core && npm run release --prefix csseditor",
"package_release": "node scripts/package-release.js",
"release": "npm run lint && npm run build_release && gulp release && npm run package_release"
}
}

View File

@ -0,0 +1,65 @@
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const releasepkg = require('../release/package.json');
const mainpkg = require('../package.json');
const corepkg = require('../core/package.json');
const clientpkg = require('../client/package.json');
const editorpkg = require('../csseditor/package.json');
// core.zip
const core = new Promise((resolve, reject) => {
const core_zip = archiver('zip');
core_zip.file('./release/package.json', {name: 'package.json'});
core_zip.file('./release/index.js', {name: 'index.js'});
core_zip.file(`./release/core.${corepkg.version}.js`, {name: `core.${corepkg.version}.js`});
core_zip.file('./release/sparkplug.js', {name: 'sparkplug.js'});
core_zip.directory('./release/modules', 'modules');
core_zip.directory('./release/node_modules', 'node_modules');
const core_zip_stream = fs.createWriteStream('./release/core.zip');
core_zip.pipe(core_zip_stream);
core_zip.on('end', resolve);
core_zip.on('error', reject);
core_zip.finalize();
});
// client.zip
const client = new Promise((resolve, reject) => {
const client_zip = archiver('zip');
client_zip.file(`./release/client.${clientpkg.version}.js`, {name: `client.${clientpkg.version}.js`});
const client_zip_stream = fs.createWriteStream('./release/client.zip');
client_zip.pipe(client_zip_stream);
client_zip.on('end', resolve);
client_zip.on('error', reject);
client_zip.finalize();
});
// csseditor.zip
const csseditor = new Promise((resolve, reject) => {
const csseditor_zip = archiver('zip');
csseditor_zip.directory('./release/csseditor', 'csseditor');
const csseditor_zip_stream = fs.createWriteStream('./release/csseditor.zip');
csseditor_zip.pipe(csseditor_zip_stream);
csseditor_zip.on('end', resolve);
csseditor_zip.on('error', reject);
csseditor_zip.finalize();
});
// full.zip
Promise.all([core, client, csseditor]).then(() => {
const full_zip = archiver('zip');
full_zip.file('./release/core.zip', {name: 'core.zip'});
full_zip.file('./release/client.zip', {name: 'client.zip'});
full_zip.file('./release/csseditor.zip', {name: 'csseditor.zip'});
const full_zip_stream = fs.createWriteStream('./release/full.zip');
full_zip.pipe(full_zip_stream);
full_zip.finalize();
});

View File

@ -1,11 +1,10 @@
{
"info": {
"name": "Example Plugin 2",
"authors": ["Jiiks"],
"version": 1.0,
"description": "Example Plugin 2 Description"
},
"main": "index.js",
"type": "plugin",
"defaultConfig": []
}
"info": {
"name": "Example Plugin 2",
"authors": [
"Jiiks"
],
"version": 1.0,
"description": "Example Plugin 2 Description"
}
}

View File

@ -2,9 +2,23 @@
"info": {
"id": "example-plugin-3",
"name": "Example Plugin 3",
"authors": [ "Samuel Elliott" ],
"authors": [
{
"name": "Samuel Elliott",
"url": "https://samuelelliott.ml",
"discord_id": "284056145272766465",
"github_username": "samuelthomas2774",
"twitter_username": "_samuelelliott"
},
{
"name": "Jiiks",
"discord_id": "81388395867156480",
"github_username": "Jiiks",
"twitter_username": "Jiiksi"
}
],
"version": 1.0,
"description": "A plugin for testing BetterDiscord plugin exports"
"description": "A plugin for testing BetterDiscord plugin bridge."
},
"main": "index.js"
}

View File

@ -3,7 +3,13 @@
"id": "example-plugin-4",
"name": "Example Plugin 4",
"authors": [
"Samuel Elliott"
{
"name": "Samuel Elliott",
"url": "https://samuelelliott.ml",
"discord_id": "284056145272766465",
"github_username": "samuelthomas2774",
"twitter_username": "_samuelelliott"
}
],
"version": 1.0,
"description": "Plugin for testing array setting events as the first example plugin has a lot of stuff in it now."

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