mirror of
https://github.com/xfgryujk/blivechat.git
synced 2024-12-26 12:50:33 +08:00
完成自定义表情前端
This commit is contained in:
parent
3da8cc4227
commit
903cfecab9
@ -336,7 +336,7 @@ export default class ChatClientDirect {
|
||||
medalLevel: roomId === this.roomId ? medalLevel : 0,
|
||||
id: getUuid4Hex(),
|
||||
translation: '',
|
||||
emoticon: info[0][13].url || null // TODO 改成对象?
|
||||
emoticon: info[0][13].url || null
|
||||
}
|
||||
this.onAddText(data)
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ export default class ChatClientRelay {
|
||||
let contentType = data[13]
|
||||
let contentTypeParams = data[14]
|
||||
if (contentType === CONTENT_TYPE_EMOTICON) {
|
||||
emoticon = contentTypeParams[0] // TODO 改成对象?
|
||||
emoticon = contentTypeParams[0]
|
||||
}
|
||||
|
||||
data = {
|
||||
|
@ -19,7 +19,6 @@ const CONTENTS = [
|
||||
'有一说一,这件事大家懂的都懂,不懂的,说了你也不明白,不如不说', '让我看看', '我柜子动了,我不玩了'
|
||||
]
|
||||
|
||||
// TODO 改成对象?
|
||||
const EMOTICONS = [
|
||||
'/static/img/emoticons/233.png',
|
||||
'/static/img/emoticons/miaoa.png',
|
||||
|
@ -9,11 +9,14 @@
|
||||
:isInMemberMessage="false" :authorName="authorName" :authorType="authorType" :privilegeType="privilegeType"
|
||||
></author-chip>
|
||||
<span id="message" class="style-scope yt-live-chat-text-message-renderer">
|
||||
<template v-if="!emoticon">{{ content }}</template>
|
||||
<img v-else class="emoji yt-formatted-string style-scope yt-live-chat-text-message-renderer"
|
||||
:src="emoticon" :alt="content" shared-tooltip-text="" id="emoji"
|
||||
>
|
||||
<el-badge :value="repeated" :max="99" v-show="repeated > 1" class="style-scope yt-live-chat-text-message-renderer"
|
||||
<template v-for="(content, index) in richContent">
|
||||
<span :key="index" v-if="content.type === CONTENT_TYPE_TEXT">{{ content.text }}</span>
|
||||
<img :key="index" v-else-if="content.type === CONTENT_TYPE_IMAGE"
|
||||
class="emoji yt-formatted-string style-scope yt-live-chat-text-message-renderer"
|
||||
:src="content.url" :alt="content.text" :shared-tooltip-text="content.text" :id="`emoji-${content.text}`"
|
||||
>
|
||||
</template>
|
||||
<el-badge :value="repeated" :max="99" v-if="repeated > 1" class="style-scope yt-live-chat-text-message-renderer"
|
||||
:style="{ '--repeated-mark-color': repeatedMarkColor }"
|
||||
></el-badge>
|
||||
</span>
|
||||
@ -42,11 +45,16 @@ export default {
|
||||
time: Date,
|
||||
authorName: String,
|
||||
authorType: Number,
|
||||
content: String,
|
||||
emoticon: String,
|
||||
richContent: Array,
|
||||
privilegeType: Number,
|
||||
repeated: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CONTENT_TYPE_TEXT: constants.CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_IMAGE: constants.CONTENT_TYPE_IMAGE
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
timeText() {
|
||||
return utils.getTimeTextHourMin(this.time)
|
||||
|
@ -34,6 +34,9 @@ export const MESSAGE_TYPE_SUPER_CHAT = 3
|
||||
export const MESSAGE_TYPE_DEL = 4
|
||||
export const MESSAGE_TYPE_UPDATE = 5
|
||||
|
||||
export const CONTENT_TYPE_TEXT = 0
|
||||
export const CONTENT_TYPE_IMAGE = 1
|
||||
|
||||
// 美元 -> 人民币 汇率
|
||||
const EXCHANGE_RATE = 7
|
||||
export const PRICE_CONFIGS = [
|
||||
@ -139,6 +142,17 @@ export function getShowContent(message) {
|
||||
return message.content
|
||||
}
|
||||
|
||||
export function getShowRichContent(message) {
|
||||
let richContent = [...message.richContent]
|
||||
if (message.translation) {
|
||||
richContent.push({
|
||||
type: CONTENT_TYPE_TEXT,
|
||||
text: `(${message.translation})`
|
||||
})
|
||||
}
|
||||
return richContent
|
||||
}
|
||||
|
||||
export function getGiftShowContent(message, showGiftName) {
|
||||
if (!showGiftName) {
|
||||
return ''
|
||||
|
@ -12,24 +12,37 @@
|
||||
<template v-for="message in messages">
|
||||
<text-message :key="message.id" v-if="message.type === MESSAGE_TYPE_TEXT"
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
:avatarUrl="message.avatarUrl" :time="message.time" :authorName="message.authorName"
|
||||
:authorType="message.authorType" :content="getShowContent(message)" :emoticon="message.emoticon"
|
||||
:privilegeType="message.privilegeType" :repeated="message.repeated"
|
||||
:time="message.time"
|
||||
:avatarUrl="message.avatarUrl"
|
||||
:authorName="message.authorName"
|
||||
:authorType="message.authorType"
|
||||
:privilegeType="message.privilegeType"
|
||||
:richContent="getShowRichContent(message)"
|
||||
:repeated="message.repeated"
|
||||
></text-message>
|
||||
<paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_GIFT"
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
:price="message.price" :avatarUrl="message.avatarUrl" :authorName="getShowAuthorName(message)"
|
||||
:time="message.time" :content="getGiftShowContent(message)"
|
||||
:time="message.time"
|
||||
:avatarUrl="message.avatarUrl"
|
||||
:authorName="getShowAuthorName(message)"
|
||||
:price="message.price"
|
||||
:content="getGiftShowContent(message)"
|
||||
></paid-message>
|
||||
<membership-item :key="message.id" v-else-if="message.type === MESSAGE_TYPE_MEMBER"
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
:avatarUrl="message.avatarUrl" :authorName="getShowAuthorName(message)" :privilegeType="message.privilegeType"
|
||||
:title="message.title" :time="message.time"
|
||||
:time="message.time"
|
||||
:avatarUrl="message.avatarUrl"
|
||||
:authorName="getShowAuthorName(message)"
|
||||
:privilegeType="message.privilegeType"
|
||||
:title="message.title"
|
||||
></membership-item>
|
||||
<paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_SUPER_CHAT"
|
||||
class="style-scope yt-live-chat-item-list-renderer"
|
||||
:price="message.price" :avatarUrl="message.avatarUrl" :authorName="getShowAuthorName(message)"
|
||||
:time="message.time" :content="getShowContent(message)"
|
||||
:time="message.time"
|
||||
:avatarUrl="message.avatarUrl"
|
||||
:authorName="getShowAuthorName(message)"
|
||||
:price="message.price"
|
||||
:content="getShowContent(message)"
|
||||
></paid-message>
|
||||
</template>
|
||||
</div>
|
||||
@ -137,6 +150,7 @@ export default {
|
||||
return constants.getGiftShowContent(message, this.showGiftName)
|
||||
},
|
||||
getShowContent: constants.getShowContent,
|
||||
getShowRichContent: constants.getShowRichContent,
|
||||
getShowAuthorName: constants.getShowAuthorName,
|
||||
|
||||
addMessage(message) {
|
||||
|
@ -1,10 +1,10 @@
|
||||
export default {
|
||||
sidebar: {
|
||||
home: 'Home',
|
||||
stylegen: 'Style generator',
|
||||
stylegen: 'Style Generator',
|
||||
help: 'Help',
|
||||
projectAddress: 'Project address',
|
||||
giftRecordOfficial: 'Official Super Chat record',
|
||||
projectAddress: 'Project Address',
|
||||
giftRecordOfficial: 'Official Super Chat Record',
|
||||
},
|
||||
home: {
|
||||
roomIdEmpty: "Room ID can't be empty",
|
||||
@ -38,8 +38,13 @@ export default {
|
||||
pinyin: 'Pinyin',
|
||||
kana: 'Kana',
|
||||
|
||||
emoticon: 'Custom Emotes',
|
||||
emoticonKeyword: 'Emote Code',
|
||||
emoticonUrl: 'URL',
|
||||
operation: 'Operation',
|
||||
addEmoticon: 'Add emote',
|
||||
|
||||
roomUrl: 'Room URL',
|
||||
copy: 'Copy',
|
||||
enterRoom: 'Enter room',
|
||||
enterTestRoom: 'Enter test room',
|
||||
exportConfig: 'Export config',
|
||||
@ -63,7 +68,7 @@ export default {
|
||||
showAvatars: 'Show avatars',
|
||||
avatarSize: 'Avatar size',
|
||||
|
||||
userNames: 'User names',
|
||||
userNames: 'User Names',
|
||||
showUserNames: 'Show user names',
|
||||
font: 'Font',
|
||||
fontSize: 'Font size',
|
||||
@ -91,7 +96,7 @@ export default {
|
||||
moderatorMessageBgColor: 'Moderator background color',
|
||||
memberMessageBgColor: 'Member background color',
|
||||
|
||||
scAndNewMember: 'Super Chat / New member',
|
||||
scAndNewMember: 'Super Chat / New Member',
|
||||
firstLineFont: 'First line font',
|
||||
firstLineFontSize: 'First line font size',
|
||||
firstLineLineHeight: 'First line line height (0 for default)',
|
||||
|
@ -38,8 +38,13 @@ export default {
|
||||
pinyin: 'ピンイン',
|
||||
kana: '仮名',
|
||||
|
||||
emoticon: 'カスタムスタンプ',
|
||||
emoticonKeyword: '置き換えるキーワード',
|
||||
emoticonUrl: 'URL',
|
||||
operation: '操作',
|
||||
addEmoticon: 'スタンプを追加',
|
||||
|
||||
roomUrl: 'ルームのURL',
|
||||
copy: 'コピー',
|
||||
enterRoom: 'ルームに入る',
|
||||
enterTestRoom: 'テストルームに入る',
|
||||
exportConfig: 'コンフィグの導出',
|
||||
|
@ -38,8 +38,13 @@ export default {
|
||||
pinyin: '拼音',
|
||||
kana: '日文假名',
|
||||
|
||||
emoticon: '自定义表情',
|
||||
emoticonKeyword: '替换关键词',
|
||||
emoticonUrl: 'URL',
|
||||
operation: '操作',
|
||||
addEmoticon: '添加表情',
|
||||
|
||||
roomUrl: '房间URL',
|
||||
copy: '复制',
|
||||
enterRoom: '进入房间',
|
||||
enterTestRoom: '进入测试房间',
|
||||
exportConfig: '导出配置',
|
||||
|
@ -1,9 +1,9 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import {
|
||||
Aside, Autocomplete, Badge, Button, Card, Col, ColorPicker, Container, Divider, Form, FormItem, Image,
|
||||
Aside, Autocomplete, Badge, Button, ButtonGroup, Card, Col, ColorPicker, Container, Divider, Form, FormItem, Image,
|
||||
Input, Main, Menu, MenuItem, Message, Option, OptionGroup, Radio, RadioGroup, Row, Select, Scrollbar,
|
||||
Slider, Submenu, Switch, TabPane, Tabs, Tooltip
|
||||
Slider, Submenu, Switch, Table, TableColumn, TabPane, Tabs, Tooltip
|
||||
} from 'element-ui'
|
||||
import axios from 'axios'
|
||||
|
||||
@ -28,6 +28,7 @@ Vue.use(Aside)
|
||||
Vue.use(Autocomplete)
|
||||
Vue.use(Badge)
|
||||
Vue.use(Button)
|
||||
Vue.use(ButtonGroup)
|
||||
Vue.use(Card)
|
||||
Vue.use(Col)
|
||||
Vue.use(ColorPicker)
|
||||
@ -50,6 +51,8 @@ Vue.use(Scrollbar)
|
||||
Vue.use(Slider)
|
||||
Vue.use(Submenu)
|
||||
Vue.use(Switch)
|
||||
Vue.use(Table)
|
||||
Vue.use(TableColumn)
|
||||
Vue.use(TabPane)
|
||||
Vue.use(Tabs)
|
||||
Vue.use(Tooltip)
|
||||
|
58
frontend/src/utils/trie.js
Normal file
58
frontend/src/utils/trie.js
Normal file
@ -0,0 +1,58 @@
|
||||
export class Trie {
|
||||
constructor() {
|
||||
this._root = this._createNode()
|
||||
}
|
||||
|
||||
_createNode() {
|
||||
return {
|
||||
children: {}, // char -> node
|
||||
value: null
|
||||
}
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
if (key === '') {
|
||||
throw new Error('key is empty')
|
||||
}
|
||||
let node = this._root
|
||||
for (let char of key) {
|
||||
let nextNode = node.children[char]
|
||||
if (nextNode === undefined) {
|
||||
nextNode = node.children[char] = this._createNode()
|
||||
}
|
||||
node = nextNode
|
||||
}
|
||||
node.value = value
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let node = this._root
|
||||
for (let char of key) {
|
||||
let nextNode = node.children[char]
|
||||
if (nextNode === undefined) {
|
||||
return null
|
||||
}
|
||||
node = nextNode
|
||||
}
|
||||
return node.value
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this.get(key) !== null
|
||||
}
|
||||
|
||||
greedyMatch(str) {
|
||||
let node = this._root
|
||||
for (let char of str) {
|
||||
let nextNode = node.children[char]
|
||||
if (nextNode === undefined) {
|
||||
return null
|
||||
}
|
||||
if (nextNode.value !== null) {
|
||||
return nextNode.value
|
||||
}
|
||||
node = nextNode
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
@ -114,6 +114,32 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('home.emoticon')">
|
||||
<el-table :data="form.emoticons">
|
||||
<el-table-column prop="keyword" :label="$t('home.emoticonKeyword')" width="170">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.keyword"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="url" :label="$t('home.emoticonUrl')">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.url"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('home.operation')" width="170">
|
||||
<template slot-scope="scope">
|
||||
<el-button-group>
|
||||
<el-button type="primary" icon="el-icon-upload2" disabled @click="uploadEmoticon(scope.row)"></el-button>
|
||||
<el-button type="danger" icon="el-icon-minus" @click="delEmoticon(scope.$index)"></el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<p>
|
||||
<el-button type="primary" icon="el-icon-plus" @click="addEmoticon">{{$t('home.addEmoticon')}}</el-button>
|
||||
</p>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</p>
|
||||
@ -123,11 +149,11 @@
|
||||
<el-form :model="form" label-width="150px">
|
||||
<el-form-item :label="$t('home.roomUrl')">
|
||||
<el-input ref="roomUrlInput" readonly :value="obsRoomUrl" style="width: calc(100% - 8em); margin-right: 1em;"></el-input>
|
||||
<el-button type="primary" @click="copyUrl">{{$t('home.copy')}}</el-button>
|
||||
<el-button type="primary" icon="el-icon-copy-document" @click="copyUrl"></el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :disabled="!roomUrl" @click="enterRoom">{{$t('home.enterRoom')}}</el-button>
|
||||
<el-button :disabled="!roomUrl" @click="enterTestRoom">{{$t('home.enterTestRoom')}}</el-button>
|
||||
<el-button @click="enterTestRoom">{{$t('home.enterTestRoom')}}</el-button>
|
||||
<el-button @click="exportConfig">{{$t('home.exportConfig')}}</el-button>
|
||||
<el-button @click="importConfig">{{$t('home.importConfig')}}</el-button>
|
||||
</el-form-item>
|
||||
@ -192,6 +218,20 @@ export default {
|
||||
this.$message.error(`Failed to fetch server information: ${e}`)
|
||||
}
|
||||
},
|
||||
|
||||
addEmoticon() {
|
||||
this.form.emoticons.push({
|
||||
keyword: '[Kappa]',
|
||||
url: ''
|
||||
})
|
||||
},
|
||||
delEmoticon(index) {
|
||||
this.form.emoticons.splice(index, 1)
|
||||
},
|
||||
uploadEmoticon() {
|
||||
// TODO WIP
|
||||
},
|
||||
|
||||
enterRoom() {
|
||||
window.open(this.roomUrl, `room ${this.form.roomId}`, 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
|
||||
},
|
||||
@ -199,7 +239,7 @@ export default {
|
||||
window.open(this.getRoomUrl(true), 'test room', 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
|
||||
},
|
||||
getRoomUrl(isTestRoom) {
|
||||
if (isTestRoom && this.form.roomId === '') {
|
||||
if (!isTestRoom && this.form.roomId === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
<script>
|
||||
import * as i18n from '@/i18n'
|
||||
import { mergeConfig, toBool, toInt } from '@/utils'
|
||||
import * as trie from '@/utils/trie'
|
||||
import * as pronunciation from '@/utils/pronunciation'
|
||||
import * as chatConfig from '@/api/chatConfig'
|
||||
import ChatClientTest from '@/api/chat/ChatClientTest'
|
||||
@ -36,11 +37,34 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
blockKeywords() {
|
||||
return this.config.blockKeywords.split('\n').filter(val => val)
|
||||
blockKeywordsTrie() {
|
||||
let blockKeywords = this.config.blockKeywords.split('\n')
|
||||
let res = new trie.Trie()
|
||||
for (let keyword of blockKeywords) {
|
||||
if (keyword !== '') {
|
||||
res.set(keyword, true)
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
blockUsers() {
|
||||
return this.config.blockUsers.split('\n').filter(val => val)
|
||||
blockUsersTrie() {
|
||||
let blockUsers = this.config.blockUsers.split('\n')
|
||||
let res = new trie.Trie()
|
||||
for (let user of blockUsers) {
|
||||
if (user !== '') {
|
||||
res.set(user, true)
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
emoticonsTrie() {
|
||||
let res = new trie.Trie()
|
||||
for (let emoticon of this.config.emoticons) {
|
||||
if (emoticon.keyword !== '' && emoticon.url !== '') {
|
||||
res.set(emoticon.keyword, emoticon)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -147,7 +171,7 @@ export default {
|
||||
authorName: data.authorName,
|
||||
authorType: data.authorType,
|
||||
content: data.content,
|
||||
emoticon: data.emoticon,
|
||||
richContent: this.getRichContent(data),
|
||||
privilegeType: data.privilegeType,
|
||||
repeated: 1,
|
||||
translation: data.translation
|
||||
@ -245,20 +269,17 @@ export default {
|
||||
return this.filterByAuthorName(data.authorName)
|
||||
},
|
||||
filterByContent(content) {
|
||||
for (let keyword of this.blockKeywords) {
|
||||
if (content.indexOf(keyword) !== -1) {
|
||||
let blockKeywordsTrie = this.blockKeywordsTrie
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let remainContent = content.substring(i)
|
||||
if (blockKeywordsTrie.greedyMatch(remainContent) !== null) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
filterByAuthorName(authorName) {
|
||||
for (let user of this.blockUsers) {
|
||||
if (authorName === user) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return !this.blockUsersTrie.has(authorName)
|
||||
},
|
||||
mergeSimilarText(content) {
|
||||
if (!this.config.mergeSimilarDanmaku) {
|
||||
@ -277,6 +298,66 @@ export default {
|
||||
return ''
|
||||
}
|
||||
return this.pronunciationConverter.getPronunciation(text)
|
||||
},
|
||||
getRichContent(data) {
|
||||
let richContent = []
|
||||
|
||||
// B站官方表情
|
||||
if (data.emoticon !== null) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_IMAGE,
|
||||
text: data.content,
|
||||
url: data.emoticon
|
||||
})
|
||||
return richContent
|
||||
}
|
||||
|
||||
// 没有自定义表情,只能是文本
|
||||
if (this.config.emoticons.length === 0) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
text: data.content
|
||||
})
|
||||
return richContent
|
||||
}
|
||||
|
||||
// 可能含有自定义表情,需要解析
|
||||
let emoticonsTrie = this.emoticonsTrie
|
||||
let startPos = 0
|
||||
let pos = 0
|
||||
while (pos < data.content.length) {
|
||||
let remainContent = data.content.substring(pos)
|
||||
let matchEmoticon = emoticonsTrie.greedyMatch(remainContent)
|
||||
if (matchEmoticon === null) {
|
||||
pos++
|
||||
continue
|
||||
}
|
||||
|
||||
// 加入之前的文本
|
||||
if (pos !== startPos) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
text: data.content.slice(startPos, pos)
|
||||
})
|
||||
}
|
||||
|
||||
// 加入表情
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_IMAGE,
|
||||
text: matchEmoticon.keyword,
|
||||
url: matchEmoticon.url
|
||||
})
|
||||
pos += matchEmoticon.keyword.length
|
||||
startPos = pos
|
||||
}
|
||||
// 加入尾部的文本
|
||||
if (pos !== startPos) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
text: data.content.slice(startPos, pos)
|
||||
})
|
||||
}
|
||||
return richContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user