Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5672c3e9c | ||
|
|
b57bac3765 | ||
|
|
7c2c48273c | ||
|
|
5bdf2e41df | ||
|
|
3d9095536d | ||
|
|
c3db0a41b3 | ||
|
|
60b9f81fe2 | ||
|
|
00c47a3e55 | ||
|
|
93e374ac12 | ||
|
|
dac531920e | ||
|
|
c3566823c1 | ||
|
|
17b4985632 | ||
|
|
4314bb61e9 | ||
|
|
bea8711353 | ||
|
|
8fa63f1d75 | ||
|
|
8e7d9b266b | ||
|
|
aea6f12bd2 | ||
|
|
ecb4a08e56 | ||
|
|
6d5232d7d8 | ||
|
|
63f541b87e | ||
|
|
e478959a1b | ||
|
|
57ec8495e1 | ||
|
|
d5e8054e12 | ||
|
|
13b3e54f2b | ||
|
|
f8520909f4 | ||
|
|
92cead6ff1 | ||
|
|
b483c6beab | ||
|
|
f748d35fe4 | ||
|
|
b98a8f6680 | ||
|
|
914a41700e | ||
|
|
085765b1a2 | ||
|
|
14c2fd48df | ||
|
|
8683c0bad7 | ||
|
|
a1189aea69 | ||
|
|
9ce1dd21aa | ||
|
|
1f89d0d1cf | ||
|
|
062e7ed1aa | ||
|
|
6c84cbb930 | ||
|
|
582508bd3f | ||
|
|
2c0dd6e1c9 | ||
|
|
1075b458aa | ||
|
|
aac9f62a70 | ||
|
|
86847c666f | ||
|
|
b2d12e84bd | ||
|
|
618164d77d | ||
|
|
9f8f238426 | ||
|
|
801280efbd | ||
|
|
2187dfc167 | ||
|
|
02ec4bc4ba | ||
|
|
24d0671dae | ||
|
|
903cfecab9 | ||
|
|
3da8cc4227 | ||
|
|
4924f15614 | ||
|
|
3a183a865d | ||
|
|
4c8cde0b3d | ||
|
|
a68b50e885 | ||
|
|
b5b9b2601a | ||
|
|
6c92859408 | ||
|
|
c87325e5e0 | ||
|
|
e40f1511ed | ||
|
|
3b556fc265 | ||
|
|
c05c70863a | ||
|
|
781de48b4b | ||
|
|
ccf9ceec0e | ||
|
|
707ad57800 | ||
|
|
8eff63e17b | ||
|
|
36345c37ab | ||
|
|
03a2801099 | ||
|
|
e318bdfcac | ||
|
|
23ebaca372 | ||
|
|
6da20d9bdd | ||
|
|
dedae6f083 | ||
|
|
3118a7de98 | ||
|
|
34f8d88e05 | ||
|
|
776b354517 | ||
|
|
2a22617c1a | ||
|
|
6b77a8a17f | ||
|
|
8780655341 | ||
|
|
6a4faa83a4 | ||
|
|
335248c076 | ||
|
|
659fddc4d5 | ||
|
|
f64a0876a1 | ||
|
|
df6097a184 | ||
|
|
0b848b1288 | ||
|
|
c6f5411efc | ||
|
|
986f008fae | ||
|
|
2df316b81c | ||
|
|
8aa80fadd3 | ||
|
|
f6e38c8804 | ||
|
|
47e5739ac0 | ||
|
|
e2f925426a | ||
|
|
e8f77cd2e0 | ||
|
|
e899149575 | ||
|
|
77d0277d4e | ||
|
|
4601ee328c | ||
|
|
5c9f11b3f9 | ||
|
|
95ce33d45a | ||
|
|
609f4c09c6 | ||
|
|
31f007fb75 | ||
|
|
2b98ab9633 | ||
|
|
1df03dca78 | ||
|
|
b689c9dce5 | ||
|
|
23f4fb95e7 | ||
|
|
845e144992 | ||
|
|
c19e7692d4 | ||
|
|
b395886535 | ||
|
|
ddc13a6327 | ||
|
|
d3cd5a3120 | ||
|
|
f50652af83 | ||
|
|
4059a06cc8 | ||
|
|
138bf8f7a5 | ||
|
|
098bc86e0c | ||
|
|
104f296929 | ||
|
|
1a52775974 | ||
|
|
4f0964c0b3 | ||
|
|
c57520fff1 | ||
|
|
ff95909d5a | ||
|
|
8c7fc2780d | ||
|
|
210749fa0f | ||
|
|
888bc896a5 | ||
|
|
e7d8fd0ff6 | ||
|
|
cce51a6990 | ||
|
|
adeba8f552 | ||
|
|
0e06c4ba24 | ||
|
|
0e060e61fa | ||
|
|
8cf4310152 | ||
|
|
d6897ca4b4 | ||
|
|
6e2f3b1a0c | ||
|
|
6a64f3c795 | ||
|
|
2dbf1769e0 | ||
|
|
54aac237ee | ||
|
|
2ec6096d4d | ||
|
|
3808609963 | ||
|
|
c66a47c3e5 | ||
|
|
435ee43c98 | ||
|
|
cb150effbb | ||
|
|
47d7528b61 | ||
|
|
152db9f00a | ||
|
|
7a244757d2 | ||
|
|
626099f835 | ||
|
|
ca55f9917d | ||
|
|
4d58245ad9 | ||
|
|
28b04f27ee | ||
|
|
1252e1942d | ||
|
|
38774252cc | ||
|
|
7cfa94c68c | ||
|
|
4e7cc7a782 | ||
|
|
bfa6054899 | ||
|
|
d25a32beaf | ||
|
|
127344802d | ||
|
|
96070ac473 | ||
|
|
583deb4588 | ||
|
|
b555c6ac2d | ||
|
|
602a9f2a44 | ||
|
|
a89b64b454 | ||
|
|
49d58a9a60 | ||
|
|
cb7ecc066c | ||
|
|
57aeada21e | ||
|
|
c2de73a1a5 | ||
|
|
71f02c0269 | ||
|
|
9b8a581a83 | ||
|
|
f554c8b6d5 | ||
|
|
b862a0906e | ||
|
|
61b13808fb | ||
|
|
7f25c219d8 | ||
|
|
32ba4187aa | ||
|
|
06c0dfa3f2 | ||
|
|
4c7bf69275 | ||
|
|
63644f5499 | ||
|
|
8a57304790 | ||
|
|
d7df071595 | ||
|
|
6e8b04ef4f | ||
|
|
b73a4e3748 | ||
|
|
102746d0b0 | ||
|
|
2d237af6fd | ||
|
|
4030ff43cd | ||
|
|
092cc8d9ab | ||
|
|
2d7106ae93 | ||
|
|
d5b885aab4 | ||
|
|
34f0effb6a | ||
|
|
d3e9300fa4 | ||
|
|
0c9560f8ca | ||
|
|
1c5df01fbc | ||
|
|
49cdc56cff | ||
|
|
00e1f31f84 | ||
|
|
b7e06652b4 | ||
|
|
0cf49ee042 | ||
|
|
7d610ecfdc | ||
|
|
c8e4278c39 | ||
|
|
07b47a26ca | ||
|
|
8d55331e6c | ||
|
|
cae06858fc | ||
|
|
8d4e8e6f35 | ||
|
|
8d40f9f9e5 | ||
|
|
e93f6b2383 | ||
|
|
4c2e216191 | ||
|
|
2a35541dc5 | ||
|
|
a927282e77 | ||
|
|
a7faac5425 | ||
|
|
a22496c355 | ||
|
|
20ec58b965 | ||
|
|
7453d7e890 | ||
|
|
1991e33b9d | ||
|
|
f429a6a03d | ||
|
|
4b98c56965 | ||
|
|
eb43a37ae5 | ||
|
|
1cf6d88460 | ||
|
|
9ddbebbd7e | ||
|
|
ca7f27d536 | ||
|
|
98184e6732 | ||
|
|
3b2616c278 | ||
|
|
9565c52e23 | ||
|
|
bb1acaecd6 | ||
|
|
dcbb90a7bd | ||
|
|
c46542bee0 | ||
|
|
709ec0585e | ||
|
|
a9c6709080 | ||
|
|
938f758920 | ||
|
|
ce4974be94 | ||
|
|
040bad0eba | ||
|
|
60e6b5ec99 | ||
|
|
695a111c9a | ||
|
|
0160549dbe | ||
|
|
3fd0d9ab28 | ||
|
|
b1061ae103 | ||
|
|
6e737ce33f | ||
|
|
10db6b9ae2 | ||
|
|
1514bfed5b | ||
|
|
dc264d40f0 | ||
|
|
104e647f61 | ||
|
|
d5d7fdde6d | ||
|
|
23aa88d564 | ||
|
|
ac1bda0392 | ||
|
|
5cdb786966 | ||
|
|
2b5ca2e219 | ||
|
|
fc77b246c2 | ||
|
|
083832cb3e | ||
|
|
804db2fec1 | ||
|
|
769964d5db | ||
|
|
aba0efca34 | ||
|
|
9ff18fe8b5 | ||
|
|
ee78cbf90e | ||
|
|
ffc3c9d8a0 | ||
|
|
a69b6b6406 | ||
|
|
1252402e5e | ||
|
|
53be587189 | ||
|
|
d3cc8b0c77 | ||
|
|
4f4d117ce0 | ||
|
|
0d1e38346e | ||
|
|
273dc0f43f | ||
|
|
26f19b6630 |
@@ -7,10 +7,18 @@ build/
|
||||
**/node_modules/
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.idea/
|
||||
|
||||
# misc
|
||||
**/.git*
|
||||
*.spec
|
||||
screenshots/
|
||||
README.md
|
||||
|
||||
# runtime data
|
||||
data/*
|
||||
!data/config.example.ini
|
||||
!data/emoticons/
|
||||
data/emoticons/*
|
||||
!data/emoticons/.gitkeep
|
||||
log/*
|
||||
|
||||
6
.gitignore
vendored
@@ -105,3 +105,9 @@ venv.bak/
|
||||
|
||||
|
||||
.idea/
|
||||
data/*
|
||||
!data/config.example.ini
|
||||
!data/emoticons/
|
||||
data/emoticons/*
|
||||
!data/emoticons/.gitkeep
|
||||
log/*
|
||||
|
||||
60
Dockerfile
@@ -1,33 +1,47 @@
|
||||
# 运行时
|
||||
FROM python:3.6.8-slim-stretch
|
||||
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak \
|
||||
&& echo "deb http://mirrors.tuna.tsinghua.edu.cn/debian/ stretch main contrib non-free">>/etc/apt/sources.list \
|
||||
&& echo "deb http://mirrors.tuna.tsinghua.edu.cn/debian/ stretch-updates main contrib non-free">>/etc/apt/sources.list \
|
||||
&& echo "deb http://mirrors.tuna.tsinghua.edu.cn/debian/ stretch-backports main contrib non-free">>/etc/apt/sources.list \
|
||||
&& echo "deb http://mirrors.tuna.tsinghua.edu.cn/debian-security stretch/updates main contrib non-free">>/etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y wget tar xz-utils
|
||||
RUN wget https://nodejs.org/dist/v10.16.0/node-v10.16.0-linux-x64.tar.xz \
|
||||
&& tar -xvf node-v10.16.0-linux-x64.tar.xz \
|
||||
&& rm node-v10.16.0-linux-x64.tar.xz \
|
||||
&& ln -s /node-v10.16.0-linux-x64/bin/node /usr/local/bin/node \
|
||||
&& ln -s /node-v10.16.0-linux-x64/bin/npm /usr/local/bin/npm
|
||||
#
|
||||
# 构建前端
|
||||
#
|
||||
|
||||
# 后端依赖
|
||||
COPY requirements.txt /blivechat/
|
||||
RUN pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r /blivechat/requirements.txt
|
||||
FROM node:18.17.0-bullseye AS builder
|
||||
ARG BASE_PATH='/root/blivechat'
|
||||
WORKDIR "${BASE_PATH}/frontend"
|
||||
|
||||
# 前端依赖
|
||||
WORKDIR /blivechat/frontend
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm i --registry=https://registry.npm.taobao.org
|
||||
COPY frontend/package.json ./
|
||||
RUN npm i
|
||||
|
||||
# 编译
|
||||
COPY . /blivechat
|
||||
# 编译前端
|
||||
COPY frontend ./
|
||||
RUN npm run build
|
||||
|
||||
#
|
||||
# 准备后端
|
||||
#
|
||||
|
||||
FROM python:3.8.12-bullseye
|
||||
ARG BASE_PATH='/root/blivechat'
|
||||
ARG EXT_DATA_PATH='/mnt/data'
|
||||
WORKDIR "${BASE_PATH}"
|
||||
|
||||
# 后端依赖
|
||||
COPY blivedm/requirements.txt blivedm/
|
||||
COPY requirements.txt ./
|
||||
RUN pip3 install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt
|
||||
|
||||
# 数据目录
|
||||
COPY . ./
|
||||
RUN mkdir -p "${EXT_DATA_PATH}/frontend/dist" \
|
||||
&& 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
|
||||
|
||||
# 编译好的前端
|
||||
COPY --from=builder "${BASE_PATH}/frontend/dist" "${EXT_DATA_PATH}/frontend/dist/"
|
||||
|
||||
# 运行
|
||||
WORKDIR /blivechat
|
||||
VOLUME "${EXT_DATA_PATH}"
|
||||
EXPOSE 12450
|
||||
ENTRYPOINT ["python3", "main.py"]
|
||||
CMD ["--host", "0.0.0.0", "--port", "12450"]
|
||||
|
||||
188
README.md
@@ -1,52 +1,158 @@
|
||||
# blivechat
|
||||
用于OBS的仿YouTube风格的bilibili直播聊天层
|
||||
用于OBS的仿YouTube风格的bilibili直播评论栏
|
||||
|
||||
最近喜欢看VTuber,想为此写些程序,于是有了这个东西。~~写到一半发现有类似项目了:[bilibili-live-chat](https://github.com/Tsuk1ko/bilibili-live-chat)、[BiliChat](https://github.com/3Shain/BiliChat)。~~ 本项目就当做练手吧,而且对YouTube的模仿程度更高
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 特性
|
||||
* 兼容YouTube直播chat的样式
|
||||
* 金瓜子礼物模仿Super Chat显示
|
||||
* 兼容YouTube直播评论栏的样式
|
||||
* 付费礼物模仿醒目留言显示
|
||||
* 高亮舰队、房管、主播的用户名
|
||||
* 支持屏蔽弹幕、限制最大速度等设置
|
||||
* 自带样式生成器
|
||||
* 支持屏蔽弹幕、合并相似弹幕等设置
|
||||
* 自带两种样式生成器,经典YouTube风格和仿微信风格
|
||||
* 支持前端直连B站服务器或者通过后端转发
|
||||
* 支持自动翻译弹幕、醒目留言到日语,可以在后台配置翻译目标语言
|
||||
* 支持标注打赏用户名的读音,可选拼音或日文假名
|
||||
* 支持配置自定义表情,不需要开通B站官方表情
|
||||
|
||||
## 使用方法
|
||||
### 发布版
|
||||
以下几种方式任选一种即可
|
||||
|
||||
### 一、本地使用
|
||||
1. 下载[发布版](https://github.com/xfgryujk/blivechat/releases)(仅提供x64 Windows版)
|
||||
2. 双击`blivechat.exe`运行服务器,或者用命令行可以指定host和端口号:
|
||||
```bat
|
||||
blivechat.exe --host 127.0.0.1 --port 12450
|
||||
```
|
||||
3. 用浏览器打开[http://localhost](http://localhost),输入房间ID,保存配置,进入房间,复制房间URL
|
||||
4. (可选)用样式生成器生成样式,复制CSS
|
||||
5. 在OBS中添加浏览器源,输入URL和自定义CSS,或者可以在首页的样式设置里输入CSS
|
||||
```sh
|
||||
blivechat.exe --host 127.0.0.1 --port 12450
|
||||
```
|
||||
3. 用浏览器打开[http://localhost:12450](http://localhost:12450),输入房间ID,复制房间URL
|
||||
4. 用样式生成器生成样式,复制CSS
|
||||
5. 在OBS中添加浏览器源,输入URL和自定义CSS
|
||||
|
||||
### 源代码版
|
||||
1. 编译前端(需要安装Node.js和npm):
|
||||
```sh
|
||||
cd frontend
|
||||
npm i
|
||||
npm run build
|
||||
```
|
||||
2. 运行服务器(需要Python3.6以上版本):
|
||||
```sh
|
||||
pip3 install -r requirements.txt
|
||||
python3 main.py
|
||||
```
|
||||
或者可以指定host和端口号:
|
||||
```sh
|
||||
python3 main.py --host 127.0.0.1 --port 12450
|
||||
```
|
||||
3. 用浏览器打开[http://localhost](http://localhost),输入房间ID,保存配置,进入房间,复制房间URL
|
||||
4. (可选)用样式生成器生成样式,复制CSS
|
||||
5. 在OBS中添加浏览器源,输入URL和自定义CSS,或者可以在首页的样式设置里输入CSS
|
||||
**注意事项:**
|
||||
|
||||
### Docker
|
||||
1. ```sh
|
||||
docker run -d -p 12450:12450 xfgryujk/blivechat:latest
|
||||
```
|
||||
2. 以下略
|
||||
* 本地使用时不要关闭blivechat.exe那个黑框,否则不能继续获取头像或弹幕
|
||||
* 样式生成器没有列出所有本地字体,但是可以手动输入本地字体
|
||||
* 如果需要使用翻译功能,建议看[配置官方翻译接口傻瓜式教程](https://www.bilibili.com/read/cv14663633)
|
||||
|
||||
### 二、公共服务器
|
||||
请优先在本地使用,使用公共服务器会有更大的延迟,而且服务器故障时可能发生直播事故
|
||||
|
||||
* [公共服务器](http://chat.bilisc.com/)
|
||||
|
||||
### 三、源代码版(自建服务器或在Windows以外平台)
|
||||
0. 由于使用了git子模块,clone时需要加上`--recursive`参数:
|
||||
```sh
|
||||
git clone --recursive https://github.com/xfgryujk/blivechat.git
|
||||
```
|
||||
如果已经clone,拉子模块的方法:
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
1. 编译前端(需要安装Node.js):
|
||||
```sh
|
||||
cd frontend
|
||||
npm i
|
||||
npm run build
|
||||
```
|
||||
2. 运行服务器(需要Python3.8以上版本):
|
||||
```sh
|
||||
pip3 install -r requirements.txt
|
||||
python3 main.py
|
||||
```
|
||||
或者可以指定host和端口号:
|
||||
```sh
|
||||
python3 main.py --host 127.0.0.1 --port 12450
|
||||
```
|
||||
3. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略
|
||||
|
||||
### 四、Docker(自建服务器)
|
||||
1. ```sh
|
||||
docker run --name blivechat -d -p 12450:12450 \
|
||||
--mount source=blivechat-data,target=/mnt/data \
|
||||
xfgryujk/blivechat:latest
|
||||
```
|
||||
2. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略
|
||||
|
||||
## 自建服务器相关补充
|
||||
### 服务器配置
|
||||
服务器配置在`data/config.ini`,可以配置数据库和允许自动翻译等,编辑后要重启生效
|
||||
|
||||
**自建服务器时强烈建议不使用加载器**,否则可能因为混合HTTP和HTTPS等原因加载不出来
|
||||
|
||||
### 参考nginx配置
|
||||
`sudo vim /etc/nginx/sites-enabled/blivechat.conf`
|
||||
|
||||
```nginx
|
||||
upstream blivechat {
|
||||
keepalive 8;
|
||||
# blivechat地址
|
||||
server 127.0.0.1:12450;
|
||||
}
|
||||
|
||||
# 强制HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name YOUR.DOMAIN.NAME;
|
||||
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name YOUR.DOMAIN.NAME;
|
||||
|
||||
# SSL
|
||||
ssl_certificate /PATH/TO/CERT.crt;
|
||||
ssl_certificate_key /PATH/TO/CERT_KEY.key;
|
||||
|
||||
set $blivechat_path /PATH/TO/BLIVECHAT;
|
||||
|
||||
client_body_buffer_size 256k;
|
||||
client_max_body_size 1.1m;
|
||||
|
||||
# 代理header
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# 静态文件
|
||||
location / {
|
||||
root $blivechat_path/frontend/dist;
|
||||
try_files $uri $uri/ @index;
|
||||
}
|
||||
# 不存在的文件请求转发到index.html,交给前端路由
|
||||
location @index {
|
||||
rewrite ^ /index.html last;
|
||||
}
|
||||
location = /index.html {
|
||||
root $blivechat_path/frontend/dist;
|
||||
# index.html不缓存,防止更新后前端还是旧版
|
||||
add_header Cache-Control no-cache;
|
||||
}
|
||||
location /emoticons {
|
||||
alias $blivechat_path/data/emoticons;
|
||||
}
|
||||
# 动态API
|
||||
location /api {
|
||||
proxy_pass http://blivechat;
|
||||
}
|
||||
# websocket
|
||||
location = /api/chat {
|
||||
proxy_pass http://blivechat;
|
||||
|
||||
# 代理websocket必须设置
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
|
||||
# 由于这个块有proxy_set_header,这些不会自动继承
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
20
api/base.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
import tornado.web
|
||||
|
||||
|
||||
class ApiHandler(tornado.web.RequestHandler): # noqa
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.json_args = None
|
||||
|
||||
def prepare(self):
|
||||
self.set_header('Cache-Control', 'no-cache')
|
||||
|
||||
if not self.request.headers.get('Content-Type', '').startswith('application/json'):
|
||||
return
|
||||
try:
|
||||
self.json_args = json.loads(self.request.body)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
360
api/chat.py
Normal file
@@ -0,0 +1,360 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import enum
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from typing import *
|
||||
|
||||
import aiohttp
|
||||
import tornado.websocket
|
||||
|
||||
import api.base
|
||||
import blivedm.blivedm.client as blivedm_client
|
||||
import config
|
||||
import services.avatar
|
||||
import services.chat
|
||||
import services.translate
|
||||
import utils.request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(enum.IntEnum):
|
||||
HEARTBEAT = 0
|
||||
JOIN_ROOM = 1
|
||||
ADD_TEXT = 2
|
||||
ADD_GIFT = 3
|
||||
ADD_MEMBER = 4
|
||||
ADD_SUPER_CHAT = 5
|
||||
DEL_SUPER_CHAT = 6
|
||||
UPDATE_TRANSLATION = 7
|
||||
|
||||
|
||||
class ContentType(enum.IntEnum):
|
||||
TEXT = 0
|
||||
EMOTICON = 1
|
||||
|
||||
|
||||
def make_message_body(cmd, data):
|
||||
return json.dumps(
|
||||
{
|
||||
'cmd': cmd,
|
||||
'data': data
|
||||
}
|
||||
).encode('utf-8')
|
||||
|
||||
|
||||
def make_text_message_data(
|
||||
avatar_url: str = services.avatar.DEFAULT_AVATAR_URL,
|
||||
timestamp: int = None,
|
||||
author_name: str = '',
|
||||
author_type: int = 0,
|
||||
content: str = '',
|
||||
privilege_type: int = 0,
|
||||
is_gift_danmaku: bool = False,
|
||||
author_level: int = 1,
|
||||
is_newbie: bool = False,
|
||||
is_mobile_verified: bool = True,
|
||||
medal_level: int = 0,
|
||||
id_: str = None,
|
||||
translation: str = '',
|
||||
content_type: int = ContentType.TEXT,
|
||||
content_type_params: list = None,
|
||||
text_emoticons: Iterable[Tuple[str, str]] = None
|
||||
):
|
||||
# 为了节省带宽用list而不是dict
|
||||
return [
|
||||
# 0: avatarUrl
|
||||
avatar_url,
|
||||
# 1: timestamp
|
||||
timestamp if timestamp is not None else int(time.time()),
|
||||
# 2: authorName
|
||||
author_name,
|
||||
# 3: authorType
|
||||
author_type,
|
||||
# 4: content
|
||||
content,
|
||||
# 5: privilegeType
|
||||
privilege_type,
|
||||
# 6: isGiftDanmaku
|
||||
1 if is_gift_danmaku else 0,
|
||||
# 7: authorLevel
|
||||
author_level,
|
||||
# 8: isNewbie
|
||||
1 if is_newbie else 0,
|
||||
# 9: isMobileVerified
|
||||
1 if is_mobile_verified else 0,
|
||||
# 10: medalLevel
|
||||
medal_level,
|
||||
# 11: id
|
||||
id_ if id_ is not None else uuid.uuid4().hex,
|
||||
# 12: translation
|
||||
translation,
|
||||
# 13: contentType
|
||||
content_type,
|
||||
# 14: contentTypeParams
|
||||
content_type_params if content_type_params is not None else [],
|
||||
# 15: textEmoticons
|
||||
text_emoticons if text_emoticons is not None else [],
|
||||
]
|
||||
|
||||
|
||||
def make_emoticon_params(url):
|
||||
return [
|
||||
# 0: url
|
||||
url,
|
||||
]
|
||||
|
||||
|
||||
def make_translation_message_data(msg_id, translation):
|
||||
return [
|
||||
# 0: id
|
||||
msg_id,
|
||||
# 1: translation
|
||||
translation,
|
||||
]
|
||||
|
||||
|
||||
class ChatHandler(tornado.websocket.WebSocketHandler): # noqa
|
||||
HEARTBEAT_INTERVAL = 10
|
||||
RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + 5
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._heartbeat_timer_handle = None
|
||||
self._receive_timeout_timer_handle = None
|
||||
|
||||
self.room_id = None
|
||||
self.auto_translate = False
|
||||
|
||||
def open(self):
|
||||
logger.info('client=%s connected', self.request.remote_ip)
|
||||
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
|
||||
self.HEARTBEAT_INTERVAL, self._on_send_heartbeat
|
||||
)
|
||||
self._refresh_receive_timeout_timer()
|
||||
|
||||
def _on_send_heartbeat(self):
|
||||
self.send_cmd_data(Command.HEARTBEAT, {})
|
||||
self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
|
||||
self.HEARTBEAT_INTERVAL, self._on_send_heartbeat
|
||||
)
|
||||
|
||||
def _refresh_receive_timeout_timer(self):
|
||||
if self._receive_timeout_timer_handle is not None:
|
||||
self._receive_timeout_timer_handle.cancel()
|
||||
self._receive_timeout_timer_handle = asyncio.get_running_loop().call_later(
|
||||
self.RECEIVE_TIMEOUT, self._on_receive_timeout
|
||||
)
|
||||
|
||||
def _on_receive_timeout(self):
|
||||
logger.info('client=%s timed out', self.request.remote_ip)
|
||||
self._receive_timeout_timer_handle = None
|
||||
self.close()
|
||||
|
||||
def on_close(self):
|
||||
logger.info('client=%s disconnected, room=%s', self.request.remote_ip, str(self.room_id))
|
||||
if self.has_joined_room:
|
||||
services.chat.client_room_manager.del_client(self.room_id, self)
|
||||
if self._heartbeat_timer_handle is not None:
|
||||
self._heartbeat_timer_handle.cancel()
|
||||
self._heartbeat_timer_handle = None
|
||||
if self._receive_timeout_timer_handle is not None:
|
||||
self._receive_timeout_timer_handle.cancel()
|
||||
self._receive_timeout_timer_handle = None
|
||||
|
||||
def on_message(self, message):
|
||||
try:
|
||||
body = json.loads(message)
|
||||
cmd = body['cmd']
|
||||
|
||||
if cmd == Command.HEARTBEAT:
|
||||
# 超时没有加入房间也断开
|
||||
if self.has_joined_room:
|
||||
self._refresh_receive_timeout_timer()
|
||||
|
||||
elif cmd == Command.JOIN_ROOM:
|
||||
if self.has_joined_room:
|
||||
return
|
||||
self._refresh_receive_timeout_timer()
|
||||
|
||||
self.room_id = int(body['data']['roomId'])
|
||||
logger.info('client=%s joining room %d', self.request.remote_ip, self.room_id)
|
||||
try:
|
||||
cfg = body['data']['config']
|
||||
self.auto_translate = bool(cfg['autoTranslate'])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
services.chat.client_room_manager.add_client(self.room_id, self)
|
||||
asyncio.create_task(self._on_joined_room())
|
||||
|
||||
else:
|
||||
logger.warning('client=%s unknown cmd=%d, body=%s', self.request.remote_ip, cmd, body)
|
||||
|
||||
except Exception: # noqa
|
||||
logger.exception('client=%s on_message error, message=%s', self.request.remote_ip, message)
|
||||
|
||||
# 跨域测试用
|
||||
def check_origin(self, origin):
|
||||
if self.application.settings['debug']:
|
||||
return True
|
||||
return super().check_origin(origin)
|
||||
|
||||
@property
|
||||
def has_joined_room(self):
|
||||
return self.room_id is not None
|
||||
|
||||
def send_cmd_data(self, cmd, data):
|
||||
self.send_body_no_raise(make_message_body(cmd, data))
|
||||
|
||||
def send_body_no_raise(self, body: Union[bytes, str, Dict[str, Any]]):
|
||||
try:
|
||||
self.write_message(body)
|
||||
except tornado.websocket.WebSocketClosedError:
|
||||
self.close()
|
||||
|
||||
async def _on_joined_room(self):
|
||||
if self.settings['debug']:
|
||||
await self._send_test_message()
|
||||
|
||||
# 不允许自动翻译的提示
|
||||
if self.auto_translate:
|
||||
cfg = config.get_config()
|
||||
if cfg.allow_translate_rooms and self.room_id not in cfg.allow_translate_rooms:
|
||||
self.send_cmd_data(Command.ADD_TEXT, make_text_message_data(
|
||||
author_name='blivechat',
|
||||
author_type=2,
|
||||
content='Translation is not allowed in this room. Please download to use translation',
|
||||
author_level=60,
|
||||
))
|
||||
|
||||
# 测试用
|
||||
async def _send_test_message(self):
|
||||
base_data = {
|
||||
'avatarUrl': await services.avatar.get_avatar_url(300474),
|
||||
'timestamp': int(time.time()),
|
||||
'authorName': 'xfgryujk',
|
||||
}
|
||||
text_data = make_text_message_data(
|
||||
avatar_url=base_data['avatarUrl'],
|
||||
timestamp=base_data['timestamp'],
|
||||
author_name=base_data['authorName'],
|
||||
content='我能吞下玻璃而不伤身体',
|
||||
author_level=60,
|
||||
)
|
||||
member_data = {
|
||||
**base_data,
|
||||
'id': uuid.uuid4().hex,
|
||||
'privilegeType': 3
|
||||
}
|
||||
gift_data = {
|
||||
**base_data,
|
||||
'id': uuid.uuid4().hex,
|
||||
'totalCoin': 450000,
|
||||
'giftName': '摩天大楼',
|
||||
'num': 1
|
||||
}
|
||||
sc_data = {
|
||||
**base_data,
|
||||
'id': str(random.randint(1, 65535)),
|
||||
'price': 30,
|
||||
'content': 'The quick brown fox jumps over the lazy dog',
|
||||
'translation': ''
|
||||
}
|
||||
self.send_cmd_data(Command.ADD_TEXT, text_data)
|
||||
text_data[4] = 'te[dog]st'
|
||||
text_data[11] = uuid.uuid4().hex
|
||||
text_data[15] = [('[dog]', 'http://i0.hdslb.com/bfs/live/4428c84e694fbf4e0ef6c06e958d9352c3582740.png')]
|
||||
self.send_cmd_data(Command.ADD_TEXT, text_data)
|
||||
text_data[2] = '主播'
|
||||
text_data[3] = 3
|
||||
text_data[4] = "I can eat glass, it doesn't hurt me."
|
||||
text_data[11] = uuid.uuid4().hex
|
||||
text_data[15] = []
|
||||
self.send_cmd_data(Command.ADD_TEXT, text_data)
|
||||
self.send_cmd_data(Command.ADD_MEMBER, member_data)
|
||||
self.send_cmd_data(Command.ADD_SUPER_CHAT, sc_data)
|
||||
sc_data['id'] = str(random.randint(1, 65535))
|
||||
sc_data['price'] = 100
|
||||
sc_data['content'] = '敏捷的棕色狐狸跳过了懒狗'
|
||||
self.send_cmd_data(Command.ADD_SUPER_CHAT, sc_data)
|
||||
# self.send_cmd_data(Command.DEL_SUPER_CHAT, {'ids': [sc_data['id']]})
|
||||
self.send_cmd_data(Command.ADD_GIFT, gift_data)
|
||||
gift_data['id'] = uuid.uuid4().hex
|
||||
gift_data['totalCoin'] = 1245000
|
||||
gift_data['giftName'] = '小电视飞船'
|
||||
self.send_cmd_data(Command.ADD_GIFT, gift_data)
|
||||
|
||||
|
||||
class RoomInfoHandler(api.base.ApiHandler): # noqa
|
||||
async def get(self):
|
||||
room_id = int(self.get_query_argument('roomId'))
|
||||
logger.info('client=%s getting room info, room=%d', self.request.remote_ip, room_id)
|
||||
room_id, owner_uid = await self._get_room_info(room_id)
|
||||
host_server_list = await self._get_server_host_list(room_id)
|
||||
if owner_uid == 0:
|
||||
# 缓存3分钟
|
||||
self.set_header('Cache-Control', 'private, max-age=180')
|
||||
else:
|
||||
# 缓存1天
|
||||
self.set_header('Cache-Control', 'private, max-age=86400')
|
||||
self.write({
|
||||
'roomId': room_id,
|
||||
'ownerUid': owner_uid,
|
||||
'hostServerList': host_server_list
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
async def _get_room_info(room_id):
|
||||
try:
|
||||
async with utils.request.http_session.get(
|
||||
blivedm_client.ROOM_INIT_URL,
|
||||
headers={
|
||||
**utils.request.BILIBILI_COMMON_HEADERS,
|
||||
'Origin': 'https://live.bilibili.com',
|
||||
'Referer': f'https://live.bilibili.com/{room_id}'
|
||||
},
|
||||
params={
|
||||
'room_id': room_id
|
||||
}
|
||||
) as res:
|
||||
if res.status != 200:
|
||||
logger.warning('room=%d _get_room_info failed: %d %s', room_id,
|
||||
res.status, res.reason)
|
||||
return room_id, 0
|
||||
data = await res.json()
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
logger.exception('room=%d _get_room_info failed', room_id)
|
||||
return room_id, 0
|
||||
|
||||
if data['code'] != 0:
|
||||
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']
|
||||
|
||||
@staticmethod
|
||||
async def _get_server_host_list(_room_id):
|
||||
# 连接其他host必须要key
|
||||
return blivedm_client.DEFAULT_DANMAKU_SERVER_LIST
|
||||
|
||||
|
||||
class AvatarHandler(api.base.ApiHandler): # noqa
|
||||
async def get(self):
|
||||
uid = int(self.get_query_argument('uid'))
|
||||
avatar_url = await services.avatar.get_avatar_url_or_none(uid)
|
||||
if avatar_url is None:
|
||||
avatar_url = services.avatar.DEFAULT_AVATAR_URL
|
||||
# 缓存3分钟
|
||||
self.set_header('Cache-Control', 'private, max-age=180')
|
||||
else:
|
||||
# 缓存1天
|
||||
self.set_header('Cache-Control', 'private, max-age=86400')
|
||||
self.write({
|
||||
'avatarUrl': avatar_url
|
||||
})
|
||||
87
api/main.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
import tornado.web
|
||||
|
||||
import api.base
|
||||
import config
|
||||
import update
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EMOTICON_UPLOAD_PATH = os.path.join(config.DATA_PATH, 'emoticons')
|
||||
EMOTICON_BASE_URL = '/emoticons'
|
||||
|
||||
|
||||
class MainHandler(tornado.web.StaticFileHandler): # noqa
|
||||
"""为了使用Vue Router的history模式,把不存在的文件请求转发到index.html"""
|
||||
async def get(self, path, include_body=True):
|
||||
if path == '':
|
||||
await self._get_index(include_body)
|
||||
return
|
||||
|
||||
try:
|
||||
await super().get(path, include_body)
|
||||
except tornado.web.HTTPError as e:
|
||||
if e.status_code != 404:
|
||||
raise
|
||||
# 不存在的文件请求转发到index.html,交给前端路由
|
||||
await self._get_index(include_body)
|
||||
|
||||
async def _get_index(self, include_body=True):
|
||||
# index.html不缓存,防止更新后前端还是旧版
|
||||
self.set_header('Cache-Control', 'no-cache')
|
||||
await super().get('index.html', include_body)
|
||||
|
||||
|
||||
class ServerInfoHandler(api.base.ApiHandler): # noqa
|
||||
async def get(self):
|
||||
cfg = config.get_config()
|
||||
self.write({
|
||||
'version': update.VERSION,
|
||||
'config': {
|
||||
'enableTranslate': cfg.enable_translate,
|
||||
'enableUploadFile': cfg.enable_upload_file,
|
||||
'loaderUrl': cfg.loader_url
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class UploadEmoticonHandler(api.base.ApiHandler): # noqa
|
||||
async def post(self):
|
||||
cfg = config.get_config()
|
||||
if not cfg.enable_upload_file:
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
try:
|
||||
file = self.request.files['file'][0]
|
||||
except LookupError:
|
||||
raise tornado.web.MissingArgumentError('file')
|
||||
if len(file.body) > 1024 * 1024:
|
||||
raise tornado.web.HTTPError(413, 'file is too large, size=%d', len(file.body))
|
||||
if not file.content_type.lower().startswith('image/'):
|
||||
raise tornado.web.HTTPError(415)
|
||||
|
||||
url = await asyncio.get_running_loop().run_in_executor(
|
||||
None, self._save_file, file.body, self.request.remote_ip
|
||||
)
|
||||
self.write({
|
||||
'url': url
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def _save_file(body, client):
|
||||
md5 = hashlib.md5(body).hexdigest()
|
||||
filename = md5 + '.png'
|
||||
path = os.path.join(EMOTICON_UPLOAD_PATH, filename)
|
||||
logger.info('client=%s uploaded file, path=%s, size=%d', client, path, len(body))
|
||||
|
||||
tmp_path = path + '.tmp'
|
||||
with open(tmp_path, 'wb') as f:
|
||||
f.write(body)
|
||||
os.replace(tmp_path, path)
|
||||
|
||||
return f'{EMOTICON_BASE_URL}/{filename}'
|
||||
2
blivedm
148
config.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import configparser
|
||||
import logging
|
||||
import os
|
||||
from typing import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BASE_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
WEB_ROOT = os.path.join(BASE_PATH, 'frontend', 'dist')
|
||||
DATA_PATH = os.path.join(BASE_PATH, 'data')
|
||||
|
||||
CONFIG_PATH_LIST = [
|
||||
os.path.join(DATA_PATH, 'config.ini'),
|
||||
os.path.join(DATA_PATH, 'config.example.ini')
|
||||
]
|
||||
|
||||
_config: Optional['AppConfig'] = None
|
||||
|
||||
|
||||
def init():
|
||||
if reload():
|
||||
return
|
||||
logger.warning('Using default config')
|
||||
global _config
|
||||
_config = AppConfig()
|
||||
|
||||
|
||||
def reload():
|
||||
config_path = ''
|
||||
for path in CONFIG_PATH_LIST:
|
||||
if os.path.exists(path):
|
||||
config_path = path
|
||||
break
|
||||
if config_path == '':
|
||||
return False
|
||||
|
||||
config = AppConfig()
|
||||
if not config.load(config_path):
|
||||
return False
|
||||
global _config
|
||||
_config = config
|
||||
return True
|
||||
|
||||
|
||||
def get_config():
|
||||
return _config
|
||||
|
||||
|
||||
class AppConfig:
|
||||
def __init__(self):
|
||||
self.host = '127.0.0.1'
|
||||
self.port = 12450
|
||||
self.database_url = 'sqlite:///data/database.db'
|
||||
self.tornado_xheaders = False
|
||||
self.loader_url = ''
|
||||
self.open_browser_at_startup = True
|
||||
self.enable_upload_file = True
|
||||
|
||||
self.fetch_avatar_max_queue_size = 1
|
||||
self.avatar_cache_size = 10000
|
||||
|
||||
self.enable_translate = True
|
||||
self.allow_translate_rooms = set()
|
||||
self.translate_max_queue_size = 10
|
||||
self.translation_cache_size = 50000
|
||||
self.translator_configs = []
|
||||
|
||||
def load(self, path):
|
||||
try:
|
||||
config = configparser.ConfigParser()
|
||||
config.read(path, 'utf-8-sig')
|
||||
|
||||
self._load_app_config(config)
|
||||
self._load_translator_configs(config)
|
||||
except Exception: # noqa
|
||||
logger.exception('Failed to load config:')
|
||||
return False
|
||||
return True
|
||||
|
||||
def _load_app_config(self, config: configparser.ConfigParser):
|
||||
app_section = config['app']
|
||||
self.host = app_section.get('host', self.host)
|
||||
self.port = app_section.getint('port', fallback=self.port)
|
||||
self.database_url = app_section.get('database_url', self.database_url)
|
||||
self.tornado_xheaders = app_section.getboolean('tornado_xheaders', fallback=self.tornado_xheaders)
|
||||
self.loader_url = app_section.get('loader_url', self.loader_url)
|
||||
self.open_browser_at_startup = app_section.getboolean('open_browser_at_startup',
|
||||
fallback=self.open_browser_at_startup)
|
||||
self.enable_upload_file = app_section.getboolean('enable_upload_file', fallback=self.enable_upload_file)
|
||||
|
||||
self.fetch_avatar_max_queue_size = app_section.getint('fetch_avatar_max_queue_size',
|
||||
fallback=self.fetch_avatar_max_queue_size)
|
||||
self.avatar_cache_size = app_section.getint('avatar_cache_size', fallback=self.avatar_cache_size)
|
||||
|
||||
self.enable_translate = app_section.getboolean('enable_translate', fallback=self.enable_translate)
|
||||
self.allow_translate_rooms = _str_to_list(app_section.get('allow_translate_rooms', ''), int, set)
|
||||
self.translate_max_queue_size = app_section.getint('translate_max_queue_size', self.translate_max_queue_size)
|
||||
self.translation_cache_size = app_section.getint('translation_cache_size', self.translation_cache_size)
|
||||
|
||||
def _load_translator_configs(self, config: configparser.ConfigParser):
|
||||
app_section = config['app']
|
||||
section_names = _str_to_list(app_section.get('translator_configs', ''))
|
||||
translator_configs = []
|
||||
for section_name in section_names:
|
||||
try:
|
||||
section = config[section_name]
|
||||
type_ = section['type']
|
||||
|
||||
translator_config = {
|
||||
'type': type_,
|
||||
'query_interval': section.getfloat('query_interval'),
|
||||
}
|
||||
if type_ == 'TencentTranslateFree':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
elif type_ == 'BilibiliTranslateFree':
|
||||
pass
|
||||
elif type_ == 'TencentTranslate':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
translator_config['secret_id'] = section['secret_id']
|
||||
translator_config['secret_key'] = section['secret_key']
|
||||
translator_config['region'] = section['region']
|
||||
elif type_ == 'BaiduTranslate':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
translator_config['app_id'] = section['app_id']
|
||||
translator_config['secret'] = section['secret']
|
||||
else:
|
||||
raise ValueError(f'Invalid translator type: {type_}')
|
||||
except Exception: # noqa
|
||||
logger.exception('Failed to load translator=%s config:', section_name)
|
||||
continue
|
||||
|
||||
translator_configs.append(translator_config)
|
||||
self.translator_configs = translator_configs
|
||||
|
||||
|
||||
def _str_to_list(value, item_type: Type = str, container_type: Type = list):
|
||||
value = value.strip()
|
||||
if value == '':
|
||||
return container_type()
|
||||
items = value.split(',')
|
||||
items = map(lambda item: item.strip(), items)
|
||||
if item_type is not str:
|
||||
items = map(lambda item: item_type(item), items)
|
||||
return container_type(items)
|
||||
139
data/config.example.ini
Normal file
@@ -0,0 +1,139 @@
|
||||
# 如果要修改配置,可以复制此文件并重命名为“config.ini”再修改
|
||||
# If you want to modify the configuration, copy this file and rename it to "config.ini" and edit
|
||||
|
||||
[app]
|
||||
# 服务器监听的地址
|
||||
# The address the server listens on
|
||||
host = 127.0.0.1
|
||||
port = 12450
|
||||
|
||||
# 数据库配置,见 https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
|
||||
# See https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
|
||||
database_url = sqlite:///data/database.db
|
||||
|
||||
# 如果使用了nginx之类的反向代理服务器,设置为true
|
||||
# Set to true if you are using a reverse proxy server such as nginx
|
||||
tornado_xheaders = false
|
||||
|
||||
# 加载器URL,本地使用时加载器可以让你先运行OBS再运行blivechat。如果为空,不使用加载器
|
||||
# **自建服务器时强烈建议不使用加载器**,否则可能因为混合HTTP和HTTPS等原因加载不出来
|
||||
# Use a loader so that you can run OBS before blivechat. If empty, no loader is used
|
||||
loader_url = https://xfgryujk.sinacloud.net/blivechat/loader.html
|
||||
|
||||
# 启动时打开浏览器
|
||||
# Open browser at startup
|
||||
open_browser_at_startup = true
|
||||
|
||||
# 允许上传自定义表情文件
|
||||
# Enable uploading custom emote file
|
||||
enable_upload_file = true
|
||||
|
||||
|
||||
# 获取头像最大队列长度
|
||||
# Maximum queue length for fetching avatar
|
||||
fetch_avatar_max_queue_size = 4
|
||||
|
||||
# 内存中头像缓存数量
|
||||
# Number of avatar caches in memory
|
||||
avatar_cache_size = 10000
|
||||
|
||||
|
||||
# 允许自动翻译到日语
|
||||
# Enable auto translate to Japanese
|
||||
enable_translate = true
|
||||
|
||||
# 允许翻译的房间ID,以逗号分隔。如果为空,允许所有房间
|
||||
# Comma separated room IDs in which translation are allowed. If empty, all rooms are allowed
|
||||
# Example: allow_translate_rooms = 4895312,22347054,21693691
|
||||
allow_translate_rooms =
|
||||
|
||||
# 翻译最大队列长度
|
||||
# Maximum queue length for translating
|
||||
translate_max_queue_size = 10
|
||||
|
||||
# 翻译缓存数量
|
||||
# Number of translation caches
|
||||
translation_cache_size = 50000
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# 以下是给字幕组看的,实在懒得翻译了_(:з」∠)_。如果你不了解以下参数的意思,使用默认值就好
|
||||
# **The following is for translation team. Leave it default if you don't know its meaning**
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
|
||||
# 翻译器配置,索引到下面的配置节。可以以逗号分隔配置多个翻译器,翻译时会自动负载均衡
|
||||
# 配置多个翻译器可以增加额度、增加QPS、容灾
|
||||
# 不同配置可以使用同一个类型,但要使用不同的账号,否则还是会遇到额度、调用频率限制
|
||||
translator_configs = tencent_translate_free
|
||||
|
||||
|
||||
[tencent_translate_free]
|
||||
# 类型:腾讯翻译白嫖版。使用了网页版的接口,**将来可能失效**
|
||||
type = TencentTranslateFree
|
||||
|
||||
# 请求间隔时间(秒),等于 1 / QPS
|
||||
query_interval = 1
|
||||
|
||||
# 自动:auto;中文:zh;日语:jp;英语:en;韩语:kr
|
||||
# 完整语言列表见文档:https://cloud.tencent.com/document/product/551/15619
|
||||
# 源语言
|
||||
source_language = zh
|
||||
# 目标语言
|
||||
target_language = jp
|
||||
|
||||
|
||||
[tencent_translate]
|
||||
# 文档:https://cloud.tencent.com/product/tmt
|
||||
# 定价:https://cloud.tencent.com/document/product/551/35017
|
||||
# * 文本翻译的每月免费额度为5百万字符
|
||||
# * 文本翻译当月需付费字符数小于100百万字符(1亿字符)时,刊例价为58元/每百万字符
|
||||
# * 文本翻译当月需付费字符数大于等于100百万字符(1亿字符)时,刊例价为50元/每百万字符
|
||||
# 限制:https://cloud.tencent.com/document/product/551/32572
|
||||
# * 文本翻译最高QPS为5
|
||||
|
||||
# 类型:腾讯翻译
|
||||
type = TencentTranslate
|
||||
|
||||
# 请求间隔时间(秒),等于 1 / QPS。理论上最高QPS为5,实际测试是3
|
||||
query_interval = 0.333
|
||||
|
||||
# 自动:auto;中文:zh;日语:ja;英语:en;韩语:ko
|
||||
# 完整语言列表见文档:https://cloud.tencent.com/document/product/551/15619
|
||||
# 源语言
|
||||
source_language = zh
|
||||
# 目标语言
|
||||
target_language = ja
|
||||
|
||||
# 腾讯云API密钥
|
||||
secret_id =
|
||||
secret_key =
|
||||
|
||||
# 腾讯云地域参数,用来标识希望操作哪个地域的数据,建议按照运行blivechat的机器所在地区就近选择
|
||||
# 北京:ap-beijing;上海:ap-shanghai;香港:ap-hongkong;首尔:ap-seoul
|
||||
# 完整地域列表见文档:https://cloud.tencent.com/document/api/551/15615#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
|
||||
region = ap-shanghai
|
||||
|
||||
|
||||
[baidu_translate]
|
||||
# 文档:https://fanyi-api.baidu.com/
|
||||
# 定价:https://fanyi-api.baidu.com/product/112
|
||||
# * 标准版每月前5万字符免费,超出仅收取超出部分费用(QPS=1),按49元/百万字符计费
|
||||
# * 高级版每月前100万字符免费,超出仅收取超出部分费用(QPS=10),按49元/百万字符计费
|
||||
# * 尊享版每月前200万字符免费,超出后仅收取超出部分费用(QPS=100),按49元/百万字符计费
|
||||
|
||||
# 类型:百度翻译
|
||||
type = BaiduTranslate
|
||||
|
||||
# 请求间隔时间(秒),等于 1 / QPS
|
||||
query_interval = 1.5
|
||||
|
||||
# 自动:auto;中文:zh;日语:jp;英语:en;韩语:kor
|
||||
# 完整语言列表见文档:https://fanyi-api.baidu.com/doc/21
|
||||
# 源语言
|
||||
source_language = zh
|
||||
# 目标语言
|
||||
target_language = jp
|
||||
|
||||
# 百度翻译开放平台应用ID和密钥
|
||||
app_id =
|
||||
secret =
|
||||
0
data/emoticons/.gitkeep
Normal file
2
frontend/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
brotli_decode.js
|
||||
pronunciation/dict*.js
|
||||
81
frontend/.eslintrc.js
Normal file
@@ -0,0 +1,81 @@
|
||||
module.exports = {
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser"
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {
|
||||
"array-bracket-spacing": ["error", "never"], // 数组括号内不加空格
|
||||
"arrow-parens": ["error", "as-needed"], // 箭头函数单个参数不加括号
|
||||
"arrow-spacing": "error", // 箭头前后加空格
|
||||
"block-spacing": "error", // 块大括号内加空格
|
||||
"brace-style": "error", // 大括号不独占一行
|
||||
"comma-spacing": "error", // 逗号前面不加空格,后面加空格
|
||||
"comma-style": "error", // 逗号在语句后面而不是下一条的前面
|
||||
"computed-property-spacing": "error", // 计算属性名前后不加空格
|
||||
"curly": "error", // 禁止省略大括号
|
||||
"dot-notation": "error", // 使用点访问成员
|
||||
"eol-last": "error", // 文件末尾加换行符
|
||||
"func-call-spacing": "error", // 调用函数名和括号间不加空格
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }], // 使用函数定义语法,而不是把函数表达式赋值到变量
|
||||
"indent": ["error", 2], // 缩进2空格
|
||||
"key-spacing": ["error", { "mode": "minimum" }],
|
||||
"keyword-spacing": "error", // 关键词前后加空格
|
||||
"lines-between-class-members": "error", // 类成员定义间加空格
|
||||
"max-lines-per-function": ["error", 150], // 每个函数最多行数
|
||||
"max-nested-callbacks": ["error", 3], // 每个函数最多嵌套回调数
|
||||
"new-parens": "error", // new调用构造函数加空格
|
||||
"no-array-constructor": "error", // 使用数组字面量,而不是数组构造函数
|
||||
"no-floating-decimal": "error", // 禁止省略浮点数首尾的0
|
||||
"no-implicit-coercion": "error", // 禁止隐式转换
|
||||
"no-empty": ["error", { "allowEmptyCatch": true }], // 禁止空的块,除了catch
|
||||
"no-extra-parens": ["error", "all", { "nestedBinaryExpressions": false }], // 禁止多余的括号
|
||||
"no-labels": "error", // 禁止使用标签
|
||||
"no-lone-blocks": "error", // 禁止没用的块
|
||||
"no-mixed-operators": "error", // 禁止混用不同优先级的操作符而不加括号
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }], // 禁止多个空格,除了行尾注释前
|
||||
"no-multiple-empty-lines": "error", // 最多2个连续空行
|
||||
"no-nested-ternary": "error", // 禁止嵌套三元表达式
|
||||
"no-sequences": "error", // 禁止使用逗号操作符
|
||||
"no-tabs": "error", // 禁止使用tab
|
||||
"no-trailing-spaces": ["error", { "skipBlankLines": true }], // 禁止行尾的空格,除了空行
|
||||
"no-unused-expressions": "error", // 禁止没用的表达式
|
||||
"no-useless-concat": "error", // 禁止没用的字符串连接
|
||||
"no-useless-rename": "error", // 禁止没用的模块导入重命名、解构赋值重命名
|
||||
"no-useless-return": "error", // 禁止没用的return
|
||||
"no-var": "error", // 禁止使用var声明变量
|
||||
"no-void": "error", // 禁止使用void
|
||||
"no-whitespace-before-property": "error", // 禁止访问属性的点前后加空格
|
||||
"object-curly-spacing": ["error", "always"], // 对象字面量括号内加空格
|
||||
"operator-assignment": "error", // 尽量使用+=
|
||||
"operator-linebreak": ["error", "before"], // 操作符放行首
|
||||
"prefer-object-spread": "error", // 使用{...obj},而不是Object.assign
|
||||
"prefer-rest-params": "error", // 使用...args,而不是arguments
|
||||
"prefer-spread": "error", // 使用func(...args),而不是apply
|
||||
"prefer-template": "error", // 使用模板字符串,而不是字符串连接
|
||||
"rest-spread-spacing": ["error", "never"], // 解包操作符不加空格
|
||||
"semi": ["error", "never"], // 禁止使用多余的分号
|
||||
"semi-spacing": "error", // 分号前面不加空格,后面加空格
|
||||
"semi-style": "error", // 分号在语句后面而不是下一条的前面
|
||||
"space-before-blocks": "error", // 块大括号前加空格
|
||||
"space-before-function-paren": ["error", "never"], // 函数定义名称和括号间不加空格
|
||||
"space-in-parens": "error", // 括号内不加空格
|
||||
"space-infix-ops": "error", // 二元操作符前后加空格
|
||||
"space-unary-ops": "error", // 关键词一元操作符后加空格,符号一元操作符不加
|
||||
"spaced-comment": ["error", "always", { "block": { "balanced": true } }], // 注释前面加空格
|
||||
"template-curly-spacing": "error", // 模板字符串中变量大括号内不加空格
|
||||
|
||||
"no-shadow": "warn", // 变量名和外部作用域重复
|
||||
|
||||
"no-console": "off", // 线上尽量不要用console输出,看不到的
|
||||
|
||||
"vue/multi-word-component-names": "off", // Vue组件名允许用1个单词
|
||||
}
|
||||
}
|
||||
3
frontend/.gitignore
vendored
@@ -19,3 +19,6 @@ yarn-error.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
package-lock.json
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
],
|
||||
plugins: [
|
||||
[
|
||||
'component', {
|
||||
'libraryName': 'element-ui',
|
||||
'styleLibraryName': 'theme-chalk'
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
17
frontend/jsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"target": 2.7,
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.js",
|
||||
"./src/**/*.vue"
|
||||
]
|
||||
}
|
||||
11483
frontend/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blivechat",
|
||||
"version": "0.1.0",
|
||||
"version": "1.7.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
@@ -8,45 +8,28 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.0",
|
||||
"core-js": "^2.6.5",
|
||||
"axios": "^1.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"downloadjs": "^1.4.7",
|
||||
"element-ui": "^2.9.1",
|
||||
"lodash": "^4.17.11",
|
||||
"vue": "^2.6.10",
|
||||
"vue-i18n": "^8.11.2",
|
||||
"vue-router": "^3.0.6"
|
||||
"element-ui": "^2.15.13",
|
||||
"lodash": "^4.17.21",
|
||||
"vue": "^2.7.14",
|
||||
"vue-i18n": "^8.28.2",
|
||||
"vue-router": "^3.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.7.0",
|
||||
"@vue/cli-plugin-eslint": "^3.7.0",
|
||||
"@vue/cli-service": "^3.7.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"babel-plugin-component": "^1.1.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^9.16.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions"
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
||||
BIN
frontend/public/static/img/emoticons/233.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/public/static/img/emoticons/lipu.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/public/static/img/emoticons/miaoa.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.0 KiB |
BIN
frontend/public/static/img/tutorial/tutorial-1.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
frontend/public/static/img/tutorial/tutorial-2.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
frontend/public/static/img/tutorial/tutorial-3.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
frontend/public/static/img/tutorial/tutorial-4.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
frontend/public/static/img/tutorial/tutorial-5.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
2287
frontend/src/api/chat/ChatClientDirect/brotli_decode.js
Normal file
435
frontend/src/api/chat/ChatClientDirect/index.js
Normal file
@@ -0,0 +1,435 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { BrotliDecode } from './brotli_decode'
|
||||
import { getUuid4Hex } from '@/utils'
|
||||
import * as avatar from '../avatar'
|
||||
|
||||
const HEADER_SIZE = 16
|
||||
|
||||
// const WS_BODY_PROTOCOL_VERSION_NORMAL = 0
|
||||
// const WS_BODY_PROTOCOL_VERSION_HEARTBEAT = 1
|
||||
// const WS_BODY_PROTOCOL_VERSION_DEFLATE = 2
|
||||
const WS_BODY_PROTOCOL_VERSION_BROTLI = 3
|
||||
|
||||
// const OP_HANDSHAKE = 0
|
||||
// const OP_HANDSHAKE_REPLY = 1
|
||||
const OP_HEARTBEAT = 2
|
||||
const OP_HEARTBEAT_REPLY = 3
|
||||
// const OP_SEND_MSG = 4
|
||||
const OP_SEND_MSG_REPLY = 5
|
||||
// const OP_DISCONNECT_REPLY = 6
|
||||
const OP_AUTH = 7
|
||||
const OP_AUTH_REPLY = 8
|
||||
// const OP_RAW = 9
|
||||
// const OP_PROTO_READY = 10
|
||||
// const OP_PROTO_FINISH = 11
|
||||
// const OP_CHANGE_ROOM = 12
|
||||
// const OP_CHANGE_ROOM_REPLY = 13
|
||||
// const OP_REGISTER = 14
|
||||
// const OP_REGISTER_REPLY = 15
|
||||
// const OP_UNREGISTER = 16
|
||||
// const OP_UNREGISTER_REPLY = 17
|
||||
// B站业务自定义OP
|
||||
// const MinBusinessOp = 1000
|
||||
// const MaxBusinessOp = 10000
|
||||
|
||||
const AUTH_REPLY_CODE_OK = 0
|
||||
// const AUTH_REPLY_CODE_TOKEN_ERROR = -101
|
||||
|
||||
const HEARTBEAT_INTERVAL = 10 * 1000
|
||||
const RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + (5 * 1000)
|
||||
|
||||
let textEncoder = new TextEncoder()
|
||||
let textDecoder = new TextDecoder()
|
||||
|
||||
export default class ChatClientDirect {
|
||||
constructor(roomId) {
|
||||
// 调用initRoom后初始化,如果失败,使用这里的默认值
|
||||
this.roomId = roomId
|
||||
this.roomOwnerUid = 0
|
||||
this.hostServerList = [
|
||||
{ host: "broadcastlv.chat.bilibili.com", port: 2243, wss_port: 443, ws_port: 2244 }
|
||||
]
|
||||
|
||||
this.onAddText = null
|
||||
this.onAddGift = null
|
||||
this.onAddMember = null
|
||||
this.onAddSuperChat = null
|
||||
this.onDelSuperChat = null
|
||||
this.onUpdateTranslation = null
|
||||
|
||||
this.websocket = null
|
||||
this.retryCount = 0
|
||||
this.isDestroying = false
|
||||
this.heartbeatTimerId = null
|
||||
this.receiveTimeoutTimerId = null
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.initRoom()
|
||||
this.wsConnect()
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isDestroying = true
|
||||
if (this.websocket) {
|
||||
this.websocket.close()
|
||||
}
|
||||
}
|
||||
|
||||
async initRoom() {
|
||||
let res
|
||||
try {
|
||||
res = (await axios.get('/api/room_info', { params: {
|
||||
roomId: this.roomId
|
||||
} })).data
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
this.roomId = res.roomId
|
||||
this.roomOwnerUid = res.ownerUid
|
||||
if (res.hostServerList.length !== 0) {
|
||||
this.hostServerList = res.hostServerList
|
||||
}
|
||||
}
|
||||
|
||||
makePacket(data, operation) {
|
||||
let body = textEncoder.encode(JSON.stringify(data))
|
||||
let header = new ArrayBuffer(HEADER_SIZE)
|
||||
let headerView = new DataView(header)
|
||||
headerView.setUint32(0, HEADER_SIZE + body.byteLength) // pack_len
|
||||
headerView.setUint16(4, HEADER_SIZE) // raw_header_size
|
||||
headerView.setUint16(6, 1) // ver
|
||||
headerView.setUint32(8, operation) // operation
|
||||
headerView.setUint32(12, 1) // seq_id
|
||||
return new Blob([header, body])
|
||||
}
|
||||
|
||||
sendAuth() {
|
||||
let authParams = {
|
||||
uid: this.roomOwnerUid,
|
||||
roomid: this.roomId,
|
||||
protover: 3,
|
||||
platform: 'web',
|
||||
type: 2
|
||||
}
|
||||
this.websocket.send(this.makePacket(authParams, OP_AUTH))
|
||||
}
|
||||
|
||||
wsConnect() {
|
||||
if (this.isDestroying) {
|
||||
return
|
||||
}
|
||||
let hostServer = this.hostServerList[this.retryCount % this.hostServerList.length]
|
||||
const url = `wss://${hostServer.host}:${hostServer.wss_port}/sub`
|
||||
this.websocket = new WebSocket(url)
|
||||
this.websocket.binaryType = 'arraybuffer'
|
||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||
this.websocket.onclose = this.onWsClose.bind(this)
|
||||
this.websocket.onmessage = this.onWsMessage.bind(this)
|
||||
}
|
||||
|
||||
onWsOpen() {
|
||||
this.sendAuth()
|
||||
this.heartbeatTimerId = window.setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
|
||||
this.refreshReceiveTimeoutTimer()
|
||||
}
|
||||
|
||||
sendHeartbeat() {
|
||||
this.websocket.send(this.makePacket({}, OP_HEARTBEAT))
|
||||
}
|
||||
|
||||
refreshReceiveTimeoutTimer() {
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||
}
|
||||
this.receiveTimeoutTimerId = window.setTimeout(this.onReceiveTimeout.bind(this), RECEIVE_TIMEOUT)
|
||||
}
|
||||
|
||||
onReceiveTimeout() {
|
||||
console.warn('接收消息超时')
|
||||
this.discardWebsocket()
|
||||
}
|
||||
|
||||
discardWebsocket() {
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||
this.receiveTimeoutTimerId = null
|
||||
}
|
||||
|
||||
// 直接丢弃阻塞的websocket,不等onclose回调了
|
||||
this.websocket.onopen = this.websocket.onclose = this.websocket.onmessage = null
|
||||
this.websocket.close()
|
||||
this.onWsClose()
|
||||
}
|
||||
|
||||
onWsClose() {
|
||||
this.websocket = null
|
||||
if (this.heartbeatTimerId) {
|
||||
window.clearInterval(this.heartbeatTimerId)
|
||||
this.heartbeatTimerId = null
|
||||
}
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||
this.receiveTimeoutTimerId = null
|
||||
}
|
||||
|
||||
if (this.isDestroying) {
|
||||
return
|
||||
}
|
||||
this.retryCount++
|
||||
console.warn('掉线重连中', this.retryCount)
|
||||
window.setTimeout(this.wsConnect.bind(this), 1000)
|
||||
}
|
||||
|
||||
onWsMessage(event) {
|
||||
if (!(event.data instanceof ArrayBuffer)) {
|
||||
console.warn('未知的websocket消息类型,data=', event.data)
|
||||
return
|
||||
}
|
||||
|
||||
let data = new Uint8Array(event.data)
|
||||
this.parseWsMessage(data)
|
||||
|
||||
// 至少成功处理1条消息
|
||||
this.retryCount = 0
|
||||
}
|
||||
|
||||
parseWsMessage(data) {
|
||||
let offset = 0
|
||||
let dataView = new DataView(data.buffer)
|
||||
let packLen = dataView.getUint32(0)
|
||||
let rawHeaderSize = dataView.getUint16(4)
|
||||
// let ver = dataView.getUint16(6)
|
||||
let operation = dataView.getUint32(8)
|
||||
// let seqId = dataView.getUint32(12)
|
||||
|
||||
switch (operation) {
|
||||
case OP_AUTH_REPLY:
|
||||
case OP_SEND_MSG_REPLY: {
|
||||
// 业务消息,可能有多个包一起发,需要分包
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
let body = new Uint8Array(data.buffer, offset + rawHeaderSize, packLen - rawHeaderSize)
|
||||
this.parseBusinessMessage(dataView, body)
|
||||
|
||||
offset += packLen
|
||||
if (offset >= data.byteLength) {
|
||||
break
|
||||
}
|
||||
|
||||
dataView = new DataView(data.buffer, offset)
|
||||
packLen = dataView.getUint32(0)
|
||||
rawHeaderSize = dataView.getUint16(4)
|
||||
}
|
||||
break
|
||||
}
|
||||
case OP_HEARTBEAT_REPLY: {
|
||||
// 服务器心跳包,包含人气值,这里没用
|
||||
this.refreshReceiveTimeoutTimer()
|
||||
break
|
||||
}
|
||||
default: {
|
||||
// 未知消息
|
||||
let body = new Uint8Array(data.buffer, offset + rawHeaderSize, packLen - rawHeaderSize)
|
||||
console.warn('未知包类型,operation=', operation, dataView, body)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseBusinessMessage(dataView, body) {
|
||||
let ver = dataView.getUint16(6)
|
||||
let operation = dataView.getUint32(8)
|
||||
|
||||
switch (operation) {
|
||||
case OP_SEND_MSG_REPLY: {
|
||||
// 业务消息
|
||||
if (ver == WS_BODY_PROTOCOL_VERSION_BROTLI) {
|
||||
// 压缩过的先解压
|
||||
body = BrotliDecode(body)
|
||||
this.parseWsMessage(body)
|
||||
} else {
|
||||
// 没压缩过的直接反序列化
|
||||
if (body.length !== 0) {
|
||||
try {
|
||||
body = JSON.parse(textDecoder.decode(body))
|
||||
this.handlerCommand(body)
|
||||
} catch (e) {
|
||||
console.error('body=', body)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case OP_AUTH_REPLY: {
|
||||
// 认证响应
|
||||
body = JSON.parse(textDecoder.decode(body))
|
||||
if (body.code !== AUTH_REPLY_CODE_OK) {
|
||||
console.error('认证响应错误,body=', body)
|
||||
// 这里应该重新获取token再重连的,但前端没有用到token,所以不重新init了
|
||||
this.discardWebsocket()
|
||||
throw new Error('认证响应错误')
|
||||
}
|
||||
this.sendHeartbeat()
|
||||
break
|
||||
}
|
||||
default: {
|
||||
// 未知消息
|
||||
console.warn('未知包类型,operation=', operation, dataView, body)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlerCommand(command) {
|
||||
let cmd = command.cmd || ''
|
||||
let pos = cmd.indexOf(':')
|
||||
if (pos != -1) {
|
||||
cmd = cmd.substr(0, pos)
|
||||
}
|
||||
let callback = CMD_CALLBACK_MAP[cmd]
|
||||
if (callback) {
|
||||
callback.call(this, command)
|
||||
}
|
||||
}
|
||||
|
||||
async danmuMsgCallback(command) {
|
||||
if (!this.onAddText) {
|
||||
return
|
||||
}
|
||||
let info = command.info
|
||||
|
||||
let roomId, medalLevel
|
||||
if (info[3]) {
|
||||
roomId = info[3][3]
|
||||
medalLevel = info[3][0]
|
||||
} else {
|
||||
roomId = medalLevel = 0
|
||||
}
|
||||
|
||||
let uid = info[2][0]
|
||||
let isAdmin = info[2][2]
|
||||
let privilegeType = info[7]
|
||||
let authorType
|
||||
if (uid === this.roomOwnerUid) {
|
||||
authorType = 3
|
||||
} else if (isAdmin) {
|
||||
authorType = 2
|
||||
} else if (privilegeType !== 0) {
|
||||
authorType = 1
|
||||
} else {
|
||||
authorType = 0
|
||||
}
|
||||
|
||||
let textEmoticons = this.parseTextEmoticons(info)
|
||||
|
||||
let data = {
|
||||
avatarUrl: await avatar.getAvatarUrl(uid),
|
||||
timestamp: info[0][4] / 1000,
|
||||
authorName: info[2][1],
|
||||
authorType: authorType,
|
||||
content: info[1],
|
||||
privilegeType: privilegeType,
|
||||
isGiftDanmaku: Boolean(info[0][9]),
|
||||
authorLevel: info[4][0],
|
||||
isNewbie: info[2][5] < 10000,
|
||||
isMobileVerified: Boolean(info[2][6]),
|
||||
medalLevel: roomId === this.roomId ? medalLevel : 0,
|
||||
id: getUuid4Hex(),
|
||||
translation: '',
|
||||
emoticon: info[0][13].url || null,
|
||||
textEmoticons: textEmoticons,
|
||||
}
|
||||
this.onAddText(data)
|
||||
}
|
||||
|
||||
parseTextEmoticons(info) {
|
||||
try {
|
||||
let modeInfo = info[0][15]
|
||||
let extra = JSON.parse(modeInfo.extra)
|
||||
if (!extra.emots) {
|
||||
return []
|
||||
}
|
||||
let res = Object.values(extra.emots).map(emoticon => [emoticon.descript, emoticon.url])
|
||||
return res
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
sendGiftCallback(command) {
|
||||
if (!this.onAddGift) {
|
||||
return
|
||||
}
|
||||
let data = command.data
|
||||
if (data.coin_type !== 'gold') { // 丢人
|
||||
return
|
||||
}
|
||||
|
||||
data = {
|
||||
id: getUuid4Hex(),
|
||||
avatarUrl: avatar.processAvatarUrl(data.face),
|
||||
timestamp: data.timestamp,
|
||||
authorName: data.uname,
|
||||
totalCoin: data.total_coin,
|
||||
giftName: data.giftName,
|
||||
num: data.num
|
||||
}
|
||||
this.onAddGift(data)
|
||||
}
|
||||
|
||||
async guardBuyCallback(command) {
|
||||
if (!this.onAddMember) {
|
||||
return
|
||||
}
|
||||
|
||||
let data = command.data
|
||||
data = {
|
||||
id: getUuid4Hex(),
|
||||
avatarUrl: await avatar.getAvatarUrl(data.uid),
|
||||
timestamp: data.start_time,
|
||||
authorName: data.username,
|
||||
privilegeType: data.guard_level
|
||||
}
|
||||
this.onAddMember(data)
|
||||
}
|
||||
|
||||
superChatMessageCallback(command) {
|
||||
if (!this.onAddSuperChat) {
|
||||
return
|
||||
}
|
||||
|
||||
let data = command.data
|
||||
data = {
|
||||
id: data.id.toString(),
|
||||
avatarUrl: avatar.processAvatarUrl(data.user_info.face),
|
||||
timestamp: data.start_time,
|
||||
authorName: data.user_info.uname,
|
||||
price: data.price,
|
||||
content: data.message,
|
||||
translation: ''
|
||||
}
|
||||
this.onAddSuperChat(data)
|
||||
}
|
||||
|
||||
superChatMessageDeleteCallback(command) {
|
||||
if (!this.onDelSuperChat) {
|
||||
return
|
||||
}
|
||||
|
||||
let ids = []
|
||||
for (let id of command.data.ids) {
|
||||
ids.push(id.toString())
|
||||
}
|
||||
this.onDelSuperChat({ ids })
|
||||
}
|
||||
}
|
||||
|
||||
const CMD_CALLBACK_MAP = {
|
||||
DANMU_MSG: ChatClientDirect.prototype.danmuMsgCallback,
|
||||
SEND_GIFT: ChatClientDirect.prototype.sendGiftCallback,
|
||||
GUARD_BUY: ChatClientDirect.prototype.guardBuyCallback,
|
||||
SUPER_CHAT_MESSAGE: ChatClientDirect.prototype.superChatMessageCallback,
|
||||
SUPER_CHAT_MESSAGE_DELETE: ChatClientDirect.prototype.superChatMessageDeleteCallback
|
||||
}
|
||||
182
frontend/src/api/chat/ChatClientRelay.js
Normal file
@@ -0,0 +1,182 @@
|
||||
const COMMAND_HEARTBEAT = 0
|
||||
const COMMAND_JOIN_ROOM = 1
|
||||
const COMMAND_ADD_TEXT = 2
|
||||
const COMMAND_ADD_GIFT = 3
|
||||
const COMMAND_ADD_MEMBER = 4
|
||||
const COMMAND_ADD_SUPER_CHAT = 5
|
||||
const COMMAND_DEL_SUPER_CHAT = 6
|
||||
const COMMAND_UPDATE_TRANSLATION = 7
|
||||
|
||||
// const CONTENT_TYPE_TEXT = 0
|
||||
const CONTENT_TYPE_EMOTICON = 1
|
||||
|
||||
const RECEIVE_TIMEOUT = 15 * 1000
|
||||
|
||||
export default class ChatClientRelay {
|
||||
constructor(roomId, autoTranslate) {
|
||||
this.roomId = roomId
|
||||
this.autoTranslate = autoTranslate
|
||||
|
||||
this.onAddText = null
|
||||
this.onAddGift = null
|
||||
this.onAddMember = null
|
||||
this.onAddSuperChat = null
|
||||
this.onDelSuperChat = null
|
||||
this.onUpdateTranslation = null
|
||||
|
||||
this.websocket = null
|
||||
this.retryCount = 0
|
||||
this.isDestroying = false
|
||||
this.receiveTimeoutTimerId = null
|
||||
}
|
||||
|
||||
start() {
|
||||
this.wsConnect()
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isDestroying = true
|
||||
if (this.websocket) {
|
||||
this.websocket.close()
|
||||
}
|
||||
}
|
||||
|
||||
wsConnect() {
|
||||
if (this.isDestroying) {
|
||||
return
|
||||
}
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const url = `${protocol}://${window.location.host}/api/chat`
|
||||
this.websocket = new WebSocket(url)
|
||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||
this.websocket.onclose = this.onWsClose.bind(this)
|
||||
this.websocket.onmessage = this.onWsMessage.bind(this)
|
||||
}
|
||||
|
||||
onWsOpen() {
|
||||
this.retryCount = 0
|
||||
this.websocket.send(JSON.stringify({
|
||||
cmd: COMMAND_JOIN_ROOM,
|
||||
data: {
|
||||
roomId: this.roomId,
|
||||
config: {
|
||||
autoTranslate: this.autoTranslate
|
||||
}
|
||||
}
|
||||
}))
|
||||
this.refreshReceiveTimeoutTimer()
|
||||
}
|
||||
|
||||
refreshReceiveTimeoutTimer() {
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||
}
|
||||
this.receiveTimeoutTimerId = window.setTimeout(this.onReceiveTimeout.bind(this), RECEIVE_TIMEOUT)
|
||||
}
|
||||
|
||||
onReceiveTimeout() {
|
||||
console.warn('接收消息超时')
|
||||
this.receiveTimeoutTimerId = null
|
||||
|
||||
// 直接丢弃阻塞的websocket,不等onclose回调了
|
||||
this.websocket.onopen = this.websocket.onclose = this.websocket.onmessage = null
|
||||
this.websocket.close()
|
||||
this.onWsClose()
|
||||
}
|
||||
|
||||
onWsClose() {
|
||||
this.websocket = null
|
||||
if (this.receiveTimeoutTimerId) {
|
||||
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||
this.receiveTimeoutTimerId = null
|
||||
}
|
||||
|
||||
if (this.isDestroying) {
|
||||
return
|
||||
}
|
||||
console.warn(`掉线重连中${++this.retryCount}`)
|
||||
window.setTimeout(this.wsConnect.bind(this), 1000)
|
||||
}
|
||||
|
||||
onWsMessage(event) {
|
||||
let { cmd, data } = JSON.parse(event.data)
|
||||
switch (cmd) {
|
||||
case COMMAND_HEARTBEAT: {
|
||||
this.refreshReceiveTimeoutTimer()
|
||||
|
||||
// 不能由定时器触发发心跳包,因为浏览器会把不活动页面的定时器调到1分钟以上
|
||||
this.websocket.send(JSON.stringify({
|
||||
cmd: COMMAND_HEARTBEAT
|
||||
}))
|
||||
break
|
||||
}
|
||||
case COMMAND_ADD_TEXT: {
|
||||
if (!this.onAddText) {
|
||||
break
|
||||
}
|
||||
|
||||
let emoticon = null
|
||||
let contentType = data[13]
|
||||
let contentTypeParams = data[14]
|
||||
if (contentType === CONTENT_TYPE_EMOTICON) {
|
||||
emoticon = contentTypeParams[0]
|
||||
}
|
||||
|
||||
data = {
|
||||
avatarUrl: data[0],
|
||||
timestamp: data[1],
|
||||
authorName: data[2],
|
||||
authorType: data[3],
|
||||
content: data[4],
|
||||
privilegeType: data[5],
|
||||
isGiftDanmaku: Boolean(data[6]),
|
||||
authorLevel: data[7],
|
||||
isNewbie: Boolean(data[8]),
|
||||
isMobileVerified: Boolean(data[9]),
|
||||
medalLevel: data[10],
|
||||
id: data[11],
|
||||
translation: data[12],
|
||||
emoticon: emoticon,
|
||||
textEmoticons: data[15],
|
||||
}
|
||||
this.onAddText(data)
|
||||
break
|
||||
}
|
||||
case COMMAND_ADD_GIFT: {
|
||||
if (this.onAddGift) {
|
||||
this.onAddGift(data)
|
||||
}
|
||||
break
|
||||
}
|
||||
case COMMAND_ADD_MEMBER: {
|
||||
if (this.onAddMember) {
|
||||
this.onAddMember(data)
|
||||
}
|
||||
break
|
||||
}
|
||||
case COMMAND_ADD_SUPER_CHAT: {
|
||||
if (this.onAddSuperChat) {
|
||||
this.onAddSuperChat(data)
|
||||
}
|
||||
break
|
||||
}
|
||||
case COMMAND_DEL_SUPER_CHAT: {
|
||||
if (this.onDelSuperChat) {
|
||||
this.onDelSuperChat(data)
|
||||
}
|
||||
break
|
||||
}
|
||||
case COMMAND_UPDATE_TRANSLATION: {
|
||||
if (!this.onUpdateTranslation) {
|
||||
break
|
||||
}
|
||||
data = {
|
||||
id: data[0],
|
||||
translation: data[1]
|
||||
}
|
||||
this.onUpdateTranslation(data)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
287
frontend/src/api/chat/ChatClientTest.js
Normal file
@@ -0,0 +1,287 @@
|
||||
import { getUuid4Hex } from '@/utils'
|
||||
import * as constants from '@/components/ChatRenderer/constants'
|
||||
import * as avatar from './avatar'
|
||||
|
||||
const NAMES = [
|
||||
'光羊',
|
||||
'黑炎',
|
||||
'孤梦星影',
|
||||
'博丽灵梦',
|
||||
'雾雨魔理沙',
|
||||
'空條承太郎',
|
||||
'ディオ・ブランドー',
|
||||
'ジョセフ・ジョースター',
|
||||
'ジョナサン・ジョースター',
|
||||
'Simon',
|
||||
'Misty',
|
||||
'Kinori',
|
||||
'shugen',
|
||||
'3Shain',
|
||||
'yuyuyzl',
|
||||
'xfgryujk',
|
||||
'Il Harper',
|
||||
'Rick Astley',
|
||||
]
|
||||
|
||||
const CONTENTS = [
|
||||
'草',
|
||||
'让我看看',
|
||||
'不要停下来啊',
|
||||
'我不做人了,JOJO',
|
||||
'已经没有什么好怕的了',
|
||||
'我柜子动了,我不玩了',
|
||||
'老板大气,老板身体健康',
|
||||
'我醉提酒游寒山,爽滑慢舔',
|
||||
'無駄無駄無駄無駄無駄無駄無駄無駄',
|
||||
'欧啦欧啦欧啦欧啦欧啦欧啦欧啦欧啦',
|
||||
'所有没好全部康复呀,我的癌也全部康复呀',
|
||||
'嚯,朝我走过来了吗,没有选择逃跑而是主动接近我么',
|
||||
'有一说一,这件事大家懂的都懂,不懂的,说了你也不明白,不如不说',
|
||||
'如来来了吗?如来嘛~他真来了吗?如~来~到底来没来?如来~如来他真来了吗?如来~你看看,来没来?如~来~',
|
||||
'迷えば、敗れる',
|
||||
'逃げるんだよォ!',
|
||||
'竜神の剣を喰らえ!',
|
||||
'竜が我が敌を喰らう!',
|
||||
'言いたいことがあるんだよ!',
|
||||
'知らず知らず隠してた 本当の声を響かせてよほら',
|
||||
'kksk',
|
||||
'8888888888',
|
||||
'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',
|
||||
'Farewell, ashen one. May the flame guide thee',
|
||||
'I am the bone of my sword. Steel is my body, and fire is my blood.',
|
||||
]
|
||||
|
||||
const EMOTICONS = [
|
||||
'/static/img/emoticons/233.png',
|
||||
'/static/img/emoticons/miaoa.png',
|
||||
'/static/img/emoticons/lipu.png'
|
||||
]
|
||||
|
||||
const AUTHOR_TYPES = [
|
||||
{ weight: 10, value: constants.AUTHRO_TYPE_NORMAL },
|
||||
{ weight: 5, value: constants.AUTHRO_TYPE_MEMBER },
|
||||
{ weight: 2, value: constants.AUTHRO_TYPE_ADMIN },
|
||||
{ weight: 1, value: constants.AUTHRO_TYPE_OWNER }
|
||||
]
|
||||
|
||||
function randGuardInfo() {
|
||||
let authorType = randomChoose(AUTHOR_TYPES)
|
||||
let privilegeType
|
||||
if (authorType === constants.AUTHRO_TYPE_MEMBER) {
|
||||
privilegeType = randInt(1, 3)
|
||||
} else if (authorType === constants.AUTHRO_TYPE_ADMIN) {
|
||||
privilegeType = randInt(0, 3)
|
||||
} else {
|
||||
privilegeType = 0
|
||||
}
|
||||
return { authorType, privilegeType }
|
||||
}
|
||||
|
||||
const GIFT_INFO_LIST = [
|
||||
{ giftName: 'B坷垃', totalCoin: 9900 },
|
||||
{ giftName: '礼花', totalCoin: 28000 },
|
||||
{ giftName: '花式夸夸', totalCoin: 39000 },
|
||||
{ giftName: '天空之翼', totalCoin: 100000 },
|
||||
{ giftName: '摩天大楼', totalCoin: 450000 },
|
||||
{ giftName: '小电视飞船', totalCoin: 1245000 }
|
||||
]
|
||||
|
||||
const SC_PRICES = [
|
||||
30, 50, 100, 200, 500, 1000
|
||||
]
|
||||
|
||||
const MESSAGE_GENERATORS = [
|
||||
// 文字
|
||||
{
|
||||
weight: 20,
|
||||
value() {
|
||||
return {
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
message: {
|
||||
...randGuardInfo(),
|
||||
avatarUrl: avatar.DEFAULT_AVATAR_URL,
|
||||
timestamp: new Date().getTime() / 1000,
|
||||
authorName: randomChoose(NAMES),
|
||||
content: randomChoose(CONTENTS),
|
||||
isGiftDanmaku: randInt(1, 10) <= 1,
|
||||
authorLevel: randInt(0, 60),
|
||||
isNewbie: randInt(1, 10) <= 1,
|
||||
isMobileVerified: randInt(1, 10) <= 9,
|
||||
medalLevel: randInt(0, 40),
|
||||
id: getUuid4Hex(),
|
||||
translation: '',
|
||||
emoticon: null,
|
||||
textEmoticons: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 表情
|
||||
{
|
||||
weight: 5,
|
||||
value() {
|
||||
return {
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
message: {
|
||||
...randGuardInfo(),
|
||||
avatarUrl: avatar.DEFAULT_AVATAR_URL,
|
||||
timestamp: new Date().getTime() / 1000,
|
||||
authorName: randomChoose(NAMES),
|
||||
content: '',
|
||||
isGiftDanmaku: false,
|
||||
authorLevel: randInt(0, 60),
|
||||
isNewbie: randInt(1, 10) <= 1,
|
||||
isMobileVerified: randInt(1, 10) <= 9,
|
||||
medalLevel: randInt(0, 40),
|
||||
id: getUuid4Hex(),
|
||||
translation: '',
|
||||
emoticon: randomChoose(EMOTICONS),
|
||||
textEmoticons: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 礼物
|
||||
{
|
||||
weight: 1,
|
||||
value() {
|
||||
return {
|
||||
type: constants.MESSAGE_TYPE_GIFT,
|
||||
message: {
|
||||
...randomChoose(GIFT_INFO_LIST),
|
||||
id: getUuid4Hex(),
|
||||
avatarUrl: avatar.DEFAULT_AVATAR_URL,
|
||||
timestamp: new Date().getTime() / 1000,
|
||||
authorName: randomChoose(NAMES),
|
||||
num: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// SC
|
||||
{
|
||||
weight: 3,
|
||||
value() {
|
||||
return {
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
message: {
|
||||
id: getUuid4Hex(),
|
||||
avatarUrl: avatar.DEFAULT_AVATAR_URL,
|
||||
timestamp: new Date().getTime() / 1000,
|
||||
authorName: randomChoose(NAMES),
|
||||
price: randomChoose(SC_PRICES),
|
||||
content: randomChoose(CONTENTS),
|
||||
translation: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 新舰长
|
||||
{
|
||||
weight: 1,
|
||||
value() {
|
||||
return {
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
message: {
|
||||
id: getUuid4Hex(),
|
||||
avatarUrl: avatar.DEFAULT_AVATAR_URL,
|
||||
timestamp: new Date().getTime() / 1000,
|
||||
authorName: randomChoose(NAMES),
|
||||
privilegeType: randInt(1, 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function randomChoose(nodes) {
|
||||
if (nodes.length === 0) {
|
||||
return null
|
||||
}
|
||||
for (let node of nodes) {
|
||||
if (node.weight === undefined || node.value === undefined) {
|
||||
return nodes[randInt(0, nodes.length - 1)]
|
||||
}
|
||||
}
|
||||
|
||||
let totalWeight = 0
|
||||
for (let node of nodes) {
|
||||
totalWeight += node.weight
|
||||
}
|
||||
let remainWeight = randInt(1, totalWeight)
|
||||
for (let node of nodes) {
|
||||
remainWeight -= node.weight
|
||||
if (remainWeight > 0) {
|
||||
continue
|
||||
}
|
||||
if (node.value instanceof Array) {
|
||||
return randomChoose(node.value)
|
||||
}
|
||||
return node.value
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function randInt(min, max) {
|
||||
return Math.floor(min + ((max - min + 1) * Math.random()))
|
||||
}
|
||||
|
||||
export default class ChatClientTest {
|
||||
constructor() {
|
||||
this.onAddText = null
|
||||
this.onAddGift = null
|
||||
this.onAddMember = null
|
||||
this.onAddSuperChat = null
|
||||
this.onDelSuperChat = null
|
||||
this.onUpdateTranslation = null
|
||||
|
||||
this.timerId = null
|
||||
}
|
||||
|
||||
start() {
|
||||
this.refreshTimer()
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.timerId) {
|
||||
window.clearTimeout(this.timerId)
|
||||
this.timerId = null
|
||||
}
|
||||
}
|
||||
|
||||
refreshTimer() {
|
||||
// 模仿B站的消息间隔模式
|
||||
let sleepTime
|
||||
if (randInt(0, 4) == 0) {
|
||||
sleepTime = randInt(1000, 2000)
|
||||
} else {
|
||||
sleepTime = randInt(0, 400)
|
||||
}
|
||||
this.timerId = window.setTimeout(this.onTimeout.bind(this), sleepTime)
|
||||
}
|
||||
|
||||
onTimeout() {
|
||||
this.refreshTimer()
|
||||
|
||||
let { type, message } = randomChoose(MESSAGE_GENERATORS)()
|
||||
switch (type) {
|
||||
case constants.MESSAGE_TYPE_TEXT:
|
||||
this.onAddText(message)
|
||||
break
|
||||
case constants.MESSAGE_TYPE_GIFT:
|
||||
this.onAddGift(message)
|
||||
break
|
||||
case constants.MESSAGE_TYPE_MEMBER:
|
||||
this.onAddMember(message)
|
||||
break
|
||||
case constants.MESSAGE_TYPE_SUPER_CHAT:
|
||||
this.onAddSuperChat(message)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
28
frontend/src/api/chat/avatar.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const DEFAULT_AVATAR_URL = '//static.hdslb.com/images/member/noface.gif'
|
||||
|
||||
export function processAvatarUrl(avatarUrl) {
|
||||
// 去掉协议,兼容HTTP、HTTPS
|
||||
let m = avatarUrl.match(/(?:https?:)?(.*)/)
|
||||
if (m) {
|
||||
avatarUrl = m[1]
|
||||
}
|
||||
// 缩小图片加快传输
|
||||
if (!avatarUrl.endsWith('noface.gif')) {
|
||||
avatarUrl += '@48w_48h'
|
||||
}
|
||||
return avatarUrl
|
||||
}
|
||||
|
||||
export async function getAvatarUrl(uid) {
|
||||
let res
|
||||
try {
|
||||
res = (await axios.get('/api/avatar_url', { params: {
|
||||
uid: uid
|
||||
} })).data
|
||||
} catch {
|
||||
return DEFAULT_AVATAR_URL
|
||||
}
|
||||
return res.avatarUrl
|
||||
}
|
||||
68
frontend/src/api/chatConfig.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
import { mergeConfig } from '@/utils'
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
minGiftPrice: 7, // $1
|
||||
showDanmaku: true,
|
||||
showGift: true,
|
||||
showGiftName: false,
|
||||
mergeSimilarDanmaku: false,
|
||||
mergeGift: true,
|
||||
maxNumber: 60,
|
||||
|
||||
blockGiftDanmaku: true,
|
||||
blockLevel: 0,
|
||||
blockNewbie: false,
|
||||
blockNotMobileVerified: false,
|
||||
blockKeywords: '',
|
||||
blockUsers: '',
|
||||
blockMedalLevel: 0,
|
||||
|
||||
relayMessagesByServer: false,
|
||||
autoTranslate: false,
|
||||
giftUsernamePronunciation: '',
|
||||
|
||||
emoticons: [] // [{ keyword: '', url: '' }, ...]
|
||||
}
|
||||
|
||||
export function deepCloneDefaultConfig() {
|
||||
return _.cloneDeep(DEFAULT_CONFIG)
|
||||
}
|
||||
|
||||
export function setLocalConfig(config) {
|
||||
config = mergeConfig(config, DEFAULT_CONFIG)
|
||||
window.localStorage.config = JSON.stringify(config)
|
||||
}
|
||||
|
||||
export function getLocalConfig() {
|
||||
try {
|
||||
let config = JSON.parse(window.localStorage.config)
|
||||
config = mergeConfig(config, deepCloneDefaultConfig())
|
||||
sanitizeConfig(config)
|
||||
return config
|
||||
} catch {
|
||||
return deepCloneDefaultConfig()
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeConfig(config) {
|
||||
let newEmoticons = []
|
||||
if (config.emoticons instanceof Array) {
|
||||
for (let emoticon of config.emoticons) {
|
||||
try {
|
||||
let newEmoticon = {
|
||||
keyword: emoticon.keyword,
|
||||
url: emoticon.url
|
||||
}
|
||||
if ((typeof newEmoticon.keyword !== 'string') || (typeof newEmoticon.url !== 'string')) {
|
||||
continue
|
||||
}
|
||||
newEmoticons.push(newEmoticon)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
config.emoticons = newEmoticons
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import {mergeConfig} from '@/utils'
|
||||
import stylegen from '@/views/StyleGenerator/stylegen'
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
minGiftPrice: 6.911, // $1
|
||||
mergeSimilarDanmaku: true,
|
||||
showDanmaku: true,
|
||||
showGift: true,
|
||||
maxSpeed: 4,
|
||||
|
||||
blockGiftDanmaku: true,
|
||||
blockLevel: 0,
|
||||
blockNewbie: true,
|
||||
blockNotMobileVerified: true,
|
||||
blockKeywords: '',
|
||||
blockUsers: '',
|
||||
blockMedalLevel: 0,
|
||||
|
||||
css: stylegen.getStyle(stylegen.DEFAULT_CONFIG)
|
||||
}
|
||||
|
||||
export function setLocalConfig (config) {
|
||||
config = mergeConfig(config, DEFAULT_CONFIG)
|
||||
window.localStorage.config = JSON.stringify(config)
|
||||
}
|
||||
|
||||
export function getLocalConfig () {
|
||||
if (!window.localStorage.config) {
|
||||
return DEFAULT_CONFIG
|
||||
}
|
||||
return mergeConfig(JSON.parse(window.localStorage.config), DEFAULT_CONFIG)
|
||||
}
|
||||
|
||||
export async function createRemoteConfig (config) {
|
||||
config = mergeConfig(config, DEFAULT_CONFIG)
|
||||
return (await axios.post('/config', config)).data
|
||||
}
|
||||
|
||||
export async function setRemoteConfig (id, config) {
|
||||
config = mergeConfig(config, DEFAULT_CONFIG)
|
||||
return (await axios.put(`/config/${id}`, config)).data
|
||||
}
|
||||
|
||||
export async function getRemoteConfig (id) {
|
||||
let config = (await axios.get(`/config/${id}`)).data
|
||||
return mergeConfig(config, DEFAULT_CONFIG)
|
||||
}
|
||||
|
||||
export default {
|
||||
DEFAULT_CONFIG,
|
||||
setLocalConfig,
|
||||
getLocalConfig,
|
||||
createRemoteConfig,
|
||||
setRemoteConfig,
|
||||
getRemoteConfig
|
||||
}
|
||||
11
frontend/src/api/main.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export async function getServerInfo() {
|
||||
return (await axios.get('/api/server_info')).data
|
||||
}
|
||||
|
||||
export async function uploadEmoticon(file) {
|
||||
let body = new FormData()
|
||||
body.set('file', file)
|
||||
return (await axios.post('/api/emoticon', body)).data
|
||||
}
|
||||
362
frontend/src/assets/css/youtube/yt-html.css
Normal file
@@ -0,0 +1,362 @@
|
||||
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);
|
||||
--yt-live-chat-action-panel-background-color-transparent: hsla(0, 0%, 97%, .8);
|
||||
--yt-live-chat-mode-change-background-color: hsla(0, 0%, 93.3%, .4);
|
||||
--yt-live-chat-primary-text-color: hsl(0, 0%, 6.7%);
|
||||
--yt-live-chat-secondary-text-color: hsla(0, 0%, 6.7%, .6);
|
||||
--yt-live-chat-tertiary-text-color: hsla(0, 0%, 6.7%, .4);
|
||||
--yt-live-chat-text-input-field-inactive-underline-color: #b8b8b8;
|
||||
--yt-live-chat-text-input-field-placeholder-color: hsla(0, 0%, 6.7%, .6);
|
||||
--yt-live-chat-icon-button-color: hsla(0, 0%, 6.7%, .4);
|
||||
--yt-live-chat-enabled-send-button-color: #4285f4;
|
||||
--yt-live-chat-disabled-icon-button-color: hsla(0, 0%, 6.7%, .2);
|
||||
--yt-live-chat-picker-button-color: hsla(0, 0%, 6.7%, .4);
|
||||
--yt-live-chat-picker-button-active-color: hsla(0, 0%, 6.7%, .8);
|
||||
--yt-live-chat-picker-button-disabled-color: var(--yt-live-chat-disabled-icon-button-color);
|
||||
--yt-live-chat-picker-button-hover-color: hsla(0, 0%, 6.7%, .6);
|
||||
--yt-live-chat-mention-background-color: #ff5722;
|
||||
--yt-live-chat-mention-text-color: hsl(0, 0%, 100%);
|
||||
--yt-live-chat-deleted-message-color: rgba(0, 0, 0, .5);
|
||||
--yt-live-chat-deleted-message-bar-color: rgba(11, 11, 11, .2);
|
||||
--yt-live-chat-disabled-button-background-color: hsl(0, 0%, 93.3%);
|
||||
--yt-live-chat-disabled-button-text-color: hsla(0, 0%, 6.7%, .4);
|
||||
--yt-live-chat-sub-panel-background-color: hsl(0, 0%, 93.3%);
|
||||
--yt-live-chat-sub-panel-background-color-transparent: hsla(0, 0%, 93%, .7);
|
||||
--yt-live-chat-header-background-color: hsla(0, 0%, 93.3%, .4);
|
||||
--yt-live-chat-header-button-color: hsl(0, 0%, 6.7%);
|
||||
--yt-live-chat-error-message-color: hsl(10, 51%, 49%);
|
||||
--yt-live-chat-reconnect-message-color: hsla(0, 0%, 7%, 0.2);
|
||||
--yt-live-chat-moderator-color: hsl(225, 84%, 66%);
|
||||
--yt-live-chat-owner-color: hsl(40, 76%, 55%);
|
||||
--yt-live-chat-author-chip-owner-text-color: rgba(0,0,0,0.87);
|
||||
--yt-live-chat-author-chip-verified-background-color: #CCCCCC;
|
||||
--yt-live-chat-author-chip-verified-text-color: #606060;
|
||||
--yt-live-chat-message-highlight-background-color: #f8f8f8;
|
||||
--yt-live-chat-sponsor-color: #107516;
|
||||
--yt-live-chat-overlay-color: hsla(0, 0%, 0%, 0.6);
|
||||
--yt-live-chat-dialog-background-color: hsl(0, 0%, 100%);
|
||||
--yt-live-chat-dialog-text-color: hsla(0, 0%, 6.7%, .6);
|
||||
--yt-live-chat-poll-choice-text-color: var(--yt-spec-text-secondary);
|
||||
--yt-live-chat-poll-choice-border-color: var(--yt-spec-10-percent-layer);
|
||||
--yt-live-chat-poll-choice-vote-bar-background-color: hsla(0, 0%, 93.3%, .8);
|
||||
--yt-live-chat-poll-choice-vote-bar-background-color-selected: #F2F8FF;
|
||||
--yt-live-chat-poll-choice-color-selected: #065FD4;
|
||||
--yt-live-chat-moderation-mode-hover-background-color: hsla(0, 0%, 6.7%, .2);
|
||||
--yt-live-chat-additional-inline-action-button-color: hsl(0, 0%, 100%);
|
||||
--yt-live-chat-additional-inline-action-button-background-color: hsla(0, 0%, 26%, 0.8);
|
||||
--yt-live-chat-additional-inline-action-button-background-color-hover: hsla(0, 0%, 26%, 1.0);
|
||||
--yt-formatted-string-emoji-size: 24px;
|
||||
--yt-live-chat-emoji-size: 24px;
|
||||
--yt-live-chat-text-input-field-suggestion-background-color: hsl(0, 0%, 100%);
|
||||
--yt-live-chat-text-input-field-suggestion-background-color-hover: #eee;
|
||||
--yt-live-chat-text-input-field-suggestion-text-color: #666;
|
||||
--yt-live-chat-text-input-field-suggestion-text-color-hover: #333;
|
||||
--yt-live-chat-ticker-arrow-background: hsl(0, 0%, 97.3%);
|
||||
--yt-emoji-picker-category-background-color: var(--yt-live-chat-action-panel-background-color-transparent);
|
||||
--yt-emoji-picker-category-color: var(--yt-live-chat-secondary-text-color);
|
||||
--yt-emoji-picker-category-button-color: var(--yt-live-chat-picker-button-color);
|
||||
--yt-emoji-picker-search-background-color: hsla(0, 0%, 100%, .6);
|
||||
--yt-emoji-picker-search-color: hsla(0, 0%, 6.7%, .8);
|
||||
--yt-emoji-picker-search-placeholder-color: hsla(0, 0%, 6.7%, .6);
|
||||
--yt-live-chat-slider-active-color: #2196f3;
|
||||
--yt-live-chat-slider-container-color: #c8c8c8;
|
||||
--yt-live-chat-slider-markers-color: #505050;
|
||||
--yt-live-chat-toast-background-color: hsl(0, 0%, 20%);
|
||||
--yt-live-chat-toast-text-color: hsl(0, 0%, 100%);
|
||||
--yt-live-chat-automod-button-background-color: hsl(0, 0%, 93.3%);
|
||||
--yt-live-chat-automod-button-background-color-hover: hsla(0, 0%, 6.7%, .2);
|
||||
--yt-live-chat-countdown-opacity: 0.3;
|
||||
--yt-live-chat-shimmer-background-color: rgba(136, 136, 136, 0.2);
|
||||
--yt-live-chat-shimmer-linear-gradient: linear-gradient(0deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0) 65%);
|
||||
--yt-live-chat-vem-background-color: hsl(0, 0%, 93.3%);
|
||||
--yt-live-chat-upsell-dialog-renderer-button-padding: 10px 16px;
|
||||
--yt-live-chat-product-picker-icon-color: rgba(17, 17, 17, 0.6);
|
||||
--yt-live-chat-product-picker-hover-color: rgba(17, 17, 16, 0.1);
|
||||
--yt-live-chat-product-picker-disabled-icon-color: rgba(17, 17, 17, 0.4);
|
||||
--yt-pdg-paid-stickers-tab-selection-bar-color: #065FD4;
|
||||
--yt-pdg-paid-stickers-author-name-font-size: 13px;
|
||||
--yt-pdg-paid-stickers-margin-left: 38px;
|
||||
}
|
||||
|
||||
html:not(.style-scope) {
|
||||
--layout_-_display: flex;
|
||||
;
|
||||
|
||||
--layout-inline_-_display: inline-flex;
|
||||
;
|
||||
|
||||
--layout-horizontal_-_display: var(--layout_-_display);
|
||||
--layout-horizontal_-_-ms-flex-direction: row;
|
||||
--layout-horizontal_-_-webkit-flex-direction: row;
|
||||
--layout-horizontal_-_flex-direction: row;
|
||||
;
|
||||
|
||||
--layout-horizontal-reverse_-_display: var(--layout_-_display);
|
||||
--layout-horizontal-reverse_-_-ms-flex-direction: row-reverse;
|
||||
--layout-horizontal-reverse_-_-webkit-flex-direction: row-reverse;
|
||||
--layout-horizontal-reverse_-_flex-direction: row-reverse;
|
||||
;
|
||||
|
||||
--layout-vertical_-_display: var(--layout_-_display);
|
||||
--layout-vertical_-_-ms-flex-direction: column;
|
||||
--layout-vertical_-_-webkit-flex-direction: column;
|
||||
--layout-vertical_-_flex-direction: column;
|
||||
;
|
||||
|
||||
--layout-vertical-reverse_-_display: var(--layout_-_display);
|
||||
--layout-vertical-reverse_-_-ms-flex-direction: column-reverse;
|
||||
--layout-vertical-reverse_-_-webkit-flex-direction: column-reverse;
|
||||
--layout-vertical-reverse_-_flex-direction: column-reverse;
|
||||
;
|
||||
|
||||
--layout-wrap_-_-ms-flex-wrap: wrap;
|
||||
--layout-wrap_-_-webkit-flex-wrap: wrap;
|
||||
--layout-wrap_-_flex-wrap: wrap;
|
||||
;
|
||||
|
||||
--layout-wrap-reverse_-_-ms-flex-wrap: wrap-reverse;
|
||||
--layout-wrap-reverse_-_-webkit-flex-wrap: wrap-reverse;
|
||||
--layout-wrap-reverse_-_flex-wrap: wrap-reverse;
|
||||
;
|
||||
|
||||
--layout-flex-auto_-_-ms-flex: 1 1 auto;
|
||||
--layout-flex-auto_-_-webkit-flex: 1 1 auto;
|
||||
--layout-flex-auto_-_flex: 1 1 auto;
|
||||
;
|
||||
|
||||
--layout-flex-none_-_-ms-flex: none;
|
||||
--layout-flex-none_-_-webkit-flex: none;
|
||||
--layout-flex-none_-_flex: none;
|
||||
;
|
||||
|
||||
--layout-flex_-_-ms-flex: 1 1 0.000000001px;
|
||||
--layout-flex_-_-webkit-flex: 1;
|
||||
--layout-flex_-_flex: 1;
|
||||
--layout-flex_-_-webkit-flex-basis: 0.000000001px;
|
||||
--layout-flex_-_flex-basis: 0.000000001px;
|
||||
;
|
||||
|
||||
--layout-flex-2_-_-ms-flex: 2;
|
||||
--layout-flex-2_-_-webkit-flex: 2;
|
||||
--layout-flex-2_-_flex: 2;
|
||||
;
|
||||
|
||||
--layout-flex-3_-_-ms-flex: 3;
|
||||
--layout-flex-3_-_-webkit-flex: 3;
|
||||
--layout-flex-3_-_flex: 3;
|
||||
;
|
||||
|
||||
--layout-flex-4_-_-ms-flex: 4;
|
||||
--layout-flex-4_-_-webkit-flex: 4;
|
||||
--layout-flex-4_-_flex: 4;
|
||||
;
|
||||
|
||||
--layout-flex-5_-_-ms-flex: 5;
|
||||
--layout-flex-5_-_-webkit-flex: 5;
|
||||
--layout-flex-5_-_flex: 5;
|
||||
;
|
||||
|
||||
--layout-flex-6_-_-ms-flex: 6;
|
||||
--layout-flex-6_-_-webkit-flex: 6;
|
||||
--layout-flex-6_-_flex: 6;
|
||||
;
|
||||
|
||||
--layout-flex-7_-_-ms-flex: 7;
|
||||
--layout-flex-7_-_-webkit-flex: 7;
|
||||
--layout-flex-7_-_flex: 7;
|
||||
;
|
||||
|
||||
--layout-flex-8_-_-ms-flex: 8;
|
||||
--layout-flex-8_-_-webkit-flex: 8;
|
||||
--layout-flex-8_-_flex: 8;
|
||||
;
|
||||
|
||||
--layout-flex-9_-_-ms-flex: 9;
|
||||
--layout-flex-9_-_-webkit-flex: 9;
|
||||
--layout-flex-9_-_flex: 9;
|
||||
;
|
||||
|
||||
--layout-flex-10_-_-ms-flex: 10;
|
||||
--layout-flex-10_-_-webkit-flex: 10;
|
||||
--layout-flex-10_-_flex: 10;
|
||||
;
|
||||
|
||||
--layout-flex-11_-_-ms-flex: 11;
|
||||
--layout-flex-11_-_-webkit-flex: 11;
|
||||
--layout-flex-11_-_flex: 11;
|
||||
;
|
||||
|
||||
--layout-flex-12_-_-ms-flex: 12;
|
||||
--layout-flex-12_-_-webkit-flex: 12;
|
||||
--layout-flex-12_-_flex: 12;
|
||||
;
|
||||
|
||||
|
||||
|
||||
--layout-start_-_-ms-flex-align: start;
|
||||
--layout-start_-_-webkit-align-items: flex-start;
|
||||
--layout-start_-_align-items: flex-start;
|
||||
;
|
||||
|
||||
--layout-center_-_-ms-flex-align: center;
|
||||
--layout-center_-_-webkit-align-items: center;
|
||||
--layout-center_-_align-items: center;
|
||||
;
|
||||
|
||||
--layout-end_-_-ms-flex-align: end;
|
||||
--layout-end_-_-webkit-align-items: flex-end;
|
||||
--layout-end_-_align-items: flex-end;
|
||||
;
|
||||
|
||||
--layout-baseline_-_-ms-flex-align: baseline;
|
||||
--layout-baseline_-_-webkit-align-items: baseline;
|
||||
--layout-baseline_-_align-items: baseline;
|
||||
;
|
||||
|
||||
|
||||
|
||||
--layout-start-justified_-_-ms-flex-pack: start;
|
||||
--layout-start-justified_-_-webkit-justify-content: flex-start;
|
||||
--layout-start-justified_-_justify-content: flex-start;
|
||||
;
|
||||
|
||||
--layout-center-justified_-_-ms-flex-pack: center;
|
||||
--layout-center-justified_-_-webkit-justify-content: center;
|
||||
--layout-center-justified_-_justify-content: center;
|
||||
;
|
||||
|
||||
--layout-end-justified_-_-ms-flex-pack: end;
|
||||
--layout-end-justified_-_-webkit-justify-content: flex-end;
|
||||
--layout-end-justified_-_justify-content: flex-end;
|
||||
;
|
||||
|
||||
--layout-around-justified_-_-ms-flex-pack: distribute;
|
||||
--layout-around-justified_-_-webkit-justify-content: space-around;
|
||||
--layout-around-justified_-_justify-content: space-around;
|
||||
;
|
||||
|
||||
--layout-justified_-_-ms-flex-pack: justify;
|
||||
--layout-justified_-_-webkit-justify-content: space-between;
|
||||
--layout-justified_-_justify-content: space-between;
|
||||
;
|
||||
|
||||
--layout-center-center_-_-ms-flex-align: var(--layout-center_-_-ms-flex-align);
|
||||
--layout-center-center_-_-webkit-align-items: var(--layout-center_-_-webkit-align-items);
|
||||
--layout-center-center_-_align-items: var(--layout-center_-_align-items);
|
||||
--layout-center-center_-_-ms-flex-pack: var(--layout-center-justified_-_-ms-flex-pack);
|
||||
--layout-center-center_-_-webkit-justify-content: var(--layout-center-justified_-_-webkit-justify-content);
|
||||
--layout-center-center_-_justify-content: var(--layout-center-justified_-_justify-content);
|
||||
;
|
||||
|
||||
|
||||
|
||||
--layout-self-start_-_-ms-align-self: flex-start;
|
||||
--layout-self-start_-_-webkit-align-self: flex-start;
|
||||
--layout-self-start_-_align-self: flex-start;
|
||||
;
|
||||
|
||||
--layout-self-center_-_-ms-align-self: center;
|
||||
--layout-self-center_-_-webkit-align-self: center;
|
||||
--layout-self-center_-_align-self: center;
|
||||
;
|
||||
|
||||
--layout-self-end_-_-ms-align-self: flex-end;
|
||||
--layout-self-end_-_-webkit-align-self: flex-end;
|
||||
--layout-self-end_-_align-self: flex-end;
|
||||
;
|
||||
|
||||
--layout-self-stretch_-_-ms-align-self: stretch;
|
||||
--layout-self-stretch_-_-webkit-align-self: stretch;
|
||||
--layout-self-stretch_-_align-self: stretch;
|
||||
;
|
||||
|
||||
--layout-self-baseline_-_-ms-align-self: baseline;
|
||||
--layout-self-baseline_-_-webkit-align-self: baseline;
|
||||
--layout-self-baseline_-_align-self: baseline;
|
||||
;
|
||||
|
||||
|
||||
|
||||
--layout-start-aligned_-_-ms-flex-line-pack: start;
|
||||
--layout-start-aligned_-_-ms-align-content: flex-start;
|
||||
--layout-start-aligned_-_-webkit-align-content: flex-start;
|
||||
--layout-start-aligned_-_align-content: flex-start;
|
||||
;
|
||||
|
||||
--layout-end-aligned_-_-ms-flex-line-pack: end;
|
||||
--layout-end-aligned_-_-ms-align-content: flex-end;
|
||||
--layout-end-aligned_-_-webkit-align-content: flex-end;
|
||||
--layout-end-aligned_-_align-content: flex-end;
|
||||
;
|
||||
|
||||
--layout-center-aligned_-_-ms-flex-line-pack: center;
|
||||
--layout-center-aligned_-_-ms-align-content: center;
|
||||
--layout-center-aligned_-_-webkit-align-content: center;
|
||||
--layout-center-aligned_-_align-content: center;
|
||||
;
|
||||
|
||||
--layout-between-aligned_-_-ms-flex-line-pack: justify;
|
||||
--layout-between-aligned_-_-ms-align-content: space-between;
|
||||
--layout-between-aligned_-_-webkit-align-content: space-between;
|
||||
--layout-between-aligned_-_align-content: space-between;
|
||||
;
|
||||
|
||||
--layout-around-aligned_-_-ms-flex-line-pack: distribute;
|
||||
--layout-around-aligned_-_-ms-align-content: space-around;
|
||||
--layout-around-aligned_-_-webkit-align-content: space-around;
|
||||
--layout-around-aligned_-_align-content: space-around;
|
||||
;
|
||||
|
||||
|
||||
|
||||
--layout-block_-_display: block;
|
||||
;
|
||||
|
||||
--layout-invisible_-_visibility: hidden !important;
|
||||
;
|
||||
|
||||
--layout-relative_-_position: relative;
|
||||
;
|
||||
|
||||
--layout-fit_-_position: absolute;
|
||||
--layout-fit_-_top: 0;
|
||||
--layout-fit_-_right: 0;
|
||||
--layout-fit_-_bottom: 0;
|
||||
--layout-fit_-_left: 0;
|
||||
;
|
||||
|
||||
--layout-scroll_-_-webkit-overflow-scrolling: touch;
|
||||
--layout-scroll_-_overflow: auto;
|
||||
;
|
||||
|
||||
--layout-fullbleed_-_margin: 0;
|
||||
--layout-fullbleed_-_height: 100vh;
|
||||
;
|
||||
|
||||
|
||||
|
||||
--layout-fixed-top_-_position: fixed;
|
||||
--layout-fixed-top_-_top: 0;
|
||||
--layout-fixed-top_-_left: 0;
|
||||
--layout-fixed-top_-_right: 0;
|
||||
;
|
||||
|
||||
--layout-fixed-right_-_position: fixed;
|
||||
--layout-fixed-right_-_top: 0;
|
||||
--layout-fixed-right_-_right: 0;
|
||||
--layout-fixed-right_-_bottom: 0;
|
||||
;
|
||||
|
||||
--layout-fixed-bottom_-_position: fixed;
|
||||
--layout-fixed-bottom_-_right: 0;
|
||||
--layout-fixed-bottom_-_bottom: 0;
|
||||
--layout-fixed-bottom_-_left: 0;
|
||||
;
|
||||
|
||||
--layout-fixed-left_-_position: fixed;
|
||||
--layout-fixed-left_-_top: 0;
|
||||
--layout-fixed-left_-_bottom: 0;
|
||||
--layout-fixed-left_-_left: 0;
|
||||
;
|
||||
}
|
||||
30
frontend/src/assets/css/youtube/yt-icon.css
Normal file
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-icon[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-icon, .yt-icon-container.yt-icon {
|
||||
display: inline-flex;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
fill: currentcolor;
|
||||
stroke: none;
|
||||
width: var(--iron-icon-width, 24px);
|
||||
height: var(--iron-icon-height, 24px);
|
||||
}
|
||||
|
||||
yt-icon.external-container {
|
||||
display: none !important;
|
||||
}
|
||||
68
frontend/src/assets/css/youtube/yt-img-shadow.css
Normal file
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-img-shadow[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-img-shadow {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
yt-img-shadow.no-transition {
|
||||
opacity: 1;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
yt-img-shadow.with-placeholder {
|
||||
background-color: transparent;
|
||||
min-height: unset;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
yt-img-shadow[loaded] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
yt-img-shadow.empty img.yt-img-shadow {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
yt-img-shadow[object-fit="FILL"] img.yt-img-shadow, yt-img-shadow[fit] img.yt-img-shadow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
yt-img-shadow[object-fit="COVER"] img.yt-img-shadow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
yt-img-shadow[object-fit="CONTAIN"] img.yt-img-shadow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
yt-img-shadow[object-position="LEFT"] img.yt-img-shadow {
|
||||
object-position: left;
|
||||
}
|
||||
|
||||
img.yt-img-shadow {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-height: none;
|
||||
max-width: 100%;
|
||||
border-radius: none;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-author-badge-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer[type='moderator'] {
|
||||
color: var(--yt-live-chat-moderator-color, #5e84f1);
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer[type='owner'] {
|
||||
color: var(--yt-live-chat-owner-color, #ffd600);
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer[type='member'] {
|
||||
color: var(--yt-live-chat-sponsor-color, #107516);
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer[type='verified'] {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
img.yt-live-chat-author-badge-renderer, yt-icon.yt-live-chat-author-badge-renderer {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
64
frontend/src/assets/css/youtube/yt-live-chat-author-chip.css
Normal file
@@ -0,0 +1,64 @@
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-author-chip[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip {
|
||||
display: inline-flex;
|
||||
-ms-flex-align: baseline;
|
||||
-webkit-align-items: baseline;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
#author-name.yt-live-chat-author-chip {
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip[is-highlighted] #author-name.yt-live-chat-author-chip {
|
||||
padding: 2px 4px;
|
||||
color: var(--yt-live-chat-author-chip-verified-text-color);
|
||||
background-color: var(--yt-live-chat-author-chip-verified-background-color);
|
||||
}
|
||||
|
||||
#author-name.yt-live-chat-author-chip[type='moderator'] {
|
||||
color: var(--yt-live-chat-moderator-color);
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip[is-highlighted] #author-name.yt-live-chat-author-chip[type='owner'], #author-name.yt-live-chat-author-chip[type='owner'] {
|
||||
background-color: #ffd600;
|
||||
color: var(--yt-live-chat-author-chip-owner-text-color);
|
||||
}
|
||||
|
||||
#author-name.yt-live-chat-author-chip[type='member'] {
|
||||
color: var(--yt-live-chat-sponsor-color);
|
||||
}
|
||||
|
||||
#chip-badges.yt-live-chat-author-chip:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip[is-highlighted] #chat-badges.yt-live-chat-author-chip:not(:empty) {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer.yt-live-chat-author-chip {
|
||||
margin: 0 0 0 2px;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip[is-highlighted] #chip-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer.yt-live-chat-author-chip {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#chip-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer.yt-live-chat-author-chip:last-of-type {
|
||||
margin-right: -2px;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-item-list-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-item-list-renderer {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
yt-live-chat-item-list-renderer[moderation-mode-enabled] {
|
||||
--yt-live-chat-item-with-inline-actions-context-menu-display: none;
|
||||
--yt-live-chat-inline-action-button-container-display: flex;
|
||||
}
|
||||
|
||||
#contents.yt-live-chat-item-list-renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#empty-state-message.yt-live-chat-item-list-renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#empty-state-message.yt-live-chat-item-list-renderer>yt-live-chat-message-renderer.yt-live-chat-item-list-renderer {
|
||||
color: var(--yt-live-chat-tertiary-text-color);
|
||||
background: transparent;
|
||||
font-size: 18px;
|
||||
--yt-live-chat-message-renderer-text-align: center;
|
||||
}
|
||||
|
||||
yt-icon-button.yt-live-chat-item-list-renderer {
|
||||
background-color: #2196f3;
|
||||
border-radius: 999px;
|
||||
bottom: 0;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0 calc(50% - 16px) 8px calc(50% - 16px);
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
transition-property: bottom;
|
||||
transition-timing-function: cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||
transition-duration: 0.15s;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
yt-icon-button.yt-live-chat-item-list-renderer[disabled] {
|
||||
bottom: -42px;
|
||||
color: #fff;
|
||||
transition-timing-function: cubic-bezier(0.4, 0.0, 1, 1);
|
||||
}
|
||||
|
||||
#item-scroller.yt-live-chat-item-list-renderer {
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
padding-right: var(--scrollbar-width);
|
||||
}
|
||||
|
||||
yt-live-chat-item-list-renderer[allow-scroll] #item-scroller.yt-live-chat-item-list-renderer {
|
||||
overflow-y: scroll;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
#item-offset.yt-live-chat-item-list-renderer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#item-scroller.animated.yt-live-chat-item-list-renderer #item-offset.yt-live-chat-item-list-renderer {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#items.yt-live-chat-item-list-renderer {
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
padding: var(--yt-live-chat-item-list-renderer-padding, 4px 0);
|
||||
}
|
||||
|
||||
#items.yt-live-chat-item-list-renderer>*.yt-live-chat-item-list-renderer:not(:first-child) {
|
||||
border-top: var(--yt-live-chat-item-list-item-border, none);
|
||||
}
|
||||
|
||||
#item-scroller.animated.yt-live-chat-item-list-renderer #items.yt-live-chat-item-list-renderer {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
#docked-messages.yt-live-chat-item-list-renderer {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-sticker-renderer.yt-live-chat-item-list-renderer {
|
||||
padding: 4px 24px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-sticker-renderer.yt-live-chat-item-list-renderer[dashboard-money-feed] {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
#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);
|
||||
color: var(--yt-live-chat-tertiary-text-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-membership-item-renderer {
|
||||
display: block;
|
||||
margin-right: 16px;
|
||||
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
|
||||
-ms-flex: var(--layout-flex-none_-_-ms-flex);
|
||||
-webkit-flex: var(--layout-flex-none_-_-webkit-flex);
|
||||
flex: var(--layout-flex-none_-_flex);
|
||||
}
|
||||
|
||||
#menu-button.yt-live-chat-membership-item-renderer {
|
||||
width: var(--yt-live-chat-32px-icon-button_-_width);
|
||||
height: var(--yt-live-chat-32px-icon-button_-_height);
|
||||
padding: var(--yt-live-chat-32px-icon-button_-_padding);
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-membership-item-renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
|
||||
transform: translateX(100px);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer:hover #menu.yt-live-chat-membership-item-renderer,
|
||||
yt-live-chat-membership-item-renderer[menu-visible] #menu.yt-live-chat-membership-item-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer:focus-within #menu.yt-live-chat-membership-item-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#inline-action-button-container.yt-live-chat-membership-item-renderer {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 0;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
|
||||
background-color: var(--yt-live-chat-moderation-mode-hover-background-color);
|
||||
display: none;
|
||||
|
||||
-ms-flex-align: var(--layout-center-center_-_-ms-flex-align);
|
||||
-webkit-align-items: var(--layout-center-center_-_-webkit-align-items);
|
||||
align-items: var(--layout-center-center_-_align-items);
|
||||
-ms-flex-pack: var(--layout-center-center_-_-ms-flex-pack);
|
||||
-webkit-justify-content: var(--layout-center-center_-_-webkit-justify-content);
|
||||
justify-content: var(--layout-center-center_-_justify-content);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[has-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-membership-item-renderer {
|
||||
display: var(--layout-horizontal_-_display);
|
||||
-ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
|
||||
-webkit-flex-direction: var(--layout-horizontal_-_-webkit-flex-direction);
|
||||
flex-direction: var(--layout-horizontal_-_flex-direction);
|
||||
|
||||
|
||||
display: var(--yt-live-chat-inline-action-button-container-display, none);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[has-inline-action-buttons][hide-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-membership-item-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[has-inline-action-buttons]:hover #menu.yt-live-chat-membership-item-renderer {
|
||||
display: var(--yt-live-chat-item-with-inline-actions-context-menu-display, block);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer,
|
||||
#additional-inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer {
|
||||
--yt-button-icon-size: 36px;
|
||||
--yt-button-icon-padding: 6px;
|
||||
|
||||
color: var(--yt-white);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer {
|
||||
background: var(--yt-luna-black-opacity-lighten-1);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-membership-item-renderer>.yt-live-chat-membership-item-renderer:hover {
|
||||
background: var(--yt-luna-black);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer {
|
||||
color: var(--yt-live-chat-additional-inline-action-button-color);
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-membership-item-renderer>.yt-live-chat-membership-item-renderer:hover {
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color-hover);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-membership-item-renderer:not(:empty) {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer:not(:first-child),
|
||||
#additional-inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
--yt-live-chat-sponsor-header-color: #0a8043;
|
||||
--yt-live-chat-sponsor-color: #0f9d58;
|
||||
--yt-live-chat-sponsor-text-color: #fff;
|
||||
--yt-live-chat-item-timestamp-display: var(--yt-live-chat-paid-message-timestamp-display, none);
|
||||
|
||||
padding: 4px 24px;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] {
|
||||
padding: 0;
|
||||
|
||||
--yt-live-chat-item-timestamp-display: block;
|
||||
}
|
||||
|
||||
#card.yt-live-chat-membership-item-renderer {
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
|
||||
display: var(--layout-vertical_-_display);
|
||||
-ms-flex-direction: var(--layout-vertical_-_-ms-flex-direction);
|
||||
-webkit-flex-direction: var(--layout-vertical_-_-webkit-flex-direction);
|
||||
flex-direction: var(--layout-vertical_-_flex-direction);
|
||||
box-shadow: var(--shadow-elevation-2dp_-_box-shadow);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] #card.yt-live-chat-membership-item-renderer {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#header.yt-live-chat-membership-item-renderer {
|
||||
position: relative;
|
||||
|
||||
background-color: var(--yt-live-chat-sponsor-header-color);
|
||||
padding: 8px 16px;
|
||||
color: #fff;
|
||||
min-height: 20px;
|
||||
|
||||
display: var(--layout-horizontal_-_display);
|
||||
-ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
|
||||
-webkit-flex-direction: var(--layout-horizontal_-_-webkit-flex-direction);
|
||||
flex-direction: var(--layout-horizontal_-_flex-direction);
|
||||
-ms-flex-align: var(--layout-center_-_-ms-flex-align);
|
||||
-webkit-align-items: var(--layout-center_-_-webkit-align-items);
|
||||
align-items: var(--layout-center_-_align-items);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[show-only-header] #header.yt-live-chat-membership-item-renderer {
|
||||
background-color: var(--yt-live-chat-sponsor-color);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] #header.yt-live-chat-membership-item-renderer {
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
background-color: var(--yt-live-chat-background-color);
|
||||
-ms-flex-align: var(--layout-start_-_-ms-flex-align);
|
||||
-webkit-align-items: var(--layout-start_-_-webkit-align-items);
|
||||
align-items: var(--layout-start_-_align-items);
|
||||
}
|
||||
|
||||
#header-content.yt-live-chat-membership-item-renderer {
|
||||
display: var(--layout-horizontal_-_display);
|
||||
-ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
|
||||
-webkit-flex-direction: var(--layout-horizontal_-_-webkit-flex-direction);
|
||||
flex-direction: var(--layout-horizontal_-_flex-direction);
|
||||
-ms-flex-pack: var(--layout-justified_-_-ms-flex-pack);
|
||||
-webkit-justify-content: var(--layout-justified_-_-webkit-justify-content);
|
||||
justify-content: var(--layout-justified_-_justify-content);
|
||||
-ms-flex: var(--layout-flex_-_-ms-flex);
|
||||
-webkit-flex: var(--layout-flex_-_-webkit-flex);
|
||||
flex: var(--layout-flex_-_flex);
|
||||
-webkit-flex-basis: var(--layout-flex_-_-webkit-flex-basis);
|
||||
flex-basis: var(--layout-flex_-_flex-basis);
|
||||
-ms-flex-align: var(--layout-baseline_-_-ms-flex-align);
|
||||
-webkit-align-items: var(--layout-baseline_-_-webkit-align-items);
|
||||
align-items: var(--layout-baseline_-_align-items);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] #header-content.yt-live-chat-membership-item-renderer {
|
||||
display: var(--layout-vertical_-_display);
|
||||
-ms-flex-direction: var(--layout-vertical_-_-ms-flex-direction);
|
||||
-webkit-flex-direction: var(--layout-vertical_-_-webkit-flex-direction);
|
||||
flex-direction: var(--layout-vertical_-_flex-direction);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] #header-content-inner-column.yt-live-chat-membership-item-renderer {
|
||||
margin-bottom: 4px;
|
||||
|
||||
display: var(--layout-horizontal_-_display);
|
||||
-ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
|
||||
-webkit-flex-direction: var(--layout-horizontal_-_-webkit-flex-direction);
|
||||
flex-direction: var(--layout-horizontal_-_flex-direction);
|
||||
-ms-flex-align: var(--layout-center_-_-ms-flex-align);
|
||||
-webkit-align-items: var(--layout-center_-_-webkit-align-items);
|
||||
align-items: var(--layout-center_-_align-items);
|
||||
-ms-flex: var(--layout-flex-none_-_-ms-flex);
|
||||
-webkit-flex: var(--layout-flex-none_-_-webkit-flex);
|
||||
flex: var(--layout-flex-none_-_flex);
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-membership-item-renderer {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
yt-icon#author-photo.yt-live-chat-membership-item-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] yt-icon#author-photo.yt-live-chat-membership-item-renderer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer:not([dashboard-money-feed]) yt-live-chat-author-chip.yt-live-chat-membership-item-renderer {
|
||||
--yt-live-chat-sponsor-color: var(--yt-live-chat-sponsor-text-color);
|
||||
--yt-live-chat-secondary-text-color: var(--yt-live-chat-sponsor-text-color);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] yt-live-chat-author-chip.yt-live-chat-membership-item-renderer {
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
--yt-live-chat-sponsor-color: var(--yt-live-chat-secondary-text-color);
|
||||
}
|
||||
|
||||
#header-subtext.yt-live-chat-membership-item-renderer {
|
||||
margin-top: 2px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#header-subtext.yt-live-chat-membership-item-renderer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] #header-subtext.yt-live-chat-membership-item-renderer {
|
||||
margin: 4px 0 13px;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
}
|
||||
|
||||
#header-primary-text.yt-live-chat-membership-item-renderer {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
#header-primary-text.yt-live-chat-membership-item-renderer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[has-primary-header-text]:not([dashboard-money-feed]) yt-live-chat-author-chip.yt-live-chat-membership-item-renderer,
|
||||
yt-live-chat-membership-item-renderer[has-primary-header-text]:not([dashboard-money-feed]) #header-subtext.yt-live-chat-membership-item-renderer {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] #header-primary-text.yt-live-chat-membership-item-renderer {
|
||||
display: inline;
|
||||
height: 24px;
|
||||
min-width: 16px;
|
||||
border-radius: 12px;
|
||||
margin-right: 8px;
|
||||
padding: 0 12px;
|
||||
background-color: var(--yt-live-chat-sponsor-color);
|
||||
color: var(--yt-white);
|
||||
display: var(--layout-inline_-_display, inline);
|
||||
-ms-flex-align: var(--layout-center-center_-_-ms-flex-align);
|
||||
-webkit-align-items: var(--layout-center-center_-_-webkit-align-items);
|
||||
align-items: var(--layout-center-center_-_align-items);
|
||||
-ms-flex-pack: var(--layout-center-center_-_-ms-flex-pack);
|
||||
-webkit-justify-content: var(--layout-center-center_-_-webkit-justify-content);
|
||||
justify-content: var(--layout-center-center_-_justify-content);
|
||||
font-size: var(--ytd-badge_-_font-size);
|
||||
font-weight: var(--ytd-badge_-_font-weight);
|
||||
line-height: var(--ytd-badge_-_line-height);
|
||||
}
|
||||
|
||||
#content.yt-live-chat-membership-item-renderer {
|
||||
background-color: var(--yt-live-chat-sponsor-color);
|
||||
color: var(--yt-live-chat-sponsor-text-color);
|
||||
padding: 8px 16px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] #content.yt-live-chat-membership-item-renderer {
|
||||
background-color: unset;
|
||||
font-size: unset;
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
padding: 0 0 16px 72px;
|
||||
}
|
||||
|
||||
#content.yt-live-chat-membership-item-renderer img.yt-live-chat-membership-item-renderer {
|
||||
/* B站表情有不是正方形的 */
|
||||
/* width: var(--yt-live-chat-emoji-size); */
|
||||
width: auto;
|
||||
height: var(--yt-live-chat-emoji-size);
|
||||
|
||||
margin: -1px 2px 1px 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[show-only-header] #content.yt-live-chat-membership-item-renderer,
|
||||
#deleted-state.yt-live-chat-membership-item-renderer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#deleted-state.yt-live-chat-membership-item-renderer {
|
||||
display: block;
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
a.yt-live-chat-membership-item-renderer {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-membership-item-renderer a.yt-live-chat-membership-item-renderer {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-membership-item-renderer a.yt-live-chat-membership-item-renderer .mention.yt-live-chat-membership-item-renderer {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-membership-item-renderer {
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-sponsor-header-color) 100%);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[show-only-header] #menu.yt-live-chat-membership-item-renderer {
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-sponsor-color) 100%);
|
||||
}
|
||||
|
||||
yt-live-chat-membership-item-renderer[dashboard-money-feed] #menu.yt-live-chat-membership-item-renderer {
|
||||
margin-top: 8px;
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-background-color) 40%);
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-paid-message-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#timestamp.yt-live-chat-paid-message-renderer {
|
||||
display: var(--yt-live-chat-item-timestamp-display, inline);
|
||||
margin: var(--yt-live-chat-item-timestamp-margin, 0 8px 0 0);
|
||||
color: var(--yt-live-chat-tertiary-text-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-paid-message-renderer {
|
||||
display: block;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
#menu-button.yt-live-chat-paid-message-renderer {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-paid-message-renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translateX(100px);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer:hover #menu.yt-live-chat-paid-message-renderer, yt-live-chat-paid-message-renderer[menu-visible] #menu.yt-live-chat-paid-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer:focus-within #menu.yt-live-chat-paid-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#inline-action-button-container.yt-live-chat-paid-message-renderer {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 0;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
background-color: var(--yt-live-chat-moderation-mode-hover-background-color);
|
||||
display: none;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[has-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
display: var(--yt-live-chat-inline-action-button-container-display, none);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[has-inline-action-buttons][hide-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-paid-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[has-inline-action-buttons]:hover #menu.yt-live-chat-paid-message-renderer {
|
||||
display: var(--yt-live-chat-item-with-inline-actions-context-menu-display, block);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer, #additional-inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer {
|
||||
--yt-button-icon-size: 36px;
|
||||
--yt-button-icon-padding: 6px;
|
||||
color: hsl(0, 0%, 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer {
|
||||
background: hsla(0, 0%, 6.7%, .8);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-paid-message-renderer>.yt-live-chat-paid-message-renderer:hover {
|
||||
background: hsl(0, 0%, 6.7%);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer {
|
||||
color: var(--yt-live-chat-additional-inline-action-button-color);
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-paid-message-renderer>.yt-live-chat-paid-message-renderer:hover {
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color-hover);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-paid-message-renderer:not(:empty) {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer:not(:first-child), #additional-inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 4px 24px;
|
||||
font-size: 15px;
|
||||
--yt-live-chat-paid-message-background-color: var(--yt-live-chat-paid-message-primary-color);
|
||||
--yt-live-chat-paid-message-header-background-color: var(--yt-live-chat-paid-message-secondary-color);
|
||||
--yt-live-chat-text-input-field-placeholder-color: var(--yt-live-chat-paid-message-color);
|
||||
--yt-live-chat-item-timestamp-display: var(--yt-live-chat-paid-message-timestamp-display, none);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] {
|
||||
padding: 0;
|
||||
padding-bottom: 8px;
|
||||
--yt-live-chat-paid-message-background-color: transparent;
|
||||
--yt-live-chat-paid-message-header-background-color: transparent;
|
||||
--yt-live-chat-item-timestamp-display: block;
|
||||
}
|
||||
|
||||
#card.yt-live-chat-paid-message-renderer {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #card.yt-live-chat-paid-message-renderer {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#header.yt-live-chat-paid-message-renderer {
|
||||
position: relative;
|
||||
background-color: var(--yt-live-chat-paid-message-header-background-color, #125aac);
|
||||
color: var(--yt-live-chat-paid-message-header-color, #fff);
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
min-height: 20px;
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[show-only-header] #header.yt-live-chat-paid-message-renderer {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #header.yt-live-chat-paid-message-renderer {
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-paid-message-renderer {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
yt-icon#author-photo.yt-live-chat-paid-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] yt-icon#author-photo.yt-live-chat-paid-message-renderer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#header-content.yt-live-chat-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
justify-content: space-between;
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
-ms-flex-align: baseline;
|
||||
-webkit-align-items: baseline;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #header-content.yt-live-chat-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#header-content-primary-column.yt-live-chat-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-ms-flex-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
justify-content: space-between;
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #header-content-primary-column.yt-live-chat-paid-message-renderer {
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: baseline;
|
||||
-webkit-align-items: baseline;
|
||||
align-items: baseline;
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
#author-name.yt-live-chat-paid-message-renderer {
|
||||
color: var(--yt-live-chat-paid-message-author-name-color, rgba(255, 255, 255, 0.7));
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #purchase-amount.yt-live-chat-paid-message-renderer {
|
||||
display: inline;
|
||||
height: 24px;
|
||||
min-width: 16px;
|
||||
border-radius: 12px;
|
||||
margin-right: 8px;
|
||||
padding: 0 12px;
|
||||
background-color: var(--yt-live-chat-paid-message-primary-color);
|
||||
color: var(--yt-live-chat-paid-message-color);
|
||||
display: inline-flex;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer {
|
||||
display: block;
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer[is-deleted] {
|
||||
margin: 4px 0 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer[is-deleted].yt-live-chat-paid-message-renderer::before {
|
||||
content: '​';
|
||||
display: inline;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-paid-message-renderer {
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-paid-message-header-background-color, #125aac) 100%);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #menu.yt-live-chat-paid-message-renderer {
|
||||
margin-top: 8px;
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-background-color) 40%);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[allow-animations] #content.yt-live-chat-paid-message-renderer, yt-live-chat-paid-message-renderer[allow-animations] #header.yt-live-chat-paid-message-renderer {
|
||||
transition-property: background-color, color;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
#content.yt-live-chat-paid-message-renderer {
|
||||
background-color: var(--yt-live-chat-paid-message-background-color, #1565c0);
|
||||
color: var(--yt-live-chat-paid-message-color, #fff);
|
||||
padding: 8px 16px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
#content.yt-live-chat-paid-message-renderer img.yt-live-chat-paid-message-renderer {
|
||||
/* B站表情有不是正方形的 */
|
||||
/* width: var(--yt-live-chat-emoji-size); */
|
||||
width: auto;
|
||||
height: var(--yt-live-chat-emoji-size);
|
||||
margin: -1px 2px 1px 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #content.yt-live-chat-paid-message-renderer {
|
||||
padding: 0;
|
||||
padding-left: 72px;
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[show-only-header] #content.yt-live-chat-paid-message-renderer {
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a.yt-live-chat-paid-message-renderer {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-paid-message-renderer a.yt-live-chat-paid-message-renderer {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #message.yt-live-chat-paid-message-renderer {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-paid-message-renderer a.yt-live-chat-paid-message-renderer .mention.yt-live-chat-paid-message-renderer {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-paid-message-renderer:empty, #author-name.yt-live-chat-paid-message-renderer:empty:not([is-deleted]).yt-live-chat-paid-message-renderer, #deleted-state.yt-live-chat-paid-message-renderer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#deleted-state.yt-live-chat-paid-message-renderer {
|
||||
display: block;
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #deleted-state.yt-live-chat-paid-message-renderer[is-deleted] {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer {
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[show-footer-divider] #footer.yt-live-chat-paid-message-renderer::before {
|
||||
content: "";
|
||||
display: flex;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[is-user-editable] #footer.yt-live-chat-paid-message-renderer:not(:empty) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
252
frontend/src/assets/css/youtube/yt-live-chat-renderer.css
Normal file
@@ -0,0 +1,252 @@
|
||||
yt-live-chat-renderer, yt-live-chat-item-list-renderer #item-scroller {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
yt-live-chat-renderer ::-webkit-scrollbar {
|
||||
content: '';
|
||||
}
|
||||
|
||||
yt-live-chat-renderer ::-webkit-scrollbar-thumb {
|
||||
background-color: hsla(0, 0%, 53.3%, .2);
|
||||
border: 2px solid #fcfcfc;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
yt-live-chat-renderer ::-webkit-scrollbar-track {
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
|
||||
|
||||
canvas.yt-live-chat-renderer, caption.yt-live-chat-renderer, center.yt-live-chat-renderer, cite.yt-live-chat-renderer, code.yt-live-chat-renderer, dd.yt-live-chat-renderer, del.yt-live-chat-renderer, dfn.yt-live-chat-renderer, div.yt-live-chat-renderer, dl.yt-live-chat-renderer, dt.yt-live-chat-renderer, em.yt-live-chat-renderer, embed.yt-live-chat-renderer, fieldset.yt-live-chat-renderer, font.yt-live-chat-renderer, form.yt-live-chat-renderer, h1.yt-live-chat-renderer, h2.yt-live-chat-renderer, h3.yt-live-chat-renderer, h4.yt-live-chat-renderer, h5.yt-live-chat-renderer, h6.yt-live-chat-renderer, hr.yt-live-chat-renderer, i.yt-live-chat-renderer, iframe.yt-live-chat-renderer, img.yt-live-chat-renderer, ins.yt-live-chat-renderer, kbd.yt-live-chat-renderer, label.yt-live-chat-renderer, legend.yt-live-chat-renderer, li.yt-live-chat-renderer, menu.yt-live-chat-renderer, object.yt-live-chat-renderer, ol.yt-live-chat-renderer, p.yt-live-chat-renderer, pre.yt-live-chat-renderer, q.yt-live-chat-renderer, s.yt-live-chat-renderer, samp.yt-live-chat-renderer, small.yt-live-chat-renderer, span.yt-live-chat-renderer, strike.yt-live-chat-renderer, strong.yt-live-chat-renderer, sub.yt-live-chat-renderer, sup.yt-live-chat-renderer, table.yt-live-chat-renderer, tbody.yt-live-chat-renderer, td.yt-live-chat-renderer, tfoot.yt-live-chat-renderer, th.yt-live-chat-renderer, thead.yt-live-chat-renderer, tr.yt-live-chat-renderer, tt.yt-live-chat-renderer, u.yt-live-chat-renderer, ul.yt-live-chat-renderer, var.yt-live-chat-renderer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-renderer {
|
||||
font-size: 13px;
|
||||
--yt-emoji-picker-renderer-height: 180px;
|
||||
--yt-button-default-text-color: var(--yt-live-chat-button-default-text-color);
|
||||
--yt-button-default-background-color: var(--yt-live-chat-button-default-background-color);
|
||||
--yt-button-dark-text-color: var(--yt-live-chat-button-dark-text-color);
|
||||
--yt-button-dark-background-color: var(--yt-live-chat-button-dark-background-color);
|
||||
--yt-button-payment-background-color: var(--yt-live-chat-sponsor-color);
|
||||
}
|
||||
|
||||
yt-live-chat-renderer {
|
||||
position: relative;
|
||||
background: var(--yt-live-chat-background-color);
|
||||
color: var(--yt-live-chat-primary-text-color);
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
yt-live-chat-renderer[hide-timestamps] {
|
||||
--yt-live-chat-item-timestamp-display: none;
|
||||
}
|
||||
|
||||
#separator.yt-live-chat-renderer {
|
||||
border-bottom: var(--yt-live-chat-header-bottom-border, none);
|
||||
}
|
||||
|
||||
#content-pages.yt-live-chat-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
}
|
||||
|
||||
#panel-pages.yt-live-chat-renderer {
|
||||
max-height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#contents.yt-live-chat-renderer {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#chat-messages.yt-live-chat-renderer, #contents.yt-live-chat-renderer, #item-list.yt-live-chat-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
}
|
||||
|
||||
#ticker.yt-live-chat-renderer {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#chat.yt-live-chat-renderer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
}
|
||||
|
||||
#chat.yt-live-chat-renderer::after {
|
||||
content: '';
|
||||
display: none;
|
||||
animation: gradient-slide 1.2s ease infinite;
|
||||
animation-name: gradient-slide;
|
||||
background-color: var(--yt-live-chat-shimmer-background-color);
|
||||
background-image: var(--yt-live-chat-shimmer-linear-gradient);
|
||||
background-size: 300% 300%;
|
||||
transform: rotateX(180deg);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
yt-live-chat-renderer[loading] #chat.yt-live-chat-renderer::after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
yt-live-chat-pinned-message-renderer.yt-live-chat-renderer {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
yt-live-chat-item-list-renderer.yt-live-chat-renderer, yt-live-chat-ninja-message-renderer.yt-live-chat-renderer {
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
}
|
||||
|
||||
#action-panel.yt-live-chat-renderer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
yt-live-chat-renderer[has-action-panel-renderer] yt-live-chat-action-panel-renderer.yt-live-chat-renderer {
|
||||
animation: slideUp cubic-bezier(0.05, 0.00, 0.00, 1.00) forwards;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
|
||||
#action-panel-backdrop.yt-live-chat-renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
yt-live-chat-renderer[has-action-panel-renderer] #action-panel-backdrop.yt-live-chat-renderer {
|
||||
visibility: visible;
|
||||
animation: fadeIn cubic-bezier(0.05, 0.00, 0.00, 1.00) forwards;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
|
||||
#input-panel.yt-live-chat-renderer {
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
#input-panel.yt-live-chat-renderer:not(:empty) {
|
||||
border-top: var(--yt-live-chat-action-panel-top-border, none);
|
||||
}
|
||||
|
||||
.hide-on-collapse.yt-live-chat-renderer {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
yt-live-chat-renderer[collapsed] .hide-on-collapse.yt-live-chat-renderer {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#loading.yt-live-chat-renderer {
|
||||
height: 387px;
|
||||
background-color: var(--yt-live-chat-action-panel-background-color);
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#loading.yt-live-chat-renderer>paper-spinner-lite.yt-live-chat-renderer {
|
||||
--paper-spinner-color: var(--yt-live-chat-primary-text-color);
|
||||
}
|
||||
|
||||
#nitrate-promo.yt-live-chat-renderer>*.yt-live-chat-renderer {
|
||||
background: var(--yt-live-chat-overlay-color);
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@keyframes gradient-slide {
|
||||
0% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
to {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(15%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
background-color: transparent;
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(0, 0, 0, 0.60);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-text-message-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#timestamp.yt-live-chat-text-message-renderer {
|
||||
display: var(--yt-live-chat-item-timestamp-display, inline);
|
||||
margin: var(--yt-live-chat-item-timestamp-margin, 0 8px 0 0);
|
||||
color: var(--yt-live-chat-tertiary-text-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-text-message-renderer {
|
||||
display: block;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
#menu-button.yt-live-chat-text-message-renderer {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-text-message-renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translateX(100px);
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer:hover #menu.yt-live-chat-text-message-renderer, yt-live-chat-text-message-renderer[menu-visible] #menu.yt-live-chat-text-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer:focus-within #menu.yt-live-chat-text-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#inline-action-button-container.yt-live-chat-text-message-renderer {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 0;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
background-color: var(--yt-live-chat-moderation-mode-hover-background-color);
|
||||
display: none;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[has-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-text-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
display: var(--yt-live-chat-inline-action-button-container-display, none);
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[has-inline-action-buttons][hide-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-text-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[has-inline-action-buttons]:hover #menu.yt-live-chat-text-message-renderer {
|
||||
display: var(--yt-live-chat-item-with-inline-actions-context-menu-display, block);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer, #additional-inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer {
|
||||
--yt-button-icon-size: 36px;
|
||||
--yt-button-icon-padding: 6px;
|
||||
color: hsl(0, 0%, 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer {
|
||||
background: hsla(0, 0%, 6.7%, .8);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-text-message-renderer>.yt-live-chat-text-message-renderer:hover {
|
||||
background: hsl(0, 0%, 6.7%);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer {
|
||||
color: var(--yt-live-chat-additional-inline-action-button-color);
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-text-message-renderer>.yt-live-chat-text-message-renderer:hover {
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color-hover);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-text-message-renderer:not(:empty) {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer:not(:first-child), #additional-inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer {
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
padding: 4px 24px;
|
||||
overflow: hidden;
|
||||
--yt-endpoint-color: var(--yt-live-chat-primary-text-color, hsl(0, 0%, 6.7%));
|
||||
--yt-endpoint-hover-color: var(--yt-live-chat-primary-text-color, var(--yt-endpoint-color));
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: start;
|
||||
-webkit-align-items: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer:hover {
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[author-is-owner] {
|
||||
background-color: var(--yt-live-chat-message-highlight-background-color);
|
||||
}
|
||||
|
||||
#content.yt-live-chat-text-message-renderer {
|
||||
-ms-align-self: center;
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip.yt-live-chat-text-message-renderer {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer {
|
||||
color: var(--yt-live-chat-primary-text-color, var(--yt-primary-text-color));
|
||||
line-height: 16px;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
font-style: var(--yt-live-chat-text-message-renderer-message-message-style_-_font-style);
|
||||
opacity: var(--yt-live-chat-text-message-renderer-message-message-style_-_opacity);
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer .emoji.yt-live-chat-text-message-renderer {
|
||||
/* B站表情有不是正方形的 */
|
||||
/* width: var(--yt-live-chat-emoji-size); */
|
||||
width: auto;
|
||||
height: var(--yt-live-chat-emoji-size);
|
||||
margin: -1px 2px 1px 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
a.yt-live-chat-text-message-renderer {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer a.yt-live-chat-text-message-renderer {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer a.yt-live-chat-text-message-renderer .mention.yt-live-chat-text-message-renderer {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#show-original.yt-live-chat-text-message-renderer {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer:empty, #deleted-state.yt-live-chat-text-message-renderer:empty, #show-original.yt-live-chat-text-message-renderer:empty, yt-live-chat-text-message-renderer[show-original] #deleted-state.yt-live-chat-text-message-renderer, yt-live-chat-text-message-renderer[show-original] #show-original.yt-live-chat-text-message-renderer, yt-live-chat-text-message-renderer[is-deleted]:not([show-original]) #message.yt-live-chat-text-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-text-message-renderer {
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-background-color, hsl(0, 0%, 100%)) 100%);
|
||||
}
|
||||
|
||||
.mention.yt-live-chat-text-message-renderer {
|
||||
background: var(--yt-live-chat-mention-background-color);
|
||||
color: var(--yt-live-chat-mention-text-color);
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#deleted-state.yt-live-chat-text-message-renderer, #show-original.yt-live-chat-text-message-renderer, yt-live-chat-text-message-renderer[is-deleted] #message.yt-live-chat-text-message-renderer {
|
||||
font-style: italic;
|
||||
color: var(--yt-live-chat-deleted-message-color, rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[show-bar]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 8px;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 4px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[is-deleted]::before {
|
||||
background: var(--yt-live-chat-deleted-message-bar-color, rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[is-dimmed] #message.yt-live-chat-text-message-renderer {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[is-dimmed]::before {
|
||||
background: var(--yt-live-chat-error-message-color, #f44336);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-ticker-paid-message-item-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-ticker-paid-message-item-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: width 0.2s;
|
||||
vertical-align: top;
|
||||
cursor: pointer;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#container.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
border-radius: 999px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer.sliding-down #container.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
opacity: 0.5;
|
||||
transform: translateY(44px);
|
||||
transition: opacity 0.2s, transform 0.2s cubic-bezier(0.4, 0.0, 1, 1);
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer.collapsing {
|
||||
margin-right: 0;
|
||||
transition: margin-right 0.2s cubic-bezier(0.4, 0.0, 0.2, 1), width 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer[dimmed] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
yt-img-shadow.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
margin-right: -4px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#content.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#text.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
margin: 0 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer[is-deleted] #author-photo.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-ticker-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-renderer {
|
||||
display: block;
|
||||
background-color: var(--yt-live-chat-header-background-color);
|
||||
}
|
||||
|
||||
#container.yt-live-chat-ticker-renderer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#items.yt-live-chat-ticker-renderer {
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 24px 8px 24px;
|
||||
}
|
||||
|
||||
#items.yt-live-chat-ticker-renderer>*.yt-live-chat-ticker-renderer {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#left-arrow-container.yt-live-chat-ticker-renderer {
|
||||
background: linear-gradient(to right, var(--yt-live-chat-ticker-arrow-background) 0, var(--yt-live-chat-ticker-arrow-background) 52px, transparent 60px);
|
||||
left: 0;
|
||||
padding: 0 16px 0 12px;
|
||||
}
|
||||
|
||||
#right-arrow-container.yt-live-chat-ticker-renderer {
|
||||
background: linear-gradient(to left, var(--yt-live-chat-ticker-arrow-background) 0, var(--yt-live-chat-ticker-arrow-background) 52px, transparent 60px);
|
||||
right: 0;
|
||||
padding: 0 12px 0 16px;
|
||||
}
|
||||
|
||||
#container.yt-live-chat-ticker-renderer:hover #left-arrow-container.yt-live-chat-ticker-renderer, #container.yt-live-chat-ticker-renderer:hover #right-arrow-container.yt-live-chat-ticker-renderer {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#left-arrow-container.yt-live-chat-ticker-renderer, #right-arrow-container.yt-live-chat-ticker-renderer {
|
||||
height: 32px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
transition: opacity 0.3s 0.1s;
|
||||
}
|
||||
|
||||
yt-icon.yt-live-chat-ticker-renderer {
|
||||
background-color: #2196f3;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
padding: 4px;
|
||||
width: 24px;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<yt-live-chat-author-badge-renderer :type="authorTypeText" v-if="isAdmin || privilegeType > 0">
|
||||
<yt-live-chat-author-badge-renderer :type="authorTypeText">
|
||||
<el-tooltip :content="readableAuthorTypeText" placement="top">
|
||||
<div id="image" class="style-scope yt-live-chat-author-badge-renderer">
|
||||
<yt-icon v-if="isAdmin" class="style-scope yt-live-chat-author-badge-renderer">
|
||||
@@ -14,7 +14,8 @@
|
||||
</svg>
|
||||
</yt-icon>
|
||||
<img v-else :src="`/static/img/icons/guard-level-${privilegeType}.png`"
|
||||
class="style-scope yt-live-chat-author-badge-renderer" :alt="readableAuthorTypeText">
|
||||
class="style-scope yt-live-chat-author-badge-renderer" :alt="readableAuthorTypeText"
|
||||
>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</yt-live-chat-author-badge-renderer>
|
||||
@@ -38,84 +39,13 @@ export default {
|
||||
},
|
||||
readableAuthorTypeText() {
|
||||
if (this.isAdmin) {
|
||||
return '管理员'
|
||||
return this.$t('chat.moderator')
|
||||
}
|
||||
return constants.GUARD_LEVEL_TO_TEXT[this.privilegeType]
|
||||
return constants.getShowGuardLevelText(this.privilegeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- yt-live-chat-author-badge-renderer -->
|
||||
<style>
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-author-badge-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer[type='moderator'] {
|
||||
color: var(--yt-live-chat-moderator-color, #5e84f1);
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer[type='owner'] {
|
||||
color: var(--yt-live-chat-owner-color, #ffd600);
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer[type='member'] {
|
||||
color: var(--yt-live-chat-sponsor-color, #107516);
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer[type='verified'] {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
img.yt-live-chat-author-badge-renderer, yt-icon.yt-live-chat-author-badge-renderer {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- yt-icon -->
|
||||
<style>
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-icon[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-icon, .yt-icon-container.yt-icon {
|
||||
display: inline-flex;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
fill: currentcolor;
|
||||
stroke: none;
|
||||
width: var(--iron-icon-width, 24px);
|
||||
height: var(--iron-icon-height, 24px);
|
||||
}
|
||||
|
||||
yt-icon.external-container {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
<style src="@/assets/css/youtube/yt-live-chat-author-badge-renderer.css"></style>
|
||||
<style src="@/assets/css/youtube/yt-icon.css"></style>
|
||||
|
||||
54
frontend/src/components/ChatRenderer/AuthorChip.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<yt-live-chat-author-chip>
|
||||
<span id="author-name" dir="auto" class="style-scope yt-live-chat-author-chip" :class="{ member: isInMemberMessage }"
|
||||
:type="authorTypeText"
|
||||
>
|
||||
<template>{{ authorName }}</template>
|
||||
<!-- 这里是已验证勋章 -->
|
||||
<span id="chip-badges" class="style-scope yt-live-chat-author-chip"></span>
|
||||
</span>
|
||||
<span id="chat-badges" class="style-scope yt-live-chat-author-chip">
|
||||
<author-badge v-if="isInMemberMessage" class="style-scope yt-live-chat-author-chip"
|
||||
:isAdmin="false" :privilegeType="privilegeType"
|
||||
></author-badge>
|
||||
<template v-else>
|
||||
<author-badge v-if="authorType === AUTHRO_TYPE_ADMIN" class="style-scope yt-live-chat-author-chip"
|
||||
isAdmin :privilegeType="0"
|
||||
></author-badge>
|
||||
<author-badge v-if="privilegeType > 0" class="style-scope yt-live-chat-author-chip"
|
||||
:isAdmin="false" :privilegeType="privilegeType"
|
||||
></author-badge>
|
||||
</template>
|
||||
</span>
|
||||
</yt-live-chat-author-chip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AuthorBadge from './AuthorBadge'
|
||||
import * as constants from './constants'
|
||||
|
||||
export default {
|
||||
name: 'AuthorChip',
|
||||
components: {
|
||||
AuthorBadge
|
||||
},
|
||||
props: {
|
||||
isInMemberMessage: Boolean,
|
||||
authorName: String,
|
||||
authorType: Number,
|
||||
privilegeType: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
AUTHRO_TYPE_ADMIN: constants.AUTHRO_TYPE_ADMIN
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
authorTypeText() {
|
||||
return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="@/assets/css/youtube/yt-live-chat-author-chip.css"></style>
|
||||
@@ -1,88 +1,37 @@
|
||||
<template>
|
||||
<yt-img-shadow class="no-transition" :height="height" :width="width" style="background-color: transparent;" loaded>
|
||||
<img id="img" class="style-scope yt-img-shadow" alt="" :height="height" :width="width" :src="imgUrl">
|
||||
<img id="img" class="style-scope yt-img-shadow" alt="" :height="height" :width="width" :src="showImgUrl" @error="onLoadError">
|
||||
</yt-img-shadow>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as avatar from '@/api/chat/avatar'
|
||||
|
||||
export default {
|
||||
name: 'ImgShadow',
|
||||
props: {
|
||||
imgUrl: String,
|
||||
height: String,
|
||||
width: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showImgUrl: this.imgUrl
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imgUrl(val) {
|
||||
this.showImgUrl = val
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onLoadError() {
|
||||
if (this.showImgUrl !== avatar.DEFAULT_AVATAR_URL) {
|
||||
this.showImgUrl = avatar.DEFAULT_AVATAR_URL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- yt-img-shadow -->
|
||||
<style>
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-img-shadow[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-img-shadow {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
yt-img-shadow.no-transition {
|
||||
opacity: 1;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
yt-img-shadow.with-placeholder {
|
||||
background-color: transparent;
|
||||
min-height: unset;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
yt-img-shadow[loaded] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
yt-img-shadow.empty img.yt-img-shadow {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
yt-img-shadow[object-fit="FILL"] img.yt-img-shadow, yt-img-shadow[fit] img.yt-img-shadow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
yt-img-shadow[object-fit="COVER"] img.yt-img-shadow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
yt-img-shadow[object-fit="CONTAIN"] img.yt-img-shadow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
yt-img-shadow[object-position="LEFT"] img.yt-img-shadow {
|
||||
object-position: left;
|
||||
}
|
||||
|
||||
img.yt-img-shadow {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-height: none;
|
||||
max-width: 100%;
|
||||
border-radius: none;
|
||||
}
|
||||
</style>
|
||||
<style src="@/assets/css/youtube/yt-img-shadow.css"></style>
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
<template>
|
||||
<yt-live-chat-legacy-paid-message-renderer class="style-scope yt-live-chat-item-list-renderer">
|
||||
<div id="card" class="style-scope yt-live-chat-legacy-paid-message-renderer">
|
||||
<img-shadow id="author-photo" height="40" width="40" class="style-scope yt-live-chat-legacy-paid-message-renderer"
|
||||
:imgUrl="avatarUrl"
|
||||
></img-shadow>
|
||||
<div id="content" class="style-scope yt-live-chat-legacy-paid-message-renderer">
|
||||
<div id="content-primary-column" class="style-scope yt-live-chat-legacy-paid-message-renderer">
|
||||
<div id="author-name" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{authorName}}</div>
|
||||
<div id="event-text" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{title}}</div>
|
||||
<div id="detail-text" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{content}}</div>
|
||||
</div>
|
||||
<div id="timestamp" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{time}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="inline-action-button-container" class="style-scope yt-live-chat-legacy-paid-message-renderer" aria-hidden="true">
|
||||
<div id="inline-action-buttons" class="style-scope yt-live-chat-legacy-paid-message-renderer"></div>
|
||||
</div>
|
||||
</yt-live-chat-legacy-paid-message-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
|
||||
export default {
|
||||
name: 'LegacyPaidMessage',
|
||||
components: {
|
||||
ImgShadow
|
||||
},
|
||||
props: {
|
||||
avatarUrl: String,
|
||||
authorName: String,
|
||||
title: String,
|
||||
content: String,
|
||||
time: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- yt-live-chat-legacy-paid-message-renderer -->
|
||||
<style>
|
||||
canvas.yt-live-chat-legacy-paid-message-renderer, caption.yt-live-chat-legacy-paid-message-renderer, center.yt-live-chat-legacy-paid-message-renderer, cite.yt-live-chat-legacy-paid-message-renderer, code.yt-live-chat-legacy-paid-message-renderer, dd.yt-live-chat-legacy-paid-message-renderer, del.yt-live-chat-legacy-paid-message-renderer, dfn.yt-live-chat-legacy-paid-message-renderer, div.yt-live-chat-legacy-paid-message-renderer, dl.yt-live-chat-legacy-paid-message-renderer, dt.yt-live-chat-legacy-paid-message-renderer, em.yt-live-chat-legacy-paid-message-renderer, embed.yt-live-chat-legacy-paid-message-renderer, fieldset.yt-live-chat-legacy-paid-message-renderer, font.yt-live-chat-legacy-paid-message-renderer, form.yt-live-chat-legacy-paid-message-renderer, h1.yt-live-chat-legacy-paid-message-renderer, h2.yt-live-chat-legacy-paid-message-renderer, h3.yt-live-chat-legacy-paid-message-renderer, h4.yt-live-chat-legacy-paid-message-renderer, h5.yt-live-chat-legacy-paid-message-renderer, h6.yt-live-chat-legacy-paid-message-renderer, hr.yt-live-chat-legacy-paid-message-renderer, i.yt-live-chat-legacy-paid-message-renderer, iframe.yt-live-chat-legacy-paid-message-renderer, img.yt-live-chat-legacy-paid-message-renderer, ins.yt-live-chat-legacy-paid-message-renderer, kbd.yt-live-chat-legacy-paid-message-renderer, label.yt-live-chat-legacy-paid-message-renderer, legend.yt-live-chat-legacy-paid-message-renderer, li.yt-live-chat-legacy-paid-message-renderer, menu.yt-live-chat-legacy-paid-message-renderer, object.yt-live-chat-legacy-paid-message-renderer, ol.yt-live-chat-legacy-paid-message-renderer, p.yt-live-chat-legacy-paid-message-renderer, pre.yt-live-chat-legacy-paid-message-renderer, q.yt-live-chat-legacy-paid-message-renderer, s.yt-live-chat-legacy-paid-message-renderer, samp.yt-live-chat-legacy-paid-message-renderer, small.yt-live-chat-legacy-paid-message-renderer, span.yt-live-chat-legacy-paid-message-renderer, strike.yt-live-chat-legacy-paid-message-renderer, strong.yt-live-chat-legacy-paid-message-renderer, sub.yt-live-chat-legacy-paid-message-renderer, sup.yt-live-chat-legacy-paid-message-renderer, table.yt-live-chat-legacy-paid-message-renderer, tbody.yt-live-chat-legacy-paid-message-renderer, td.yt-live-chat-legacy-paid-message-renderer, tfoot.yt-live-chat-legacy-paid-message-renderer, th.yt-live-chat-legacy-paid-message-renderer, thead.yt-live-chat-legacy-paid-message-renderer, tr.yt-live-chat-legacy-paid-message-renderer, tt.yt-live-chat-legacy-paid-message-renderer, u.yt-live-chat-legacy-paid-message-renderer, ul.yt-live-chat-legacy-paid-message-renderer, var.yt-live-chat-legacy-paid-message-renderer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-legacy-paid-message-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#timestamp.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: var(--yt-live-chat-item-timestamp-display, inline);
|
||||
margin: var(--yt-live-chat-item-timestamp-margin, 0 8px 0 0);
|
||||
color: var(--yt-live-chat-tertiary-text-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: block;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
#menu-button.yt-live-chat-legacy-paid-message-renderer {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-legacy-paid-message-renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translateX(100px);
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer:hover #menu.yt-live-chat-legacy-paid-message-renderer, yt-live-chat-legacy-paid-message-renderer[menu-visible] #menu.yt-live-chat-legacy-paid-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer:focus-within #menu.yt-live-chat-legacy-paid-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#inline-action-button-container.yt-live-chat-legacy-paid-message-renderer {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 0;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
background-color: var(--yt-live-chat-moderation-mode-hover-background-color);
|
||||
display: none;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[has-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
display: var(--yt-live-chat-inline-action-button-container-display, none);
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[has-inline-action-buttons][hide-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[has-inline-action-buttons]:hover #menu.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: var(--yt-live-chat-item-with-inline-actions-context-menu-display, block);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer, #additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer {
|
||||
--yt-button-icon-size: 36px;
|
||||
--yt-button-icon-padding: 6px;
|
||||
color: hsl(0, 0%, 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer {
|
||||
background: hsla(0, 0%, 6.7%, .8);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>.yt-live-chat-legacy-paid-message-renderer:hover {
|
||||
background: hsl(0, 0%, 6.7%);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer {
|
||||
color: var(--yt-live-chat-additional-inline-action-button-color);
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>.yt-live-chat-legacy-paid-message-renderer:hover {
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color-hover);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer:not(:empty) {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer:not(:first-child), #additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer {
|
||||
position: relative;
|
||||
display: block;
|
||||
--yt-live-chat-sponsor-color: #0f9d58;
|
||||
--yt-live-chat-item-timestamp-display: var(--yt-live-chat-paid-message-timestamp-display, none);
|
||||
padding: 4px 24px;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#card.yt-live-chat-legacy-paid-message-renderer {
|
||||
position: relative;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--yt-live-chat-sponsor-color);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #card.yt-live-chat-legacy-paid-message-renderer {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background-color: var(--yt-live-chat-background-color);
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-legacy-paid-message-renderer {
|
||||
-ms-align-self: flex-start;
|
||||
-webkit-align-self: flex-start;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
#author-name.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: block;
|
||||
margin-right: 8px;
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#content.yt-live-chat-legacy-paid-message-renderer {
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #content.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #content-primary-column.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: baseline;
|
||||
-webkit-align-items: baseline;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
#event-text.yt-live-chat-legacy-paid-message-renderer {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #event-text.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: inline;
|
||||
height: 24px;
|
||||
min-width: 16px;
|
||||
border-radius: 12px;
|
||||
margin-right: 8px;
|
||||
padding: 0 12px;
|
||||
background-color: var(--yt-live-chat-sponsor-color);
|
||||
color: hsl(0, 0%, 100%);
|
||||
display: inline-flex;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
#detail-text.yt-live-chat-legacy-paid-message-renderer {
|
||||
font-size: 15px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#detail-text.yt-live-chat-legacy-paid-message-renderer .emoji.yt-live-chat-legacy-paid-message-renderer {
|
||||
width: var(--yt-live-chat-emoji-size);
|
||||
height: var(--yt-live-chat-emoji-size);
|
||||
margin: -1px 2px 1px 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #detail-text.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a.yt-live-chat-legacy-paid-message-renderer {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#detail-text.yt-live-chat-legacy-paid-message-renderer a.yt-live-chat-legacy-paid-message-renderer {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#detail-text.yt-live-chat-legacy-paid-message-renderer a.yt-live-chat-legacy-paid-message-renderer .mention.yt-live-chat-legacy-paid-message-renderer {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-legacy-paid-message-renderer {
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-sponsor-color) 100%);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #menu.yt-live-chat-legacy-paid-message-renderer {
|
||||
margin-top: 8px;
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-background-color) 40%);
|
||||
}
|
||||
</style>
|
||||
50
frontend/src/components/ChatRenderer/MembershipItem.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<yt-live-chat-membership-item-renderer class="style-scope yt-live-chat-item-list-renderer" show-only-header>
|
||||
<div id="card" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<div id="header" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<img-shadow id="author-photo" height="40" width="40" class="style-scope yt-live-chat-membership-item-renderer"
|
||||
:imgUrl="avatarUrl"
|
||||
></img-shadow>
|
||||
<div id="header-content" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<div id="header-content-primary-column" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<div id="header-content-inner-column" class="style-scope yt-live-chat-membership-item-renderer">
|
||||
<author-chip class="style-scope yt-live-chat-membership-item-renderer"
|
||||
isInMemberMessage :authorName="authorName" :authorType="0" :privilegeType="privilegeType"
|
||||
></author-chip>
|
||||
</div>
|
||||
<div id="header-subtext" class="style-scope yt-live-chat-membership-item-renderer">{{ title }}</div>
|
||||
</div>
|
||||
<div id="timestamp" class="style-scope yt-live-chat-membership-item-renderer">{{ timeText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</yt-live-chat-membership-item-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImgShadow from './ImgShadow'
|
||||
import AuthorChip from './AuthorChip'
|
||||
import * as utils from '@/utils'
|
||||
|
||||
export default {
|
||||
name: 'MembershipItem',
|
||||
components: {
|
||||
ImgShadow,
|
||||
AuthorChip
|
||||
},
|
||||
props: {
|
||||
avatarUrl: String,
|
||||
authorName: String,
|
||||
privilegeType: Number,
|
||||
title: String,
|
||||
time: Date
|
||||
},
|
||||
computed: {
|
||||
timeText() {
|
||||
return utils.getTimeTextHourMin(this.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="@/assets/css/youtube/yt-live-chat-membership-item-renderer.css"></style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<yt-live-chat-paid-message-renderer class="style-scope yt-live-chat-item-list-renderer" allow-animations :style="{
|
||||
<yt-live-chat-paid-message-renderer class="style-scope yt-live-chat-item-list-renderer" allow-animations
|
||||
:show-only-header="!content" :style="{
|
||||
'--yt-live-chat-paid-message-primary-color': color.contentBg,
|
||||
'--yt-live-chat-paid-message-secondary-color': color.headerBg,
|
||||
'--yt-live-chat-paid-message-header-color': color.header,
|
||||
@@ -15,24 +16,23 @@
|
||||
></img-shadow>
|
||||
<div id="header-content" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="header-content-primary-column" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="author-name" class="style-scope yt-live-chat-paid-message-renderer">{{authorName}}</div>
|
||||
<div id="purchase-amount" class="style-scope yt-live-chat-paid-message-renderer">CN¥{{price}}</div>
|
||||
<div id="author-name" class="style-scope yt-live-chat-paid-message-renderer">{{ authorName }}</div>
|
||||
<div id="purchase-amount" class="style-scope yt-live-chat-paid-message-renderer">{{ priceText }}</div>
|
||||
</div>
|
||||
<span id="timestamp" class="style-scope yt-live-chat-paid-message-renderer">{{time}}</span>
|
||||
<span id="timestamp" class="style-scope yt-live-chat-paid-message-renderer">{{ timeText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="message" dir="auto" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
{{content}}
|
||||
</div>
|
||||
<div id="message" dir="auto" class="style-scope yt-live-chat-paid-message-renderer">{{ content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</yt-live-chat-paid-message-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import ImgShadow from './ImgShadow'
|
||||
import * as constants from './constants'
|
||||
import * as utils from '@/utils'
|
||||
|
||||
export default {
|
||||
name: 'PaidMessage',
|
||||
@@ -43,395 +43,21 @@ export default {
|
||||
avatarUrl: String,
|
||||
authorName: String,
|
||||
price: Number, // 价格,人民币
|
||||
time: String,
|
||||
time: Date,
|
||||
content: String
|
||||
},
|
||||
computed: {
|
||||
color() {
|
||||
return constants.getPriceConfig(this.price).colors
|
||||
},
|
||||
priceText() {
|
||||
return `CN¥${utils.formatCurrency(this.price)}`
|
||||
},
|
||||
timeText() {
|
||||
return utils.getTimeTextHourMin(this.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- yt-live-chat-paid-message-renderer -->
|
||||
<style>
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-paid-message-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#timestamp.yt-live-chat-paid-message-renderer {
|
||||
display: var(--yt-live-chat-item-timestamp-display, inline);
|
||||
margin: var(--yt-live-chat-item-timestamp-margin, 0 8px 0 0);
|
||||
color: var(--yt-live-chat-tertiary-text-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-paid-message-renderer {
|
||||
display: block;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
#menu-button.yt-live-chat-paid-message-renderer {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-paid-message-renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translateX(100px);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer:hover #menu.yt-live-chat-paid-message-renderer, yt-live-chat-paid-message-renderer[menu-visible] #menu.yt-live-chat-paid-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer:focus-within #menu.yt-live-chat-paid-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#inline-action-button-container.yt-live-chat-paid-message-renderer {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 0;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
background-color: var(--yt-live-chat-moderation-mode-hover-background-color);
|
||||
display: none;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[has-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
display: var(--yt-live-chat-inline-action-button-container-display, none);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[has-inline-action-buttons][hide-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-paid-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[has-inline-action-buttons]:hover #menu.yt-live-chat-paid-message-renderer {
|
||||
display: var(--yt-live-chat-item-with-inline-actions-context-menu-display, block);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer, #additional-inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer {
|
||||
--yt-button-icon-size: 36px;
|
||||
--yt-button-icon-padding: 6px;
|
||||
color: hsl(0, 0%, 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer {
|
||||
background: hsla(0, 0%, 6.7%, .8);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-paid-message-renderer>.yt-live-chat-paid-message-renderer:hover {
|
||||
background: hsl(0, 0%, 6.7%);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer {
|
||||
color: var(--yt-live-chat-additional-inline-action-button-color);
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-paid-message-renderer>.yt-live-chat-paid-message-renderer:hover {
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color-hover);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-paid-message-renderer:not(:empty) {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer:not(:first-child), #additional-inline-action-buttons.yt-live-chat-paid-message-renderer>*.yt-live-chat-paid-message-renderer:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 4px 24px;
|
||||
font-size: 15px;
|
||||
--yt-live-chat-paid-message-background-color: var(--yt-live-chat-paid-message-primary-color);
|
||||
--yt-live-chat-paid-message-header-background-color: var(--yt-live-chat-paid-message-secondary-color);
|
||||
--yt-live-chat-text-input-field-placeholder-color: var(--yt-live-chat-paid-message-color);
|
||||
--yt-live-chat-item-timestamp-display: var(--yt-live-chat-paid-message-timestamp-display, none);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] {
|
||||
padding: 0;
|
||||
padding-bottom: 8px;
|
||||
--yt-live-chat-paid-message-background-color: transparent;
|
||||
--yt-live-chat-paid-message-header-background-color: transparent;
|
||||
--yt-live-chat-item-timestamp-display: block;
|
||||
}
|
||||
|
||||
#card.yt-live-chat-paid-message-renderer {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #card.yt-live-chat-paid-message-renderer {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#header.yt-live-chat-paid-message-renderer {
|
||||
position: relative;
|
||||
background-color: var(--yt-live-chat-paid-message-header-background-color, #125aac);
|
||||
color: var(--yt-live-chat-paid-message-header-color, #fff);
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
min-height: 20px;
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[show-only-header] #header.yt-live-chat-paid-message-renderer {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #header.yt-live-chat-paid-message-renderer {
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-paid-message-renderer {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
yt-icon#author-photo.yt-live-chat-paid-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] yt-icon#author-photo.yt-live-chat-paid-message-renderer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#header-content.yt-live-chat-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
justify-content: space-between;
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
-ms-flex-align: baseline;
|
||||
-webkit-align-items: baseline;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #header-content.yt-live-chat-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#header-content-primary-column.yt-live-chat-paid-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-ms-flex-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
justify-content: space-between;
|
||||
-ms-flex: 1 1 0.000000001px;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0.000000001px;
|
||||
flex-basis: 0.000000001px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #header-content-primary-column.yt-live-chat-paid-message-renderer {
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: baseline;
|
||||
-webkit-align-items: baseline;
|
||||
align-items: baseline;
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
#author-name.yt-live-chat-paid-message-renderer {
|
||||
color: var(--yt-live-chat-paid-message-author-name-color, rgba(255, 255, 255, 0.7));
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #purchase-amount.yt-live-chat-paid-message-renderer {
|
||||
display: inline;
|
||||
height: 24px;
|
||||
min-width: 16px;
|
||||
border-radius: 12px;
|
||||
margin-right: 8px;
|
||||
padding: 0 12px;
|
||||
background-color: var(--yt-live-chat-paid-message-primary-color);
|
||||
color: var(--yt-live-chat-paid-message-color);
|
||||
display: inline-flex;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer {
|
||||
display: block;
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer[is-deleted] {
|
||||
margin: 4px 0 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer[is-deleted].yt-live-chat-paid-message-renderer::before {
|
||||
content: '​';
|
||||
display: inline;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-paid-message-renderer {
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-paid-message-header-background-color, #125aac) 100%);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #menu.yt-live-chat-paid-message-renderer {
|
||||
margin-top: 8px;
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-background-color) 40%);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[allow-animations] #content.yt-live-chat-paid-message-renderer, yt-live-chat-paid-message-renderer[allow-animations] #header.yt-live-chat-paid-message-renderer {
|
||||
transition-property: background-color, color;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
#content.yt-live-chat-paid-message-renderer {
|
||||
background-color: var(--yt-live-chat-paid-message-background-color, #1565c0);
|
||||
color: var(--yt-live-chat-paid-message-color, #fff);
|
||||
padding: 8px 16px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
#content.yt-live-chat-paid-message-renderer img.yt-live-chat-paid-message-renderer {
|
||||
width: var(--yt-live-chat-emoji-size);
|
||||
height: var(--yt-live-chat-emoji-size);
|
||||
margin: -1px 2px 1px 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #content.yt-live-chat-paid-message-renderer {
|
||||
padding: 0;
|
||||
padding-left: 72px;
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[show-only-header] #content.yt-live-chat-paid-message-renderer {
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a.yt-live-chat-paid-message-renderer {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-paid-message-renderer a.yt-live-chat-paid-message-renderer {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #message.yt-live-chat-paid-message-renderer {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-paid-message-renderer a.yt-live-chat-paid-message-renderer .mention.yt-live-chat-paid-message-renderer {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-paid-message-renderer:empty, #author-name.yt-live-chat-paid-message-renderer:empty:not([is-deleted]).yt-live-chat-paid-message-renderer, #deleted-state.yt-live-chat-paid-message-renderer:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#deleted-state.yt-live-chat-paid-message-renderer {
|
||||
display: block;
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #deleted-state.yt-live-chat-paid-message-renderer[is-deleted] {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-paid-message-renderer {
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[show-footer-divider] #footer.yt-live-chat-paid-message-renderer::before {
|
||||
content: "";
|
||||
display: flex;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer[is-user-editable] #footer.yt-live-chat-paid-message-renderer:not(:empty) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
</style>
|
||||
<style src="@/assets/css/youtube/yt-live-chat-paid-message-renderer.css"></style>
|
||||
|
||||
@@ -4,51 +4,61 @@
|
||||
:imgUrl="avatarUrl"
|
||||
></img-shadow>
|
||||
<div id="content" class="style-scope yt-live-chat-text-message-renderer">
|
||||
<span id="timestamp" class="style-scope yt-live-chat-text-message-renderer">{{time}}</span>
|
||||
<yt-live-chat-author-chip class="style-scope yt-live-chat-text-message-renderer">
|
||||
<span id="author-name" dir="auto" class="style-scope yt-live-chat-author-chip" :type="authorTypeText">
|
||||
{{authorName}}
|
||||
<!-- 这里是已验证勋章 -->
|
||||
<span id="chip-badges" class="style-scope yt-live-chat-author-chip"></span>
|
||||
</span>
|
||||
<span id="chat-badges" class="style-scope yt-live-chat-author-chip">
|
||||
<author-badge class="style-scope yt-live-chat-author-chip"
|
||||
:isAdmin="authorType === 2" :privilegeType="privilegeType"
|
||||
></author-badge>
|
||||
</span>
|
||||
</yt-live-chat-author-chip>
|
||||
<span id="message" class="style-scope yt-live-chat-text-message-renderer">{{content}}</span>
|
||||
<el-badge :value="repeated" :max="99" v-show="repeated > 1" class="style-scope yt-live-chat-text-message-renderer"
|
||||
:style="{'--repeated-mark-color': repeatedMarkColor}"
|
||||
></el-badge>
|
||||
<span id="timestamp" class="style-scope yt-live-chat-text-message-renderer">{{ timeText }}</span>
|
||||
<author-chip class="style-scope yt-live-chat-text-message-renderer"
|
||||
: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>
|
||||
<img :key="index" v-else-if="content.type === CONTENT_TYPE_IMAGE"
|
||||
class="emoji yt-formatted-string style-scope yt-live-chat-text-message-renderer"
|
||||
:src="content.url" :alt="content.text" :shared-tooltip-text="content.text" :id="`emoji-${content.text}`"
|
||||
>
|
||||
</template>
|
||||
<el-badge :value="repeated" :max="99" v-if="repeated > 1" class="style-scope yt-live-chat-text-message-renderer"
|
||||
:style="{ '--repeated-mark-color': repeatedMarkColor }"
|
||||
></el-badge>
|
||||
</span>
|
||||
</div>
|
||||
</yt-live-chat-text-message-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import AuthorBadge from './AuthorBadge.vue'
|
||||
import ImgShadow from './ImgShadow'
|
||||
import AuthorChip from './AuthorChip'
|
||||
import * as constants from './constants'
|
||||
import * as utils from '@/utils'
|
||||
|
||||
const REPEATED_MARK_COLOR_START = [64, 158, 255]
|
||||
const REPEATED_MARK_COLOR_END = [245, 108, 108]
|
||||
// HSL
|
||||
const REPEATED_MARK_COLOR_START = [210, 100.0, 62.5]
|
||||
const REPEATED_MARK_COLOR_END = [360, 87.3, 69.2]
|
||||
|
||||
export default {
|
||||
name: 'TextMessage',
|
||||
components: {
|
||||
ImgShadow,
|
||||
AuthorBadge
|
||||
AuthorChip
|
||||
},
|
||||
props: {
|
||||
avatarUrl: String,
|
||||
time: String,
|
||||
time: Date,
|
||||
authorName: String,
|
||||
authorType: Number,
|
||||
content: String,
|
||||
richContent: Array,
|
||||
privilegeType: Number,
|
||||
repeated: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CONTENT_TYPE_TEXT: constants.CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_IMAGE: constants.CONTENT_TYPE_IMAGE
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
timeText() {
|
||||
return utils.getTimeTextHourMin(this.time)
|
||||
},
|
||||
authorTypeText() {
|
||||
return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
|
||||
},
|
||||
@@ -62,328 +72,29 @@ export default {
|
||||
color = [0, 0, 0]
|
||||
let t = (this.repeated - 2) / (10 - 2)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
color[i] = REPEATED_MARK_COLOR_START[i] + (REPEATED_MARK_COLOR_END[i] - REPEATED_MARK_COLOR_START[i]) * t
|
||||
color[i] = REPEATED_MARK_COLOR_START[i] + ((REPEATED_MARK_COLOR_END[i] - REPEATED_MARK_COLOR_START[i]) * t)
|
||||
}
|
||||
}
|
||||
return `rgb(${color.join(', ')})`
|
||||
return `hsl(${color[0]}, ${color[1]}%, ${color[2]}%)`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
yt-live-chat-text-message-renderer>#content>.el-badge {
|
||||
yt-live-chat-text-message-renderer>#content>#message>.el-badge {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer>#content>.el-badge .el-badge__content {
|
||||
yt-live-chat-text-message-renderer>#content>#message>.el-badge .el-badge__content {
|
||||
font-size: 12px !important;
|
||||
line-height: 18px !important;
|
||||
text-shadow: none !important;
|
||||
font-family: sans-serif !important;
|
||||
color: #FFF !important;
|
||||
background-color: var(--repeated-mark-color) !important;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- yt-live-chat-text-message-renderer -->
|
||||
<style>
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-text-message-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#timestamp.yt-live-chat-text-message-renderer {
|
||||
display: var(--yt-live-chat-item-timestamp-display, inline);
|
||||
margin: var(--yt-live-chat-item-timestamp-margin, 0 8px 0 0);
|
||||
color: var(--yt-live-chat-tertiary-text-color);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#author-photo.yt-live-chat-text-message-renderer {
|
||||
display: block;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
-ms-flex: none;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
#menu-button.yt-live-chat-text-message-renderer {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-text-message-renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translateX(100px);
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer:hover #menu.yt-live-chat-text-message-renderer, yt-live-chat-text-message-renderer[menu-visible] #menu.yt-live-chat-text-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer:focus-within #menu.yt-live-chat-text-message-renderer {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#inline-action-button-container.yt-live-chat-text-message-renderer {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 0;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
background-color: var(--yt-live-chat-moderation-mode-hover-background-color);
|
||||
display: none;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[has-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-text-message-renderer {
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
display: var(--yt-live-chat-inline-action-button-container-display, none);
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[has-inline-action-buttons][hide-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-text-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[has-inline-action-buttons]:hover #menu.yt-live-chat-text-message-renderer {
|
||||
display: var(--yt-live-chat-item-with-inline-actions-context-menu-display, block);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer, #additional-inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer {
|
||||
--yt-button-icon-size: 36px;
|
||||
--yt-button-icon-padding: 6px;
|
||||
color: hsl(0, 0%, 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer {
|
||||
background: hsla(0, 0%, 6.7%, .8);
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-text-message-renderer>.yt-live-chat-text-message-renderer:hover {
|
||||
background: hsl(0, 0%, 6.7%);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer {
|
||||
color: var(--yt-live-chat-additional-inline-action-button-color);
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-text-message-renderer>.yt-live-chat-text-message-renderer:hover {
|
||||
background: var(--yt-live-chat-additional-inline-action-button-background-color-hover);
|
||||
}
|
||||
|
||||
#additional-inline-action-buttons.yt-live-chat-text-message-renderer:not(:empty) {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
#inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer:not(:first-child), #additional-inline-action-buttons.yt-live-chat-text-message-renderer>*.yt-live-chat-text-message-renderer:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer {
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
padding: 4px 24px;
|
||||
overflow: hidden;
|
||||
--yt-endpoint-color: var(--yt-live-chat-primary-text-color, hsl(0, 0%, 6.7%));
|
||||
--yt-endpoint-hover-color: var(--yt-live-chat-primary-text-color, var(--yt-endpoint-color));
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: start;
|
||||
-webkit-align-items: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer:hover {
|
||||
overflow: initial;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[author-is-owner] {
|
||||
background-color: var(--yt-live-chat-message-highlight-background-color);
|
||||
}
|
||||
|
||||
#content.yt-live-chat-text-message-renderer {
|
||||
-ms-align-self: center;
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip.yt-live-chat-text-message-renderer {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer {
|
||||
color: var(--yt-live-chat-primary-text-color, var(--yt-primary-text-color));
|
||||
line-height: 16px;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
font-style: var(--yt-live-chat-text-message-renderer-message-message-style_-_font-style);
|
||||
opacity: var(--yt-live-chat-text-message-renderer-message-message-style_-_opacity);
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer .emoji.yt-live-chat-text-message-renderer {
|
||||
width: var(--yt-live-chat-emoji-size);
|
||||
height: var(--yt-live-chat-emoji-size);
|
||||
margin: -1px 2px 1px 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
a.yt-live-chat-text-message-renderer {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer a.yt-live-chat-text-message-renderer {
|
||||
display: inline;
|
||||
text-decoration: underline;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer a.yt-live-chat-text-message-renderer .mention.yt-live-chat-text-message-renderer {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#show-original.yt-live-chat-text-message-renderer {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
#message.yt-live-chat-text-message-renderer:empty, #deleted-state.yt-live-chat-text-message-renderer:empty, #show-original.yt-live-chat-text-message-renderer:empty, yt-live-chat-text-message-renderer[show-original] #deleted-state.yt-live-chat-text-message-renderer, yt-live-chat-text-message-renderer[show-original] #show-original.yt-live-chat-text-message-renderer, yt-live-chat-text-message-renderer[is-deleted]:not([show-original]) #message.yt-live-chat-text-message-renderer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#menu.yt-live-chat-text-message-renderer {
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
background: linear-gradient(to right, transparent, var(--yt-live-chat-background-color, hsl(0, 0%, 100%)) 100%);
|
||||
}
|
||||
|
||||
.mention.yt-live-chat-text-message-renderer {
|
||||
background: var(--yt-live-chat-mention-background-color);
|
||||
color: var(--yt-live-chat-mention-text-color);
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#deleted-state.yt-live-chat-text-message-renderer, #show-original.yt-live-chat-text-message-renderer, yt-live-chat-text-message-renderer[is-deleted] #message.yt-live-chat-text-message-renderer {
|
||||
font-style: italic;
|
||||
color: var(--yt-live-chat-deleted-message-color, rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[show-bar]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 8px;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 4px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[is-deleted]::before {
|
||||
background: var(--yt-live-chat-deleted-message-bar-color, rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[is-dimmed] #message.yt-live-chat-text-message-renderer {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[is-dimmed]::before {
|
||||
background: var(--yt-live-chat-error-message-color, #f44336);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- yt-live-chat-author-chip -->
|
||||
<style>
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-author-chip[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip {
|
||||
display: inline-flex;
|
||||
-ms-flex-align: baseline;
|
||||
-webkit-align-items: baseline;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
#author-name.yt-live-chat-author-chip {
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
color: var(--yt-live-chat-secondary-text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip[is-highlighted] #author-name.yt-live-chat-author-chip {
|
||||
padding: 2px 4px;
|
||||
color: var(--yt-live-chat-author-chip-verified-text-color);
|
||||
background-color: var(--yt-live-chat-author-chip-verified-background-color);
|
||||
}
|
||||
|
||||
#author-name.yt-live-chat-author-chip[type='moderator'] {
|
||||
color: var(--yt-live-chat-moderator-color);
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip[is-highlighted] #author-name.yt-live-chat-author-chip[type='owner'], #author-name.yt-live-chat-author-chip[type='owner'] {
|
||||
background-color: #ffd600;
|
||||
color: var(--yt-live-chat-author-chip-owner-text-color);
|
||||
}
|
||||
|
||||
#author-name.yt-live-chat-author-chip[type='member'] {
|
||||
color: var(--yt-live-chat-sponsor-color);
|
||||
}
|
||||
|
||||
#chip-badges.yt-live-chat-author-chip:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip[is-highlighted] #chat-badges.yt-live-chat-author-chip:not(:empty) {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
yt-live-chat-author-badge-renderer.yt-live-chat-author-chip {
|
||||
margin: 0 0 0 2px;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip[is-highlighted] #chip-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer.yt-live-chat-author-chip {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#chip-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer.yt-live-chat-author-chip:last-of-type {
|
||||
margin-right: -2px;
|
||||
}
|
||||
</style>
|
||||
<style src="@/assets/css/youtube/yt-live-chat-text-message-renderer.css"></style>
|
||||
|
||||
@@ -1,96 +1,148 @@
|
||||
<template>
|
||||
<yt-live-chat-ticker-renderer>
|
||||
<yt-live-chat-ticker-renderer :hidden="showMessages.length === 0">
|
||||
<div id="container" dir="ltr" class="style-scope yt-live-chat-ticker-renderer">
|
||||
<div id="items" class="style-scope yt-live-chat-ticker-renderer">
|
||||
<template v-for="message in messages">
|
||||
<yt-live-chat-ticker-paid-message-item-renderer :key="message.id" v-if="needToShow(message)"
|
||||
tabindex="0" class="style-scope yt-live-chat-ticker-renderer" style="overflow: hidden;"
|
||||
@click="onItemClick(message)"
|
||||
>
|
||||
<div id="container" dir="ltr" class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
|
||||
:style="{
|
||||
background: getBgColor(message),
|
||||
}"
|
||||
>
|
||||
<div id="content" class="style-scope yt-live-chat-ticker-paid-message-item-renderer" :style="{
|
||||
color: getColor(message)
|
||||
}">
|
||||
<img-shadow id="author-photo" height="24" width="24" class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
|
||||
:imgUrl="message.avatarUrl"
|
||||
></img-shadow>
|
||||
<span id="text" dir="ltr" class="style-scope yt-live-chat-ticker-paid-message-item-renderer">{{getText(message)}}</span>
|
||||
</div>
|
||||
<transition-group tag="div" :css="false" @enter="onTickerItemEnter" @leave="onTickerItemLeave"
|
||||
id="items" class="style-scope yt-live-chat-ticker-renderer"
|
||||
>
|
||||
<yt-live-chat-ticker-paid-message-item-renderer v-for="message in showMessages" :key="message.raw.id"
|
||||
tabindex="0" class="style-scope yt-live-chat-ticker-renderer" style="overflow: hidden;"
|
||||
@click="onItemClick(message.raw)"
|
||||
>
|
||||
<div id="container" dir="ltr" class="style-scope yt-live-chat-ticker-paid-message-item-renderer" :style="{
|
||||
background: message.bgColor,
|
||||
}">
|
||||
<div id="content" class="style-scope yt-live-chat-ticker-paid-message-item-renderer" :style="{
|
||||
color: message.color
|
||||
}">
|
||||
<img-shadow id="author-photo" height="24" width="24" class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
|
||||
:imgUrl="message.raw.avatarUrl"
|
||||
></img-shadow>
|
||||
<span id="text" dir="ltr" class="style-scope yt-live-chat-ticker-paid-message-item-renderer">{{ message.text }}</span>
|
||||
</div>
|
||||
</yt-live-chat-ticker-paid-message-item-renderer>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</yt-live-chat-ticker-paid-message-item-renderer>
|
||||
</transition-group>
|
||||
</div>
|
||||
<template v-if="pinnedMessage">
|
||||
<legacy-paid-message :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
|
||||
<membership-item :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
|
||||
class="style-scope yt-live-chat-ticker-renderer"
|
||||
:avatarUrl="pinnedMessage.avatarUrl" :title="pinnedMessage.title" :content="pinnedMessage.content"
|
||||
:time="pinnedMessage.time"
|
||||
></legacy-paid-message>
|
||||
:avatarUrl="pinnedMessage.avatarUrl" :authorName="getShowAuthorName(pinnedMessage)" :privilegeType="pinnedMessage.privilegeType"
|
||||
:title="pinnedMessage.title" :time="pinnedMessage.time"
|
||||
></membership-item>
|
||||
<paid-message :key="pinnedMessage.id" v-else
|
||||
class="style-scope yt-live-chat-ticker-renderer"
|
||||
:price="pinnedMessage.price" :avatarUrl="pinnedMessage.avatarUrl" :authorName="pinnedMessage.authorName"
|
||||
:time="pinnedMessage.time" :content="pinnedMessage.content"
|
||||
:price="pinnedMessage.price" :avatarUrl="pinnedMessage.avatarUrl" :authorName="getShowAuthorName(pinnedMessage)"
|
||||
:time="pinnedMessage.time" :content="pinnedMessageShowContent"
|
||||
></paid-message>
|
||||
</template>
|
||||
</yt-live-chat-ticker-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import LegacyPaidMessage from './LegacyPaidMessage.vue'
|
||||
import PaidMessage from './PaidMessage.vue'
|
||||
import * as chatConfig from '@/api/chatConfig'
|
||||
import { formatCurrency } from '@/utils'
|
||||
import ImgShadow from './ImgShadow'
|
||||
import MembershipItem from './MembershipItem'
|
||||
import PaidMessage from './PaidMessage'
|
||||
import * as constants from './constants'
|
||||
|
||||
export default {
|
||||
name: 'Ticker',
|
||||
components: {
|
||||
ImgShadow,
|
||||
LegacyPaidMessage,
|
||||
MembershipItem,
|
||||
PaidMessage
|
||||
},
|
||||
props: {
|
||||
messages: Array
|
||||
messages: Array,
|
||||
showGiftName: {
|
||||
type: Boolean,
|
||||
default: chatConfig.DEFAULT_CONFIG.showGiftName
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
MESSAGE_TYPE_MEMBER: constants.MESSAGE_TYPE_MEMBER,
|
||||
|
||||
updateTimerId: window.setInterval(this.updateProgress.bind(this), 1000),
|
||||
curTime: new Date(),
|
||||
updateTimerId: window.setInterval(this.updateProgress, 1000),
|
||||
pinnedMessage: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showMessages() {
|
||||
let res = []
|
||||
for (let message of this.messages) {
|
||||
if (!this.needToShow(message)) {
|
||||
continue
|
||||
}
|
||||
res.push({
|
||||
raw: message,
|
||||
bgColor: this.getBgColor(message),
|
||||
color: this.getColor(message),
|
||||
text: this.getText(message)
|
||||
})
|
||||
}
|
||||
return res
|
||||
},
|
||||
pinnedMessageShowContent() {
|
||||
if (!this.pinnedMessage) {
|
||||
return ''
|
||||
}
|
||||
if (this.pinnedMessage.type === constants.MESSAGE_TYPE_GIFT) {
|
||||
return constants.getGiftShowContent(this.pinnedMessage, this.showGiftName)
|
||||
} else {
|
||||
return constants.getShowContent(this.pinnedMessage)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.clearInterval(this.updateTimerId)
|
||||
},
|
||||
methods: {
|
||||
needToShow(message) {
|
||||
let pinTime
|
||||
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
pinTime = 2
|
||||
} else {
|
||||
let config = constants.getPriceConfig(message.price)
|
||||
pinTime = config.pinTime
|
||||
async onTickerItemEnter(el, done) {
|
||||
let width = el.clientWidth
|
||||
if (width === 0) {
|
||||
// CSS指定了不显示固定栏
|
||||
done()
|
||||
return
|
||||
}
|
||||
el.style.width = 0
|
||||
await this.$nextTick()
|
||||
el.style.width = `${width}px`
|
||||
window.setTimeout(done, 200)
|
||||
},
|
||||
onTickerItemLeave(el, done) {
|
||||
el.classList.add('sliding-down')
|
||||
window.setTimeout(() => {
|
||||
el.classList.add('collapsing')
|
||||
el.style.width = 0
|
||||
window.setTimeout(() => {
|
||||
el.classList.remove('sliding-down')
|
||||
el.classList.remove('collapsing')
|
||||
el.style.width = 'auto'
|
||||
done()
|
||||
}, 200)
|
||||
}, 200)
|
||||
},
|
||||
|
||||
getShowAuthorName: constants.getShowAuthorName,
|
||||
needToShow(message) {
|
||||
let pinTime = this.getPinTime(message)
|
||||
return (new Date() - message.addTime) / (60 * 1000) < pinTime
|
||||
},
|
||||
getBgColor(message) {
|
||||
let color1, color2, pinTime
|
||||
let color1, color2
|
||||
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
color1 = 'rgba(15,157,88,1)'
|
||||
color2 = 'rgba(11,128,67,1)'
|
||||
pinTime = 2
|
||||
} else {
|
||||
let config = constants.getPriceConfig(message.type === constants.MESSAGE_TYPE_MEMBER ? 28 : message.price)
|
||||
let config = constants.getPriceConfig(message.price)
|
||||
color1 = config.colors.contentBg
|
||||
color2 = config.colors.headerBg
|
||||
pinTime = config.pinTime
|
||||
}
|
||||
let progress = (1 - (new Date() - message.addTime) / (60 * 1000) / pinTime) * 100
|
||||
let pinTime = this.getPinTime(message)
|
||||
let progress = (1 - ((this.curTime - message.addTime) / (60 * 1000) / pinTime)) * 100
|
||||
if (progress < 0) {
|
||||
progress = 0
|
||||
} else if (progress > 100) {
|
||||
@@ -106,31 +158,37 @@ export default {
|
||||
},
|
||||
getText(message) {
|
||||
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
return '会员'
|
||||
return this.$t('chat.tickerMembership')
|
||||
}
|
||||
return `CN¥${message.price}`
|
||||
return `CN¥${formatCurrency(message.price)}`
|
||||
},
|
||||
getPinTime(message) {
|
||||
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
return 2
|
||||
}
|
||||
return constants.getPriceConfig(message.price).pinTime
|
||||
},
|
||||
updateProgress() {
|
||||
let curTime = new Date()
|
||||
for (let i = 0; i < this.messages.length;) {
|
||||
let pinTime
|
||||
if (this.messages[i].type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
pinTime = 2
|
||||
} else {
|
||||
let config = constants.getPriceConfig(this.messages[i].price)
|
||||
pinTime = config.pinTime
|
||||
}
|
||||
if ((curTime - this.messages[i].addTime) / (60 * 1000) >= pinTime) {
|
||||
if (this.pinnedMessage == this.messages[i]) {
|
||||
// 更新进度
|
||||
this.curTime = new Date()
|
||||
|
||||
// 删除过期的消息
|
||||
let filteredMessages = []
|
||||
let messagesChanged = false
|
||||
for (let message of this.messages) {
|
||||
let pinTime = this.getPinTime(message)
|
||||
if ((this.curTime - message.addTime) / (60 * 1000) >= pinTime) {
|
||||
messagesChanged = true
|
||||
if (this.pinnedMessage === message) {
|
||||
this.pinnedMessage = null
|
||||
}
|
||||
this.messages.splice(i, 1)
|
||||
} else {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
filteredMessages.push(message)
|
||||
}
|
||||
if (messagesChanged) {
|
||||
this.$emit('update:messages', filteredMessages)
|
||||
}
|
||||
|
||||
this.$forceUpdate()
|
||||
},
|
||||
onItemClick(message) {
|
||||
if (this.pinnedMessage == message) {
|
||||
@@ -143,155 +201,5 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- yt-live-chat-ticker-renderer -->
|
||||
<style>
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-ticker-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-renderer {
|
||||
display: block;
|
||||
background-color: var(--yt-live-chat-header-background-color);
|
||||
}
|
||||
|
||||
#container.yt-live-chat-ticker-renderer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#items.yt-live-chat-ticker-renderer {
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 0 24px 8px 24px;
|
||||
}
|
||||
|
||||
#items.yt-live-chat-ticker-renderer>*.yt-live-chat-ticker-renderer {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#left-arrow-container.yt-live-chat-ticker-renderer {
|
||||
background: linear-gradient(to right, var(--yt-live-chat-ticker-arrow-background) 0, var(--yt-live-chat-ticker-arrow-background) 52px, transparent 60px);
|
||||
left: 0;
|
||||
padding: 0 16px 0 12px;
|
||||
}
|
||||
|
||||
#right-arrow-container.yt-live-chat-ticker-renderer {
|
||||
background: linear-gradient(to left, var(--yt-live-chat-ticker-arrow-background) 0, var(--yt-live-chat-ticker-arrow-background) 52px, transparent 60px);
|
||||
right: 0;
|
||||
padding: 0 12px 0 16px;
|
||||
}
|
||||
|
||||
#container.yt-live-chat-ticker-renderer:hover #left-arrow-container.yt-live-chat-ticker-renderer, #container.yt-live-chat-ticker-renderer:hover #right-arrow-container.yt-live-chat-ticker-renderer {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#left-arrow-container.yt-live-chat-ticker-renderer, #right-arrow-container.yt-live-chat-ticker-renderer {
|
||||
height: 32px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
transition: opacity 0.3s 0.1s;
|
||||
}
|
||||
|
||||
yt-icon.yt-live-chat-ticker-renderer {
|
||||
background-color: #2196f3;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
padding: 4px;
|
||||
width: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- yt-live-chat-ticker-paid-message-item-renderer -->
|
||||
<style>
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-ticker-paid-message-item-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
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;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.yt-live-chat-ticker-paid-message-item-renderer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: width 0.2s;
|
||||
vertical-align: top;
|
||||
cursor: pointer;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#container.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
border-radius: 999px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer.sliding-down #container.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
opacity: 0.5;
|
||||
transform: translateY(44px);
|
||||
transition: opacity 0.2s, transform 0.2s cubic-bezier(0.4, 0.0, 1, 1);
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer.collapsing {
|
||||
margin-right: 0;
|
||||
transition: margin-right 0.2s cubic-bezier(0.4, 0.0, 0.2, 1), width 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer[dimmed] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
yt-img-shadow.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
margin-right: -4px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#content.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
-ms-flex-direction: row;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-align: center;
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#text.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
margin: 0 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer[is-deleted] #author-photo.yt-live-chat-ticker-paid-message-item-renderer {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<style src="@/assets/css/youtube/yt-live-chat-ticker-renderer.css"></style>
|
||||
<style src="@/assets/css/youtube/yt-live-chat-ticker-paid-message-item-renderer.css"></style>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import * as i18n from '@/i18n'
|
||||
|
||||
export const AUTHRO_TYPE_NORMAL = 0
|
||||
export const AUTHRO_TYPE_MEMBER = 1
|
||||
export const AUTHRO_TYPE_ADMIN = 2
|
||||
@@ -10,20 +12,36 @@ export const AUTHOR_TYPE_TO_TEXT = [
|
||||
'owner' // 主播
|
||||
]
|
||||
|
||||
export const GUARD_LEVEL_TO_TEXT = [
|
||||
const GUARD_LEVEL_TO_TEXT_KEY = [
|
||||
'',
|
||||
'总督',
|
||||
'提督',
|
||||
'舰长'
|
||||
'chat.guardLevel1',
|
||||
'chat.guardLevel2',
|
||||
'chat.guardLevel3'
|
||||
]
|
||||
|
||||
export const MESSAGE_TYPE_TEXT = 0
|
||||
export const MESSAGE_TYPE_MEMBER = 1
|
||||
export const MESSAGE_TYPE_GIFT = 2
|
||||
export function getShowGuardLevelText(guardLevel) {
|
||||
let key = GUARD_LEVEL_TO_TEXT_KEY[guardLevel] || ''
|
||||
if (key === '') {
|
||||
return ''
|
||||
}
|
||||
return i18n.i18n.t(key)
|
||||
}
|
||||
|
||||
export const MESSAGE_TYPE_TEXT = 0
|
||||
export const MESSAGE_TYPE_GIFT = 1
|
||||
export const MESSAGE_TYPE_MEMBER = 2
|
||||
export const MESSAGE_TYPE_SUPER_CHAT = 3
|
||||
export const MESSAGE_TYPE_DEL = 4
|
||||
export const MESSAGE_TYPE_UPDATE = 5
|
||||
|
||||
export const CONTENT_TYPE_TEXT = 0
|
||||
export const CONTENT_TYPE_IMAGE = 1
|
||||
|
||||
// 美元 -> 人民币 汇率
|
||||
const EXCHANGE_RATE = 7
|
||||
export const PRICE_CONFIGS = [
|
||||
{ // $100红
|
||||
price: 1245, // >= 1245,小电视飞船
|
||||
price: 100 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(230,33,23,1)',
|
||||
headerBg: 'rgba(208,0,0,1)',
|
||||
@@ -35,7 +53,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 60
|
||||
},
|
||||
{ // $50品红
|
||||
price: 450, // 450~1245,摩天大楼
|
||||
price: 50 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(233,30,99,1)',
|
||||
headerBg: 'rgba(194,24,91,1)',
|
||||
@@ -47,7 +65,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 30
|
||||
},
|
||||
{ // $20橙
|
||||
price: 100, // 100~450,节奏风暴、天空之翼
|
||||
price: 20 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(245,124,0,1)',
|
||||
headerBg: 'rgba(230,81,0,1)',
|
||||
@@ -59,7 +77,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 10
|
||||
},
|
||||
{ // $10黄
|
||||
price: 52, // 52~100,疯狂打call
|
||||
price: 10 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(255,202,40,1)',
|
||||
headerBg: 'rgba(255,179,0,1)',
|
||||
@@ -71,7 +89,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 5
|
||||
},
|
||||
{ // $5绿
|
||||
price: 28, // 28~52,礼花
|
||||
price: 5 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(29,233,182,1)',
|
||||
headerBg: 'rgba(0,191,165,1)',
|
||||
@@ -83,7 +101,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 2
|
||||
},
|
||||
{ // $2浅蓝
|
||||
price: 9.9, // 9.9~28,B坷垃
|
||||
price: 2 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(0,229,255,1)',
|
||||
headerBg: 'rgba(0,184,212,1)',
|
||||
@@ -95,7 +113,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 0
|
||||
},
|
||||
{ // $1蓝
|
||||
price: 0, // 0~9.9,丢人
|
||||
price: EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(30,136,229,1)',
|
||||
headerBg: 'rgba(21,101,192,1)',
|
||||
@@ -108,7 +126,7 @@ export const PRICE_CONFIGS = [
|
||||
}
|
||||
]
|
||||
|
||||
export function getPriceConfig (price) {
|
||||
export function getPriceConfig(price) {
|
||||
for (const config of PRICE_CONFIGS) {
|
||||
if (price >= config.price) {
|
||||
return config
|
||||
@@ -116,3 +134,35 @@ export function getPriceConfig (price) {
|
||||
}
|
||||
return PRICE_CONFIGS[PRICE_CONFIGS.length - 1]
|
||||
}
|
||||
|
||||
export function getShowContent(message) {
|
||||
if (message.translation) {
|
||||
return `${message.content}(${message.translation})`
|
||||
}
|
||||
return message.content
|
||||
}
|
||||
|
||||
export function getShowRichContent(message) {
|
||||
let richContent = [...message.richContent]
|
||||
if (message.translation) {
|
||||
richContent.push({
|
||||
type: CONTENT_TYPE_TEXT,
|
||||
text: `(${message.translation})`
|
||||
})
|
||||
}
|
||||
return richContent
|
||||
}
|
||||
|
||||
export function getGiftShowContent(message, showGiftName) {
|
||||
if (!showGiftName) {
|
||||
return ''
|
||||
}
|
||||
return i18n.i18n.t('chat.sendGift', { giftName: message.giftName, num: message.num })
|
||||
}
|
||||
|
||||
export function getShowAuthorName(message) {
|
||||
if (message.authorNamePronunciation && message.authorNamePronunciation !== message.authorName) {
|
||||
return `${message.authorName}(${message.authorNamePronunciation})`
|
||||
}
|
||||
return message.authorName
|
||||
}
|
||||
|
||||
49
frontend/src/i18n.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
||||
import zh from '@/lang/zh'
|
||||
|
||||
let lastSetLocale = 'zh'
|
||||
let loadedLocales = ['zh']
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
export async function setLocale(locale) {
|
||||
lastSetLocale = locale
|
||||
if (loadedLocales.indexOf(locale) === -1) {
|
||||
// eslint-disable-next-line prefer-template
|
||||
let langModule = await import('@/lang/' + locale)
|
||||
i18n.setLocaleMessage(locale, langModule.default)
|
||||
loadedLocales.push(locale)
|
||||
|
||||
// 加载完成之前又调用了setLocale,这次的不生效
|
||||
if (locale !== lastSetLocale) {
|
||||
return
|
||||
}
|
||||
}
|
||||
window.localStorage.lang = i18n.locale = locale
|
||||
}
|
||||
|
||||
export const i18n = new VueI18n({
|
||||
locale: 'zh',
|
||||
fallbackLocale: 'zh',
|
||||
messages: {
|
||||
zh
|
||||
}
|
||||
})
|
||||
|
||||
function getDefaultLocale() {
|
||||
let locale = window.localStorage.lang
|
||||
if (!locale) {
|
||||
let lang = navigator.language
|
||||
if (lang.startsWith('zh')) {
|
||||
locale = 'zh'
|
||||
} else if (lang.startsWith('ja')) {
|
||||
locale = 'ja'
|
||||
} else {
|
||||
locale = 'en'
|
||||
}
|
||||
}
|
||||
return locale
|
||||
}
|
||||
setLocale(getDefaultLocale())
|
||||
@@ -1,8 +1,10 @@
|
||||
export default {
|
||||
sidebar: {
|
||||
home: 'Home',
|
||||
stylegen: 'Style generator',
|
||||
projectAddress: 'Project address'
|
||||
stylegen: 'Style Generator',
|
||||
help: 'Help',
|
||||
projectAddress: 'Project Address',
|
||||
giftRecordOfficial: 'Official Super Chat Record',
|
||||
},
|
||||
home: {
|
||||
roomIdEmpty: "Room ID can't be empty",
|
||||
@@ -12,34 +14,52 @@ export default {
|
||||
roomId: 'Room ID',
|
||||
showDanmaku: 'Show messages',
|
||||
showGift: 'Show Super Chats',
|
||||
showGiftName: 'Show gift name',
|
||||
mergeSimilarDanmaku: 'Merge similar messages',
|
||||
mergeGift: 'Merge gifts',
|
||||
minGiftPrice: 'Min price of Super Chats to show (CNY)',
|
||||
maxSpeed: 'Max speed of messages (/second, 0 for unlimited)',
|
||||
maxNumber: 'Max number of messages',
|
||||
|
||||
block: 'Block',
|
||||
giftDanmaku: 'System messages (gift effect)',
|
||||
blockLevel: 'User level lower than',
|
||||
informalUser: 'Informal users',
|
||||
unverifiedUser: 'Unverified users',
|
||||
giftDanmaku: 'Block system messages (gift effect)',
|
||||
blockLevel: 'Block user level lower than',
|
||||
informalUser: 'Block informal users',
|
||||
unverifiedUser: 'Block unverified users',
|
||||
blockKeywords: 'Block keywords',
|
||||
onePerLine: 'One per line',
|
||||
blockUsers: 'Block users',
|
||||
blockMedalLevel: 'Medal level lower than',
|
||||
blockMedalLevel: 'Block medal level lower than',
|
||||
|
||||
style: 'Style',
|
||||
advanced: 'Advanced',
|
||||
relayMessagesByServer: 'Relay messages by server',
|
||||
autoTranslate: 'Auto translate messages to Japanese (requires relay messages by server)',
|
||||
giftUsernamePronunciation: 'Pronunciation of gift username',
|
||||
dontShow: 'None',
|
||||
pinyin: 'Pinyin',
|
||||
kana: 'Kana',
|
||||
|
||||
emoticon: 'Custom Emotes',
|
||||
emoticonKeyword: 'Emote Code',
|
||||
emoticonUrl: 'URL',
|
||||
operation: 'Operation',
|
||||
addEmoticon: 'Add emote',
|
||||
emoticonFileTooLarge: 'File size is too large. Max size is 1MB',
|
||||
|
||||
roomUrl: 'Room URL',
|
||||
copy: 'Copy',
|
||||
saveConfig: 'Save config',
|
||||
enterRoom: 'Enter room',
|
||||
enterTestRoom: 'Enter test room',
|
||||
exportConfig: 'Export config',
|
||||
importConfig: 'Import config',
|
||||
|
||||
failedToSave: 'Failed to save: ',
|
||||
successfullySaved: 'Successfully saved',
|
||||
failedToParseConfig: 'Failed to parse config: '
|
||||
},
|
||||
stylegen: {
|
||||
legacy: 'Classic',
|
||||
lineLike: 'Line-like',
|
||||
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
|
||||
outlines: 'Outlines',
|
||||
showOutlines: 'Show outlines',
|
||||
outlineSize: 'Outline size',
|
||||
@@ -49,7 +69,8 @@ export default {
|
||||
showAvatars: 'Show avatars',
|
||||
avatarSize: 'Avatar size',
|
||||
|
||||
userNames: 'User names',
|
||||
userNames: 'User Names',
|
||||
showUserNames: 'Show user names',
|
||||
font: 'Font',
|
||||
fontSize: 'Font size',
|
||||
lineHeight: 'Line height (0 for default)',
|
||||
@@ -59,6 +80,7 @@ export default {
|
||||
memberColor: 'Member color',
|
||||
showBadges: 'Show badges',
|
||||
showColon: 'Show colon after name',
|
||||
emoticonSize: 'Emoticon size',
|
||||
|
||||
messages: 'Messages',
|
||||
color: 'Color',
|
||||
@@ -75,7 +97,7 @@ export default {
|
||||
moderatorMessageBgColor: 'Moderator background color',
|
||||
memberMessageBgColor: 'Member background color',
|
||||
|
||||
scAndNewMember: 'Super Chat / New member',
|
||||
scAndNewMember: 'Super Chat / New Member',
|
||||
firstLineFont: 'First line font',
|
||||
firstLineFontSize: 'First line font size',
|
||||
firstLineLineHeight: 'First line line height (0 for default)',
|
||||
@@ -105,5 +127,22 @@ export default {
|
||||
result: 'Result',
|
||||
copy: 'Copy',
|
||||
resetConfig: 'Reset config'
|
||||
},
|
||||
help: {
|
||||
help: 'Help',
|
||||
p1: '1. Copy the room ID from the Bilibili live room webpage',
|
||||
p2: '2. Enter the room ID into the room ID on the home page. Copy the room URL',
|
||||
p3: '3. Generate styles with the style generator. Copy the CSS',
|
||||
p4: '4. Add browser source in OBS',
|
||||
p5: '5. Enter the previously copied room URL at URL, and enter the previously copied CSS at custom CSS'
|
||||
},
|
||||
chat: {
|
||||
moderator: 'moderator',
|
||||
guardLevel1: 'governor',
|
||||
guardLevel2: 'admiral',
|
||||
guardLevel3: 'captain',
|
||||
sendGift: 'Sent {giftName}x{num}',
|
||||
membershipTitle: 'New member',
|
||||
tickerMembership: 'Member'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export default {
|
||||
export default {
|
||||
sidebar: {
|
||||
home: 'トップページ',
|
||||
stylegen: 'スタイルジェネレータ',
|
||||
projectAddress: 'プロジェクトアドレス'
|
||||
help: 'ヘルプ',
|
||||
projectAddress: 'プロジェクトアドレス',
|
||||
giftRecordOfficial: '公式スーパーチャット記録',
|
||||
},
|
||||
home: {
|
||||
roomIdEmpty: 'ルームのIDを空白にすることはできません',
|
||||
@@ -12,34 +14,52 @@
|
||||
roomId: 'ルームID',
|
||||
showDanmaku: 'コメントを表示する',
|
||||
showGift: 'スーパーチャットと新メンバーを表示する',
|
||||
showGiftName: 'ギフト名を表示する',
|
||||
mergeSimilarDanmaku: '同じコメントを合併する',
|
||||
mergeGift: 'ギフトを合併する',
|
||||
minGiftPrice: '最低表示スーパーチャット価格(CNY)',
|
||||
maxSpeed: 'コメントの最大速度(バー/秒、0は無制限)',
|
||||
maxNumber: '最大コメント数',
|
||||
|
||||
block: 'ブロック',
|
||||
giftDanmaku: 'システムコメント(プレゼント効果)',
|
||||
blockLevel: 'ユーザーレベルがx未満',
|
||||
informalUser: '非公式ユーザー',
|
||||
unverifiedUser: '認証されていないユーザー',
|
||||
blockKeywords: 'キーワード',
|
||||
giftDanmaku: 'ブロックシステムコメント(プレゼント効果)',
|
||||
blockLevel: 'ブロックユーザーレベルがx未満',
|
||||
informalUser: 'ブロック非公式ユーザー',
|
||||
unverifiedUser: 'ブロック認証されていないユーザー',
|
||||
blockKeywords: 'ブロックキーワード',
|
||||
onePerLine: '1行に1つずつ',
|
||||
blockUsers: 'ユーザー',
|
||||
blockMedalLevel: '勲章等級がx未満',
|
||||
blockUsers: 'ブロックユーザー',
|
||||
blockMedalLevel: 'ブロック勲章等級がx未満',
|
||||
|
||||
style: 'スタイル',
|
||||
advanced: 'アドバンスド',
|
||||
relayMessagesByServer: 'サーバを介してメッセージを転送する',
|
||||
autoTranslate: 'コメントを日本語に翻訳する(サーバを介してメッセージを転送する必要)',
|
||||
giftUsernamePronunciation: 'スーパーチャットのユーザー名の発音',
|
||||
dontShow: '非表示',
|
||||
pinyin: 'ピンイン',
|
||||
kana: '仮名',
|
||||
|
||||
emoticon: 'カスタムスタンプ',
|
||||
emoticonKeyword: '置き換えるキーワード',
|
||||
emoticonUrl: 'URL',
|
||||
operation: '操作',
|
||||
addEmoticon: 'スタンプを追加',
|
||||
emoticonFileTooLarge: 'ファイルサイズが大きすぎます。最大サイズは1MBです',
|
||||
|
||||
roomUrl: 'ルームのURL',
|
||||
copy: 'コピー',
|
||||
saveConfig: 'コンフィグを保存する',
|
||||
enterRoom: 'ルームに入る',
|
||||
enterTestRoom: 'テストルームに入る',
|
||||
exportConfig: 'コンフィグの導出',
|
||||
importConfig: 'コンフィグの導入',
|
||||
|
||||
failedToSave: '保存に失敗しました:',
|
||||
successfullySaved: '保存に成功しました',
|
||||
failedToParseConfig: 'コンフィグ解析に失敗しました'
|
||||
},
|
||||
stylegen: {
|
||||
legacy: '古典',
|
||||
lineLike: 'Line風',
|
||||
|
||||
light: '明るい',
|
||||
dark: '暗い',
|
||||
|
||||
outlines: 'アウトライン',
|
||||
showOutlines: 'アウトラインを表示する',
|
||||
outlineSize: 'アウトラインのサイズ',
|
||||
@@ -50,6 +70,7 @@
|
||||
avatarSize: 'アイコンのサイズ',
|
||||
|
||||
userNames: 'ユーザー名',
|
||||
showUserNames: 'ユーザー名を表示する',
|
||||
font: 'フォント',
|
||||
fontSize: 'フォントサイズ',
|
||||
lineHeight: '行の高さ(0はデフォルト)',
|
||||
@@ -59,6 +80,7 @@
|
||||
memberColor: 'メンバーの色',
|
||||
showBadges: '勲章を見せる',
|
||||
showColon: 'ユーザー名の後にコロンが表示されます',
|
||||
emoticonSize: 'スタンプサイズ',
|
||||
|
||||
messages: 'コメント',
|
||||
color: '色',
|
||||
@@ -105,5 +127,22 @@
|
||||
result: '結果',
|
||||
copy: 'コピー',
|
||||
resetConfig: 'デフォルトに戻す'
|
||||
},
|
||||
help: {
|
||||
help: 'ヘルプ',
|
||||
p1: '1. ビリビリの生放送ウェブから生放送ルームIDをこぴーする',
|
||||
p2: '2. ホームページでコピーしたIDを入力し、ルームのURLをこぴーする',
|
||||
p3: '3. スタイルジェネレータでお好みのコメント様子を選び、出力したCSSをコピーする',
|
||||
p4: '4. OBSでブラウザを新規作成する',
|
||||
p5: '5. プロパティでこぴーしたURLを入力し、カスタムCSSでスタイルジェネレータのCSSを入力する'
|
||||
},
|
||||
chat: {
|
||||
moderator: 'モデレーター',
|
||||
guardLevel1: '総督',
|
||||
guardLevel2: '提督',
|
||||
guardLevel3: '艦長',
|
||||
sendGift: '{giftName}x{num} を贈りました',
|
||||
membershipTitle: '新規メンバー',
|
||||
tickerMembership: 'メンバー'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ export default {
|
||||
sidebar: {
|
||||
home: '首页',
|
||||
stylegen: '样式生成器',
|
||||
projectAddress: '项目地址'
|
||||
help: '帮助',
|
||||
projectAddress: '项目地址',
|
||||
giftRecordOfficial: '官方打赏记录',
|
||||
},
|
||||
home: {
|
||||
roomIdEmpty: '房间ID不能为空',
|
||||
@@ -11,35 +13,53 @@ export default {
|
||||
general: '常规',
|
||||
roomId: '房间ID',
|
||||
showDanmaku: '显示弹幕',
|
||||
showGift: '显示礼物和新舰长',
|
||||
showGift: '显示打赏和新舰长',
|
||||
showGiftName: '显示礼物名',
|
||||
mergeSimilarDanmaku: '合并相似弹幕',
|
||||
minGiftPrice: '最低显示礼物价格(元)',
|
||||
maxSpeed: '弹幕最大速度(条/秒,0为无限制)',
|
||||
mergeGift: '合并礼物',
|
||||
minGiftPrice: '最低显示打赏价格(元)',
|
||||
maxNumber: '最大弹幕数',
|
||||
|
||||
block: '屏蔽',
|
||||
giftDanmaku: '礼物弹幕',
|
||||
blockLevel: '用户等级低于',
|
||||
informalUser: '非正式会员',
|
||||
unverifiedUser: '未绑定手机用户',
|
||||
giftDanmaku: '屏蔽礼物弹幕',
|
||||
blockLevel: '屏蔽用户等级低于',
|
||||
informalUser: '屏蔽非正式会员',
|
||||
unverifiedUser: '屏蔽未绑定手机用户',
|
||||
blockKeywords: '屏蔽关键词',
|
||||
onePerLine: '一行一个',
|
||||
blockUsers: '屏蔽用户',
|
||||
blockMedalLevel: '当前直播间勋章等级低于',
|
||||
blockMedalLevel: '屏蔽当前直播间勋章等级低于',
|
||||
|
||||
style: '样式',
|
||||
advanced: '高级',
|
||||
relayMessagesByServer: '通过服务器转发消息',
|
||||
autoTranslate: '自动翻译弹幕到日语(需要通过服务器转发消息)',
|
||||
giftUsernamePronunciation: '标注打赏用户名读音',
|
||||
dontShow: '不显示',
|
||||
pinyin: '拼音',
|
||||
kana: '日文假名',
|
||||
|
||||
emoticon: '自定义表情',
|
||||
emoticonKeyword: '替换关键词',
|
||||
emoticonUrl: 'URL',
|
||||
operation: '操作',
|
||||
addEmoticon: '添加表情',
|
||||
emoticonFileTooLarge: '文件尺寸太大,最大1MB',
|
||||
|
||||
roomUrl: '房间URL',
|
||||
copy: '复制',
|
||||
saveConfig: '保存配置',
|
||||
enterRoom: '进入房间',
|
||||
enterTestRoom: '进入测试房间',
|
||||
exportConfig: '导出配置',
|
||||
importConfig: '导入配置',
|
||||
|
||||
failedToSave: '保存失败:',
|
||||
successfullySaved: '保存成功',
|
||||
failedToParseConfig: '配置解析失败:'
|
||||
},
|
||||
stylegen: {
|
||||
legacy: '经典',
|
||||
lineLike: '仿微信',
|
||||
|
||||
light: '明亮',
|
||||
dark: '黑暗',
|
||||
|
||||
outlines: '描边',
|
||||
showOutlines: '显示描边',
|
||||
outlineSize: '描边尺寸',
|
||||
@@ -50,6 +70,7 @@ export default {
|
||||
avatarSize: '头像尺寸',
|
||||
|
||||
userNames: '用户名',
|
||||
showUserNames: '显示用户名',
|
||||
font: '字体',
|
||||
fontSize: '字体尺寸',
|
||||
lineHeight: '行高(0为默认)',
|
||||
@@ -59,6 +80,7 @@ export default {
|
||||
memberColor: '舰长颜色',
|
||||
showBadges: '显示勋章',
|
||||
showColon: '用户名后显示冒号',
|
||||
emoticonSize: '表情大小',
|
||||
|
||||
messages: '消息',
|
||||
color: '颜色',
|
||||
@@ -69,13 +91,13 @@ export default {
|
||||
|
||||
backgrounds: '背景',
|
||||
bgColor: '背景色',
|
||||
useBarsInsteadOfBg: '用条代替背景',
|
||||
useBarsInsteadOfBg: '用条代替消息背景',
|
||||
messageBgColor: '消息背景色',
|
||||
ownerMessageBgColor: '主播消息背景色',
|
||||
moderatorMessageBgColor: '房管消息背景色',
|
||||
memberMessageBgColor: '舰长消息背景色',
|
||||
|
||||
scAndNewMember: '礼物、舰长',
|
||||
scAndNewMember: '打赏、舰长',
|
||||
firstLineFont: '第一行字体',
|
||||
firstLineFontSize: '第一行字体尺寸',
|
||||
firstLineLineHeight: '第一行行高(0为默认)',
|
||||
@@ -89,14 +111,14 @@ export default {
|
||||
scContentLineLineHeight: 'Super Chat内容行高(0为默认)',
|
||||
scContentLineColor: 'Super Chat内容颜色',
|
||||
showNewMemberBg: '显示新舰长背景',
|
||||
showScTicker: '显示Super Chat贴纸',
|
||||
showOtherThings: '显示Super Chat贴纸之外的内容',
|
||||
showScTicker: '显示Super Chat固定栏',
|
||||
showOtherThings: '显示Super Chat固定栏之外的内容',
|
||||
|
||||
animation: '动画',
|
||||
animateIn: '进入动画',
|
||||
fadeInTime: '淡入时间(毫秒)',
|
||||
animateOut: '移除旧消息',
|
||||
animateOutWaitTime: '等待时间(秒)',
|
||||
animateOutWaitTime: '移除前等待时间(秒)',
|
||||
fadeOutTime: '淡出时间(毫秒)',
|
||||
slide: '滑动',
|
||||
reverseSlide: '反向滑动',
|
||||
@@ -105,5 +127,22 @@ export default {
|
||||
result: '结果',
|
||||
copy: '复制',
|
||||
resetConfig: '恢复默认设置'
|
||||
},
|
||||
help: {
|
||||
help: '帮助',
|
||||
p1: '1. 从B站直播间网页复制房间ID',
|
||||
p2: '2. 把房间ID输入到首页的房间ID,复制房间URL',
|
||||
p3: '3. 使用样式生成器生成样式,复制CSS',
|
||||
p4: '4. 在OBS中添加浏览器源',
|
||||
p5: '5. URL处输入之前复制的房间URL,自定义CSS处输入之前复制的CSS'
|
||||
},
|
||||
chat: {
|
||||
moderator: '管理员',
|
||||
guardLevel1: '总督',
|
||||
guardLevel2: '提督',
|
||||
guardLevel3: '舰长',
|
||||
sendGift: '赠送 {giftName}x{num}',
|
||||
membershipTitle: '新会员',
|
||||
tickerMembership: '会员'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,38 +8,53 @@
|
||||
:default-active="$route.path"
|
||||
>
|
||||
<el-menu-item index="/">
|
||||
<i class="el-icon-s-home"></i>{{$t('sidebar.home')}}
|
||||
<i class="el-icon-s-home"></i>{{ $t('sidebar.home') }}
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="$router.resolve({name: 'stylegen'}).href">
|
||||
<i class="el-icon-brush"></i>{{$t('sidebar.stylegen')}}
|
||||
<el-menu-item :index="$router.resolve({ name: 'stylegen' }).href">
|
||||
<i class="el-icon-brush"></i>{{ $t('sidebar.stylegen') }}
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="$router.resolve({ name: 'help' }).href">
|
||||
<i class="el-icon-question"></i>{{ $t('sidebar.help') }}
|
||||
</el-menu-item>
|
||||
<a href="https://github.com/xfgryujk/blivechat" target="_blank">
|
||||
<el-menu-item>
|
||||
<i class="el-icon-share"></i>{{$t('sidebar.projectAddress')}}
|
||||
<i class="el-icon-share"></i>{{ $t('sidebar.projectAddress') }}
|
||||
</el-menu-item>
|
||||
</a>
|
||||
<a href="http://link.bilibili.com/ctool/vtuber" target="_blank">
|
||||
<el-menu-item>
|
||||
<i class="el-icon-link"></i>{{ $t('sidebar.giftRecordOfficial') }}
|
||||
</el-menu-item>
|
||||
</a>
|
||||
<el-submenu index="null">
|
||||
<template slot="title">
|
||||
<i class="el-icon-chat-line-square"></i>Language
|
||||
</template>
|
||||
<el-menu-item v-for="{locale, name} in [
|
||||
{locale: 'zh', name: '中文'},
|
||||
{locale: 'ja', name: '日本語'},
|
||||
{locale: 'en', name: 'English'}
|
||||
]" :key="locale"
|
||||
@click="onSelectLanguage(locale)"
|
||||
>{{name}}</el-menu-item>
|
||||
<el-menu-item v-for="locale in LOCALES" :key="locale.locale" @click="onSelectLanguage(locale.locale)">
|
||||
<template>{{ locale.name }}</template>
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as i18n from '@/i18n'
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
data() {
|
||||
return {
|
||||
LOCALES: [
|
||||
{ locale: 'zh', name: '中文' },
|
||||
{ locale: 'ja', name: '日本語' },
|
||||
{ locale: 'en', name: 'English' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSelectLanguage(locale) {
|
||||
window.localStorage.lang = this.$i18n.locale = locale
|
||||
i18n.setLocale(locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
<template>
|
||||
<el-container class="app-wrapper" :class="{mobile: isMobile}">
|
||||
<el-container class="app-wrapper" :class="{ mobile: isMobile }">
|
||||
<div v-show="isMobile && !hideSidebar" class="drawer-bg" @click="hideSidebar = true"></div>
|
||||
<el-aside width="210px" class="sidebar-container" :class="{'hide-sidebar': hideSidebar}">
|
||||
<el-aside width="230px" class="sidebar-container" :class="{ 'hide-sidebar': hideSidebar }">
|
||||
<div class="logo-container">
|
||||
<router-link to="/">
|
||||
<img src="@/assets/img/logo.png" class="sidebar-logo">
|
||||
<h1 class="sidebar-title">blivechat</h1>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="version">{{ APP_VERSION }}</div>
|
||||
<sidebar></sidebar>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
<el-button v-show="isMobile" class="menu-button" icon="el-icon-s-unfold" @click="hideSidebar = false"></el-button>
|
||||
<router-view></router-view>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import Sidebar from './Sidebar'
|
||||
|
||||
export default {
|
||||
name: 'Layout',
|
||||
@@ -27,11 +30,13 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
APP_VERSION: process.env.APP_VERSION,
|
||||
|
||||
isMobile: false,
|
||||
hideSidebar: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.onResize)
|
||||
this.onResize()
|
||||
},
|
||||
@@ -47,12 +52,17 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html {
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "\5FAE \8F6F \96C5 \9ED1 ", "微软雅黑", Arial, sans-serif;
|
||||
}
|
||||
|
||||
html, body, #app, .app-wrapper, .sidebar-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
a, a:focus, a:hover {
|
||||
@@ -85,7 +95,7 @@ a, a:focus, a:hover {
|
||||
.app-wrapper.mobile .sidebar-container.hide-sidebar {
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
transform: translate3d(-210px, 0, 0);
|
||||
transform: translate3d(-230px, 0, 0);
|
||||
}
|
||||
|
||||
.sidebar-container .logo-container {
|
||||
@@ -114,6 +124,17 @@ a, a:focus, a:hover {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sidebar-container .version {
|
||||
height: 30px;
|
||||
background: #2b2f3a;
|
||||
color: #aaa;
|
||||
font-weight: 600;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-container .is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,58 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import ElementUI from 'element-ui'
|
||||
import 'element-ui/lib/theme-chalk/index.css'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import {
|
||||
Aside, Autocomplete, Badge, Button, ButtonGroup, Card, Col, ColorPicker, Container, Divider, Form, FormItem, Image,
|
||||
Input, Main, Menu, MenuItem, Message, Option, OptionGroup, Radio, RadioGroup, Row, Select, Scrollbar,
|
||||
Slider, Submenu, Switch, Table, TableColumn, TabPane, Tabs, Tooltip
|
||||
} from 'element-ui'
|
||||
import axios from 'axios'
|
||||
|
||||
import App from './App.vue'
|
||||
import * as i18n from './i18n'
|
||||
import App from './App'
|
||||
import Layout from './layout'
|
||||
import Home from './views/Home.vue'
|
||||
import Home from './views/Home'
|
||||
import StyleGenerator from './views/StyleGenerator'
|
||||
import Room from './views/Room.vue'
|
||||
import NotFound from './views/NotFound.vue'
|
||||
import Help from './views/Help'
|
||||
import Room from './views/Room'
|
||||
import NotFound from './views/NotFound'
|
||||
|
||||
import zh from './lang/zh'
|
||||
import ja from './lang/ja'
|
||||
import en from './lang/en'
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 开发时使用localhost:12450
|
||||
axios.defaults.baseURL = 'http://localhost:12450'
|
||||
}
|
||||
axios.defaults.timeout = 10 * 1000
|
||||
|
||||
Vue.use(VueRouter)
|
||||
Vue.use(ElementUI)
|
||||
Vue.use(VueI18n)
|
||||
// 初始化element
|
||||
Vue.use(Aside)
|
||||
Vue.use(Autocomplete)
|
||||
Vue.use(Badge)
|
||||
Vue.use(Button)
|
||||
Vue.use(ButtonGroup)
|
||||
Vue.use(Card)
|
||||
Vue.use(Col)
|
||||
Vue.use(ColorPicker)
|
||||
Vue.use(Container)
|
||||
Vue.use(Divider)
|
||||
Vue.use(Form)
|
||||
Vue.use(FormItem)
|
||||
Vue.use(Image)
|
||||
Vue.use(Input)
|
||||
Vue.use(Main)
|
||||
Vue.use(Menu)
|
||||
Vue.use(MenuItem)
|
||||
Vue.use(Option)
|
||||
Vue.use(OptionGroup)
|
||||
Vue.use(Radio)
|
||||
Vue.use(RadioGroup)
|
||||
Vue.use(Row)
|
||||
Vue.use(Select)
|
||||
Vue.use(Scrollbar)
|
||||
Vue.use(Slider)
|
||||
Vue.use(Submenu)
|
||||
Vue.use(Switch)
|
||||
Vue.use(Table)
|
||||
Vue.use(TableColumn)
|
||||
Vue.use(TabPane)
|
||||
Vue.use(Tabs)
|
||||
Vue.use(Tooltip)
|
||||
Vue.prototype.$message = Message
|
||||
|
||||
Vue.config.ignoredElements = [
|
||||
/^yt-/
|
||||
@@ -36,36 +65,35 @@ const router = new VueRouter({
|
||||
path: '/',
|
||||
component: Layout,
|
||||
children: [
|
||||
{path: '', component: Home},
|
||||
{path: 'stylegen', name: 'stylegen', component: StyleGenerator}
|
||||
{ path: '', component: Home },
|
||||
{ path: 'stylegen', name: 'stylegen', component: StyleGenerator },
|
||||
{ path: 'help', name: 'help', component: Help }
|
||||
]
|
||||
},
|
||||
{path: '/room/:roomId', name: 'room', component: Room},
|
||||
{path: '*', component: NotFound}
|
||||
{
|
||||
path: '/room/test',
|
||||
name: 'test_room',
|
||||
component: Room,
|
||||
props: route => ({ strConfig: route.query })
|
||||
},
|
||||
{
|
||||
path: '/room/:roomId',
|
||||
name: 'room',
|
||||
component: Room,
|
||||
props(route) {
|
||||
let roomId = parseInt(route.params.roomId)
|
||||
if (isNaN(roomId)) {
|
||||
roomId = null
|
||||
}
|
||||
return { roomId, strConfig: route.query }
|
||||
}
|
||||
},
|
||||
{ path: '*', component: NotFound }
|
||||
]
|
||||
})
|
||||
|
||||
let locale = window.localStorage.lang
|
||||
if (!locale) {
|
||||
let lang = navigator.language
|
||||
if (lang.startsWith('zh')) {
|
||||
locale = 'zh'
|
||||
} else if (lang.startsWith('ja')) {
|
||||
locale = 'ja'
|
||||
} else {
|
||||
locale = 'en'
|
||||
}
|
||||
}
|
||||
const i18n = new VueI18n({
|
||||
locale,
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
zh, ja, en
|
||||
}
|
||||
})
|
||||
|
||||
new Vue({
|
||||
render: h => h(App),
|
||||
router,
|
||||
i18n
|
||||
i18n: i18n.i18n
|
||||
}).$mount('#app')
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export function mergeConfig (config, defaultConfig) {
|
||||
let res = {}
|
||||
for (let i in defaultConfig) {
|
||||
res[i] = i in config ? config[i] : defaultConfig[i]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export default {
|
||||
mergeConfig
|
||||
}
|
||||
43
frontend/src/utils/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
export function mergeConfig(config, defaultConfig) {
|
||||
let res = {}
|
||||
for (let i in defaultConfig) {
|
||||
res[i] = i in config ? config[i] : defaultConfig[i]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export function toBool(val) {
|
||||
if (typeof val === 'string') {
|
||||
return ['false', 'no', 'off', '0', ''].indexOf(val.toLowerCase()) === -1
|
||||
}
|
||||
return Boolean(val)
|
||||
}
|
||||
|
||||
export function toInt(val, _default) {
|
||||
let res = parseInt(val)
|
||||
if (isNaN(res)) {
|
||||
res = _default
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export function formatCurrency(price) {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
minimumFractionDigits: price < 100 ? 2 : 0
|
||||
}).format(price)
|
||||
}
|
||||
|
||||
export function getTimeTextHourMin(date) {
|
||||
let hour = date.getHours()
|
||||
let min = `00${date.getMinutes()}`.slice(-2)
|
||||
return `${hour}:${min}`
|
||||
}
|
||||
|
||||
export function getUuid4Hex() {
|
||||
let chars = []
|
||||
for (let i = 0; i < 32; i++) {
|
||||
let char = Math.floor(Math.random() * 16).toString(16)
|
||||
chars.push(char)
|
||||
}
|
||||
return chars.join('')
|
||||
}
|
||||
20902
frontend/src/utils/pronunciation/dictKana.js
Normal file
20902
frontend/src/utils/pronunciation/dictPinyin.js
Normal file
54
frontend/src/utils/pronunciation/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
export const DICT_PINYIN = 'pinyin'
|
||||
export const DICT_KANA = 'kana'
|
||||
|
||||
export class PronunciationConverter {
|
||||
constructor() {
|
||||
this.pronunciationMap = new Map()
|
||||
}
|
||||
|
||||
async loadDict(dictName) {
|
||||
let promise
|
||||
switch (dictName) {
|
||||
case DICT_PINYIN:
|
||||
promise = import('./dictPinyin')
|
||||
break
|
||||
case DICT_KANA:
|
||||
promise = import('./dictKana')
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
let dictTxt = (await promise).default
|
||||
let pronunciationMap = new Map()
|
||||
for (let item of dictTxt.split('\n')) {
|
||||
if (item.length === 0) {
|
||||
continue
|
||||
}
|
||||
pronunciationMap.set(item.substring(0, 1), item.substring(1))
|
||||
}
|
||||
this.pronunciationMap = pronunciationMap
|
||||
}
|
||||
|
||||
getPronunciation(text) {
|
||||
let res = []
|
||||
let lastHasPronunciation = null
|
||||
for (let char of text) {
|
||||
let pronunciation = this.pronunciationMap.get(char)
|
||||
if (pronunciation === undefined) {
|
||||
if (lastHasPronunciation !== null && lastHasPronunciation) {
|
||||
res.push(' ')
|
||||
}
|
||||
lastHasPronunciation = false
|
||||
res.push(char)
|
||||
} else {
|
||||
if (lastHasPronunciation !== null) {
|
||||
res.push(' ')
|
||||
}
|
||||
lastHasPronunciation = true
|
||||
res.push(pronunciation)
|
||||
}
|
||||
}
|
||||
return res.join('')
|
||||
}
|
||||
}
|
||||
58
frontend/src/utils/trie.js
Normal file
@@ -0,0 +1,58 @@
|
||||
export class Trie {
|
||||
constructor() {
|
||||
this._root = this._createNode()
|
||||
}
|
||||
|
||||
_createNode() {
|
||||
return {
|
||||
children: {}, // char -> node
|
||||
value: null
|
||||
}
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
if (key === '') {
|
||||
throw new Error('key is empty')
|
||||
}
|
||||
let node = this._root
|
||||
for (let char of key) {
|
||||
let nextNode = node.children[char]
|
||||
if (nextNode === undefined) {
|
||||
nextNode = node.children[char] = this._createNode()
|
||||
}
|
||||
node = nextNode
|
||||
}
|
||||
node.value = value
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let node = this._root
|
||||
for (let char of key) {
|
||||
let nextNode = node.children[char]
|
||||
if (nextNode === undefined) {
|
||||
return null
|
||||
}
|
||||
node = nextNode
|
||||
}
|
||||
return node.value
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this.get(key) !== null
|
||||
}
|
||||
|
||||
lazyMatch(str) {
|
||||
let node = this._root
|
||||
for (let char of str) {
|
||||
let nextNode = node.children[char]
|
||||
if (nextNode === undefined) {
|
||||
return null
|
||||
}
|
||||
if (nextNode.value !== null) {
|
||||
return nextNode.value
|
||||
}
|
||||
node = nextNode
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
34
frontend/src/views/Help.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ $t('help.help') }}</h1>
|
||||
<p>{{ $t('help.p1') }}</p>
|
||||
<p class="img-container"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-1.png"></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.png"></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.png"></el-image></p>
|
||||
<p>{{ $t('help.p4') }}</p>
|
||||
<p class="img-container"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-4.png"></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.png"></el-image></p>
|
||||
<p><br><br><br><br><br><br><br><br>--------------------------------------------------------------------------------------------------------</p>
|
||||
<p>喜欢的话可以推荐给别人,专栏求支持_(:з」∠)_ <a href="https://www.bilibili.com/read/cv4594365" target="_blank">https://www.bilibili.com/read/cv4594365</a></p>
|
||||
<p>配置官方翻译接口傻瓜式教程 <a href="https://www.bilibili.com/read/cv14663633" target="_blank">https://www.bilibili.com/read/cv14663633</a>。注意必须下载到本地才能改后台配置,本地使用方法看 <a href="https://github.com/xfgryujk/blivechat#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95" target="_blank">项目地址中的使用方法说明</a></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Help'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.img-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.img-container.large-img .el-image {
|
||||
height: 80vh;
|
||||
}
|
||||
</style>
|
||||
@@ -1,133 +1,292 @@
|
||||
<template>
|
||||
<el-form :model="form" ref="form" label-width="150px" :rules="{
|
||||
roomId: [
|
||||
{required: true, message: $t('home.roomIdEmpty'), trigger: 'blur'},
|
||||
{type: 'integer', min: 1, message: $t('home.roomIdInteger'), trigger: 'blur'}
|
||||
]
|
||||
}">
|
||||
<el-tabs>
|
||||
<el-tab-pane :label="$t('home.general')">
|
||||
<el-form-item :label="$t('home.roomId')" required prop="roomId">
|
||||
<el-input v-model.number="form.roomId" type="number" min="1"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.showDanmaku')">
|
||||
<el-switch v-model="form.showDanmaku"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.showGift')">
|
||||
<el-switch v-model="form.showGift"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.mergeSimilarDanmaku')">
|
||||
<el-switch v-model="form.mergeSimilarDanmaku"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.minGiftPrice')">
|
||||
<el-input v-model.number="form.minGiftPrice" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.maxSpeed')">
|
||||
<el-input v-model.number="form.maxSpeed" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
<div>
|
||||
<p>
|
||||
<el-form :model="form" ref="form" label-width="150px" :rules="{
|
||||
roomId: [
|
||||
{required: true, message: $t('home.roomIdEmpty'), trigger: 'blur'},
|
||||
{type: 'integer', min: 1, message: $t('home.roomIdInteger'), trigger: 'blur'}
|
||||
]
|
||||
}">
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane :label="$t('home.general')">
|
||||
<el-form-item :label="$t('home.roomId')" required prop="roomId">
|
||||
<el-input v-model.number="form.roomId" type="number" min="1"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.showDanmaku')">
|
||||
<el-switch v-model="form.showDanmaku"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.showGift')">
|
||||
<el-switch v-model="form.showGift"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.showGiftName')">
|
||||
<el-switch v-model="form.showGiftName"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.mergeSimilarDanmaku')">
|
||||
<el-switch v-model="form.mergeSimilarDanmaku"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.mergeGift')">
|
||||
<el-switch v-model="form.mergeGift"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.minGiftPrice')">
|
||||
<el-input v-model.number="form.minGiftPrice" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.maxNumber')">
|
||||
<el-input v-model.number="form.maxNumber" type="number" min="1"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('home.block')">
|
||||
<el-form-item :label="$t('home.giftDanmaku')">
|
||||
<el-switch v-model="form.blockGiftDanmaku"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.blockLevel')">
|
||||
<el-slider v-model="form.blockLevel" show-input :min="0" :max="60"></el-slider>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.informalUser')">
|
||||
<el-switch v-model="form.blockNewbie"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.unverifiedUser')">
|
||||
<el-switch v-model="form.blockNotMobileVerified"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.blockKeywords')">
|
||||
<el-input v-model="form.blockKeywords" type="textarea" :rows="5" :placeholder="$t('home.onePerLine')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.blockUsers')">
|
||||
<el-input v-model="form.blockUsers" type="textarea" :rows="5" :placeholder="$t('home.onePerLine')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.blockMedalLevel')">
|
||||
<el-slider v-model="form.blockMedalLevel" show-input :min="0" :max="20"></el-slider>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('home.block')">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.giftDanmaku')">
|
||||
<el-switch v-model="form.blockGiftDanmaku"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.informalUser')">
|
||||
<el-switch v-model="form.blockNewbie"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.unverifiedUser')">
|
||||
<el-switch v-model="form.blockNotMobileVerified"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('home.blockLevel')">
|
||||
<el-slider v-model="form.blockLevel" show-input :min="0" :max="60"></el-slider>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="$t('home.blockMedalLevel')">
|
||||
<el-slider v-model="form.blockMedalLevel" show-input :min="0" :max="40"></el-slider>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('home.blockKeywords')">
|
||||
<el-input v-model="form.blockKeywords" type="textarea" :rows="5" :placeholder="$t('home.onePerLine')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('home.blockUsers')">
|
||||
<el-input v-model="form.blockUsers" type="textarea" :rows="5" :placeholder="$t('home.onePerLine')"></el-input>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('home.style')">
|
||||
<el-form-item label="CSS">
|
||||
<el-input v-model="form.css" type="textarea" :rows="20"></el-input>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<el-form-item :label="$t('home.roomUrl')" v-show="roomUrl">
|
||||
<el-input ref="roomUrlInput" readonly :value="roomUrl" style="width: calc(100% - 6em); margin-right: 1em;"></el-input>
|
||||
<el-button type="primary" @click="copyUrl">{{$t('home.copy')}}</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveConfig">{{$t('home.saveConfig')}}</el-button>
|
||||
<el-button type="primary" :disabled="!roomUrl" @click="enterRoom">{{$t('home.enterRoom')}}</el-button>
|
||||
<el-button type="primary" @click="exportConfig">{{$t('home.exportConfig')}}</el-button>
|
||||
<el-button type="primary" @click="importConfig">{{$t('home.importConfig')}}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-tab-pane :label="$t('home.advanced')">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.relayMessagesByServer')">
|
||||
<el-switch v-model="form.relayMessagesByServer"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="8">
|
||||
<el-form-item :label="$t('home.autoTranslate')">
|
||||
<el-switch v-model="form.autoTranslate" :disabled="!serverConfig.enableTranslate || !form.relayMessagesByServer"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('home.giftUsernamePronunciation')">
|
||||
<el-radio-group v-model="form.giftUsernamePronunciation">
|
||||
<el-radio label="">{{$t('home.dontShow')}}</el-radio>
|
||||
<el-radio label="pinyin">{{$t('home.pinyin')}}</el-radio>
|
||||
<el-radio label="kana">{{$t('home.kana')}}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('home.emoticon')">
|
||||
<el-table :data="form.emoticons">
|
||||
<el-table-column prop="keyword" :label="$t('home.emoticonKeyword')" width="170">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.keyword"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="url" :label="$t('home.emoticonUrl')">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.url"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('home.operation')" width="170">
|
||||
<template slot-scope="scope">
|
||||
<el-button-group>
|
||||
<el-button type="primary" icon="el-icon-upload2" :disabled="!serverConfig.enableUploadFile"
|
||||
@click="uploadEmoticon(scope.row)"
|
||||
></el-button>
|
||||
<el-button type="danger" icon="el-icon-minus" @click="delEmoticon(scope.$index)"></el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<p>
|
||||
<el-button type="primary" icon="el-icon-plus" @click="addEmoticon">{{$t('home.addEmoticon')}}</el-button>
|
||||
</p>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<el-card>
|
||||
<el-form :model="form" label-width="150px">
|
||||
<el-form-item :label="$t('home.roomUrl')">
|
||||
<el-input ref="roomUrlInput" readonly :value="obsRoomUrl" style="width: calc(100% - 8em); margin-right: 1em;"></el-input>
|
||||
<el-button type="primary" icon="el-icon-copy-document" @click="copyUrl"></el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :disabled="!roomUrl" @click="enterRoom">{{$t('home.enterRoom')}}</el-button>
|
||||
<el-button @click="enterTestRoom">{{$t('home.enterTestRoom')}}</el-button>
|
||||
<el-button @click="exportConfig">{{$t('home.exportConfig')}}</el-button>
|
||||
<el-button @click="importConfig">{{$t('home.importConfig')}}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import download from 'downloadjs'
|
||||
|
||||
import {mergeConfig} from '@/utils'
|
||||
import config from '@/api/config'
|
||||
import { mergeConfig } from '@/utils'
|
||||
import * as mainApi from '@/api/main'
|
||||
import * as chatConfig from '@/api/chatConfig'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
roomId: parseInt(window.localStorage.roomId || '1'),
|
||||
...config.getLocalConfig()
|
||||
serverConfig: {
|
||||
enableTranslate: true,
|
||||
enableUploadFile: true,
|
||||
loaderUrl: ''
|
||||
},
|
||||
roomUrl: ''
|
||||
form: {
|
||||
...chatConfig.getLocalConfig(),
|
||||
roomId: parseInt(window.localStorage.roomId || '1')
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
roomUrl() {
|
||||
return this.getRoomUrl(false)
|
||||
},
|
||||
obsRoomUrl() {
|
||||
if (this.roomUrl === '') {
|
||||
return ''
|
||||
}
|
||||
if (this.serverConfig.loaderUrl === '') {
|
||||
return this.roomUrl
|
||||
}
|
||||
let url = new URL(this.serverConfig.loaderUrl)
|
||||
url.searchParams.append('url', this.roomUrl)
|
||||
return url.href
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
roomUrl: _.debounce(function() {
|
||||
window.localStorage.roomId = this.form.roomId
|
||||
chatConfig.setLocalConfig(this.form)
|
||||
}, 500)
|
||||
},
|
||||
mounted() {
|
||||
this.updateServerConfig()
|
||||
},
|
||||
methods: {
|
||||
saveConfig() {
|
||||
this.$refs.form.validate(async valid => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
window.localStorage.roomId = this.form.roomId
|
||||
config.setLocalConfig(this.form)
|
||||
async updateServerConfig() {
|
||||
try {
|
||||
this.serverConfig = (await mainApi.getServerInfo()).config
|
||||
} catch (e) {
|
||||
this.$message.error(`Failed to fetch server information: ${e}`)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
|
||||
try {
|
||||
if (window.localStorage.configId) {
|
||||
try {
|
||||
await config.setRemoteConfig(window.localStorage.configId, this.form)
|
||||
} catch (e) { // 404
|
||||
window.localStorage.configId = (await config.createRemoteConfig(this.form)).id
|
||||
}
|
||||
} else {
|
||||
window.localStorage.configId = (await config.createRemoteConfig(this.form)).id
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('home.failedToSave') + e)
|
||||
return
|
||||
}
|
||||
this.$message({message: this.$t('home.successfullySaved'), type: 'success'})
|
||||
|
||||
let resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId},
|
||||
query: {config_id: window.localStorage.configId}})
|
||||
this.roomUrl = `http://${window.location.host}${resolved.href}`
|
||||
addEmoticon() {
|
||||
this.form.emoticons.push({
|
||||
keyword: '[Kappa]',
|
||||
url: ''
|
||||
})
|
||||
},
|
||||
delEmoticon(index) {
|
||||
this.form.emoticons.splice(index, 1)
|
||||
},
|
||||
uploadEmoticon(emoticon) {
|
||||
let input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/png, image/jpeg, image/jpg, image/gif'
|
||||
input.onchange = async() => {
|
||||
let file = input.files[0]
|
||||
if (file.size > 1024 * 1024) {
|
||||
this.$message.error(this.$t('home.emoticonFileTooLarge'))
|
||||
return
|
||||
}
|
||||
|
||||
let res
|
||||
try {
|
||||
res = await mainApi.uploadEmoticon(file)
|
||||
} catch (e) {
|
||||
this.$message.error(`Failed to upload: ${e}`)
|
||||
throw e
|
||||
}
|
||||
emoticon.url = res.url
|
||||
}
|
||||
input.click()
|
||||
},
|
||||
|
||||
enterRoom() {
|
||||
window.open(this.roomUrl, `room ${this.form.roomId}`, 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
|
||||
},
|
||||
enterTestRoom() {
|
||||
window.open(this.getRoomUrl(true), 'test room', 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
|
||||
},
|
||||
getRoomUrl(isTestRoom) {
|
||||
if (!isTestRoom && this.form.roomId === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
let query = {
|
||||
...this.form,
|
||||
emoticons: JSON.stringify(this.form.emoticons),
|
||||
lang: this.$i18n.locale
|
||||
}
|
||||
delete query.roomId
|
||||
|
||||
let resolved
|
||||
if (isTestRoom) {
|
||||
resolved = this.$router.resolve({ name: 'test_room', query })
|
||||
} else {
|
||||
resolved = this.$router.resolve({ name: 'room', params: { roomId: this.form.roomId }, query })
|
||||
}
|
||||
return `${window.location.protocol}//${window.location.host}${resolved.href}`
|
||||
},
|
||||
copyUrl() {
|
||||
this.$refs.roomUrlInput.select()
|
||||
document.execCommand('Copy')
|
||||
},
|
||||
exportConfig() {
|
||||
let cfg = mergeConfig(this.form, config.DEFAULT_CONFIG)
|
||||
let cfg = mergeConfig(this.form, chatConfig.DEFAULT_CONFIG)
|
||||
download(JSON.stringify(cfg, null, 2), 'blivechat.json', 'application/json')
|
||||
},
|
||||
importConfig() {
|
||||
@@ -144,19 +303,20 @@ export default {
|
||||
this.$message.error(this.$t('home.failedToParseConfig') + e)
|
||||
return
|
||||
}
|
||||
cfg = mergeConfig(cfg, config.DEFAULT_CONFIG)
|
||||
this.form = {roomId: this.form.roomId, ...cfg}
|
||||
this.importConfigFromObj(cfg)
|
||||
}
|
||||
reader.readAsText(input.files[0])
|
||||
}
|
||||
input.click()
|
||||
},
|
||||
importConfigFromObj(cfg) {
|
||||
cfg = mergeConfig(cfg, chatConfig.deepCloneDefaultConfig())
|
||||
chatConfig.sanitizeConfig(cfg)
|
||||
this.form = {
|
||||
...cfg,
|
||||
roomId: this.form.roomId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-form {
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,136 +1,266 @@
|
||||
<template>
|
||||
<chat-renderer :paidMessages="paidMessages" :messages="messages" :css="config.css"></chat-renderer>
|
||||
<chat-renderer ref="renderer" :maxNumber="config.maxNumber" :showGiftName="config.showGiftName"></chat-renderer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from '@/api/config'
|
||||
import * as i18n from '@/i18n'
|
||||
import { mergeConfig, toBool, toInt } from '@/utils'
|
||||
import * as trie from '@/utils/trie'
|
||||
import * as pronunciation from '@/utils/pronunciation'
|
||||
import * as chatConfig from '@/api/chatConfig'
|
||||
import ChatClientTest from '@/api/chat/ChatClientTest'
|
||||
import ChatClientDirect from '@/api/chat/ChatClientDirect'
|
||||
import ChatClientRelay from '@/api/chat/ChatClientRelay'
|
||||
import ChatRenderer from '@/components/ChatRenderer'
|
||||
import * as constants from '@/components/ChatRenderer/constants'
|
||||
|
||||
const COMMAND_JOIN_ROOM = 0
|
||||
const COMMAND_ADD_TEXT = 1
|
||||
const COMMAND_ADD_GIFT = 2
|
||||
const COMMAND_ADD_MEMBER = 3
|
||||
|
||||
export default {
|
||||
name: 'Room',
|
||||
components: {
|
||||
ChatRenderer
|
||||
},
|
||||
data() {
|
||||
let cfg = {...config.DEFAULT_CONFIG}
|
||||
cfg.blockKeywords = cfg.blockKeywords.split('\n').filter(val => val)
|
||||
cfg.blockUsers = cfg.blockUsers.split('\n').filter(val => val)
|
||||
cfg.maxSpeed = 0
|
||||
return {
|
||||
config: cfg,
|
||||
websocket: null,
|
||||
messagesBufferTimerId: null,
|
||||
nextId: 0,
|
||||
messagesBuffer: [], // 暂时不显示的消息,可能会丢弃
|
||||
messages: [], // 正在显示的消息
|
||||
paidMessages: []
|
||||
props: {
|
||||
roomId: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
strConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
// 开发时使用localhost:12450
|
||||
const url = process.env.NODE_ENV === 'development' ? 'ws://localhost:12450/chat' : `ws://${window.location.host}/chat`
|
||||
this.websocket = new WebSocket(url)
|
||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||
this.websocket.onmessage = this.onWsMessage.bind(this)
|
||||
|
||||
if (this.$route.query.config_id) {
|
||||
try {
|
||||
let cfg = await config.getRemoteConfig(this.$route.query.config_id)
|
||||
cfg.blockKeywords = cfg.blockKeywords.split('\n').filter(val => val)
|
||||
cfg.blockUsers = cfg.blockUsers.split('\n').filter(val => val)
|
||||
this.config = cfg
|
||||
} catch (e) {
|
||||
this.$message.error('获取配置失败:' + e)
|
||||
}
|
||||
data() {
|
||||
return {
|
||||
config: chatConfig.deepCloneDefaultConfig(),
|
||||
chatClient: null,
|
||||
pronunciationConverter: null,
|
||||
textEmoticons: {}, // 官方的文本表情,运行时从弹幕消息收集
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
blockKeywordsTrie() {
|
||||
let blockKeywords = this.config.blockKeywords.split('\n')
|
||||
let res = new trie.Trie()
|
||||
for (let keyword of blockKeywords) {
|
||||
if (keyword !== '') {
|
||||
res.set(keyword, true)
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
blockUsersTrie() {
|
||||
let blockUsers = this.config.blockUsers.split('\n')
|
||||
let res = new trie.Trie()
|
||||
for (let user of blockUsers) {
|
||||
if (user !== '') {
|
||||
res.set(user, true)
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
emoticonsTrie() {
|
||||
let res = new trie.Trie()
|
||||
for (let emoticon of this.config.emoticons) {
|
||||
if (emoticon.keyword !== '' && emoticon.url !== '') {
|
||||
res.set(emoticon.keyword, emoticon)
|
||||
}
|
||||
}
|
||||
for (let emoticon of Object.values(this.textEmoticons)) {
|
||||
res.set(emoticon.keyword, emoticon)
|
||||
}
|
||||
return res
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initConfig()
|
||||
this.initChatClient()
|
||||
if (this.config.giftUsernamePronunciation !== '') {
|
||||
this.pronunciationConverter = new pronunciation.PronunciationConverter()
|
||||
this.pronunciationConverter.loadDict(this.config.giftUsernamePronunciation)
|
||||
}
|
||||
|
||||
// 提示用户已加载
|
||||
this.$message({
|
||||
message: 'Loaded',
|
||||
duration: '500'
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.messagesBufferTimerId) {
|
||||
window.clearInterval(this.messagesBufferTimerId)
|
||||
}
|
||||
this.websocket.close()
|
||||
},
|
||||
watch: {
|
||||
config(val) {
|
||||
if (this.messagesBufferTimerId) {
|
||||
window.clearInterval(this.messagesBufferTimerId)
|
||||
this.messagesBufferTimerId = null
|
||||
}
|
||||
if (val.maxSpeed > 0) {
|
||||
this.messagesBufferTimerId = window.setInterval(this.handleMessagesBuffer.bind(this), 1000 / val.maxSpeed)
|
||||
}
|
||||
if (this.chatClient) {
|
||||
this.chatClient.stop()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onWsOpen() {
|
||||
this.websocket.send(JSON.stringify({
|
||||
cmd: COMMAND_JOIN_ROOM,
|
||||
data: {
|
||||
roomId: parseInt(this.$route.params.roomId)
|
||||
initConfig() {
|
||||
let locale = this.strConfig.lang
|
||||
if (locale) {
|
||||
i18n.setLocale(locale)
|
||||
}
|
||||
|
||||
let cfg = {}
|
||||
// 留空的使用默认值
|
||||
for (let i in this.strConfig) {
|
||||
if (this.strConfig[i] !== '') {
|
||||
cfg[i] = this.strConfig[i]
|
||||
}
|
||||
}))
|
||||
}
|
||||
cfg = mergeConfig(cfg, chatConfig.deepCloneDefaultConfig())
|
||||
|
||||
cfg.minGiftPrice = toInt(cfg.minGiftPrice, chatConfig.DEFAULT_CONFIG.minGiftPrice)
|
||||
cfg.showDanmaku = toBool(cfg.showDanmaku)
|
||||
cfg.showGift = toBool(cfg.showGift)
|
||||
cfg.showGiftName = toBool(cfg.showGiftName)
|
||||
cfg.mergeSimilarDanmaku = toBool(cfg.mergeSimilarDanmaku)
|
||||
cfg.mergeGift = toBool(cfg.mergeGift)
|
||||
cfg.maxNumber = toInt(cfg.maxNumber, chatConfig.DEFAULT_CONFIG.maxNumber)
|
||||
|
||||
cfg.blockGiftDanmaku = toBool(cfg.blockGiftDanmaku)
|
||||
cfg.blockLevel = toInt(cfg.blockLevel, chatConfig.DEFAULT_CONFIG.blockLevel)
|
||||
cfg.blockNewbie = toBool(cfg.blockNewbie)
|
||||
cfg.blockNotMobileVerified = toBool(cfg.blockNotMobileVerified)
|
||||
cfg.blockMedalLevel = toInt(cfg.blockMedalLevel, chatConfig.DEFAULT_CONFIG.blockMedalLevel)
|
||||
|
||||
cfg.relayMessagesByServer = toBool(cfg.relayMessagesByServer)
|
||||
cfg.autoTranslate = toBool(cfg.autoTranslate)
|
||||
cfg.emoticons = this.toObjIfJson(cfg.emoticons)
|
||||
|
||||
chatConfig.sanitizeConfig(cfg)
|
||||
this.config = cfg
|
||||
},
|
||||
onWsMessage(event) {
|
||||
let {cmd, data} = JSON.parse(event.data)
|
||||
let message = null
|
||||
let time = data.timestamp ? new Date(data.timestamp * 1000) : new Date()
|
||||
switch(cmd) {
|
||||
case COMMAND_ADD_TEXT:
|
||||
if (!this.config.showDanmaku || !this.filterTextMessage(data) || this.mergeSimilar(data.content)) {
|
||||
break
|
||||
}
|
||||
message = {
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
authorName: data.authorName,
|
||||
authorType: data.authorType,
|
||||
content: data.content,
|
||||
privilegeType: data.privilegeType,
|
||||
repeated: 1
|
||||
}
|
||||
break
|
||||
case COMMAND_ADD_GIFT: {
|
||||
if (!this.config.showGift) {
|
||||
break
|
||||
}
|
||||
let price = data.totalCoin / 1000
|
||||
if (price < this.config.minGiftPrice) // 丢人
|
||||
break
|
||||
message = {
|
||||
type: constants.MESSAGE_TYPE_GIFT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
authorName: data.authorName,
|
||||
price: price,
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
content: `Sent ${data.giftName}x${data.giftNum}`
|
||||
}
|
||||
break
|
||||
toObjIfJson(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return str
|
||||
}
|
||||
case COMMAND_ADD_MEMBER:
|
||||
if (!this.config.showGift) {
|
||||
break
|
||||
}
|
||||
message = {
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
avatarUrl: data.avatarUrl,
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
authorName: data.authorName,
|
||||
title: 'NEW MEMBER!',
|
||||
content: `Welcome ${data.authorName}`
|
||||
}
|
||||
break
|
||||
}
|
||||
if (message) {
|
||||
this.addMessageBuffer(message)
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
initChatClient() {
|
||||
if (this.roomId === null) {
|
||||
this.chatClient = new ChatClientTest()
|
||||
} else {
|
||||
if (!this.config.relayMessagesByServer) {
|
||||
this.chatClient = new ChatClientDirect(this.roomId)
|
||||
} else {
|
||||
this.chatClient = new ChatClientRelay(this.roomId, this.config.autoTranslate)
|
||||
}
|
||||
}
|
||||
this.chatClient.onAddText = this.onAddText
|
||||
this.chatClient.onAddGift = this.onAddGift
|
||||
this.chatClient.onAddMember = this.onAddMember
|
||||
this.chatClient.onAddSuperChat = this.onAddSuperChat
|
||||
this.chatClient.onDelSuperChat = this.onDelSuperChat
|
||||
this.chatClient.onUpdateTranslation = this.onUpdateTranslation
|
||||
this.chatClient.start()
|
||||
},
|
||||
|
||||
start() {
|
||||
this.chatClient.start()
|
||||
},
|
||||
stop() {
|
||||
this.chatClient.stop()
|
||||
},
|
||||
|
||||
onAddText(data) {
|
||||
if (!this.config.showDanmaku || !this.filterTextMessage(data) || this.mergeSimilarText(data.content)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新官方文本表情
|
||||
for (let [keyword, url] of data.textEmoticons) {
|
||||
if (!(keyword in this.textEmoticons)) {
|
||||
let emoticon = { keyword, url }
|
||||
this.$set(this.textEmoticons, keyword, emoticon)
|
||||
}
|
||||
}
|
||||
|
||||
let message = {
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
authorName: data.authorName,
|
||||
authorType: data.authorType,
|
||||
content: data.content,
|
||||
richContent: this.getRichContent(data),
|
||||
privilegeType: data.privilegeType,
|
||||
repeated: 1,
|
||||
translation: data.translation
|
||||
}
|
||||
this.$refs.renderer.addMessage(message)
|
||||
},
|
||||
onAddGift(data) {
|
||||
if (!this.config.showGift) {
|
||||
return
|
||||
}
|
||||
let price = data.totalCoin / 1000
|
||||
if (this.mergeSimilarGift(data.authorName, price, data.giftName, data.num)) {
|
||||
return
|
||||
}
|
||||
if (price < this.config.minGiftPrice) { // 丢人
|
||||
return
|
||||
}
|
||||
let message = {
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_GIFT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
authorName: data.authorName,
|
||||
authorNamePronunciation: this.getPronunciation(data.authorName),
|
||||
price: price,
|
||||
giftName: data.giftName,
|
||||
num: data.num
|
||||
}
|
||||
this.$refs.renderer.addMessage(message)
|
||||
},
|
||||
onAddMember(data) {
|
||||
if (!this.config.showGift || !this.filterNewMemberMessage(data)) {
|
||||
return
|
||||
}
|
||||
let message = {
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
avatarUrl: data.avatarUrl,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
authorName: data.authorName,
|
||||
authorNamePronunciation: this.getPronunciation(data.authorName),
|
||||
privilegeType: data.privilegeType,
|
||||
title: this.$t('chat.membershipTitle')
|
||||
}
|
||||
this.$refs.renderer.addMessage(message)
|
||||
},
|
||||
onAddSuperChat(data) {
|
||||
if (!this.config.showGift || !this.filterSuperChatMessage(data)) {
|
||||
return
|
||||
}
|
||||
if (data.price < this.config.minGiftPrice) { // 丢人
|
||||
return
|
||||
}
|
||||
let message = {
|
||||
id: data.id,
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
authorName: data.authorName,
|
||||
authorNamePronunciation: this.getPronunciation(data.authorName),
|
||||
price: data.price,
|
||||
time: new Date(data.timestamp * 1000),
|
||||
content: data.content.trim(),
|
||||
translation: data.translation
|
||||
}
|
||||
this.$refs.renderer.addMessage(message)
|
||||
},
|
||||
onDelSuperChat(data) {
|
||||
this.$refs.renderer.delMessages(data.ids)
|
||||
},
|
||||
onUpdateTranslation(data) {
|
||||
if (!this.config.autoTranslate) {
|
||||
return
|
||||
}
|
||||
this.$refs.renderer.updateMessage(data.id, { translation: data.translation })
|
||||
},
|
||||
|
||||
filterTextMessage(data) {
|
||||
if (this.config.blockGiftDanmaku && data.isGiftDanmaku) {
|
||||
return false
|
||||
@@ -143,77 +273,104 @@ export default {
|
||||
} else if (this.config.blockMedalLevel > 0 && data.medalLevel < this.config.blockMedalLevel) {
|
||||
return false
|
||||
}
|
||||
for (let keyword of this.config.blockKeywords) {
|
||||
if (data.content.indexOf(keyword) !== -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (let user of this.config.blockUsers) {
|
||||
if (data.authorName === user) {
|
||||
return this.filterByContent(data.content) && this.filterByAuthorName(data.authorName)
|
||||
},
|
||||
filterSuperChatMessage(data) {
|
||||
return this.filterByContent(data.content) && this.filterByAuthorName(data.authorName)
|
||||
},
|
||||
filterNewMemberMessage(data) {
|
||||
return this.filterByAuthorName(data.authorName)
|
||||
},
|
||||
filterByContent(content) {
|
||||
let blockKeywordsTrie = this.blockKeywordsTrie
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let remainContent = content.substring(i)
|
||||
if (blockKeywordsTrie.lazyMatch(remainContent) !== null) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
mergeSimilar(content) {
|
||||
filterByAuthorName(authorName) {
|
||||
return !this.blockUsersTrie.has(authorName)
|
||||
},
|
||||
mergeSimilarText(content) {
|
||||
if (!this.config.mergeSimilarDanmaku) {
|
||||
return false
|
||||
}
|
||||
for (let i = this.messages.length - 1; i >= 0 && i >= this.messages.length - 5; i--) {
|
||||
let message = this.messages[i]
|
||||
if (
|
||||
(message.content.indexOf(content) !== -1 || content.indexOf(message.content) !== -1) // 包含对方
|
||||
&& Math.abs(message.content.length - content.length) < Math.min(message.content.length, content.length) // 长度差比两者长度都小
|
||||
) {
|
||||
message.repeated++
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return this.$refs.renderer.mergeSimilarText(content)
|
||||
},
|
||||
addMessageBuffer(message) {
|
||||
if (this.config.maxSpeed > 0 && message.type === 0) {
|
||||
message.addTime = new Date()
|
||||
this.messagesBuffer.push(message)
|
||||
} else {
|
||||
// 无速度限制或者是礼物
|
||||
this.addMessageShow(message)
|
||||
mergeSimilarGift(authorName, price, giftName, num) {
|
||||
if (!this.config.mergeGift) {
|
||||
return false
|
||||
}
|
||||
return this.$refs.renderer.mergeSimilarGift(authorName, price, giftName, num)
|
||||
},
|
||||
addMessageShow(message) {
|
||||
message.id = this.nextId++
|
||||
message.addTime = new Date()
|
||||
this.messages.push(message)
|
||||
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
||||
this.paidMessages.unshift(message)
|
||||
getPronunciation(text) {
|
||||
if (this.pronunciationConverter === null) {
|
||||
return ''
|
||||
}
|
||||
// 防止同时添加和删除项目时所有的项目重新渲染 https://github.com/vuejs/vue/issues/6857
|
||||
this.$nextTick(() => {
|
||||
if (this.messages.length > 50) {
|
||||
this.messages.splice(0, this.messages.length - 50)
|
||||
}
|
||||
})
|
||||
return this.pronunciationConverter.getPronunciation(text)
|
||||
},
|
||||
handleMessagesBuffer() {
|
||||
// 3秒内未发出则丢弃
|
||||
let i
|
||||
let curTime = new Date()
|
||||
for (i = 0; i < this.messagesBuffer.length; i++) {
|
||||
if (curTime - this.messagesBuffer[i].addTime <= 3 * 1000) {
|
||||
break
|
||||
getRichContent(data) {
|
||||
let richContent = []
|
||||
|
||||
// B站官方表情
|
||||
if (data.emoticon !== null) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_IMAGE,
|
||||
text: data.content,
|
||||
url: data.emoticon
|
||||
})
|
||||
return richContent
|
||||
}
|
||||
|
||||
// 没有文本表情,只能是纯文本
|
||||
if (this.config.emoticons.length === 0 && Object.keys(this.textEmoticons).length === 0) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
text: data.content
|
||||
})
|
||||
return richContent
|
||||
}
|
||||
|
||||
// 可能含有文本表情,需要解析
|
||||
let emoticonsTrie = this.emoticonsTrie
|
||||
let startPos = 0
|
||||
let pos = 0
|
||||
while (pos < data.content.length) {
|
||||
let remainContent = data.content.substring(pos)
|
||||
let matchEmoticon = emoticonsTrie.lazyMatch(remainContent)
|
||||
if (matchEmoticon === null) {
|
||||
pos++
|
||||
continue
|
||||
}
|
||||
|
||||
// 加入之前的文本
|
||||
if (pos !== startPos) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
text: data.content.slice(startPos, pos)
|
||||
})
|
||||
}
|
||||
|
||||
// 加入表情
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_IMAGE,
|
||||
text: matchEmoticon.keyword,
|
||||
url: matchEmoticon.url
|
||||
})
|
||||
pos += matchEmoticon.keyword.length
|
||||
startPos = pos
|
||||
}
|
||||
this.messagesBuffer.splice(0, i)
|
||||
if (this.messagesBuffer.length <= 0) {
|
||||
return
|
||||
// 加入尾部的文本
|
||||
if (pos !== startPos) {
|
||||
richContent.push({
|
||||
type: constants.CONTENT_TYPE_TEXT,
|
||||
text: data.content.slice(startPos, pos)
|
||||
})
|
||||
}
|
||||
let message = this.messagesBuffer.shift()
|
||||
// 防止短时间发送多条时不能合并
|
||||
if (this.mergeSimilar(message.content)) {
|
||||
this.handleMessagesBuffer()
|
||||
return
|
||||
}
|
||||
this.addMessageShow(message)
|
||||
return richContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
frontend/src/views/StyleGenerator/FontSelect.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<el-select :value="value" @input="val => $emit('input', val)" filterable allow-create default-first-option>
|
||||
<el-option-group>
|
||||
<el-option v-for="font in LOCAL_FONTS" :key="font" :value="font"></el-option>
|
||||
</el-option-group>
|
||||
<el-option-group>
|
||||
<el-option v-for="font in NETWORK_FONTS" :key="font" :value="font"></el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as fonts from './fonts'
|
||||
|
||||
export default {
|
||||
name: 'FontSelect',
|
||||
props: {
|
||||
value: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
LOCAL_FONTS: fonts.LOCAL_FONTS,
|
||||
NETWORK_FONTS: fonts.NETWORK_FONTS
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
</style>
|
||||
702
frontend/src/views/StyleGenerator/Legacy.vue
Normal file
@@ -0,0 +1,702 @@
|
||||
<template>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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-row>
|
||||
</el-card>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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-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-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>
|
||||
|
||||
<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-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import FontSelect from './FontSelect'
|
||||
import * as common from './common'
|
||||
import { mergeConfig } from '@/utils'
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
showOutlines: true,
|
||||
outlineSize: 2,
|
||||
outlineColor: '#000000',
|
||||
|
||||
showAvatars: true,
|
||||
avatarSize: 24,
|
||||
|
||||
showUserNames: true,
|
||||
userNameFont: 'Changa One',
|
||||
userNameFontSize: 20,
|
||||
userNameLineHeight: 0,
|
||||
userNameColor: '#cccccc',
|
||||
ownerUserNameColor: '#ffd600',
|
||||
moderatorUserNameColor: '#5e84f1',
|
||||
memberUserNameColor: '#0f9d58',
|
||||
showBadges: true,
|
||||
showColon: true,
|
||||
|
||||
messageFont: 'Imprima',
|
||||
messageFontSize: 18,
|
||||
messageLineHeight: 0,
|
||||
messageColor: '#ffffff',
|
||||
messageOnNewLine: false,
|
||||
emoticonSize: 18,
|
||||
|
||||
showTime: false,
|
||||
timeFont: 'Imprima',
|
||||
timeFontSize: 16,
|
||||
timeLineHeight: 0,
|
||||
timeColor: '#999999',
|
||||
|
||||
bgColor: 'rgba(0, 0, 0, 0)',
|
||||
useBarsInsteadOfBg: false,
|
||||
messageBgColor: 'rgba(204, 204, 204, 0)',
|
||||
ownerMessageBgColor: 'rgba(255, 214, 0, 0)',
|
||||
moderatorMessageBgColor: 'rgba(94, 132, 241, 0)',
|
||||
memberMessageBgColor: 'rgba(15, 157, 88, 0)',
|
||||
|
||||
firstLineFont: 'Changa One',
|
||||
firstLineFontSize: 20,
|
||||
firstLineLineHeight: 0,
|
||||
firstLineColor: '#ffffff',
|
||||
secondLineFont: 'Imprima',
|
||||
secondLineFontSize: 18,
|
||||
secondLineLineHeight: 0,
|
||||
secondLineColor: '#ffffff',
|
||||
scContentFont: 'Imprima',
|
||||
scContentFontSize: 18,
|
||||
scContentLineHeight: 0,
|
||||
scContentColor: '#ffffff',
|
||||
showNewMemberBg: true,
|
||||
showScTicker: false,
|
||||
showOtherThings: true,
|
||||
|
||||
animateIn: false,
|
||||
fadeInTime: 200, // ms
|
||||
animateOut: false,
|
||||
animateOutWaitTime: 30, // s
|
||||
fadeOutTime: 200, // ms
|
||||
slide: false,
|
||||
reverseSlide: false
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Legacy',
|
||||
components: {
|
||||
FontSelect
|
||||
},
|
||||
props: {
|
||||
value: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: this.loadConfig()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
result() {
|
||||
return `${this.importStyle}
|
||||
|
||||
${common.COMMON_STYLE}
|
||||
|
||||
${this.paddingStyle}
|
||||
|
||||
${this.outlineStyle}
|
||||
|
||||
${this.avatarStyle}
|
||||
|
||||
${this.userNameStyle}
|
||||
|
||||
${this.messageStyle}
|
||||
|
||||
${this.timeStyle}
|
||||
|
||||
${this.backgroundStyle}
|
||||
|
||||
${this.scAndNewMemberStyle}
|
||||
|
||||
${this.animationStyle}
|
||||
`
|
||||
},
|
||||
importStyle() {
|
||||
let allFonts = []
|
||||
for (let name of ['userNameFont', 'messageFont', 'timeFont', 'firstLineFont', 'secondLineFont', 'scContentFont']) {
|
||||
allFonts.push(this.form[name])
|
||||
}
|
||||
return common.getImportStyle(allFonts)
|
||||
},
|
||||
paddingStyle() {
|
||||
return `/* Reduce side padding */
|
||||
yt-live-chat-text-message-renderer {
|
||||
padding-left: ${this.form.useBarsInsteadOfBg ? 20 : 4}px !important;
|
||||
padding-right: 4px !important;
|
||||
}`
|
||||
},
|
||||
outlineStyle() {
|
||||
return `/* Outlines */
|
||||
yt-live-chat-renderer * {
|
||||
${this.showOutlinesStyle}
|
||||
font-family: "${common.cssEscapeStr(this.form.messageFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.messageFontSize}px !important;
|
||||
line-height: ${this.form.messageLineHeight || this.form.messageFontSize}px !important;
|
||||
}`
|
||||
},
|
||||
showOutlinesStyle() {
|
||||
if (!this.form.showOutlines || !this.form.outlineSize) {
|
||||
return ''
|
||||
}
|
||||
let shadow = []
|
||||
for (let x = -this.form.outlineSize; x <= this.form.outlineSize; x += Math.ceil(this.form.outlineSize / 4)) {
|
||||
for (let y = -this.form.outlineSize; y <= this.form.outlineSize; y += Math.ceil(this.form.outlineSize / 4)) {
|
||||
shadow.push(`${x}px ${y}px ${this.form.outlineColor}`)
|
||||
}
|
||||
}
|
||||
return `text-shadow: ${shadow.join(', ')};`
|
||||
},
|
||||
avatarStyle() {
|
||||
return common.getAvatarStyle(this.form)
|
||||
},
|
||||
userNameStyle() {
|
||||
return `/* Channel names */
|
||||
yt-live-chat-text-message-renderer #author-name[type="owner"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
|
||||
${this.form.ownerUserNameColor ? `color: ${this.form.ownerUserNameColor} !important;` : ''}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #author-name[type="moderator"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
|
||||
${this.form.moderatorUserNameColor ? `color: ${this.form.moderatorUserNameColor} !important;` : ''}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #author-name[type="member"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
|
||||
${this.form.memberUserNameColor ? `color: ${this.form.memberUserNameColor} !important;` : ''}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #author-name {
|
||||
${this.form.showUserNames ? '' : 'display: none !important;'}
|
||||
${this.form.userNameColor ? `color: ${this.form.userNameColor} !important;` : ''}
|
||||
font-family: "${common.cssEscapeStr(this.form.userNameFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.userNameFontSize}px !important;
|
||||
line-height: ${this.form.userNameLineHeight || this.form.userNameFontSize}px !important;
|
||||
}
|
||||
|
||||
${!this.form.showColon ? '' : `/* Show colon */
|
||||
yt-live-chat-text-message-renderer #author-name::after {
|
||||
content: ":";
|
||||
margin-left: ${this.form.outlineSize}px;
|
||||
}`}
|
||||
|
||||
/* Hide badges */
|
||||
yt-live-chat-text-message-renderer #chat-badges {
|
||||
${this.form.showBadges ? '' : 'display: none !important;'}
|
||||
vertical-align: text-top !important;
|
||||
}`
|
||||
},
|
||||
messageStyle() {
|
||||
return `/* Messages */
|
||||
yt-live-chat-text-message-renderer #message,
|
||||
yt-live-chat-text-message-renderer #message * {
|
||||
${this.form.messageColor ? `color: ${this.form.messageColor} !important;` : ''}
|
||||
font-family: "${common.cssEscapeStr(this.form.messageFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.messageFontSize}px !important;
|
||||
line-height: ${this.form.messageLineHeight || this.form.messageFontSize}px !important;
|
||||
}
|
||||
|
||||
${!this.form.messageOnNewLine ? '' : `yt-live-chat-text-message-renderer #message {
|
||||
display: block !important;
|
||||
overflow: visible !important;
|
||||
}`}
|
||||
|
||||
yt-live-chat-text-message-renderer #message .emoji {
|
||||
width: auto !important;
|
||||
height: ${this.form.emoticonSize}px !important;
|
||||
}`
|
||||
},
|
||||
timeStyle() {
|
||||
return common.getTimeStyle(this.form)
|
||||
},
|
||||
backgroundStyle() {
|
||||
return `/* Background colors */
|
||||
body {
|
||||
overflow: hidden;
|
||||
${this.form.bgColor ? `background-color: ${this.form.bgColor};` : ''}
|
||||
}
|
||||
|
||||
${this.getBgStyleForAuthorType('', this.form.messageBgColor)}
|
||||
|
||||
${this.getBgStyleForAuthorType('owner', this.form.ownerMessageBgColor)}
|
||||
|
||||
${this.getBgStyleForAuthorType('moderator', this.form.moderatorMessageBgColor)}
|
||||
|
||||
${this.getBgStyleForAuthorType('member', this.form.memberMessageBgColor)}`
|
||||
},
|
||||
scAndNewMemberStyle() {
|
||||
return `/* SuperChat/Fan Funding Messages */
|
||||
yt-live-chat-paid-message-renderer {
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
|
||||
${this.scAndNewMemberFontStyle}
|
||||
|
||||
yt-live-chat-membership-item-renderer #card,
|
||||
yt-live-chat-membership-item-renderer #header {
|
||||
${this.showNewMemberBgStyle}
|
||||
}
|
||||
|
||||
${this.scTickerStyle}
|
||||
|
||||
${this.form.showOtherThings ? '' : `yt-live-chat-item-list-renderer {
|
||||
display: none !important;
|
||||
}`}`
|
||||
},
|
||||
scAndNewMemberFontStyle() {
|
||||
return `yt-live-chat-paid-message-renderer #author-name,
|
||||
yt-live-chat-paid-message-renderer #author-name *,
|
||||
yt-live-chat-membership-item-renderer #header-content-inner-column,
|
||||
yt-live-chat-membership-item-renderer #header-content-inner-column * {
|
||||
${this.form.firstLineColor ? `color: ${this.form.firstLineColor} !important;` : ''}
|
||||
font-family: "${common.cssEscapeStr(this.form.firstLineFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.firstLineFontSize}px !important;
|
||||
line-height: ${this.form.firstLineLineHeight || this.form.firstLineFontSize}px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #purchase-amount,
|
||||
yt-live-chat-paid-message-renderer #purchase-amount *,
|
||||
yt-live-chat-membership-item-renderer #header-subtext,
|
||||
yt-live-chat-membership-item-renderer #header-subtext * {
|
||||
${this.form.secondLineColor ? `color: ${this.form.secondLineColor} !important;` : ''}
|
||||
font-family: "${common.cssEscapeStr(this.form.secondLineFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.secondLineFontSize}px !important;
|
||||
line-height: ${this.form.secondLineLineHeight || this.form.secondLineFontSize}px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #content,
|
||||
yt-live-chat-paid-message-renderer #content * {
|
||||
${this.form.scContentColor ? `color: ${this.form.scContentColor} !important;` : ''}
|
||||
font-family: "${common.cssEscapeStr(this.form.scContentFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.scContentFontSize}px !important;
|
||||
line-height: ${this.form.scContentLineHeight || this.form.scContentFontSize}px !important;
|
||||
}`
|
||||
},
|
||||
showNewMemberBgStyle() {
|
||||
if (this.form.showNewMemberBg) {
|
||||
return `background-color: ${this.form.memberUserNameColor} !important;
|
||||
margin: 4px 0 !important;`
|
||||
} else {
|
||||
return `background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
margin: 0 !important;`
|
||||
}
|
||||
},
|
||||
scTickerStyle() {
|
||||
return `${this.form.showScTicker ? '' : `yt-live-chat-ticker-renderer {
|
||||
display: none !important;
|
||||
}`}
|
||||
|
||||
/* SuperChat Ticker */
|
||||
yt-live-chat-ticker-paid-message-item-renderer,
|
||||
yt-live-chat-ticker-paid-message-item-renderer *,
|
||||
yt-live-chat-ticker-sponsor-item-renderer,
|
||||
yt-live-chat-ticker-sponsor-item-renderer * {
|
||||
${this.form.secondLineColor ? `color: ${this.form.secondLineColor} !important;` : ''}
|
||||
font-family: "${common.cssEscapeStr(this.form.secondLineFont)}"${common.FALLBACK_FONTS};
|
||||
}`
|
||||
},
|
||||
animationStyle() {
|
||||
return common.getAnimationStyle(this.form)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
result(val) {
|
||||
this.$emit('input', val)
|
||||
this.saveConfig()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$emit('input', this.result)
|
||||
},
|
||||
methods: {
|
||||
saveConfig: _.debounce(function() {
|
||||
let config = mergeConfig(this.form, DEFAULT_CONFIG)
|
||||
window.localStorage.stylegenConfig = JSON.stringify(config)
|
||||
}, 500),
|
||||
loadConfig() {
|
||||
try {
|
||||
return mergeConfig(JSON.parse(window.localStorage.stylegenConfig), DEFAULT_CONFIG)
|
||||
} catch {
|
||||
return { ...DEFAULT_CONFIG }
|
||||
}
|
||||
},
|
||||
resetConfig() {
|
||||
this.form = { ...DEFAULT_CONFIG }
|
||||
},
|
||||
|
||||
getBgStyleForAuthorType(authorType, color) {
|
||||
let typeSelector = authorType ? `[author-type="${authorType}"]` : ''
|
||||
if (!this.form.useBarsInsteadOfBg) {
|
||||
return `yt-live-chat-text-message-renderer${typeSelector},
|
||||
yt-live-chat-text-message-renderer${typeSelector}[is-highlighted] {
|
||||
${color ? `background-color: ${color} !important;` : ''}
|
||||
}`
|
||||
} else {
|
||||
return `yt-live-chat-text-message-renderer${typeSelector}::after {
|
||||
${color ? `border: 2px solid ${color};` : ''}
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 8px;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 1px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
611
frontend/src/views/StyleGenerator/LineLike.vue
Normal file
@@ -0,0 +1,611 @@
|
||||
<template>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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-row>
|
||||
</el-card>
|
||||
|
||||
<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>
|
||||
|
||||
<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-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>
|
||||
|
||||
<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-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.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>
|
||||
|
||||
<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-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import FontSelect from './FontSelect'
|
||||
import * as common from './common'
|
||||
import { mergeConfig } from '@/utils'
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
showAvatars: true,
|
||||
avatarSize: 40,
|
||||
|
||||
showUserNames: true,
|
||||
userNameFont: 'Noto Sans SC',
|
||||
userNameFontSize: 20,
|
||||
userNameLineHeight: 0,
|
||||
userNameColor: '#cccccc',
|
||||
ownerUserNameColor: '#ffd600',
|
||||
moderatorUserNameColor: '#5e84f1',
|
||||
memberUserNameColor: '#0f9d58',
|
||||
showBadges: true,
|
||||
|
||||
messageFont: 'Noto Sans SC',
|
||||
messageFontSize: 18,
|
||||
messageLineHeight: 0,
|
||||
messageColor: '#000000',
|
||||
emoticonSize: 18,
|
||||
|
||||
showTime: false,
|
||||
timeFont: 'Noto Sans SC',
|
||||
timeFontSize: 16,
|
||||
timeLineHeight: 0,
|
||||
timeColor: '#999999',
|
||||
|
||||
bgColor: 'rgba(0, 0, 0, 0)',
|
||||
messageBgColor: '#ffffff',
|
||||
ownerMessageBgColor: 'rgba(231, 199, 30, 1)',
|
||||
moderatorMessageBgColor: 'rgba(41, 95, 251, 1)',
|
||||
memberMessageBgColor: 'rgba(43, 234, 43, 1)',
|
||||
|
||||
firstLineFont: 'Noto Sans SC',
|
||||
firstLineFontSize: 20,
|
||||
firstLineLineHeight: 0,
|
||||
secondLineFont: 'Noto Sans SC',
|
||||
secondLineFontSize: 18,
|
||||
secondLineLineHeight: 0,
|
||||
scContentFont: 'Noto Sans SC',
|
||||
scContentFontSize: 18,
|
||||
scContentLineHeight: 0,
|
||||
showScTicker: false,
|
||||
showOtherThings: true,
|
||||
|
||||
animateIn: true,
|
||||
fadeInTime: 200, // ms
|
||||
animateOut: false,
|
||||
animateOutWaitTime: 30, // s
|
||||
fadeOutTime: 200, // ms
|
||||
slide: true,
|
||||
reverseSlide: false
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'LineLike',
|
||||
components: {
|
||||
FontSelect
|
||||
},
|
||||
props: {
|
||||
value: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: this.loadConfig()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
result() {
|
||||
return `${this.importStyle}
|
||||
|
||||
${common.COMMON_STYLE}
|
||||
|
||||
${this.paddingStyle}
|
||||
|
||||
${this.avatarStyle}
|
||||
|
||||
${this.userNameStyle}
|
||||
|
||||
${this.messageStyle}
|
||||
|
||||
${this.timeStyle}
|
||||
|
||||
${this.backgroundStyle}
|
||||
|
||||
${this.scAndNewMemberStyle}
|
||||
|
||||
${this.animationStyle}
|
||||
`
|
||||
},
|
||||
importStyle() {
|
||||
let allFonts = []
|
||||
for (let name of ['userNameFont', 'messageFont', 'timeFont', 'firstLineFont', 'secondLineFont', 'scContentFont']) {
|
||||
allFonts.push(this.form[name])
|
||||
}
|
||||
return common.getImportStyle(allFonts)
|
||||
},
|
||||
paddingStyle() {
|
||||
return `/* Reduce side padding */
|
||||
yt-live-chat-text-message-renderer {
|
||||
padding-left: 4px !important;
|
||||
padding-right: 4px !important;
|
||||
}`
|
||||
},
|
||||
avatarStyle() {
|
||||
return common.getAvatarStyle(this.form)
|
||||
},
|
||||
userNameStyle() {
|
||||
return `/* Channel names */
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-chip {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #author-name[type="owner"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
|
||||
${this.form.ownerUserNameColor ? `color: ${this.form.ownerUserNameColor} !important;` : ''}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #author-name[type="moderator"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
|
||||
${this.form.moderatorUserNameColor ? `color: ${this.form.moderatorUserNameColor} !important;` : ''}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #author-name[type="member"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
|
||||
${this.form.memberUserNameColor ? `color: ${this.form.memberUserNameColor} !important;` : ''}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #author-name {
|
||||
${this.form.showUserNames ? '' : 'display: none !important;'}
|
||||
${this.form.userNameColor ? `color: ${this.form.userNameColor} !important;` : ''}
|
||||
font-family: "${common.cssEscapeStr(this.form.userNameFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.userNameFontSize}px !important;
|
||||
line-height: ${this.form.userNameLineHeight || this.form.userNameFontSize}px !important;
|
||||
}
|
||||
|
||||
/* Hide badges */
|
||||
yt-live-chat-text-message-renderer #chat-badges {
|
||||
${this.form.showBadges ? '' : 'display: none !important;'}
|
||||
vertical-align: text-top !important;
|
||||
}`
|
||||
},
|
||||
messageStyle() {
|
||||
return `/* Messages */
|
||||
yt-live-chat-text-message-renderer #message,
|
||||
yt-live-chat-text-message-renderer #message * {
|
||||
${this.form.messageColor ? `color: ${this.form.messageColor} !important;` : ''}
|
||||
font-family: "${common.cssEscapeStr(this.form.messageFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.messageFontSize}px !important;
|
||||
line-height: ${this.form.messageLineHeight || this.form.messageFontSize}px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #message {
|
||||
display: block !important;
|
||||
overflow: visible !important;
|
||||
padding: 20px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #message .emoji {
|
||||
width: auto !important;
|
||||
height: ${this.form.emoticonSize}px !important;
|
||||
}
|
||||
|
||||
/* The triangle beside dialog */
|
||||
yt-live-chat-text-message-renderer #message::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: ${this.form.showUserNames ? (this.form.userNameLineHeight || this.form.userNameFontSize) + 10 : 20}px;
|
||||
left: ${this.form.showAvatars ? this.form.avatarSize + (this.form.avatarSize / 4) - 8 : -8}px;
|
||||
border: 8px solid transparent;
|
||||
border-right: 18px solid;
|
||||
transform: rotate(35deg);
|
||||
}`
|
||||
},
|
||||
timeStyle() {
|
||||
return common.getTimeStyle(this.form)
|
||||
},
|
||||
backgroundStyle() {
|
||||
return `/* Background colors */
|
||||
body {
|
||||
overflow: hidden;
|
||||
${this.form.bgColor ? `background-color: ${this.form.bgColor};` : ''}
|
||||
}
|
||||
|
||||
${this.getBgStyleForAuthorType('', this.form.messageBgColor)}
|
||||
|
||||
${this.getBgStyleForAuthorType('owner', this.form.ownerMessageBgColor)}
|
||||
|
||||
${this.getBgStyleForAuthorType('moderator', this.form.moderatorMessageBgColor)}
|
||||
|
||||
${this.getBgStyleForAuthorType('member', this.form.memberMessageBgColor)}`
|
||||
},
|
||||
scAndNewMemberStyle() {
|
||||
return `/* SuperChat/Fan Funding Messages */
|
||||
yt-live-chat-paid-message-renderer {
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
|
||||
${this.scAndNewMemberFontStyle}
|
||||
|
||||
yt-live-chat-membership-item-renderer #card,
|
||||
yt-live-chat-membership-item-renderer #header {
|
||||
${this.showNewMemberBgStyle}
|
||||
}
|
||||
|
||||
${this.scTickerStyle}
|
||||
|
||||
${this.form.showOtherThings ? '' : `yt-live-chat-item-list-renderer {
|
||||
display: none !important;
|
||||
}`}`
|
||||
},
|
||||
scAndNewMemberFontStyle() {
|
||||
return `yt-live-chat-paid-message-renderer #author-name,
|
||||
yt-live-chat-paid-message-renderer #author-name *,
|
||||
yt-live-chat-membership-item-renderer #header-content-inner-column,
|
||||
yt-live-chat-membership-item-renderer #header-content-inner-column * {
|
||||
font-family: "${common.cssEscapeStr(this.form.firstLineFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.firstLineFontSize}px !important;
|
||||
line-height: ${this.form.firstLineLineHeight || this.form.firstLineFontSize}px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #purchase-amount,
|
||||
yt-live-chat-paid-message-renderer #purchase-amount *,
|
||||
yt-live-chat-membership-item-renderer #header-subtext,
|
||||
yt-live-chat-membership-item-renderer #header-subtext * {
|
||||
font-family: "${common.cssEscapeStr(this.form.secondLineFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.secondLineFontSize}px !important;
|
||||
line-height: ${this.form.secondLineLineHeight || this.form.secondLineFontSize}px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #content,
|
||||
yt-live-chat-paid-message-renderer #content * {
|
||||
font-family: "${common.cssEscapeStr(this.form.scContentFont)}"${common.FALLBACK_FONTS};
|
||||
font-size: ${this.form.scContentFontSize}px !important;
|
||||
line-height: ${this.form.scContentLineHeight || this.form.scContentFontSize}px !important;
|
||||
}`
|
||||
},
|
||||
showNewMemberBgStyle() {
|
||||
return `background-color: ${this.form.memberUserNameColor} !important;
|
||||
margin: 4px 0 !important;`
|
||||
},
|
||||
scTickerStyle() {
|
||||
return `${this.form.showScTicker ? '' : `yt-live-chat-ticker-renderer {
|
||||
display: none !important;
|
||||
}`}
|
||||
|
||||
/* SuperChat Ticker */
|
||||
yt-live-chat-ticker-paid-message-item-renderer,
|
||||
yt-live-chat-ticker-paid-message-item-renderer *,
|
||||
yt-live-chat-ticker-sponsor-item-renderer,
|
||||
yt-live-chat-ticker-sponsor-item-renderer * {
|
||||
font-family: "${common.cssEscapeStr(this.form.secondLineFont)}"${common.FALLBACK_FONTS};
|
||||
}`
|
||||
},
|
||||
animationStyle() {
|
||||
return common.getAnimationStyle(this.form)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
result(val) {
|
||||
this.$emit('input', val)
|
||||
this.saveConfig()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$emit('input', this.result)
|
||||
},
|
||||
methods: {
|
||||
saveConfig: _.debounce(function() {
|
||||
let config = mergeConfig(this.form, DEFAULT_CONFIG)
|
||||
window.localStorage.stylegenLineLikeConfig = JSON.stringify(config)
|
||||
}, 500),
|
||||
loadConfig() {
|
||||
try {
|
||||
return mergeConfig(JSON.parse(window.localStorage.stylegenLineLikeConfig), DEFAULT_CONFIG)
|
||||
} catch {
|
||||
return { ...DEFAULT_CONFIG }
|
||||
}
|
||||
},
|
||||
resetConfig() {
|
||||
this.form = { ...DEFAULT_CONFIG }
|
||||
},
|
||||
|
||||
getBgStyleForAuthorType(authorType, color) {
|
||||
if (!color) {
|
||||
color = '#ffffff'
|
||||
}
|
||||
let typeSelector = authorType ? `[author-type="${authorType}"]` : ''
|
||||
return `yt-live-chat-text-message-renderer${typeSelector} #message {
|
||||
background-color: ${color} !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer${typeSelector} #message::before {
|
||||
border-right-color: ${color};
|
||||
}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
162
frontend/src/views/StyleGenerator/common.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import * as fonts from './fonts'
|
||||
|
||||
export const FALLBACK_FONTS = ', "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "\\5FAE \\8F6F \\96C5 \\9ED1 ", SimHei, Arial, sans-serif'
|
||||
|
||||
export const COMMON_STYLE = `/* Transparent background */
|
||||
yt-live-chat-renderer {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-renderer {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-author-chip #author-name {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Hide scrollbar */
|
||||
yt-live-chat-item-list-renderer #items {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
yt-live-chat-item-list-renderer #item-scroller {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #content,
|
||||
yt-live-chat-membership-item-renderer #content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Hide header and input */
|
||||
yt-live-chat-header-renderer,
|
||||
yt-live-chat-message-input-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide unimportant messages */
|
||||
yt-live-chat-text-message-renderer[is-deleted],
|
||||
yt-live-chat-membership-item-renderer[is-deleted] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-mode-change-message-renderer,
|
||||
yt-live-chat-viewer-engagement-message-renderer,
|
||||
yt-live-chat-restricted-participation-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer a,
|
||||
yt-live-chat-membership-item-renderer a {
|
||||
text-decoration: none !important;
|
||||
}`
|
||||
|
||||
export function getImportStyle(allFonts) {
|
||||
let fontsNeedToImport = new Set()
|
||||
for (let font of allFonts) {
|
||||
if (fonts.NETWORK_FONTS.indexOf(font) !== -1) {
|
||||
fontsNeedToImport.add(font)
|
||||
}
|
||||
}
|
||||
let res = []
|
||||
for (let font of fontsNeedToImport) {
|
||||
res.push(`@import url("https://fonts.googleapis.com/css?family=${encodeURIComponent(font)}");`)
|
||||
}
|
||||
return res.join('\n')
|
||||
}
|
||||
|
||||
export function getAvatarStyle(config) {
|
||||
return `/* Avatars */
|
||||
yt-live-chat-text-message-renderer #author-photo,
|
||||
yt-live-chat-text-message-renderer #author-photo img,
|
||||
yt-live-chat-paid-message-renderer #author-photo,
|
||||
yt-live-chat-paid-message-renderer #author-photo img,
|
||||
yt-live-chat-membership-item-renderer #author-photo,
|
||||
yt-live-chat-membership-item-renderer #author-photo img {
|
||||
${config.showAvatars ? '' : 'display: none !important;'}
|
||||
width: ${config.avatarSize}px !important;
|
||||
height: ${config.avatarSize}px !important;
|
||||
border-radius: ${config.avatarSize}px !important;
|
||||
margin-right: ${config.avatarSize / 4}px !important;
|
||||
}`
|
||||
}
|
||||
|
||||
export function getTimeStyle(config) {
|
||||
return `/* Timestamps */
|
||||
yt-live-chat-text-message-renderer #timestamp {
|
||||
display: ${config.showTime ? 'inline' : 'none'} !important;
|
||||
${config.timeColor ? `color: ${config.timeColor} !important;` : ''}
|
||||
font-family: "${cssEscapeStr(config.timeFont)}"${FALLBACK_FONTS};
|
||||
font-size: ${config.timeFontSize}px !important;
|
||||
line-height: ${config.timeLineHeight || config.timeFontSize}px !important;
|
||||
}`
|
||||
}
|
||||
|
||||
export function getAnimationStyle(config) {
|
||||
if (!config.animateIn && !config.animateOut) {
|
||||
return ''
|
||||
}
|
||||
let totalTime = 0
|
||||
if (config.animateIn) {
|
||||
totalTime += config.fadeInTime
|
||||
}
|
||||
if (config.animateOut) {
|
||||
totalTime += config.animateOutWaitTime * 1000
|
||||
totalTime += config.fadeOutTime
|
||||
}
|
||||
let keyframes = []
|
||||
let curTime = 0
|
||||
if (config.animateIn) {
|
||||
keyframes.push(` 0% { opacity: 0;${!config.slide ? ''
|
||||
: ` transform: translateX(${config.reverseSlide ? 16 : -16}px);`
|
||||
} }`)
|
||||
curTime += config.fadeInTime
|
||||
keyframes.push(` ${curTime / totalTime * 100}% { opacity: 1; transform: none; }`)
|
||||
}
|
||||
if (config.animateOut) {
|
||||
curTime += config.animateOutWaitTime * 1000
|
||||
keyframes.push(` ${curTime / totalTime * 100}% { opacity: 1; transform: none; }`)
|
||||
curTime += config.fadeOutTime
|
||||
keyframes.push(` ${curTime / totalTime * 100}% { opacity: 0;${!config.slide ? ''
|
||||
: ` transform: translateX(${config.reverseSlide ? -16 : 16}px);`
|
||||
} }`)
|
||||
}
|
||||
return `/* Animation */
|
||||
@keyframes anim {
|
||||
${keyframes.join('\n')}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer,
|
||||
yt-live-chat-membership-item-renderer,
|
||||
yt-live-chat-paid-message-renderer {
|
||||
animation: anim ${totalTime}ms;
|
||||
animation-fill-mode: both;
|
||||
}`
|
||||
}
|
||||
|
||||
export function cssEscapeStr(str) {
|
||||
let res = []
|
||||
for (let char of str) {
|
||||
res.push(cssEscapeChar(char))
|
||||
}
|
||||
return res.join('')
|
||||
}
|
||||
|
||||
function cssEscapeChar(char) {
|
||||
if (!needEscapeChar(char)) {
|
||||
return char
|
||||
}
|
||||
let hexCode = char.codePointAt(0).toString(16)
|
||||
// https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
|
||||
return `\\${hexCode} `
|
||||
}
|
||||
|
||||
function needEscapeChar(char) {
|
||||
let code = char.codePointAt(0)
|
||||
if (0x20 <= code && code <= 0x7E) {
|
||||
return char === '"' || char === '\\'
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,196 +1,43 @@
|
||||
<template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<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-tabs>
|
||||
|
||||
<el-form label-width="150px" size="mini">
|
||||
<h3>{{$t('stylegen.outlines')}}</h3>
|
||||
<el-form-item :label="$t('stylegen.showOutlines')">
|
||||
<el-switch v-model="form.showOutlines"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.outlineSize')">
|
||||
<el-input v-model.number="form.outlineSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.outlineColor')">
|
||||
<el-color-picker v-model="form.outlineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
|
||||
<h3>{{$t('stylegen.avatars')}}</h3>
|
||||
<el-form-item :label="$t('stylegen.showAvatars')">
|
||||
<el-switch v-model="form.showAvatars"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.avatarSize')">
|
||||
<el-input v-model.number="form.avatarSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<h3>{{$t('stylegen.userNames')}}</h3>
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<el-autocomplete v-model="form.userNameFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.userNameFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.userNameLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.normalColor')">
|
||||
<el-color-picker v-model="form.userNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.ownerColor')">
|
||||
<el-color-picker v-model="form.ownerUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.moderatorColor')">
|
||||
<el-color-picker v-model="form.moderatorUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.memberColor')">
|
||||
<el-color-picker v-model="form.memberUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.showBadges')">
|
||||
<el-switch v-model="form.showBadges"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.showColon')">
|
||||
<el-switch v-model="form.showColon"></el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<h3>{{$t('stylegen.messages')}}</h3>
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<el-autocomplete v-model="form.messageFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.messageFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.messageLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.messageColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.onNewLine')">
|
||||
<el-switch v-model="form.messageOnNewLine"></el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<h3>{{$t('stylegen.time')}}</h3>
|
||||
<el-form-item :label="$t('stylegen.showTime')">
|
||||
<el-switch v-model="form.showTime"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.font')">
|
||||
<el-autocomplete v-model="form.timeFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.fontSize')">
|
||||
<el-input v-model.number="form.timeFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||
<el-input v-model.number="form.timeLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.timeColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
|
||||
<h3>{{$t('stylegen.backgrounds')}}</h3>
|
||||
<el-form-item :label="$t('stylegen.bgColor')">
|
||||
<el-color-picker v-model="form.bgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.useBarsInsteadOfBg')">
|
||||
<el-switch v-model="form.useBarsInsteadOfBg"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.messageBgColor')">
|
||||
<el-color-picker v-model="form.messageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.ownerMessageBgColor')">
|
||||
<el-color-picker v-model="form.ownerMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.moderatorMessageBgColor')">
|
||||
<el-color-picker v-model="form.moderatorMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.memberMessageBgColor')">
|
||||
<el-color-picker v-model="form.memberMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
|
||||
<h3>{{$t('stylegen.scAndNewMember')}}</h3>
|
||||
<el-form-item :label="$t('stylegen.firstLineFont')">
|
||||
<el-autocomplete v-model="form.firstLineFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.firstLineFontSize')">
|
||||
<el-input v-model.number="form.firstLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.firstLineLineHeight')">
|
||||
<el-input v-model.number="form.firstLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.firstLineColor')">
|
||||
<el-color-picker v-model="form.firstLineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.secondLineFont')">
|
||||
<el-autocomplete v-model="form.secondLineFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.secondLineFontSize')">
|
||||
<el-input v-model.number="form.secondLineFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.secondLineLineHeight')">
|
||||
<el-input v-model.number="form.secondLineLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.secondLineColor')">
|
||||
<el-color-picker v-model="form.secondLineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.scContentLineFont')">
|
||||
<el-autocomplete v-model="form.scContentFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.scContentLineFontSize')">
|
||||
<el-input v-model.number="form.scContentFontSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.scContentLineLineHeight')">
|
||||
<el-input v-model.number="form.scContentLineHeight" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.scContentLineColor')">
|
||||
<el-color-picker v-model="form.scContentColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.showNewMemberBg')">
|
||||
<el-switch v-model="form.showNewMemberBg"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.showScTicker')">
|
||||
<el-switch v-model="form.showScTicker"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.showOtherThings')">
|
||||
<el-switch v-model="form.showOtherThings"></el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<h3>{{$t('stylegen.animation')}}</h3>
|
||||
<el-form-item :label="$t('stylegen.animateIn')">
|
||||
<el-switch v-model="form.animateIn"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.fadeInTime')">
|
||||
<el-input v-model.number="form.fadeInTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.animateOut')">
|
||||
<el-switch v-model="form.animateOut"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.animateOutWaitTime')">
|
||||
<el-input v-model.number="form.animateOutWaitTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.fadeOutTime')">
|
||||
<el-input v-model.number="form.fadeOutTime" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.slide')">
|
||||
<el-switch v-model="form.slide"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.reverseSlide')">
|
||||
<el-switch v-model="form.reverseSlide"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="playAnimation">{{$t('stylegen.playAnimation')}}</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<h3>{{$t('stylegen.result')}}</h3>
|
||||
<el-form-item label="CSS">
|
||||
<el-input v-model="result" ref="result" type="textarea" :rows="20"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="copyResult">{{$t('stylegen.copy')}}</el-button>
|
||||
<el-button @click="resetConfig">{{$t('stylegen.resetConfig')}}</el-button>
|
||||
</el-form-item>
|
||||
<h3>{{ $t('stylegen.result') }}</h3>
|
||||
<el-card shadow="never">
|
||||
<el-form-item label="CSS">
|
||||
<el-input v-model="inputResult" ref="result" type="textarea" :rows="20"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="copyResult">{{ $t('stylegen.copy') }}</el-button>
|
||||
<el-button @click="resetConfig">{{ $t('stylegen.resetConfig') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div id="example-container">
|
||||
<div id="fakebody">
|
||||
<chat-renderer :paidMessages="examplePaidMessages" :messages="messages" :css="exampleCss"></chat-renderer>
|
||||
|
||||
<el-col :sm="24" :md="8">
|
||||
<div :style="{ position: 'relative', top: `${exampleTop}px` }">
|
||||
<el-form inline style="line-height: 40px">
|
||||
<el-form-item :label="$t('stylegen.playAnimation')" style="margin: 0">
|
||||
<el-switch v-model="playAnimation" @change="onPlayAnimationChange"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('stylegen.backgrounds')" style="margin: 0 0 0 30px">
|
||||
<el-switch v-model="exampleBgLight" :active-text="$t('stylegen.light')" :inactive-text="$t('stylegen.dark')"></el-switch>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div id="example-container" :class="{ light: exampleBgLight }">
|
||||
<div id="fakebody">
|
||||
<room ref="room"></room>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
@@ -200,166 +47,100 @@
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import stylegen from './stylegen'
|
||||
import fonts from './fonts'
|
||||
import ChatRenderer from '@/components/ChatRenderer'
|
||||
import * as constants from '@/components/ChatRenderer/constants'
|
||||
|
||||
let time = new Date()
|
||||
let textMessageTemplate = {
|
||||
id: 0,
|
||||
addTime: time,
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
authorName: '',
|
||||
authorType: constants.AUTHRO_TYPE_NORMAL,
|
||||
content: '',
|
||||
privilegeType: 0,
|
||||
repeated: 1
|
||||
}
|
||||
let legacyPaidMessageTemplate = {
|
||||
id: 0,
|
||||
addTime: time,
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
authorName: '',
|
||||
title: 'NEW MEMBER!',
|
||||
content: ''
|
||||
}
|
||||
let paidMessageTemplate = {
|
||||
id: 0,
|
||||
addTime: time,
|
||||
type: constants.MESSAGE_TYPE_GIFT,
|
||||
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
|
||||
authorName: '',
|
||||
price: 0,
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
content: ''
|
||||
}
|
||||
let nextId = 0
|
||||
const EXAMPLE_MESSAGES = [
|
||||
{
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: 'mob路人',
|
||||
content: '8888888888',
|
||||
repeated: 12
|
||||
}, {
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: 'member舰长',
|
||||
authorType: constants.AUTHRO_TYPE_MEMBER,
|
||||
content: '草',
|
||||
privilegeType: 3,
|
||||
repeated: 3
|
||||
}, {
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: 'admin房管',
|
||||
authorType: constants.AUTHRO_TYPE_ADMIN,
|
||||
content: 'kksk'
|
||||
}, {
|
||||
...legacyPaidMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: 'Paryi',
|
||||
content: 'Welcome Paryi'
|
||||
}, {
|
||||
...paidMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: '石油佬',
|
||||
price: 1245,
|
||||
content: 'Sent 小电视飞船x1'
|
||||
}, {
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: 'streamer主播',
|
||||
authorType: constants.AUTHRO_TYPE_OWNER,
|
||||
content: '感谢石油佬送的小电视'
|
||||
}, {
|
||||
...paidMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: '臭DD',
|
||||
price: 28,
|
||||
content: 'Sent 礼花x1'
|
||||
}
|
||||
]
|
||||
let examplePaidMessages = []
|
||||
for (let message of EXAMPLE_MESSAGES) {
|
||||
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
||||
examplePaidMessages.push(message)
|
||||
}
|
||||
}
|
||||
import Legacy from './Legacy'
|
||||
import LineLike from './LineLike'
|
||||
import Room from '@/views/Room'
|
||||
|
||||
export default {
|
||||
name: 'StyleGenerator',
|
||||
components: {
|
||||
ChatRenderer
|
||||
Legacy, LineLike, Room
|
||||
},
|
||||
data() {
|
||||
let config = stylegen.getLocalConfig()
|
||||
let result = stylegen.getStyle(config)
|
||||
let styleElement = document.createElement('style')
|
||||
document.head.appendChild(styleElement)
|
||||
// 数据流:
|
||||
// 输入框 --\
|
||||
// 子组件 -> subComponentResults -> subComponentResult -> inputResult -> 防抖延迟0.5s后 -> debounceResult -> exampleCss
|
||||
return {
|
||||
FONTS: [...fonts.LOCAL_FONTS, ...fonts.NETWORK_FONTS],
|
||||
form: {...config},
|
||||
result,
|
||||
exampleCss: result.replace(/^body\b/gm, '#fakebody'),
|
||||
messages: EXAMPLE_MESSAGES,
|
||||
examplePaidMessages
|
||||
// 子组件的结果
|
||||
subComponentResults: {
|
||||
legacy: '',
|
||||
lineLike: ''
|
||||
},
|
||||
activeTab: 'legacy',
|
||||
// 输入框的结果
|
||||
inputResult: '',
|
||||
// 防抖后延迟变化的结果
|
||||
debounceResult: '',
|
||||
|
||||
styleElement,
|
||||
exampleTop: 0,
|
||||
playAnimation: true,
|
||||
exampleBgLight: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedResult() {
|
||||
return stylegen.getStyle(this.form)
|
||||
// 子组件的结果
|
||||
subComponentResult() {
|
||||
return this.subComponentResults[this.activeTab]
|
||||
},
|
||||
// 应用到预览上的CSS
|
||||
exampleCss() {
|
||||
return this.debounceResult.replace(/^body\b/gm, '#fakebody')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFontSuggestions(query, callback) {
|
||||
let res = this.FONTS.map(font => {return {value: font}})
|
||||
if (query) {
|
||||
query = query.toLowerCase()
|
||||
res = res.filter(
|
||||
font => font.value.toLowerCase().indexOf(query) !== -1
|
||||
)
|
||||
}
|
||||
callback(res)
|
||||
watch: {
|
||||
subComponentResult(val) {
|
||||
this.inputResult = val
|
||||
},
|
||||
playAnimation() {
|
||||
this.messages = []
|
||||
this.$nextTick(() => this.messages = EXAMPLE_MESSAGES)
|
||||
inputResult: _.debounce(function(val) {
|
||||
this.debounceResult = val
|
||||
}, 500),
|
||||
exampleCss(val) {
|
||||
this.styleElement.innerText = val
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.debounceResult = this.inputResult = this.subComponentResult
|
||||
|
||||
this.$parent.$el.addEventListener('scroll', this.onParentScroll)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$parent.$el.removeEventListener('scroll', this.onParentScroll)
|
||||
|
||||
document.head.removeChild(this.styleElement)
|
||||
},
|
||||
methods: {
|
||||
onParentScroll(event) {
|
||||
if (document.body.clientWidth <= 992) {
|
||||
this.exampleTop = 0
|
||||
} else {
|
||||
this.exampleTop = event.target.scrollTop
|
||||
}
|
||||
},
|
||||
onPlayAnimationChange(value) {
|
||||
if (value) {
|
||||
this.$refs.room.start()
|
||||
} else {
|
||||
this.$refs.room.stop()
|
||||
}
|
||||
},
|
||||
copyResult() {
|
||||
this.$refs.result.select()
|
||||
document.execCommand('Copy')
|
||||
},
|
||||
resetConfig() {
|
||||
this.form = {...stylegen.DEFAULT_CONFIG}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
computedResult: _.debounce(function(val) {
|
||||
this.result = val
|
||||
stylegen.setLocalConfig(this.form)
|
||||
}, 500),
|
||||
result(val) {
|
||||
this.exampleCss = val.replace(/^body\b/gm, '#fakebody')
|
||||
this.$refs[this.activeTab].resetConfig()
|
||||
this.inputResult = this.subComponentResult
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-form {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
#example-container {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
width: 400px;
|
||||
height: calc(100vh - 110px);
|
||||
height: calc(100vh - 150px);
|
||||
|
||||
background-color: #444;
|
||||
background-image:
|
||||
@@ -373,17 +154,30 @@ export default {
|
||||
-webkit-gradient(linear, 0 100%, 100% 0, color-stop(.75, transparent), color-stop(.75, #333)),
|
||||
-webkit-gradient(linear, 0 0, 100% 100%, color-stop(.75, transparent), color-stop(.75, #333));
|
||||
|
||||
-moz-background-size:32px 32px;
|
||||
background-size:32px 32px;
|
||||
-webkit-background-size:32px 32px;
|
||||
-moz-background-size: 32px 32px;
|
||||
background-size: 32px 32px;
|
||||
-webkit-background-size: 32px 32px;
|
||||
|
||||
background-position:0 0, 16px 0, 16px -16px, 0px 16px;
|
||||
background-position: 0 0, 16px 0, 16px -16px, 0px 16px;
|
||||
|
||||
padding: 25px;
|
||||
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-wrapper.mobile #example-container {
|
||||
display: none;
|
||||
#example-container.light {
|
||||
background-color: #ddd;
|
||||
background-image:
|
||||
-moz-linear-gradient(45deg, #eee 25%, transparent 25%),
|
||||
-moz-linear-gradient(-45deg, #eee 25%, transparent 25%),
|
||||
-moz-linear-gradient(45deg, transparent 75%, #eee 75%),
|
||||
-moz-linear-gradient(-45deg, transparent 75%, #eee 75%);
|
||||
background-image:
|
||||
-webkit-gradient(linear, 0 100%, 100% 0, color-stop(.25, #eee), color-stop(.25, transparent)),
|
||||
-webkit-gradient(linear, 0 0, 100% 100%, color-stop(.25, #eee), color-stop(.25, transparent)),
|
||||
-webkit-gradient(linear, 0 100%, 100% 0, color-stop(.75, transparent), color-stop(.75, #eee)),
|
||||
-webkit-gradient(linear, 0 0, 100% 100%, color-stop(.75, transparent), color-stop(.75, #eee));
|
||||
}
|
||||
|
||||
#fakebody {
|
||||
|
||||
@@ -1,411 +0,0 @@
|
||||
import {mergeConfig} from '@/utils'
|
||||
import fonts from './fonts'
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
showOutlines: true,
|
||||
outlineSize: 2,
|
||||
outlineColor: '#000000',
|
||||
|
||||
showAvatars: true,
|
||||
avatarSize: 24,
|
||||
|
||||
userNameFont: 'Changa One',
|
||||
userNameFontSize: 20,
|
||||
userNameLineHeight: 0,
|
||||
userNameColor: '#cccccc',
|
||||
ownerUserNameColor: '#ffd600',
|
||||
moderatorUserNameColor: '#5e84f1',
|
||||
memberUserNameColor: '#0f9d58',
|
||||
showBadges: false,
|
||||
showColon: true,
|
||||
|
||||
messageFont: 'Imprima',
|
||||
messageFontSize: 18,
|
||||
messageLineHeight: 0,
|
||||
messageColor: '#ffffff',
|
||||
messageOnNewLine: false,
|
||||
|
||||
showTime: false,
|
||||
timeFont: 'Imprima',
|
||||
timeFontSize: 16,
|
||||
timeLineHeight: 0,
|
||||
timeColor: '#999999',
|
||||
|
||||
bgColor: 'rgba(0, 0, 0, 0)',
|
||||
useBarsInsteadOfBg: false,
|
||||
messageBgColor: 'rgba(204, 204, 204, 0)',
|
||||
ownerMessageBgColor: 'rgba(255, 214, 0, 0)',
|
||||
moderatorMessageBgColor: 'rgba(94, 132, 241, 0)',
|
||||
memberMessageBgColor: 'rgba(15, 157, 88, 0)',
|
||||
|
||||
firstLineFont: 'Changa One',
|
||||
firstLineFontSize: 20,
|
||||
firstLineLineHeight: 0,
|
||||
firstLineColor: '#ffffff',
|
||||
secondLineFont: 'Imprima',
|
||||
secondLineFontSize: 18,
|
||||
secondLineLineHeight: 0,
|
||||
secondLineColor: '#ffffff',
|
||||
scContentFont: 'Imprima',
|
||||
scContentFontSize: 18,
|
||||
scContentLineHeight: 0,
|
||||
scContentColor: '#ffffff',
|
||||
showNewMemberBg: true,
|
||||
showScTicker: false,
|
||||
showOtherThings: true,
|
||||
|
||||
animateIn: false,
|
||||
fadeInTime: 200, // ms
|
||||
animateOut: false,
|
||||
animateOutWaitTime: 30, // s
|
||||
fadeOutTime: 200, // ms
|
||||
slide: false,
|
||||
reverseSlide: false
|
||||
}
|
||||
|
||||
export function setLocalConfig (config) {
|
||||
config = mergeConfig(config, DEFAULT_CONFIG)
|
||||
window.localStorage.stylegenConfig = JSON.stringify(config)
|
||||
}
|
||||
|
||||
export function getLocalConfig () {
|
||||
if (!window.localStorage.stylegenConfig) {
|
||||
return DEFAULT_CONFIG
|
||||
}
|
||||
return mergeConfig(JSON.parse(window.localStorage.stylegenConfig), DEFAULT_CONFIG)
|
||||
}
|
||||
|
||||
export function getStyle (config) {
|
||||
config = mergeConfig(config, DEFAULT_CONFIG)
|
||||
return `${getImports(config)}
|
||||
|
||||
/* Background colors */
|
||||
body {
|
||||
overflow: hidden;
|
||||
${config.bgColor ? `background-color: ${config.bgColor};` : ''}
|
||||
}
|
||||
|
||||
/* Transparent background. */
|
||||
yt-live-chat-renderer {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
${getMessageColorStyle('', config.messageBgColor, config.useBarsInsteadOfBg)}
|
||||
|
||||
${getMessageColorStyle('owner', config.ownerMessageBgColor, config.useBarsInsteadOfBg)}
|
||||
|
||||
${getMessageColorStyle('moderator', config.moderatorMessageBgColor, config.useBarsInsteadOfBg)}
|
||||
|
||||
${getMessageColorStyle('member', config.memberMessageBgColor, config.useBarsInsteadOfBg)}
|
||||
|
||||
yt-live-chat-author-chip #author-name {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Outlines */
|
||||
yt-live-chat-renderer * {
|
||||
${getShowOutlinesStyle(config)}
|
||||
font-family: "${config.messageFont}", sans-serif;
|
||||
font-size: ${config.messageFontSize}px !important;
|
||||
line-height: ${config.messageLineHeight}px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #content,
|
||||
yt-live-chat-legacy-paid-message-renderer #content {
|
||||
overflow: initial !important;
|
||||
}
|
||||
|
||||
/* Hide scrollbar. */
|
||||
yt-live-chat-item-list-renderer #items{
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
yt-live-chat-item-list-renderer #item-scroller{
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* Hide header and input. */
|
||||
yt-live-chat-header-renderer,
|
||||
yt-live-chat-message-input-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Reduce side padding. */
|
||||
yt-live-chat-text-message-renderer,
|
||||
yt-live-chat-legacy-paid-message-renderer {
|
||||
${getPaddingStyle(config)}
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #header {
|
||||
${getPaddingStyle(config)}
|
||||
}
|
||||
|
||||
/* Avatars. */
|
||||
yt-live-chat-text-message-renderer #author-photo,
|
||||
yt-live-chat-text-message-renderer #author-photo img,
|
||||
yt-live-chat-paid-message-renderer #author-photo,
|
||||
yt-live-chat-paid-message-renderer #author-photo img,
|
||||
yt-live-chat-legacy-paid-message-renderer #author-photo,
|
||||
yt-live-chat-legacy-paid-message-renderer #author-photo img {
|
||||
${config.showAvatars ? '' : 'display: none !important;'}
|
||||
width: ${config.avatarSize}px !important;
|
||||
height: ${config.avatarSize}px !important;
|
||||
border-radius: ${config.avatarSize}px !important;
|
||||
margin-right: ${config.avatarSize / 4}px !important;
|
||||
}
|
||||
|
||||
/* Hide badges. */
|
||||
yt-live-chat-text-message-renderer #chat-badges {
|
||||
${config.showBadges ? '' : 'display: none !important;'}
|
||||
vertical-align: text-top !important;
|
||||
}
|
||||
|
||||
/* Timestamps. */
|
||||
yt-live-chat-text-message-renderer #timestamp {
|
||||
display: ${config.showTime ? 'inline' : 'none'} !important;
|
||||
${config.timeColor ? `color: ${config.timeColor} !important;` : ''}
|
||||
font-family: "${config.timeFont}", sans-serif;
|
||||
font-size: ${config.timeFontSize}px !important;
|
||||
line-height: ${config.timeLineHeight || config.timeFontSize}px !important;
|
||||
}
|
||||
|
||||
/* Badges. */
|
||||
yt-live-chat-text-message-renderer #author-name[type="owner"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
|
||||
${config.ownerUserNameColor ? `color: ${config.ownerUserNameColor} !important;` : ''}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #author-name[type="moderator"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
|
||||
${config.moderatorUserNameColor ? `color: ${config.moderatorUserNameColor} !important;` : ''}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer #author-name[type="member"],
|
||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
|
||||
${config.memberUserNameColor ? `color: ${config.memberUserNameColor} !important;` : ''}
|
||||
}
|
||||
|
||||
/* Channel names. */
|
||||
yt-live-chat-text-message-renderer #author-name {
|
||||
${config.userNameColor ? `color: ${config.userNameColor} !important;` : ''}
|
||||
font-family: "${config.userNameFont}", sans-serif;
|
||||
font-size: ${config.userNameFontSize}px !important;
|
||||
line-height: ${config.userNameLineHeight || config.userNameFontSize}px !important;
|
||||
}
|
||||
|
||||
${getShowColonStyle(config)}
|
||||
|
||||
/* Messages. */
|
||||
yt-live-chat-text-message-renderer #message,
|
||||
yt-live-chat-text-message-renderer #message * {
|
||||
${config.messageColor ? `color: ${config.messageColor} !important;` : ''}
|
||||
font-family: "${config.messageFont}", sans-serif;
|
||||
font-size: ${config.messageFontSize}px !important;
|
||||
line-height: ${config.messageLineHeight || config.messageFontSize}px !important;
|
||||
}
|
||||
|
||||
${!config.messageOnNewLine ? '' : `yt-live-chat-text-message-renderer #message {
|
||||
display: block !important;
|
||||
}`}
|
||||
|
||||
/* SuperChat/Fan Funding Messages. */
|
||||
yt-live-chat-paid-message-renderer #author-name,
|
||||
yt-live-chat-paid-message-renderer #author-name *,
|
||||
yt-live-chat-legacy-paid-message-renderer #event-text,
|
||||
yt-live-chat-legacy-paid-message-renderer #event-text * {
|
||||
${config.firstLineColor ? `color: ${config.firstLineColor} !important;` : ''}
|
||||
font-family: "${config.firstLineFont}", sans-serif;
|
||||
font-size: ${config.firstLineFontSize}px !important;
|
||||
line-height: ${config.firstLineLineHeight || config.firstLineFontSize}px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #purchase-amount,
|
||||
yt-live-chat-paid-message-renderer #purchase-amount *,
|
||||
yt-live-chat-legacy-paid-message-renderer #detail-text,
|
||||
yt-live-chat-legacy-paid-message-renderer #detail-text * {
|
||||
${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
|
||||
font-family: "${config.secondLineFont}", sans-serif;
|
||||
font-size: ${config.secondLineFontSize}px !important;
|
||||
line-height: ${config.secondLineLineHeight || config.secondLineFontSize}px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer #content,
|
||||
yt-live-chat-paid-message-renderer #content * {
|
||||
${config.scContentColor ? `color: ${config.scContentColor} !important;` : ''}
|
||||
font-family: "${config.scContentFont}", sans-serif;
|
||||
font-size: ${config.scContentFontSize}px !important;
|
||||
line-height: ${config.scContentLineHeight || config.scContentFontSize}px !important;
|
||||
}
|
||||
|
||||
yt-live-chat-paid-message-renderer {
|
||||
margin: 4px 0 !important;
|
||||
}
|
||||
|
||||
yt-live-chat-legacy-paid-message-renderer #card {
|
||||
${getShowNewMemberBgStyle(config)}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer a,
|
||||
yt-live-chat-legacy-paid-message-renderer a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer[is-deleted],
|
||||
yt-live-chat-legacy-paid-message-renderer[is-deleted] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
yt-live-chat-ticker-renderer {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
${config.showScTicker ? '' : `yt-live-chat-ticker-renderer {
|
||||
display: none !important;
|
||||
}`}
|
||||
|
||||
${config.showOtherThings ? '' : `yt-live-chat-item-list-renderer {
|
||||
display: none !important;
|
||||
}`}
|
||||
|
||||
yt-live-chat-ticker-paid-message-item-renderer,
|
||||
yt-live-chat-ticker-paid-message-item-renderer *,
|
||||
yt-live-chat-ticker-sponsor-item-renderer,
|
||||
yt-live-chat-ticker-sponsor-item-renderer * {
|
||||
${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
|
||||
font-family: "${config.secondLineFont}", sans-serif;
|
||||
}
|
||||
|
||||
yt-live-chat-mode-change-message-renderer,
|
||||
yt-live-chat-viewer-engagement-message-renderer,
|
||||
yt-live-chat-restricted-participation-renderer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
${getAnimationStyle(config)}
|
||||
`
|
||||
}
|
||||
|
||||
function getImports (config) {
|
||||
let fontsNeedToImport = new Set()
|
||||
for (let name of ['userNameFont', 'messageFont', 'timeFont', 'firstLineFont', 'secondLineFont', 'scContentFont']) {
|
||||
let font = config[name]
|
||||
if (fonts.NETWORK_FONTS.indexOf(font) !== -1) {
|
||||
fontsNeedToImport.add(font)
|
||||
}
|
||||
}
|
||||
let res = []
|
||||
for (let font of fontsNeedToImport) {
|
||||
res.push(`@import url("https://fonts.googleapis.com/css?family=${encodeURIComponent(font)}");`)
|
||||
}
|
||||
return res.join('\n')
|
||||
}
|
||||
|
||||
function getMessageColorStyle (authorType, color, useBarsInsteadOfBg) {
|
||||
let typeSelector = authorType ? `[author-type="${authorType}"]` : ''
|
||||
if (!useBarsInsteadOfBg) {
|
||||
return `yt-live-chat-text-message-renderer${typeSelector},
|
||||
yt-live-chat-text-message-renderer${typeSelector}[is-highlighted] {
|
||||
${color ? `background-color: ${color} !important;` : ''}
|
||||
}`
|
||||
} else {
|
||||
return `yt-live-chat-text-message-renderer${typeSelector}::after {
|
||||
${color ? `border: 2px solid ${color};` : ''}
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 8px;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 1px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
}`
|
||||
}
|
||||
}
|
||||
|
||||
function getShowOutlinesStyle (config) {
|
||||
if (!config.showOutlines || !config.outlineSize) {
|
||||
return ''
|
||||
}
|
||||
let shadow = []
|
||||
for (var x = -config.outlineSize; x <= config.outlineSize; x += Math.ceil(config.outlineSize / 4)) {
|
||||
for (var y = -config.outlineSize; y <= config.outlineSize; y += Math.ceil(config.outlineSize / 4)) {
|
||||
shadow.push(`${x}px ${y}px ${config.outlineColor}`)
|
||||
}
|
||||
}
|
||||
return `text-shadow: ${shadow.join(', ')};`
|
||||
}
|
||||
|
||||
function getPaddingStyle (config) {
|
||||
return `padding-left: ${config.useBarsInsteadOfBg ? 20 : 4}px !important;
|
||||
padding-right: 4px !important;`
|
||||
}
|
||||
|
||||
function getShowColonStyle (config) {
|
||||
if (!config.showColon) {
|
||||
return ''
|
||||
}
|
||||
return `yt-live-chat-text-message-renderer #author-name::after {
|
||||
content: ":";
|
||||
margin-left: ${config.outlineSize}px;
|
||||
}`
|
||||
}
|
||||
|
||||
function getShowNewMemberBgStyle (config) {
|
||||
if (config.showNewMemberBg) {
|
||||
return `background-color: ${config.memberUserNameColor} !important;
|
||||
margin: 4px 0 !important;`
|
||||
} else {
|
||||
return `background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
margin: 0 !important;`
|
||||
}
|
||||
}
|
||||
|
||||
function getAnimationStyle (config) {
|
||||
if (!config.animateIn && !config.animateOut) {
|
||||
return ''
|
||||
}
|
||||
let totalTime = 0
|
||||
if (config.animateIn) {
|
||||
totalTime += config.fadeInTime
|
||||
}
|
||||
if (config.animateOut) {
|
||||
totalTime += config.animateOutWaitTime * 1000
|
||||
totalTime += config.fadeOutTime
|
||||
}
|
||||
let keyframes = []
|
||||
let curTime = 0
|
||||
if (config.animateIn) {
|
||||
keyframes.push(` 0% { opacity: 0;${!config.slide ? ''
|
||||
: ` transform: translateX(${config.reverseSlide ? 16 : -16}px);`
|
||||
} }`)
|
||||
curTime += config.fadeInTime
|
||||
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none;}`)
|
||||
}
|
||||
if (config.animateOut) {
|
||||
curTime += config.animateOutWaitTime * 1000
|
||||
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none;}`)
|
||||
curTime += config.fadeOutTime
|
||||
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 0;${!config.slide ? ''
|
||||
: ` transform: translateX(${config.reverseSlide ? -16 : 16}px);`
|
||||
} }`)
|
||||
}
|
||||
return `@keyframes anim {
|
||||
${keyframes.join('\n')}
|
||||
}
|
||||
|
||||
yt-live-chat-text-message-renderer,
|
||||
yt-live-chat-legacy-paid-message-renderer {
|
||||
animation: anim ${totalTime}ms;
|
||||
animation-fill-mode: both;
|
||||
}`
|
||||
}
|
||||
|
||||
export default {
|
||||
DEFAULT_CONFIG,
|
||||
setLocalConfig,
|
||||
getLocalConfig,
|
||||
getStyle
|
||||
}
|
||||
27
frontend/vue.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// 不能用localhost,https://forum.dfinity.org/t/development-workflow-quickly-test-code-modifications/1793/21
|
||||
const API_BASE_URL = 'http://127.0.0.1:12450'
|
||||
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: API_BASE_URL,
|
||||
ws: true
|
||||
},
|
||||
'/emoticons': {
|
||||
target: API_BASE_URL
|
||||
}
|
||||
}
|
||||
},
|
||||
chainWebpack: config => {
|
||||
const APP_VERSION = `v${process.env.npm_package_version}`
|
||||
|
||||
config.plugin('define')
|
||||
.tap(args => {
|
||||
let defineMap = args[0]
|
||||
let env = defineMap['process.env']
|
||||
env['APP_VERSION'] = JSON.stringify(APP_VERSION)
|
||||
return args
|
||||
})
|
||||
}
|
||||
}
|
||||
0
log/.gitkeep
Normal file
112
main.py
@@ -1,54 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import webbrowser
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
|
||||
import views.chat
|
||||
import views.config
|
||||
import views.main
|
||||
import api.chat
|
||||
import api.main
|
||||
import config
|
||||
import models.database
|
||||
import services.avatar
|
||||
import services.chat
|
||||
import services.translate
|
||||
import update
|
||||
import utils.request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WEB_ROOT = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
|
||||
routes = [
|
||||
(r'/api/server_info', api.main.ServerInfoHandler),
|
||||
(r'/api/emoticon', api.main.UploadEmoticonHandler),
|
||||
|
||||
(r'/api/chat', api.chat.ChatHandler),
|
||||
(r'/api/room_info', api.chat.RoomInfoHandler),
|
||||
(r'/api/avatar_url', api.chat.AvatarHandler),
|
||||
|
||||
(rf'{api.main.EMOTICON_BASE_URL}/(.*)', tornado.web.StaticFileHandler, {'path': api.main.EMOTICON_UPLOAD_PATH}),
|
||||
(r'/(.*)', api.main.MainHandler, {'path': config.WEB_ROOT})
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='用于OBS的仿YouTube风格的bilibili直播聊天层')
|
||||
parser.add_argument('--host', help='服务器host,默认为127.0.0.1', default='127.0.0.1')
|
||||
parser.add_argument('--port', help='服务器端口,默认为12450', type=int, default=12450)
|
||||
parser.add_argument('--debug', help='调试模式', action='store_true')
|
||||
args = parser.parse_args()
|
||||
args = parse_args()
|
||||
|
||||
init_logging(args.debug)
|
||||
config.init()
|
||||
|
||||
utils.request.init()
|
||||
models.database.init(args.debug)
|
||||
|
||||
services.avatar.init()
|
||||
services.translate.init()
|
||||
services.chat.init()
|
||||
|
||||
update.check_update()
|
||||
|
||||
run_server(args.host, args.port, args.debug)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='用于OBS的仿YouTube风格的bilibili直播评论栏')
|
||||
parser.add_argument('--host', help='服务器host,默认和配置中的一样', default=None)
|
||||
parser.add_argument('--port', help='服务器端口,默认和配置中的一样', type=int, default=None)
|
||||
parser.add_argument('--debug', help='调试模式', action='store_true')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def init_logging(debug):
|
||||
filename = os.path.join(config.BASE_PATH, 'log', 'blivechat.log')
|
||||
stream_handler = logging.StreamHandler()
|
||||
file_handler = logging.handlers.TimedRotatingFileHandler(
|
||||
filename, encoding='utf-8', when='midnight', backupCount=7, delay=True
|
||||
)
|
||||
logging.basicConfig(
|
||||
format='{asctime} {levelname} [{name}]: {message}',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
style='{',
|
||||
level=logging.INFO if not args.debug else logging.DEBUG
|
||||
level=logging.INFO if not debug else logging.DEBUG,
|
||||
handlers=[stream_handler, file_handler]
|
||||
)
|
||||
|
||||
# 屏蔽访问日志
|
||||
logging.getLogger('tornado.access').setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def run_server(host, port, debug):
|
||||
cfg = config.get_config()
|
||||
if host is None:
|
||||
host = cfg.host
|
||||
if port is None:
|
||||
port = cfg.port
|
||||
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(r'/chat', views.chat.ChatHandler),
|
||||
(r'/config', views.config.ConfigsHandler),
|
||||
(r'/config/(.+)', views.config.ConfigHandler),
|
||||
|
||||
(r'/((css|fonts|img|js|static)/.*)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
|
||||
(r'/(favicon\.ico)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
|
||||
(r'/.*', views.main.MainHandler, {'path': WEB_ROOT})
|
||||
],
|
||||
websocket_ping_interval=30,
|
||||
debug=args.debug,
|
||||
routes,
|
||||
websocket_ping_interval=10,
|
||||
debug=debug,
|
||||
autoreload=False
|
||||
)
|
||||
app.listen(args.port, args.host)
|
||||
logger.info('服务器启动:%s:%d', args.host, args.port)
|
||||
url = 'http://localhost' if args.port == 80 else f'http://localhost:{args.port}'
|
||||
webbrowser.open(url)
|
||||
try:
|
||||
app.listen(
|
||||
port,
|
||||
host,
|
||||
xheaders=cfg.tornado_xheaders,
|
||||
max_body_size=1024 * 1024,
|
||||
max_buffer_size=1024 * 1024
|
||||
)
|
||||
except OSError:
|
||||
logger.warning('Address is used %s:%d', host, port)
|
||||
return
|
||||
finally:
|
||||
if cfg.open_browser_at_startup:
|
||||
url = 'http://localhost/' if port == 80 else f'http://localhost:{port}/'
|
||||
webbrowser.open(url)
|
||||
logger.info('Server started: %s:%d', host, port)
|
||||
tornado.ioloop.IOLoop.current().start()
|
||||
|
||||
|
||||
|
||||
14
models/bilibili.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
import models.database
|
||||
|
||||
|
||||
class BilibiliUser(models.database.OrmBase):
|
||||
__tablename__ = 'bilibili_users'
|
||||
uid: Mapped[int] = mapped_column(sqlalchemy.BigInteger, primary_key=True) # 创建表后最好手动改成unsigned
|
||||
avatar_url: Mapped[str] = mapped_column(sqlalchemy.String(100))
|
||||
update_time: Mapped[datetime.datetime]
|
||||
32
models/database.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import *
|
||||
|
||||
import sqlalchemy.orm
|
||||
|
||||
import config
|
||||
|
||||
_engine: Optional[sqlalchemy.Engine] = None
|
||||
|
||||
|
||||
class OrmBase(sqlalchemy.orm.DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
def init(_debug):
|
||||
cfg = config.get_config()
|
||||
global _engine
|
||||
_engine = sqlalchemy.create_engine(
|
||||
cfg.database_url,
|
||||
pool_size=5, # 保持的连接数
|
||||
max_overflow=5, # 临时的额外连接数
|
||||
pool_timeout=3, # 连接数达到最大时获取新连接的超时时间
|
||||
# pool_pre_ping=True, # 获取连接时先检测是否可用
|
||||
pool_recycle=60 * 60, # 回收超过1小时的连接,防止数据库服务器主动断开不活跃的连接
|
||||
# echo=debug, # 输出SQL语句
|
||||
)
|
||||
|
||||
OrmBase.metadata.create_all(_engine)
|
||||
|
||||
|
||||
def get_session() -> sqlalchemy.orm.Session:
|
||||
return sqlalchemy.orm.Session(_engine)
|
||||
@@ -1,2 +1,5 @@
|
||||
aiohttp==3.5.4
|
||||
tornado==6.0.2
|
||||
-r blivedm/requirements.txt
|
||||
cachetools==5.3.1
|
||||
pycryptodome==3.10.1
|
||||
sqlalchemy==2.0.19
|
||||
tornado==6.3.2
|
||||
|
||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 299 KiB |
552
services/avatar.py
Normal file
@@ -0,0 +1,552 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
import urllib.parse
|
||||
from typing import *
|
||||
|
||||
import aiohttp
|
||||
import cachetools
|
||||
import sqlalchemy.exc
|
||||
|
||||
import config
|
||||
import models.bilibili as bl_models
|
||||
import models.database
|
||||
import utils.request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_AVATAR_URL = '//static.hdslb.com/images/member/noface.gif'
|
||||
|
||||
_avatar_fetchers: List['AvatarFetcher'] = []
|
||||
# user_id -> avatar_url
|
||||
_avatar_url_cache: Optional[cachetools.TTLCache] = None
|
||||
# 正在获取头像的Future,user_id -> Future
|
||||
_uid_fetch_future_map: Dict[int, asyncio.Future] = {}
|
||||
# 正在获取头像的任务队列
|
||||
_task_queue: Optional['asyncio.Queue[FetchTask]'] = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FetchTask:
|
||||
user_id: int
|
||||
future: 'asyncio.Future[Optional[str]]'
|
||||
|
||||
|
||||
def init():
|
||||
cfg = config.get_config()
|
||||
global _avatar_url_cache, _task_queue
|
||||
_avatar_url_cache = cachetools.TTLCache(cfg.avatar_cache_size, 10 * 60)
|
||||
_task_queue = asyncio.Queue(cfg.fetch_avatar_max_queue_size)
|
||||
asyncio.get_event_loop().create_task(_do_init())
|
||||
|
||||
|
||||
async def _do_init():
|
||||
fetchers = [
|
||||
UserSpaceAvatarFetcher(5.5),
|
||||
MedalAnchorAvatarFetcher(3),
|
||||
UserCardAvatarFetcher(3),
|
||||
GameUserCenterAvatarFetcher(3),
|
||||
]
|
||||
await asyncio.gather(*(fetcher.init() for fetcher in fetchers))
|
||||
global _avatar_fetchers
|
||||
_avatar_fetchers = fetchers
|
||||
|
||||
|
||||
async def get_avatar_url(user_id) -> str:
|
||||
avatar_url = await get_avatar_url_or_none(user_id)
|
||||
if avatar_url is None:
|
||||
avatar_url = DEFAULT_AVATAR_URL
|
||||
return avatar_url
|
||||
|
||||
|
||||
async def get_avatar_url_or_none(user_id) -> Optional[str]:
|
||||
if user_id == 0:
|
||||
return None
|
||||
|
||||
# 查内存
|
||||
avatar_url = _get_avatar_url_from_memory(user_id)
|
||||
if avatar_url is not None:
|
||||
return avatar_url
|
||||
|
||||
# 查数据库
|
||||
user = await _get_avatar_url_from_database(user_id)
|
||||
if user is not None:
|
||||
avatar_url = user.avatar_url
|
||||
_update_avatar_cache_in_memory(user_id, avatar_url)
|
||||
# 如果距离数据库上次更新太久,则在后台从接口获取,并更新所有缓存
|
||||
if (datetime.datetime.now() - user.update_time).days >= 1:
|
||||
asyncio.create_task(_refresh_avatar_cache_from_web(user_id))
|
||||
return avatar_url
|
||||
|
||||
# 从接口获取
|
||||
avatar_url = await _get_avatar_url_from_web(user_id)
|
||||
if avatar_url is not None:
|
||||
update_avatar_cache(user_id, avatar_url)
|
||||
return avatar_url
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def _refresh_avatar_cache_from_web(user_id):
|
||||
avatar_url = await _get_avatar_url_from_web(user_id)
|
||||
if avatar_url is None:
|
||||
return
|
||||
update_avatar_cache(user_id, avatar_url)
|
||||
|
||||
|
||||
def update_avatar_cache(user_id, avatar_url):
|
||||
_update_avatar_cache_in_memory(user_id, avatar_url)
|
||||
_update_avatar_cache_in_database(user_id, avatar_url)
|
||||
|
||||
|
||||
def update_avatar_cache_if_expired(user_id, avatar_url):
|
||||
# 内存缓存过期了才更新,减少写入数据库的频率
|
||||
if _get_avatar_url_from_memory(user_id) is None:
|
||||
update_avatar_cache(user_id, avatar_url)
|
||||
|
||||
|
||||
def process_avatar_url(avatar_url):
|
||||
# 去掉协议,兼容HTTP、HTTPS
|
||||
m = re.fullmatch(r'(?:https?:)?(.*)', avatar_url)
|
||||
if m is not None:
|
||||
avatar_url = m[1]
|
||||
# 缩小图片加快传输
|
||||
if not avatar_url.endswith('noface.gif'):
|
||||
avatar_url += '@48w_48h'
|
||||
return avatar_url
|
||||
|
||||
|
||||
def _get_avatar_url_from_memory(user_id) -> Optional[str]:
|
||||
return _avatar_url_cache.get(user_id, None)
|
||||
|
||||
|
||||
def _update_avatar_cache_in_memory(user_id, avatar_url):
|
||||
_avatar_url_cache[user_id] = avatar_url
|
||||
|
||||
|
||||
def _get_avatar_url_from_database(user_id) -> Awaitable[Optional[bl_models.BilibiliUser]]:
|
||||
loop = asyncio.get_running_loop()
|
||||
return loop.run_in_executor(None, _do_get_avatar_url_from_database, user_id)
|
||||
|
||||
|
||||
def _do_get_avatar_url_from_database(user_id) -> Optional[bl_models.BilibiliUser]:
|
||||
try:
|
||||
with models.database.get_session() as session:
|
||||
user: bl_models.BilibiliUser = session.scalars(
|
||||
sqlalchemy.select(bl_models.BilibiliUser).filter(
|
||||
bl_models.BilibiliUser.uid == user_id
|
||||
)
|
||||
).one_or_none()
|
||||
if user is None:
|
||||
return None
|
||||
return user
|
||||
except sqlalchemy.exc.OperationalError:
|
||||
# SQLite会锁整个文件,忽略就行
|
||||
return None
|
||||
except sqlalchemy.exc.SQLAlchemyError:
|
||||
logger.exception('_do_get_avatar_url_from_database failed:')
|
||||
return None
|
||||
|
||||
|
||||
def _update_avatar_cache_in_database(user_id, avatar_url) -> Awaitable[None]:
|
||||
return asyncio.get_running_loop().run_in_executor(
|
||||
None, _do_update_avatar_cache_in_database, user_id, avatar_url
|
||||
)
|
||||
|
||||
|
||||
def _do_update_avatar_cache_in_database(user_id, avatar_url):
|
||||
try:
|
||||
with models.database.get_session() as session:
|
||||
user = session.scalars(
|
||||
sqlalchemy.select(bl_models.BilibiliUser).filter(
|
||||
bl_models.BilibiliUser.uid == user_id
|
||||
)
|
||||
).one_or_none()
|
||||
if user is None:
|
||||
user = bl_models.BilibiliUser(
|
||||
uid=user_id
|
||||
)
|
||||
session.add(user)
|
||||
user.avatar_url = avatar_url
|
||||
user.update_time = datetime.datetime.now()
|
||||
session.commit()
|
||||
except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.IntegrityError):
|
||||
# SQLite会锁整个文件,忽略就行。另外还有多线程导致ID重复的问题,这里对一致性要求不高就没加for update
|
||||
pass
|
||||
except sqlalchemy.exc.SQLAlchemyError:
|
||||
logger.exception('_do_update_avatar_cache_in_database failed:')
|
||||
|
||||
|
||||
def _get_avatar_url_from_web(user_id) -> Awaitable[Optional[str]]:
|
||||
# 如果已有正在获取的future则返回,防止重复获取同一个uid
|
||||
future = _uid_fetch_future_map.get(user_id, None)
|
||||
if future is not None:
|
||||
return future
|
||||
# 否则创建一个获取任务
|
||||
future = asyncio.get_running_loop().create_future()
|
||||
|
||||
task = FetchTask(
|
||||
user_id=user_id,
|
||||
future=future
|
||||
)
|
||||
if not _push_task(task):
|
||||
future.set_result(None)
|
||||
return future
|
||||
|
||||
_uid_fetch_future_map[user_id] = future
|
||||
future.add_done_callback(lambda _future: _uid_fetch_future_map.pop(user_id, None))
|
||||
return future
|
||||
|
||||
|
||||
def _push_task(task: FetchTask):
|
||||
if not _has_available_avatar_fetcher():
|
||||
return False
|
||||
|
||||
try:
|
||||
_task_queue.put_nowait(task)
|
||||
return True
|
||||
except asyncio.QueueFull:
|
||||
return False
|
||||
|
||||
|
||||
def _pop_task() -> Awaitable[FetchTask]:
|
||||
return _task_queue.get()
|
||||
|
||||
|
||||
def _cancel_all_tasks_if_no_available_avatar_fetcher():
|
||||
if _has_available_avatar_fetcher():
|
||||
return
|
||||
|
||||
logger.warning('No available avatar fetcher')
|
||||
while not _task_queue.empty():
|
||||
task = _task_queue.get_nowait()
|
||||
task.future.set_result(None)
|
||||
|
||||
|
||||
def _has_available_avatar_fetcher():
|
||||
return any(fetcher.is_available for fetcher in _avatar_fetchers)
|
||||
|
||||
|
||||
class AvatarFetcher:
|
||||
def __init__(self, query_interval):
|
||||
self._query_interval = query_interval
|
||||
self._be_available_event = asyncio.Event()
|
||||
self._be_available_event.set()
|
||||
|
||||
self._cool_down_timer_handle = None
|
||||
|
||||
async def init(self):
|
||||
asyncio.create_task(self._fetch_consumer())
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
return self._cool_down_timer_handle is None
|
||||
|
||||
def _on_availability_change(self):
|
||||
if self.is_available:
|
||||
self._be_available_event.set()
|
||||
else:
|
||||
self._be_available_event.clear()
|
||||
_cancel_all_tasks_if_no_available_avatar_fetcher()
|
||||
|
||||
async def _fetch_consumer(self):
|
||||
cls_name = type(self).__name__
|
||||
while True:
|
||||
try:
|
||||
if not self.is_available:
|
||||
logger.info('%s waiting to become available', cls_name)
|
||||
await self._be_available_event.wait()
|
||||
logger.info('%s became available', cls_name)
|
||||
|
||||
task = await _pop_task()
|
||||
# 为了简化代码,约定只会在_fetch_wrapper里变成不可用,所以获取task之后这里还是可用的
|
||||
assert self.is_available
|
||||
|
||||
start_time = datetime.datetime.now()
|
||||
await self._fetch_wrapper(task)
|
||||
cost_time = (datetime.datetime.now() - start_time).total_seconds()
|
||||
|
||||
# 限制频率,防止被B站ban
|
||||
await asyncio.sleep(self._query_interval - cost_time)
|
||||
except Exception: # noqa
|
||||
logger.exception('%s error:', cls_name)
|
||||
|
||||
async def _fetch_wrapper(self, task: FetchTask) -> Optional[str]:
|
||||
try:
|
||||
avatar_url = await self._do_fetch(task.user_id)
|
||||
except BaseException as e:
|
||||
task.future.set_exception(e)
|
||||
return None
|
||||
|
||||
task.future.set_result(avatar_url)
|
||||
return avatar_url
|
||||
|
||||
async def _do_fetch(self, user_id) -> Optional[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
def _cool_down(self, sleep_time):
|
||||
if self._cool_down_timer_handle is not None:
|
||||
return
|
||||
|
||||
self._cool_down_timer_handle = asyncio.get_running_loop().call_later(
|
||||
sleep_time, self._on_cool_down_timeout
|
||||
)
|
||||
self._on_availability_change()
|
||||
|
||||
def _on_cool_down_timeout(self):
|
||||
self._cool_down_timer_handle = None
|
||||
self._on_availability_change()
|
||||
|
||||
|
||||
class UserSpaceAvatarFetcher(AvatarFetcher):
|
||||
# wbi密码表
|
||||
WBI_KEY_INDEX_TABLE = [
|
||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35,
|
||||
27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13
|
||||
]
|
||||
|
||||
def __init__(self, query_interval):
|
||||
super().__init__(query_interval)
|
||||
|
||||
# wbi鉴权口令
|
||||
self._wbi_key = ''
|
||||
|
||||
async def _do_fetch(self, user_id) -> Optional[str]:
|
||||
if self._wbi_key == '':
|
||||
await self._refresh_wbi_key()
|
||||
if self._wbi_key == '':
|
||||
return None
|
||||
|
||||
try:
|
||||
async with utils.request.http_session.get(
|
||||
'https://api.bilibili.com/x/space/wbi/acc/info',
|
||||
headers={
|
||||
**utils.request.BILIBILI_COMMON_HEADERS,
|
||||
'Origin': 'https://space.bilibili.com',
|
||||
'Referer': f'https://space.bilibili.com/{user_id}/'
|
||||
},
|
||||
params=self._add_wbi_sign({'mid': user_id}),
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning(
|
||||
'UserSpaceAvatarFetcher failed to fetch avatar: status=%d %s uid=%d',
|
||||
r.status, r.reason, user_id
|
||||
)
|
||||
if r.status == 412:
|
||||
# 被B站ban了
|
||||
self._cool_down(3 * 60)
|
||||
await self._refresh_wbi_key()
|
||||
return None
|
||||
data = await r.json()
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
return None
|
||||
|
||||
code = data['code']
|
||||
if code != 0:
|
||||
logger.info(
|
||||
'UserSpaceAvatarFetcher failed to fetch avatar: code=%d %s uid=%d',
|
||||
code, data['message'], user_id
|
||||
)
|
||||
if code == -401:
|
||||
# 被B站ban了
|
||||
self._cool_down(3 * 60)
|
||||
await self._refresh_wbi_key()
|
||||
elif code == -403:
|
||||
# 签名错误
|
||||
self._wbi_key = ''
|
||||
await self._refresh_wbi_key()
|
||||
return None
|
||||
|
||||
return process_avatar_url(data['data']['face'])
|
||||
|
||||
async def _refresh_wbi_key(self):
|
||||
wbi_key = await self._get_wbi_key()
|
||||
if wbi_key != '':
|
||||
self._wbi_key = wbi_key
|
||||
|
||||
async def _get_wbi_key(self):
|
||||
try:
|
||||
async with utils.request.http_session.get(
|
||||
'https://api.bilibili.com/nav',
|
||||
headers=utils.request.BILIBILI_COMMON_HEADERS,
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning('UserSpaceAvatarFetcher failed to get wbi key: status=%d %s', r.status, r.reason)
|
||||
return ''
|
||||
data = await r.json()
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
logger.exception('UserSpaceAvatarFetcher failed to get wbi key:')
|
||||
return ''
|
||||
|
||||
try:
|
||||
wbi_img = data['data']['wbi_img']
|
||||
img_key = wbi_img['img_url'].rpartition('/')[2].partition('.')[0]
|
||||
sub_key = wbi_img['sub_url'].rpartition('/')[2].partition('.')[0]
|
||||
except KeyError:
|
||||
logger.warning('UserSpaceAvatarFetcher failed to get wbi key: data=%s', data)
|
||||
return ''
|
||||
|
||||
shuffled_key = img_key + sub_key
|
||||
wbi_key = []
|
||||
for index in self.WBI_KEY_INDEX_TABLE:
|
||||
if index < len(shuffled_key):
|
||||
wbi_key.append(shuffled_key[index])
|
||||
return ''.join(wbi_key)
|
||||
|
||||
def _add_wbi_sign(self, params: dict):
|
||||
if self._wbi_key == '':
|
||||
return params
|
||||
|
||||
wts = str(int(datetime.datetime.now().timestamp()))
|
||||
params_to_sign = {**params, 'wts': wts}
|
||||
|
||||
# 按key字典序排序
|
||||
params_to_sign = {
|
||||
key: params_to_sign[key]
|
||||
for key in sorted(params_to_sign.keys())
|
||||
}
|
||||
# 过滤一些字符
|
||||
for key, value in params_to_sign.items():
|
||||
value = ''.join(
|
||||
ch
|
||||
for ch in str(value)
|
||||
if ch not in "!'()*"
|
||||
)
|
||||
params_to_sign[key] = value
|
||||
|
||||
str_to_sign = urllib.parse.urlencode(params_to_sign) + self._wbi_key
|
||||
w_rid = hashlib.md5(str_to_sign.encode('utf-8')).hexdigest()
|
||||
return {
|
||||
**params,
|
||||
'wts': wts,
|
||||
'w_rid': w_rid
|
||||
}
|
||||
|
||||
|
||||
class MedalAnchorAvatarFetcher(AvatarFetcher):
|
||||
async def _do_fetch(self, user_id) -> Optional[str]:
|
||||
try:
|
||||
async with utils.request.http_session.get(
|
||||
'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuMedalAnchorInfo',
|
||||
headers={
|
||||
**utils.request.BILIBILI_COMMON_HEADERS,
|
||||
'Origin': 'https://live.bilibili.com',
|
||||
'Referer': 'https://live.bilibili.com/'
|
||||
},
|
||||
params={
|
||||
'ruid': user_id,
|
||||
'token': '',
|
||||
'platform': 'web',
|
||||
'jsonp': 'jsonp'
|
||||
}
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning(
|
||||
'MedalAnchorAvatarFetcher failed to fetch avatar: status=%d %s uid=%d',
|
||||
r.status, r.reason, user_id
|
||||
)
|
||||
if r.status == 412:
|
||||
# 被B站ban了
|
||||
self._cool_down(3 * 60)
|
||||
return None
|
||||
data = await r.json()
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
return None
|
||||
|
||||
code = data['code']
|
||||
if code != 0:
|
||||
# 这里虽然失败但不会被ban一段时间
|
||||
logger.info(
|
||||
'MedalAnchorAvatarFetcher failed to fetch avatar: code=%d %s uid=%d',
|
||||
code, data['message'], user_id
|
||||
)
|
||||
return None
|
||||
|
||||
return process_avatar_url(data['data']['rface'])
|
||||
|
||||
|
||||
class UserCardAvatarFetcher(AvatarFetcher):
|
||||
async def _do_fetch(self, user_id) -> Optional[str]:
|
||||
try:
|
||||
async with utils.request.http_session.get(
|
||||
'https://api.bilibili.com/x/web-interface/card',
|
||||
headers={
|
||||
**utils.request.BILIBILI_COMMON_HEADERS,
|
||||
'Origin': 'https://t.bilibili.com',
|
||||
'Referer': 'https://t.bilibili.com/'
|
||||
},
|
||||
params={
|
||||
'mid': user_id,
|
||||
'photo': 'true'
|
||||
}
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning(
|
||||
'UserCardAvatarFetcher failed to fetch avatar: status=%d %s uid=%d',
|
||||
r.status, r.reason, user_id
|
||||
)
|
||||
if r.status == 412:
|
||||
# 被B站ban了
|
||||
self._cool_down(3 * 60)
|
||||
return None
|
||||
data = await r.json()
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
return None
|
||||
|
||||
code = data['code']
|
||||
if code != 0:
|
||||
# 这里虽然失败但不会被ban一段时间
|
||||
logger.info(
|
||||
'UserCardAvatarFetcher failed to fetch avatar: code=%d %s uid=%d',
|
||||
code, data['message'], user_id
|
||||
)
|
||||
return None
|
||||
|
||||
return process_avatar_url(data['data']['card']['face'])
|
||||
|
||||
|
||||
class GameUserCenterAvatarFetcher(AvatarFetcher):
|
||||
async def _do_fetch(self, user_id) -> Optional[str]:
|
||||
try:
|
||||
async with utils.request.http_session.get(
|
||||
'https://line3-h5-mobile-api.biligame.com/game/center/h5/user/space/info',
|
||||
headers={
|
||||
**utils.request.BILIBILI_COMMON_HEADERS,
|
||||
'Origin': 'https://app.biligame.com',
|
||||
'Referer': 'https://app.biligame.com/'
|
||||
},
|
||||
params={
|
||||
'uid': user_id,
|
||||
'sdk_type': '1'
|
||||
}
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
# 这个接口经常502
|
||||
logger.info(
|
||||
'GameUserCenterAvatarFetcher failed to fetch avatar: status=%d %s uid=%d',
|
||||
r.status, r.reason, user_id
|
||||
)
|
||||
if r.status == 412:
|
||||
# 被B站ban了
|
||||
self._cool_down(3 * 60)
|
||||
return None
|
||||
data = await r.json()
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
return None
|
||||
|
||||
code = data['code']
|
||||
if code != 0:
|
||||
# 这里虽然失败但不会被ban一段时间
|
||||
logger.info(
|
||||
'GameUserCenterAvatarFetcher failed to fetch avatar: code=%d %s uid=%d',
|
||||
code, data['message'], user_id
|
||||
)
|
||||
return None
|
||||
|
||||
return process_avatar_url(data['data']['face'])
|
||||
499
services/chat.py
Normal file
@@ -0,0 +1,499 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import *
|
||||
|
||||
import api.chat
|
||||
import blivedm.blivedm as blivedm
|
||||
import blivedm.blivedm.models.pb as blivedm_pb
|
||||
import config
|
||||
import services.avatar
|
||||
import services.translate
|
||||
import utils.request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 到B站的连接管理
|
||||
_live_client_manager: Optional['LiveClientManager'] = None
|
||||
# 到客户端的连接管理
|
||||
client_room_manager: Optional['ClientRoomManager'] = None
|
||||
# 直播消息处理器
|
||||
_live_msg_handler: Optional['LiveMsgHandler'] = None
|
||||
|
||||
|
||||
def init():
|
||||
global _live_client_manager, client_room_manager, _live_msg_handler
|
||||
_live_client_manager = LiveClientManager()
|
||||
client_room_manager = ClientRoomManager()
|
||||
_live_msg_handler = LiveMsgHandler()
|
||||
|
||||
|
||||
class LiveClientManager:
|
||||
"""管理到B站的连接"""
|
||||
def __init__(self):
|
||||
self._live_clients: Dict[int, LiveClient] = {}
|
||||
|
||||
def add_live_client(self, room_id):
|
||||
if room_id in self._live_clients:
|
||||
return
|
||||
logger.info('room=%d creating live client', room_id)
|
||||
self._live_clients[room_id] = live_client = LiveClient(room_id)
|
||||
live_client.add_handler(_live_msg_handler)
|
||||
asyncio.create_task(self._init_live_client(live_client))
|
||||
logger.info('room=%d live client created, %d live clients', room_id, len(self._live_clients))
|
||||
|
||||
async def _init_live_client(self, live_client: 'LiveClient'):
|
||||
if not await live_client.init_room():
|
||||
logger.warning('room=%d live client init failed', live_client.tmp_room_id)
|
||||
self.del_live_client(live_client.tmp_room_id)
|
||||
return
|
||||
logger.info('room=%d (%d) live client init succeeded', live_client.tmp_room_id, live_client.room_id)
|
||||
live_client.start()
|
||||
|
||||
def del_live_client(self, room_id):
|
||||
live_client = self._live_clients.pop(room_id, None)
|
||||
if live_client is None:
|
||||
return
|
||||
logger.info('room=%d removing live client', room_id)
|
||||
live_client.remove_handler(_live_msg_handler)
|
||||
asyncio.create_task(live_client.stop_and_close())
|
||||
logger.info('room=%d live client removed, %d live clients', room_id, len(self._live_clients))
|
||||
|
||||
client_room_manager.del_room(room_id)
|
||||
|
||||
|
||||
class LiveClient(blivedm.BLiveClient):
|
||||
HEARTBEAT_INTERVAL = 10
|
||||
|
||||
def __init__(self, room_id):
|
||||
super().__init__(room_id, session=utils.request.http_session, heartbeat_interval=self.HEARTBEAT_INTERVAL)
|
||||
|
||||
@property
|
||||
def tmp_room_id(self):
|
||||
"""初始化参数传入的房间ID,room_id可能改变,这个不会变"""
|
||||
return self._tmp_room_id
|
||||
|
||||
async def init_room(self):
|
||||
await super().init_room()
|
||||
return True
|
||||
|
||||
|
||||
class ClientRoomManager:
|
||||
"""管理到客户端的连接"""
|
||||
# 房间没有客户端后延迟多久删除房间,不立即删除防止短时间后重连
|
||||
DELAY_DEL_ROOM_TIMEOUT = 10
|
||||
|
||||
def __init__(self):
|
||||
self._rooms: Dict[int, ClientRoom] = {}
|
||||
# room_id -> timer_handle
|
||||
self._delay_del_timer_handles: Dict[int, asyncio.TimerHandle] = {}
|
||||
|
||||
def add_client(self, room_id, client: 'api.chat.ChatHandler'):
|
||||
room = self._get_or_add_room(room_id)
|
||||
room.add_client(client)
|
||||
|
||||
self._clear_delay_del_timer(room_id)
|
||||
|
||||
def del_client(self, room_id, client: 'api.chat.ChatHandler'):
|
||||
room = self.get_room(room_id)
|
||||
if room is None:
|
||||
return
|
||||
room.del_client(client)
|
||||
|
||||
if room.client_count == 0:
|
||||
self.delay_del_room(room_id, self.DELAY_DEL_ROOM_TIMEOUT)
|
||||
|
||||
def get_room(self, room_id):
|
||||
return self._rooms.get(room_id, None)
|
||||
|
||||
def _get_or_add_room(self, room_id):
|
||||
room = self._rooms.get(room_id, None)
|
||||
if room is None:
|
||||
logger.info('room=%d creating client room', room_id)
|
||||
self._rooms[room_id] = room = ClientRoom(room_id)
|
||||
logger.info('room=%d client room created, %d client rooms', room_id, len(self._rooms))
|
||||
|
||||
_live_client_manager.add_live_client(room_id)
|
||||
return room
|
||||
|
||||
def del_room(self, room_id):
|
||||
self._clear_delay_del_timer(room_id)
|
||||
|
||||
room = self._rooms.pop(room_id, None)
|
||||
if room is None:
|
||||
return
|
||||
logger.info('room=%d removing client room', room_id)
|
||||
room.clear_clients()
|
||||
logger.info('room=%d client room removed, %d client rooms', room_id, len(self._rooms))
|
||||
|
||||
_live_client_manager.del_live_client(room_id)
|
||||
|
||||
def delay_del_room(self, room_id, timeout):
|
||||
self._clear_delay_del_timer(room_id)
|
||||
self._delay_del_timer_handles[room_id] = asyncio.get_running_loop().call_later(
|
||||
timeout, self._on_delay_del_room, room_id
|
||||
)
|
||||
|
||||
def _clear_delay_del_timer(self, room_id):
|
||||
timer_handle = self._delay_del_timer_handles.pop(room_id, None)
|
||||
if timer_handle is not None:
|
||||
timer_handle.cancel()
|
||||
|
||||
def _on_delay_del_room(self, room_id):
|
||||
self._delay_del_timer_handles.pop(room_id, None)
|
||||
self.del_room(room_id)
|
||||
|
||||
|
||||
class ClientRoom:
|
||||
def __init__(self, room_id):
|
||||
self._room_id = room_id
|
||||
self._clients: List[api.chat.ChatHandler] = []
|
||||
self._auto_translate_count = 0
|
||||
|
||||
@property
|
||||
def room_id(self):
|
||||
return self._room_id
|
||||
|
||||
@property
|
||||
def client_count(self):
|
||||
return len(self._clients)
|
||||
|
||||
@property
|
||||
def need_translate(self):
|
||||
return self._auto_translate_count > 0
|
||||
|
||||
def add_client(self, client: 'api.chat.ChatHandler'):
|
||||
logger.info('room=%d addding client %s', self._room_id, client.request.remote_ip)
|
||||
self._clients.append(client)
|
||||
if client.auto_translate:
|
||||
self._auto_translate_count += 1
|
||||
logger.info('room=%d added client %s, %d clients', self._room_id, client.request.remote_ip,
|
||||
self.client_count)
|
||||
|
||||
def del_client(self, client: 'api.chat.ChatHandler'):
|
||||
client.close()
|
||||
try:
|
||||
self._clients.remove(client)
|
||||
except ValueError:
|
||||
return
|
||||
if client.auto_translate:
|
||||
self._auto_translate_count -= 1
|
||||
logger.info('room=%d removed client %s, %d clients', self._room_id, client.request.remote_ip,
|
||||
self.client_count)
|
||||
|
||||
def clear_clients(self):
|
||||
logger.info('room=%d clearing %d clients', self._room_id, self.client_count)
|
||||
for client in self._clients:
|
||||
client.close()
|
||||
self._clients.clear()
|
||||
self._auto_translate_count = 0
|
||||
|
||||
def send_cmd_data(self, cmd, data):
|
||||
body = api.chat.make_message_body(cmd, data)
|
||||
for client in self._clients:
|
||||
client.send_body_no_raise(body)
|
||||
|
||||
def send_cmd_data_if(self, filterer: Callable[['api.chat.ChatHandler'], bool], cmd, data):
|
||||
body = api.chat.make_message_body(cmd, data)
|
||||
for client in filter(filterer, self._clients):
|
||||
client.send_body_no_raise(body)
|
||||
|
||||
|
||||
class LiveMsgHandler(blivedm.BaseHandler):
|
||||
# 重新定义XXX_callback是为了减少对字段名的依赖,防止B站改字段名
|
||||
def __danmu_msg_callback(self, client: LiveClient, command: dict):
|
||||
info = command['info']
|
||||
dm_v2 = command.get('dm_v2', '')
|
||||
|
||||
proto: Optional[blivedm_pb.SimpleDm] = None
|
||||
if dm_v2 != '':
|
||||
try:
|
||||
proto = blivedm_pb.SimpleDm.loads(base64.b64decode(dm_v2))
|
||||
except (binascii.Error, KeyError, TypeError, ValueError):
|
||||
pass
|
||||
if proto is not None:
|
||||
face = proto.user.face
|
||||
else:
|
||||
face = ''
|
||||
|
||||
if len(info[3]) != 0:
|
||||
medal_level = info[3][0]
|
||||
medal_room_id = info[3][3]
|
||||
else:
|
||||
medal_level = 0
|
||||
medal_room_id = 0
|
||||
|
||||
message = blivedm.DanmakuMessage(
|
||||
timestamp=info[0][4],
|
||||
msg_type=info[0][9],
|
||||
dm_type=info[0][12],
|
||||
emoticon_options=info[0][13],
|
||||
mode_info=info[0][15],
|
||||
|
||||
msg=info[1],
|
||||
|
||||
uid=info[2][0],
|
||||
uname=info[2][1],
|
||||
face=face,
|
||||
admin=info[2][2],
|
||||
urank=info[2][5],
|
||||
mobile_verify=info[2][6],
|
||||
|
||||
medal_level=medal_level,
|
||||
medal_room_id=medal_room_id,
|
||||
|
||||
user_level=info[4][0],
|
||||
|
||||
privilege_type=info[7],
|
||||
)
|
||||
return self._on_danmaku(client, message)
|
||||
|
||||
def __send_gift_callback(self, client: LiveClient, command: dict):
|
||||
data = command['data']
|
||||
message = blivedm.GiftMessage(
|
||||
gift_name=data['giftName'],
|
||||
num=data['num'],
|
||||
uname=data['uname'],
|
||||
face=data['face'],
|
||||
uid=data['uid'],
|
||||
timestamp=data['timestamp'],
|
||||
coin_type=data['coin_type'],
|
||||
total_coin=data['total_coin'],
|
||||
)
|
||||
return self._on_gift(client, message)
|
||||
|
||||
def __guard_buy_callback(self, client: LiveClient, command: dict):
|
||||
data = command['data']
|
||||
message = blivedm.GuardBuyMessage(
|
||||
uid=data['uid'],
|
||||
username=data['username'],
|
||||
guard_level=data['guard_level'],
|
||||
start_time=data['start_time'],
|
||||
)
|
||||
return self._on_buy_guard(client, message)
|
||||
|
||||
def __super_chat_message_callback(self, client: LiveClient, command: dict):
|
||||
data = command['data']
|
||||
message = blivedm.SuperChatMessage(
|
||||
price=data['price'],
|
||||
message=data['message'],
|
||||
start_time=data['start_time'],
|
||||
id=data['id'],
|
||||
uid=data['uid'],
|
||||
uname=data['user_info']['uname'],
|
||||
face=data['user_info']['face'],
|
||||
)
|
||||
return self._on_super_chat(client, message)
|
||||
|
||||
_CMD_CALLBACK_DICT = {
|
||||
**blivedm.BaseHandler._CMD_CALLBACK_DICT,
|
||||
'DANMU_MSG': __danmu_msg_callback,
|
||||
'SEND_GIFT': __send_gift_callback,
|
||||
'GUARD_BUY': __guard_buy_callback,
|
||||
'SUPER_CHAT_MESSAGE': __super_chat_message_callback
|
||||
}
|
||||
|
||||
async def _on_danmaku(self, client: LiveClient, message: blivedm.DanmakuMessage):
|
||||
asyncio.create_task(self.__on_danmaku(client, message))
|
||||
|
||||
async def __on_danmaku(self, client: LiveClient, message: blivedm.DanmakuMessage):
|
||||
avatar_url = message.face
|
||||
if avatar_url != '':
|
||||
services.avatar.update_avatar_cache_if_expired(message.uid, avatar_url)
|
||||
else:
|
||||
# 先异步调用再获取房间,因为返回时房间可能已经不存在了
|
||||
avatar_url = await services.avatar.get_avatar_url(message.uid)
|
||||
|
||||
room = client_room_manager.get_room(client.tmp_room_id)
|
||||
if room is None:
|
||||
return
|
||||
|
||||
if message.uid == client.room_owner_uid:
|
||||
author_type = 3 # 主播
|
||||
elif message.admin:
|
||||
author_type = 2 # 房管
|
||||
elif message.privilege_type != 0: # 1总督,2提督,3舰长
|
||||
author_type = 1 # 舰队
|
||||
else:
|
||||
author_type = 0
|
||||
|
||||
if message.dm_type == 1:
|
||||
content_type = api.chat.ContentType.EMOTICON
|
||||
content_type_params = api.chat.make_emoticon_params(
|
||||
message.emoticon_options_dict['url'],
|
||||
)
|
||||
else:
|
||||
content_type = api.chat.ContentType.TEXT
|
||||
content_type_params = None
|
||||
|
||||
text_emoticons = self._parse_text_emoticons(message)
|
||||
|
||||
need_translate = content_type != api.chat.ContentType.EMOTICON and self._need_translate(message.msg, room)
|
||||
if need_translate:
|
||||
translation = services.translate.get_translation_from_cache(message.msg)
|
||||
if translation is None:
|
||||
# 没有缓存,需要后面异步翻译后通知
|
||||
translation = ''
|
||||
else:
|
||||
need_translate = False
|
||||
else:
|
||||
translation = ''
|
||||
|
||||
msg_id = uuid.uuid4().hex
|
||||
room.send_cmd_data(api.chat.Command.ADD_TEXT, api.chat.make_text_message_data(
|
||||
avatar_url=avatar_url,
|
||||
timestamp=int(message.timestamp / 1000),
|
||||
author_name=message.uname,
|
||||
author_type=author_type,
|
||||
content=message.msg,
|
||||
privilege_type=message.privilege_type,
|
||||
is_gift_danmaku=bool(message.msg_type),
|
||||
author_level=message.user_level,
|
||||
is_newbie=message.urank < 10000,
|
||||
is_mobile_verified=bool(message.mobile_verify),
|
||||
medal_level=0 if message.medal_room_id != client.room_id else message.medal_level,
|
||||
id_=msg_id,
|
||||
translation=translation,
|
||||
content_type=content_type,
|
||||
content_type_params=content_type_params,
|
||||
text_emoticons=text_emoticons,
|
||||
))
|
||||
|
||||
if need_translate:
|
||||
await self._translate_and_response(message.msg, room.room_id, msg_id)
|
||||
|
||||
@staticmethod
|
||||
def _parse_text_emoticons(message: blivedm.DanmakuMessage):
|
||||
try:
|
||||
extra = json.loads(message.mode_info['extra'])
|
||||
# {"[dog]":{"emoticon_id":208,"emoji":"[dog]","descript":"[dog]","url":"http://i0.hdslb.com/bfs/live/4428c8
|
||||
# 4e694fbf4e0ef6c06e958d9352c3582740.png","width":20,"height":20,"emoticon_unique":"emoji_208","count":1}}
|
||||
emoticons = extra['emots']
|
||||
if emoticons is None:
|
||||
return []
|
||||
res = [
|
||||
(emoticon['descript'], emoticon['url'])
|
||||
for emoticon in emoticons.values()
|
||||
]
|
||||
return res
|
||||
except (json.JSONDecodeError, TypeError, KeyError):
|
||||
return []
|
||||
|
||||
async def _on_gift(self, client: LiveClient, message: blivedm.GiftMessage):
|
||||
avatar_url = services.avatar.process_avatar_url(message.face)
|
||||
services.avatar.update_avatar_cache_if_expired(message.uid, avatar_url)
|
||||
|
||||
# 丢人
|
||||
if message.coin_type != 'gold':
|
||||
return
|
||||
|
||||
room = client_room_manager.get_room(client.tmp_room_id)
|
||||
if room is None:
|
||||
return
|
||||
|
||||
room.send_cmd_data(api.chat.Command.ADD_GIFT, {
|
||||
'id': uuid.uuid4().hex,
|
||||
'avatarUrl': avatar_url,
|
||||
'timestamp': message.timestamp,
|
||||
'authorName': message.uname,
|
||||
'totalCoin': message.total_coin,
|
||||
'giftName': message.gift_name,
|
||||
'num': message.num
|
||||
})
|
||||
|
||||
async def _on_buy_guard(self, client: LiveClient, message: blivedm.GuardBuyMessage):
|
||||
asyncio.create_task(self.__on_buy_guard(client, message))
|
||||
|
||||
@staticmethod
|
||||
async def __on_buy_guard(client: LiveClient, message: blivedm.GuardBuyMessage):
|
||||
# 先异步调用再获取房间,因为返回时房间可能已经不存在了
|
||||
avatar_url = await services.avatar.get_avatar_url(message.uid)
|
||||
|
||||
room = client_room_manager.get_room(client.tmp_room_id)
|
||||
if room is None:
|
||||
return
|
||||
|
||||
room.send_cmd_data(api.chat.Command.ADD_MEMBER, {
|
||||
'id': uuid.uuid4().hex,
|
||||
'avatarUrl': avatar_url,
|
||||
'timestamp': message.start_time,
|
||||
'authorName': message.username,
|
||||
'privilegeType': message.guard_level
|
||||
})
|
||||
|
||||
async def _on_super_chat(self, client: LiveClient, message: blivedm.SuperChatMessage):
|
||||
avatar_url = services.avatar.process_avatar_url(message.face)
|
||||
services.avatar.update_avatar_cache_if_expired(message.uid, avatar_url)
|
||||
|
||||
room = client_room_manager.get_room(client.tmp_room_id)
|
||||
if room is None:
|
||||
return
|
||||
|
||||
need_translate = self._need_translate(message.message, room)
|
||||
if need_translate:
|
||||
translation = services.translate.get_translation_from_cache(message.message)
|
||||
if translation is None:
|
||||
# 没有缓存,需要后面异步翻译后通知
|
||||
translation = ''
|
||||
else:
|
||||
need_translate = False
|
||||
else:
|
||||
translation = ''
|
||||
|
||||
msg_id = str(message.id)
|
||||
room.send_cmd_data(api.chat.Command.ADD_SUPER_CHAT, {
|
||||
'id': msg_id,
|
||||
'avatarUrl': avatar_url,
|
||||
'timestamp': message.start_time,
|
||||
'authorName': message.uname,
|
||||
'price': message.price,
|
||||
'content': message.message,
|
||||
'translation': translation
|
||||
})
|
||||
|
||||
if need_translate:
|
||||
asyncio.create_task(self._translate_and_response(
|
||||
message.message, room.room_id, msg_id, services.translate.Priority.HIGH
|
||||
))
|
||||
|
||||
async def _on_super_chat_delete(self, client: LiveClient, message: blivedm.SuperChatDeleteMessage):
|
||||
room = client_room_manager.get_room(client.tmp_room_id)
|
||||
if room is None:
|
||||
return
|
||||
|
||||
room.send_cmd_data(api.chat.Command.DEL_SUPER_CHAT, {
|
||||
'ids': list(map(str, message.ids))
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def _need_translate(text, room: ClientRoom):
|
||||
cfg = config.get_config()
|
||||
return (
|
||||
cfg.enable_translate
|
||||
and room.need_translate
|
||||
and (not cfg.allow_translate_rooms or room.room_id in cfg.allow_translate_rooms)
|
||||
and services.translate.need_translate(text)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _translate_and_response(text, room_id, msg_id, priority=services.translate.Priority.NORMAL):
|
||||
translation = await services.translate.translate(text, priority)
|
||||
if translation is None:
|
||||
return
|
||||
|
||||
room = client_room_manager.get_room(room_id)
|
||||
if room is None:
|
||||
return
|
||||
|
||||
room.send_cmd_data_if(
|
||||
lambda client: client.auto_translate,
|
||||
api.chat.Command.UPDATE_TRANSLATION,
|
||||
api.chat.make_translation_message_data(
|
||||
msg_id,
|
||||
translation
|
||||
)
|
||||
)
|
||||
681
services/translate.py
Normal file
@@ -0,0 +1,681 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import base64
|
||||
import dataclasses
|
||||
import datetime
|
||||
import enum
|
||||
import functools
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
from typing import *
|
||||
|
||||
import Crypto.Cipher.AES as cry_aes # noqa
|
||||
import Crypto.Util.Padding as cry_pad # noqa
|
||||
import aiohttp
|
||||
import cachetools
|
||||
|
||||
import config
|
||||
import utils.request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
NO_TRANSLATE_TEXTS = {
|
||||
'草', '草草', '草草草', '草生', '大草原', '上手', '上手上手', '理解', '理解理解', '天才', '天才天才',
|
||||
'强', '余裕', '余裕余裕', '大丈夫', '再放送', '放送事故', '清楚', '清楚清楚'
|
||||
}
|
||||
|
||||
_translate_providers: List['TranslateProvider'] = []
|
||||
# text -> res
|
||||
_translate_cache: Optional[cachetools.LRUCache] = None
|
||||
# 正在翻译的Future,text -> Future
|
||||
_text_future_map: Dict[str, asyncio.Future] = {}
|
||||
# 正在翻译的任务队列,索引是优先级
|
||||
_task_queues: List['asyncio.Queue[TranslateTask]'] = []
|
||||
|
||||
|
||||
class Priority(enum.IntEnum):
|
||||
HIGH = 0
|
||||
NORMAL = 1
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TranslateTask:
|
||||
priority: Priority
|
||||
text: str
|
||||
future: 'asyncio.Future[Optional[str]]'
|
||||
remain_retry_count: int
|
||||
|
||||
|
||||
def init():
|
||||
cfg = config.get_config()
|
||||
global _translate_cache, _task_queues
|
||||
_translate_cache = cachetools.LRUCache(cfg.translation_cache_size)
|
||||
# 总队列长度会超过translate_max_queue_size,不用这么严格
|
||||
_task_queues = [asyncio.Queue(cfg.translate_max_queue_size) for _ in range(len(Priority))]
|
||||
asyncio.get_event_loop().create_task(_do_init())
|
||||
|
||||
|
||||
async def _do_init():
|
||||
cfg = config.get_config()
|
||||
if not cfg.enable_translate:
|
||||
return
|
||||
providers = []
|
||||
for trans_cfg in cfg.translator_configs:
|
||||
provider = create_translate_provider(trans_cfg)
|
||||
if provider is not None:
|
||||
providers.append(provider)
|
||||
await asyncio.gather(*(provider.init() for provider in providers))
|
||||
global _translate_providers
|
||||
_translate_providers = providers
|
||||
|
||||
|
||||
def create_translate_provider(cfg):
|
||||
type_ = cfg['type']
|
||||
if type_ == 'TencentTranslateFree':
|
||||
return TencentTranslateFree(
|
||||
cfg['query_interval'], cfg['source_language'], cfg['target_language']
|
||||
)
|
||||
elif type_ == 'TencentTranslate':
|
||||
return TencentTranslate(
|
||||
cfg['query_interval'], cfg['source_language'], cfg['target_language'],
|
||||
cfg['secret_id'], cfg['secret_key'], cfg['region']
|
||||
)
|
||||
elif type_ == 'BaiduTranslate':
|
||||
return BaiduTranslate(
|
||||
cfg['query_interval'], cfg['source_language'], cfg['target_language'],
|
||||
cfg['app_id'], cfg['secret']
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def need_translate(text):
|
||||
text = text.strip()
|
||||
# 没有中文,平时打不出的字不管
|
||||
if not any(0x4E00 <= ord(c) <= 0x9FFF for c in text):
|
||||
return False
|
||||
# 含有日文假名
|
||||
if any(0x3040 <= ord(c) <= 0x30FF for c in text):
|
||||
return False
|
||||
# 弹幕同传
|
||||
if '【' in text:
|
||||
return False
|
||||
# 中日双语
|
||||
if text in NO_TRANSLATE_TEXTS:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_translation_from_cache(text):
|
||||
key = text.strip().lower()
|
||||
return _translate_cache.get(key, None)
|
||||
|
||||
|
||||
def translate(text, priority=Priority.NORMAL) -> Awaitable[Optional[str]]:
|
||||
key = text.strip().lower()
|
||||
# 如果已有正在翻译的future则返回,防止重复翻译
|
||||
future = _text_future_map.get(key, None)
|
||||
if future is not None:
|
||||
return future
|
||||
# 否则创建一个翻译任务
|
||||
future = asyncio.get_running_loop().create_future()
|
||||
|
||||
# 查缓存
|
||||
res = _translate_cache.get(key, None)
|
||||
if res is not None:
|
||||
future.set_result(res)
|
||||
return future
|
||||
|
||||
task = TranslateTask(
|
||||
priority=priority,
|
||||
text=text,
|
||||
future=future,
|
||||
remain_retry_count=3 if priority == Priority.HIGH else 1
|
||||
)
|
||||
if not _push_task(task):
|
||||
future.set_result(None)
|
||||
return future
|
||||
|
||||
_text_future_map[key] = future
|
||||
future.add_done_callback(functools.partial(_on_translate_done, key))
|
||||
return future
|
||||
|
||||
|
||||
def _on_translate_done(key, future):
|
||||
_text_future_map.pop(key, None)
|
||||
# 缓存
|
||||
try:
|
||||
res = future.result()
|
||||
except Exception: # noqa
|
||||
return
|
||||
if res is None:
|
||||
return
|
||||
_translate_cache[key] = res
|
||||
|
||||
|
||||
def _push_task(task: TranslateTask):
|
||||
if not _has_available_translate_provider():
|
||||
return False
|
||||
|
||||
queue = _task_queues[task.priority]
|
||||
if not queue.full():
|
||||
queue.put_nowait(task)
|
||||
return True
|
||||
|
||||
if task.priority != Priority.HIGH:
|
||||
return False
|
||||
|
||||
# 高优先级的尝试降级,挤掉低优先级的任务
|
||||
queue = _task_queues[Priority.NORMAL]
|
||||
if queue.full():
|
||||
lower_task = queue.get_nowait()
|
||||
lower_task.future.set_result(None)
|
||||
queue.put_nowait(task)
|
||||
return True
|
||||
|
||||
|
||||
async def _pop_task() -> TranslateTask:
|
||||
# 按优先级遍历,看是否已经有任务
|
||||
for queue in _task_queues:
|
||||
if not queue.empty():
|
||||
return queue.get_nowait()
|
||||
|
||||
done_future_set, pending_future_set = await asyncio.wait(
|
||||
[asyncio.create_task(queue.get()) for queue in _task_queues],
|
||||
return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
for future in pending_future_set:
|
||||
future.cancel()
|
||||
|
||||
# 如果有多个队列都取到任务了,只返回优先级最高的那一个,剩下的放回队列
|
||||
assert len(done_future_set) != 0
|
||||
tasks = [await future for future in done_future_set]
|
||||
if len(tasks) > 1:
|
||||
tasks.sort(key=lambda task_: task_.priority)
|
||||
|
||||
res = None
|
||||
for task in tasks:
|
||||
if res is None:
|
||||
res = task
|
||||
continue
|
||||
|
||||
if not _push_task(task):
|
||||
task.future.set_result(None)
|
||||
return res
|
||||
|
||||
|
||||
def _cancel_all_tasks_if_no_available_translate_provider():
|
||||
if _has_available_translate_provider():
|
||||
return
|
||||
|
||||
logger.warning('No available translate provider')
|
||||
for queue in _task_queues:
|
||||
while not queue.empty():
|
||||
task = queue.get_nowait()
|
||||
task.future.set_result(None)
|
||||
|
||||
|
||||
def _has_available_translate_provider():
|
||||
return any(provider.is_available for provider in _translate_providers)
|
||||
|
||||
|
||||
class TranslateProvider:
|
||||
def __init__(self, query_interval):
|
||||
self._query_interval = query_interval
|
||||
self._be_available_event = asyncio.Event()
|
||||
self._be_available_event.set()
|
||||
|
||||
async def init(self):
|
||||
asyncio.create_task(self._translate_consumer())
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
return True
|
||||
|
||||
def _on_availability_change(self):
|
||||
if self.is_available:
|
||||
self._be_available_event.set()
|
||||
else:
|
||||
self._be_available_event.clear()
|
||||
_cancel_all_tasks_if_no_available_translate_provider()
|
||||
|
||||
async def _translate_consumer(self):
|
||||
cls_name = type(self).__name__
|
||||
while True:
|
||||
try:
|
||||
if not self.is_available:
|
||||
logger.info('%s waiting to become available', cls_name)
|
||||
await self._be_available_event.wait()
|
||||
logger.info('%s became available', cls_name)
|
||||
|
||||
task = await _pop_task()
|
||||
# 为了简化代码,约定只会在_translate_wrapper里变成不可用,所以获取task之后这里还是可用的
|
||||
assert self.is_available
|
||||
|
||||
start_time = datetime.datetime.now()
|
||||
await self._translate_wrapper(task)
|
||||
cost_time = (datetime.datetime.now() - start_time).total_seconds()
|
||||
|
||||
# 频率限制
|
||||
await asyncio.sleep(self._query_interval - cost_time)
|
||||
except Exception: # noqa
|
||||
logger.exception('%s error:', cls_name)
|
||||
|
||||
async def _translate_wrapper(self, task: TranslateTask) -> Optional[str]:
|
||||
try:
|
||||
exc = None
|
||||
task.remain_retry_count -= 1
|
||||
res = await self._do_translate(task.text)
|
||||
except BaseException as e:
|
||||
exc = e
|
||||
res = None
|
||||
if res is not None:
|
||||
task.future.set_result(res)
|
||||
return res
|
||||
|
||||
if task.remain_retry_count > 0:
|
||||
# 还可以重试则放回队列
|
||||
if not _push_task(task):
|
||||
task.future.set_result(None)
|
||||
else:
|
||||
# 否则设置异常或None结果
|
||||
if exc is not None:
|
||||
task.future.set_exception(exc)
|
||||
else:
|
||||
task.future.set_result(None)
|
||||
return None
|
||||
|
||||
async def _do_translate(self, text) -> Optional[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TencentTranslateFree(TranslateProvider):
|
||||
def __init__(self, query_interval, source_language, target_language):
|
||||
super().__init__(query_interval)
|
||||
self._be_available_event.clear() # _do_init之后才可用
|
||||
self._source_language = source_language
|
||||
self._target_language = target_language
|
||||
|
||||
self._server_time_delta = 0
|
||||
self._uc_key = self._uc_iv = ''
|
||||
self._qtv = self._qtk = ''
|
||||
self._reinit_future = None
|
||||
|
||||
# 连续失败的次数
|
||||
self._fail_count = 0
|
||||
|
||||
async def init(self):
|
||||
if not await super().init():
|
||||
return False
|
||||
self._reinit_future = asyncio.create_task(self._reinit_coroutine())
|
||||
return True
|
||||
|
||||
async def _do_init(self):
|
||||
try:
|
||||
async with utils.request.http_session.get('https://fanyi.qq.com/') as r:
|
||||
if r.status != 200:
|
||||
logger.warning('TencentTranslateFree init request failed: status=%d %s', r.status, r.reason)
|
||||
return False
|
||||
html = await r.text()
|
||||
|
||||
try:
|
||||
server_time = r.headers['Date']
|
||||
server_time = datetime.datetime.strptime(server_time, '%a, %d %b %Y %H:%M:%S GMT')
|
||||
server_time = server_time.replace(tzinfo=datetime.timezone.utc).timestamp()
|
||||
self._server_time_delta = int((datetime.datetime.now().timestamp() - server_time) * 1000)
|
||||
except (KeyError, ValueError):
|
||||
self._server_time_delta = 0
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
||||
logger.exception('TencentTranslateFree init error:')
|
||||
return False
|
||||
|
||||
# 获取token URL
|
||||
m = re.search(r"""\breauthuri\s*=\s*['"](.+?)['"]""", html)
|
||||
if m is None:
|
||||
logger.exception('TencentTranslateFree init failed: reauthuri not found')
|
||||
return False
|
||||
reauthuri = m[1]
|
||||
|
||||
# 获取验证用的key、iv
|
||||
m = re.search(r"""\s*=\s*['"]((?:\w+\|\w+-)+\w+\|\w+)['"]""", html)
|
||||
if m is None:
|
||||
logger.exception('TencentTranslateFree init failed: initial global variables not found')
|
||||
return False
|
||||
uc_key = None
|
||||
uc_iv = None
|
||||
for item in m[1].split('-'):
|
||||
key, _, value = item.partition('|')
|
||||
if key == 'a137':
|
||||
uc_key = value
|
||||
elif key == 'E74':
|
||||
uc_iv = value
|
||||
if uc_key is not None and uc_iv is not None:
|
||||
break
|
||||
|
||||
# 获取token
|
||||
try:
|
||||
async with utils.request.http_session.post('https://fanyi.qq.com/api/' + reauthuri) as r:
|
||||
if r.status != 200:
|
||||
logger.warning('TencentTranslateFree init request failed: reauthuri=%s, status=%d %s',
|
||||
reauthuri, r.status, r.reason)
|
||||
return False
|
||||
data = await r.json()
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
||||
logger.exception('TencentTranslateFree init error:')
|
||||
return False
|
||||
|
||||
qtv = data.get('qtv', None)
|
||||
if qtv is None:
|
||||
logger.warning('TencentTranslateFree init failed: qtv not found')
|
||||
return False
|
||||
qtk = data.get('qtk', None)
|
||||
if qtk is None:
|
||||
logger.warning('TencentTranslateFree init failed: qtk not found')
|
||||
return False
|
||||
|
||||
self._uc_key = uc_key
|
||||
self._uc_iv = uc_iv
|
||||
self._qtv = qtv
|
||||
self._qtk = qtk
|
||||
|
||||
self._on_availability_change()
|
||||
return True
|
||||
|
||||
async def _reinit_coroutine(self):
|
||||
try:
|
||||
while True:
|
||||
logger.debug('TencentTranslateFree reinit')
|
||||
start_time = datetime.datetime.now()
|
||||
try:
|
||||
await self._do_init()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except BaseException: # noqa
|
||||
pass
|
||||
cost_time = (datetime.datetime.now() - start_time).total_seconds()
|
||||
|
||||
await asyncio.sleep(30 - cost_time)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
return '' not in (self._uc_key, self._uc_iv, self._qtv, self._qtk) and super().is_available
|
||||
|
||||
async def _translate_wrapper(self, task: TranslateTask) -> Optional[str]:
|
||||
res = await super()._translate_wrapper(task)
|
||||
if res is not None:
|
||||
self._fail_count = 0
|
||||
else:
|
||||
self._on_fail()
|
||||
return res
|
||||
|
||||
async def _do_translate(self, text) -> Optional[str]:
|
||||
try:
|
||||
async with utils.request.http_session.post(
|
||||
'https://fanyi.qq.com/api/translate',
|
||||
headers={
|
||||
'Referer': 'https://fanyi.qq.com/',
|
||||
'uc': self._get_uc()
|
||||
},
|
||||
data={
|
||||
'source': self._source_language,
|
||||
'target': self._target_language,
|
||||
'sourceText': text,
|
||||
'qtv': self._qtv,
|
||||
'qtk': self._qtk
|
||||
}
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning('TencentTranslateFree request failed: status=%d %s', r.status, r.reason)
|
||||
return None
|
||||
self._update_uc_key(r)
|
||||
data = await r.json()
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
||||
return None
|
||||
if data['errCode'] != 0:
|
||||
logger.warning('TencentTranslateFree failed: %d %s', data['errCode'], data['errMsg'])
|
||||
return None
|
||||
res = ''.join(record['targetText'] for record in data['translate']['records'])
|
||||
if res == '' and text.strip() != '':
|
||||
# qtv、qtk过期
|
||||
logger.info('TencentTranslateFree result is empty %s', data)
|
||||
return None
|
||||
return res
|
||||
|
||||
def _get_uc(self):
|
||||
user_actions = self._gen_user_actions()
|
||||
cur_timestamp = str(int(datetime.datetime.now().timestamp() * 1000))
|
||||
server_time_delta = str(self._server_time_delta)
|
||||
uc = '|'.join([user_actions, cur_timestamp, server_time_delta])
|
||||
|
||||
aes = cry_aes.new(self._uc_key.encode('utf-8'), cry_aes.MODE_CBC, self._uc_iv.encode('utf-8'))
|
||||
uc = cry_pad.pad(uc.encode('utf-8'), aes.block_size, 'pkcs7')
|
||||
uc = aes.encrypt(uc)
|
||||
uc = base64.b64encode(uc).decode('utf-8')
|
||||
return uc
|
||||
|
||||
@staticmethod
|
||||
def _gen_user_actions():
|
||||
# 1:点击翻译;2:源输入框聚焦或失去焦点;3:点击源语言列表;4:点击交换语言;5:点击目标语言列表;6:源输入框输入、粘贴
|
||||
user_actions = []
|
||||
if random.randint(1, 5) == 1:
|
||||
for i in range(random.randint(1, 2)):
|
||||
user_actions.append('2')
|
||||
user_actions.append('6')
|
||||
for i in range(random.randint(0, 6)):
|
||||
user_actions.append(random.choice('26'))
|
||||
if random.randint(1, 5) == 1:
|
||||
user_actions.append('1')
|
||||
return ''.join(user_actions)
|
||||
|
||||
def _update_uc_key(self, r):
|
||||
try:
|
||||
hf_f = r.headers['f']
|
||||
hf_ts = int(r.headers['ts'])
|
||||
except (KeyError, ValueError):
|
||||
return
|
||||
|
||||
cur_timestamp = int(datetime.datetime.now().timestamp() * 1000)
|
||||
hf_f = base64.b64decode(hf_f.encode('utf-8')).decode('utf-8')
|
||||
pos = int(hf_f[72: 72 + 4])
|
||||
uc_key = hf_f[pos: pos + 16]
|
||||
uc_iv = hf_f[pos + 16: pos + 16 + 16]
|
||||
|
||||
self._server_time_delta = cur_timestamp - hf_ts
|
||||
self._uc_key = uc_key
|
||||
self._uc_iv = uc_iv
|
||||
|
||||
def _on_fail(self):
|
||||
self._fail_count += 1
|
||||
# 为了可靠性,连续失败5次时冷却直到下次重新init
|
||||
if self._fail_count >= 5:
|
||||
self._cool_down()
|
||||
|
||||
def _cool_down(self):
|
||||
logger.info('TencentTranslateFree is cooling down')
|
||||
# 下次_do_init后恢复
|
||||
self._uc_key = self._uc_iv = ''
|
||||
self._qtv = self._qtk = ''
|
||||
self._fail_count = 0
|
||||
|
||||
self._on_availability_change()
|
||||
|
||||
|
||||
class TencentTranslate(TranslateProvider):
|
||||
def __init__(self, query_interval, source_language, target_language,
|
||||
secret_id, secret_key, region):
|
||||
super().__init__(query_interval)
|
||||
self._source_language = source_language
|
||||
self._target_language = target_language
|
||||
self._secret_id = secret_id
|
||||
self._secret_key = secret_key
|
||||
self._region = region
|
||||
|
||||
self._cool_down_timer_handle = None
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
return self._cool_down_timer_handle is None and super().is_available
|
||||
|
||||
async def _do_translate(self, text) -> Optional[str]:
|
||||
try:
|
||||
async with self._request_tencent_cloud(
|
||||
'TextTranslate',
|
||||
'2018-03-21',
|
||||
{
|
||||
'SourceText': text,
|
||||
'Source': self._source_language,
|
||||
'Target': self._target_language,
|
||||
'ProjectId': 0
|
||||
}
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning('TencentTranslate request failed: status=%d %s', r.status, r.reason)
|
||||
return None
|
||||
data = (await r.json())['Response']
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
return None
|
||||
error = data.get('Error', None)
|
||||
if error is not None:
|
||||
logger.warning('TencentTranslate failed: %s %s, RequestId=%s', error['Code'],
|
||||
error['Message'], data['RequestId'])
|
||||
self._on_fail(error['Code'])
|
||||
return None
|
||||
return data['TargetText']
|
||||
|
||||
def _request_tencent_cloud(self, action, version, body):
|
||||
body_bytes = json.dumps(body).encode('utf-8')
|
||||
|
||||
canonical_headers = 'content-type:application/json; charset=utf-8\nhost:tmt.tencentcloudapi.com\n'
|
||||
signed_headers = 'content-type;host'
|
||||
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')
|
||||
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}'
|
||||
|
||||
def sign(key, msg):
|
||||
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
|
||||
|
||||
secret_date = sign(('TC3' + self._secret_key).encode('utf-8'), date)
|
||||
secret_service = sign(secret_date, 'tmt')
|
||||
secret_signing = sign(secret_service, 'tc3_request')
|
||||
signature = hmac.new(secret_signing, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (
|
||||
f'TC3-HMAC-SHA256 Credential={self._secret_id}/{credential_scope}, '
|
||||
f'SignedHeaders={signed_headers}, Signature={signature}'
|
||||
)
|
||||
|
||||
headers = {
|
||||
'Authorization': authorization,
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'X-TC-Action': action,
|
||||
'X-TC-Version': version,
|
||||
'X-TC-Timestamp': str(request_timestamp),
|
||||
'X-TC-Region': self._region
|
||||
}
|
||||
|
||||
return utils.request.http_session.post('https://tmt.tencentcloudapi.com/', headers=headers, data=body_bytes)
|
||||
|
||||
def _on_fail(self, code):
|
||||
if self._cool_down_timer_handle is not None:
|
||||
return
|
||||
|
||||
sleep_time = 0
|
||||
if code == 'FailedOperation.NoFreeAmount':
|
||||
# 下个月恢复免费额度
|
||||
cur_time = datetime.datetime.now()
|
||||
year = cur_time.year
|
||||
month = cur_time.month + 1
|
||||
if month > 12:
|
||||
year += 1
|
||||
month = 1
|
||||
next_month_time = datetime.datetime(year, month, 1, minute=5)
|
||||
sleep_time = (next_month_time - cur_time).total_seconds()
|
||||
# Python 3.8之前不能超过一天
|
||||
sleep_time = min(sleep_time, 24 * 60 * 60 - 1)
|
||||
elif code in ('FailedOperation.ServiceIsolate', 'LimitExceeded'):
|
||||
# 需要手动处理,等5分钟
|
||||
sleep_time = 5 * 60
|
||||
if sleep_time != 0:
|
||||
self._cool_down_timer_handle = asyncio.get_running_loop().call_later(
|
||||
sleep_time, self._on_cool_down_timeout
|
||||
)
|
||||
self._on_availability_change()
|
||||
|
||||
def _on_cool_down_timeout(self):
|
||||
self._cool_down_timer_handle = None
|
||||
self._on_availability_change()
|
||||
|
||||
|
||||
class BaiduTranslate(TranslateProvider):
|
||||
def __init__(self, query_interval, source_language, target_language,
|
||||
app_id, secret):
|
||||
super().__init__(query_interval)
|
||||
self._source_language = source_language
|
||||
self._target_language = target_language
|
||||
self._app_id = app_id
|
||||
self._secret = secret
|
||||
|
||||
self._cool_down_timer_handle = None
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
return self._cool_down_timer_handle is None and super().is_available
|
||||
|
||||
async def _do_translate(self, text) -> Optional[str]:
|
||||
try:
|
||||
async with utils.request.http_session.post(
|
||||
'https://fanyi-api.baidu.com/api/trans/vip/translate',
|
||||
data=self._add_sign({
|
||||
'q': text,
|
||||
'from': self._source_language,
|
||||
'to': self._target_language,
|
||||
'appid': self._app_id,
|
||||
'salt': random.randint(1, 999999999)
|
||||
})
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning('BaiduTranslate request failed: status=%d %s', r.status, r.reason)
|
||||
return None
|
||||
data = await r.json()
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
return None
|
||||
error_code = data.get('error_code', None)
|
||||
if error_code is not None:
|
||||
logger.warning('BaiduTranslate failed: %s %s', error_code, data['error_msg'])
|
||||
self._on_fail(error_code)
|
||||
return None
|
||||
return ''.join(result['dst'] for result in data['trans_result'])
|
||||
|
||||
def _add_sign(self, data):
|
||||
str_to_sign = f"{self._app_id}{data['q']}{data['salt']}{self._secret}"
|
||||
sign = hashlib.md5(str_to_sign.encode('utf-8')).hexdigest()
|
||||
return {**data, 'sign': sign}
|
||||
|
||||
def _on_fail(self, code):
|
||||
if self._cool_down_timer_handle is not None:
|
||||
return
|
||||
|
||||
sleep_time = 0
|
||||
if code == '54004':
|
||||
# 账户余额不足,需要手动处理,等5分钟
|
||||
sleep_time = 5 * 60
|
||||
if sleep_time != 0:
|
||||
self._cool_down_timer_handle = asyncio.get_running_loop().call_later(
|
||||
sleep_time, self._on_cool_down_timeout
|
||||
)
|
||||
self._on_availability_change()
|
||||
|
||||
def _on_cool_down_timeout(self):
|
||||
self._cool_down_timer_handle = None
|
||||
self._on_availability_change()
|
||||
30
update.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
|
||||
import utils.request
|
||||
|
||||
VERSION = 'v1.7.0'
|
||||
|
||||
|
||||
def check_update():
|
||||
asyncio.get_event_loop().create_task(_do_check_update())
|
||||
|
||||
|
||||
async def _do_check_update():
|
||||
try:
|
||||
async with utils.request.http_session.get(
|
||||
'https://api.github.com/repos/xfgryujk/blivechat/releases/latest'
|
||||
) as r:
|
||||
data = await r.json()
|
||||
if data['name'] != VERSION:
|
||||
print('---------------------------------------------')
|
||||
print('New version available:', data['name'])
|
||||
print(data['body'])
|
||||
print('Download:', data['html_url'])
|
||||
print('---------------------------------------------')
|
||||
except aiohttp.ClientConnectionError:
|
||||
print('Failed to check update: connection failed')
|
||||
except asyncio.TimeoutError:
|
||||
print('Failed to check update: timeout')
|
||||
24
utils/request.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
from typing import *
|
||||
|
||||
import aiohttp
|
||||
|
||||
# 不带这堆头部有时候也能成功请求,但是带上后成功的概率更高
|
||||
BILIBILI_COMMON_HEADERS = {
|
||||
'Origin': 'https://www.bilibili.com',
|
||||
'Referer': 'https://www.bilibili.com/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
|
||||
' Chrome/114.0.0.0 Safari/537.36'
|
||||
}
|
||||
|
||||
http_session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
|
||||
def init():
|
||||
# ClientSession要在异步函数中创建
|
||||
async def do_init():
|
||||
global http_session
|
||||
http_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
|
||||
|
||||
asyncio.get_event_loop().run_until_complete(do_init())
|
||||
@@ -1,28 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
|
||||
import tornado.web
|
||||
|
||||
|
||||
# noinspection PyAbstractClass
|
||||
class ApiHandler(tornado.web.RequestHandler):
|
||||
def set_default_headers(self):
|
||||
# 跨域测试用
|
||||
if not self.application.settings['debug']:
|
||||
return
|
||||
self.set_header('Access-Control-Allow-Origin', '*')
|
||||
self.set_header('Access-Control-Allow-Methods', 'OPTIONS, PUT, POST, GET, DELETE')
|
||||
if 'Access-Control-Request-Headers' in self.request.headers:
|
||||
self.set_header('Access-Control-Allow-Headers',
|
||||
self.request.headers['Access-Control-Request-Headers'])
|
||||
|
||||
def prepare(self):
|
||||
if self.request.headers.get('Content-Type', '').startswith('application/json'):
|
||||
self.json_args = json.loads(self.request.body)
|
||||
else:
|
||||
self.json_args = None
|
||||
|
||||
async def options(self, *_args, **_kwargs):
|
||||
# 跨域测试用
|
||||
self.set_status(204 if self.application.settings['debug'] else 405)
|
||||
264
views/chat.py
@@ -1,264 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import enum
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import *
|
||||
|
||||
import aiohttp
|
||||
import tornado.websocket
|
||||
|
||||
import blivedm.blivedm as blivedm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(enum.IntEnum):
|
||||
JOIN_ROOM = 0
|
||||
ADD_TEXT = 1
|
||||
ADD_GIFT = 2
|
||||
ADD_MEMBER = 3
|
||||
|
||||
|
||||
DEFAULT_AVATAR_URL = 'https://static.hdslb.com/images/member/noface.gif'
|
||||
|
||||
_http_session = aiohttp.ClientSession()
|
||||
_avatar_url_cache: Dict[int, str] = {}
|
||||
_last_fetch_avatar_time = datetime.datetime.now()
|
||||
_last_avatar_failed_time = None
|
||||
_uids_to_fetch_avatar = asyncio.Queue(15)
|
||||
|
||||
|
||||
async def get_avatar_url(user_id):
|
||||
if user_id in _avatar_url_cache:
|
||||
return _avatar_url_cache[user_id]
|
||||
|
||||
global _last_avatar_failed_time, _last_fetch_avatar_time
|
||||
cur_time = datetime.datetime.now()
|
||||
# 防止获取头像频率太高被ban
|
||||
if (cur_time - _last_fetch_avatar_time).total_seconds() < 0.2:
|
||||
# 由_fetch_avatar_loop过一段时间再获取并缓存
|
||||
try:
|
||||
_uids_to_fetch_avatar.put_nowait(user_id)
|
||||
except asyncio.QueueFull:
|
||||
pass
|
||||
return DEFAULT_AVATAR_URL
|
||||
|
||||
if _last_avatar_failed_time is not None:
|
||||
if (cur_time - _last_avatar_failed_time).total_seconds() < 5 * 60 + 3:
|
||||
# 5分钟以内被ban,解封大约要15分钟
|
||||
return DEFAULT_AVATAR_URL
|
||||
else:
|
||||
_last_avatar_failed_time = None
|
||||
|
||||
_last_fetch_avatar_time = cur_time
|
||||
try:
|
||||
async with _http_session.get('https://api.bilibili.com/x/space/acc/info',
|
||||
params={'mid': user_id}) as r:
|
||||
if r.status != 200: # 可能会被B站ban
|
||||
logger.warning('获取头像失败:status=%d %s uid=%d', r.status, r.reason, user_id)
|
||||
_last_avatar_failed_time = cur_time
|
||||
return DEFAULT_AVATAR_URL
|
||||
data = await r.json()
|
||||
except aiohttp.ClientConnectionError:
|
||||
return DEFAULT_AVATAR_URL
|
||||
url = data['data']['face']
|
||||
if not url.endswith('noface.gif'):
|
||||
url += '@48w_48h'
|
||||
_avatar_url_cache[user_id] = url
|
||||
|
||||
if len(_avatar_url_cache) > 50000:
|
||||
for _, key in zip(range(100), _avatar_url_cache):
|
||||
del _avatar_url_cache[key]
|
||||
|
||||
return url
|
||||
|
||||
|
||||
async def _fetch_avatar_loop():
|
||||
while True:
|
||||
try:
|
||||
user_id = await _uids_to_fetch_avatar.get()
|
||||
if user_id in _avatar_url_cache:
|
||||
continue
|
||||
# 延时长一些使实时弹幕有机会获取头像
|
||||
await asyncio.sleep(0.4 - (datetime.datetime.now() - _last_fetch_avatar_time).total_seconds())
|
||||
asyncio.ensure_future(get_avatar_url(user_id))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
asyncio.ensure_future(_fetch_avatar_loop())
|
||||
|
||||
|
||||
class Room(blivedm.BLiveClient):
|
||||
def __init__(self, room_id):
|
||||
super().__init__(room_id, session=_http_session)
|
||||
self.clients: List['ChatHandler'] = []
|
||||
|
||||
def stop_and_close(self):
|
||||
if self.is_running:
|
||||
future = self.stop()
|
||||
future.add_done_callback(lambda _future: asyncio.ensure_future(self.close()))
|
||||
else:
|
||||
asyncio.ensure_future(self.close())
|
||||
|
||||
def send_message(self, cmd, data):
|
||||
body = json.dumps({'cmd': cmd, 'data': data})
|
||||
for client in self.clients:
|
||||
client.write_message(body)
|
||||
|
||||
async def _on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
|
||||
asyncio.ensure_future(self.__on_receive_danmaku(danmaku))
|
||||
|
||||
async def __on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
|
||||
if danmaku.uid == self.room_owner_uid:
|
||||
author_type = 3 # 主播
|
||||
elif danmaku.admin:
|
||||
author_type = 2 # 房管
|
||||
elif danmaku.privilege_type != 0: # 1总督,2提督,3舰长
|
||||
author_type = 1 # 舰队
|
||||
else:
|
||||
author_type = 0
|
||||
self.send_message(Command.ADD_TEXT, {
|
||||
'avatarUrl': await get_avatar_url(danmaku.uid),
|
||||
'timestamp': danmaku.timestamp,
|
||||
'authorName': danmaku.uname,
|
||||
'authorType': author_type,
|
||||
'content': danmaku.msg,
|
||||
'privilegeType': danmaku.privilege_type,
|
||||
'isGiftDanmaku': bool(danmaku.msg_type),
|
||||
'authorLevel': danmaku.user_level,
|
||||
'isNewbie': danmaku.urank < 10000,
|
||||
'isMobileVerified': bool(danmaku.mobile_verify),
|
||||
'medalLevel': 0 if danmaku.room_id != self.room_id else danmaku.medal_level
|
||||
})
|
||||
|
||||
async def _on_receive_gift(self, gift: blivedm.GiftMessage):
|
||||
if gift.coin_type != 'gold': # 丢人
|
||||
return
|
||||
self.send_message(Command.ADD_GIFT, {
|
||||
'avatarUrl': gift.face,
|
||||
'timestamp': gift.timestamp,
|
||||
'authorName': gift.uname,
|
||||
'giftName': gift.gift_name,
|
||||
'giftNum': gift.num,
|
||||
'totalCoin': gift.total_coin
|
||||
})
|
||||
|
||||
async def _on_buy_guard(self, message: blivedm.GuardBuyMessage):
|
||||
asyncio.ensure_future(self.__on_buy_guard(message))
|
||||
|
||||
async def __on_buy_guard(self, message: blivedm.GuardBuyMessage):
|
||||
self.send_message(Command.ADD_MEMBER, {
|
||||
'avatarUrl': await get_avatar_url(message.uid),
|
||||
'timestamp': message.start_time,
|
||||
'authorName': message.username
|
||||
})
|
||||
|
||||
|
||||
class RoomManager:
|
||||
def __init__(self):
|
||||
self._rooms: Dict[int, Room] = {}
|
||||
|
||||
def add_client(self, room_id, client: 'ChatHandler'):
|
||||
if room_id in self._rooms:
|
||||
room = self._rooms[room_id]
|
||||
else:
|
||||
logger.info('创建房间%d', room_id)
|
||||
room = Room(room_id)
|
||||
self._rooms[room_id] = room
|
||||
room.start()
|
||||
room.clients.append(client)
|
||||
|
||||
if client.application.settings['debug']:
|
||||
self.__send_test_message(room)
|
||||
|
||||
def del_client(self, room_id, client: 'ChatHandler'):
|
||||
if room_id not in self._rooms:
|
||||
return
|
||||
room = self._rooms[room_id]
|
||||
room.clients.remove(client)
|
||||
if not room.clients:
|
||||
logger.info('移除房间%d', room_id)
|
||||
room.stop_and_close()
|
||||
del self._rooms[room_id]
|
||||
|
||||
# 测试用
|
||||
@staticmethod
|
||||
def __send_test_message(room):
|
||||
base_data = {
|
||||
'avatarUrl': 'https://i0.hdslb.com/bfs/face/29b6be8aa611e70a3d3ac219cdaf5e72b604f2de.jpg@48w_48h',
|
||||
'timestamp': time.time(),
|
||||
'authorName': 'xfgryujk',
|
||||
}
|
||||
text_data = {
|
||||
**base_data,
|
||||
'authorType': 0,
|
||||
'content': '我能吞下玻璃而不伤身体',
|
||||
'privilegeType': 0,
|
||||
'isGiftDanmaku': False,
|
||||
'authorLevel': 20,
|
||||
'isNewbie': False,
|
||||
'isMobileVerified': True
|
||||
}
|
||||
vip_data = base_data
|
||||
gift_data = {
|
||||
**base_data,
|
||||
'giftName': '礼花',
|
||||
'giftNum': 1,
|
||||
'totalCoin': 28000
|
||||
}
|
||||
room.send_message(Command.ADD_TEXT, text_data)
|
||||
text_data['authorName'] = '主播'
|
||||
text_data['authorType'] = 3
|
||||
text_data['content'] = "I can eat glass, it doesn't hurt me."
|
||||
room.send_message(Command.ADD_TEXT, text_data)
|
||||
room.send_message(Command.ADD_MEMBER, vip_data)
|
||||
room.send_message(Command.ADD_GIFT, gift_data)
|
||||
gift_data['giftName'] = '节奏风暴'
|
||||
gift_data['totalCoin'] = 100000
|
||||
room.send_message(Command.ADD_GIFT, gift_data)
|
||||
gift_data['giftName'] = '摩天大楼'
|
||||
gift_data['totalCoin'] = 450000
|
||||
room.send_message(Command.ADD_GIFT, gift_data)
|
||||
gift_data['giftName'] = '小电视飞船'
|
||||
gift_data['totalCoin'] = 1245000
|
||||
room.send_message(Command.ADD_GIFT, gift_data)
|
||||
|
||||
|
||||
room_manager = RoomManager()
|
||||
|
||||
|
||||
# noinspection PyAbstractClass
|
||||
class ChatHandler(tornado.websocket.WebSocketHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.room_id = None
|
||||
|
||||
def open(self):
|
||||
logger.info('Websocket连接 %s', self.request.remote_ip)
|
||||
|
||||
def on_message(self, message):
|
||||
if self.room_id is not None:
|
||||
return
|
||||
body = json.loads(message)
|
||||
if body['cmd'] == Command.JOIN_ROOM:
|
||||
self.room_id = int(body['data']['roomId'])
|
||||
logger.info('客户端%s加入房间%d', self.request.remote_ip, self.room_id)
|
||||
room_manager.add_client(self.room_id, self)
|
||||
else:
|
||||
logger.warning('未知的命令: %s data: %s', body['cmd'], body['data'])
|
||||
|
||||
def on_close(self):
|
||||
logger.info('Websocket断开 %s room: %s', self.request.remote_ip, self.room_id)
|
||||
if self.room_id is not None:
|
||||
room_manager.del_client(self.room_id, self)
|
||||
|
||||
# 跨域测试用
|
||||
def check_origin(self, origin):
|
||||
if self.application.settings['debug']:
|
||||
return True
|
||||
return super().check_origin(origin)
|
||||
@@ -1,50 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import uuid
|
||||
|
||||
import views.base
|
||||
from typing import *
|
||||
|
||||
configs: Dict[str, dict] = {}
|
||||
|
||||
ALLOWED_FIELDS = (
|
||||
'showDanmaku', 'showGift', 'mergeSimilarDanmaku', 'minGiftPrice', 'maxSpeed',
|
||||
'blockGiftDanmaku', 'blockLevel', 'blockNewbie', 'blockNotMobileVerified',
|
||||
'blockKeywords', 'blockUsers', 'blockMedalLevel', 'css'
|
||||
)
|
||||
|
||||
|
||||
# noinspection PyAbstractClass
|
||||
class ConfigsHandler(views.base.ApiHandler):
|
||||
async def post(self):
|
||||
config_id = str(uuid.uuid4())
|
||||
config = {
|
||||
name: self.json_args[name] for name in ALLOWED_FIELDS
|
||||
}
|
||||
config['id'] = config_id
|
||||
configs[config_id] = config
|
||||
self.set_status(201)
|
||||
self.write(config)
|
||||
|
||||
if len(configs) > 10000:
|
||||
for _, key in zip(range(100), configs):
|
||||
del configs[key]
|
||||
|
||||
|
||||
# noinspection PyAbstractClass
|
||||
class ConfigHandler(views.base.ApiHandler):
|
||||
async def put(self, config_id):
|
||||
config = configs.get(config_id, None)
|
||||
if config is None:
|
||||
self.set_status(404)
|
||||
return
|
||||
for name in ALLOWED_FIELDS:
|
||||
config[name] = self.json_args[name]
|
||||
self.write(config)
|
||||
|
||||
async def get(self, config_id):
|
||||
config = configs.get(config_id, None)
|
||||
if config is None:
|
||||
self.set_status(404)
|
||||
return
|
||||
self.write(config)
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import tornado.web
|
||||
|
||||
|
||||
# noinspection PyAbstractClass
|
||||
class MainHandler(tornado.web.StaticFileHandler):
|
||||
"""为了使用Vue Router的history模式,把所有请求转发到index.html"""
|
||||
async def get(self, *args, **kwargs):
|
||||
await super().get('index.html', *args, **kwargs)
|
||||