Merge pull request #165 from JsSucks/emotes-and-databases
Emotes and databases
This commit is contained in:
commit
ad522dd745
|
@ -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>
|
|
@ -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
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,3 +8,4 @@
|
|||
@import './drawers.scss';
|
||||
@import './preformatted.scss';
|
||||
@import './refreshbtn.scss';
|
||||
@import './autocomplete.scss';
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue