Compare commits

..

145 Commits

Author SHA1 Message Date
John Smith
b2d12e84bd 更新版本号v1.6.0 2022-03-04 20:28:49 +08:00
John Smith
618164d77d 自定义表情存储目录改到data里 2022-03-01 22:13:25 +08:00
John Smith
9f8f238426 整理docker构建、使用多阶段构建 2022-03-01 00:57:16 +08:00
John Smith
801280efbd 更新readme 2022-02-28 21:08:12 +08:00
John Smith
2187dfc167 更新截图到新版本 2022-02-28 20:48:23 +08:00
John Smith
02ec4bc4ba 升级SQLAlchemy到1.4.31 2022-02-28 20:17:20 +08:00
John Smith
24d0671dae 完成上传表情文件 2022-02-27 22:05:37 +08:00
John Smith
903cfecab9 完成自定义表情前端 2022-02-27 14:45:19 +08:00
John Smith
3da8cc4227 添加自定义表情配置项 2022-02-26 23:27:10 +08:00
John Smith
4924f15614 房间内也使用i18n 2022-02-26 14:53:22 +08:00
John Smith
3a183a865d i18n延迟加载 2022-02-25 22:42:50 +08:00
John Smith
4c8cde0b3d 支持同时显示房管和舰长图标 2022-02-23 23:15:21 +08:00
John Smith
a68b50e885 更新舰长图标 2022-02-23 23:12:25 +08:00
John Smith
b5b9b2601a 不直接在Ticker修改父组件状态 2022-02-22 23:48:01 +08:00
John Smith
6c92859408 延迟删除房间,防止客户端短时间后重连 2022-02-20 00:42:31 +08:00
John Smith
c87325e5e0 到B站的连接和到客户端的连接解耦 2022-02-19 14:58:29 +08:00
John Smith
e40f1511ed 拆出服务模块 2022-02-15 00:18:46 +08:00
John Smith
3b556fc265 Merge branch 'master' into dev 2022-02-14 22:22:35 +08:00
John Smith
c05c70863a Merge pull request #59 from npmmirror/master
update https://registry.npm.taobao.org to https://registry.npmmirror.com
2022-02-11 22:41:58 +08:00
NPM Mirror Bot
781de48b4b update https://registry.npm.taobao.org to https://registry.npmmirror.com 2022-02-11 14:02:43 +00:00
John Smith
ccf9ceec0e 升级tornado到6.1.0,支持Python 3.8在Windows下的默认事件循环 2022-01-19 21:48:56 +08:00
John Smith
707ad57800 修复接收弹幕的协程被阻塞的问题 2022-01-19 21:47:59 +08:00
John Smith
8eff63e17b 前端显示的版本号用package.json里的 2022-01-07 23:18:34 +08:00
John Smith
36345c37ab 整理前端代码风格 2022-01-04 21:41:12 +08:00
John Smith
03a2801099 默认样式表情可以不是正方形 2022-01-04 20:10:15 +08:00
John Smith
e318bdfcac 前端直连升级弹幕协议版本 2022-01-03 23:35:10 +08:00
John Smith
23ebaca372 添加配置官方翻译接口教程链接 2022-01-02 13:51:22 +08:00
John Smith
6da20d9bdd 修复一些配置的说明 2021-12-31 00:45:47 +08:00
John Smith
dedae6f083 通过服务器转发支持表情 2021-12-26 23:34:45 +08:00
John Smith
3118a7de98 修改表情相关的一些实现和代码风格 2021-12-26 20:44:23 +08:00
Zhu Chuang
34f8d88e05 添加表情支持 (#55)
* 添加表情支持

* 添加表情的样式生成器选项

* 修复了样式生成器表情气泡总为舰长颜色
2021-12-26 19:12:25 +08:00
John Smith
776b354517 升级blivedm 2021-12-19 18:46:32 +08:00
John Smith
2a22617c1a 更新版本号v1.5.3 2021-07-18 14:55:13 +08:00
John Smith
6b77a8a17f 修复腾讯翻译白嫖版 2021-07-18 14:53:36 +08:00
John Smith
8780655341 配置文件兼容带BOM的UTF-8 2021-07-17 13:17:40 +08:00
John Smith
6a4faa83a4 修复一些PyCharm警告 2021-07-17 13:03:45 +08:00
John Smith
335248c076 更新版本号v1.5.2 2021-04-24 11:31:19 +08:00
John Smith
659fddc4d5 Merge branch 'dev' 2021-04-24 11:29:31 +08:00
John Smith
f64a0876a1 升级依赖 2021-04-24 11:28:15 +08:00
John Smith
df6097a184 添加百度翻译官方接口 2021-04-23 21:35:57 +08:00
John Smith
0b848b1288 翻译器负载均衡 2021-04-20 22:09:47 +08:00
John Smith
c6f5411efc 添加获取头像间隔、缓存等配置 2021-04-20 21:30:57 +08:00
John Smith
986f008fae git忽略配置文件 2021-04-18 17:57:43 +08:00
John Smith
2df316b81c 配置文件不进版本库 2021-04-18 17:54:41 +08:00
John Smith
8aa80fadd3 添加腾讯翻译官方接口 2021-04-11 22:11:13 +08:00
John Smith
f6e38c8804 支持配置翻译器、移除有道翻译 2021-04-11 14:16:32 +08:00
John Smith
47e5739ac0 UI优化 2021-04-05 22:42:42 +08:00
John Smith
e2f925426a 样式生成器预览内容自动生成 2021-04-05 20:55:31 +08:00
John Smith
e8f77cd2e0 添加测试房间 2021-04-05 18:48:49 +08:00
John Smith
e899149575 转发WS也不等待onclose回调 2021-03-28 20:01:48 +08:00
John Smith
77d0277d4e 添加HTTP请求超时 2021-03-28 18:30:31 +08:00
John Smith
4601ee328c 样式生成器预览可选明亮背景 2021-03-15 23:13:45 +08:00
John Smith
5c9f11b3f9 优化帮助页 2021-03-14 18:38:59 +08:00
John Smith
95ce33d45a 添加仿line样式生成器 2021-03-14 18:13:58 +08:00
John Smith
609f4c09c6 UI优化 2021-03-07 14:40:58 +08:00
John Smith
31f007fb75 样式拆成计算属性 2021-03-06 23:43:47 +08:00
John Smith
2b98ab9633 优化样式生成器输入框防抖 2021-03-06 19:04:31 +08:00
John Smith
1df03dca78 样式生成器拆成子组件 2021-03-06 17:51:11 +08:00
John Smith
b689c9dce5 Merge pull request #36 from xfgryujk/dependabot/pip/aiohttp-3.7.4
Bump aiohttp from 3.5.4 to 3.7.4
2021-02-26 12:16:52 +08:00
dependabot[bot]
23f4fb95e7 Bump aiohttp from 3.5.4 to 3.7.4
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.5.4 to 3.7.4.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.5.4...v3.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-26 02:29:46 +00:00
John Smith
845e144992 修复了样式生成器中切换SC固定栏后不显示的问题 2021-02-14 14:31:53 +08:00
John Smith
c19e7692d4 添加SC固定栏过渡 2021-02-12 17:25:49 +08:00
John Smith
b395886535 添加接收消息超时 2021-02-01 23:49:31 +08:00
John Smith
ddc13a6327 一些小优化 2021-01-28 23:50:12 +08:00
John Smith
d3cd5a3120 优化消息平滑,让发消息速度变化不要太频繁 2021-01-24 19:10:25 +08:00
John Smith
f50652af83 删除package-lock.json 2020-11-28 23:50:18 +08:00
John Smith
4059a06cc8 删除package-lock.json 2020-11-28 23:04:44 +08:00
John Smith
138bf8f7a5 Merge branch 'dev' 2020-11-28 22:58:16 +08:00
John Smith
098bc86e0c 更新版本号v1.5.1 2020-11-28 22:57:30 +08:00
John Smith
104f296929 修改一些说明 2020-11-28 22:56:45 +08:00
John Smith
1a52775974 修复B站翻译 2020-11-28 22:05:44 +08:00
John Smith
4f0964c0b3 添加标注打赏用户名读音 2020-11-28 21:53:49 +08:00
John Smith
c57520fff1 Merge pull request #34 from xfgryujk/dependabot/npm_and_yarn/frontend/highlight.js-9.18.5
Bump highlight.js from 9.18.1 to 9.18.5 in /frontend
2020-11-25 13:10:59 +08:00
dependabot[bot]
ff95909d5a Bump highlight.js from 9.18.1 to 9.18.5 in /frontend
Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 9.18.1 to 9.18.5.
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/9.18.5/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/9.18.1...9.18.5)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-25 00:00:10 +00:00
John Smith
8c7fc2780d 兼容腾讯翻译升级 2020-11-24 23:17:30 +08:00
John Smith
210749fa0f 兼容腾讯翻译升级 2020-11-17 21:56:01 +08:00
John Smith
888bc896a5 修正B站API错误信息字段 2020-10-17 17:12:22 +08:00
John Smith
e7d8fd0ff6 Merge branch 'dev' 2020-10-11 14:51:55 +08:00
John Smith
cce51a6990 更新版本号v1.5.0 2020-10-11 14:37:30 +08:00
John Smith
adeba8f552 优化SC固定栏 2020-10-11 14:36:23 +08:00
John Smith
0e06c4ba24 前端头像加载失败则使用默认头像 2020-10-11 13:14:10 +08:00
John Smith
0e060e61fa 修改一些说明 2020-10-11 13:00:57 +08:00
John Smith
8cf4310152 init_room支持降级 2020-10-11 12:52:15 +08:00
John Smith
d6897ca4b4 添加通过服务器转发消息配置 2020-09-14 21:50:40 +08:00
John Smith
6e2f3b1a0c 完成房间信息和头像接口 2020-09-13 21:24:20 +08:00
John Smith
6a64f3c795 添加前端直连B站弹幕服务器 2020-09-13 16:29:49 +08:00
John Smith
2dbf1769e0 分离房间和弹幕API 2020-09-12 17:37:52 +08:00
John Smith
54aac237ee Merge pull request #30 from xfgryujk/dependabot/npm_and_yarn/frontend/http-proxy-1.18.1
Bump http-proxy from 1.18.0 to 1.18.1 in /frontend
2020-09-12 15:57:13 +08:00
John Smith
2ec6096d4d 把YouTube的CSS移到单独文件 2020-09-12 15:50:16 +08:00
John Smith
3808609963 数据库头像URL类型改成VARCHAR 2020-09-12 11:33:11 +08:00
John Smith
c66a47c3e5 git忽略日志 2020-09-12 10:51:29 +08:00
dependabot[bot]
435ee43c98 Bump http-proxy from 1.18.0 to 1.18.1 in /frontend
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.18.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-09 00:12:58 +00:00
John Smith
cb150effbb Merge branch 'dev' 2020-09-05 12:08:50 +08:00
John Smith
47d7528b61 更新版本号v1.4.5 2020-09-05 12:08:17 +08:00
John Smith
152db9f00a 优化消息平滑 2020-09-05 12:05:01 +08:00
John Smith
7a244757d2 修复OBS 25.0.8不支持中文字体名的问题 2020-09-05 09:44:40 +08:00
John Smith
626099f835 docker暴露一些目录 2020-09-05 08:45:19 +08:00
John Smith
ca55f9917d 日志保存到文件 2020-09-03 20:01:54 +08:00
John Smith
4d58245ad9 添加加载器URL配置 2020-08-30 17:46:04 +08:00
John Smith
28b04f27ee 改一些默认配置 2020-08-29 14:27:15 +08:00
John Smith
1252e1942d 添加tornado xheaders配置 2020-08-22 18:38:52 +08:00
John Smith
38774252cc 更新blivedm 2020-08-19 23:17:29 +08:00
John Smith
7cfa94c68c 改API路径 2020-08-19 23:16:01 +08:00
John Smith
4e7cc7a782 更新nginx配置 2020-08-19 22:55:54 +08:00
John Smith
bfa6054899 显示的房间URL改成加载器URL 2020-08-18 21:48:33 +08:00
John Smith
d25a32beaf 增加牌子等级上限 2020-08-16 12:15:01 +08:00
John Smith
127344802d 添加允许自动翻译的房间配置 2020-08-16 12:13:53 +08:00
John Smith
96070ac473 更新使用方法 2020-08-16 10:10:49 +08:00
John Smith
583deb4588 Merge pull request #25 from xfgryujk/dependabot/npm_and_yarn/frontend/elliptic-6.5.3
Bump elliptic from 6.5.2 to 6.5.3 in /frontend
2020-07-30 23:43:30 +08:00
dependabot[bot]
b555c6ac2d Bump elliptic from 6.5.2 to 6.5.3 in /frontend
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-30 15:40:11 +00:00
John Smith
602a9f2a44 更新初始化房间API 2020-07-19 22:19:05 +08:00
John Smith
a89b64b454 修改样式 2020-07-19 21:33:26 +08:00
John Smith
49d58a9a60 删除BiliSC链接 2020-07-19 16:31:49 +08:00
John Smith
cb7ecc066c Merge pull request #23 from xfgryujk/dependabot/npm_and_yarn/frontend/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19 in /frontend
2020-07-17 21:33:12 +08:00
dependabot[bot]
57aeada21e Bump lodash from 4.17.15 to 4.17.19 in /frontend
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-17 02:05:38 +00:00
John Smith
c2de73a1a5 Merge pull request #22 from xfgryujk/dependabot/npm_and_yarn/frontend/websocket-extensions-0.1.4
Bump websocket-extensions from 0.1.3 to 0.1.4 in /frontend
2020-06-07 01:46:39 +08:00
dependabot[bot]
71f02c0269 Bump websocket-extensions from 0.1.3 to 0.1.4 in /frontend
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-06 16:14:56 +00:00
John Smith
9b8a581a83 更新版本号v1.4.4 2020-06-06 11:41:55 +08:00
John Smith
f554c8b6d5 兼容B站弹幕升级(使用旧接口获取token会连不上某些服务器) 2020-05-20 21:26:00 +08:00
John Smith
b862a0906e 修复弹幕时间不正确的问题 2020-05-20 21:20:08 +08:00
John Smith
61b13808fb 更新版本号v1.4.3 2020-05-10 14:14:14 +08:00
John Smith
7f25c219d8 前端优化 2020-05-09 23:17:49 +08:00
John Smith
32ba4187aa 修改一些文本 2020-05-09 22:46:24 +08:00
John Smith
06c0dfa3f2 修复冒号两边空白太宽的问题、消息颜色影响重复标记颜色的问题 2020-05-09 22:15:34 +08:00
John Smith
4c7bf69275 (真)修复启动长时间后自动翻译失效的问题 2020-05-09 21:38:21 +08:00
John Smith
63644f5499 修复qtv、qtk反了 2020-03-21 19:13:06 +08:00
John Smith
8a57304790 更新版本号v1.4.2 2020-03-21 19:05:53 +08:00
John Smith
d7df071595 修复启动长时间后自动翻译失效的问题 2020-03-21 18:42:27 +08:00
John Smith
6e8b04ef4f 修复删缓存时的错误、提高容错性 2020-03-21 15:23:31 +08:00
John Smith
b73a4e3748 更新版本号v1.4.1 2020-03-01 14:22:12 +08:00
John Smith
102746d0b0 添加显示礼物名、只合并同类礼物 2020-03-01 14:14:19 +08:00
John Smith
2d237af6fd 提示用户已加载页面 2020-02-29 19:20:29 +08:00
John Smith
4030ff43cd 减小编译后文件尺寸 2020-02-22 17:21:47 +08:00
John Smith
092cc8d9ab 更新帮助 2020-02-06 20:31:43 +08:00
John Smith
2d7106ae93 更新版本号v1.4.0 2020-02-06 19:53:33 +08:00
John Smith
d5b885aab4 添加前端获取后端配置 2020-02-06 19:51:03 +08:00
John Smith
34f0effb6a 修复翻译导致弹幕无法滚动到底部的问题 2020-02-06 18:31:05 +08:00
John Smith
d3e9300fa4 添加自动翻译前端 2020-02-06 17:39:56 +08:00
John Smith
0c9560f8ca 添加自动翻译后端 2020-02-06 12:48:20 +08:00
John Smith
1c5df01fbc websocket消息优化 2020-02-05 17:28:10 +08:00
John Smith
49cdc56cff 防止队列中出现相同uid时重复获取头像 2020-02-05 12:58:26 +08:00
John Smith
00e1f31f84 优化前端防卡代码 2020-02-04 20:33:29 +08:00
John Smith
b7e06652b4 防止前端弹幕卡住 2020-02-04 20:13:11 +08:00
John Smith
0cf49ee042 防止更新版本后浏览器加载缓存 2020-02-04 16:26:28 +08:00
John Smith
7d610ecfdc 修复获取头像future没有设置结果的问题、部分头像URL没有处理的问题 2020-02-04 16:12:16 +08:00
97 changed files with 52467 additions and 27457 deletions

View File

@@ -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/*

7
.gitignore vendored
View File

@@ -105,4 +105,9 @@ venv.bak/
.idea/
data/database.db
data/*
!data/config.example.ini
!data/emoticons/
data/emoticons/*
!data/emoticons/.gitkeep
log/*

View File

@@ -1,33 +1,46 @@
# 运行时
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:16.14.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 --registry=https://registry.npmmirror.com
# 编译
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 requirements.txt ./
RUN pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/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"]

162
README.md
View File

@@ -1,56 +1,148 @@
# blivechat
用于OBS的仿YouTube风格的bilibili直播评论栏
最近喜欢看VTuber想为此写些程序于是有了这个东西。~~写到一半发现有类似项目了:[bilibili-live-chat](https://github.com/Tsuk1ko/bilibili-live-chat)、[BiliChat](https://github.com/3Shain/BiliChat)。~~ 本项目就当做练手吧而且对YouTube的模仿程度更高
![OBS截图](https://github.com/xfgryujk/blivechat/blob/master/screenshots/obs.png)
![Chrome截图](https://github.com/xfgryujk/blivechat/blob/master/screenshots/chrome.png)
![样式生成器截图](https://github.com/xfgryujk/blivechat/blob/master/screenshots/stylegen.png)
![OBS截图](./screenshots/obs.png)
![Chrome截图](./screenshots/chrome.png)
![样式生成器截图](./screenshots/stylegen.png)
## 特性
* 兼容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:12450](http://localhost:12450)输入房间ID保存配置,复制房间URL
```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
### 公共服务器
请优先在本地使用,使用公共服务器会有更大的弹幕延迟,而且服务器故障时可能出现直播事故
**注意事项:**
* [第三方公共服务器](http://chat.bilisc.com/)
* [仅样式生成器](https://style.vtbs.moe/)
* 本地使用时不要关闭blivechat.exe那个黑框否则不能继续获取头像或弹幕
* 样式生成器没有列出所有本地字体,但是可以手动输入本地字体
* 如果需要使用翻译功能,建议看[配置官方翻译接口傻瓜式教程](https://www.bilibili.com/read/cv14663633)
### 源代码版
1. 编译前端需要安装Node.js和npm
```sh
cd frontend
npm i
npm run build
```
### 二、公共服务器
请优先在本地使用,使用公共服务器会有更大的延迟,而且服务器故障时可能发生直播事故
* [公共服务器](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.6以上版本):
```sh
pip3 install -r requirements.txt
python3 main.py
```
或者可以指定host和端口号
```sh
python3 main.py --host 127.0.0.1 --port 12450
```
```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 -d -p 12450:12450 xfgryujk/blivechat:latest
```
### 四、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;
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 /PATH/TO/BLIVECHAT/frontend/dist;
# 如果文件不存在,交给前端路由
try_files $uri $uri/ /index.html;
}
location /emoticons {
alias /PATH/TO/BLIVECHAT/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;
}
}
```

View File

@@ -1,31 +1,18 @@
# -*- 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'])
class ApiHandler(tornado.web.RequestHandler): # noqa
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.json_args = None
def prepare(self):
if self.request.headers.get('Content-Type', '').startswith('application/json'):
try:
self.json_args = json.loads(self.request.body)
except json.JSONDecodeError:
self.json_args = None
else:
self.json_args = None
async def options(self, *_args, **_kwargs):
# 跨域测试用
self.set_status(204 if self.application.settings['debug'] else 405)
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

View File

@@ -1,17 +1,23 @@
# -*- 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 blivedm.blivedm as blivedm
import models.avatar
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__)
@@ -24,235 +30,172 @@ class Command(enum.IntEnum):
ADD_MEMBER = 4
ADD_SUPER_CHAT = 5
DEL_SUPER_CHAT = 6
UPDATE_TRANSLATION = 7
_http_session = aiohttp.ClientSession()
room_manager: Optional['RoomManager'] = None
class ContentType(enum.IntEnum):
TEXT = 0
EMOTICON = 1
def init():
global room_manager
room_manager = RoomManager()
def make_message_body(cmd, data):
return json.dumps(
{
'cmd': cmd,
'data': data
}
).encode('utf-8')
class Room(blivedm.BLiveClient):
# 重新定义parse_XXX是为了减少对字段名的依赖防止B站改字段名
def __parse_danmaku(self, command):
info = command['info']
if info[3]:
room_id = info[3][3]
medal_level = info[3][0]
else:
room_id = medal_level = 0
return self._on_receive_danmaku(blivedm.DanmakuMessage(
None, None, None, info[0][4], None, None, info[0][9], None,
info[1],
info[2][0], info[2][1], info[2][2], None, None, info[2][5], info[2][6], None,
medal_level, None, None, room_id, None, None,
info[4][0], None, None,
None, None,
info[7]
))
def __parse_gift(self, command):
data = command['data']
return self._on_receive_gift(blivedm.GiftMessage(
data['giftName'], data['num'], data['uname'], data['face'], None,
data['uid'], data['timestamp'], None, None,
None, None, None, data['coin_type'], data['total_coin']
))
def __parse_buy_guard(self, command):
data = command['data']
return self._on_buy_guard(blivedm.GuardBuyMessage(
data['uid'], data['username'], None, None, None,
None, None, data['start_time'], None
))
def __parse_super_chat(self, command):
data = command['data']
return self._on_super_chat(blivedm.SuperChatMessage(
data['price'], data['message'], None, data['start_time'],
None, None, data['id'], None,
None, data['uid'], data['user_info']['uname'],
data['user_info']['face'], None,
None, None,
None, None, None,
None
))
_COMMAND_HANDLERS = {
**blivedm.BLiveClient._COMMAND_HANDLERS,
'DANMU_MSG': __parse_danmaku,
'SEND_GIFT': __parse_gift,
'GUARD_BUY': __parse_buy_guard,
'SUPER_CHAT_MESSAGE': __parse_super_chat
}
def __init__(self, room_id):
super().__init__(room_id, session=_http_session, heartbeat_interval=10)
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:
try:
client.write_message(body)
except tornado.websocket.WebSocketClosedError:
pass
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 models.avatar.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):
avatar_url = gift.face.replace('http:', '').replace('https:', '')
models.avatar.update_avatar_cache(gift.uid, avatar_url)
if gift.coin_type != 'gold': # 丢人
return
self.send_message(Command.ADD_GIFT, {
'avatarUrl': avatar_url,
'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 models.avatar.get_avatar_url(message.uid),
'timestamp': message.start_time,
'authorName': message.username
})
async def _on_super_chat(self, message: blivedm.SuperChatMessage):
avatar_url = message.face.replace('http:', '').replace('https:', '')
models.avatar.update_avatar_cache(message.uid, avatar_url)
self.send_message(Command.ADD_SUPER_CHAT, {
'avatarUrl': avatar_url,
'timestamp': message.start_time,
'authorName': message.uname,
'price': message.price,
'content': message.message,
'id': message.id
})
async def _on_super_chat_delete(self, message: blivedm.SuperChatDeleteMessage):
self.send_message(Command.ADD_SUPER_CHAT, {
'ids': message.ids
})
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,
):
# 为了节省带宽用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 [],
]
class RoomManager:
def __init__(self):
self._rooms: Dict[int, Room] = {}
async def add_client(self, room_id, client: 'ChatHandler'):
if room_id not in self._rooms:
if not await self._add_room(room_id):
client.close()
return
room = self._rooms[room_id]
room.clients.append(client)
logger.info('%d clients in room %s', len(room.clients), room_id)
if client.application.settings['debug']:
client.send_test_message()
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)
logger.info('%d clients in room %s', len(room.clients), room_id)
if not room.clients:
self._del_room(room_id)
async def _add_room(self, room_id):
if room_id in self._rooms:
return True
logger.info('Creating room %d', room_id)
room = Room(room_id)
self._rooms[room_id] = room
if await room.init_room():
room.start()
return True
else:
self._del_room(room_id)
return False
def _del_room(self, room_id):
if room_id not in self._rooms:
return
logger.info('Removing room %d', room_id)
room = self._rooms[room_id]
for client in room.clients:
client.close()
room.stop_and_close()
del self._rooms[room_id]
def make_emoticon_params(url):
return [
# 0: url
url,
]
# noinspection PyAbstractClass
class ChatHandler(tornado.websocket.WebSocketHandler):
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('Websocket connected %s', self.request.remote_ip)
logger.info('client=%s connected', self.request.remote_ip)
self._heartbeat_timer_handle = asyncio.get_event_loop().call_later(
self.HEARTBEAT_INTERVAL, self._on_send_heartbeat
)
self._refresh_receive_timeout_timer()
def on_message(self, message):
body = json.loads(message)
cmd = body['cmd']
if cmd == Command.HEARTBEAT:
pass
elif cmd == Command.JOIN_ROOM:
if self.room_id is not None:
return
self.room_id = int(body['data']['roomId'])
logger.info('Client %s is joining room %d', self.request.remote_ip, self.room_id)
asyncio.ensure_future(room_manager.add_client(self.room_id, self))
else:
logger.warning('Unknown cmd: %s body: %s', cmd, body)
def _on_send_heartbeat(self):
self.send_cmd_data(Command.HEARTBEAT, {})
self._heartbeat_timer_handle = asyncio.get_event_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_event_loop().call_later(
self.RECEIVE_TIMEOUT, self._on_receive_timeout
)
def _on_receive_timeout(self):
logger.warning('client=%s timed out', self.request.remote_ip)
self._receive_timeout_timer_handle = None
self.close()
def on_close(self):
logger.info('Websocket disconnected %s room: %s', self.request.remote_ip, str(self.room_id))
if self.room_id is not None:
room_manager.del_client(self.room_id, 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:
# 超时没有加入房间也断开
if self.has_joined_room:
self._refresh_receive_timeout_timer()
body = json.loads(message)
cmd = body['cmd']
if cmd == Command.HEARTBEAT:
pass
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.ensure_future(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):
@@ -260,56 +203,144 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
return True
return super().check_origin(origin)
# 测试用
def send_test_message(self):
base_data = {
'avatarUrl': '//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
}
member_data = base_data
gift_data = {
**base_data,
'giftName': '摩天大楼',
'giftNum': 1,
'totalCoin': 450000
}
sc_data = {
**base_data,
'price': 30,
'content': 'The quick brown fox jumps over the lazy dog',
'id': 1
}
self.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."
self.send_message(Command.ADD_TEXT, text_data)
self.send_message(Command.ADD_MEMBER, member_data)
self.send_message(Command.ADD_SUPER_CHAT, sc_data)
sc_data['price'] = 100
sc_data['content'] = '敏捷的棕色狐狸跳过了懒狗'
sc_data['id'] = 2
self.send_message(Command.ADD_SUPER_CHAT, sc_data)
# self.send_message(Command.DEL_SUPER_CHAT, {'ids': [1, 2]})
self.send_message(Command.ADD_GIFT, gift_data)
gift_data['giftName'] = '小电视飞船'
gift_data['totalCoin'] = 1245000
self.send_message(Command.ADD_GIFT, gift_data)
@property
def has_joined_room(self):
return self.room_id is not None
def send_message(self, cmd, data):
body = json.dumps({'cmd': cmd, 'data': data})
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:
pass
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[2] = '主播'
text_data[3] = 3
text_data[4] = "I can eat glass, it doesn't hurt me."
text_data[11] = uuid.uuid4().hex
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_message(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, 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
})

View File

@@ -1,10 +1,78 @@
# -*- coding: utf-8 -*-
import asyncio
import hashlib
import logging
import os
import tornado.web
import api.base
import config
import update
# 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)
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):
try:
await super().get(path, include_body)
except tornado.web.HTTPError as e:
if e.status_code != 404:
raise
# 不存在的文件请求转发到index.html交给前端路由
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_event_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}'

Submodule blivedm updated: 15669a2084...d8f7f6b782

121
config.py
View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
import configparser
import logging
import os
@@ -7,20 +6,41 @@ from typing import *
logger = logging.getLogger(__name__)
CONFIG_PATH = os.path.join('data', 'config.ini')
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():
reload()
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 config.load(CONFIG_PATH):
global _config
_config = config
if not config.load(config_path):
return False
global _config
_config = config
return True
def get_config():
@@ -30,14 +50,93 @@ def get_config():
class AppConfig:
def __init__(self):
self.database_url = 'sqlite:///data/database.db'
self.tornado_xheaders = False
self.loader_url = ''
self.enable_upload_file = True
self.fetch_avatar_interval = 3.5
self.fetch_avatar_max_queue_size = 2
self.avatar_cache_size = 50000
self.enable_translate = True
self.allow_translate_rooms = set()
self.translation_cache_size = 50000
self.translator_configs = []
def load(self, path):
config = configparser.ConfigParser()
config.read(path)
try:
app_section = config['app']
self.database_url = app_section['database_url']
except (KeyError, ValueError):
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.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.enable_upload_file = app_section.getboolean('enable_upload_file', fallback=self.enable_upload_file)
self.fetch_avatar_interval = app_section.getfloat('fetch_avatar_interval', fallback=self.fetch_avatar_interval)
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.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'),
'max_queue_size': section.getint('max_queue_size')
}
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)

146
data/config.example.ini Normal file
View File

@@ -0,0 +1,146 @@
# 如果要修改配置可以复制此文件并重命名为“config.ini”再修改
# If you want to modify the configuration, copy this file and rename it to "config.ini" and edit
[app]
# 数据库配置见https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
# See https://docs.sqlalchemy.org/en/13/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
# 允许上传自定义表情文件
# Enable uploading custom emote file
enable_upload_file = true
# 获取头像间隔时间。如果小于3秒有很大概率被服务器拉黑
# Interval between fetching avatars (seconds). At least 3 seconds is recommended
fetch_avatar_interval = 3.5
# 获取头像最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
# Maximum queue length for fetching avatar
fetch_avatar_max_queue_size = 2
# 头像缓存数量
# Number of avatar caches
avatar_cache_size = 50000
# 允许自动翻译到日语
# 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 =
# 翻译缓存数量
# 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,bilibili_translate_free
[tencent_translate_free]
# 类型:腾讯翻译白嫖版。使用了网页版的接口,**将来可能失效**
type = TencentTranslateFree
# 请求间隔时间(秒),等于 1 / QPS。目前没有遇到此接口有调用频率限制10QPS应该够用了
query_interval = 0.1
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
max_queue_size = 100
# 自动auto中文zh日语jp英语en韩语kr
# 完整语言列表见文档https://cloud.tencent.com/document/product/551/15619
# 源语言
source_language = zh
# 目标语言
target_language = jp
[bilibili_translate_free]
# 类型B站翻译白嫖版。使用了B站直播网页的接口**将来可能失效**。目前B站翻译后端是百度翻译
type = BilibiliTranslateFree
# 请求间隔时间(秒),等于 1 / QPS。目前此接口频率限制是3秒一次
query_interval = 3.1
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
max_queue_size = 3
[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
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
max_queue_size = 30
# 自动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
# * 标准版完全免费不限使用字符量QPS=1
# * 高级版每月前200万字符免费超出后仅收取超出部分费用QPS=1049元/百万字符
# * 尊享版每月前200万字符免费超出后仅收取超出部分费用QPS=10049元/百万字符
# 类型:百度翻译
type = BaiduTranslate
# 请求间隔时间(秒),等于 1 / QPS
query_interval = 1.5
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
max_queue_size = 9
# 自动auto中文zh日语jp英语en韩语kor
# 完整语言列表见文档https://fanyi-api.baidu.com/doc/21
# 源语言
source_language = zh
# 目标语言
target_language = jp
# 百度翻译开放平台应用ID和密钥
app_id =
secret =

View File

@@ -1,8 +0,0 @@
[app]
# See https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
database_url = sqlite:///data/database.db
# DON'T modify this section
[DEFAULT]
database_url = sqlite:///data/database.db

0
data/emoticons/.gitkeep Normal file
View File

2
frontend/.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
brotli_decode.js
pronunciation/dict*.js

79
frontend/.eslintrc.js Normal file
View File

@@ -0,0 +1,79 @@
module.exports = {
"root": true,
"env": {
"browser": true,
"node": true
},
"parserOptions": {
"parser": "babel-eslint"
},
"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输出看不到的
}
}

3
frontend/.gitignore vendored
View File

@@ -19,3 +19,6 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?
package-lock.json

View File

@@ -1,5 +1,13 @@
module.exports = {
presets: [
'@vue/app'
'@vue/cli-plugin-babel/preset'
],
plugins: [
[
'component', {
'libraryName': 'element-ui',
'styleLibraryName': 'theme-chalk'
}
]
]
}

14
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2015",
"module": "esnext",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": [
"./src/**/*.js",
"./src/**/*.vue"
]
}

23828
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "blivechat",
"version": "0.1.0",
"version": "1.6.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@@ -8,38 +8,25 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.19.0",
"core-js": "^2.6.5",
"axios": "^0.21.1",
"core-js": "^3.6.5",
"downloadjs": "^1.4.7",
"element-ui": "^2.9.1",
"lodash": "^4.17.13",
"lodash": "^4.17.19",
"vue": "^2.6.10",
"vue-i18n": "^8.11.2",
"vue-router": "^3.0.6"
},
"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/cli-plugin-babel": "^4.5.12",
"@vue/cli-plugin-eslint": "^4.5.12",
"@vue/cli-service": "~4.5.12",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"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": {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 110 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,418 @@
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: 0,
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) {
this.refreshReceiveTimeoutTimer()
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: {
// 服务器心跳包,包含人气值,这里没用
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 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
}
this.onAddText(data)
}
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
}

View File

@@ -0,0 +1,190 @@
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 HEARTBEAT_INTERVAL = 10 * 1000
const RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + (5 * 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.heartbeatTimerId = null
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.heartbeatTimerId = window.setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
this.refreshReceiveTimeoutTimer()
}
sendHeartbeat() {
this.websocket.send(JSON.stringify({
cmd: COMMAND_HEARTBEAT
}))
}
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.heartbeatTimerId) {
window.clearInterval(this.heartbeatTimerId)
this.heartbeatTimerId = 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) {
this.refreshReceiveTimeoutTimer()
let { cmd, data } = JSON.parse(event.data)
switch (cmd) {
case 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
}
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
}
}
}
}

View File

@@ -0,0 +1,244 @@
import { getUuid4Hex } from '@/utils'
import * as constants from '@/components/ChatRenderer/constants'
import * as avatar from './avatar'
const NAMES = [
'xfgryujk', 'Simon', 'Il Harper', 'Kinori', 'shugen', 'yuyuyzl', '3Shain', '光羊', '黑炎', 'Misty', '孤梦星影',
'ジョナサン・ジョースター', 'ジョセフ・ジョースター', 'ディオ・ブランドー', '空條承太郎', '博丽灵梦', '雾雨魔理沙',
'Rick Astley'
]
const CONTENTS = [
'草', 'kksk', '8888888888', '888888888888888888888888888888', '老板大气,老板身体健康',
'The quick brown fox jumps over the lazy dog', "I can eat glass, it doesn't hurt me",
'我不做人了JOJO', '無駄無駄無駄無駄無駄無駄無駄無駄', '欧啦欧啦欧啦欧啦欧啦欧啦欧啦欧啦', '逃げるんだよォ!',
'嚯,朝我走过来了吗,没有选择逃跑而是主动接近我么', '不要停下来啊', '已经没有什么好怕的了',
'I am the bone of my sword. Steel is my body, and fire is my blood.', '言いたいことがあるんだよ!',
'我忘不掉夏小姐了。如果不是知道了夏小姐,说不定我已经对这个世界没有留恋了', '迷えば、敗れる',
'Farewell, ashen one. May the flame guide thee', '竜神の剣を喰らえ!', '竜が我が敌を喰らう!',
'有一说一,这件事大家懂的都懂,不懂的,说了你也不明白,不如不说', '让我看看', '我柜子动了,我不玩了'
]
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
}
}
}
},
// 表情
{
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)
}
}
}
},
// 礼物
{
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.minSleepTime = 800
this.maxSleepTime = 1200
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() {
this.timerId = window.setTimeout(this.onTimeout.bind(this), randInt(this.minSleepTime, this.maxSleepTime))
}
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
}
}
}

View 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
}

View 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
}

View File

@@ -1,29 +0,0 @@
import {mergeConfig} from '@/utils'
export const DEFAULT_CONFIG = {
minGiftPrice: 7, // $1
mergeSimilarDanmaku: true,
showDanmaku: true,
showGift: true,
maxNumber: 60,
blockGiftDanmaku: true,
blockLevel: 0,
blockNewbie: true,
blockNotMobileVerified: true,
blockKeywords: '',
blockUsers: '',
blockMedalLevel: 0
}
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)
}

11
frontend/src/api/main.js Normal file
View 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
}

View 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;
;
}

View 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;
}

View 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;
}

View File

@@ -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;
}

View 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;
}

View File

@@ -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;
}

View File

@@ -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%);
}

View File

@@ -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;
}

View 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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -1,306 +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">{{timeText}}</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'
import * as utils from '@/utils'
export default {
name: 'LegacyPaidMessage',
components: {
ImgShadow
},
props: {
avatarUrl: String,
authorName: String,
title: String,
content: String,
time: Date
},
computed: {
timeText() {
return utils.getTimeTextMinSec(this.time)
}
}
}
</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>

View 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>

View File

@@ -16,23 +16,21 @@
></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">{{priceText}}</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">{{timeText}}</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'
@@ -53,393 +51,13 @@ export default {
return constants.getPriceConfig(this.price).colors
},
priceText() {
return 'CN¥' + utils.formatCurrency(this.price)
return `CN¥${utils.formatCurrency(this.price)}`
},
timeText() {
return utils.getTimeTextMinSec(this.time)
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>

View File

@@ -4,23 +4,20 @@
: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">{{timeText}}</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="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">
{{content}}
<el-badge :value="repeated" :max="99" v-show="repeated > 1" class="style-scope yt-live-chat-text-message-renderer"
:style="{'--repeated-mark-color': repeatedMarkColor}"
<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>
@@ -28,8 +25,8 @@
</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'
@@ -41,20 +38,26 @@ export default {
name: 'TextMessage',
components: {
ImgShadow,
AuthorBadge
AuthorChip
},
props: {
avatarUrl: 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.getTimeTextMinSec(this.time)
return utils.getTimeTextHourMin(this.time)
},
authorTypeText() {
return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
@@ -69,7 +72,7 @@ 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 `hsl(${color[0]}, ${color[1]}%, ${color[2]}%)`
@@ -80,7 +83,7 @@ export default {
<style>
yt-live-chat-text-message-renderer>#content>#message>.el-badge {
margin-left: 5px;
margin-left: 10px;
}
yt-live-chat-text-message-renderer>#content>#message>.el-badge .el-badge__content {
@@ -88,309 +91,10 @@ yt-live-chat-text-message-renderer>#content>#message>.el-badge .el-badge__conten
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>

View File

@@ -1,97 +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'
import {formatCurrency} from '@/utils'
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) {
@@ -107,31 +158,37 @@ export default {
},
getText(message) {
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
return 'Member'
return this.$t('chat.tickerMembership')
}
return 'CN¥' + formatCurrency(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) {
@@ -144,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>

View File

@@ -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,17 +12,30 @@ 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 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_MEMBER = 1
export const MESSAGE_TYPE_SUPER_CHAT = 2
export const MESSAGE_TYPE_DEL = 3
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
@@ -98,7 +113,7 @@ export const PRICE_CONFIGS = [
pinTime: 0
},
{ // $1蓝
price: 1 * EXCHANGE_RATE,
price: EXCHANGE_RATE,
colors: {
contentBg: 'rgba(30,136,229,1)',
headerBg: 'rgba(21,101,192,1)',
@@ -111,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
@@ -119,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
}

File diff suppressed because it is too large Load Diff

49
frontend/src/i18n.js Normal file
View 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())

View File

@@ -1,10 +1,10 @@
export default {
sidebar: {
home: 'Home',
stylegen: 'Style generator',
stylegen: 'Style Generator',
help: 'Help',
projectAddress: 'Project address',
giftRecord: 'Super Chat record'
projectAddress: 'Project Address',
giftRecordOfficial: 'Official Super Chat Record',
},
home: {
roomIdEmpty: "Room ID can't be empty",
@@ -14,7 +14,9 @@ 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)',
maxNumber: 'Max number of messages',
@@ -28,15 +30,36 @@ export default {
blockUsers: 'Block users',
blockMedalLevel: 'Block medal level lower than',
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',
enterRoom: 'Enter room',
enterTestRoom: 'Enter test room',
exportConfig: 'Export config',
importConfig: 'Import config',
failedToParseConfig: 'Failed to parse config: '
},
stylegen: {
legacy: 'Classic',
lineLike: 'Line-like',
light: 'light',
dark: 'dark',
outlines: 'Outlines',
showOutlines: 'Show outlines',
outlineSize: 'Outline size',
@@ -46,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)',
@@ -56,6 +80,7 @@ export default {
memberColor: 'Member color',
showBadges: 'Show badges',
showColon: 'Show colon after name',
emoticonSize: 'Emoticon size',
messages: 'Messages',
color: 'Color',
@@ -72,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)',
@@ -106,9 +131,18 @@ export default {
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 after saving the configuration',
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'
}
}

View File

@@ -4,7 +4,7 @@ export default {
stylegen: 'スタイルジェネレータ',
help: 'ヘルプ',
projectAddress: 'プロジェクトアドレス',
giftRecord: 'スーパーチャット記録'
giftRecordOfficial: '公式スーパーチャット記録',
},
home: {
roomIdEmpty: 'ルームのIDを空白にすることはできません',
@@ -14,7 +14,9 @@ export default {
roomId: 'ルームID',
showDanmaku: 'コメントを表示する',
showGift: 'スーパーチャットと新メンバーを表示する',
showGiftName: 'ギフト名を表示する',
mergeSimilarDanmaku: '同じコメントを合併する',
mergeGift: 'ギフトを合併する',
minGiftPrice: '最低表示スーパーチャット価格CNY',
maxNumber: '最大コメント数',
@@ -28,15 +30,36 @@ export default {
blockUsers: 'ブロックユーザー',
blockMedalLevel: 'ブロック勲章等級がx未満',
advanced: 'アドバンスド',
relayMessagesByServer: 'サーバを介してメッセージを転送する',
autoTranslate: 'コメントを日本語に翻訳する(サーバを介してメッセージを転送する必要)',
giftUsernamePronunciation: 'スーパーチャットのユーザー名の発音',
dontShow: '非表示',
pinyin: 'ピンイン',
kana: '仮名',
emoticon: 'カスタムスタンプ',
emoticonKeyword: '置き換えるキーワード',
emoticonUrl: 'URL',
operation: '操作',
addEmoticon: 'スタンプを追加',
emoticonFileTooLarge: 'ファイルサイズが大きすぎます。最大サイズは1MBです',
roomUrl: 'ルームのURL',
copy: 'コピー',
enterRoom: 'ルームに入る',
enterTestRoom: 'テストルームに入る',
exportConfig: 'コンフィグの導出',
importConfig: 'コンフィグの導入',
failedToParseConfig: 'コンフィグ解析に失敗しました'
},
stylegen: {
legacy: '古典',
lineLike: 'Line風',
light: '明るい',
dark: '暗い',
outlines: 'アウトライン',
showOutlines: 'アウトラインを表示する',
outlineSize: 'アウトラインのサイズ',
@@ -47,6 +70,7 @@ export default {
avatarSize: 'アイコンのサイズ',
userNames: 'ユーザー名',
showUserNames: 'ユーザー名を表示する',
font: 'フォント',
fontSize: 'フォントサイズ',
lineHeight: '行の高さ0はデフォルト',
@@ -56,6 +80,7 @@ export default {
memberColor: 'メンバーの色',
showBadges: '勲章を見せる',
showColon: 'ユーザー名の後にコロンが表示されます',
emoticonSize: 'スタンプサイズ',
messages: 'コメント',
color: '色',
@@ -106,9 +131,18 @@ export default {
help: {
help: 'ヘルプ',
p1: '1. ビリビリの生放送ウェブから生放送ルームIDをこぴーする',
p2: '2. ホームページでコピーしたIDを入力し、配置を保存すると、ルームのURLをこぴーする',
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: 'メンバー'
}
}

View File

@@ -4,7 +4,7 @@ export default {
stylegen: '样式生成器',
help: '帮助',
projectAddress: '项目地址',
giftRecord: '打赏记录'
giftRecordOfficial: '官方打赏记录',
},
home: {
roomIdEmpty: '房间ID不能为空',
@@ -14,7 +14,9 @@ export default {
roomId: '房间ID',
showDanmaku: '显示弹幕',
showGift: '显示打赏和新舰长',
showGiftName: '显示礼物名',
mergeSimilarDanmaku: '合并相似弹幕',
mergeGift: '合并礼物',
minGiftPrice: '最低显示打赏价格(元)',
maxNumber: '最大弹幕数',
@@ -28,15 +30,36 @@ export default {
blockUsers: '屏蔽用户',
blockMedalLevel: '屏蔽当前直播间勋章等级低于',
advanced: '高级',
relayMessagesByServer: '通过服务器转发消息',
autoTranslate: '自动翻译弹幕到日语(需要通过服务器转发消息)',
giftUsernamePronunciation: '标注打赏用户名读音',
dontShow: '不显示',
pinyin: '拼音',
kana: '日文假名',
emoticon: '自定义表情',
emoticonKeyword: '替换关键词',
emoticonUrl: 'URL',
operation: '操作',
addEmoticon: '添加表情',
emoticonFileTooLarge: '文件尺寸太大最大1MB',
roomUrl: '房间URL',
copy: '复制',
enterRoom: '进入房间',
enterTestRoom: '进入测试房间',
exportConfig: '导出配置',
importConfig: '导入配置',
failedToParseConfig: '配置解析失败:'
},
stylegen: {
legacy: '经典',
lineLike: '仿微信',
light: '明亮',
dark: '黑暗',
outlines: '描边',
showOutlines: '显示描边',
outlineSize: '描边尺寸',
@@ -47,6 +70,7 @@ export default {
avatarSize: '头像尺寸',
userNames: '用户名',
showUserNames: '显示用户名',
font: '字体',
fontSize: '字体尺寸',
lineHeight: '行高0为默认',
@@ -56,6 +80,7 @@ export default {
memberColor: '舰长颜色',
showBadges: '显示勋章',
showColon: '用户名后显示冒号',
emoticonSize: '表情大小',
messages: '消息',
color: '颜色',
@@ -66,7 +91,7 @@ export default {
backgrounds: '背景',
bgColor: '背景色',
useBarsInsteadOfBg: '用条代替背景',
useBarsInsteadOfBg: '用条代替消息背景',
messageBgColor: '消息背景色',
ownerMessageBgColor: '主播消息背景色',
moderatorMessageBgColor: '房管消息背景色',
@@ -93,7 +118,7 @@ export default {
animateIn: '进入动画',
fadeInTime: '淡入时间(毫秒)',
animateOut: '移除旧消息',
animateOutWaitTime: '等待时间(秒)',
animateOutWaitTime: '移除前等待时间(秒)',
fadeOutTime: '淡出时间(毫秒)',
slide: '滑动',
reverseSlide: '反向滑动',
@@ -106,9 +131,18 @@ export default {
help: {
help: '帮助',
p1: '1. 从B站直播间网页复制房间ID',
p2: '2. 把房间ID输入到首页的房间ID保存配置后复制房间URL',
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: '会员'
}
}

View File

@@ -8,46 +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 :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="https://bilisc.com/" target="_blank">
<a href="http://link.bilibili.com/ctool/vtuber" target="_blank">
<el-menu-item>
<i class="el-icon-share"></i>{{$t('sidebar.giftRecord')}}
<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)
}
}
}

View File

@@ -1,27 +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">
v1.3.0
</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',
@@ -30,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()
},
@@ -51,7 +53,7 @@ export default {
<style>
html {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
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 {
@@ -60,6 +62,7 @@ html, body, #app, .app-wrapper, .sidebar-container {
body {
margin: 0;
background-color: #f6f8fa;
}
a, a:focus, a:hover {
@@ -92,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 {

View File

@@ -1,30 +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 Help from './views/Help'
import Room from './views/Room.vue'
import NotFound from './views/NotFound.vue'
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-/
@@ -37,37 +65,35 @@ const router = new VueRouter({
path: '/',
component: Layout,
children: [
{path: '', component: Home},
{path: 'stylegen', name: 'stylegen', component: StyleGenerator},
{path: 'help', name: 'help', component: Help}
{ 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')

View File

@@ -1,34 +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 function toBool (val) {
if (typeof val === 'string') {
return val !== 'false' && val !== ''
}
return !!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 getTimeTextMinSec (date) {
let min = ('00' + date.getMinutes()).slice(-2)
let sec = ('00' + date.getSeconds()).slice(-2)
return `${min}:${sec}`
}

View 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('')
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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('')
}
}

View File

@@ -0,0 +1,58 @@
export class Trie {
constructor() {
this._root = this._createNode()
}
_createNode() {
return {
children: {}, // char -> node
value: null
}
}
set(key, value) {
if (key === '') {
throw new Error('key is empty')
}
let node = this._root
for (let char of key) {
let nextNode = node.children[char]
if (nextNode === undefined) {
nextNode = node.children[char] = this._createNode()
}
node = nextNode
}
node.value = value
}
get(key) {
let node = this._root
for (let char of key) {
let nextNode = node.children[char]
if (nextNode === undefined) {
return null
}
node = nextNode
}
return node.value
}
has(key) {
return this.get(key) !== null
}
greedyMatch(str) {
let node = this._root
for (let char of str) {
let nextNode = node.children[char]
if (nextNode === undefined) {
return null
}
if (nextNode.value !== null) {
return nextNode.value
}
node = nextNode
}
return null
}
}

View File

@@ -1,16 +1,19 @@
<template>
<div>
<h1>{{$t('help.help')}}</h1>
<p>{{$t('help.p1')}}</p>
<p><el-image src="/static/img/tutorial/tutorial-1.png"></el-image></p>
<p>{{$t('help.p2')}}</p>
<p><el-image src="/static/img/tutorial/tutorial-2.png"></el-image></p>
<p>{{$t('help.p3')}}</p>
<p><el-image src="/static/img/tutorial/tutorial-3.png"></el-image></p>
<p>{{$t('help.p4')}}</p>
<p><el-image src="/static/img/tutorial/tutorial-4.png"></el-image></p>
<p>{{$t('help.p5')}}</p>
<p><el-image src="/static/img/tutorial/tutorial-5.png"></el-image></p>
<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>
@@ -19,3 +22,13 @@ export default {
name: 'Help'
}
</script>
<style scoped>
.img-container {
text-align: center;
}
.img-container.large-img .el-image {
height: 80vh;
}
</style>

View File

@@ -1,114 +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.maxNumber')">
<el-input v-model.number="form.maxNumber" type="number" min="1"></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-tabs>
<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-divider></el-divider>
<el-form-item :label="$t('home.roomUrl')">
<el-input ref="roomUrlInput" readonly :value="roomUrl" style="width: calc(100% - 8em); 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" :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 * as 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 {
serverConfig: {
enableTranslate: true,
enableUploadFile: true,
loaderUrl: ''
},
form: {
roomId: parseInt(window.localStorage.roomId || '1'),
...config.getLocalConfig()
...chatConfig.getLocalConfig(),
roomId: parseInt(window.localStorage.roomId || '1')
}
}
},
computed: {
roomUrl() {
if (this.form.roomId === '') {
return this.getRoomUrl(false)
},
obsRoomUrl() {
if (this.roomUrl === '') {
return ''
}
let query = {...this.form}
delete query.roomId
let resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId}, query})
return `${window.location.protocol}//${window.location.host}${resolved.href}`
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
config.setLocalConfig(this.form)
chatConfig.setLocalConfig(this.form)
}, 500)
},
mounted() {
this.updateServerConfig()
},
methods: {
async updateServerConfig() {
try {
this.serverConfig = (await mainApi.getServerInfo()).config
} catch (e) {
this.$message.error(`Failed to fetch server information: ${e}`)
throw e
}
},
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() {
@@ -125,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>

View File

@@ -1,199 +1,253 @@
<template>
<chat-renderer ref="renderer" :maxNumber="config.maxNumber"></chat-renderer>
<chat-renderer ref="renderer" :maxNumber="config.maxNumber" :showGiftName="config.showGiftName"></chat-renderer>
</template>
<script>
import {mergeConfig, toBool, toInt} from '@/utils'
import * as 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_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
export default {
name: 'Room',
components: {
ChatRenderer
},
props: {
roomId: {
type: Number,
default: null
},
strConfig: {
type: Object,
default: () => ({})
}
},
data() {
return {
config: {...config.DEFAULT_CONFIG},
websocket: null,
retryCount: 0,
isDestroying: false,
heartbeatTimerId: null,
nextId: 0,
config: chatConfig.deepCloneDefaultConfig(),
chatClient: null,
pronunciationConverter: null
}
},
computed: {
blockKeywords() {
return this.config.blockKeywords.split('\n').filter(val => val)
blockKeywordsTrie() {
let blockKeywords = this.config.blockKeywords.split('\n')
let res = new trie.Trie()
for (let keyword of blockKeywords) {
if (keyword !== '') {
res.set(keyword, true)
}
}
return res
},
blockUsers() {
return this.config.blockUsers.split('\n').filter(val => val)
blockUsersTrie() {
let blockUsers = this.config.blockUsers.split('\n')
let res = new trie.Trie()
for (let user of blockUsers) {
if (user !== '') {
res.set(user, true)
}
}
return res
},
emoticonsTrie() {
let res = new trie.Trie()
for (let emoticon of this.config.emoticons) {
if (emoticon.keyword !== '' && emoticon.url !== '') {
res.set(emoticon.keyword, emoticon)
}
}
return res
}
},
created() {
this.wsConnect()
this.updateConfig()
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() {
this.isDestroying = true
this.websocket.close()
if (this.chatClient) {
this.chatClient.stop()
}
},
methods: {
updateConfig() {
initConfig() {
let locale = this.strConfig.lang
if (locale) {
i18n.setLocale(locale)
}
let cfg = {}
// 留空的使用默认值
for (let i in this.$route.query) {
if (this.$route.query[i] !== '') {
cfg[i] = this.$route.query[i]
for (let i in this.strConfig) {
if (this.strConfig[i] !== '') {
cfg[i] = this.strConfig[i]
}
}
cfg = mergeConfig(cfg, config.DEFAULT_CONFIG)
cfg = mergeConfig(cfg, chatConfig.deepCloneDefaultConfig())
cfg.minGiftPrice = toInt(cfg.minGiftPrice, config.DEFAULT_CONFIG.minGiftPrice)
cfg.mergeSimilarDanmaku = toBool(cfg.mergeSimilarDanmaku)
cfg.minGiftPrice = toInt(cfg.minGiftPrice, chatConfig.DEFAULT_CONFIG.minGiftPrice)
cfg.showDanmaku = toBool(cfg.showDanmaku)
cfg.showGift = toBool(cfg.showGift)
cfg.maxNumber = toInt(cfg.maxNumber, config.DEFAULT_CONFIG.maxNumber)
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, config.DEFAULT_CONFIG.blockLevel)
cfg.blockLevel = toInt(cfg.blockLevel, chatConfig.DEFAULT_CONFIG.blockLevel)
cfg.blockNewbie = toBool(cfg.blockNewbie)
cfg.blockNotMobileVerified = toBool(cfg.blockNotMobileVerified)
cfg.blockMedalLevel = toInt(cfg.blockMedalLevel, config.DEFAULT_CONFIG.blockMedalLevel)
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
},
wsConnect() {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
// 开发时使用localhost:12450
const host = process.env.NODE_ENV === 'development' ? 'localhost:12450' : window.location.host
const url = `${protocol}://${host}/chat`
this.websocket = new WebSocket(url)
this.websocket.onopen = this.onWsOpen
this.websocket.onclose = this.onWsClose
this.websocket.onmessage = this.onWsMessage
this.heartbeatTimerId = window.setInterval(this.sendHeartbeat, 10 * 1000)
},
sendHeartbeat() {
this.websocket.send(JSON.stringify({
cmd: COMMAND_HEARTBEAT
}))
},
onWsOpen() {
this.retryCount = 0
this.websocket.send(JSON.stringify({
cmd: COMMAND_JOIN_ROOM,
data: {
roomId: parseInt(this.$route.params.roomId)
}
}))
},
onWsClose() {
if (this.heartbeatTimerId) {
window.clearInterval(this.heartbeatTimerId)
this.heartbeatTimerId = null
toObjIfJson(str) {
if (typeof str !== 'string') {
return str
}
if (this.isDestroying) {
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
}
window.console.log(`掉线重连中${++this.retryCount}`)
this.wsConnect()
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)
},
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.mergeSimilarText(data.content)) {
break
}
message = {
id: `text_${this.nextId++}`,
type: constants.MESSAGE_TYPE_TEXT,
avatarUrl: data.avatarUrl,
time: time,
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
}
if (this.mergeSimilarGift(data.authorName, price)) {
break
}
message = {
id: `gift_${this.nextId++}`,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: data.avatarUrl,
authorName: data.authorName,
price: price,
time: time,
content: '' // 有了SC礼物不需要内容了
}
break
onAddGift(data) {
if (!this.config.showGift) {
return
}
case COMMAND_ADD_MEMBER:
if (!this.config.showGift || !this.filterNewMemberMessage(data)) {
break
}
message = {
id: `member_${this.nextId++}`,
type: constants.MESSAGE_TYPE_MEMBER,
avatarUrl: data.avatarUrl,
time: time,
authorName: data.authorName,
title: 'NEW MEMBER!',
content: `Welcome ${data.authorName}!`
}
break
case COMMAND_ADD_SUPER_CHAT:
if (!this.config.showGift || !this.filterSuperChatMessage(data)) {
break
}
if (data.price < this.config.minGiftPrice) { // 丢人
break
}
message = {
id: `sc_${data.id}`,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: data.avatarUrl,
authorName: data.authorName,
price: data.price,
time: time,
content: data.content.trim()
}
break
case COMMAND_DEL_SUPER_CHAT:
for (let id of data.ids) {
id = `sc_${id}`
this.$refs.renderer.delMessage(id)
}
break
let price = data.totalCoin / 1000
if (this.mergeSimilarGift(data.authorName, price, data.giftName, data.num)) {
return
}
if (message) {
this.$refs.renderer.addMessage(message)
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
@@ -206,35 +260,104 @@ export default {
} else if (this.config.blockMedalLevel > 0 && data.medalLevel < this.config.blockMedalLevel) {
return false
}
return this.filterSuperChatMessage(data)
return this.filterByContent(data.content) && this.filterByAuthorName(data.authorName)
},
filterSuperChatMessage(data) {
for (let keyword of this.blockKeywords) {
if (data.content.indexOf(keyword) !== -1) {
return false
}
}
return this.filterNewMemberMessage(data)
return this.filterByContent(data.content) && this.filterByAuthorName(data.authorName)
},
filterNewMemberMessage(data) {
for (let user of this.blockUsers) {
if (data.authorName === user) {
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.greedyMatch(remainContent) !== null) {
return false
}
}
return true
},
filterByAuthorName(authorName) {
return !this.blockUsersTrie.has(authorName)
},
mergeSimilarText(content) {
if (!this.config.mergeSimilarDanmaku) {
return false
}
return this.$refs.renderer.mergeSimilarText(content)
},
mergeSimilarGift(authorName, price) {
if (!this.config.mergeSimilarDanmaku) {
mergeSimilarGift(authorName, price, giftName, num) {
if (!this.config.mergeGift) {
return false
}
return this.$refs.renderer.mergeSimilarGift(authorName, price)
return this.$refs.renderer.mergeSimilarGift(authorName, price, giftName, num)
},
getPronunciation(text) {
if (this.pronunciationConverter === null) {
return ''
}
return this.pronunciationConverter.getPronunciation(text)
},
getRichContent(data) {
let richContent = []
// B站官方表情
if (data.emoticon !== null) {
richContent.push({
type: constants.CONTENT_TYPE_IMAGE,
text: data.content,
url: data.emoticon
})
return richContent
}
// 没有自定义表情,只能是文本
if (this.config.emoticons.length === 0) {
richContent.push({
type: constants.CONTENT_TYPE_TEXT,
text: data.content
})
return richContent
}
// 可能含有自定义表情,需要解析
let emoticonsTrie = this.emoticonsTrie
let startPos = 0
let pos = 0
while (pos < data.content.length) {
let remainContent = data.content.substring(pos)
let matchEmoticon = emoticonsTrie.greedyMatch(remainContent)
if (matchEmoticon === null) {
pos++
continue
}
// 加入之前的文本
if (pos !== startPos) {
richContent.push({
type: constants.CONTENT_TYPE_TEXT,
text: data.content.slice(startPos, pos)
})
}
// 加入表情
richContent.push({
type: constants.CONTENT_TYPE_IMAGE,
text: matchEmoticon.keyword,
url: matchEmoticon.url
})
pos += matchEmoticon.keyword.length
startPos = pos
}
// 加入尾部的文本
if (pos !== startPos) {
richContent.push({
type: constants.CONTENT_TYPE_TEXT,
text: data.content.slice(startPos, pos)
})
}
return richContent
}
}
}

View 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>

View 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>

View 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>

View 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
}

File diff suppressed because one or more lines are too long

View File

@@ -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 ref="exampleContainer" id="example-container">
<div id="fakebody">
<chat-renderer ref="renderer" :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,171 +47,100 @@
<script>
import _ from 'lodash'
import * as stylegen from './stylegen'
import * as 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,
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,
authorName: '',
title: 'NEW MEMBER!',
content: ''
}
let paidMessageTemplate = {
id: 0,
addTime: time,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
authorName: '',
price: 0,
time: time,
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: '少年Pi',
content: 'Welcome 少年Pi!'
},
{
...paidMessageTemplate,
id: nextId++,
authorName: '无火的残渣',
price: 66600,
content: 'Sent 小电视飞船x100'
},
{
...textMessageTemplate,
id: nextId++,
authorName: 'streamer主播',
authorType: constants.AUTHRO_TYPE_OWNER,
content: '老板大气,老板身体健康'
},
{
...paidMessageTemplate,
id: nextId++,
authorName: '夏色祭保護協会会長',
price: 30,
content: '言いたいことがあるんだよ!'
}
]
import Legacy from './Legacy'
import LineLike from './LineLike'
import Room from '@/views/Room'
export default {
name: 'StyleGenerator',
components: {
ChatRenderer
Legacy, LineLike, Room
},
data() {
let stylegenConfig = stylegen.getLocalConfig()
let result = stylegen.getStyle(stylegenConfig)
let styleElement = document.createElement('style')
document.head.appendChild(styleElement)
// 数据流:
// 输入框 --\
// 子组件 -> subComponentResults -> subComponentResult -> inputResult -> 防抖延迟0.5s后 -> debounceResult -> exampleCss
return {
FONTS: [...fonts.LOCAL_FONTS, ...fonts.NETWORK_FONTS],
// 子组件的结果
subComponentResults: {
legacy: '',
lineLike: ''
},
activeTab: 'legacy',
// 输入框的结果
inputResult: '',
// 防抖后延迟变化的结果
debounceResult: '',
form: {...stylegenConfig},
result,
exampleCss: result.replace(/^body\b/gm, '#fakebody'),
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')
}
},
watch: {
computedResult: _.debounce(function(val) {
this.result = val
stylegen.setLocalConfig(this.form)
subComponentResult(val) {
this.inputResult = val
},
inputResult: _.debounce(function(val) {
this.debounceResult = val
}, 500),
result(val) {
this.exampleCss = val.replace(/^body\b/gm, '#fakebody')
exampleCss(val) {
this.styleElement.innerText = val
}
},
mounted() {
this.$refs.renderer.addMessages(EXAMPLE_MESSAGES)
this.debounceResult = this.inputResult = this.subComponentResult
let observer = new MutationObserver(() => this.$refs.renderer.scrollToBottom())
observer.observe(this.$refs.exampleContainer, {attributes: true})
this.$parent.$el.addEventListener('scroll', this.onParentScroll)
},
beforeDestroy() {
this.$parent.$el.removeEventListener('scroll', this.onParentScroll)
document.head.removeChild(this.styleElement)
},
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
)
onParentScroll(event) {
if (document.body.clientWidth <= 992) {
this.exampleTop = 0
} else {
this.exampleTop = event.target.scrollTop
}
callback(res)
},
playAnimation() {
this.$refs.renderer.clearMessages()
this.$nextTick(() => this.$refs.renderer.addMessages(EXAMPLE_MESSAGES))
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}
this.$refs[this.activeTab].resetConfig()
this.inputResult = this.subComponentResult
}
}
}
</script>
<style scoped>
.el-form {
max-width: 800px;
}
#example-container {
position: fixed;
top: 30px;
left: calc(210px + 40px + (100vw - 210px - 40px) / 2);
width: calc((100vw - 210px - 40px) / 2 - 40px - 30px);
height: calc(100vh - 110px);
height: calc(100vh - 150px);
background-color: #444;
background-image:
@@ -378,11 +154,11 @@ 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;
@@ -390,8 +166,18 @@ export default {
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 {

View File

@@ -1,406 +0,0 @@
import {mergeConfig} from '@/utils'
import * as 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
}
const FALLBACK_FONTS = ', "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "\\5FAE\\8F6F\\96C5\\9ED1", SimHei, Arial, sans-serif'
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}"${FALLBACK_FONTS};
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}"${FALLBACK_FONTS};
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}"${FALLBACK_FONTS};
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}"${FALLBACK_FONTS};
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}"${FALLBACK_FONTS};
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}"${FALLBACK_FONTS};
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}"${FALLBACK_FONTS};
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}"${FALLBACK_FONTS};
}
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;
}`
}

26
frontend/vue.config.js Normal file
View File

@@ -0,0 +1,26 @@
const API_BASE_URL = 'http://localhost: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
View File

51
main.py
View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import argparse
import logging
import logging.handlers
import os
import webbrowser
@@ -11,20 +11,24 @@ import tornado.web
import api.chat
import api.main
import config
import models.avatar
import models.database
import services.avatar
import services.chat
import services.translate
import update
logger = logging.getLogger(__name__)
WEB_ROOT = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
routes = [
(r'/chat', api.chat.ChatHandler),
(r'/api/server_info', api.main.ServerInfoHandler),
(r'/api/emoticon', api.main.UploadEmoticonHandler),
(r'/((css|fonts|img|js|static)/.*)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
(r'/(favicon\.ico)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
(r'/.*', api.main.MainHandler, {'path': WEB_ROOT})
(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, 'default_filename': 'index.html'})
]
@@ -34,15 +38,16 @@ def main():
init_logging(args.debug)
config.init()
models.database.init(args.debug)
models.avatar.init()
api.chat.init()
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 = 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')
@@ -50,13 +55,22 @@ def 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 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):
app = tornado.web.Application(
@@ -65,13 +79,22 @@ def run_server(host, port, debug):
debug=debug,
autoreload=False
)
cfg = config.get_config()
try:
app.listen(port, host)
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:
url = 'http://localhost' if port == 80 else f'http://localhost:{port}'
url = 'http://localhost/' if port == 80 else f'http://localhost:{port}/'
# 防止更新版本后浏览器加载缓存
url += '?_v=' + update.VERSION
webbrowser.open(url)
logger.info('Server started: %s:%d', host, port)
tornado.ioloop.IOLoop.current().start()

11
models/bilibili.py Normal file
View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
import sqlalchemy
import models.database
class BilibiliUser(models.database.OrmBase):
__tablename__ = 'bilibili_users'
uid = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
avatar_url = sqlalchemy.Column(sqlalchemy.String(100))
update_time = sqlalchemy.Column(sqlalchemy.DateTime)

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import contextlib
from typing import *
import sqlalchemy.ext.declarative
@@ -9,26 +7,19 @@ import sqlalchemy.orm
import config
OrmBase = sqlalchemy.ext.declarative.declarative_base()
engine = None
DbSession: Optional[Type[sqlalchemy.orm.Session]] = None
_engine = None
_DbSession: Optional[Type[sqlalchemy.orm.Session]] = None
def init(debug):
def init(_debug):
cfg = config.get_config()
global engine, DbSession
engine = sqlalchemy.create_engine(cfg.database_url, echo=debug)
DbSession = sqlalchemy.orm.sessionmaker(bind=engine)
global _engine, _DbSession
# engine = sqlalchemy.create_engine(cfg.database_url, echo=debug)
_engine = sqlalchemy.create_engine(cfg.database_url)
_DbSession = sqlalchemy.orm.sessionmaker(bind=_engine)
OrmBase.metadata.create_all(engine)
OrmBase.metadata.create_all(_engine)
@contextlib.contextmanager
def get_session():
session = DbSession()
try:
yield session
except:
session.rollback()
raise
finally:
session.close()
return _DbSession()

View File

@@ -1,3 +1,5 @@
aiohttp==3.5.4
sqlalchemy==1.3.13
tornado==6.0.2
aiohttp==3.7.4
Brotli==1.0.9
pycryptodome==3.10.1
sqlalchemy==1.4.31
tornado==6.1.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 299 KiB

View File

@@ -1,15 +1,18 @@
# -*- coding: utf-8 -*-
import asyncio
import datetime
import logging
import re
from typing import *
import aiohttp
import sqlalchemy
import sqlalchemy.exc
import config
import models.bilibili as bl_models
import models.database
import utils.request
logger = logging.getLogger(__name__)
@@ -17,19 +20,31 @@ logger = logging.getLogger(__name__)
DEFAULT_AVATAR_URL = '//static.hdslb.com/images/member/noface.gif'
_main_event_loop = asyncio.get_event_loop()
_http_session = aiohttp.ClientSession()
# user_id -> avatar_url
_avatar_url_cache: Dict[int, str] = {}
# (user_id, future)
_fetch_task_queue = asyncio.Queue(15)
_last_fetch_failed_time: Optional[datetime.datetime] = None
# 正在获取头像的Futureuser_id -> Future
_uid_fetch_future_map: Dict[int, asyncio.Future] = {}
# 正在获取头像的user_id队列
_uid_queue_to_fetch: Optional[asyncio.Queue] = None
# 上次被B站ban时间
_last_fetch_banned_time: Optional[datetime.datetime] = None
def init():
cfg = config.get_config()
global _uid_queue_to_fetch
_uid_queue_to_fetch = asyncio.Queue(cfg.fetch_avatar_max_queue_size)
asyncio.ensure_future(_get_avatar_url_from_web_consumer())
async def get_avatar_url(user_id):
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):
avatar_url = get_avatar_url_from_memory(user_id)
if avatar_url is not None:
return avatar_url
@@ -52,7 +67,9 @@ def get_avatar_url_from_database(user_id) -> Awaitable[Optional[str]]:
def _do_get_avatar_url_from_database(user_id):
try:
with models.database.get_session() as session:
user = session.query(BilibiliUser).filter(BilibiliUser.uid == user_id).one_or_none()
user = session.query(bl_models.BilibiliUser).filter(
bl_models.BilibiliUser.uid == user_id
).one_or_none()
if user is None:
return None
avatar_url = user.avatar_url
@@ -60,10 +77,7 @@ def _do_get_avatar_url_from_database(user_id):
# 如果离上次更新太久就更新所有缓存
if (datetime.datetime.now() - user.update_time).days >= 3:
def refresh_cache():
try:
del _avatar_url_cache[user_id]
except KeyError:
pass
_avatar_url_cache.pop(user_id, None)
get_avatar_url_from_web(user_id)
_main_event_loop.call_soon(refresh_cache)
@@ -79,42 +93,47 @@ def _do_get_avatar_url_from_database(user_id):
return avatar_url
def get_avatar_url_from_web(user_id) -> Awaitable[str]:
future = _main_event_loop.create_future()
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
# 否则创建一个获取任务
_uid_fetch_future_map[user_id] = future = _main_event_loop.create_future()
future.add_done_callback(lambda _future: _uid_fetch_future_map.pop(user_id, None))
try:
_fetch_task_queue.put_nowait((user_id, future))
_uid_queue_to_fetch.put_nowait(user_id)
except asyncio.QueueFull:
future.set_result(DEFAULT_AVATAR_URL)
future.set_result(None)
return future
async def _get_avatar_url_from_web_consumer():
while True:
try:
user_id, future = await _fetch_task_queue.get()
# 先查缓存防止队列中出现相同uid时重复获取
avatar_url = get_avatar_url_from_memory(user_id)
if avatar_url is not None:
user_id = await _uid_queue_to_fetch.get()
future = _uid_fetch_future_map.get(user_id, None)
if future is None:
continue
# 防止在被ban的时候获取
global _last_fetch_failed_time
if _last_fetch_failed_time is not None:
global _last_fetch_banned_time
if _last_fetch_banned_time is not None:
cur_time = datetime.datetime.now()
if (cur_time - _last_fetch_failed_time).total_seconds() < 3 * 60 + 3:
# 3分钟以内被ban则先返回默认头像解封大约要15分钟
future.set_result(DEFAULT_AVATAR_URL)
if (cur_time - _last_fetch_banned_time).total_seconds() < 3 * 60 + 3:
# 3分钟以内被ban解封大约要15分钟
future.set_result(None)
continue
else:
_last_fetch_failed_time = None
_last_fetch_banned_time = None
asyncio.ensure_future(_get_avatar_url_from_web_coroutine(user_id, future))
# 限制频率防止被B站ban
await asyncio.sleep(0.2)
except:
pass
cfg = config.get_config()
await asyncio.sleep(cfg.fetch_avatar_interval)
except Exception: # noqa
logger.exception('_get_avatar_url_from_web_consumer error:')
async def _get_avatar_url_from_web_coroutine(user_id, future):
@@ -122,30 +141,39 @@ async def _get_avatar_url_from_web_coroutine(user_id, future):
avatar_url = await _do_get_avatar_url_from_web(user_id)
except BaseException as e:
future.set_exception(e)
return
future.set_result(avatar_url)
else:
future.set_result(avatar_url)
async def _do_get_avatar_url_from_web(user_id):
try:
async with _http_session.get('https://api.bilibili.com/x/space/acc/info',
params={'mid': user_id}) as r:
async with utils.request.http_session.get(
'https://api.bilibili.com/x/space/acc/info', params={'mid': user_id}
) as r:
if r.status != 200:
logger.warning('Failed to fetch avatar: status=%d %s uid=%d', r.status, r.reason, user_id)
if r.status == 412:
# 被B站ban了
global _last_fetch_failed_time
_last_fetch_failed_time = datetime.datetime.now()
return DEFAULT_AVATAR_URL
global _last_fetch_banned_time
_last_fetch_banned_time = datetime.datetime.now()
return None
data = await r.json()
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
return DEFAULT_AVATAR_URL
return None
avatar_url = data['data']['face'].replace('http:', '').replace('https:', '')
avatar_url = process_avatar_url(data['data']['face'])
update_avatar_cache(user_id, avatar_url)
return 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'
update_avatar_cache(user_id, avatar_url)
return avatar_url
@@ -158,32 +186,27 @@ def update_avatar_cache(user_id, avatar_url):
def _update_avatar_cache_in_memory(user_id, avatar_url):
_avatar_url_cache[user_id] = avatar_url
if len(_avatar_url_cache) > 50000:
for _, key in zip(range(100), _avatar_url_cache):
del _avatar_url_cache[key]
cfg = config.get_config()
while len(_avatar_url_cache) > cfg.avatar_cache_size:
_avatar_url_cache.pop(next(iter(_avatar_url_cache)), None)
def _update_avatar_cache_in_database(user_id, avatar_url):
try:
with models.database.get_session() as session:
user = session.query(BilibiliUser).filter(BilibiliUser.uid == user_id).one_or_none()
user = session.query(bl_models.BilibiliUser).filter(
bl_models.BilibiliUser.uid == user_id
).one_or_none()
if user is None:
user = BilibiliUser(uid=user_id, avatar_url=avatar_url,
update_time=datetime.datetime.now())
user = bl_models.BilibiliUser(
uid=user_id
)
session.add(user)
else:
user.avatar_url = avatar_url
user.update_time = datetime.datetime.now()
user.avatar_url = avatar_url
user.update_time = datetime.datetime.now()
session.commit()
except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.IntegrityError):
# SQLite会锁整个文件忽略就行另外还有多线程导致ID重复的问题
pass
except sqlalchemy.exc.SQLAlchemyError:
logger.exception('_update_avatar_cache_in_database failed:')
class BilibiliUser(models.database.OrmBase):
__tablename__ = 'bilibili_users'
uid = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
avatar_url = sqlalchemy.Column(sqlalchemy.Text)
update_time = sqlalchemy.Column(sqlalchemy.DateTime)

456
services/chat.py Normal file
View File

@@ -0,0 +1,456 @@
# -*- coding: utf-8 -*-
import asyncio
import logging
import uuid
from typing import *
import api.chat
import blivedm.blivedm as blivedm
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.ensure_future(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.ensure_future(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):
"""初始化参数传入的房间IDroom_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_event_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']
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],
msg=info[1],
uid=info[2][0],
uname=info[2][1],
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.ensure_future(self.__on_danmaku(client, message))
async def __on_danmaku(self, client: LiveClient, message: blivedm.DanmakuMessage):
# 先异步调用再获取房间,因为返回时房间可能已经不存在了
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
need_translate = 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,
))
if need_translate:
await self._translate_and_response(message.msg, room.room_id, msg_id)
async def _on_gift(self, client: LiveClient, message: blivedm.GiftMessage):
avatar_url = services.avatar.process_avatar_url(message.face)
# 服务器白给的头像URL直接缓存
services.avatar.update_avatar_cache(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.ensure_future(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)
# 服务器白给的头像URL直接缓存
services.avatar.update_avatar_cache(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.ensure_future(self._translate_and_response(message.message, room.room_id, msg_id))
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.ADD_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):
translation = await services.translate.translate(text)
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
)
)

615
services/translate.py Normal file
View File

@@ -0,0 +1,615 @@
# -*- coding: utf-8 -*-
import asyncio
import base64
import datetime
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 config
import utils.request
logger = logging.getLogger(__name__)
NO_TRANSLATE_TEXTS = {
'', '草草', '草草草', '草生', '大草原', '上手', '上手上手', '理解', '理解理解', '天才', '天才天才',
'', '余裕', '余裕余裕', '大丈夫', '再放送', '放送事故', '清楚', '清楚清楚'
}
_translate_providers: List['TranslateProvider'] = []
# text -> res
_translate_cache: Dict[str, str] = {}
# 正在翻译的Futuretext -> Future
_text_future_map: Dict[str, asyncio.Future] = {}
def init():
asyncio.ensure_future(_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['max_queue_size'], cfg['source_language'],
cfg['target_language']
)
elif type_ == 'BilibiliTranslateFree':
return BilibiliTranslateFree(cfg['query_interval'], cfg['max_queue_size'])
elif type_ == 'TencentTranslate':
return TencentTranslate(
cfg['query_interval'], cfg['max_queue_size'], cfg['source_language'],
cfg['target_language'], cfg['secret_id'], cfg['secret_key'],
cfg['region']
)
elif type_ == 'BaiduTranslate':
return BaiduTranslate(
cfg['query_interval'], cfg['max_queue_size'], 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) -> 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_event_loop().create_future()
# 查缓存
res = _translate_cache.get(key, None)
if res is not None:
future.set_result(res)
return future
# 负载均衡找等待时间最少的provider
min_wait_time = None
min_wait_time_provider = None
for provider in _translate_providers:
if not provider.is_available:
continue
wait_time = provider.wait_time
if min_wait_time is None or wait_time < min_wait_time:
min_wait_time = wait_time
min_wait_time_provider = provider
# 没有可用的
if min_wait_time_provider is None:
future.set_result(None)
return future
_text_future_map[key] = future
future.add_done_callback(functools.partial(_on_translate_done, key))
min_wait_time_provider.translate(text, future)
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
cfg = config.get_config()
while len(_translate_cache) > cfg.translation_cache_size:
_translate_cache.pop(next(iter(_translate_cache)), None)
class TranslateProvider:
async def init(self):
return True
@property
def is_available(self):
return True
@property
def wait_time(self):
return 0
def translate(self, text, future):
raise NotImplementedError
class FlowControlTranslateProvider(TranslateProvider):
def __init__(self, query_interval, max_queue_size):
self._query_interval = query_interval
# (text, future)
self._text_queue = asyncio.Queue(max_queue_size)
async def init(self):
asyncio.ensure_future(self._translate_consumer())
return True
@property
def is_available(self):
return not self._text_queue.full()
@property
def wait_time(self):
return self._text_queue.qsize() * self._query_interval
def translate(self, text, future):
try:
self._text_queue.put_nowait((text, future))
except asyncio.QueueFull:
future.set_result(None)
async def _translate_consumer(self):
while True:
try:
text, future = await self._text_queue.get()
asyncio.ensure_future(self._translate_coroutine(text, future))
# 频率限制
await asyncio.sleep(self._query_interval)
except Exception: # noqa
logger.exception('FlowControlTranslateProvider error:')
async def _translate_coroutine(self, text, future):
try:
res = await self._do_translate(text)
except BaseException as e:
future.set_exception(e)
else:
future.set_result(res)
async def _do_translate(self, text):
raise NotImplementedError
class TencentTranslateFree(FlowControlTranslateProvider):
def __init__(self, query_interval, max_queue_size, source_language, target_language):
super().__init__(query_interval, max_queue_size)
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
if not await self._do_init():
return False
self._reinit_future = asyncio.ensure_future(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.ClientConnectionError, 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.ClientConnectionError, 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
return True
async def _reinit_coroutine(self):
try:
while True:
await asyncio.sleep(30)
logger.debug('TencentTranslateFree reinit')
asyncio.ensure_future(self._do_init())
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_coroutine(self, text, future):
try:
res = await self._do_translate(text)
except BaseException as e:
future.set_exception(e)
self._on_fail()
return
future.set_result(res)
if res is None:
self._on_fail()
else:
self._fail_count = 0
async def _do_translate(self, text):
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.ClientConnectionError, 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.warning('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
# 为了可靠性连续失败10次时冷却直到下次重新init
if self._fail_count >= 10:
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
class BilibiliTranslateFree(FlowControlTranslateProvider):
def __init__(self, query_interval, max_queue_size):
super().__init__(query_interval, max_queue_size)
async def _do_translate(self, text):
try:
async with utils.request.http_session.get(
'https://api.live.bilibili.com/av/v1/SuperChat/messageTranslate',
params={
'room_id': '21396545',
'ruid': '407106379',
'parent_area_id': '9',
'area_id': '371',
'msg': text
}
) as r:
if r.status != 200:
logger.warning('BilibiliTranslateFree request failed: status=%d %s', r.status, r.reason)
return None
data = await r.json()
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
return None
if data['code'] != 0:
logger.warning('BilibiliTranslateFree failed: %d %s', data['code'], data['msg'])
return None
return data['data']['message_trans']
class TencentTranslate(FlowControlTranslateProvider):
def __init__(self, query_interval, max_queue_size, source_language, target_language,
secret_id, secret_key, region):
super().__init__(query_interval, max_queue_size)
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):
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_event_loop().call_later(
sleep_time, self._on_cool_down_timeout
)
def _on_cool_down_timeout(self):
self._cool_down_timer_handle = None
class BaiduTranslate(FlowControlTranslateProvider):
def __init__(self, query_interval, max_queue_size, source_language, target_language,
app_id, secret):
super().__init__(query_interval, max_queue_size)
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):
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_event_loop().call_later(
sleep_time, self._on_cool_down_timeout
)
def _on_cool_down_timeout(self):
self._cool_down_timer_handle = None

View File

@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
import asyncio
import aiohttp
VERSION = 'v1.3.0'
import utils.request
VERSION = 'v1.6.0'
def check_update():
@@ -13,12 +14,17 @@ def check_update():
async def _do_check_update():
try:
async with aiohttp.ClientSession() as session:
async with session.get('https://api.github.com/repos/xfgryujk/blivechat/releases/latest') as r:
data = await r.json()
if data['name'] != VERSION:
print('New version available:', data['name'])
print(data['body'])
print('Download:', data['html_url'])
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')

4
utils/request.py Normal file
View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
import aiohttp
http_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))