This commit is contained in:
Samuel Elliott 2019-08-24 21:48:58 +00:00 committed by GitHub
commit e1d0b138cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 365 additions and 278 deletions

View File

@ -279,8 +279,6 @@ export default new class EmoteModule extends BuiltinModule {
const { content } = args[1];
if (!content) return orig(...args);
Logger.log('EmoteModule', ['Sending message', MessageActions, args, orig]);
const emoteAsImage = Settings.getSetting('emotes', 'default', 'emoteasimage').value &&
(DiscordApi.currentChannel.type === 'DM' || DiscordApi.currentChannel.type === 'GROUP_DM' || DiscordApi.currentChannel.checkPermissions(DiscordApi.modules.DiscordPermissions.ATTACH_FILES));

View File

@ -31,7 +31,7 @@
"id": "developer-mode",
"type": "bool",
"text": "Developer mode",
"hint": "Adds some of BetterDiscord's internal modules to `global._bd`.",
"hint": "Adds some of BetterDiscord's internal modules to `global._bd` and enable additional options in plugin and theme settings.",
"value": false
},
{

View File

@ -22,7 +22,7 @@ const ignoreExternal = tests && true;
class BetterDiscord {
constructor() {
Logger.file = tests ? path.resolve(__dirname, '..', '..', 'tests', 'log.txt') : path.join(__dirname, 'log.txt');
Logger.file = tests ? path.resolve(__dirname, '..', '..', 'tests', 'log.txt') : `${__dirname}-log.txt`;
Logger.trimLogFile();
Logger.log('main', 'BetterDiscord starting');

View File

@ -118,7 +118,7 @@ export default class Content extends AsyncEventEmitter {
* @return {Promise}
*/
async enable(save = true) {
if (this.enabled) return;
if (this.enabled || this.unloaded) return;
await this.emit('enable');
await this.emit('start');

View File

@ -16,7 +16,7 @@ import { remote } from 'electron';
import Content from './content';
import Globals from './globals';
import Database from './database';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { Utils, FileUtils, ClientLogger as Logger, ClientIPC } from 'common';
import { SettingsSet, ErrorEvent } from 'structs';
import { Modals } from 'ui';
import Combokeys from 'combokeys';
@ -73,23 +73,23 @@ export default class {
}
static async packContent(path, contentPath) {
return new Promise((resolve, reject) => {
remote.dialog.showSaveDialog({
title: 'Save Package',
defaultPath: path,
filters: [
{
name: 'BetterDiscord Package',
extensions: ['bd']
}
]
}, filepath => {
if (!filepath) return;
const filepath = await ClientIPC.send('bd-native-save', {
title: 'Save Package',
defaultPath: path,
filters: [
{
name: 'BetterDiscord Package',
extensions: ['bd']
}
]
});
asar.uncache(filepath);
asar.createPackage(contentPath, filepath, () => {
resolve(filepath);
});
if (!filepath) return;
return new Promise((resolve, reject) => {
asar.uncache(filepath);
asar.createPackage(contentPath, filepath, () => {
resolve(filepath);
});
});
}
@ -104,13 +104,8 @@ export default class {
const directories = await FileUtils.listDirectory(this.contentPath);
for (const dir of directories) {
const packed = dir.endsWith('.bd');
if (!packed) {
try {
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; }
}
const stat = await FileUtils.stat(path.join(this.contentPath, dir));
const packed = stat.isFile();
try {
if (packed) {
@ -148,6 +143,8 @@ export default class {
* @param {bool} suppressErrors Suppress any errors that occur during loading of content
*/
static async refreshContent(suppressErrors = false) {
this.loaded = true;
if (!this.localContent.length) return this.loadAllContent();
try {
@ -155,18 +152,18 @@ export default class {
const directories = await FileUtils.listDirectory(this.contentPath);
for (const dir of directories) {
const packed = dir.endsWith('.bd');
// If content is already loaded this should resolve
if (this.getContentByDirName(dir)) continue;
try {
await FileUtils.directoryExists(path.join(this.contentPath, dir));
} catch (err) { continue; }
const stat = await FileUtils.stat(path.join(this.contentPath, dir));
const packed = stat.isFile();
try {
// Load if not
await this.preloadContent(dir);
if (packed) {
await this.preloadPackedContent(dir);
} else {
await this.preloadContent(dir);
}
} catch (err) {
// We don't want every plugin/theme to fail loading when one does
this.errors.push(new ErrorEvent({
@ -217,7 +214,8 @@ export default class {
await FileUtils.fileExists(packagePath);
const config = JSON.parse(asar.extractFile(packagePath, 'config.json').toString());
const unpackedPath = path.join(Globals.getPath('tmp'), packageName);
const id = config.info.id || config.info.name.toLowerCase().replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
const unpackedPath = path.join(Globals.getPath('tmp'), this.pathId, id);
asar.extractAll(packagePath, unpackedPath);
@ -348,7 +346,7 @@ export default class {
await unload;
await FileUtils.recursiveDeleteDirectory(content.paths.contentPath);
if (content.packed) await FileUtils.recursiveDeleteDirectory(content.packagePath);
if (content.packed) await FileUtils.deleteFile(content.packagePath);
return true;
} catch (err) {
Logger.err(this.moduleName, err);
@ -368,6 +366,8 @@ export default class {
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
try {
Object.defineProperty(content, 'unloaded', {configurable: true, value: true});
const disablePromise = content.disable(false);
const unloadPromise = content.emit('unload', reload);
@ -380,8 +380,14 @@ export default class {
if (this.unloadContentHook) this.unloadContentHook(content);
if (reload) return content.packed ? this.preloadPackedContent(content.packagePath, true, index) : this.preloadContent(content.dirName, true, index);
if (reload) {
const newcontent = content.packed ? this.preloadPackedContent(content.dirName.pkg, true, index) :
this.preloadContent(content.dirName, true, index);
Object.defineProperty(content, 'unloaded', {value: newcontent});
return newcontent;
}
Object.defineProperty(content, 'unloaded', {value: true});
this.localContent.splice(index, 1);
} catch (err) {
Logger.err(this.moduleName, err);
@ -433,7 +439,7 @@ export default class {
static getContentIndex(content) { return this.localContent.findIndex(c => c === content) }
static getContentById(id) { return this.localContent.find(c => c.id === id) }
static getContentByDirName(dirName) { return this.localContent.find(c => c.dirName === dirName) }
static getContentByDirName(dirName) { return this.localContent.find(c => !c.packed ? c.dirName === dirName : c.dirName.pkg === dirName) }
static getContentByPath(path) { return this.localContent.find(c => c.contentPath === path) }
static getContentByName(name) { return this.localContent.find(c => c.name === name) }

View File

@ -190,15 +190,34 @@ class ReactComponent {
}
}
ReactComponent.important = Symbol('BD.ReactComponent.important');
export class ReactComponents {
/** @type {ReactComponent[]} */
static get components() { return this._components || (this._components = []) }
/** @type {Reflection.modules.React.Component[]} */
static get unknownComponents() { return this._unknownComponents || (this._unknownComponents = []) }
/** @type {{id: string, listeners: function[]}[]} */
static get listeners() { return this._listeners || (this._listeners = []) }
/** @type {<{name: string, filter: function}[]>} */
static get nameSetters() { return this._nameSetters || (this._nameSetters = []) }
static get componentAliases() { return this._componentAliases || (this._componentAliases = []) }
/** @type {Object.<string, string>} */
static get componentAliases() { return this._componentAliases || (this._componentAliases = {}) }
static get ReactComponent() { return ReactComponent }
/**
* Processes a React component.
* @param {Reflection.modules.React.Component} component The React component class
* @param {object} retVal
* @param {object} important
* @param {string} important.selector A query selector the component will render elements matching (used to select all component instances to force them to rerender)
* @return {ReactComponent}
*/
static push(component, retVal, important) {
if (!(component instanceof Function)) return null;
const { displayName } = component;
@ -212,6 +231,8 @@ export class ReactComponents {
return component;
}
if (!important) important = component[ReactComponent.important];
const c = new ReactComponent(displayName, component, retVal, important);
this.components.push(c);
@ -226,16 +247,19 @@ export class ReactComponents {
/**
* Finds a component from the components array or by waiting for it to be mounted.
* @param {String} name The component's name
* @param {Object} important An object containing a selector to look for
* @param {Function} filter A function to filter components if a single element is rendered by multiple components
* @return {Promise => ReactComponent}
* @param {string} name The component's name
* @param {object} important An object containing a selector to look for
* @param {function} filter A function to filter components if a single element is rendered by multiple components
* @return {Promise<ReactComponent>}
*/
static async getComponent(name, important, filter) {
name = this.getComponentName(name);
const have = this.components.find(c => c.id === name);
if (have) return have;
if (have) {
if (!have.important) have.important = important;
return have;
}
if (important) {
const callback = () => {
@ -262,7 +286,7 @@ export class ReactComponents {
}
if (!component && filter) {
Logger.log('ReactComponents', ['Found elements matching the query selector but no components passed the filter']);
Logger.log('ReactComponents', ['Found elements matching the query selector but no components passed the filter', name, important, filter]);
return;
}
@ -321,8 +345,12 @@ export class ReactComponents {
return this.nameSetters.push({ name, filter });
}
/**
* Processes a React component that isn't known.
* @param {Reflection.modules.React.Component} component
* @param {} retVal
*/
static processUnknown(component, retVal) {
const have = this.unknownComponents.find(c => c.component === component);
for (const [fi, filter] of this.nameSetters.entries()) {
if (filter.filter.filter(component)) {
Logger.log('ReactComponents', 'Filter match!');
@ -331,9 +359,8 @@ export class ReactComponents {
return this.push(component, retVal);
}
}
if (have) return have;
this.unknownComponents.push(component);
return component;
if (!this.unknownComponents.includes(component)) this.unknownComponents.push(component);
}
}
@ -441,13 +468,13 @@ export class ReactAutoPatcher {
}
static async patchNameTag() {
const { selector } = Reflection.resolve('nameTag', 'username', 'discriminator', 'ownerIcon');
const { selector } = Reflection.resolve('nameTag', 'username', 'discriminator', 'bot');
this.NameTag = await ReactComponents.getComponent('NameTag', {selector});
}
static async patchGuild() {
const selector = `div.${Reflection.resolve('container', 'guildIcon', 'selected', 'unread').className}:not(:first-child)`;
this.Guild = await ReactComponents.getComponent('Guild', {selector}, m => m.prototype.renderBadge);
const { selector } = Reflection.resolve('listItem', 'guildSeparator', 'selected', 'friendsOnline');
this.Guild = await ReactComponents.getComponent('Guild', {selector}, m => m.displayName === 'Guild');
this.unpatchGuild = MonkeyPatch('BD:ReactComponents', this.Guild.component.prototype).after('render', (component, args, retVal) => {
const { guild } = component.props;
@ -505,7 +532,7 @@ export class ReactAutoPatcher {
ReactComponents.componentAliases.GuildVoiceChannel = 'VoiceChannel';
const { selector } = Reflection.resolve('containerDefault', 'actionIcon');
this.GuildVoiceChannel = await ReactComponents.getComponent('GuildVoiceChannel', {selector}, c => c.prototype.handleVoiceConnect);
this.GuildVoiceChannel = await ReactComponents.getComponent('GuildVoiceChannel', {selector}, c => c.prototype.renderVoiceUsers);
this.unpatchGuildVoiceChannel = MonkeyPatch('BD:ReactComponents', this.GuildVoiceChannel.component.prototype).after('render', this._afterChannelRender);

View File

@ -22,10 +22,20 @@ export default class Theme extends Content {
const watchfiles = Settings.getSetting('css', 'default', 'watch-files');
if (watchfiles.value) this.watchfiles = this.files;
watchfiles.on('setting-updated', event => {
watchfiles.on('setting-updated', this.__watchFilesSettingUpdated = event => {
if (event.value) this.watchfiles = this.files;
else this.watchfiles = [];
});
this.on('unload', () => {
watchfiles.off('setting-updated', this.__watchFilesSettingUpdated);
if (this._filewatcher) {
this._filewatcher.removeAll();
delete this._filewatcher;
}
});
}
get type() { return 'theme' }
@ -61,7 +71,7 @@ export default class Theme extends Content {
async compile() {
Logger.log(this.name, 'Compiling CSS');
if (this.info.type === 'sass') {
if (this.info.type === 'sass' || this.info.type === 'scss') {
const config = await ThemeManager.getConfigAsSCSS(this.settings);
const result = await ClientIPC.send('bd-compileSass', {
@ -147,7 +157,7 @@ export default class Theme extends Content {
* @param {Array} files Files to watch
*/
set watchfiles(files) {
if (this.packed) {
if (this.unloaded || this.packed) {
// Don't watch files for packed themes
return;
}

View File

@ -40,7 +40,7 @@ export default class ThemeManager extends ContentManager {
});
if (instance.enabled) {
instance.userConfig.enabled = false;
instance.enable();
instance.enable(false);
}
return instance;
} catch (err) {

View File

@ -1,7 +1,6 @@
.bd-card {
display: flex;
flex-direction: column;
flex-grow: 1;
background: transparent;
border-bottom: 1px solid rgba(114, 118, 126, .3);
min-height: 150px;

View File

@ -1,59 +0,0 @@
.bd-formCollection {
display: flex;
flex-direction: column;
div {
&:first-child {
flex: 1 1 auto;
}
}
.bd-collectionItem {
display: flex;
flex-grow: 1;
margin-top: 5px;
.bd-removeCollectionItem {
width: 20px;
flex: 0 1 auto;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
margin-bottom: 30px;
&:hover {
svg {
fill: #fff;
}
}
svg {
width: 16px;
height: 16px;
fill: #ccc;
}
}
}
.bd-newCollectionItem {
display: flex;
cursor: pointer;
align-self: flex-end;
justify-content: center;
align-items: center;
margin-right: 2px;
svg {
width: 16px;
height: 16px;
fill: #ccc;
}
&:hover {
svg {
fill: #fff;
}
}
}
}

View File

@ -1,16 +1,17 @@
.bd-pluginsview,
.bd-themesview {
.bd-localPh {
.bd-scroller {
padding: 0 20px 0 0;
}
}
flex-grow: 1;
.bd-onlinePh,
.bd-localPh {
display: flex;
flex-direction: column;
margin: 10px 0;
flex-grow: 1;
> .bd-scrollerWrap > .bd-scroller {
padding: 0 20px 0 0;
overflow-y: scroll;
}
.bd-spinnerContainer {
display: flex;
@ -19,8 +20,9 @@
.bd-onlinePhHeader {
display: flex;
padding: 0 20px 0 10px;
padding: 0 20px 0 0;
min-height: 80px;
margin-top: 20px;
.bd-flexRow {
min-height: 40px;
@ -81,16 +83,10 @@
}
.bd-onlinePhBody {
margin-top: 10px;
.bd-spinnerContainer {
min-height: 40px;
padding: 0;
}
.bd-scroller {
padding: 0 20px 0 0;
}
}
h3 {

View File

@ -8,6 +8,5 @@
@import './updater';
@import './window-preferences';
@import './kvp';
@import './collection';
@import './e2ee';
@import './devview';

View File

@ -1,7 +1,7 @@
.bd-remoteCard {
flex-direction: column;
margin-top: 10px;
padding: 10px;
padding: 10px 0;
border-radius: 0;
border-bottom: 1px solid rgba(114, 118, 126, .3);

View File

@ -1,13 +1,17 @@
// sass-lint:disable-all
body:not(.bd-hideButton) {
[class*='layer-'] > * > [class*='wrapper-'] {
[class*='layers-'] > [class*='layer-'] > * > [class*='wrapper-'] {
padding-top: 49px !important;
}
.platform-osx [class*='layer-'] > * > [class*='wrapper-'] {
.platform-osx [class*='layers-'] > [class*='layer-'] > * > [class*='wrapper-'] {
margin-top: 26px;
}
[class*='layer-'] > * > [class*='wrapper-'] + [class*='flex'] {
.platform-osx [class*='layers-'] > [class*='layer-'] > * > [class*='wrapper-'] [class*='listItem-']:first-child {
margin-top: 12px;
}
[class*='layers-'] > [class*='layer-'] > * > [class*='wrapper-'] + [class*='flex'] {
border-radius: 0 0 0 5px;
}

View File

@ -1,6 +1,8 @@
.bd-scrollerWrap {
display: flex;
width: 100%;
height: 100%;
flex-grow: 1;
.bd-scroller {
@include scrollbar;

View File

@ -17,18 +17,13 @@
flex-direction: column;
flex-grow: 1;
> .bd-scrollerWrap {
flex-grow: 1;
> .bd-scroller {
overflow-y: scroll;
}
> .bd-scrollerWrap > .bd-scroller {
overflow-y: scroll;
}
.bd-settings & {
.platform-darwin & { // sass-lint:disable-line class-name-format
padding-top: 22px;
}
.platform-darwin .bd-settings &.bd-settingswrapNoscroller, // sass-lint:disable-line class-name-format
.platform-darwin .bd-settings & > .bd-scrollerWrap > .bd-scroller { // sass-lint:disable-line class-name-format
padding-top: 22px;
}
.bd-settingswrapHeader {

View File

@ -12,12 +12,14 @@ import { Module, Reflection } from 'modules';
const normalizedPrefix = 'da';
const randClass = new RegExp(`^(?!${normalizedPrefix}-)((?:[A-Za-z]|[0-9]|-)+)-(?:[A-Za-z]|[0-9]|-|_){6}$`);
const normalisedClass = /^(([a-zA-Z0-9]+)-[^\s]{6}) da-([a-zA-Z0-9]+)$/;
export default class ClassNormaliser extends Module {
init() {
this.patchClassModules(Reflection.module.getModule(this.moduleFilter.bind(this), false));
this.normalizeElement(document.querySelector('#app-mount'));
this.patchDOMMethods();
}
patchClassModules(modules) {
@ -26,6 +28,36 @@ export default class ClassNormaliser extends Module {
}
}
patchDOMMethods() {
const add = DOMTokenList.prototype.add;
DOMTokenList.prototype.add = function(...tokens) {
for (const token of tokens) {
let match;
if (typeof token === 'string' && (match = token.match(normalisedClass)) && match[2] === match[3]) return add.call(this, match[1]);
return add.call(this, token);
}
};
const remove = DOMTokenList.prototype.remove;
DOMTokenList.prototype.remove = function(...tokens) {
for (const token of tokens) {
let match;
if (typeof token === 'string' && (match = token.match(normalisedClass)) && match[2] === match[3]) return remove.call(this, match[1]);
return remove.call(this, token);
}
};
const contains = DOMTokenList.prototype.contains;
DOMTokenList.prototype.contains = function(token) {
let match;
if (typeof token === 'string' && (match = token.match(normalisedClass)) && match[2] === match[3]) return contains.call(this, match[1]);
return contains.call(this, token);
};
}
shouldIgnore(value) {
if (!isNaN(value)) return true;
if (value.endsWith('px') || value.endsWith('ch') || value.endsWith('em') || value.endsWith('ms')) return true;

View File

@ -62,7 +62,7 @@
SettingsWrapper
},
methods: {
showConnectWindow() {
showConnectWindow() {
if (this.connecting) return;
this.connecting = true;
const x = (window.screenX + window.outerWidth / 2) - 520 / 2;

View File

@ -15,7 +15,7 @@
<Button v-if="devmode && !plugin.packed" v-tooltip="'Package Plugin'" @click="package"><MiBoxDownload 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-if="devmode && !plugin.packed" 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>
@ -33,14 +33,19 @@
export default {
data() {
return {
devmode: Settings.getSetting('core', 'advanced', 'developer-mode').value
}
devmodeSetting: Settings.getSetting('core', 'advanced', 'developer-mode')
};
},
props: ['plugin'],
components: {
Card, Button, ButtonGroup, SettingSwitch,
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension, MiBoxDownload
},
computed: {
devmode() {
return this.devmodeSetting.value;
}
},
methods: {
async package() {
try {

View File

@ -34,9 +34,11 @@
<div class="bd-spinner7" />
</div>
<div class="bd-searchHint">{{searchHint}}</div>
<div class="bd-fancySearch" :class="{'bd-disabled': loadingOnline, 'bd-active': loadingOnline || (onlinePlugins && onlinePlugins.docs)}">
<input type="text" class="bd-textInput" placeholder="Search" @keydown.enter="searchInput" @keyup.stop :value="onlinePlugins.filters.sterm" />
</div>
<form @submit.prevent="refreshOnline">
<div class="bd-fancySearch" :class="{'bd-disabled': loadingOnline, 'bd-active': loadingOnline || (onlinePlugins && onlinePlugins.docs)}">
<input type="text" class="bd-textInput" placeholder="Search" v-model="onlinePlugins.filters.sterm" :disabled="loadingOnline" @input="search" @keyup.stop/>
</div>
</form>
</div>
<div class="bd-flex bd-flexRow" v-if="onlinePlugins && onlinePlugins.docs && onlinePlugins.docs.length">
<div class="bd-searchSort bd-flex bd-flexGrow">
@ -49,11 +51,13 @@
</div>
</div>
</div>
<ScrollerWrap class="bd-onlinePhBody" v-if="!loadingOnline && onlinePlugins" :scrollend="scrollend">
<RemoteCard v-if="onlinePlugins && onlinePlugins.docs" v-for="plugin in onlinePlugins.docs" :key="onlinePlugins.id" :item="plugin" :tagClicked="searchByTag" />
<div class="bd-spinnerContainer">
<div v-if="loadingMore" class="bd-spinner7" />
</div>
<ScrollerWrap class="bd-onlinePhBody" @scrollend="scrollend">
<template v-if="!loadingOnline && onlinePlugins">
<RemoteCard v-if="onlinePlugins && onlinePlugins.docs" v-for="plugin in onlinePlugins.docs" :key="onlinePlugins.id" :item="plugin" @tagclicked="searchByTag" />
<div class="bd-spinnerContainer">
<div v-if="loadingMore" class="bd-spinner7" />
</div>
</template>
</ScrollerWrap>
</div>
</div>
@ -94,7 +98,8 @@
},
loadingOnline: false,
loadingMore: false,
searchHint: ''
searchHint: '',
searchTimeout: null
};
},
components: {
@ -107,8 +112,9 @@
showLocal() {
this.local = true;
},
showOnline() {
async showOnline() {
this.local = false;
if (!this.onlinePlugins.pagination.total) await this.refreshOnline();
},
async refreshLocal() {
await this.PluginManager.refreshPlugins();
@ -156,11 +162,6 @@
dont_clone
});
},
searchInput(e) {
if (this.loadingOnline || this.loadingMore) return;
this.onlinePlugins.filters.sterm = e.target.value;
this.refreshOnline();
},
async scrollend(e) {
if (this.onlinePlugins.pagination.page >= this.onlinePlugins.pagination.pages) return;
if (this.loadingOnline || this.loadingMore) return;
@ -193,6 +194,10 @@
}
this.refreshOnline();
},
async search() {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(this.refreshOnline, 1000);
},
async searchByTag(tag) {
if (this.loadingOnline || this.loadingMore) return;
this.onlinePlugins.filters.sterm = tag;

View File

@ -27,7 +27,7 @@
<div class="bd-flexRow bd-flex bd-flexGrow">
<div class="bd-flexGrow bd-remoteCardTags">
<div v-for="(tag, index) in item.tags" class="bd-remoteCardTag">
<div @click="tagClicked(tag)">{{tag}}</div><span v-if="index + 1 < item.tags.length">, </span>
<div @click="$emit('tagclicked', tag)">{{tag}}</div><span v-if="index + 1 < item.tags.length">, </span>
</div>
</div>
<div class="bd-buttonGroup">
@ -44,7 +44,7 @@
import { shell } from 'electron';
export default {
props: ['item', 'tagClicked'],
props: ['item'],
data() {
return {}
},

View File

@ -9,8 +9,8 @@
*/
<template>
<div class="bd-settingswrap">
<div v-if="noscroller" class="bd-flex bd-flexCol">
<div class="bd-settingswrap" :class="{'bd-settingswrapNoscroller': noscroller}">
<div v-if="noscroller" class="bd-flex bd-flexCol bd-flexGrow">
<div class="bd-settingswrapHeader">
<span class="bd-settingswrapHeaderText">{{ headertext }}</span>
<slot name="header" />
@ -19,7 +19,7 @@
<slot />
</div>
</div>
<ScrollerWrap v-else :scrollend="scrollend">
<ScrollerWrap v-else @scrollend="$emit('scrollend', $event)">
<div class="bd-settingswrapHeader">
<span class="bd-settingswrapHeaderText">{{ headertext }}</span>
<slot name="header" />
@ -36,7 +36,7 @@
import { ScrollerWrap } from '../common';
export default {
props: ['headertext', 'scrollend', 'noscroller'],
props: ['headertext', 'noscroller'],
components: {
ScrollerWrap
}

View File

@ -15,7 +15,7 @@
<Button v-if="devmode && !theme.packed" v-tooltip="'Package Theme'" @click="package"><MiBoxDownload 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-if="devmode && !theme.packed" 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>
@ -33,14 +33,19 @@
export default {
data() {
return {
devmode: Settings.getSetting('core', 'advanced', 'developer-mode').value
}
devmodeSetting: Settings.getSetting('core', 'advanced', 'developer-mode')
};
},
props: ['theme', 'online'],
components: {
Card, Button, ButtonGroup, SettingSwitch,
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension, MiBoxDownload
},
computed: {
devmode() {
return this.devmodeSetting.value;
}
},
methods: {
async package() {
try {

View File

@ -34,9 +34,11 @@
<div class="bd-spinner7" />
</div>
<div class="bd-searchHint">{{searchHint}}</div>
<div class="bd-fancySearch" :class="{'bd-disabled': loadingOnline, 'bd-active': loadingOnline || (onlineThemes && onlineThemes.docs)}">
<input type="text" class="bd-textInput" placeholder="Search" @keydown.enter="searchInput" @keyup.stop :value="onlineThemes.filters.sterm"/>
</div>
<form @submit.prevent="refreshOnline">
<div class="bd-fancySearch" :class="{'bd-disabled': loadingOnline, 'bd-active': loadingOnline || (onlineThemes && onlineThemes.docs)}">
<input type="text" class="bd-textInput" placeholder="Search" v-model="onlineThemes.filters.sterm" :disabled="loadingOnline" @input="search" @keyup.stop/>
</div>
</form>
</div>
<div class="bd-flex bd-flexRow" v-if="onlineThemes && onlineThemes.docs && onlineThemes.docs.length">
<div class="bd-searchSort bd-flex bd-flexGrow">
@ -48,11 +50,13 @@
</div>
</div>
</div>
<ScrollerWrap class="bd-onlinePhBody" v-if="!loadingOnline && onlineThemes" :scrollend="scrollend">
<RemoteCard v-if="onlineThemes && onlineThemes.docs" v-for="theme in onlineThemes.docs" :key="theme.id" :item="theme" :tagClicked="searchByTag"/>
<div class="bd-spinnerContainer">
<div v-if="loadingMore" class="bd-spinner7"/>
</div>
<ScrollerWrap class="bd-onlinePhBody" @scrollend="scrollend">
<template v-if="!loadingOnline && onlineThemes">
<RemoteCard v-if="onlineThemes && onlineThemes.docs" v-for="theme in onlineThemes.docs" :key="theme.id" :item="theme" @tagclicked="searchByTag"/>
<div class="bd-spinnerContainer">
<div v-if="loadingMore" class="bd-spinner7"/>
</div>
</template>
</ScrollerWrap>
</div>
</div>
@ -93,7 +97,8 @@
},
loadingOnline: false,
loadingMore: false,
searchHint: ''
searchHint: '',
searchTimeout: null
};
},
components: {
@ -108,6 +113,7 @@
},
async showOnline() {
this.local = false;
if (!this.onlineThemes.pagination.total) await this.refreshOnline();
},
async refreshLocal() {
await this.ThemeManager.refreshThemes();
@ -154,11 +160,6 @@
dont_clone
});
},
searchInput(e) {
if (this.loadingOnline || this.loadingMore) return;
this.onlineThemes.filters.sterm = e.target.value;
this.refreshOnline();
},
async scrollend(e) {
if (this.onlineThemes.pagination.page >= this.onlineThemes.pagination.pages) return;
if (this.loadingOnline || this.loadingMore) return;
@ -191,6 +192,10 @@
}
this.refreshOnline();
},
async search() {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(this.refreshOnline, 1000);
},
async searchByTag(tag) {
if (this.loadingOnline || this.loadingMore) return;
this.onlineThemes.filters.sterm = tag;

View File

@ -12,7 +12,7 @@
<div class="bd-settingSwitch">
<div class="bd-title">
<h3>{{item.text}}</h3>
<div class="bd-switchWrapper" @click="() => toggle(item)">
<div class="bd-switchWrapper" @click="$emit('toggle', item)">
<input type="checkbox" class="bd-switchCheckbox" />
<div class="bd-switch" :class="{'bd-checked': item.status.update}" />
</div>
@ -23,6 +23,6 @@
<script>
export default {
props: ['item', 'toggle']
props: ['item']
}
</script>

View File

@ -19,7 +19,7 @@
<div class="bd-formDivider"></div>
<div v-for="update in bdUpdates">
<UpdaterStatus :item="update" v-if="update.status.updating" />
<UpdaterToggle :item="update" :toggle="() => updater.toggleUpdate(update)" v-else />
<UpdaterToggle :item="update" @toggle="updater.toggleUpdate(update)" v-else />
<div class="bd-formDivider"></div>
</div>
</div>

View File

@ -51,7 +51,7 @@
},
removeItem(file_path) {
if (this.setting.disabled) return;
this.setting.value = Utils.removeFromArray(this.setting.value, file_path);
this.setting.value = this.setting.value.filter(f => f !== file_path);
}
}
}

View File

@ -10,13 +10,13 @@
<template>
<div class="bd-dropdown" :class="{'bd-active': active, 'bd-disabled': disabled}">
<div class="bd-dropdownCurrent" @click="() => active = !active && !disabled">
<span class="bd-dropdownText">{{ getSelectedText() }}</span>
<div class="bd-dropdownCurrent" @click.stop="() => active = !active && !disabled">
<span class="bd-dropdownText">{{ selectedText }}</span>
<span class="bd-dropdownArrowWrap">
<span class="bd-dropdownArrow"></span>
</span>
</div>
<div class="bd-dropdownOptions bd-flex bd-flexCol" ref="options" v-if="active">
<div class="bd-dropdownOptions bd-flex bd-flexCol" ref="options" v-if="active && !disabled">
<div class="bd-dropdownOption" v-for="option in options" :class="{'bd-dropdownOptionSelected': value === option.value}" @click="select(option)">{{ option.text }}</div>
</div>
</div>
@ -31,11 +31,15 @@
clickHandler: null
};
},
methods: {
getSelectedText() {
const selected_option = this.options.find(option => option.value === this.value);
return selected_option ? selected_option.text : this.value;
computed: {
selectedOption() {
return this.options.find(option => option.value === this.value);
},
selectedText() {
return this.selectedOption ? this.selectedOption.text : this.value;
}
},
methods: {
select(option) {
this.$emit('input', option.value);
this.active = false;

View File

@ -18,12 +18,11 @@
<script>
export default {
props: ['dark', 'scrollend'],
props: ['dark'],
methods: {
onscroll(e) {
if (!this.scrollend) return;
const { offsetHeight, scrollTop, scrollHeight } = e.target;
if (offsetHeight + scrollTop >= scrollHeight) this.scrollend(e);
if (offsetHeight + scrollTop >= scrollHeight) this.$emit('scrollend', e);
}
}
}

View File

@ -22,6 +22,6 @@
<script>
export default {
props: ['item', 'onClick', 'checked']
props: ['item', 'checked']
}
</script>

View File

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

View File

@ -9,15 +9,15 @@
*/
<template>
<div class="bd-buttonGroup" :class="classes">
<Button v-for="(button, index) in buttons" :text="button.text" :classes="button.classes" :onClick="button.onClick" :key="index"/>
<div class="bd-buttonGroup">
<Button v-for="(button, index) in buttons" :text="button.text" :classes="button.class" @click="button.onClick" :key="index"/>
</div>
</template>
<script>
import Button from './Button.vue';
export default {
props: ['buttons', 'classes'],
props: ['buttons'],
components: { Button }
}
</script>

View File

@ -84,7 +84,7 @@ export class DiscordContextMenu {
}
static renderCm(component, args, retVal, res) {
if (!retVal.props || !res.props) return;
if (!retVal.props || !retVal.props.style || !res.props) return;
const { target } = component.props;
const { top, left } = retVal.props.style;
if (!target || !top || !left) return;

View File

@ -71,7 +71,7 @@ export default class extends Module {
const c = contributors.find(c => c.id === user.id);
if (!c) return;
const nameTag = retVal.props.children.props.children[1].props.children[0];
const nameTag = retVal.props.children[1].props.children[0].props.children[0];
nameTag.type = this.PatchedNameTag || nameTag.type;
});
@ -89,26 +89,24 @@ export default class extends Module {
const NameTag = await ReactComponents.getComponent('NameTag');
this.PatchedNameTag = class extends NameTag.component {
render() {
const retVal = NameTag.component.prototype.render.call(this, arguments);
try {
if (!retVal.props || !retVal.props.children) return;
this.PatchedNameTag = function (props) {
const retVal = NameTag.component.apply(this, arguments);
try {
if (!retVal.props || !retVal.props.children) return retVal;
const user = ReactHelpers.findProp(this, 'user');
if (!user) return;
const contributor = contributors.find(c => c.id === user.id);
if (!contributor) return;
const user = ReactHelpers.findProp(props, 'user');
if (!user) return retVal;
const contributor = contributors.find(c => c.id === user.id);
if (!contributor) return retVal;
retVal.props.children.splice(1, 0, VueInjector.createReactElement(BdBadge, {
contributor,
type: 'nametag'
}));
} catch (err) {
Logger.err('ProfileBadges', ['Error thrown while rendering a NameTag', err]);
}
return retVal;
retVal.props.children.splice(1, 0, VueInjector.createReactElement(BdBadge, {
contributor,
type: 'nametag'
}));
} catch (err) {
Logger.err('ProfileBadges', ['Error thrown while rendering a NameTag', err]);
}
return retVal;
};
// Rerender all channel members

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { Reflection } from 'modules';
import { Reflection, ReactComponents } from 'modules';
import Vue from 'vue';
export default class {
@ -16,12 +16,12 @@ export default class {
/**
* Creates a new Vue object and mounts it in the passed element.
* @param {HTMLElement} root The element to mount the new Vue object at
* @param {Object} options Options to pass to Vue
* @param {Object} options Options to pass to Vue (see https://vuejs.org/v2/api/#Options-Data)
* @param {BdNode} bdnode The element to append
* @return {Vue}
*/
static inject(root, options, bdnode) {
if(bdnode) bdnode.appendTo(root);
if (bdnode) bdnode.appendTo(root);
const vue = new Vue(options);
@ -37,18 +37,18 @@ export default class {
* @return {React.Element}
*/
static createReactElement(component, props, mountAtTop) {
const { React } = Reflection.modules;
return React.createElement(this.ReactCompatibility, {component, mountAtTop, props});
return Reflection.modules.React.createElement(this.ReactCompatibility, {component, mountAtTop, props});
}
static get ReactCompatibility() {
if (this._ReactCompatibility) return this._ReactCompatibility;
const { React, ReactDOM } = Reflection.modules;
const { React, ReactDOM} = Reflection.modules;
return this._ReactCompatibility = class VueComponent extends React.Component {
/**
* A React component that renders a Vue component.
*/
const ReactCompatibility = class VueComponent extends React.Component {
render() {
return React.createElement('span');
return React.createElement('span', {className: 'bd-reactVueComponent'});
}
componentDidMount() {
@ -89,7 +89,13 @@ export default class {
}
}));
}
}
};
// Add a name for ReactComponents
ReactCompatibility.displayName = 'BD.VueComponent';
ReactCompatibility[ReactComponents.ReactComponent.important] = {selector: '.bd-reactVueComponent'};
return Object.defineProperty(this, 'ReactCompatibility', {value: ReactCompatibility}).ReactCompatibility;
}
static install(Vue) {
@ -98,6 +104,9 @@ export default class {
}
/**
* A Vue component that renders a React component.
*/
export const ReactComponent = {
props: ['component', 'component-props', 'component-children', 'react-element'],
render(createElement) {

View File

@ -17,7 +17,7 @@ const config = {
})
],
externals: {
sparkplug: 'require("./sparkplug")'
sparkplug: 'require("../core/sparkplug")'
}
};

View File

@ -37,7 +37,6 @@ const TEST_ARGS = () => {
'data': path.resolve(_baseDataPath, 'data'),
'editor': path.resolve(_basePath, 'editor', 'dist'),
// tmp: path.join(_basePath, 'tmp')
tmp: path.join(os.tmpdir(), 'betterdiscord', `${process.getuid()}`)
}
}
}
@ -83,6 +82,12 @@ class Comms {
});
});
BDIpc.on('bd-native-save', (event, options) => {
dialog.showSaveDialog(OriginalBrowserWindow.fromWebContents(event.ipcEvent.sender), options, filename => {
event.reply(filename);
});
});
BDIpc.on('bd-compileSass', (event, options) => {
if (typeof options.path === 'string' && typeof options.data === 'string') {
options.data = `${options.data} @import '${options.path.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}';`;
@ -103,7 +108,7 @@ class Comms {
BDIpc.on('bd-keytar-find-credentials', (event, { service }) => keytar.findCredentials(service), true);
BDIpc.on('bd-readDataFile', async (event, fileName) => {
const rf = await FileUtils.readFile(path.resolve(configProxy().getPath('data'), fileName));
const rf = await FileUtils.readFile(path.resolve(this.bd.config.getPath('data'), fileName));
event.reply(rf);
});
@ -197,6 +202,11 @@ class BrowserWindow extends OriginalBrowserWindow {
}
}
// Some Electron APIs depend on browserWindow.constructor being BrowserWindow
Object.defineProperty(BrowserWindow.prototype, 'constructor', {
value: OriginalBrowserWindow
});
export class BetterDiscord {
get comms() { return this._comms ? this._comms : (this._commas = new Comms(this)); }
@ -207,9 +217,13 @@ export class BetterDiscord {
get updater() { return this._updater ? this._updater : (this._updater = new Updater(this)); }
get sendToDiscord() { return this.windowUtils.send; }
constructor(args) {
if (TESTS) args = TEST_ARGS();
constructor(...args) {
if (TESTS) args.unshift(TEST_ARGS());
args = deepmerge.all(args);
console.log('[BetterDiscord|args] ', JSON.stringify(args, null, 4));
if (BetterDiscord.loaded) {
console.log('[BetterDiscord] Creating two BetterDiscord objects???');
return null;
@ -337,6 +351,10 @@ export class BetterDiscord {
this.config.addPath('userfiles', userfiles);
this.config.addPath('snippets', snippets);
if (!this.config.getPath('editor')) this.config.addPath('editor', path.resolve(base, 'editor'));
if (!this.config.getPath('tmp')) this.config.addPath('tmp', process.platform !== 'win32' ?
path.join(os.tmpdir(), 'betterdiscord', `${process.getuid()}`) :
path.join(os.tmpdir(), 'betterdiscord'));
}
/**

View File

@ -9,6 +9,8 @@
*/
import Module from './modulebase';
import path from 'path';
import os from 'os';
export default class Config extends Module {
@ -72,6 +74,9 @@ export default class Config extends Module {
// Compatibility with old client code and new installer args
compatibility() {
this.args.paths = Object.entries(this.args.paths).map(([id, path]) => ({ id, path }));
this.args.paths = Object.entries(this.args.paths).map(([id, _path]) => ({
id, path: path.resolve(os.homedir(), _path)
}));
}
}

View File

@ -14,6 +14,7 @@ import semver from 'semver';
import Axi from './axi';
import zlib from 'zlib';
import tarfs from 'tar-fs';
import path from 'path';
const TEST_UPDATE = [
{
@ -91,7 +92,8 @@ export default class Updater extends Module {
async updateBd(update) {
try {
console.log('[BetterDiscord:Updater] Updating', update.id);
await this.downloadTarGz(`https://github.com/JsSucks/BetterDiscordApp${update.remote}`, this.bd.config.getPath('base'));
await this.downloadTarGz(`https://github.com/JsSucks/BetterDiscordApp${update.remote}`, this.bd.config.getPath('tmp'));
await FileUtils.rn(path.join(this.bd.config.getPath('tmp'), update.id), this.bd.config.getPath(update.id));
this.updateFinished(update);
// Cleanup
await FileUtils.rm(`${this.bd.config.getPath(update.id)}_old`);

View File

@ -84,14 +84,7 @@ gulp.task('client-pkg', function() {
]);
});
gulp.task('client-sparkplug', function() {
return pump([
gulp.src('core/dist/sparkplug.js'),
gulp.dest('release/client')
]);
});
gulp.task('client-release', gulp.parallel('client-main', 'client-pkg', 'client-sparkplug'));
gulp.task('client-release', gulp.parallel('client-main', 'client-pkg'));
// Editor

View File

@ -1,16 +1,21 @@
const bdinfo = require('./bd.json');
const bdinfo = require('./bd');
const { app } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
const Module = require('module');
const packagePath = path.join(__dirname, '..', 'app.asar');
const packagePath = path.resolve(__dirname, '..', 'app.asar');
app.getAppPath = () => packagePath;
function loadBd() {
const { paths } = bdinfo;
const { BetterDiscord } = require(paths.core);
const instance = new BetterDiscord(bdinfo);
const userconfig = (() => {
try {
return require(path.resolve(os.homedir(), bdinfo.paths.userconfig));
} catch (err) {}
})() || {};
const { BetterDiscord } = require(path.resolve(os.homedir(), (userconfig.paths || {}).core || bdinfo.paths.core));
const instance = new BetterDiscord(bdinfo, userconfig);
}
app.on('ready', loadBd);

View File

@ -1,7 +1,9 @@
const args = process.argv;
const process = require('process');
const fs = require('fs');
const path = require('path');
const args = process.argv;
const useBdRelease = args[2] && args[2].toLowerCase() === 'release';
const releaseInput = useBdRelease ? args[3] && args[3].toLowerCase() : args[2] && args[2].toLowerCase();
const release = releaseInput === 'canary' ? 'Discord Canary' : releaseInput === 'ptb' ? 'Discord PTB' : 'Discord';
@ -16,7 +18,7 @@ const discordPath = (function() {
} else if (process.platform === 'darwin') {
const appPath = releaseInput === 'canary' ? path.join('/Applications', 'Discord Canary.app')
: releaseInput === 'ptb' ? path.join('/Applications', 'Discord PTB.app')
: useBdRelease && args[3] ? args[3] ? args[2] : args[2]
: useBdRelease && args[3] ? args[3] : !useBdRelease && args[2] ? args[2]
: path.join('/Applications', 'Discord.app');
return path.join(appPath, 'Contents', 'Resources');
@ -31,30 +33,53 @@ console.log(`Found ${release} in ${discordPath}`);
const appPath = path.join(discordPath, 'app');
const packageJson = path.join(appPath, 'package.json');
const indexJs = path.join(appPath, 'index.js');
const bdJson = path.join(appPath, 'bd.json');
if (!fs.existsSync(appPath)) fs.mkdirSync(appPath);
if (fs.existsSync(packageJson)) fs.unlinkSync(packageJson);
if (fs.existsSync(indexJs)) fs.unlinkSync(indexJs);
const bdPath = useBdRelease ? path.resolve(__dirname, '..', 'release') : path.resolve(__dirname, '..');
if (fs.existsSync(bdJson)) fs.unlinkSync(bdJson);
console.log(`Writing package.json`);
fs.writeFileSync(packageJson, JSON.stringify({
fs.writeFileSync(path.join(appPath, 'package.json'), JSON.stringify({
name: 'betterdiscord',
description: 'BetterDiscord',
main: 'index.js',
private: true
}, null, 4));
console.log(`Writing index.js`);
fs.writeFileSync(indexJs, `const path = require('path');
const fs = require('fs');
const Module = require('module');
const electron = require('electron');
const basePath = path.join(__dirname, '..', 'app.asar');
electron.app.getAppPath = () => basePath;
Module._load(basePath, null, true);
electron.app.on('ready', () => new (require('${bdPath.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}').BetterDiscord)());
`);
if (useBdRelease) {
console.log(`Writing index.js`);
fs.writeFileSync(path.join(appPath, 'index.js'), fs.readFileSync(path.resolve(__dirname, '..', 'installer', 'stub.js')));
console.log(`Writing bd.json`);
fs.writeFileSync(path.join(appPath, 'bd.json'), JSON.stringify({
options: {
autoInject: true,
commonCore: true,
commonData: true
},
paths: {
core: path.resolve(__dirname, '..', 'release', 'core'),
client: path.resolve(__dirname, '..', 'release', 'client'),
editor: path.resolve(__dirname, '..', 'release', 'editor'),
data: path.resolve(__dirname, '..', 'release', 'data'),
// tmp: path.resolve(os.tmpdir(), 'betterdiscord', `${process.getuid()}`)
}
}, null, 4));
} else {
const bdPath = path.resolve(__dirname, '..');
console.log(`Writing index.js`);
fs.writeFileSync(path.join(appPath, 'index.js'), `const path = require('path');
const fs = require('fs');
const Module = require('module');
const electron = require('electron');
const basePath = path.join(__dirname, '..', 'app.asar');
electron.app.getAppPath = () => basePath;
Module._load(basePath, null, true);
electron.app.on('ready', () => new (require('${bdPath.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}').BetterDiscord)());
`);
}
console.log(`Injection successful, please restart ${release}.`);

View File

@ -101,15 +101,15 @@ module.exports = (Plugin, Api, Vendor) => {
if (!returnValue.props.children instanceof Array) returnValue.props.children = [returnValue.props.children];
// Add a generic Button component provided by BD
returnValue.props.children.push(Api.Components.ButtonGroup({
classes: [ 'exampleBtnGroup' ], // Additional classes for button group
class: [ 'exampleBtnGroup' ], // Additional classes for button group
buttons: [
{
classes: ['exampleBtn'], // Additional classes for button
class: ['exampleBtn'], // Additional classes for button
text: 'Hello World!', // Text for button
onClick: e => Logger.log('Hello World!') // Button click handler
},
{
classes: ['exampleBtn'],
class: ['exampleBtn'],
text: 'Button',
onClick: e => Logger.log('Button!')
}