mirror of
https://github.com/xfgryujk/blivechat.git
synced 2026-08-23 20:03:28 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbfcb01b17 | ||
|
|
edd3636ece | ||
|
|
8dfb40662c | ||
|
|
11234c8e29 | ||
|
|
9f7cdac674 | ||
|
|
7ff3c9e646 | ||
|
|
d24946fc55 | ||
|
|
491dbb4fd9 | ||
|
|
d2990ba8d8 | ||
|
|
8c3dfa902c | ||
|
|
22d80e3af2 | ||
|
|
7b91cbae3f | ||
|
|
79f070fc26 | ||
|
|
2042a3fd4c | ||
|
|
069ff30f47 | ||
|
|
0065a47b78 | ||
|
|
3b343464ae | ||
|
|
87922e0809 | ||
|
|
9bf8041860 | ||
|
|
096da47032 | ||
|
|
3273fb4d34 | ||
|
|
7f77fdd889 | ||
|
|
8538bc6abe | ||
|
|
b73608af79 | ||
|
|
45db27d3c7 | ||
|
|
aae8bd2f15 | ||
|
|
1244210bfe | ||
|
|
3976aa79ed | ||
|
|
e27955b8b5 | ||
|
|
faf272c8a7 | ||
|
|
c45825922a | ||
|
|
4b9c01b0c3 | ||
|
|
928ec4a41e |
@@ -24,6 +24,8 @@ data/*
|
||||
!data/custom_public/
|
||||
data/custom_public/*
|
||||
!data/custom_public/README.txt
|
||||
!data/custom_public/templates/
|
||||
data/custom_public/templates/*
|
||||
!data/emoticons/
|
||||
data/emoticons/*
|
||||
!data/plugins/
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -18,7 +18,7 @@ RUN npm run build
|
||||
# 准备后端
|
||||
#
|
||||
|
||||
FROM python:3.8.12-bullseye
|
||||
FROM python:3.12.10-bookworm
|
||||
ARG BASE_PATH='/root/blivechat'
|
||||
ARG EXT_DATA_PATH='/mnt/data'
|
||||
WORKDIR "${BASE_PATH}"
|
||||
@@ -30,18 +30,17 @@ RUN pip3 install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple -r req
|
||||
|
||||
# 数据目录
|
||||
COPY . ./
|
||||
RUN mkdir -p "${EXT_DATA_PATH}/frontend/dist" \
|
||||
RUN sed 's/^host =.*$/host = 0.0.0.0/; s/^loader_url =.*$/loader_url =/' data/config.example.ini >> data/config.ini
|
||||
RUN mkdir -p "${EXT_DATA_PATH}" \
|
||||
&& mv data "${EXT_DATA_PATH}/data" \
|
||||
&& ln -s "${EXT_DATA_PATH}/data" data \
|
||||
&& mv log "${EXT_DATA_PATH}/log" \
|
||||
&& ln -s "${EXT_DATA_PATH}/log" log \
|
||||
&& ln -s "${EXT_DATA_PATH}/frontend/dist" frontend/dist
|
||||
&& ln -s "${EXT_DATA_PATH}/log" log
|
||||
|
||||
# 编译好的前端
|
||||
COPY --from=builder "${BASE_PATH}/frontend/dist" "${EXT_DATA_PATH}/frontend/dist/"
|
||||
COPY --from=builder "${BASE_PATH}/frontend/dist" frontend/dist
|
||||
|
||||
# 运行
|
||||
VOLUME "${EXT_DATA_PATH}"
|
||||
EXPOSE 12450
|
||||
ENTRYPOINT ["python3", "main.py"]
|
||||
CMD ["--host", "0.0.0.0", "--port", "12450"]
|
||||
|
||||
64
README.md
64
README.md
@@ -11,41 +11,44 @@
|
||||
## 特性
|
||||
|
||||
* 兼容YouTube直播评论栏的样式
|
||||
* 高亮舰队、 ~~房管~~ 、主播的用户名
|
||||
* 高亮舰队、房管、主播的用户名
|
||||
* 自带两种样式生成器,经典YouTube风格和仿微信风格
|
||||
* 支持屏蔽弹幕、合并礼物等设置
|
||||
* 支持前端直连B站服务器或者通过后端转发
|
||||
* 支持自动翻译弹幕、醒目留言到日语,可以在后台配置翻译目标语言
|
||||
* 支持标注打赏用户名的读音,可选拼音或日文假名
|
||||
* 支持配置自定义表情,不需要开通B站官方表情
|
||||
* 支持插件开发
|
||||
* 支持[自定义HTML模板](https://github.com/xfgryujk/blivechat/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89HTML%E6%A8%A1%E6%9D%BF)
|
||||
* 支持[插件开发](https://github.com/xfgryujk/blivechat/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F)
|
||||
|
||||
## 使用方法
|
||||
|
||||
以下几种方式任选一种即可。**正式使用之前记得看[注意事项](https://github.com/xfgryujk/blivechat/wiki/%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9%E5%92%8C%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98)**
|
||||
|
||||
推荐的方式:如果你需要使用插件、翻译等高级特性,则在本地使用;否则推荐直接通过公共服务器在线使用。因为本地使用时不会自动升级版本,有时候出了问题不能及时解决;但公共服务器会禁用部分高级特性,如果你有需要,只能本地使用了
|
||||
推荐的方式:如果你需要使用插件、翻译等高级特性,则在本地使用;否则推荐直接通过公共服务器在线使用。因为本地使用时不会自动升级版本,有时候出了问题不能及时解决;但在线使用时会禁用部分高级特性,如果你有需要,只能本地使用了
|
||||
|
||||
### 一、本地使用
|
||||
### 一、在线使用
|
||||
|
||||
1. 这些是作者维护的公共服务器,根据情况随便选一个,直接用浏览器打开
|
||||
|
||||
* [blive.chat](https://blive.chat/):自动节点,一般等于cn.blive.chat,如果不可用了会进行切换,但切换需要一段时间
|
||||
* [cn.blive.chat](https://cn.blive.chat/):墙内专用节点,不容易被墙,但如果受到攻击会变得不可用
|
||||
* [cloudflare.blive.chat](https://cloudflare.blive.chat/):Cloudflare美国节点,不容易被攻击,但容易被墙
|
||||
* [vercel.blive.chat](https://vercel.blive.chat/):Vercel美国节点,不容易被攻击,但容易被墙
|
||||
|
||||
2. 输入主播在开始直播时获得的身份码,复制房间URL
|
||||
3. 用样式生成器生成样式,复制CSS
|
||||
4. 在OBS中添加浏览器源,输入URL和自定义CSS
|
||||
|
||||
### 二、本地使用
|
||||
|
||||
1. 下载[本地分发版](https://github.com/xfgryujk/blivechat/releases)(仅提供x64 Windows版)。也可以在[B站商店](https://play-live.bilibili.com/details/1694397161340)下载
|
||||
2. 双击`blivechat.exe`运行服务器。或者用命令行可以指定host和端口号:
|
||||
2. 双击`blivechat.exe`(或者`start.exe`)运行服务器
|
||||
3. 用浏览器打开[http://localhost:12450](http://localhost:12450),剩下的步骤和在线使用时是一样的
|
||||
|
||||
```sh
|
||||
blivechat.exe --host 127.0.0.1 --port 12450
|
||||
```
|
||||
### 三、从源码运行
|
||||
|
||||
或者也可以在配置文件里指定host和端口号
|
||||
|
||||
3. 用浏览器打开[http://localhost:12450](http://localhost:12450),输入主播在开始直播时获得的身份码,复制房间URL
|
||||
4. 用样式生成器生成样式,复制CSS
|
||||
5. 在OBS中添加浏览器源,输入URL和自定义CSS
|
||||
|
||||
### 二、公共服务器
|
||||
|
||||
直接用浏览器打开[公共服务器](http://chat.bilisc.com/),剩下的步骤和本地使用时是一样的
|
||||
|
||||
### 三、源代码版(自建服务器或在Windows以外平台)
|
||||
此方式适用于自建服务器或者在Windows以外的平台运行
|
||||
|
||||
0. 由于使用了git子模块,clone时需要加上`--recursive`参数:
|
||||
|
||||
@@ -67,22 +70,29 @@
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. 运行服务器(需要Python3.8以上版本):
|
||||
2. 安装服务器依赖(需要Python 3.12以上版本):
|
||||
|
||||
```sh
|
||||
pip3 install -r requirements.txt
|
||||
python3 main.py
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. 运行服务器:
|
||||
|
||||
```sh
|
||||
python main.py
|
||||
```
|
||||
|
||||
或者可以指定host和端口号:
|
||||
|
||||
```sh
|
||||
python3 main.py --host 127.0.0.1 --port 12450
|
||||
python main.py --host 127.0.0.1 --port 12450
|
||||
```
|
||||
|
||||
3. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略
|
||||
4. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略
|
||||
|
||||
### 四、Docker(自建服务器)
|
||||
### 四、Docker
|
||||
|
||||
此方式适用于自建服务器。示例的运行参数只是最基本的,可以根据需要修改
|
||||
|
||||
1. ```sh
|
||||
docker run --name blivechat -d -p 12450:12450 \
|
||||
@@ -94,9 +104,9 @@
|
||||
|
||||
## 服务器配置
|
||||
|
||||
服务器配置在`data/config.ini`,可以配置数据库和允许自动翻译等,编辑后要重启生效
|
||||
服务器配置文件在`data/config.ini`,可以配置数据库和允许自动翻译等,编辑后要重启生效
|
||||
|
||||
**自建服务器时强烈建议不使用加载器**,否则可能因为混合HTTP和HTTPS等原因加载不出来
|
||||
**自建服务器时注意要删除loader_url配置**,否则加载不了房间页面
|
||||
|
||||
## 常用链接
|
||||
|
||||
|
||||
@@ -375,8 +375,8 @@ class RoomInfoHandler(api.base.ApiHandler):
|
||||
logger.warning('room=%d _get_room_info failed: %s', room_id, data['message'])
|
||||
return room_id, 0
|
||||
|
||||
room_info = data['data']['room_info']
|
||||
return room_info['room_id'], room_info['uid']
|
||||
data = data['data']
|
||||
return data['room_id'], data['uid']
|
||||
|
||||
async def _get_server_host_list_and_token(self, room_id) -> Tuple[dict, Optional[str]]:
|
||||
try:
|
||||
|
||||
77
api/main.py
77
api/main.py
@@ -1,10 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import cachetools
|
||||
import tornado.web
|
||||
import yarl
|
||||
|
||||
import api.base
|
||||
import config
|
||||
@@ -15,6 +18,10 @@ logger = logging.getLogger(__name__)
|
||||
EMOTICON_UPLOAD_PATH = os.path.join(config.DATA_PATH, 'emoticons')
|
||||
EMOTICON_BASE_URL = '/emoticons'
|
||||
CUSTOM_PUBLIC_PATH = os.path.join(config.DATA_PATH, 'custom_public')
|
||||
TEMPLATE_PATH = os.path.join(CUSTOM_PUBLIC_PATH, 'templates')
|
||||
TEMPLATE_BASE_URL = '/custom_public/templates'
|
||||
|
||||
_templates_cache = cachetools.TTLCache(1, 10)
|
||||
|
||||
|
||||
class StaticHandler(tornado.web.StaticFileHandler):
|
||||
@@ -100,6 +107,75 @@ class UploadEmoticonHandler(api.base.ApiHandler):
|
||||
return f'{EMOTICON_BASE_URL}/{filename}'
|
||||
|
||||
|
||||
class TemplatesHandler(api.base.ApiHandler):
|
||||
async def get(self):
|
||||
templates = _templates_cache.get('templates', None)
|
||||
if templates is None:
|
||||
templates = await asyncio.get_running_loop().run_in_executor(None, self._get_templates)
|
||||
_templates_cache['templates'] = templates
|
||||
|
||||
self.set_header('Cache-Control', 'private, max-age=10')
|
||||
self.write({'templates': templates})
|
||||
|
||||
def _get_templates(self):
|
||||
template_ids = []
|
||||
try:
|
||||
with os.scandir(TEMPLATE_PATH) as it:
|
||||
for entry in it:
|
||||
if entry.is_dir() and os.path.isfile(os.path.join(entry.path, 'template.json')):
|
||||
template_ids.append(entry.name)
|
||||
except OSError:
|
||||
logger.exception('Failed to discover templates:')
|
||||
return []
|
||||
if not template_ids:
|
||||
return []
|
||||
|
||||
templates = []
|
||||
for template_id in template_ids:
|
||||
try:
|
||||
template = self._load_template_config(template_id)
|
||||
templates.append(template)
|
||||
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
||||
logger.exception('template_id=%s failed to load config:', template_id)
|
||||
return templates
|
||||
|
||||
@staticmethod
|
||||
def _load_template_config(template_id):
|
||||
config_path = os.path.join(TEMPLATE_PATH, template_id, 'template.json')
|
||||
with open(config_path, encoding='utf-8') as f:
|
||||
cfg = json.load(f)
|
||||
if not isinstance(cfg, dict):
|
||||
raise TypeError(f'Config type error, type={type(cfg)}')
|
||||
|
||||
# 相对于模板目录
|
||||
base_url = yarl.URL(f'{TEMPLATE_BASE_URL}/{template_id}/')
|
||||
|
||||
def ensure_abs_url(url_str_):
|
||||
url_ = yarl.URL(url_str_)
|
||||
if not url_.absolute:
|
||||
url_ = base_url.join(url_)
|
||||
url_str_ = str(url_)
|
||||
return url_str_
|
||||
|
||||
thumbnail_url_str = str(cfg.get('thumbnail', ''))
|
||||
if thumbnail_url_str != '':
|
||||
thumbnail_url_str = ensure_abs_url(thumbnail_url_str)
|
||||
|
||||
url_str = str(cfg.get('url', ''))
|
||||
url_str = ensure_abs_url(url_str)
|
||||
|
||||
template = {
|
||||
'id': template_id,
|
||||
'name': str(cfg.get('name', '')),
|
||||
'version': str(cfg.get('version', '')),
|
||||
'author': str(cfg.get('author', '')),
|
||||
'description': str(cfg.get('description', '')),
|
||||
'thumbnail': thumbnail_url_str,
|
||||
'url': url_str,
|
||||
}
|
||||
return template
|
||||
|
||||
|
||||
class NoCacheStaticFileHandler(tornado.web.StaticFileHandler):
|
||||
def set_extra_headers(self, path):
|
||||
self.set_header('Cache-Control', 'no-cache')
|
||||
@@ -110,6 +186,7 @@ ROUTES = [
|
||||
(r'/api/endpoints', ServiceDiscoveryHandler),
|
||||
(r'/api/ping', PingHandler),
|
||||
(r'/api/emoticon', UploadEmoticonHandler),
|
||||
(r'/api/templates', TemplatesHandler),
|
||||
]
|
||||
# 通配的放在最后
|
||||
LAST_ROUTES = [
|
||||
|
||||
@@ -275,6 +275,17 @@ class EndGamePublicHandler(_PublicHandlerBase):
|
||||
_OPEN_LIVE_URL = END_GAME_OPEN_LIVE_URL
|
||||
_COMMON_SERVER_URL = END_GAME_COMMON_SERVER_URL
|
||||
|
||||
def set_default_headers(self):
|
||||
super().set_default_headers()
|
||||
|
||||
if self._headers.get('Access-Control-Allow-Origin', None) is None:
|
||||
return
|
||||
|
||||
# 前端sendBeacon跨域发送JSON时会带凭证
|
||||
self.set_header('Access-Control-Allow-Credentials', 'true')
|
||||
self.set_header('Access-Control-Allow-Methods', 'POST')
|
||||
self.set_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
|
||||
|
||||
class EndGamePrivateHandler(_PrivateHandlerBase):
|
||||
_OPEN_LIVE_URL = END_GAME_OPEN_LIVE_URL
|
||||
|
||||
2
blivedm
2
blivedm
Submodule blivedm updated: 7f400e9c61...a45ee8f677
10
config.py
10
config.py
@@ -174,7 +174,6 @@ class AppConfig:
|
||||
'%E7%BF%BB%E8%AF%91%E6%8E%A5%E5%8F%A3'
|
||||
)
|
||||
logger.warning('%s is deprecated, please see %s', type_, doc_url)
|
||||
continue
|
||||
elif type_ == 'TencentTranslate':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
@@ -187,11 +186,16 @@ class AppConfig:
|
||||
translator_config['app_id'] = section['app_id']
|
||||
translator_config['secret'] = section['secret']
|
||||
elif type_ == 'GeminiTranslate':
|
||||
translator_config['proxy'] = section['proxy']
|
||||
logger.warning('%s is deprecated, please migrate to OpenAiApi', type_)
|
||||
elif type_ == 'OpenAiApi':
|
||||
translator_config['api_key'] = section['api_key']
|
||||
translator_config['model_code'] = section['model_code']
|
||||
translator_config['base_url'] = section['base_url']
|
||||
translator_config['proxy'] = section['proxy']
|
||||
translator_config['model'] = section['model']
|
||||
translator_config['prompt'] = section['prompt'].replace('\n', ' ').replace('\\n', '\n')
|
||||
translator_config['max_tokens'] = section.getint('max_tokens')
|
||||
translator_config['temperature'] = section.getfloat('temperature')
|
||||
translator_config['top_p'] = section.getfloat('top_p')
|
||||
else:
|
||||
raise ValueError(f'Invalid translator type: {type_}')
|
||||
except Exception: # noqa
|
||||
|
||||
@@ -138,48 +138,36 @@ app_id =
|
||||
secret =
|
||||
|
||||
|
||||
[gemini_translate]
|
||||
# 文档:https://ai.google.dev/docs
|
||||
# 定价:https://ai.google.dev/pricing
|
||||
# * 目前只有免费版,最高QPS为1
|
||||
# * 收费版 输入 $0.000125 / 1000 个字符;输出 $0.000375 / 1000 个字符
|
||||
# 模型规格:https://ai.google.dev/models/gemini
|
||||
# * Gemini Pro 最高QPS为1
|
||||
[openai_api]
|
||||
# 大语言模型对话,支持任何兼容OpenAI API的平台,参考各平台的文档吧
|
||||
|
||||
# 类型:Gemini翻译
|
||||
type = GeminiTranslate
|
||||
# 类型:OpenAI API
|
||||
type = OpenAiApi
|
||||
|
||||
# 请求间隔时间(秒),等于 1 / QPS
|
||||
query_interval = 1.1
|
||||
|
||||
# 代理地址,如果为空,不使用代理。注意必须要在指定地区才能使用:https://ai.google.dev/available_regions
|
||||
proxy = http://127.0.0.1:7890
|
||||
|
||||
# API密钥
|
||||
api_key =
|
||||
# API端点,“/chat/completions”之前的部分
|
||||
base_url = https://api.deepseek.com/v1
|
||||
# 代理地址,如果为空,不使用代理
|
||||
# Example: proxy = http://127.0.0.1:7890
|
||||
proxy =
|
||||
|
||||
# 模型代码
|
||||
model_code = models/gemini-1.0-pro
|
||||
# 提示词
|
||||
prompt = As an advanced translation software, your role is to provide accurate translations that respect
|
||||
the original content's meaning and tone.
|
||||
Ensure that texts between square brackets "[]" are kept untranslated.
|
||||
Aim to deliver the translated text in a polite and gentle tone, as per the preferences of the audience.\n\n
|
||||
Below are some examples to guide you:\n\n
|
||||
Input1:\n来拉(莱拉)设置一下吧\nOutput1:\nライラちゃん、設定してみましょうか\n\n
|
||||
Input2:\n我来拉一下各位进队伍\nOutput2:\n皆さんをチームに入れさせていただきます\n\n
|
||||
Input3:\n死了w\nOutput3:\n死んじゃったわw\n\n
|
||||
Input4:\n每天来宝这里打个卡[比心]\nOutput4:\n毎日、ここに来て宝さんにチェックインしま[比心]\n\n
|
||||
\n
|
||||
Please keep in mind that accurate translation is not just about linguistic conversion;
|
||||
it is about conveying the same sentiment and meaning as the original input.
|
||||
Pay special attention to internet vernaculars and colloquial expressions to maintain the nuances of the
|
||||
source language.\n\n
|
||||
Now, proceed with the translation of the following Chinese comment text into Japanese, ensuring accuracy
|
||||
and preservation of the original message:\n
|
||||
Input:\n{original_text}\nOutput ONLY:\n
|
||||
# 温度,用于控制令牌选择的随机性,较低的温度适合需要更具确定性或不够开放的回答的提示,而较高的温度可以产生更加多样化或更具创意的结果
|
||||
temperature = 0.4
|
||||
# 模型
|
||||
model = deepseek-chat
|
||||
# 提示词,换行符会替换为空格,“\n”会替换为换行符
|
||||
prompt = 你是一个日本主播的翻译助手,精通中日文翻译。
|
||||
用户会输入直播观众的聊天内容,请翻译成日文,并确保符合日文语言习惯。
|
||||
你可以调整语气和风格,并考虑到某些词语的文化内涵和地区差异。
|
||||
只回答翻译结果,不需要解释。
|
||||
# 最大输出token数
|
||||
max_tokens = 200
|
||||
# 采样温度,用于控制令牌选择的随机性。较低的温度适合需要更具确定性或不够开放的回答的提示,而较高的温度可以产生更加多样化或更具创意的结果
|
||||
temperature = 1.3
|
||||
# 核采样概率阈值,温度采样的替代方案。模型会考虑概率质量在top_p内的token结果
|
||||
top_p = 1.0
|
||||
|
||||
|
||||
# 傻逼B站,获取表情都要登录,开放平台也不发文本表情的URL,我服了
|
||||
|
||||
0
data/custom_public/templates/.gitkeep
Normal file
0
data/custom_public/templates/.gitkeep
Normal file
@@ -1,2 +1,3 @@
|
||||
brotli_decode.js
|
||||
pronunciation/dict*.js
|
||||
blcsdk.js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blivechat",
|
||||
"version": "1.9.3",
|
||||
"version": "1.10.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { apiClient as axios } from '@/api/base'
|
||||
import { apiClient as axios, getBaseUrl } from '@/api/base'
|
||||
import * as chat from '.'
|
||||
import * as chatModels from './models'
|
||||
import * as base from './ChatClientOfficialBase'
|
||||
@@ -13,6 +13,8 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
|
||||
this.roomOwnerAuthCode = roomOwnerAuthCode
|
||||
|
||||
this.boundEndGameBeforeUnload = this.endGameBeforeUnload.bind(this)
|
||||
|
||||
// 调用initRoom后初始化
|
||||
this.roomOwnerOpenId = null
|
||||
this.hostServerUrlList = []
|
||||
@@ -22,7 +24,15 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
this.gameHeartbeatTimerId = null
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start()
|
||||
|
||||
window.addEventListener('beforeunload', this.boundEndGameBeforeUnload)
|
||||
}
|
||||
|
||||
stop() {
|
||||
window.removeEventListener('beforeunload', this.boundEndGameBeforeUnload)
|
||||
|
||||
this.endGame()
|
||||
|
||||
super.stop()
|
||||
@@ -99,6 +109,29 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
return true
|
||||
}
|
||||
|
||||
endGameBeforeUnload() {
|
||||
let baseUrl = getBaseUrl()
|
||||
if (baseUrl === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.needInitRoom = true
|
||||
if (!this.gameId) {
|
||||
return
|
||||
}
|
||||
let gameId = this.gameId
|
||||
// 直接丢弃将要关闭的gameId
|
||||
this.gameId = null
|
||||
|
||||
let url = `${baseUrl}/api/open_live/end_game`
|
||||
let body = {
|
||||
app_id: 0,
|
||||
game_id: gameId
|
||||
}
|
||||
body = new Blob([JSON.stringify(body)], { type: 'application/json' })
|
||||
window.navigator.sendBeacon(url, body)
|
||||
}
|
||||
|
||||
onSendGameHeartbeat() {
|
||||
// 加上随机延迟,减少同时请求的概率
|
||||
let sleepTime = GAME_HEARTBEAT_INTERVAL - (2 * 1000) + (Math.random() * 3 * 1000)
|
||||
@@ -181,18 +214,38 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
super.delayReconnect()
|
||||
}
|
||||
|
||||
async dmCallback(command) {
|
||||
interactionEndCallback(command) {
|
||||
if (command.data.game_id !== this.gameId) {
|
||||
return
|
||||
}
|
||||
// 服务器主动停止推送,可能是心跳超时,需要重新开启项目
|
||||
console.error(`Open Live session end by server, gameId=${this.gameId}`)
|
||||
this.addDebugMsg('Open Live session end by server')
|
||||
|
||||
this.gameId = null
|
||||
this.needInitRoom = true
|
||||
this.discardWebsocket()
|
||||
}
|
||||
|
||||
dmCallback(command) {
|
||||
let data = command.data
|
||||
|
||||
let authorType
|
||||
if (data.open_id === this.roomOwnerOpenId) {
|
||||
authorType = 3
|
||||
} else if (data.is_admin) {
|
||||
authorType = 2
|
||||
} else if (data.guard_level !== 0) {
|
||||
authorType = 1
|
||||
} else {
|
||||
authorType = 0
|
||||
}
|
||||
|
||||
let showContent = data.msg
|
||||
if (data.reply_uname !== '') {
|
||||
showContent = `@${data.reply_uname} ${showContent}`
|
||||
}
|
||||
|
||||
let emoticon = null
|
||||
if (data.dm_type === 1) {
|
||||
emoticon = data.emoji_img_url
|
||||
@@ -203,19 +256,22 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
timestamp: data.timestamp,
|
||||
authorName: data.uname,
|
||||
authorType: authorType,
|
||||
content: data.msg,
|
||||
content: showContent,
|
||||
privilegeType: data.guard_level,
|
||||
isGiftDanmaku: chat.isGiftDanmakuByContent(data.msg),
|
||||
medalLevel: data.fans_medal_wearing_status ? data.fans_medal_level : 0,
|
||||
id: data.msg_id,
|
||||
emoticon: emoticon,
|
||||
// 给模板用的字段
|
||||
uid: data.open_id,
|
||||
medalName: data.fans_medal_wearing_status ? data.fans_medal_name : '',
|
||||
})
|
||||
this.msgHandler.onAddText(data)
|
||||
}
|
||||
|
||||
sendGiftCallback(command) {
|
||||
let data = command.data
|
||||
let totalCoin = data.price * data.gift_num
|
||||
let totalCoin = data.r_price * data.gift_num
|
||||
data = new chatModels.AddGiftMsg({
|
||||
id: data.msg_id,
|
||||
avatarUrl: chat.processAvatarUrl(data.uface),
|
||||
@@ -224,19 +280,33 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
totalCoin: data.paid ? totalCoin : 0,
|
||||
totalFreeCoin: !data.paid ? totalCoin : 0,
|
||||
giftName: data.gift_name,
|
||||
num: data.gift_num
|
||||
num: data.gift_num,
|
||||
// 给模板用的字段
|
||||
giftId: data.gift_id,
|
||||
giftIconUrl: data.gift_icon,
|
||||
uid: data.open_id,
|
||||
privilegeType: data.guard_level,
|
||||
medalLevel: data.fans_medal_wearing_status ? data.fans_medal_level : 0,
|
||||
medalName: data.fans_medal_wearing_status ? data.fans_medal_name : '',
|
||||
})
|
||||
this.msgHandler.onAddGift(data)
|
||||
}
|
||||
|
||||
async guardCallback(command) {
|
||||
guardCallback(command) {
|
||||
let data = command.data
|
||||
data = new chatModels.AddMemberMsg({
|
||||
id: data.msg_id,
|
||||
avatarUrl: chat.processAvatarUrl(data.user_info.uface),
|
||||
timestamp: data.timestamp,
|
||||
authorName: data.user_info.uname,
|
||||
privilegeType: data.guard_level
|
||||
privilegeType: data.guard_level,
|
||||
// 给模板用的字段
|
||||
num: data.guard_num,
|
||||
unit: data.guard_unit,
|
||||
total_coin: data.price * data.guard_num,
|
||||
uid: data.user_info.open_id,
|
||||
medalLevel: data.fans_medal_wearing_status ? data.fans_medal_level : 0,
|
||||
medalName: data.fans_medal_wearing_status ? data.fans_medal_name : '',
|
||||
})
|
||||
this.msgHandler.onAddMember(data)
|
||||
}
|
||||
@@ -250,6 +320,11 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
authorName: data.uname,
|
||||
price: data.rmb,
|
||||
content: data.message,
|
||||
// 给模板用的字段
|
||||
uid: data.open_id,
|
||||
privilegeType: data.guard_level,
|
||||
medalLevel: data.fans_medal_wearing_status ? data.fans_medal_level : 0,
|
||||
medalName: data.fans_medal_wearing_status ? data.fans_medal_name : '',
|
||||
})
|
||||
this.msgHandler.onAddSuperChat(data)
|
||||
}
|
||||
@@ -265,6 +340,7 @@ export default class ChatClientDirectOpenLive extends ChatClientOfficialBase {
|
||||
}
|
||||
|
||||
const CMD_CALLBACK_MAP = {
|
||||
LIVE_OPEN_PLATFORM_INTERACTION_END: ChatClientDirectOpenLive.prototype.interactionEndCallback,
|
||||
LIVE_OPEN_PLATFORM_DM: ChatClientDirectOpenLive.prototype.dmCallback,
|
||||
LIVE_OPEN_PLATFORM_SEND_GIFT: ChatClientDirectOpenLive.prototype.sendGiftCallback,
|
||||
LIVE_OPEN_PLATFORM_GUARD: ChatClientDirectOpenLive.prototype.guardCallback,
|
||||
|
||||
@@ -70,12 +70,20 @@ export default class ChatClientDirectWeb extends ChatClientOfficialBase {
|
||||
async danmuMsgCallback(command) {
|
||||
let info = command.info
|
||||
|
||||
let roomId, medalLevel
|
||||
let modeInfo = info[0][15]
|
||||
let avatarUrl = modeInfo?.user?.base?.face ?? ''
|
||||
if (!avatarUrl) {
|
||||
avatarUrl = await chat.getAvatarUrl(uid, authorName)
|
||||
}
|
||||
|
||||
let medalRoomId, medalLevel, medalName
|
||||
if (info[3]) {
|
||||
roomId = info[3][3]
|
||||
medalRoomId = info[3][3]
|
||||
medalLevel = info[3][0]
|
||||
medalName = info[3][1]
|
||||
} else {
|
||||
roomId = medalLevel = 0
|
||||
medalRoomId = medalLevel = 0
|
||||
medalName = ''
|
||||
}
|
||||
|
||||
let uid = info[2][0]
|
||||
@@ -92,21 +100,40 @@ export default class ChatClientDirectWeb extends ChatClientOfficialBase {
|
||||
authorType = 0
|
||||
}
|
||||
|
||||
let authorName = info[2][1]
|
||||
let extra
|
||||
try {
|
||||
extra = modeInfo.extra
|
||||
if (typeof extra !== 'object') {
|
||||
extra = JSON.parse(extra)
|
||||
}
|
||||
} catch {
|
||||
extra = {}
|
||||
}
|
||||
|
||||
let content = info[1]
|
||||
let replyUname = extra.reply_uname ?? ''
|
||||
let showContent = content
|
||||
if (replyUname !== '') {
|
||||
showContent = `@${replyUname} ${showContent}`
|
||||
}
|
||||
|
||||
let authorName = info[2][1]
|
||||
let data = new chatModels.AddTextMsg({
|
||||
avatarUrl: await chat.getAvatarUrl(uid, authorName),
|
||||
avatarUrl: avatarUrl,
|
||||
timestamp: info[0][4] / 1000,
|
||||
authorName: authorName,
|
||||
authorType: authorType,
|
||||
content: content,
|
||||
content: showContent,
|
||||
privilegeType: privilegeType,
|
||||
isGiftDanmaku: Boolean(info[0][9]) || chat.isGiftDanmakuByContent(content),
|
||||
authorLevel: info[4][0],
|
||||
isNewbie: info[2][5] < 10000,
|
||||
isMobileVerified: Boolean(info[2][6]),
|
||||
medalLevel: roomId === this.roomId ? medalLevel : 0,
|
||||
medalLevel: medalRoomId === this.roomId ? medalLevel : 0,
|
||||
emoticon: info[0][13].url || null,
|
||||
// 给模板用的字段
|
||||
uid: info[2][0] ? info[2][0].toString() : authorName,
|
||||
medalName: medalRoomId === this.roomId ? medalName : '',
|
||||
})
|
||||
this.msgHandler.onAddText(data)
|
||||
}
|
||||
@@ -114,6 +141,17 @@ export default class ChatClientDirectWeb extends ChatClientOfficialBase {
|
||||
sendGiftCallback(command) {
|
||||
let data = command.data
|
||||
let isPaidGift = data.coin_type === 'gold'
|
||||
|
||||
let medalRuid, medalLevel, medalName
|
||||
if (data.medal_info) {
|
||||
medalRuid = data.medal_info.target_id
|
||||
medalLevel = data.medal_info.medal_level
|
||||
medalName = data.medal_info.medal_name
|
||||
} else {
|
||||
medalRuid = medalLevel = 0
|
||||
medalName = ''
|
||||
}
|
||||
|
||||
data = new chatModels.AddGiftMsg({
|
||||
avatarUrl: chat.processAvatarUrl(data.face),
|
||||
timestamp: data.timestamp,
|
||||
@@ -121,24 +159,66 @@ export default class ChatClientDirectWeb extends ChatClientOfficialBase {
|
||||
totalCoin: isPaidGift ? data.total_coin : 0,
|
||||
totalFreeCoin: !isPaidGift ? data.total_coin : 0,
|
||||
giftName: data.giftName,
|
||||
num: data.num
|
||||
num: data.num,
|
||||
// 给模板用的字段
|
||||
giftId: data.giftId,
|
||||
giftIconUrl: data.gift_info.img_basic,
|
||||
uid: data.uid ? data.uid.toString() : data.uname,
|
||||
privilegeType: data.guard_level,
|
||||
medalLevel: medalRuid === this.roomOwnerUid ? medalLevel : 0,
|
||||
medalName: medalRuid === this.roomOwnerUid ? medalName : '',
|
||||
})
|
||||
this.msgHandler.onAddGift(data)
|
||||
}
|
||||
|
||||
async guardBuyCallback(command) {
|
||||
async userToastV2Callback(command) {
|
||||
let data = command.data
|
||||
let {
|
||||
sender_uinfo: {
|
||||
uid,
|
||||
base: {
|
||||
name: username,
|
||||
},
|
||||
},
|
||||
guard_info: {
|
||||
start_time: timestamp,
|
||||
guard_level: guardLevel,
|
||||
},
|
||||
pay_info: {
|
||||
num,
|
||||
unit,
|
||||
price,
|
||||
},
|
||||
} = data
|
||||
data = new chatModels.AddMemberMsg({
|
||||
avatarUrl: await chat.getAvatarUrl(data.uid, data.username),
|
||||
timestamp: data.start_time,
|
||||
authorName: data.username,
|
||||
privilegeType: data.guard_level
|
||||
avatarUrl: await chat.getAvatarUrl(uid, username),
|
||||
timestamp: timestamp,
|
||||
authorName: username,
|
||||
privilegeType: guardLevel,
|
||||
// 给模板用的字段
|
||||
num: num,
|
||||
unit: unit,
|
||||
total_coin: price * num,
|
||||
uid: uid ? uid.toString() : username,
|
||||
medalLevel: 0,
|
||||
medalName: '',
|
||||
})
|
||||
this.msgHandler.onAddMember(data)
|
||||
}
|
||||
|
||||
superChatMessageCallback(command) {
|
||||
let data = command.data
|
||||
|
||||
let medalRoomId, medalLevel, medalName
|
||||
if (data.medal_info) {
|
||||
medalRoomId = data.medal_info.anchor_roomid
|
||||
medalLevel = data.medal_info.medal_level
|
||||
medalName = data.medal_info.medal_name
|
||||
} else {
|
||||
medalRoomId = medalLevel = 0
|
||||
medalName = ''
|
||||
}
|
||||
|
||||
data = new chatModels.AddSuperChatMsg({
|
||||
id: data.id.toString(),
|
||||
avatarUrl: chat.processAvatarUrl(data.user_info.face),
|
||||
@@ -146,6 +226,11 @@ export default class ChatClientDirectWeb extends ChatClientOfficialBase {
|
||||
authorName: data.user_info.uname,
|
||||
price: data.price,
|
||||
content: data.message,
|
||||
// 给模板用的字段
|
||||
uid: data.uid ? data.uid.toString() : data.user_info.uname,
|
||||
privilegeType: data.user_info.guard_level,
|
||||
medalLevel: medalRoomId === this.roomId ? medalLevel : 0,
|
||||
medalName: medalRoomId === this.roomId ? medalName : '',
|
||||
})
|
||||
this.msgHandler.onAddSuperChat(data)
|
||||
}
|
||||
@@ -163,7 +248,7 @@ export default class ChatClientDirectWeb extends ChatClientOfficialBase {
|
||||
const CMD_CALLBACK_MAP = {
|
||||
DANMU_MSG: ChatClientDirectWeb.prototype.danmuMsgCallback,
|
||||
SEND_GIFT: ChatClientDirectWeb.prototype.sendGiftCallback,
|
||||
GUARD_BUY: ChatClientDirectWeb.prototype.guardBuyCallback,
|
||||
USER_TOAST_MSG_V2: ChatClientDirectWeb.prototype.userToastV2Callback,
|
||||
SUPER_CHAT_MESSAGE: ChatClientDirectWeb.prototype.superChatMessageCallback,
|
||||
SUPER_CHAT_MESSAGE_DELETE: ChatClientDirectWeb.prototype.superChatMessageDeleteCallback
|
||||
}
|
||||
|
||||
@@ -180,7 +180,9 @@ export default class ChatClientRelay {
|
||||
medalLevel: data[10],
|
||||
id: data[11],
|
||||
translation: data[12],
|
||||
emoticon: emoticon
|
||||
emoticon: emoticon,
|
||||
uid: data[16],
|
||||
medalName: data[17],
|
||||
})
|
||||
this.msgHandler.onAddText(data)
|
||||
break
|
||||
|
||||
@@ -3,59 +3,61 @@ import * as chat from '.'
|
||||
import * as chatModels from './models'
|
||||
|
||||
const NAMES = [
|
||||
'光羊',
|
||||
'黑炎',
|
||||
'孤梦星影',
|
||||
'成龙',
|
||||
'杨戬',
|
||||
'孙悟空',
|
||||
'哈基米',
|
||||
'大张伟',
|
||||
'周冠宇',
|
||||
'五条悟',
|
||||
'博丽灵梦',
|
||||
'雾雨魔理沙',
|
||||
'御剑侍伶',
|
||||
'田所浩二',
|
||||
'小岛秀夫',
|
||||
'長崎そよ',
|
||||
'柚木つばめ',
|
||||
'空條承太郎',
|
||||
'みもりあいの',
|
||||
'ディオ・ブランドー',
|
||||
'ジョセフ・ジョースター',
|
||||
'ジョナサン・ジョースター',
|
||||
'Simon',
|
||||
'Misty',
|
||||
'Kinori',
|
||||
'shugen',
|
||||
'3Shain',
|
||||
'yuyuyzl',
|
||||
'Dante',
|
||||
'xQcOW',
|
||||
'Makarov',
|
||||
'xfgryujk',
|
||||
'Il Harper',
|
||||
'Jim Hacker',
|
||||
'Rick Astley',
|
||||
'Tifa Lockhart',
|
||||
'Arthur Morgan',
|
||||
]
|
||||
|
||||
const CONTENTS = [
|
||||
'草',
|
||||
'会赢的',
|
||||
'让我看看',
|
||||
'不要停下来啊',
|
||||
'卑鄙的外乡人',
|
||||
'我不做人了,JOJO',
|
||||
'想吃广东菜✋😭✋',
|
||||
'已经没有什么好怕的了',
|
||||
'我柜子动了,我不玩了',
|
||||
'老板大气,老板身体健康',
|
||||
'我醉提酒游寒山,爽滑慢舔',
|
||||
'你这猴子,真令我欢喜',
|
||||
'[dog]文本[比心]表情[喝彩]',
|
||||
'阿祖,投降吧,外面全是警察',
|
||||
'無駄無駄無駄無駄無駄無駄無駄無駄',
|
||||
'欧啦欧啦欧啦欧啦欧啦欧啦欧啦欧啦',
|
||||
'所有没好全部康复呀,我的癌也全部康复呀',
|
||||
'嚯,朝我走过来了吗,没有选择逃跑而是主动接近我么',
|
||||
'有一说一,这件事大家懂的都懂,不懂的,说了你也不明白,不如不说',
|
||||
'如来来了吗?如来嘛~他真来了吗?如~来~到底来没来?如来~如来他真来了吗?如来~你看看,来没来?如~来~',
|
||||
'我衰咗三年,我等緊個機會,爭番口氣',
|
||||
'因为你的缘故,我的心中萌生了多余的情感',
|
||||
'迷えば、敗れる',
|
||||
'逃げるんだよォ!',
|
||||
'竜神の剣を喰らえ!',
|
||||
'竜が我が敌を喰らう!',
|
||||
'言いたいことがあるんだよ!',
|
||||
'知らず知らず隠してた 本当の声を響かせてよほら',
|
||||
'届かない恋をしていても',
|
||||
'なんで春日影やったの!?',
|
||||
'kksk',
|
||||
'Y.M.C.A.',
|
||||
'8888888888',
|
||||
'text[吃瓜]emoticon',
|
||||
'Remember... no Russian',
|
||||
'Never gonna give you up',
|
||||
'Never gonna let you down',
|
||||
'888888888888888888888888888888',
|
||||
'I am the storm that is approaching',
|
||||
"I can eat glass, it doesn't hurt me",
|
||||
'The quick brown fox jumps over the lazy dog',
|
||||
'DU↗DU→DU↗DU↓ Max Verstappen',
|
||||
'Farewell, ashen one. May the flame guide thee',
|
||||
'I am the bone of my sword. Steel is my body, and fire is my blood.',
|
||||
'Hey Vergil, your portal opening days are over. Give me the Yamato',
|
||||
'<script>alert("CHECK YOUR CODE")</script>',
|
||||
'<img src=1 onerror="alert(\'CHECK YOUR CODE\')">',
|
||||
]
|
||||
|
||||
const EMOTICONS = [
|
||||
@@ -64,7 +66,13 @@ const EMOTICONS = [
|
||||
'lipu',
|
||||
'huangdou_xihuan',
|
||||
'sakaban_jiayu_yutou',
|
||||
].map(name => `/static/img/emoticons/${name}.png`)
|
||||
].map(name => `${window.location.origin}/static/img/emoticons/${name}.png`)
|
||||
|
||||
const TRANSLATIONS = [
|
||||
'这是翻译',
|
||||
'これは翻訳です',
|
||||
'blah blah blah',
|
||||
]
|
||||
|
||||
const AUTHOR_TYPES = [
|
||||
{ weight: 10, value: constants.AUTHOR_TYPE_NORMAL },
|
||||
@@ -73,6 +81,10 @@ const AUTHOR_TYPES = [
|
||||
{ weight: 1, value: constants.AUTHOR_TYPE_OWNER }
|
||||
]
|
||||
|
||||
const GUARD_LEVEL_TO_PRICE = [
|
||||
0, 19998, 1998, 198
|
||||
]
|
||||
|
||||
function randGuardInfo() {
|
||||
let authorType = randomChoose(AUTHOR_TYPES)
|
||||
let privilegeType
|
||||
@@ -86,14 +98,28 @@ function randGuardInfo() {
|
||||
return { authorType, privilegeType }
|
||||
}
|
||||
|
||||
const MEDAL_NAMES = [
|
||||
'ikun',
|
||||
'K学姐',
|
||||
'小孤独',
|
||||
'Go学长',
|
||||
'不登校',
|
||||
]
|
||||
|
||||
function randMedalInfo() {
|
||||
let medalLevel = randInt(1, 4) <= 1 ? 0 : randInt(1, 40)
|
||||
let medalName = medalLevel === 0 ? '' : randomChoose(MEDAL_NAMES)
|
||||
return { medalLevel, medalName }
|
||||
}
|
||||
|
||||
const GIFT_INFO_LIST = [
|
||||
{ giftName: '辣条', totalFreeCoin: 1000, num: 10 },
|
||||
{ giftName: 'B坷垃', totalCoin: 9900 },
|
||||
{ giftName: '礼花', totalCoin: 28000 },
|
||||
{ giftName: '花式夸夸', totalCoin: 39000 },
|
||||
{ giftName: '天空之翼', totalCoin: 100000 },
|
||||
{ giftName: '摩天大楼', totalCoin: 450000 },
|
||||
{ giftName: '小电视飞船', totalCoin: 1245000 }
|
||||
{ giftId: 1, giftName: '粉丝团灯牌', totalFreeCoin: 200, num: 10, giftIconUrl: '//s1.hdslb.com/bfs/live/e051dfd4557678f8edcac4993ed00a0935cbd9cc.png' },
|
||||
{ giftId: 2, giftName: '可爱捏', totalCoin: 9900, giftIconUrl: '//s1.hdslb.com/bfs/live/6dab14826b531c731521345e00d6b56a6708a449.png' },
|
||||
{ giftId: 3, giftName: '花式夸夸', totalCoin: 29900, giftIconUrl: '//s1.hdslb.com/bfs/live/28186596880db45a7b843f17d6ebb70feeac06f9.png' },
|
||||
{ giftId: 4, giftName: '情书', totalCoin: 52000, num: 10, giftIconUrl: '//s1.hdslb.com/bfs/live/14dafbf217618f0931c08897e0b3eefc00d0da22.png' },
|
||||
{ giftId: 5, giftName: '极速超跑', totalCoin: 100000, giftIconUrl: '//s1.hdslb.com/bfs/live/27b9734d1a5f77ea6fc94957e3fcbeb55505c6b9.png' },
|
||||
{ giftId: 6, giftName: '为你摘星', totalCoin: 520000, giftIconUrl: '//s1.hdslb.com/bfs/live/5bd584b6fdfb03d66de56102e775582fb29ceab7.png' },
|
||||
{ giftId: 7, giftName: '次元之城', totalCoin: 1245000, giftIconUrl: '//s1.hdslb.com/bfs/live/cdae8136b1ee767609aeec688bca8124651d4d01.png' }
|
||||
]
|
||||
|
||||
const SC_PRICES = [
|
||||
@@ -109,13 +135,14 @@ const MESSAGE_GENERATORS = [
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
message: new chatModels.AddTextMsg({
|
||||
...randGuardInfo(),
|
||||
...randMedalInfo(),
|
||||
authorName: randomChoose(NAMES),
|
||||
content: randomChoose(CONTENTS),
|
||||
isGiftDanmaku: randInt(1, 10) <= 1,
|
||||
authorLevel: randInt(1, 60),
|
||||
isNewbie: randInt(1, 10) <= 1,
|
||||
isMobileVerified: randInt(1, 10) <= 9,
|
||||
medalLevel: randInt(0, 40),
|
||||
translation: randInt(1, 10) <= 1 ? randomChoose(TRANSLATIONS) : '',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -128,11 +155,11 @@ const MESSAGE_GENERATORS = [
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
message: new chatModels.AddTextMsg({
|
||||
...randGuardInfo(),
|
||||
...randMedalInfo(),
|
||||
authorName: randomChoose(NAMES),
|
||||
authorLevel: randInt(1, 60),
|
||||
isNewbie: randInt(1, 10) <= 1,
|
||||
isMobileVerified: randInt(1, 10) <= 9,
|
||||
medalLevel: randInt(0, 40),
|
||||
emoticon: randomChoose(EMOTICONS),
|
||||
})
|
||||
}
|
||||
@@ -145,8 +172,10 @@ const MESSAGE_GENERATORS = [
|
||||
return {
|
||||
type: constants.MESSAGE_TYPE_GIFT,
|
||||
message: new chatModels.AddGiftMsg({
|
||||
...randMedalInfo(),
|
||||
...randomChoose(GIFT_INFO_LIST),
|
||||
authorName: randomChoose(NAMES),
|
||||
privilegeType: randInt(0, 3),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -158,9 +187,12 @@ const MESSAGE_GENERATORS = [
|
||||
return {
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
message: new chatModels.AddSuperChatMsg({
|
||||
...randMedalInfo(),
|
||||
authorName: randomChoose(NAMES),
|
||||
price: randomChoose(SC_PRICES),
|
||||
content: randomChoose(CONTENTS),
|
||||
translation: randInt(1, 10) <= 1 ? randomChoose(TRANSLATIONS) : '',
|
||||
privilegeType: randInt(0, 3),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -169,11 +201,14 @@ const MESSAGE_GENERATORS = [
|
||||
{
|
||||
weight: 1,
|
||||
value() {
|
||||
let privilegeType = randInt(1, 3)
|
||||
return {
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
message: new chatModels.AddMemberMsg({
|
||||
...randMedalInfo(),
|
||||
authorName: randomChoose(NAMES),
|
||||
privilegeType: randInt(1, 3)
|
||||
privilegeType: privilegeType,
|
||||
total_coin: GUARD_LEVEL_TO_PRICE[privilegeType] * 1000,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -251,6 +286,7 @@ export default class ChatClientTest {
|
||||
switch (type) {
|
||||
case constants.MESSAGE_TYPE_TEXT:
|
||||
this.msgHandler.onAddText(message)
|
||||
this.maybeTranslate(message)
|
||||
break
|
||||
case constants.MESSAGE_TYPE_GIFT:
|
||||
this.msgHandler.onAddGift(message)
|
||||
@@ -260,7 +296,34 @@ export default class ChatClientTest {
|
||||
break
|
||||
case constants.MESSAGE_TYPE_SUPER_CHAT:
|
||||
this.msgHandler.onAddSuperChat(message)
|
||||
this.maybeTranslate(message)
|
||||
this.maybeDeleteSc(message)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
maybeTranslate(message) {
|
||||
if (message.translation || randInt(1, 4) <= 1) {
|
||||
return
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
let translateMessage = new chatModels.UpdateTranslationMsg({
|
||||
id: message.id,
|
||||
translation: randomChoose(TRANSLATIONS),
|
||||
})
|
||||
this.msgHandler.onUpdateTranslation(translateMessage)
|
||||
}, randInt(1000, 3000))
|
||||
}
|
||||
|
||||
maybeDeleteSc(message) {
|
||||
if (randInt(1, 5) <= 4) {
|
||||
return
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
let deleteMessage = new chatModels.DelSuperChatMsg({
|
||||
ids: [message.id],
|
||||
})
|
||||
this.msgHandler.onDelSuperChat(deleteMessage)
|
||||
}, randInt(1000, 3000))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ export class AddTextMsg {
|
||||
id = getUuid4Hex(),
|
||||
translation = '',
|
||||
emoticon = null,
|
||||
// 给模板用的字段
|
||||
uid = '',
|
||||
medalName = '',
|
||||
} = {}) {
|
||||
this.avatarUrl = avatarUrl
|
||||
this.timestamp = timestamp
|
||||
@@ -33,6 +36,9 @@ export class AddTextMsg {
|
||||
this.id = id
|
||||
this.translation = translation
|
||||
this.emoticon = emoticon
|
||||
// 给模板用的字段
|
||||
this.uid = uid
|
||||
this.medalName = medalName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +52,13 @@ export class AddGiftMsg {
|
||||
totalFreeCoin = 0,
|
||||
giftName = '',
|
||||
num = 1,
|
||||
// 给模板用的字段
|
||||
giftId = 0,
|
||||
giftIconUrl = '',
|
||||
uid = '',
|
||||
privilegeType = 0,
|
||||
medalLevel = 0,
|
||||
medalName = '',
|
||||
} = {}) {
|
||||
this.id = id
|
||||
this.avatarUrl = avatarUrl
|
||||
@@ -55,6 +68,13 @@ export class AddGiftMsg {
|
||||
this.totalFreeCoin = totalFreeCoin
|
||||
this.giftName = giftName
|
||||
this.num = num
|
||||
// 给模板用的字段
|
||||
this.giftId = giftId
|
||||
this.giftIconUrl = giftIconUrl
|
||||
this.uid = uid
|
||||
this.privilegeType = privilegeType
|
||||
this.medalLevel = medalLevel
|
||||
this.medalName = medalName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,12 +85,26 @@ export class AddMemberMsg {
|
||||
timestamp = new Date().getTime() / 1000,
|
||||
authorName = '',
|
||||
privilegeType = 1,
|
||||
// 给模板用的字段
|
||||
num = 1,
|
||||
unit = '月',
|
||||
total_coin = 0,
|
||||
uid = '',
|
||||
medalLevel = 0,
|
||||
medalName = '',
|
||||
} = {}) {
|
||||
this.id = id
|
||||
this.avatarUrl = avatarUrl
|
||||
this.timestamp = timestamp
|
||||
this.authorName = authorName
|
||||
this.privilegeType = privilegeType
|
||||
// 给模板用的字段
|
||||
this.num = num
|
||||
this.unit = unit
|
||||
this.totalCoin = total_coin
|
||||
this.uid = uid
|
||||
this.medalLevel = medalLevel
|
||||
this.medalName = medalName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +117,11 @@ export class AddSuperChatMsg {
|
||||
price = 0,
|
||||
content = '',
|
||||
translation = '',
|
||||
// 给模板用的字段
|
||||
uid = '',
|
||||
privilegeType = 0,
|
||||
medalLevel = 0,
|
||||
medalName = '',
|
||||
} = {}) {
|
||||
this.id = id
|
||||
this.avatarUrl = avatarUrl
|
||||
@@ -91,6 +130,11 @@ export class AddSuperChatMsg {
|
||||
this.price = price
|
||||
this.content = content
|
||||
this.translation = translation
|
||||
// 给模板用的字段
|
||||
this.uid = uid
|
||||
this.privilegeType = privilegeType
|
||||
this.medalLevel = medalLevel
|
||||
this.medalName = medalName
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ export const DEFAULT_CONFIG = {
|
||||
giftUsernamePronunciation: '',
|
||||
importPresetCss: false,
|
||||
|
||||
emoticons: [] // [{ keyword: '', url: '' }, ...]
|
||||
emoticons: [], // [{ keyword: '', url: '' }, ...]
|
||||
|
||||
templateUrl: '',
|
||||
}
|
||||
|
||||
export function deepCloneDefaultConfig() {
|
||||
|
||||
@@ -9,3 +9,7 @@ export async function uploadEmoticon(file) {
|
||||
body.set('file', file)
|
||||
return (await axios.post('/api/emoticon', body)).data
|
||||
}
|
||||
|
||||
export async function getTemplates() {
|
||||
return (await axios.get('/api/templates')).data
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
html:not(.style-scope) {
|
||||
--yt-live-chat-background-color: hsl(0, 0%, 100%);
|
||||
--yt-live-chat-action-panel-background-color: hsla(0, 0%, 93.3%, .4);
|
||||
@@ -360,3 +362,5 @@ html:not(.style-scope) {
|
||||
--layout-fixed-left_-_left: 0;
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
canvas.yt-icon, caption.yt-icon, center.yt-icon, cite.yt-icon, code.yt-icon, dd.yt-icon, del.yt-icon, dfn.yt-icon, div.yt-icon, dl.yt-icon, dt.yt-icon, em.yt-icon, embed.yt-icon, fieldset.yt-icon, font.yt-icon, form.yt-icon, h1.yt-icon, h2.yt-icon, h3.yt-icon, h4.yt-icon, h5.yt-icon, h6.yt-icon, hr.yt-icon, i.yt-icon, iframe.yt-icon, img.yt-icon, ins.yt-icon, kbd.yt-icon, label.yt-icon, legend.yt-icon, li.yt-icon, menu.yt-icon, object.yt-icon, ol.yt-icon, p.yt-icon, pre.yt-icon, q.yt-icon, s.yt-icon, samp.yt-icon, small.yt-icon, span.yt-icon, strike.yt-icon, strong.yt-icon, sub.yt-icon, sup.yt-icon, table.yt-icon, tbody.yt-icon, td.yt-icon, tfoot.yt-icon, th.yt-icon, thead.yt-icon, tr.yt-icon, tt.yt-icon, u.yt-icon, ul.yt-icon, var.yt-icon {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -28,3 +30,5 @@ yt-icon, .yt-icon-container.yt-icon {
|
||||
yt-icon.external-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
canvas.yt-img-shadow, caption.yt-img-shadow, center.yt-img-shadow, cite.yt-img-shadow, code.yt-img-shadow, dd.yt-img-shadow, del.yt-img-shadow, dfn.yt-img-shadow, div.yt-img-shadow, dl.yt-img-shadow, dt.yt-img-shadow, em.yt-img-shadow, embed.yt-img-shadow, fieldset.yt-img-shadow, font.yt-img-shadow, form.yt-img-shadow, h1.yt-img-shadow, h2.yt-img-shadow, h3.yt-img-shadow, h4.yt-img-shadow, h5.yt-img-shadow, h6.yt-img-shadow, hr.yt-img-shadow, i.yt-img-shadow, iframe.yt-img-shadow, img.yt-img-shadow, ins.yt-img-shadow, kbd.yt-img-shadow, label.yt-img-shadow, legend.yt-img-shadow, li.yt-img-shadow, menu.yt-img-shadow, object.yt-img-shadow, ol.yt-img-shadow, p.yt-img-shadow, pre.yt-img-shadow, q.yt-img-shadow, s.yt-img-shadow, samp.yt-img-shadow, small.yt-img-shadow, span.yt-img-shadow, strike.yt-img-shadow, strong.yt-img-shadow, sub.yt-img-shadow, sup.yt-img-shadow, table.yt-img-shadow, tbody.yt-img-shadow, td.yt-img-shadow, tfoot.yt-img-shadow, th.yt-img-shadow, thead.yt-img-shadow, tr.yt-img-shadow, tt.yt-img-shadow, u.yt-img-shadow, ul.yt-img-shadow, var.yt-img-shadow {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -66,3 +68,5 @@ img.yt-img-shadow {
|
||||
max-width: 100%;
|
||||
border-radius: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
canvas.yt-live-chat-author-badge-renderer, caption.yt-live-chat-author-badge-renderer, center.yt-live-chat-author-badge-renderer, cite.yt-live-chat-author-badge-renderer, code.yt-live-chat-author-badge-renderer, dd.yt-live-chat-author-badge-renderer, del.yt-live-chat-author-badge-renderer, dfn.yt-live-chat-author-badge-renderer, div.yt-live-chat-author-badge-renderer, dl.yt-live-chat-author-badge-renderer, dt.yt-live-chat-author-badge-renderer, em.yt-live-chat-author-badge-renderer, embed.yt-live-chat-author-badge-renderer, fieldset.yt-live-chat-author-badge-renderer, font.yt-live-chat-author-badge-renderer, form.yt-live-chat-author-badge-renderer, h1.yt-live-chat-author-badge-renderer, h2.yt-live-chat-author-badge-renderer, h3.yt-live-chat-author-badge-renderer, h4.yt-live-chat-author-badge-renderer, h5.yt-live-chat-author-badge-renderer, h6.yt-live-chat-author-badge-renderer, hr.yt-live-chat-author-badge-renderer, i.yt-live-chat-author-badge-renderer, iframe.yt-live-chat-author-badge-renderer, img.yt-live-chat-author-badge-renderer, ins.yt-live-chat-author-badge-renderer, kbd.yt-live-chat-author-badge-renderer, label.yt-live-chat-author-badge-renderer, legend.yt-live-chat-author-badge-renderer, li.yt-live-chat-author-badge-renderer, menu.yt-live-chat-author-badge-renderer, object.yt-live-chat-author-badge-renderer, ol.yt-live-chat-author-badge-renderer, p.yt-live-chat-author-badge-renderer, pre.yt-live-chat-author-badge-renderer, q.yt-live-chat-author-badge-renderer, s.yt-live-chat-author-badge-renderer, samp.yt-live-chat-author-badge-renderer, small.yt-live-chat-author-badge-renderer, span.yt-live-chat-author-badge-renderer, strike.yt-live-chat-author-badge-renderer, strong.yt-live-chat-author-badge-renderer, sub.yt-live-chat-author-badge-renderer, sup.yt-live-chat-author-badge-renderer, table.yt-live-chat-author-badge-renderer, tbody.yt-live-chat-author-badge-renderer, td.yt-live-chat-author-badge-renderer, tfoot.yt-live-chat-author-badge-renderer, th.yt-live-chat-author-badge-renderer, thead.yt-live-chat-author-badge-renderer, tr.yt-live-chat-author-badge-renderer, tt.yt-live-chat-author-badge-renderer, u.yt-live-chat-author-badge-renderer, ul.yt-live-chat-author-badge-renderer, var.yt-live-chat-author-badge-renderer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -34,3 +36,5 @@ img.yt-live-chat-author-badge-renderer, yt-icon.yt-live-chat-author-badge-render
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
canvas.yt-live-chat-author-chip, caption.yt-live-chat-author-chip, center.yt-live-chat-author-chip, cite.yt-live-chat-author-chip, code.yt-live-chat-author-chip, dd.yt-live-chat-author-chip, del.yt-live-chat-author-chip, dfn.yt-live-chat-author-chip, div.yt-live-chat-author-chip, dl.yt-live-chat-author-chip, dt.yt-live-chat-author-chip, em.yt-live-chat-author-chip, embed.yt-live-chat-author-chip, fieldset.yt-live-chat-author-chip, font.yt-live-chat-author-chip, form.yt-live-chat-author-chip, h1.yt-live-chat-author-chip, h2.yt-live-chat-author-chip, h3.yt-live-chat-author-chip, h4.yt-live-chat-author-chip, h5.yt-live-chat-author-chip, h6.yt-live-chat-author-chip, hr.yt-live-chat-author-chip, i.yt-live-chat-author-chip, iframe.yt-live-chat-author-chip, img.yt-live-chat-author-chip, ins.yt-live-chat-author-chip, kbd.yt-live-chat-author-chip, label.yt-live-chat-author-chip, legend.yt-live-chat-author-chip, li.yt-live-chat-author-chip, menu.yt-live-chat-author-chip, object.yt-live-chat-author-chip, ol.yt-live-chat-author-chip, p.yt-live-chat-author-chip, pre.yt-live-chat-author-chip, q.yt-live-chat-author-chip, s.yt-live-chat-author-chip, samp.yt-live-chat-author-chip, small.yt-live-chat-author-chip, span.yt-live-chat-author-chip, strike.yt-live-chat-author-chip, strong.yt-live-chat-author-chip, sub.yt-live-chat-author-chip, sup.yt-live-chat-author-chip, table.yt-live-chat-author-chip, tbody.yt-live-chat-author-chip, td.yt-live-chat-author-chip, tfoot.yt-live-chat-author-chip, th.yt-live-chat-author-chip, thead.yt-live-chat-author-chip, tr.yt-live-chat-author-chip, tt.yt-live-chat-author-chip, u.yt-live-chat-author-chip, ul.yt-live-chat-author-chip, var.yt-live-chat-author-chip {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -62,3 +64,5 @@ yt-live-chat-author-chip[is-highlighted] #chip-badges.yt-live-chat-author-chip y
|
||||
#chip-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer.yt-live-chat-author-chip:last-of-type {
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
canvas.yt-live-chat-item-list-renderer, caption.yt-live-chat-item-list-renderer, center.yt-live-chat-item-list-renderer, cite.yt-live-chat-item-list-renderer, code.yt-live-chat-item-list-renderer, dd.yt-live-chat-item-list-renderer, del.yt-live-chat-item-list-renderer, dfn.yt-live-chat-item-list-renderer, div.yt-live-chat-item-list-renderer, dl.yt-live-chat-item-list-renderer, dt.yt-live-chat-item-list-renderer, em.yt-live-chat-item-list-renderer, embed.yt-live-chat-item-list-renderer, fieldset.yt-live-chat-item-list-renderer, font.yt-live-chat-item-list-renderer, form.yt-live-chat-item-list-renderer, h1.yt-live-chat-item-list-renderer, h2.yt-live-chat-item-list-renderer, h3.yt-live-chat-item-list-renderer, h4.yt-live-chat-item-list-renderer, h5.yt-live-chat-item-list-renderer, h6.yt-live-chat-item-list-renderer, hr.yt-live-chat-item-list-renderer, i.yt-live-chat-item-list-renderer, iframe.yt-live-chat-item-list-renderer, img.yt-live-chat-item-list-renderer, ins.yt-live-chat-item-list-renderer, kbd.yt-live-chat-item-list-renderer, label.yt-live-chat-item-list-renderer, legend.yt-live-chat-item-list-renderer, li.yt-live-chat-item-list-renderer, menu.yt-live-chat-item-list-renderer, object.yt-live-chat-item-list-renderer, ol.yt-live-chat-item-list-renderer, p.yt-live-chat-item-list-renderer, pre.yt-live-chat-item-list-renderer, q.yt-live-chat-item-list-renderer, s.yt-live-chat-item-list-renderer, samp.yt-live-chat-item-list-renderer, small.yt-live-chat-item-list-renderer, span.yt-live-chat-item-list-renderer, strike.yt-live-chat-item-list-renderer, strong.yt-live-chat-item-list-renderer, sub.yt-live-chat-item-list-renderer, sup.yt-live-chat-item-list-renderer, table.yt-live-chat-item-list-renderer, tbody.yt-live-chat-item-list-renderer, td.yt-live-chat-item-list-renderer, tfoot.yt-live-chat-item-list-renderer, th.yt-live-chat-item-list-renderer, thead.yt-live-chat-item-list-renderer, tr.yt-live-chat-item-list-renderer, tt.yt-live-chat-item-list-renderer, u.yt-live-chat-item-list-renderer, ul.yt-live-chat-item-list-renderer, var.yt-live-chat-item-list-renderer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -138,3 +140,5 @@ yt-live-chat-paid-sticker-renderer.yt-live-chat-item-list-renderer {
|
||||
yt-live-chat-paid-sticker-renderer.yt-live-chat-item-list-renderer[dashboard-money-feed] {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
#timestamp.yt-live-chat-membership-item-renderer {
|
||||
display: var(--yt-live-chat-item-timestamp-display, inline);
|
||||
margin: var(--yt-live-chat-item-timestamp-margin, 0 8px 0 0);
|
||||
@@ -360,3 +362,5 @@ yt-live-chat-membership-item-renderer[dashboard-money-feed] #menu.yt-live-chat-m
|
||||
margin-top: 8px;
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-background-color) 40%);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
canvas.yt-live-chat-paid-message-renderer, caption.yt-live-chat-paid-message-renderer, center.yt-live-chat-paid-message-renderer, cite.yt-live-chat-paid-message-renderer, code.yt-live-chat-paid-message-renderer, dd.yt-live-chat-paid-message-renderer, del.yt-live-chat-paid-message-renderer, dfn.yt-live-chat-paid-message-renderer, div.yt-live-chat-paid-message-renderer, dl.yt-live-chat-paid-message-renderer, dt.yt-live-chat-paid-message-renderer, em.yt-live-chat-paid-message-renderer, embed.yt-live-chat-paid-message-renderer, fieldset.yt-live-chat-paid-message-renderer, font.yt-live-chat-paid-message-renderer, form.yt-live-chat-paid-message-renderer, h1.yt-live-chat-paid-message-renderer, h2.yt-live-chat-paid-message-renderer, h3.yt-live-chat-paid-message-renderer, h4.yt-live-chat-paid-message-renderer, h5.yt-live-chat-paid-message-renderer, h6.yt-live-chat-paid-message-renderer, hr.yt-live-chat-paid-message-renderer, i.yt-live-chat-paid-message-renderer, iframe.yt-live-chat-paid-message-renderer, img.yt-live-chat-paid-message-renderer, ins.yt-live-chat-paid-message-renderer, kbd.yt-live-chat-paid-message-renderer, label.yt-live-chat-paid-message-renderer, legend.yt-live-chat-paid-message-renderer, li.yt-live-chat-paid-message-renderer, menu.yt-live-chat-paid-message-renderer, object.yt-live-chat-paid-message-renderer, ol.yt-live-chat-paid-message-renderer, p.yt-live-chat-paid-message-renderer, pre.yt-live-chat-paid-message-renderer, q.yt-live-chat-paid-message-renderer, s.yt-live-chat-paid-message-renderer, samp.yt-live-chat-paid-message-renderer, small.yt-live-chat-paid-message-renderer, span.yt-live-chat-paid-message-renderer, strike.yt-live-chat-paid-message-renderer, strong.yt-live-chat-paid-message-renderer, sub.yt-live-chat-paid-message-renderer, sup.yt-live-chat-paid-message-renderer, table.yt-live-chat-paid-message-renderer, tbody.yt-live-chat-paid-message-renderer, td.yt-live-chat-paid-message-renderer, tfoot.yt-live-chat-paid-message-renderer, th.yt-live-chat-paid-message-renderer, thead.yt-live-chat-paid-message-renderer, tr.yt-live-chat-paid-message-renderer, tt.yt-live-chat-paid-message-renderer, u.yt-live-chat-paid-message-renderer, ul.yt-live-chat-paid-message-renderer, var.yt-live-chat-paid-message-renderer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -378,3 +380,5 @@ yt-live-chat-paid-message-renderer[show-footer-divider] #footer.yt-live-chat-pai
|
||||
yt-live-chat-paid-message-renderer[is-user-editable] #footer.yt-live-chat-paid-message-renderer:not(:empty) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
yt-live-chat-renderer, yt-live-chat-item-list-renderer #item-scroller {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -250,3 +252,5 @@ yt-live-chat-renderer[collapsed] .hide-on-collapse.yt-live-chat-renderer {
|
||||
background-color: rgba(0, 0, 0, 0.60);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
canvas.yt-live-chat-text-message-renderer, caption.yt-live-chat-text-message-renderer, center.yt-live-chat-text-message-renderer, cite.yt-live-chat-text-message-renderer, code.yt-live-chat-text-message-renderer, dd.yt-live-chat-text-message-renderer, del.yt-live-chat-text-message-renderer, dfn.yt-live-chat-text-message-renderer, div.yt-live-chat-text-message-renderer, dl.yt-live-chat-text-message-renderer, dt.yt-live-chat-text-message-renderer, em.yt-live-chat-text-message-renderer, embed.yt-live-chat-text-message-renderer, fieldset.yt-live-chat-text-message-renderer, font.yt-live-chat-text-message-renderer, form.yt-live-chat-text-message-renderer, h1.yt-live-chat-text-message-renderer, h2.yt-live-chat-text-message-renderer, h3.yt-live-chat-text-message-renderer, h4.yt-live-chat-text-message-renderer, h5.yt-live-chat-text-message-renderer, h6.yt-live-chat-text-message-renderer, hr.yt-live-chat-text-message-renderer, i.yt-live-chat-text-message-renderer, iframe.yt-live-chat-text-message-renderer, img.yt-live-chat-text-message-renderer, ins.yt-live-chat-text-message-renderer, kbd.yt-live-chat-text-message-renderer, label.yt-live-chat-text-message-renderer, legend.yt-live-chat-text-message-renderer, li.yt-live-chat-text-message-renderer, menu.yt-live-chat-text-message-renderer, object.yt-live-chat-text-message-renderer, ol.yt-live-chat-text-message-renderer, p.yt-live-chat-text-message-renderer, pre.yt-live-chat-text-message-renderer, q.yt-live-chat-text-message-renderer, s.yt-live-chat-text-message-renderer, samp.yt-live-chat-text-message-renderer, small.yt-live-chat-text-message-renderer, span.yt-live-chat-text-message-renderer, strike.yt-live-chat-text-message-renderer, strong.yt-live-chat-text-message-renderer, sub.yt-live-chat-text-message-renderer, sup.yt-live-chat-text-message-renderer, table.yt-live-chat-text-message-renderer, tbody.yt-live-chat-text-message-renderer, td.yt-live-chat-text-message-renderer, tfoot.yt-live-chat-text-message-renderer, th.yt-live-chat-text-message-renderer, thead.yt-live-chat-text-message-renderer, tr.yt-live-chat-text-message-renderer, tt.yt-live-chat-text-message-renderer, u.yt-live-chat-text-message-renderer, ul.yt-live-chat-text-message-renderer, var.yt-live-chat-text-message-renderer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -230,3 +232,5 @@ yt-live-chat-text-message-renderer[is-dimmed] #message.yt-live-chat-text-message
|
||||
yt-live-chat-text-message-renderer[is-dimmed]::before {
|
||||
background: var(--yt-live-chat-error-message-color, #f44336);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
canvas.yt-live-chat-ticker-paid-message-item-renderer, caption.yt-live-chat-ticker-paid-message-item-renderer, center.yt-live-chat-ticker-paid-message-item-renderer, cite.yt-live-chat-ticker-paid-message-item-renderer, code.yt-live-chat-ticker-paid-message-item-renderer, dd.yt-live-chat-ticker-paid-message-item-renderer, del.yt-live-chat-ticker-paid-message-item-renderer, dfn.yt-live-chat-ticker-paid-message-item-renderer, div.yt-live-chat-ticker-paid-message-item-renderer, dl.yt-live-chat-ticker-paid-message-item-renderer, dt.yt-live-chat-ticker-paid-message-item-renderer, em.yt-live-chat-ticker-paid-message-item-renderer, embed.yt-live-chat-ticker-paid-message-item-renderer, fieldset.yt-live-chat-ticker-paid-message-item-renderer, font.yt-live-chat-ticker-paid-message-item-renderer, form.yt-live-chat-ticker-paid-message-item-renderer, h1.yt-live-chat-ticker-paid-message-item-renderer, h2.yt-live-chat-ticker-paid-message-item-renderer, h3.yt-live-chat-ticker-paid-message-item-renderer, h4.yt-live-chat-ticker-paid-message-item-renderer, h5.yt-live-chat-ticker-paid-message-item-renderer, h6.yt-live-chat-ticker-paid-message-item-renderer, hr.yt-live-chat-ticker-paid-message-item-renderer, i.yt-live-chat-ticker-paid-message-item-renderer, iframe.yt-live-chat-ticker-paid-message-item-renderer, img.yt-live-chat-ticker-paid-message-item-renderer, ins.yt-live-chat-ticker-paid-message-item-renderer, kbd.yt-live-chat-ticker-paid-message-item-renderer, label.yt-live-chat-ticker-paid-message-item-renderer, legend.yt-live-chat-ticker-paid-message-item-renderer, li.yt-live-chat-ticker-paid-message-item-renderer, menu.yt-live-chat-ticker-paid-message-item-renderer, object.yt-live-chat-ticker-paid-message-item-renderer, ol.yt-live-chat-ticker-paid-message-item-renderer, p.yt-live-chat-ticker-paid-message-item-renderer, pre.yt-live-chat-ticker-paid-message-item-renderer, q.yt-live-chat-ticker-paid-message-item-renderer, s.yt-live-chat-ticker-paid-message-item-renderer, samp.yt-live-chat-ticker-paid-message-item-renderer, small.yt-live-chat-ticker-paid-message-item-renderer, span.yt-live-chat-ticker-paid-message-item-renderer, strike.yt-live-chat-ticker-paid-message-item-renderer, strong.yt-live-chat-ticker-paid-message-item-renderer, sub.yt-live-chat-ticker-paid-message-item-renderer, sup.yt-live-chat-ticker-paid-message-item-renderer, table.yt-live-chat-ticker-paid-message-item-renderer, tbody.yt-live-chat-ticker-paid-message-item-renderer, td.yt-live-chat-ticker-paid-message-item-renderer, tfoot.yt-live-chat-ticker-paid-message-item-renderer, th.yt-live-chat-ticker-paid-message-item-renderer, thead.yt-live-chat-ticker-paid-message-item-renderer, tr.yt-live-chat-ticker-paid-message-item-renderer, tt.yt-live-chat-ticker-paid-message-item-renderer, u.yt-live-chat-ticker-paid-message-item-renderer, ul.yt-live-chat-ticker-paid-message-item-renderer, var.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -78,3 +80,5 @@ yt-img-shadow.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
yt-live-chat-ticker-paid-message-item-renderer[is-deleted] #author-photo.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@layer yt {
|
||||
|
||||
canvas.yt-live-chat-ticker-renderer, caption.yt-live-chat-ticker-renderer, center.yt-live-chat-ticker-renderer, cite.yt-live-chat-ticker-renderer, code.yt-live-chat-ticker-renderer, dd.yt-live-chat-ticker-renderer, del.yt-live-chat-ticker-renderer, dfn.yt-live-chat-ticker-renderer, div.yt-live-chat-ticker-renderer, dl.yt-live-chat-ticker-renderer, dt.yt-live-chat-ticker-renderer, em.yt-live-chat-ticker-renderer, embed.yt-live-chat-ticker-renderer, fieldset.yt-live-chat-ticker-renderer, font.yt-live-chat-ticker-renderer, form.yt-live-chat-ticker-renderer, h1.yt-live-chat-ticker-renderer, h2.yt-live-chat-ticker-renderer, h3.yt-live-chat-ticker-renderer, h4.yt-live-chat-ticker-renderer, h5.yt-live-chat-ticker-renderer, h6.yt-live-chat-ticker-renderer, hr.yt-live-chat-ticker-renderer, i.yt-live-chat-ticker-renderer, iframe.yt-live-chat-ticker-renderer, img.yt-live-chat-ticker-renderer, ins.yt-live-chat-ticker-renderer, kbd.yt-live-chat-ticker-renderer, label.yt-live-chat-ticker-renderer, legend.yt-live-chat-ticker-renderer, li.yt-live-chat-ticker-renderer, menu.yt-live-chat-ticker-renderer, object.yt-live-chat-ticker-renderer, ol.yt-live-chat-ticker-renderer, p.yt-live-chat-ticker-renderer, pre.yt-live-chat-ticker-renderer, q.yt-live-chat-ticker-renderer, s.yt-live-chat-ticker-renderer, samp.yt-live-chat-ticker-renderer, small.yt-live-chat-ticker-renderer, span.yt-live-chat-ticker-renderer, strike.yt-live-chat-ticker-renderer, strong.yt-live-chat-ticker-renderer, sub.yt-live-chat-ticker-renderer, sup.yt-live-chat-ticker-renderer, table.yt-live-chat-ticker-renderer, tbody.yt-live-chat-ticker-renderer, td.yt-live-chat-ticker-renderer, tfoot.yt-live-chat-ticker-renderer, th.yt-live-chat-ticker-renderer, thead.yt-live-chat-ticker-renderer, tr.yt-live-chat-ticker-renderer, tt.yt-live-chat-ticker-renderer, u.yt-live-chat-ticker-renderer, ul.yt-live-chat-ticker-renderer, var.yt-live-chat-ticker-renderer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -20,7 +22,9 @@ yt-live-chat-ticker-renderer {
|
||||
|
||||
#items.yt-live-chat-ticker-renderer {
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
/* 为了支持滚动 */
|
||||
/* overflow: hidden; */
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
padding: 0 24px 8px 24px;
|
||||
}
|
||||
@@ -63,3 +67,5 @@ yt-icon.yt-live-chat-ticker-renderer {
|
||||
padding: 4px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
483
frontend/src/blcsdk.js
Normal file
483
frontend/src/blcsdk.js
Normal file
@@ -0,0 +1,483 @@
|
||||
/** @module blcsdk */
|
||||
|
||||
(function(root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define([], factory)
|
||||
} else {
|
||||
root.blcsdk = factory()
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function() {
|
||||
const exports = {}
|
||||
|
||||
const VERSION = '1.0.0'
|
||||
/**
|
||||
* 取SDK版本
|
||||
* @returns {string} "1.0.0"
|
||||
*/
|
||||
function getVersion() {
|
||||
return VERSION
|
||||
}
|
||||
exports.getVersion = getVersion
|
||||
|
||||
// 初始化消息的Promise {promise, resolve, reject}
|
||||
let initPromise = null
|
||||
// 初始化消息,包含版本、配置等信息
|
||||
let initMsg = null
|
||||
|
||||
/**
|
||||
* 消息处理器
|
||||
* @type {MsgHandler}
|
||||
*/
|
||||
let msgHandler = null
|
||||
/**
|
||||
* 用户设置的消息处理器
|
||||
* @type {MsgHandler}
|
||||
*/
|
||||
let rawMsgHandler = null
|
||||
|
||||
/**
|
||||
* @typedef InitOptions
|
||||
* @property {boolean} noMsgDelay 去掉消息延迟,但会导致消息不平滑
|
||||
*/
|
||||
|
||||
/**
|
||||
* 初始化SDK
|
||||
*
|
||||
* 在调用除了{@link setMsgHandler}以外的其他接口之前必须先调用这个
|
||||
*/
|
||||
async function init(
|
||||
/** @type {?InitOptions} */
|
||||
{noMsgDelay = false} = {}
|
||||
) {
|
||||
if (initPromise) {
|
||||
throw new Error('Cannot call init() again')
|
||||
}
|
||||
// initPromise = Promise.withResolvers()
|
||||
initPromise = {}
|
||||
initPromise.promise = new Promise((resolve, reject) => {
|
||||
initPromise.resolve = resolve
|
||||
initPromise.reject = reject
|
||||
})
|
||||
|
||||
if (window.parent === window) {
|
||||
initPromise.reject(new Error('No parent window'))
|
||||
return initPromise.promise
|
||||
}
|
||||
|
||||
msgHandler = noMsgDelay ? new SdkMsgHandler() : new SmoothedSdkMsgHandler()
|
||||
window.addEventListener('message', onWindowMessage)
|
||||
|
||||
// 连接blivechat
|
||||
blcSendMsg('blcTemplateConnect')
|
||||
setTimeout(() => initPromise.reject(new Error('Timed out waiting for blcInit message')), 10 * 1000)
|
||||
|
||||
// 等待初始化消息
|
||||
initMsg = await initPromise.promise
|
||||
console.debug('blcsdk initialized, init_msg=', initMsg)
|
||||
}
|
||||
exports.init = init
|
||||
|
||||
/**
|
||||
* 设置消息处理器
|
||||
* @param {?MsgHandler} handler 消息处理器
|
||||
*/
|
||||
function setMsgHandler(handler) {
|
||||
rawMsgHandler = handler
|
||||
}
|
||||
exports.setMsgHandler = setMsgHandler
|
||||
|
||||
/**
|
||||
* 取blivechat前端版本
|
||||
* @returns {string} "v1.10.0-dev"
|
||||
*/
|
||||
function getBlcVersion() {
|
||||
if (!initMsg) {
|
||||
throw new Error('Please call init() first')
|
||||
}
|
||||
return initMsg.blcVersion
|
||||
}
|
||||
exports.getBlcVersion = getBlcVersion
|
||||
|
||||
/**
|
||||
* 取blivechat前端用的SDK版本。是父窗口用的版本,不是这个包的{@link getVersion}返回值
|
||||
* @returns {string} "1.0.0"
|
||||
*/
|
||||
function getBlcSdkVersion() {
|
||||
if (!initMsg) {
|
||||
throw new Error('Please call init() first')
|
||||
}
|
||||
return initMsg.sdkVersion
|
||||
}
|
||||
exports.getBlcSdkVersion = getBlcSdkVersion
|
||||
|
||||
/**
|
||||
* @typedef Config
|
||||
* @property {boolean} showGiftName 显示礼物名
|
||||
* @property {boolean} mergeSimilarDanmaku 合并相似弹幕
|
||||
* @property {boolean} mergeGift 合并礼物
|
||||
* @property {number} maxNumber 最大弹幕数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 取blivechat前端房间部分配置
|
||||
* @returns {Config}
|
||||
*/
|
||||
function getConfig() {
|
||||
if (!initMsg) {
|
||||
throw new Error('Please call init() first')
|
||||
}
|
||||
return Object.freeze(initMsg.config)
|
||||
}
|
||||
exports.getConfig = getConfig
|
||||
|
||||
function blcSendMsg(type, data = null) {
|
||||
if (window.parent === window) {
|
||||
return
|
||||
}
|
||||
let msg = { type, data }
|
||||
window.parent.postMessage(msg, '*')
|
||||
}
|
||||
|
||||
function onWindowMessage(event) {
|
||||
if (event.source !== window.parent) {
|
||||
return
|
||||
}
|
||||
|
||||
let { type, data } = event.data
|
||||
switch (type) {
|
||||
case 'blcInit':
|
||||
initPromise.resolve(data)
|
||||
break
|
||||
case 'blcAddMsg':
|
||||
msgHandler.addMsg(data)
|
||||
break
|
||||
case 'blcDelMsgs':
|
||||
msgHandler.delMsgs(data.ids)
|
||||
break
|
||||
case 'blcUpdateMsg':
|
||||
msgHandler.updateMsg(data.id, data.newValuesObj)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/** 模板消息处理器接口 */
|
||||
class MsgHandler {
|
||||
/**
|
||||
* 添加消息
|
||||
* @param {AnyDisplayMsg} msg
|
||||
*/
|
||||
addMsg(msg) {}
|
||||
|
||||
/**
|
||||
* 删除消息,主要用于撤回醒目留言
|
||||
* @param {string[]} ids 要删除的消息ID
|
||||
*/
|
||||
delMsgs(ids) {}
|
||||
|
||||
/**
|
||||
* 更新消息字段,主要用于更新翻译结果
|
||||
* @param {string} id 要更新的消息ID
|
||||
* @param {Object} newValuesObj 字段和对应的新值
|
||||
*/
|
||||
updateMsg(id, newValuesObj) {}
|
||||
}
|
||||
exports.MsgHandler = MsgHandler
|
||||
|
||||
class SdkMsgHandler extends MsgHandler {
|
||||
addMsg(msg) { this._callRawHandler('addMsg', msg) }
|
||||
delMsgs(ids) { this._callRawHandler('delMsgs', ids) }
|
||||
updateMsg(id, newValuesObj) { this._callRawHandler('updateMsg', id, newValuesObj) }
|
||||
_callRawHandler(...args) { doCallRawHandler(...args) }
|
||||
}
|
||||
|
||||
function doCallRawHandler(funcName, ...args) {
|
||||
if (!rawMsgHandler) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
let func = rawMsgHandler[funcName]
|
||||
return func.call(rawMsgHandler, ...args)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息时间间隔范围
|
||||
const MSG_MIN_INTERVAL = 80
|
||||
const MSG_MAX_INTERVAL = 1000
|
||||
|
||||
class SmoothedSdkMsgHandler extends SdkMsgHandler {
|
||||
constructor() {
|
||||
super()
|
||||
// 消息队列
|
||||
this._queue = []
|
||||
// 消费消息队列的定时器ID
|
||||
this._emitSmoothedMsgTimerId = null
|
||||
// 最近进队列的时间间隔,用来估计下次进队列的时间
|
||||
this._enqueueIntervals = []
|
||||
// 上次进队列的时间
|
||||
this._lastEnqueueTime = null
|
||||
// 估计的下次进队列时间间隔
|
||||
this._estimatedEnqueueInterval = null
|
||||
|
||||
this._boundEmitSmoothedMsgs = this._emitSmoothedMsgs.bind(this)
|
||||
}
|
||||
|
||||
_callRawHandler(funcName, ...args) {
|
||||
let msg = {funcName, args}
|
||||
this._enqueueMsg(msg)
|
||||
}
|
||||
|
||||
_enqueueMsg(msg) {
|
||||
// 估计进队列时间间隔
|
||||
if (!this._lastEnqueueTime) {
|
||||
this._lastEnqueueTime = new Date()
|
||||
} else {
|
||||
let curTime = new Date()
|
||||
let interval = curTime - this._lastEnqueueTime
|
||||
// 真实的进队列时间间隔模式大概是这样:2500, 300, 300, 300, 2500, 300, ...
|
||||
// B站消息有缓冲,会一次发多条消息。这里把波峰视为发送了一次真实的WS消息,所以要过滤掉间隔太小的
|
||||
if (interval > 1000 || this._enqueueIntervals.length < 5) {
|
||||
this._enqueueIntervals.push(interval)
|
||||
if (this._enqueueIntervals.length > 5) {
|
||||
this._enqueueIntervals.splice(0, this._enqueueIntervals.length - 5)
|
||||
}
|
||||
// 这边估计得尽量大,只要不太早把消息缓冲发完就是平滑的。有MESSAGE_MAX_INTERVAL保底,不会让消息延迟太大
|
||||
// 其实可以用单调队列求最大值,偷懒不写了
|
||||
this._estimatedEnqueueInterval = Math.max(...this._enqueueIntervals)
|
||||
}
|
||||
// 上次入队时间还是要设置,否则会太早把消息缓冲发完,然后较长时间没有新消息
|
||||
this._lastEnqueueTime = curTime
|
||||
}
|
||||
|
||||
this._queue.push(msg)
|
||||
|
||||
if (!this._emitSmoothedMsgTimerId) {
|
||||
this._emitSmoothedMsgTimerId = setTimeout(this._boundEmitSmoothedMsgs)
|
||||
}
|
||||
}
|
||||
|
||||
_emitSmoothedMsgs() {
|
||||
this._emitSmoothedMsgTimerId = null
|
||||
if (this._queue.length <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 估计的下次进队列剩余时间
|
||||
let estimatedNextEnqueueRemainTime = 10 * 1000
|
||||
if (this._estimatedEnqueueInterval) {
|
||||
estimatedNextEnqueueRemainTime = Math.max(this._lastEnqueueTime - new Date() + this._estimatedEnqueueInterval, 1)
|
||||
}
|
||||
// 计算发送的消息数,保证在下次进队列之前发完
|
||||
// 下次进队列之前应该发多少条消息
|
||||
let shouldEmitNum = Math.max(this._queue.length, 0)
|
||||
// 下次进队列之前最多能发多少次
|
||||
let maxCanEmitCount = estimatedNextEnqueueRemainTime / MSG_MIN_INTERVAL
|
||||
// 这次发多少条消息
|
||||
let numToEmit
|
||||
if (shouldEmitNum < maxCanEmitCount) {
|
||||
// 队列中消息数很少,每次发1条也能发完
|
||||
numToEmit = 1
|
||||
} else {
|
||||
// 每次发1条以上,保证按最快速度能发完
|
||||
numToEmit = Math.ceil(shouldEmitNum / maxCanEmitCount)
|
||||
}
|
||||
|
||||
// 发消息
|
||||
let msgs = this._queue.splice(0, numToEmit)
|
||||
for (let msg of msgs) {
|
||||
doCallRawHandler(msg.funcName, ...msg.args)
|
||||
}
|
||||
|
||||
if (this._queue.length <= 0) {
|
||||
return
|
||||
}
|
||||
// 消息没发完,计算下次发消息时间
|
||||
let sleepTime
|
||||
if (numToEmit === 1) {
|
||||
// 队列中消息数很少,随便定个[MESSAGE_MIN_INTERVAL, MESSAGE_MAX_INTERVAL]的时间
|
||||
sleepTime = estimatedNextEnqueueRemainTime / this._queue.length
|
||||
sleepTime *= 0.5 + Math.random()
|
||||
if (sleepTime > MSG_MAX_INTERVAL) {
|
||||
sleepTime = MSG_MAX_INTERVAL
|
||||
} else if (sleepTime < MSG_MIN_INTERVAL) {
|
||||
sleepTime = MSG_MIN_INTERVAL
|
||||
}
|
||||
} else {
|
||||
// 按最快速度发
|
||||
sleepTime = MSG_MIN_INTERVAL
|
||||
}
|
||||
this._emitSmoothedMsgTimerId = window.setTimeout(this._boundEmitSmoothedMsgs, sleepTime)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
* @enum {number}
|
||||
*/
|
||||
const MsgType = Object.freeze({
|
||||
/** 评论 @see TextMsg */
|
||||
TEXT: 0,
|
||||
/** 礼物 @see GiftMsg */
|
||||
GIFT: 1,
|
||||
/** 上舰 @see MemberMsg */
|
||||
MEMBER: 2,
|
||||
/** 醒目留言 @see SuperChatMsg */
|
||||
SUPER_CHAT: 3,
|
||||
})
|
||||
exports.MsgType = MsgType
|
||||
|
||||
/**
|
||||
* 作者类型
|
||||
* @enum {number}
|
||||
*/
|
||||
const AuthorType = Object.freeze({
|
||||
NORMAL: 0,
|
||||
/** 舰队 */
|
||||
MEMBER: 1,
|
||||
/** 房管 */
|
||||
ADMIN: 2,
|
||||
/** 主播 */
|
||||
OWNER: 3,
|
||||
})
|
||||
exports.AuthorType = AuthorType
|
||||
|
||||
/**
|
||||
* 舰队等级。因为历史原因,消息里的字段名叫`privilegeType`
|
||||
* @enum {number}
|
||||
*/
|
||||
const GuardLevel = Object.freeze({
|
||||
NONE: 0,
|
||||
/** 总督 */
|
||||
LV3: 1,
|
||||
/** 提督 */
|
||||
LV2: 2,
|
||||
/** 舰长 */
|
||||
LV1: 3,
|
||||
})
|
||||
exports.GuardLevel = GuardLevel
|
||||
|
||||
/**
|
||||
* 一段内容的类型
|
||||
* @enum {number}
|
||||
*/
|
||||
const ContentPartType = Object.freeze({
|
||||
/** 文本 */
|
||||
TEXT: 0,
|
||||
/** 图片 */
|
||||
IMAGE: 1,
|
||||
})
|
||||
exports.ContentPartType = ContentPartType
|
||||
|
||||
//
|
||||
// 以下只用于类型注解,运行时没什么用
|
||||
//
|
||||
|
||||
/**
|
||||
* 用于显示的消息
|
||||
* @typedef {TextMsg | GiftMsg | MemberMsg | SuperChatMsg} AnyDisplayMsg
|
||||
*/
|
||||
|
||||
/**
|
||||
* 评论消息。因为历史原因叫TextMsg,实际上会包含表情图片
|
||||
* @typedef TextMsg
|
||||
* @property {string} id 消息ID
|
||||
* @property {MsgType} type 消息类型
|
||||
* @property {string} avatarUrl 用户头像URL
|
||||
* @property {Date} time 时间
|
||||
* @property {string} authorName 用户名
|
||||
* @property {AuthorType} authorType 用户类型
|
||||
* @property {string} content 纯文本表示的内容
|
||||
* @property {AnyContentPart[]} contentParts 解析后的内容,包括文本、图片
|
||||
* @property {GuardLevel} privilegeType 舰队等级
|
||||
* @property {string} translation 内容的翻译,刚添加时一般是空的,之后通过更新消息赋值
|
||||
* @property {string} uid 用户Open ID或ID,使用房间ID连接时不保证是唯一的
|
||||
* @property {number} medalLevel 勋章等级,如果没戴当前房间勋章则为0
|
||||
* @property {string} medalName 勋章名,如果没戴当前房间勋章则为空字符串
|
||||
*/
|
||||
exports.TextMsg = /** @type {TextMsg} */ (undefined)
|
||||
|
||||
/**
|
||||
* 一段内容
|
||||
* @typedef {TextContentPart | ImageContentPart} AnyContentPart
|
||||
*/
|
||||
/**
|
||||
|
||||
* 一段文本内容
|
||||
* @typedef TextContentPart
|
||||
* @property {ContentPartType} type 内容类型
|
||||
* @property {string} text 内容
|
||||
*/
|
||||
|
||||
/**
|
||||
* 一段图片内容
|
||||
* @typedef ImageContentPart
|
||||
* @property {ContentPartType} type 内容类型
|
||||
* @property {string} text 纯文本表示的内容
|
||||
* @property {string} url 图片URL
|
||||
* @property {number} width 宽度,加载失败则为0
|
||||
* @property {number} height 高度,加载失败则为0
|
||||
*/
|
||||
|
||||
/**
|
||||
* 礼物消息
|
||||
* @typedef GiftMsg
|
||||
* @property {string} id 消息ID
|
||||
* @property {MsgType} type 消息类型
|
||||
* @property {string} avatarUrl 用户头像URL
|
||||
* @property {Date} time 时间
|
||||
* @property {string} authorName 用户名
|
||||
* @property {string} authorNamePronunciation 用户名读音
|
||||
* @property {number} price 总价(元),免费礼物则为0
|
||||
* @property {string} giftName 礼物名
|
||||
* @property {number} num 数量
|
||||
* @property {number} totalFreeCoin 免费礼物总价(银瓜子数),付费礼物则为0
|
||||
* @property {number} giftId 礼物ID
|
||||
* @property {string} giftIconUrl 礼物图标URL
|
||||
* @property {string} uid 用户Open ID或ID,使用房间ID连接时不保证是唯一的
|
||||
* @property {GuardLevel} privilegeType 舰队等级
|
||||
* @property {number} medalLevel 勋章等级,如果没戴当前房间勋章则为0
|
||||
* @property {string} medalName 勋章名,如果没戴当前房间勋章则为空字符串
|
||||
*/
|
||||
exports.GiftMsg = /** @type {GiftMsg} */ (undefined)
|
||||
|
||||
/**
|
||||
* 上舰消息
|
||||
* @typedef MemberMsg
|
||||
* @property {string} id 消息ID
|
||||
* @property {MsgType} type 消息类型
|
||||
* @property {string} avatarUrl 用户头像URL
|
||||
* @property {Date} time 时间
|
||||
* @property {string} authorName 用户名
|
||||
* @property {string} authorNamePronunciation 用户名读音
|
||||
* @property {GuardLevel} privilegeType 舰队等级
|
||||
* @property {number} num 数量
|
||||
* @property {string} unit 单位("月")
|
||||
* @property {number} price 总价(元)
|
||||
* @property {string} uid 用户Open ID或ID,使用房间ID连接时不保证是唯一的
|
||||
* @property {number} medalLevel 勋章等级,如果没戴当前房间勋章则为0
|
||||
* @property {string} medalName 勋章名,如果没戴当前房间勋章则为空字符串
|
||||
*/
|
||||
exports.MemberMsg = /** @type {MemberMsg} */ (undefined)
|
||||
|
||||
/**
|
||||
* 醒目留言消息
|
||||
* @typedef SuperChatMsg
|
||||
* @property {string} id 消息ID
|
||||
* @property {MsgType} type 消息类型
|
||||
* @property {string} avatarUrl 用户头像URL
|
||||
* @property {Date} time 时间
|
||||
* @property {string} authorName 用户名
|
||||
* @property {string} authorNamePronunciation 用户名读音
|
||||
* @property {number} price 价格(元)
|
||||
* @property {string} content 内容
|
||||
* @property {string} translation 内容的翻译,刚添加时一般是空的,之后通过更新消息赋值
|
||||
* @property {string} uid 用户Open ID或ID,使用房间ID连接时不保证是唯一的
|
||||
* @property {GuardLevel} privilegeType 舰队等级
|
||||
* @property {number} medalLevel 勋章等级,如果没戴当前房间勋章则为0
|
||||
* @property {string} medalName 勋章名,如果没戴当前房间勋章则为空字符串
|
||||
*/
|
||||
exports.SuperChatMsg = /** @type {SuperChatMsg} */ (undefined)
|
||||
|
||||
return exports
|
||||
}))
|
||||
@@ -9,10 +9,10 @@
|
||||
:isInMemberMessage="false" :authorName="authorName" :authorType="authorType" :privilegeType="privilegeType"
|
||||
></author-chip>
|
||||
<span id="message" 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>
|
||||
<template v-for="(content, index) in contentParts">
|
||||
<span :key="index" v-if="content.type === CONTENT_PART_TYPE_TEXT">{{ content.text }}</span>
|
||||
<!-- 如果CSS设置的尺寸比属性设置的尺寸还大,在图片加载完后布局会变化,可能导致滚动卡住,没什么好的解决方法 -->
|
||||
<img :key="index" v-else-if="content.type === CONTENT_TYPE_IMAGE"
|
||||
<img :key="index" v-else-if="content.type === CONTENT_PART_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}`"
|
||||
:width="content.width" :height="content.height"
|
||||
@@ -48,14 +48,14 @@ export default {
|
||||
time: Date,
|
||||
authorName: String,
|
||||
authorType: Number,
|
||||
richContent: Array,
|
||||
contentParts: Array,
|
||||
privilegeType: Number,
|
||||
repeated: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CONTENT_TYPE_TEXT: constants.CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_IMAGE: constants.CONTENT_TYPE_IMAGE
|
||||
CONTENT_PART_TYPE_TEXT: constants.CONTENT_PART_TYPE_TEXT,
|
||||
CONTENT_PART_TYPE_IMAGE: constants.CONTENT_PART_TYPE_IMAGE
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -84,12 +84,12 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
yt-live-chat-text-message-renderer>#content>#message>.el-badge {
|
||||
<style scoped>
|
||||
#message >>> .el-badge {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer>#content>#message>.el-badge .el-badge__content {
|
||||
#message >>> .el-badge__content {
|
||||
font-size: 12px !important;
|
||||
line-height: 18px !important;
|
||||
text-shadow: none !important;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<yt-live-chat-ticker-renderer :hidden="showMessages.length === 0">
|
||||
<div id="container" dir="ltr" class="style-scope yt-live-chat-ticker-renderer">
|
||||
<el-scrollbar ref="scrollbar" id="container" dir="ltr" class="style-scope yt-live-chat-ticker-renderer">
|
||||
<transition-group tag="div" :css="false" @enter="onTickerItemEnter" @leave="onTickerItemLeave"
|
||||
id="items" class="style-scope yt-live-chat-ticker-renderer"
|
||||
>
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
</yt-live-chat-ticker-paid-message-item-renderer>
|
||||
</transition-group>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template v-if="pinnedMessage">
|
||||
<membership-item :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
|
||||
class="style-scope yt-live-chat-ticker-renderer"
|
||||
@@ -110,7 +110,11 @@ export default {
|
||||
el.style.width = 0
|
||||
await this.$nextTick()
|
||||
el.style.width = `${width}px`
|
||||
window.setTimeout(done, 200)
|
||||
window.setTimeout(() => {
|
||||
done()
|
||||
// 因为el-scrollbar监听不到事件,这里必须手动调update
|
||||
this.$refs.scrollbar.update()
|
||||
}, 200)
|
||||
},
|
||||
onTickerItemLeave(el, done) {
|
||||
el.classList.add('sliding-down')
|
||||
@@ -122,6 +126,8 @@ export default {
|
||||
el.classList.remove('collapsing')
|
||||
el.style.width = 'auto'
|
||||
done()
|
||||
// 因为el-scrollbar监听不到事件,这里必须手动调update
|
||||
this.$refs.scrollbar.update()
|
||||
}, 200)
|
||||
}, 200)
|
||||
},
|
||||
|
||||
@@ -34,8 +34,8 @@ 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
|
||||
export const CONTENT_PART_TYPE_TEXT = 0
|
||||
export const CONTENT_PART_TYPE_IMAGE = 1
|
||||
|
||||
// 美元 -> 人民币 汇率
|
||||
const EXCHANGE_RATE = 7
|
||||
@@ -173,15 +173,15 @@ export function getShowContent(message) {
|
||||
return message.content
|
||||
}
|
||||
|
||||
export function getShowRichContent(message) {
|
||||
let richContent = [...message.richContent]
|
||||
export function getShowContentParts(message) {
|
||||
let contentParts = [...message.contentParts]
|
||||
if (message.translation) {
|
||||
richContent.push({
|
||||
type: CONTENT_TYPE_TEXT,
|
||||
contentParts.push({
|
||||
type: CONTENT_PART_TYPE_TEXT,
|
||||
text: `(${message.translation})`
|
||||
})
|
||||
}
|
||||
return richContent
|
||||
return contentParts
|
||||
}
|
||||
|
||||
export function getGiftShowContent(message, showGiftName) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
:authorName="message.authorName"
|
||||
:authorType="message.authorType"
|
||||
:privilegeType="message.privilegeType"
|
||||
:richContent="getShowRichContent(message)"
|
||||
:contentParts="getShowContentParts(message)"
|
||||
:repeated="message.repeated"
|
||||
></text-message>
|
||||
<paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_GIFT"
|
||||
@@ -153,7 +153,7 @@ export default {
|
||||
},
|
||||
getGiftShowNameAndNum: constants.getGiftShowNameAndNum,
|
||||
getShowContent: constants.getShowContent,
|
||||
getShowRichContent: constants.getShowRichContent,
|
||||
getShowContentParts: constants.getShowContentParts,
|
||||
getShowAuthorName: constants.getShowAuthorName,
|
||||
|
||||
addMessage(message) {
|
||||
@@ -192,7 +192,7 @@ export default {
|
||||
}
|
||||
return false
|
||||
},
|
||||
mergeSimilarGift(authorName, price, _freePrice, giftName, num) {
|
||||
mergeSimilarGift(authorName, price, _totalFreeCoin, giftName, num) {
|
||||
for (let message of this.iterRecentMessages(5)) {
|
||||
if (
|
||||
message.type === constants.MESSAGE_TYPE_GIFT
|
||||
@@ -201,7 +201,7 @@ export default {
|
||||
) {
|
||||
this.updateMessage(message.id, { $add: {
|
||||
price: price,
|
||||
// freePrice: freePrice, // 暂时没用到
|
||||
// totalFreeCoin: totalFreeCoin, // 暂时没用到
|
||||
num: num
|
||||
} })
|
||||
return true
|
||||
|
||||
@@ -65,6 +65,25 @@ export default {
|
||||
addEmoticon: 'Add emote',
|
||||
emoticonFileTooLarge: 'File size is too large. Max size is 1MB',
|
||||
|
||||
template: 'Custom HTML Templates',
|
||||
templateHelp: 'Help',
|
||||
templateHelpContent: `\
|
||||
<p>Custom HTML templates allow complete customization of the room page, including DOM structure and CSS styles.
|
||||
Templates may be developed by third-party authors, and their security and quality are the responsibility of the template author.
|
||||
You can find some published templates on <a target="_blank" href="https://github.com/xfgryujk/blivechat/discussions/categories/%E8%87%AA%E5%AE%9A%E4%B9%89html%E6%A8%A1%E6%9D%BF"
|
||||
>GitHub Discussions</a></p>
|
||||
<p>To install a template: Put the extracted template directory into the "data/custom_public/templates" directory, then wait
|
||||
about 10 seconds and refresh the webpage. Alternatively, you can directly enter the template URL to use an online template</p>
|
||||
<p>Note: After modifying the template setting, the custom CSS in OBS will not take effect</p>
|
||||
<p><a target="_blank" href="https://github.com/xfgryujk/blivechat/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89HTML%E6%A8%A1%E6%9D%BF"
|
||||
>Template Development Documentation</a></p>
|
||||
`,
|
||||
templateDefaultTitle: 'Default',
|
||||
templateDefaultDescription: 'A YouTube-style template that can be styled with custom CSS. Choose this if you are unfamiliar with custom HTML templates, otherwise your custom CSS in OBS will not take effect',
|
||||
templateCustomUrlTitle: 'Enter template URL',
|
||||
templateCustomUrlDescription: 'For templates not listed, you can manually enter the URL provided by the author here',
|
||||
author: 'Author: ',
|
||||
|
||||
urlTooLong: 'The room URL is too long, and will be truncated by Livehime (but not by OBS)',
|
||||
roomUrlUpdated: 'The room URL is updated. Remember to copy it again',
|
||||
roomUrl: 'Room URL',
|
||||
@@ -186,12 +205,12 @@ export default {
|
||||
be developed by third-party authors, and the security and quality are the responsibility of the plugin author. You can
|
||||
find some published plugins in <a target="_blank" href="https://github.com/xfgryujk/blivechat/discussions/categories/%E6%8F%92%E4%BB%B6"
|
||||
>GitHub Discussions</a></p>
|
||||
<p>Plugin installation method: Put the extracted plugin directory into the "data/plugins" directory, then restart blivechat</p>
|
||||
<p>To install a plugin: Put the extracted plugin directory into the "data/plugins" directory, then restart blivechat</p>
|
||||
<p>Notes: Most plugins require enabling the "Relay messages by the server" option and connecting to the room
|
||||
in order to receive messages</p>
|
||||
<p><a target="_blank" href="https://www.bilibili.com/video/BV1nZ42187TX/">Introducing video</a></p>
|
||||
<p><a target="_blank" href="https://github.com/xfgryujk/blivechat/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F">
|
||||
Plugin development documentation</a></p>
|
||||
<p><a target="_blank" href="https://www.bilibili.com/video/BV1nZ42187TX/">Introducing Video</a></p>
|
||||
<p><a target="_blank" href="https://github.com/xfgryujk/blivechat/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F"
|
||||
>Plugin Development Documentation</a></p>
|
||||
`,
|
||||
author: 'Author: ',
|
||||
disabledByServer: 'Administration for plugins is disabled by the server',
|
||||
|
||||
@@ -65,6 +65,25 @@ export default {
|
||||
addEmoticon: 'スタンプを追加',
|
||||
emoticonFileTooLarge: 'ファイルサイズが大きすぎます。最大サイズは1MBです',
|
||||
|
||||
template: 'カスタムHTMLテンプレート',
|
||||
templateHelp: 'ヘルプ',
|
||||
templateHelpContent: `\
|
||||
<p>カスタムHTMLテンプレートを使用すると、DOM構造やCSSスタイルを含むフロントエンドページを完全にカスタマイズできます。
|
||||
テンプレートはサードパーティの作者によって開発される場合があり、そのセキュリティと品質はテンプレート作者の責任となります。
|
||||
公開されているテンプレートは<a target="_blank" href="https://github.com/xfgryujk/blivechat/discussions/categories/%E8%87%AA%E5%AE%9A%E4%B9%89html%E6%A8%A1%E6%9D%BF"
|
||||
>GitHub Discussions</a>で入手できます</p>
|
||||
<p>テンプレートのインストール方法:解凍したテンプレートディレクトリを「data/custom_public/templates」フォルダに配置し、
|
||||
約10秒待ってからウェブページを更新してください。また、テンプレートURLを直接入力してオンラインテンプレートを使用することもできます</p>
|
||||
<p>注意:カスタムテンプレートを変更した場合、OBS内のカスタムCSSは反映されません</p>
|
||||
<p><a target="_blank" href="https://github.com/xfgryujk/blivechat/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89HTML%E6%A8%A1%E6%9D%BF"
|
||||
>テンプレート開発ドキュメント</a></p>
|
||||
`,
|
||||
templateDefaultTitle: 'デフォルト',
|
||||
templateDefaultDescription: 'YouTube風のテンプレートで、カスタムCSSでスタイルを変更できます。カスタムHTMLテンプレートの機能に慣れていない場合はこれを選択してください。そうでない場合、OBS内のカスタムCSSは反映されません',
|
||||
templateCustomUrlTitle: 'テンプレートURLを入力',
|
||||
templateCustomUrlDescription: 'リストにないテンプレートは、作者が提供するURLをここで手動で入力できます',
|
||||
author: '作者:',
|
||||
|
||||
urlTooLong: 'ルームのURLが長すぎて、直播姬によって切り詰められます(ただし、OBSでは切り詰められません)',
|
||||
roomUrlUpdated: 'ルームのURLが更新されました。再度コピーすることをお忘れなく',
|
||||
roomUrl: 'ルームのURL',
|
||||
|
||||
@@ -65,6 +65,23 @@ export default {
|
||||
addEmoticon: '添加表情',
|
||||
emoticonFileTooLarge: '文件尺寸太大,最大1MB',
|
||||
|
||||
template: '自定义HTML模板',
|
||||
templateHelp: '帮助',
|
||||
templateHelpContent: `\
|
||||
<p>自定义HTML模板可以完全自定义房间页面,包括DOM结构、CSS样式。模板可能由第三方作者开发,其安全性和质量由模板作者负责。
|
||||
你可以在<a target="_blank" href="https://github.com/xfgryujk/blivechat/discussions/categories/%E8%87%AA%E5%AE%9A%E4%B9%89html%E6%A8%A1%E6%9D%BF"
|
||||
>GitHub Discussions</a>获取一些已发布的模板</p>
|
||||
<p>模板安装方法:把解压后的模板目录放到“data/custom_public/templates”目录,然后等待约10秒后刷新网页。
|
||||
另外你也可以直接输入模板URL来使用在线模板</p>
|
||||
<p>注意:修改自定义模板设置后,OBS中的自定义CSS将不会生效</p>
|
||||
<p><a target="_blank" href="https://github.com/xfgryujk/blivechat/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89HTML%E6%A8%A1%E6%9D%BF">模板开发文档</a></p>
|
||||
`,
|
||||
templateDefaultTitle: '默认',
|
||||
templateDefaultDescription: '仿YouTube风格的模板,可以用自定义CSS修改样式。如果你不了解自定义HTML模板的功能,就选择这个,否则OBS中的自定义CSS不会生效',
|
||||
templateCustomUrlTitle: '输入模板URL',
|
||||
templateCustomUrlDescription: '没有列出的模板,也可以在这里手动输入作者提供的URL',
|
||||
author: '作者:',
|
||||
|
||||
urlTooLong: '房间URL太长了,会被直播姬截断(OBS不会)',
|
||||
roomUrlUpdated: '房间URL已更新,记得重新复制',
|
||||
roomUrl: '房间URL',
|
||||
@@ -79,8 +96,8 @@ export default {
|
||||
legacy: '经典',
|
||||
lineLike: '仿微信',
|
||||
|
||||
light: '明亮',
|
||||
dark: '黑暗',
|
||||
light: '亮色',
|
||||
dark: '暗色',
|
||||
|
||||
outlines: '描边',
|
||||
showOutlines: '显示描边',
|
||||
@@ -150,7 +167,7 @@ export default {
|
||||
fadeOutTime: '淡出时间(毫秒)',
|
||||
slide: '滑动',
|
||||
reverseSlide: '反向滑动',
|
||||
playAnimation: '播放动画',
|
||||
playAnimation: '生成消息',
|
||||
|
||||
result: '结果',
|
||||
copy: '复制',
|
||||
@@ -186,7 +203,7 @@ export default {
|
||||
你可以在<a target="_blank" href="https://github.com/xfgryujk/blivechat/discussions/categories/%E6%8F%92%E4%BB%B6"
|
||||
>GitHub Discussions</a>获取一些已发布的插件</p>
|
||||
<p>插件安装方法:把解压后的插件目录放到“data/plugins”目录,然后重启blivechat</p>
|
||||
<p>注意事项:大部分插件需要开启“通过服务器转发消息”,并且连接到房间,才能接收消息</p>
|
||||
<p>注意:大部分插件需要开启“通过服务器转发消息”,并且连接到房间,才能接收消息</p>
|
||||
<p><a target="_blank" href="https://www.bilibili.com/video/BV1nZ42187TX/">介绍视频</a></p>
|
||||
<p><a target="_blank" href="https://github.com/xfgryujk/blivechat/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F">插件开发文档</a></p>
|
||||
`,
|
||||
|
||||
@@ -83,16 +83,20 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scrollbar-wrapper {
|
||||
.el-scrollbar >>> .scrollbar-wrapper {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.scrollbar-wrapper .el-menu {
|
||||
.el-scrollbar >>> .el-menu {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-scrollbar >>> .is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<sidebar></sidebar>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
<el-button v-show="isMobile" class="menu-button" icon="el-icon-s-unfold" @click="hideSidebar = false"></el-button>
|
||||
<el-button v-show="isMobile" icon="el-icon-s-unfold" @click="hideSidebar = false"></el-button>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
@@ -51,8 +51,8 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.app-wrapper, .sidebar-container {
|
||||
<style scoped>
|
||||
.app-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -67,11 +67,12 @@ export default {
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
height: 100%;
|
||||
background-color: #304156;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-wrapper.mobile .sidebar-container {
|
||||
.mobile .sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -79,13 +80,13 @@ export default {
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.app-wrapper.mobile .sidebar-container.hide-sidebar {
|
||||
.mobile .sidebar-container.hide-sidebar {
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
transform: translate3d(-230px, 0, 0);
|
||||
}
|
||||
|
||||
.sidebar-container .logo-container {
|
||||
.logo-container {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
@@ -93,14 +94,14 @@ export default {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-container .logo-container .sidebar-logo {
|
||||
.sidebar-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.sidebar-container .logo-container .sidebar-title {
|
||||
.sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
@@ -111,7 +112,7 @@ export default {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sidebar-container .version {
|
||||
.version {
|
||||
height: 30px;
|
||||
background: #2b2f3a;
|
||||
color: #aaa;
|
||||
@@ -121,8 +122,4 @@ export default {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-container .is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<div>
|
||||
<h1>{{ $t('help.help') }}</h1>
|
||||
<p>{{ $t('help.p1_1') }} <a href="https://play-live.bilibili.com/" target="_blank">https://play-live.bilibili.com/</a> {{ $t('help.p1_2') }}</p>
|
||||
<p class="img-container"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-1.jpg"></el-image></p>
|
||||
<p><el-image lazy src="/static/img/tutorial/tutorial-1.jpg"></el-image></p>
|
||||
<p>{{ $t('help.p2') }}</p>
|
||||
<p class="img-container large-img"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-2.jpg"></el-image></p>
|
||||
<p><el-image lazy src="/static/img/tutorial/tutorial-2.jpg"></el-image></p>
|
||||
<p>{{ $t('help.p3') }}</p>
|
||||
<p class="img-container large-img"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-3.jpg"></el-image></p>
|
||||
<p><el-image lazy src="/static/img/tutorial/tutorial-3.jpg"></el-image></p>
|
||||
<p>{{ $t('help.p4') }}</p>
|
||||
<p class="img-container"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-4.jpg"></el-image></p>
|
||||
<p><el-image lazy src="/static/img/tutorial/tutorial-4.jpg"></el-image></p>
|
||||
<p>{{ $t('help.p5') }}</p>
|
||||
<p class="img-container large-img"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-5.jpg"></el-image></p>
|
||||
<p><el-image lazy src="/static/img/tutorial/tutorial-5.jpg"></el-image></p>
|
||||
<p><br><br><br><br><br><br><br><br>--------------------------------------------------------------------------------------------------------</p>
|
||||
<p>使用前必看:<a href="https://github.com/xfgryujk/blivechat/wiki/%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9%E5%92%8C%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98" target="_blank">注意事项和常见问题</a></p>
|
||||
<p>喜欢的话可以推荐给别人 _(:з」∠)_</p>
|
||||
@@ -22,13 +22,3 @@ export default {
|
||||
name: 'Help'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.img-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.img-container.large-img .el-image {
|
||||
height: 80vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
222
frontend/src/views/Home/TemplateSelect.vue
Normal file
222
frontend/src/views/Home/TemplateSelect.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
<el-button round icon="el-icon-question" @click="isHelpVisible = true">{{$t('home.templateHelp')}}</el-button>
|
||||
<el-dialog :title="$t('home.templateHelp')" :visible.sync="isHelpVisible">
|
||||
<div style="word-break: initial;" v-html="$t('home.templateHelpContent')"></div>
|
||||
</el-dialog>
|
||||
</p>
|
||||
|
||||
<el-radio-group :value="selectedIndex" @input="onSelect">
|
||||
<el-radio :label="INDEX_DEFAULT" border>
|
||||
<div class="card-body">
|
||||
<div class="title-line">
|
||||
<h3 class="name">{{ $t('home.templateDefaultTitle') }}</h3>
|
||||
</div>
|
||||
<div class="description-line">
|
||||
<div class="description">
|
||||
<p>{{ $t('home.templateDefaultDescription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-radio>
|
||||
|
||||
<el-radio :label="INDEX_CUSTOM" border>
|
||||
<div class="card-body">
|
||||
<div class="title-line">
|
||||
<h3 class="name">{{ $t('home.templateCustomUrlTitle') }}</h3>
|
||||
</div>
|
||||
<div class="description-line">
|
||||
<div class="description">
|
||||
<p>{{ $t('home.templateCustomUrlDescription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-input v-model="customUrl" placeholder="https://example.com/path/to/you/template/index.html"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</el-radio>
|
||||
|
||||
<el-radio v-for="(template, index) in templates" :key="template.id" :label="index" border>
|
||||
<div class="card-body">
|
||||
<div class="title-line">
|
||||
<h3 class="name">{{ template.name }}</h3>
|
||||
<span class="version">{{ template.version }}</span>
|
||||
<span class="author push-inline-end">{{ $t('home.author') }}{{ template.author }}</span>
|
||||
</div>
|
||||
<div class="description-line">
|
||||
<el-image v-if="template.thumbnail !== ''" class="thumbnail" lazy fit="cover" :src="template.thumbnail"></el-image>
|
||||
<div class="description">
|
||||
<p v-if="template.description !== ''">{{ template.description }}</p>
|
||||
<p>URL: {{ template.url }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as mainApi from '@/api/main'
|
||||
|
||||
const INDEX_DEFAULT = 'default'
|
||||
const INDEX_CUSTOM = 'custom'
|
||||
|
||||
export default {
|
||||
name: 'TemplateSelect',
|
||||
props: {
|
||||
value: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
INDEX_DEFAULT,
|
||||
INDEX_CUSTOM,
|
||||
|
||||
isHelpVisible: false,
|
||||
templates: [],
|
||||
customUrl: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedIndex() {
|
||||
if (this.value == '') {
|
||||
return INDEX_DEFAULT
|
||||
}
|
||||
let index = this.templates.findIndex(template => template.url === this.value)
|
||||
if (index !== -1) {
|
||||
return index
|
||||
}
|
||||
return INDEX_CUSTOM
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (this.selectedIndex === INDEX_CUSTOM) {
|
||||
this.customUrl = val
|
||||
}
|
||||
}
|
||||
},
|
||||
customUrl(val) {
|
||||
if (this.selectedIndex === INDEX_CUSTOM) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateTemplates()
|
||||
},
|
||||
methods: {
|
||||
async updateTemplates() {
|
||||
try {
|
||||
this.templates = (await mainApi.getTemplates()).templates
|
||||
} catch (e) {
|
||||
this.$message.error(`Failed to fetch templates: ${e}`)
|
||||
throw e
|
||||
} finally {
|
||||
if (this.selectedIndex !== INDEX_CUSTOM) {
|
||||
this.customUrl = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
onSelect(selectedIndex) {
|
||||
let url
|
||||
if (selectedIndex === INDEX_DEFAULT) {
|
||||
url = ''
|
||||
} else if (selectedIndex === INDEX_CUSTOM) {
|
||||
url = this.customUrl
|
||||
} else {
|
||||
url = this.templates[selectedIndex].url
|
||||
}
|
||||
this.$emit('input', url)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-radio-group {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.el-radio {
|
||||
display: block;
|
||||
height: unset;
|
||||
margin: 0 0 16px 0 !important;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-radio >>> .el-radio__input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-radio >>> .el-radio__label {
|
||||
padding-left: unset;
|
||||
}
|
||||
|
||||
.push-inline-end {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: baseline;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-block: 0 0;
|
||||
margin-inline-end: 8px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.version {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.description-line {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: 1em;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
flex: none;
|
||||
width: 190px;
|
||||
height: 105px;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: auto;
|
||||
overflow-y: auto;
|
||||
text-wrap: wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.description-line {
|
||||
flex-wrap: wrap;
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -217,6 +217,10 @@
|
||||
<el-button type="primary" icon="el-icon-plus" @click="addEmoticon">{{$t('home.addEmoticon')}}</el-button>
|
||||
</p>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('home.template')" lazy>
|
||||
<template-select v-model="form.templateUrl"></template-select>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</p>
|
||||
@@ -249,12 +253,14 @@
|
||||
import _ from 'lodash'
|
||||
import download from 'downloadjs'
|
||||
|
||||
import TemplateSelect from './TemplateSelect'
|
||||
import { mergeConfig } from '@/utils'
|
||||
import * as mainApi from '@/api/main'
|
||||
import * as chatConfig from '@/api/chatConfig'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: { TemplateSelect },
|
||||
data() {
|
||||
return {
|
||||
serverConfig: {
|
||||
@@ -384,7 +390,8 @@ export default {
|
||||
getUnvalidatedRoomUrl(isTestRoom) {
|
||||
// 重要的字段放在前面,因为如果被截断就连接不了房间了
|
||||
let frontFields = {
|
||||
roomKeyType: this.form.roomKeyType
|
||||
roomKeyType: this.form.roomKeyType,
|
||||
templateUrl: this.form.templateUrl,
|
||||
}
|
||||
let backFields = {
|
||||
lang: this.$i18n.locale,
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<chat-renderer ref="renderer" :maxNumber="config.maxNumber" :showGiftName="config.showGiftName"></chat-renderer>
|
||||
<chat-renderer v-if="!useCustomTemplate" ref="renderer" :maxNumber="config.maxNumber" :showGiftName="config.showGiftName"></chat-renderer>
|
||||
<div v-else class="template-container">
|
||||
<iframe ref="templateIframe" :src="config.templateUrl" class="template-iframe" frameborder="0"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -12,6 +15,101 @@ import * as chat from '@/api/chat'
|
||||
import * as chatModels from '@/api/chat/models'
|
||||
import ChatRenderer from '@/components/ChatRenderer'
|
||||
import * as constants from '@/components/ChatRenderer/constants'
|
||||
/** @import * as blcsdk from '@/blcsdk' */
|
||||
|
||||
class DefaultRenderer {
|
||||
constructor(rendererVm) {
|
||||
this.addMessage = rendererVm.addMessage
|
||||
this.delMessages = rendererVm.delMessages
|
||||
this.updateMessage = rendererVm.updateMessage
|
||||
this.mergeSimilarText = rendererVm.mergeSimilarText
|
||||
this.mergeSimilarGift = rendererVm.mergeSimilarGift
|
||||
}
|
||||
|
||||
destroy() {
|
||||
let dummyFunc = () => {}
|
||||
this.addMessage = dummyFunc
|
||||
this.delMessages = dummyFunc
|
||||
this.updateMessage = dummyFunc
|
||||
this.mergeSimilarText = dummyFunc
|
||||
this.mergeSimilarGift = dummyFunc
|
||||
}
|
||||
}
|
||||
|
||||
const BLC_SDK_VERSION = '1.0.0'
|
||||
|
||||
class CustomTemplateRenderer {
|
||||
constructor(templateIframe, config) {
|
||||
this._templateIframe = templateIframe
|
||||
this._config = config
|
||||
|
||||
this._enabledSendMessageToTemplate = (type, data) => {
|
||||
let msg = { type, data }
|
||||
templateIframe.contentWindow.postMessage(msg, '*')
|
||||
}
|
||||
this._sendMessageToTemplate = () => {}
|
||||
|
||||
this._boundOnWindowMessage = this._onWindowMessage.bind(this)
|
||||
window.addEventListener('message', this._boundOnWindowMessage)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
window.removeEventListener('message', this._boundOnWindowMessage)
|
||||
|
||||
let dummyFunc = () => {}
|
||||
this._enabledSendMessageToTemplate = dummyFunc
|
||||
this._sendMessageToTemplate = dummyFunc
|
||||
}
|
||||
|
||||
addMessage(message) {
|
||||
this._sendMessageToTemplate('blcAddMsg', message)
|
||||
}
|
||||
|
||||
delMessages(ids) {
|
||||
let data = { ids }
|
||||
this._sendMessageToTemplate('blcDelMsgs', data)
|
||||
}
|
||||
|
||||
updateMessage(id, newValuesObj) {
|
||||
let data = { id, newValuesObj }
|
||||
this._sendMessageToTemplate('blcUpdateMsg', data)
|
||||
}
|
||||
|
||||
mergeSimilarText() {
|
||||
return false
|
||||
}
|
||||
|
||||
mergeSimilarGift() {
|
||||
return false
|
||||
}
|
||||
|
||||
_onWindowMessage(event) {
|
||||
if (event.source !== this._templateIframe.contentWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
let { type } = event.data
|
||||
switch (type) {
|
||||
case 'blcTemplateConnect': {
|
||||
this._sendMessageToTemplate = this._enabledSendMessageToTemplate
|
||||
|
||||
// 发送初始化消息
|
||||
let initData = {
|
||||
blcVersion: process.env.APP_VERSION,
|
||||
sdkVersion: BLC_SDK_VERSION,
|
||||
config: {
|
||||
showGiftName: this._config.showGiftName,
|
||||
mergeSimilarDanmaku: this._config.mergeSimilarDanmaku,
|
||||
mergeGift: this._config.mergeGift,
|
||||
maxNumber: this._config.maxNumber,
|
||||
}
|
||||
}
|
||||
this._sendMessageToTemplate('blcInit', initData)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Room',
|
||||
@@ -42,8 +140,10 @@ export default {
|
||||
textEmoticons: [], // 官方的文本表情(后端配置的)
|
||||
pronunciationConverter: null,
|
||||
|
||||
customStyleElement, // 仅用于样式生成器中预览样式
|
||||
customStyleElement, // 仅用于样式生成器中预览样式和使用自定义模板时
|
||||
presetCssLinkElement: null,
|
||||
|
||||
renderer: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -72,9 +172,26 @@ export default {
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
useCustomTemplate() {
|
||||
return this.config.templateUrl !== ''
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
this.initConfig()
|
||||
|
||||
// 主框架改成透明背景,防止影响到模板
|
||||
if (this.useCustomTemplate) {
|
||||
this.customStyleElement.textContent = 'body { background-color: transparent; }'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.useCustomTemplate) {
|
||||
this.renderer = new CustomTemplateRenderer(this.$refs.templateIframe, this.config)
|
||||
} else {
|
||||
this.renderer = new DefaultRenderer(this.$refs.renderer)
|
||||
}
|
||||
|
||||
if (document.visibilityState === 'visible') {
|
||||
if (this.roomKeyValue === null) {
|
||||
this.init()
|
||||
@@ -101,6 +218,10 @@ export default {
|
||||
if (this.presetCssLinkElement) {
|
||||
document.head.removeChild(this.presetCssLinkElement)
|
||||
}
|
||||
|
||||
if (this.renderer) {
|
||||
this.renderer.destroy()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onVisibilityChange() {
|
||||
@@ -111,8 +232,6 @@ export default {
|
||||
this.init()
|
||||
},
|
||||
async init() {
|
||||
this.initConfig()
|
||||
|
||||
let initChatClientPromise = this.initChatClient()
|
||||
this.initTextEmoticons()
|
||||
if (this.config.giftUsernamePronunciation !== '') {
|
||||
@@ -242,7 +361,7 @@ export default {
|
||||
let { type, data } = event.data
|
||||
switch (type) {
|
||||
case 'roomSetCustomStyle':
|
||||
this.customStyleElement.innerText = data.css
|
||||
this.customStyleElement.textContent = data.css
|
||||
break
|
||||
case 'roomStartClient':
|
||||
if (this.chatClient) {
|
||||
@@ -262,11 +381,12 @@ export default {
|
||||
if (!this.config.showDanmaku || !this.filterTextMessage(data)) {
|
||||
return
|
||||
}
|
||||
let richContent = await this.getRichContent(data)
|
||||
let contentParts = await this.parseContentParts(data)
|
||||
// 合并要放在异步调用后面,因为异步调用后可能有新的消息,会漏合并
|
||||
if (this.mergeSimilarText(data.content)) {
|
||||
return
|
||||
}
|
||||
/** @type {typeof blcsdk.TextMsg} */
|
||||
let message = {
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
@@ -275,12 +395,16 @@ export default {
|
||||
authorName: data.authorName,
|
||||
authorType: data.authorType,
|
||||
content: data.content,
|
||||
richContent: richContent,
|
||||
contentParts: contentParts,
|
||||
privilegeType: data.privilegeType,
|
||||
repeated: 1,
|
||||
translation: data.translation
|
||||
translation: this.config.autoTranslate ? data.translation : '',
|
||||
// 给模板用的字段
|
||||
uid: data.uid,
|
||||
medalLevel: data.medalLevel,
|
||||
medalName: data.medalName,
|
||||
}
|
||||
this.$refs.renderer.addMessage(message)
|
||||
this.renderer.addMessage(message)
|
||||
},
|
||||
/** @param {chatModels.AddGiftMsg} data */
|
||||
onAddGift(data) {
|
||||
@@ -294,6 +418,7 @@ export default {
|
||||
if (price < this.config.minGiftPrice) { // 丢人
|
||||
return
|
||||
}
|
||||
/** @type {typeof blcsdk.GiftMsg} */
|
||||
let message = {
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_GIFT,
|
||||
@@ -302,17 +427,25 @@ export default {
|
||||
authorName: data.authorName,
|
||||
authorNamePronunciation: this.getPronunciation(data.authorName),
|
||||
price: price,
|
||||
// freePrice: data.totalFreeCoin, // 暂时没用到
|
||||
giftName: data.giftName,
|
||||
num: data.num
|
||||
num: data.num,
|
||||
// 给模板用的字段
|
||||
totalFreeCoin: data.totalFreeCoin,
|
||||
giftId: data.giftId,
|
||||
giftIconUrl: data.giftIconUrl,
|
||||
uid: data.uid,
|
||||
privilegeType: data.privilegeType,
|
||||
medalLevel: data.medalLevel,
|
||||
medalName: data.medalName,
|
||||
}
|
||||
this.$refs.renderer.addMessage(message)
|
||||
this.renderer.addMessage(message)
|
||||
},
|
||||
/** @param {chatModels.AddMemberMsg} data */
|
||||
onAddMember(data) {
|
||||
if (!this.config.showGift || !this.filterNewMemberMessage(data)) {
|
||||
return
|
||||
}
|
||||
/** @type {typeof blcsdk.MemberMsg} */
|
||||
let message = {
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
@@ -321,9 +454,16 @@ export default {
|
||||
authorName: data.authorName,
|
||||
authorNamePronunciation: this.getPronunciation(data.authorName),
|
||||
privilegeType: data.privilegeType,
|
||||
title: this.$t('chat.membershipTitle')
|
||||
title: this.$t('chat.membershipTitle'),
|
||||
// 给模板用的字段
|
||||
num: data.num,
|
||||
unit: data.unit,
|
||||
price: data.totalCoin / 1000,
|
||||
uid: data.uid,
|
||||
medalLevel: data.medalLevel,
|
||||
medalName: data.medalName,
|
||||
}
|
||||
this.$refs.renderer.addMessage(message)
|
||||
this.renderer.addMessage(message)
|
||||
},
|
||||
/** @param {chatModels.AddSuperChatMsg} data */
|
||||
onAddSuperChat(data) {
|
||||
@@ -333,6 +473,7 @@ export default {
|
||||
if (data.price < this.config.minGiftPrice) { // 丢人
|
||||
return
|
||||
}
|
||||
/** @type {typeof blcsdk.SuperChatMsg} */
|
||||
let message = {
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
@@ -342,20 +483,25 @@ export default {
|
||||
price: data.price,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
content: data.content.trim(),
|
||||
translation: data.translation
|
||||
translation: this.config.autoTranslate ? data.translation : '',
|
||||
// 给模板用的字段
|
||||
uid: data.uid,
|
||||
privilegeType: data.privilegeType,
|
||||
medalLevel: data.medalLevel,
|
||||
medalName: data.medalName,
|
||||
}
|
||||
this.$refs.renderer.addMessage(message)
|
||||
this.renderer.addMessage(message)
|
||||
},
|
||||
/** @param {chatModels.DelSuperChatMsg} data */
|
||||
onDelSuperChat(data) {
|
||||
this.$refs.renderer.delMessages(data.ids)
|
||||
this.renderer.delMessages(data.ids)
|
||||
},
|
||||
/** @param {chatModels.UpdateTranslationMsg} data */
|
||||
onUpdateTranslation(data) {
|
||||
if (!this.config.autoTranslate) {
|
||||
return
|
||||
}
|
||||
this.$refs.renderer.updateMessage(data.id, { translation: data.translation })
|
||||
this.renderer.updateMessage(data.id, { translation: data.translation })
|
||||
},
|
||||
/** @param {chatModels.ChatClientFatalError} error */
|
||||
onFatalError(error) {
|
||||
@@ -425,13 +571,13 @@ export default {
|
||||
if (!this.config.mergeSimilarDanmaku) {
|
||||
return false
|
||||
}
|
||||
return this.$refs.renderer.mergeSimilarText(content)
|
||||
return this.renderer.mergeSimilarText(content)
|
||||
},
|
||||
mergeSimilarGift(authorName, price, freePrice, giftName, num) {
|
||||
if (!this.config.mergeGift) {
|
||||
return false
|
||||
}
|
||||
return this.$refs.renderer.mergeSimilarGift(authorName, price, freePrice, giftName, num)
|
||||
return this.renderer.mergeSimilarGift(authorName, price, freePrice, giftName, num)
|
||||
},
|
||||
getPronunciation(text) {
|
||||
if (this.pronunciationConverter === null) {
|
||||
@@ -439,29 +585,29 @@ export default {
|
||||
}
|
||||
return this.pronunciationConverter.getPronunciation(text)
|
||||
},
|
||||
async getRichContent(data) {
|
||||
let richContent = []
|
||||
async parseContentParts(data) {
|
||||
let contentParts = []
|
||||
|
||||
// 官方的非文本表情
|
||||
if (data.emoticon !== null) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_IMAGE,
|
||||
contentParts.push({
|
||||
type: constants.CONTENT_PART_TYPE_IMAGE,
|
||||
text: data.content,
|
||||
url: data.emoticon,
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
await this.fillImageContentSizes(richContent)
|
||||
return richContent
|
||||
await this.fillImageContentSizes(contentParts)
|
||||
return contentParts
|
||||
}
|
||||
|
||||
// 没有文本表情,只能是纯文本
|
||||
if (this.config.emoticons.length === 0 && this.textEmoticons.length === 0) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
contentParts.push({
|
||||
type: constants.CONTENT_PART_TYPE_TEXT,
|
||||
text: data.content
|
||||
})
|
||||
return richContent
|
||||
return contentParts
|
||||
}
|
||||
|
||||
// 可能含有文本表情,需要解析
|
||||
@@ -478,15 +624,15 @@ export default {
|
||||
|
||||
// 加入之前的文本
|
||||
if (pos !== startPos) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
contentParts.push({
|
||||
type: constants.CONTENT_PART_TYPE_TEXT,
|
||||
text: data.content.slice(startPos, pos)
|
||||
})
|
||||
}
|
||||
|
||||
// 加入表情
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_IMAGE,
|
||||
contentParts.push({
|
||||
type: constants.CONTENT_PART_TYPE_IMAGE,
|
||||
text: matchEmoticon.keyword,
|
||||
url: matchEmoticon.url,
|
||||
width: 0,
|
||||
@@ -497,19 +643,19 @@ export default {
|
||||
}
|
||||
// 加入尾部的文本
|
||||
if (pos !== startPos) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
contentParts.push({
|
||||
type: constants.CONTENT_PART_TYPE_TEXT,
|
||||
text: data.content.slice(startPos, pos)
|
||||
})
|
||||
}
|
||||
|
||||
await this.fillImageContentSizes(richContent)
|
||||
return richContent
|
||||
await this.fillImageContentSizes(contentParts)
|
||||
return contentParts
|
||||
},
|
||||
async fillImageContentSizes(richContent) {
|
||||
async fillImageContentSizes(contentParts) {
|
||||
let urlSizeMap = new Map()
|
||||
for (let content of richContent) {
|
||||
if (content.type === constants.CONTENT_TYPE_IMAGE) {
|
||||
for (let content of contentParts) {
|
||||
if (content.type === constants.CONTENT_PART_TYPE_IMAGE) {
|
||||
urlSizeMap.set(content.url, { width: 0, height: 0 })
|
||||
}
|
||||
}
|
||||
@@ -539,8 +685,8 @@ export default {
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
for (let content of richContent) {
|
||||
if (content.type === constants.CONTENT_TYPE_IMAGE) {
|
||||
for (let content of contentParts) {
|
||||
if (content.type === constants.CONTENT_PART_TYPE_IMAGE) {
|
||||
let size = urlSizeMap.get(content.url)
|
||||
content.width = size.width
|
||||
content.height = size.height
|
||||
@@ -550,3 +696,16 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.template-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,360 +1,378 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<el-form label-width="150px" size="mini">
|
||||
<h3>{{ $t('stylegen.outlines') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showOutlines')">
|
||||
<el-switch v-model="form.showOutlines"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.outlineColor')">
|
||||
<el-color-picker v-model="form.outlineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('stylegen.outlineSize')">
|
||||
<el-input v-model.number="form.outlineSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-collapse>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.outlines') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showOutlines')">
|
||||
<el-switch v-model="form.showOutlines"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.outlineColor')">
|
||||
<el-color-picker v-model="form.outlineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('stylegen.outlineSize')">
|
||||
<el-input v-model.number="form.outlineSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.avatars') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showAvatars')">
|
||||
<el-switch v-model="form.showAvatars"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.avatarSize')">
|
||||
<el-input v-model.number="form.avatarSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.avatars') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showAvatars')">
|
||||
<el-switch v-model="form.showAvatars"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.avatarSize')">
|
||||
<el-input v-model.number="form.avatarSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.userNames') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showUserNames')">
|
||||
<el-switch v-model="form.showUserNames"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.userNameFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.userNameFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.userNameLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.normalColor')">
|
||||
<el-color-picker v-model="form.userNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.memberColor')">
|
||||
<el-color-picker v-model="form.memberUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.moderatorColor')">
|
||||
<el-color-picker v-model="form.moderatorUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.ownerColor')">
|
||||
<el-color-picker v-model="form.ownerUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showBadges')">
|
||||
<el-switch v-model="form.showBadges"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showColon')">
|
||||
<el-switch v-model="form.showColon"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.userNames') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showUserNames')">
|
||||
<el-switch v-model="form.showUserNames"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.userNameFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.userNameFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.userNameLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.normalColor')">
|
||||
<el-color-picker v-model="form.userNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.memberColor')">
|
||||
<el-color-picker v-model="form.memberUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.moderatorColor')">
|
||||
<el-color-picker v-model="form.moderatorUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.ownerColor')">
|
||||
<el-color-picker v-model="form.ownerUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showBadges')">
|
||||
<el-switch v-model="form.showBadges"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showColon')">
|
||||
<el-switch v-model="form.showColon"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.messages') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.messageFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.messageColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.messageFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.messageLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('stylegen.onNewLine')">
|
||||
<el-switch v-model="form.messageOnNewLine"></el-switch>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.emoticonSize')">
|
||||
<el-input v-model.number="form.emoticonSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.largeEmoticonSize')">
|
||||
<el-input v-model.number="form.largeEmoticonSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.messages') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.messageFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.messageColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.messageFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.messageLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('stylegen.onNewLine')">
|
||||
<el-switch v-model="form.messageOnNewLine"></el-switch>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.emoticonSize')">
|
||||
<el-input v-model.number="form.emoticonSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.largeEmoticonSize')">
|
||||
<el-input v-model.number="form.largeEmoticonSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.time') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-form-item :label="$t('stylegen.showTime')">
|
||||
<el-switch v-model="form.showTime"></el-switch>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.timeFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.timeColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.timeFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.timeLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.time') }}</h3>
|
||||
</template>
|
||||
<el-form-item :label="$t('stylegen.showTime')">
|
||||
<el-switch v-model="form.showTime"></el-switch>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.timeFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.timeColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.timeFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.timeLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.backgrounds') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.bgColor')">
|
||||
<el-color-picker v-model="form.bgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.useBarsInsteadOfBg')">
|
||||
<el-switch v-model="form.useBarsInsteadOfBg"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.messageBgColor')">
|
||||
<el-color-picker v-model="form.messageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.memberMessageBgColor')">
|
||||
<el-color-picker v-model="form.memberMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.moderatorMessageBgColor')">
|
||||
<el-color-picker v-model="form.moderatorMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.ownerMessageBgColor')">
|
||||
<el-color-picker v-model="form.ownerMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.backgrounds') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.bgColor')">
|
||||
<el-color-picker v-model="form.bgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.useBarsInsteadOfBg')">
|
||||
<el-switch v-model="form.useBarsInsteadOfBg"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.messageBgColor')">
|
||||
<el-color-picker v-model="form.messageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.memberMessageBgColor')">
|
||||
<el-color-picker v-model="form.memberMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.moderatorMessageBgColor')">
|
||||
<el-color-picker v-model="form.moderatorMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.ownerMessageBgColor')">
|
||||
<el-color-picker v-model="form.ownerMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.scAndNewMember') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineFont')">
|
||||
<font-select v-model="form.firstLineFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineColor')">
|
||||
<el-color-picker v-model="form.firstLineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineFontSize')">
|
||||
<el-input v-model.number="form.firstLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineLineHeight')">
|
||||
<el-input v-model.number="form.firstLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.scAndNewMember') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineFont')">
|
||||
<font-select v-model="form.firstLineFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineColor')">
|
||||
<el-color-picker v-model="form.firstLineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineFontSize')">
|
||||
<el-input v-model.number="form.firstLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineLineHeight')">
|
||||
<el-input v-model.number="form.firstLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineFont')">
|
||||
<font-select v-model="form.secondLineFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineColor')">
|
||||
<el-color-picker v-model="form.secondLineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineFontSize')">
|
||||
<el-input v-model.number="form.secondLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineLineHeight')">
|
||||
<el-input v-model.number="form.secondLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineFont')">
|
||||
<font-select v-model="form.secondLineFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineColor')">
|
||||
<el-color-picker v-model="form.secondLineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineFontSize')">
|
||||
<el-input v-model.number="form.secondLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineLineHeight')">
|
||||
<el-input v-model.number="form.secondLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineFont')">
|
||||
<font-select v-model="form.scContentFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineColor')">
|
||||
<el-color-picker v-model="form.scContentColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineFontSize')">
|
||||
<el-input v-model.number="form.scContentFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineLineHeight')">
|
||||
<el-input v-model.number="form.scContentLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineFont')">
|
||||
<font-select v-model="form.scContentFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineColor')">
|
||||
<el-color-picker v-model="form.scContentColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineFontSize')">
|
||||
<el-input v-model.number="form.scContentFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineLineHeight')">
|
||||
<el-input v-model.number="form.scContentLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-form-item :label="$t('stylegen.showNewMemberBg')">
|
||||
<el-switch v-model="form.showNewMemberBg"></el-switch>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showScTicker')">
|
||||
<el-switch v-model="form.showScTicker"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showOtherThings')">
|
||||
<el-switch v-model="form.showOtherThings"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-form-item :label="$t('stylegen.showNewMemberBg')">
|
||||
<el-switch v-model="form.showNewMemberBg"></el-switch>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showScTicker')">
|
||||
<el-switch v-model="form.showScTicker"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showOtherThings')">
|
||||
<el-switch v-model="form.showOtherThings"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.animation') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.animateIn')">
|
||||
<el-switch v-model="form.animateIn"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fadeInTime')">
|
||||
<el-input v-model.number="form.fadeInTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.animateOut')">
|
||||
<el-switch v-model="form.animateOut"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fadeOutTime')">
|
||||
<el-input v-model.number="form.fadeOutTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('stylegen.animateOutWaitTime')">
|
||||
<el-input v-model.number="form.animateOutWaitTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.slide')">
|
||||
<el-switch v-model="form.slide"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.reverseSlide')">
|
||||
<el-switch v-model="form.reverseSlide"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.animation') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.animateIn')">
|
||||
<el-switch v-model="form.animateIn"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fadeInTime')">
|
||||
<el-input v-model.number="form.fadeInTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.animateOut')">
|
||||
<el-switch v-model="form.animateOut"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fadeOutTime')">
|
||||
<el-input v-model.number="form.fadeOutTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('stylegen.animateOutWaitTime')">
|
||||
<el-input v-model.number="form.animateOutWaitTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.slide')">
|
||||
<el-switch v-model="form.slide"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.reverseSlide')">
|
||||
<el-switch v-model="form.reverseSlide"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,315 +1,331 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<el-form label-width="150px" size="mini">
|
||||
<h3>{{ $t('stylegen.avatars') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showAvatars')">
|
||||
<el-switch v-model="form.showAvatars"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.avatarSize')">
|
||||
<el-input v-model.number="form.avatarSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.avatars') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showAvatars')">
|
||||
<el-switch v-model="form.showAvatars"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.avatarSize')">
|
||||
<el-input v-model.number="form.avatarSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.userNames') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showUserNames')">
|
||||
<el-switch v-model="form.showUserNames"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.userNameFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.userNameFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.userNameLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.normalColor')">
|
||||
<el-color-picker v-model="form.userNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.memberColor')">
|
||||
<el-color-picker v-model="form.memberUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.moderatorColor')">
|
||||
<el-color-picker v-model="form.moderatorUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.ownerColor')">
|
||||
<el-color-picker v-model="form.ownerUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showBadges')">
|
||||
<el-switch v-model="form.showBadges"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.userNames') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showUserNames')">
|
||||
<el-switch v-model="form.showUserNames"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.userNameFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.userNameFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.userNameLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.normalColor')">
|
||||
<el-color-picker v-model="form.userNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.memberColor')">
|
||||
<el-color-picker v-model="form.memberUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.moderatorColor')">
|
||||
<el-color-picker v-model="form.moderatorUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.ownerColor')">
|
||||
<el-color-picker v-model="form.ownerUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showBadges')">
|
||||
<el-switch v-model="form.showBadges"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.messages') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.messageFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.messageColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.messageFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.messageLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.emoticonSize')">
|
||||
<el-input v-model.number="form.emoticonSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.largeEmoticonSize')">
|
||||
<el-input v-model.number="form.largeEmoticonSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.messages') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.messageFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.messageColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.messageFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.messageLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.emoticonSize')">
|
||||
<el-input v-model.number="form.emoticonSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.largeEmoticonSize')">
|
||||
<el-input v-model.number="form.largeEmoticonSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.time') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-form-item :label="$t('stylegen.showTime')">
|
||||
<el-switch v-model="form.showTime"></el-switch>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.timeFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.timeColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.timeFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.timeLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.time') }}</h3>
|
||||
</template>
|
||||
<el-form-item :label="$t('stylegen.showTime')">
|
||||
<el-switch v-model="form.showTime"></el-switch>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<font-select v-model="form.timeFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.timeColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.timeFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.timeLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.backgrounds') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.bgColor')">
|
||||
<el-color-picker v-model="form.bgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showLargeEmoticonBg')">
|
||||
<el-switch v-model="form.showLargeEmoticonBg"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.messageBgColor')">
|
||||
<el-color-picker v-model="form.messageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.memberMessageBgColor')">
|
||||
<el-color-picker v-model="form.memberMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.moderatorMessageBgColor')">
|
||||
<el-color-picker v-model="form.moderatorMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.ownerMessageBgColor')">
|
||||
<el-color-picker v-model="form.ownerMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.backgrounds') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.bgColor')">
|
||||
<el-color-picker v-model="form.bgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showLargeEmoticonBg')">
|
||||
<el-switch v-model="form.showLargeEmoticonBg"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.messageBgColor')">
|
||||
<el-color-picker v-model="form.messageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.memberMessageBgColor')">
|
||||
<el-color-picker v-model="form.memberMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.moderatorMessageBgColor')">
|
||||
<el-color-picker v-model="form.moderatorMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.ownerMessageBgColor')">
|
||||
<el-color-picker v-model="form.ownerMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.scAndNewMember') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineFont')">
|
||||
<font-select v-model="form.firstLineFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineFontSize')">
|
||||
<el-input v-model.number="form.firstLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineLineHeight')">
|
||||
<el-input v-model.number="form.firstLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.scAndNewMember') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineFont')">
|
||||
<font-select v-model="form.firstLineFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineFontSize')">
|
||||
<el-input v-model.number="form.firstLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.firstLineLineHeight')">
|
||||
<el-input v-model.number="form.firstLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineFont')">
|
||||
<font-select v-model="form.secondLineFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineFontSize')">
|
||||
<el-input v-model.number="form.secondLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineLineHeight')">
|
||||
<el-input v-model.number="form.secondLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineFont')">
|
||||
<font-select v-model="form.secondLineFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineFontSize')">
|
||||
<el-input v-model.number="form.secondLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.secondLineLineHeight')">
|
||||
<el-input v-model.number="form.secondLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineFont')">
|
||||
<font-select v-model="form.scContentFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineFontSize')">
|
||||
<el-input v-model.number="form.scContentFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineLineHeight')">
|
||||
<el-input v-model.number="form.scContentLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineFont')">
|
||||
<font-select v-model="form.scContentFont"></font-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineFontSize')">
|
||||
<el-input v-model.number="form.scContentFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.scContentLineLineHeight')">
|
||||
<el-input v-model.number="form.scContentLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showScTicker')">
|
||||
<el-switch v-model="form.showScTicker"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showOtherThings')">
|
||||
<el-switch v-model="form.showOtherThings"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showScTicker')">
|
||||
<el-switch v-model="form.showScTicker"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.showOtherThings')">
|
||||
<el-switch v-model="form.showOtherThings"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
|
||||
<h3>{{ $t('stylegen.animation') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.animateIn')">
|
||||
<el-switch v-model="form.animateIn"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fadeInTime')">
|
||||
<el-input v-model.number="form.fadeInTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.animateOut')">
|
||||
<el-switch v-model="form.animateOut"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fadeOutTime')">
|
||||
<el-input v-model.number="form.fadeOutTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('stylegen.animateOutWaitTime')">
|
||||
<el-input v-model.number="form.animateOutWaitTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.slide')">
|
||||
<el-switch v-model="form.slide"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.reverseSlide')">
|
||||
<el-switch v-model="form.reverseSlide"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<el-collapse-item>
|
||||
<template slot="title">
|
||||
<h3>{{ $t('stylegen.animation') }}</h3>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.animateIn')">
|
||||
<el-switch v-model="form.animateIn"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fadeInTime')">
|
||||
<el-input v-model.number="form.fadeInTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.animateOut')">
|
||||
<el-switch v-model="form.animateOut"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.fadeOutTime')">
|
||||
<el-input v-model.number="form.fadeOutTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('stylegen.animateOutWaitTime')">
|
||||
<el-input v-model.number="form.animateOutWaitTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.slide')">
|
||||
<el-switch v-model="form.slide"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('stylegen.reverseSlide')">
|
||||
<el-switch v-model="form.reverseSlide"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane :label="$t('stylegen.legacy')" name="legacy">
|
||||
<legacy ref="legacy" v-model="subComponentResults.legacy"></legacy>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('stylegen.lineLike')" name="lineLike">
|
||||
<line-like ref="lineLike" v-model="subComponentResults.lineLike"></line-like>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('stylegen.legacy')" name="legacy">
|
||||
<legacy ref="legacy" v-model="subComponentResults.legacy"></legacy>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-form label-width="150px" size="mini">
|
||||
@@ -64,14 +64,14 @@ export default {
|
||||
legacy: '',
|
||||
lineLike: ''
|
||||
},
|
||||
activeTab: 'legacy',
|
||||
activeTab: 'lineLike',
|
||||
// 输入框的结果
|
||||
inputResult: '',
|
||||
// 防抖后延迟变化的结果
|
||||
debounceResult: '',
|
||||
|
||||
playAnimation: true,
|
||||
exampleBgLight: false
|
||||
exampleBgLight: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
cachetools==5.3.1
|
||||
circuitbreaker==2.0.0
|
||||
pycryptodome==3.19.1
|
||||
sqlalchemy==2.0.19
|
||||
tornado==6.4.1
|
||||
sqlalchemy==2.0.37
|
||||
tornado==6.4.2
|
||||
|
||||
@@ -193,7 +193,6 @@ class WebLiveClient(blivedm.BLiveClient):
|
||||
assert room_key.type == RoomKeyType.ROOM_ID
|
||||
super().__init__(
|
||||
room_key.value,
|
||||
uid=0,
|
||||
session=utils.request.http_session,
|
||||
heartbeat_interval=self.HEARTBEAT_INTERVAL,
|
||||
)
|
||||
@@ -508,7 +507,9 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
|
||||
async def __on_danmaku(self, client: WebLiveClient, message: dm_web_models.DanmakuMessage):
|
||||
# 先异步调用再获取房间,因为返回时房间可能已经不存在了
|
||||
avatar_url = await services.avatar.get_avatar_url(message.uid, message.uname)
|
||||
avatar_url = message.face
|
||||
if avatar_url == '':
|
||||
avatar_url = await services.avatar.get_avatar_url(message.uid, message.uname)
|
||||
|
||||
room = client_room_manager.get_room(client.room_key)
|
||||
if room is None:
|
||||
@@ -523,6 +524,11 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
else:
|
||||
author_type = 0
|
||||
|
||||
show_content = message.msg
|
||||
reply_uname = message.extra_dict.get('reply_uname', '')
|
||||
if reply_uname != '':
|
||||
show_content = f'@{reply_uname} {show_content}'
|
||||
|
||||
if message.dm_type == 1:
|
||||
content_type = api.chat.ContentType.EMOTICON
|
||||
content_type_params = api.chat.make_emoticon_params(
|
||||
@@ -551,7 +557,7 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
timestamp=int(message.timestamp / 1000),
|
||||
author_name=message.uname,
|
||||
author_type=author_type,
|
||||
content=message.msg,
|
||||
content=show_content,
|
||||
privilege_type=message.privilege_type,
|
||||
is_gift_danmaku=bool(message.msg_type),
|
||||
author_level=message.user_level,
|
||||
@@ -594,22 +600,22 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
'num': message.num,
|
||||
# 给插件用的字段
|
||||
'giftId': message.gift_id,
|
||||
'giftIconUrl': '',
|
||||
'giftIconUrl': message.gift_img_basic,
|
||||
'uid': str(message.uid) if message.uid != 0 else message.uname,
|
||||
'privilegeType': message.guard_level,
|
||||
'medalLevel': 0,
|
||||
'medalName': '',
|
||||
'medalLevel': 0 if message.medal_ruid != client.room_owner_uid else message.medal_level,
|
||||
'medalName': '' if message.medal_ruid != client.room_owner_uid else message.medal_name,
|
||||
}
|
||||
room.send_cmd_data(api.chat.Command.ADD_GIFT, data)
|
||||
services.plugin.broadcast_cmd_data(
|
||||
sdk_models.Command.ADD_GIFT, data, make_plugin_msg_extra_from_live_client(client)
|
||||
)
|
||||
|
||||
def _on_buy_guard(self, client: WebLiveClient, message: dm_web_models.GuardBuyMessage):
|
||||
def _on_user_toast_v2(self, client: WebLiveClient, message: dm_web_models.UserToastV2Message):
|
||||
utils.async_io.create_task_with_ref(self.__on_buy_guard(client, message))
|
||||
|
||||
@staticmethod
|
||||
async def __on_buy_guard(client: WebLiveClient, message: dm_web_models.GuardBuyMessage):
|
||||
async def __on_buy_guard(client: WebLiveClient, message: dm_web_models.UserToastV2Message):
|
||||
# 先异步调用再获取房间,因为返回时房间可能已经不存在了
|
||||
avatar_url = await services.avatar.get_avatar_url(message.uid, message.username)
|
||||
|
||||
@@ -625,7 +631,7 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
'privilegeType': message.guard_level,
|
||||
# 给插件用的字段
|
||||
'num': message.num,
|
||||
'unit': '月', # 单位在USER_TOAST_MSG消息里,不想改消息。现在没有别的单位,web接口也很少有人用了,先写死吧
|
||||
'unit': message.unit,
|
||||
'total_coin': message.price * message.num,
|
||||
'uid': str(message.uid) if message.uid != 0 else message.username,
|
||||
'medalLevel': 0,
|
||||
@@ -667,8 +673,8 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
# 给插件用的字段
|
||||
'uid': str(message.uid) if message.uid != 0 else message.uname,
|
||||
'privilegeType': message.guard_level,
|
||||
'medalLevel': 0,
|
||||
'medalName': '',
|
||||
'medalLevel': 0 if message.medal_room_id != client.room_id else message.medal_level,
|
||||
'medalName': '' if message.medal_room_id != client.room_id else message.medal_name,
|
||||
}
|
||||
room.send_cmd_data(api.chat.Command.ADD_SUPER_CHAT, data)
|
||||
services.plugin.broadcast_cmd_data(
|
||||
@@ -735,11 +741,17 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
|
||||
if message.open_id == client.room_owner_open_id:
|
||||
author_type = 3 # 主播
|
||||
elif message.is_admin:
|
||||
author_type = 2 # 房管
|
||||
elif message.guard_level != 0: # 1总督,2提督,3舰长
|
||||
author_type = 1 # 舰队
|
||||
else:
|
||||
author_type = 0
|
||||
|
||||
show_content = message.msg
|
||||
if message.reply_uname != '':
|
||||
show_content = f'@{message.reply_uname} {show_content}'
|
||||
|
||||
if message.dm_type == 1:
|
||||
content_type = api.chat.ContentType.EMOTICON
|
||||
content_type_params = api.chat.make_emoticon_params(message.emoji_img_url)
|
||||
@@ -765,7 +777,7 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
timestamp=message.timestamp,
|
||||
author_name=message.uname,
|
||||
author_type=author_type,
|
||||
content=message.msg,
|
||||
content=show_content,
|
||||
privilege_type=message.guard_level,
|
||||
medal_level=0 if not message.fans_medal_wearing_status else message.fans_medal_level,
|
||||
id_=message.msg_id,
|
||||
@@ -791,7 +803,7 @@ class LiveMsgHandler(blivedm.BaseHandler):
|
||||
if room is None:
|
||||
return
|
||||
|
||||
total_coin = message.price * message.gift_num
|
||||
total_coin = message.r_price * message.gift_num
|
||||
data = {
|
||||
'id': message.msg_id,
|
||||
'avatarUrl': services.avatar.process_avatar_url(message.uface),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import copy
|
||||
import dataclasses
|
||||
import datetime
|
||||
import enum
|
||||
@@ -15,6 +16,7 @@ import Crypto.Cipher.AES as cry_aes # noqa
|
||||
import Crypto.Util.Padding as cry_pad # noqa
|
||||
import aiohttp
|
||||
import cachetools
|
||||
import circuitbreaker
|
||||
|
||||
import config
|
||||
import utils.async_io
|
||||
@@ -79,10 +81,10 @@ def create_translate_provider(cfg):
|
||||
cfg['query_interval'], cfg['source_language'], cfg['target_language'],
|
||||
cfg['app_id'], cfg['secret']
|
||||
)
|
||||
elif type_ == 'GeminiTranslate':
|
||||
return GeminiTranslate(
|
||||
cfg['query_interval'], cfg['proxy'], cfg['api_key'], cfg['model_code'],
|
||||
cfg['prompt'], cfg['temperature']
|
||||
elif type_ == 'OpenAiApi':
|
||||
return OpenAiApi(
|
||||
cfg['query_interval'], cfg['api_key'], cfg['base_url'], cfg['proxy'], cfg['model'],
|
||||
cfg['prompt'], cfg['max_tokens'], cfg['temperature'], cfg['top_p']
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -255,8 +257,10 @@ class TranslateProvider:
|
||||
logger.info('%s became available', cls_name)
|
||||
|
||||
task = await _pop_task()
|
||||
# 为了简化代码,约定只会在_translate_wrapper里变成不可用,所以获取task之后这里还是可用的
|
||||
assert self.is_available
|
||||
if not self.is_available:
|
||||
if not _push_task(task):
|
||||
task.future.set_result(None)
|
||||
continue
|
||||
|
||||
start_time = datetime.datetime.now()
|
||||
await self._translate_wrapper(task)
|
||||
@@ -267,7 +271,7 @@ class TranslateProvider:
|
||||
except Exception: # noqa
|
||||
logger.exception('%s error:', cls_name)
|
||||
|
||||
async def _translate_wrapper(self, task: TranslateTask) -> Optional[str]:
|
||||
async def _translate_wrapper(self, task: TranslateTask):
|
||||
try:
|
||||
exc = None
|
||||
task.remain_retry_count -= 1
|
||||
@@ -277,7 +281,7 @@ class TranslateProvider:
|
||||
res = None
|
||||
if res is not None:
|
||||
task.future.set_result(res)
|
||||
return res
|
||||
return
|
||||
|
||||
if task.remain_retry_count > 0:
|
||||
# 还可以重试则放回队列
|
||||
@@ -289,7 +293,6 @@ class TranslateProvider:
|
||||
task.future.set_exception(exc)
|
||||
else:
|
||||
task.future.set_result(None)
|
||||
return None
|
||||
|
||||
async def _do_translate(self, text) -> Optional[str]:
|
||||
raise NotImplementedError
|
||||
@@ -345,8 +348,9 @@ class TencentTranslate(TranslateProvider):
|
||||
hashed_request_payload = hashlib.sha256(body_bytes).hexdigest()
|
||||
canonical_request = f'POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_request_payload}'
|
||||
|
||||
request_timestamp = int(datetime.datetime.now().timestamp())
|
||||
date = datetime.datetime.utcfromtimestamp(request_timestamp).strftime('%Y-%m-%d')
|
||||
cur_time_utc = datetime.datetime.now(datetime.UTC)
|
||||
request_timestamp = int(cur_time_utc.timestamp())
|
||||
date = cur_time_utc.strftime('%Y-%m-%d')
|
||||
credential_scope = f'{date}/tmt/tc3_request'
|
||||
hashed_canonical_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
|
||||
string_to_sign = f'TC3-HMAC-SHA256\n{request_timestamp}\n{credential_scope}\n{hashed_canonical_request}'
|
||||
@@ -470,96 +474,96 @@ class BaiduTranslate(TranslateProvider):
|
||||
self._on_availability_change()
|
||||
|
||||
|
||||
class GeminiTranslate(TranslateProvider):
|
||||
def __init__(self, query_interval, proxy, api_key, model_code, prompt, temperature):
|
||||
class _OpenAiApiExpectedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OpenAiApi(TranslateProvider):
|
||||
def __init__(self, query_interval, api_key, base_url, proxy, model, prompt, max_tokens, temperature, top_p):
|
||||
super().__init__(query_interval)
|
||||
self._url = base_url + '/chat/completions'
|
||||
self._proxy = proxy or None
|
||||
self._api_key = api_key
|
||||
self._url = f'https://generativelanguage.googleapis.com/v1beta/{model_code}:generateContent'
|
||||
self._prompt = prompt
|
||||
self._headers = {
|
||||
'Authorization': 'Bearer ' + api_key
|
||||
}
|
||||
self._body = {
|
||||
'contents': [
|
||||
{
|
||||
# 'role': 'user',
|
||||
'parts': [{'text': ''}]
|
||||
}
|
||||
'model': model,
|
||||
'messages': [
|
||||
{'role': 'system', 'content': prompt},
|
||||
{'role': 'user', 'content': ''},
|
||||
],
|
||||
'safetySettings': [
|
||||
{
|
||||
'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
||||
'threshold': 'BLOCK_NONE'
|
||||
},
|
||||
{
|
||||
'category': 'HARM_CATEGORY_HATE_SPEECH',
|
||||
'threshold': 'BLOCK_NONE'
|
||||
},
|
||||
{
|
||||
'category': 'HARM_CATEGORY_HARASSMENT',
|
||||
'threshold': 'BLOCK_NONE'
|
||||
},
|
||||
{
|
||||
'category': 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
||||
'threshold': 'BLOCK_NONE'
|
||||
}
|
||||
],
|
||||
'generationConfig': {
|
||||
'temperature': temperature,
|
||||
'topP': 1,
|
||||
'topK': 32,
|
||||
'candidateCount': 1,
|
||||
'maxOutputTokens': 8192
|
||||
}
|
||||
'stream': False,
|
||||
'max_tokens': max_tokens,
|
||||
'temperature': temperature,
|
||||
'top_p': top_p,
|
||||
}
|
||||
|
||||
self._breaker = circuitbreaker.CircuitBreaker(
|
||||
failure_threshold=5,
|
||||
recovery_timeout=60,
|
||||
)
|
||||
self._cool_down_timer_handle = None
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
return self._cool_down_timer_handle is None and super().is_available
|
||||
return self._breaker.state != circuitbreaker.STATE_OPEN and super().is_available
|
||||
|
||||
async def _translate_wrapper(self, task: TranslateTask):
|
||||
# 并发请求,不然太慢了
|
||||
utils.async_io.create_task_with_ref(super()._translate_wrapper(task))
|
||||
|
||||
async def _do_translate(self, text) -> Optional[str]:
|
||||
input_text = self._prompt.format(original_text=text)
|
||||
self._body['contents'][0]['parts'][0]['text'] = input_text
|
||||
data = None
|
||||
try:
|
||||
async with utils.request.http_session.post(
|
||||
self._url,
|
||||
params={'key': self._api_key},
|
||||
json=self._body,
|
||||
proxy=self._proxy,
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning('GeminiTranslate request failed: status=%d %s', r.status, r.reason)
|
||||
self._on_fail(r.status)
|
||||
return None
|
||||
data = await r.json()
|
||||
with self._breaker:
|
||||
body = copy.deepcopy(self._body)
|
||||
body['messages'][-1]['content'] = text
|
||||
async with utils.request.http_session.post(
|
||||
self._url,
|
||||
headers=self._headers,
|
||||
json=body,
|
||||
proxy=self._proxy,
|
||||
timeout=aiohttp.ClientTimeout(total=30),
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
rsp_body = await r.text()
|
||||
logger.warning(
|
||||
'OpenAiApi request failed: status=%d %s, body=%s', r.status, r.reason, rsp_body
|
||||
)
|
||||
raise _OpenAiApiExpectedError('OpenAiApi request failed')
|
||||
data = await r.json()
|
||||
return data['choices'][0]['message']['content']
|
||||
|
||||
except (_OpenAiApiExpectedError, circuitbreaker.CircuitBreakerError):
|
||||
pass
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as e:
|
||||
logger.warning('GeminiTranslate request failed, %s: %s', type(e).__name__, e)
|
||||
return None
|
||||
|
||||
try:
|
||||
candidates = data['candidates']
|
||||
if not candidates:
|
||||
block_reason = data['promptFeedback'].get('blockReason', '')
|
||||
logger.warning('GeminiTranslate no candidate, block_reason=%s, text=%s', block_reason, text)
|
||||
return None
|
||||
|
||||
first_content_parts = candidates[0]['content']['parts']
|
||||
return ''.join(part.get('text', '') for part in first_content_parts)
|
||||
logger.warning('OpenAiApi request failed, %s: %s', type(e).__name__, e)
|
||||
except (KeyError, IndexError):
|
||||
logger.warning('GeminiTranslate failed to parse response: %s', data)
|
||||
return None
|
||||
if data is not None:
|
||||
logger.warning('OpenAiApi failed to parse response: %s', data)
|
||||
else:
|
||||
logger.exception('OpenAiApi unknown exception:')
|
||||
|
||||
def _on_fail(self, code):
|
||||
if self._breaker.state == circuitbreaker.STATE_OPEN:
|
||||
self._on_breaker_open()
|
||||
|
||||
return None
|
||||
|
||||
def _on_breaker_open(self):
|
||||
if self._cool_down_timer_handle is not None:
|
||||
return
|
||||
|
||||
if code in (401, 403):
|
||||
# API密钥无效,没有权限,或者不在有效地区。需要手动处理,等5分钟
|
||||
self._cool_down_timer_handle = asyncio.get_running_loop().call_later(
|
||||
5 * 60, self._on_cool_down_timeout
|
||||
)
|
||||
self._on_availability_change()
|
||||
self._cool_down_timer_handle = asyncio.get_running_loop().call_later(
|
||||
self._breaker.open_remaining, self._on_cool_down_timeout
|
||||
)
|
||||
self._on_availability_change()
|
||||
|
||||
def _on_cool_down_timeout(self):
|
||||
self._cool_down_timer_handle = None
|
||||
if self._breaker.state == circuitbreaker.STATE_OPEN:
|
||||
self._cool_down_timer_handle = asyncio.get_running_loop().call_later(
|
||||
self._breaker.open_remaining, self._on_cool_down_timeout
|
||||
)
|
||||
return
|
||||
|
||||
self._on_availability_change()
|
||||
|
||||
@@ -6,7 +6,7 @@ import aiohttp
|
||||
import utils.async_io
|
||||
import utils.request
|
||||
|
||||
VERSION = 'v1.9.3'
|
||||
VERSION = 'v1.10.0'
|
||||
|
||||
|
||||
def check_update():
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
from typing import *
|
||||
|
||||
import aiohttp
|
||||
@@ -13,6 +15,8 @@ import utils.async_io
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
COOKIE_JAR_PATH = os.path.join(config.DATA_PATH, 'cookie_jar.pickle')
|
||||
|
||||
# 不带这堆头部有时候也能成功请求,但是带上后成功的概率更高
|
||||
BILIBILI_COMMON_HEADERS = {
|
||||
'Origin': 'https://www.bilibili.com',
|
||||
@@ -37,10 +41,17 @@ _common_server_base_url_to_circuit_breaker: Dict[str, circuitbreaker.CircuitBrea
|
||||
|
||||
|
||||
def init():
|
||||
try:
|
||||
cookie_jar = aiohttp.CookieJar()
|
||||
cookie_jar.load(COOKIE_JAR_PATH)
|
||||
except (OSError, pickle.PickleError):
|
||||
cookie_jar = None
|
||||
|
||||
global http_session
|
||||
http_session = aiohttp.ClientSession(
|
||||
response_class=CustomClientResponse,
|
||||
timeout=aiohttp.ClientTimeout(total=10),
|
||||
cookie_jar=cookie_jar,
|
||||
)
|
||||
|
||||
cfg = config.get_config()
|
||||
|
||||
Reference in New Issue
Block a user