Merge pull request #165 from JsSucks/emotes-and-databases

Emotes and databases
This commit is contained in:
Alexei Stukov 2018-03-10 10:26:49 +02:00 committed by GitHub
commit ad522dd745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 347 additions and 27 deletions

View File

@ -0,0 +1,33 @@
/**
* 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

@ -7,21 +7,26 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Events } from 'modules';
import { FileUtils } from 'common';
import { Events, Globals } from 'modules';
import { DOM, VueInjector } from 'ui';
import EditedTimeStamp from './EditedTimeStamp.vue';
import EmoteComponent from './EmoteComponent.vue';
import TwitchEmotes from '../data/twitch_emotes.json';
let emotes = null;
export default class {
static observe() {
Events.on('ui:mutable:.markup', markup => {
this.injectEmotes(markup);
});
static async observe() {
const dataPath = Globals.getObject('paths').find(path => path.id === 'data').path;
try {
emotes = await FileUtils.readJsonFromFile(dataPath + '/emotes.json');
Events.on('ui:mutable:.markup',
markup => {
if (!emotes) return;
this.injectEmotes(markup);
});
} catch (err) {
console.log(err);
}
}
static injectEmotes(node) {
@ -84,11 +89,30 @@ export default class {
}
static isEmote(word) {
if (!emotes) return null;
const name = word.replace(/:/g, '');
if (TwitchEmotes.hasOwnProperty(name)) {
const src = `https://static-cdn.jtvnw.net/emoticons/v1/${TwitchEmotes[name]}/1.0`;
return { name, src };
}
return null;
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) };
}
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) {
let index = 0;
return emotes.filter(emote => {
if (index >= limit) return false;
if (regex.test(emote.id)) {
index++;
return true;
}
});
}
}

File diff suppressed because one or more lines are too long

View File

@ -13,7 +13,7 @@ import BdCss from './styles/index.scss';
import { Events, CssEditor, Globals, ExtModuleManager, PluginManager, ThemeManager, ModuleManager, WebpackModules, Settings, Database } from 'modules';
import { ClientLogger as Logger, ClientIPC } from 'common';
import { EmoteModule } from 'builtin';
const ignoreExternal = false;
const ignoreExternal = true;
class BetterDiscord {
@ -31,7 +31,6 @@ class BetterDiscord {
window.bdlogs = Logger;
window.emotes = EmoteModule;
window.dom = DOM;
EmoteModule.observe();
DOM.injectStyle(BdCss, 'bdmain');
Events.on('global-ready', this.globalReady.bind(this));
@ -52,6 +51,7 @@ class BetterDiscord {
Modals.showContentManagerErrors();
Events.emit('ready');
Events.emit('discord-ready');
EmoteModule.observe();
} catch (err) {
Logger.err('main', ['FAILED TO LOAD!', err]);
}

View File

@ -0,0 +1,76 @@
.bd-autocomplete {
border-radius: 5px 5px 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
z-index: 3;
bottom: 100%;
left: 0;
position: absolute;
right: 0;
background-color: #2f3136;
.bd-autocomplete-inner {
padding-bottom: 8px;
white-space: nowrap;
.bd-autocompleteRow {
padding: 0 8px;
font-size: 14px;
line-height: 16px;
.bd-autocompleteSelector {
border-radius: 3px;
padding: 8px;
&.bd-selectable {
cursor: pointer;
}
&.bd-selected {
background-color: #36393f;
}
.bd-autocompleteTitle {
color: #72767d;
padding: 4px 0;
text-transform: uppercase;
font-weight: 600;
line-height: 16px;
font-size: 12px;
strong {
color: #fff;
text-transform: none;
font-weight: 500;
}
}
.bd-autocompleteField {
display: flex;
flex: 1 1 auto;
color: #f6f6f7;
min-height: 16px;
-webkit-box-direction: normal;
-webkit-box-orient: horizontal;
flex-direction: row;
flex-wrap: nowrap;
-webkit-box-pack: start;
justify-content: flex-start;
-webkit-box-align: center;
align-items: center;
img {
min-width: 16px;
width: 16px;
}
div {
margin-left: 8px;
color: #f6f6f7;
}
}
}
}
}
}

View File

@ -8,3 +8,4 @@
@import './drawers.scss';
@import './preformatted.scss';
@import './refreshbtn.scss';
@import './autocomplete.scss';

View File

@ -13,6 +13,7 @@ import Reflection from './reflection';
import DOM from './dom';
import VueInjector from './vueinjector';
import EditedTimeStamp from './components/common/EditedTimestamp.vue';
import Autocomplete from './components/common/Autocomplete.vue';
class TempApi {
static get currentGuildId() {
@ -42,6 +43,7 @@ export default class extends EventListener {
constructor() {
super();
window.injectAc = this.injectAutocomplete;
const messageFilter = function (m) {
return m.addedNodes && m.addedNodes.length && m.addedNodes[0].classList && m.addedNodes[0].classList.contains('message-group');
}
@ -75,7 +77,8 @@ export default class extends EventListener {
{ id: 'server-switch', callback: this.manipAll },
{ id: 'channel-switch', callback: this.manipAll },
{ id: 'discord:MESSAGE_CREATE', callback: this.markupInjector },
{ id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector }
{ id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector },
{ id: 'gkh:keyup', callback: this.injectAutocomplete }
];
}
@ -195,4 +198,20 @@ export default class extends EventListener {
get appMount() {
return document.getElementById('app-mount');
}
injectAutocomplete(e) {
if (document.querySelector('.bd-autocomplete')) return;
if (!e.target.closest('[class*=channelTextArea]')) return;
const root = document.createElement('span');
const parent = document.querySelector('[class*="channelTextArea"] > [class*="inner"]');
if (!parent) return;
parent.append(root);
VueInjector.inject(
root,
DOM.createElement('span'),
{ Autocomplete },
`<Autocomplete initial="${e.target.value}"/>`,
true
);
}
}

View File

@ -51,6 +51,7 @@ export default class {
server: TempApi.currentGuild,
channel: TempApi.currentChannel
};
window.addEventListener('keyup', e => Events.emit('gkh:keyup', e));
this.autoManip = new AutoManip();
const defer = setInterval(() => {
if (!this.profilePopupModule) return;

View File

@ -0,0 +1,128 @@
/**
* 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 v-if="emotes && emotes.length" class="bd-autocomplete-inner">
<div class="bd-autocompleteRow">
<div class="bd-autocompleteSelector">
<div class="bd-autocompleteTitle">
Emotes Matching:
<strong>{{title}}</strong>
</div>
</div>
</div>
<div v-for="(emote, index) in emotes" class="bd-autocompleteRow" :key="emote.id">
<div class="bd-autocompleteSelector bd-selectable" :class="{'bd-selected': index === selectedIndex}" @mouseover="() => { selected = emote.id }" @click="() => inject(emote)">
<div class="bd-autocompleteField">
<img :src="getEmoteSrc(emote)"/>
<div>{{emote.id}}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { EmoteModule } from 'builtin';
import { Events } from 'modules';
import { DOM } from 'ui';
export default {
data() {
return {
emotes: [],
title: '',
selIndex: 0,
selected: '',
open: false,
selectedIndex: 0,
sterm: ''
}
},
props: ['initial'],
beforeMount() {
// this.emotes = EmoteModule.filter(new RegExp(this.initial, 'i'), 10);
// this.open = this.emotes.length;
},
created() {
window.addEventListener('keydown', this.prevents);
const ta = document.querySelector('.chat textarea');
ta.addEventListener('keydown', this.setCaret);
ta.addEventListener('keyup', this.searchEmotes);
},
destroyed() {
window.removeEventListener('keydown', this.prevents);
const ta = document.querySelector('.chat textarea');
ta.removeEventListener('keydown', this.setCaret);
ta.removeEventListener('keyup', this.searchEmotes);
},
methods: {
prevents(e) {
if (!this.open) return;
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Tab') return;
e.stopPropagation();
e.preventDefault();
},
setCaret(e) {
this.caret = e.target.selectionEnd;
},
getEmoteSrc(emote) {
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 uri.replace(':id', value);
},
searchEmotes(e) {
if (e.key === 'ArrowDown' && this.open && this.caret) {
this.selectedIndex = (this.selectedIndex + 1) >= 10 ? 0 : this.selectedIndex + 1;
return;
} else if (e.key === 'ArrowUp' && this.open && this.caret) {
this.selectedIndex = (this.selectedIndex - 1) < 0 ? 9 : this.selectedIndex - 1;
return;
}
if (e.key === 'Tab' && this.open && this.caret) {
const selected = this.emotes[this.selectedIndex];
if (!selected) return;
this.inject(selected);
return;
}
const se = e.target.selectionEnd;
this.sterm = e.target.value.substr(0, se).split(' ').slice(-1).pop();
if (this.sterm.length < 3) {
this.emotes = [];
this.selected = '';
this.selectedIndex = 0;
return;
}
this.title = this.sterm;
this.emotes = EmoteModule.filter(new RegExp(this.sterm, ''), 10);
this.open = this.emotes.length;
},
inject(emote) {
const ta = document.querySelector('.chat textarea');
if (!ta) return;
const currentText = document.querySelector('.chat textarea').value;
const se = ta.selectionEnd;
const split = currentText.substr(0, se).split(' ');
split.pop();
split.push(`:${emote.id}:`);
const join = split.join(' ');
const rest = currentText.substr(se, currentText.length);
DOM.manip.setText(join + ' ' + rest, false);
this.emotes = [];
this.open = false;
this.selectedIndex = 0;
this.selected = '';
ta.selectionEnd = ta.selectionStart = se + `:${emote.id}:`.length - this.title.length;
}
}
}
</script>

View File

@ -85,8 +85,29 @@ class DOMObserver {
}
class Manip {
static setText(text, refocus) {
const activeElement = document.activeElement;
const txt = document.querySelector('.chat form textarea');
if (!txt) return;
txt.focus();
txt.select();
document.execCommand('insertText', false, text);
if (activeElement && refocus) activeElement.focus();
}
static getText() {
const txt = document.querySelector('.chat form textarea');
if (!txt) return '';
return txt.value;
}
}
export default class DOM {
static get manip() {
return Manip;
}
static get observer() {
return this._observer || (this._observer = new DOMObserver());
}

View File

@ -41,9 +41,7 @@ const dummyArgs = {
{ 'id': 'modules', 'path': __modulePath }
]
};
const dbInstance = new Database(dummyArgs.paths.find(path => path.id === 'data').path + '/storage');
const dbInstance = new Database(dummyArgs.paths.find(path => path.id === 'data').path);
console.log(dummyArgs);

View File

@ -15,23 +15,41 @@ class Database {
constructor(dbPath) {
this.exec = this.exec.bind(this);
this.update = this.update.bind(this);
this.db = new Datastore({ filename: dbPath, autoload: true });
this.init(dbPath);
//this.db.emotes.loadDatabase();
//this.db = new Datastore({ filename: dbPath, autoload: true });
}
async init(dbPath) {
this.db = {
storage: new Datastore({ filename: `${dbPath}/storage`, autoload: true })
};
}
async update(cmd) {
const db = cmd.db ? this.db[cmd.db] : this.db.storage;
return new Promise((resolve, reject) => {
this.db.update(cmd.args, cmd.data, { upsert: true }, (err, docs) => {
db.update(cmd.args, cmd.data, { upsert: true }, (err, docs) => {
if (err) return reject(err);
this.db.persistence.compactDatafile();
db.persistence.compactDatafile();
resolve(docs);
});
});
}
async find(cmd) {
console.log('FIND', cmd);
async loadDatabase(db) {
return new Promise((resolve, reject) => {
this.db.find(cmd.args, (err, docs) => {
db.loadDatabase();
resolve();
});
}
async find(cmd) {
const db = cmd.db ? this.db[cmd.db] : this.db.storage;
let args = cmd.args;
if (cmd.regex) args = { [cmd.regex.param]: new RegExp(cmd.regex.source, cmd.regex.flags) };
return new Promise((resolve, reject) => {
db.find(args, (err, docs) => {
if (err) return reject(err);
resolve(docs);
});