mirror of
https://github.com/xfgryujk/blivechat.git
synced 2026-08-24 04:13:27 +08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60e6b5ec99 | ||
|
|
695a111c9a | ||
|
|
0160549dbe | ||
|
|
3fd0d9ab28 | ||
|
|
b1061ae103 | ||
|
|
6e737ce33f | ||
|
|
10db6b9ae2 | ||
|
|
1514bfed5b | ||
|
|
dc264d40f0 | ||
|
|
104e647f61 | ||
|
|
d5d7fdde6d | ||
|
|
23aa88d564 | ||
|
|
ac1bda0392 | ||
|
|
5cdb786966 | ||
|
|
2b5ca2e219 | ||
|
|
fc77b246c2 | ||
|
|
083832cb3e | ||
|
|
804db2fec1 | ||
|
|
769964d5db | ||
|
|
aba0efca34 | ||
|
|
9ff18fe8b5 | ||
|
|
ee78cbf90e | ||
|
|
ffc3c9d8a0 | ||
|
|
a69b6b6406 | ||
|
|
1252402e5e | ||
|
|
53be587189 | ||
|
|
d3cc8b0c77 | ||
|
|
4f4d117ce0 | ||
|
|
0d1e38346e | ||
|
|
273dc0f43f | ||
|
|
26f19b6630 | ||
|
|
f2d2528afe | ||
|
|
d0fd997415 | ||
|
|
7086acb001 | ||
|
|
2914a19288 | ||
|
|
64cca50982 | ||
|
|
702bd56869 | ||
|
|
1eedee1119 | ||
|
|
b95ef95cfd | ||
|
|
01a3ec367e |
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@@ -0,0 +1,16 @@
|
||||
# compiled output
|
||||
**/__pycache__/
|
||||
build/
|
||||
**/dist/
|
||||
|
||||
# dependencies
|
||||
**/node_modules/
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
|
||||
# misc
|
||||
**/.git*
|
||||
*.spec
|
||||
screenshots/
|
||||
README.md
|
||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# 运行时
|
||||
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
|
||||
|
||||
# 前端依赖
|
||||
WORKDIR /blivechat/frontend
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm i --registry=https://registry.npm.taobao.org
|
||||
|
||||
# 编译
|
||||
COPY . /blivechat
|
||||
RUN npm run build
|
||||
|
||||
# 运行
|
||||
WORKDIR /blivechat
|
||||
EXPOSE 12450
|
||||
ENTRYPOINT ["python3", "main.py"]
|
||||
CMD ["--host", "0.0.0.0", "--port", "12450"]
|
||||
23
README.md
23
README.md
@@ -19,27 +19,34 @@
|
||||
1. 下载[发布版](https://github.com/xfgryujk/blivechat/releases)(仅提供x64 Windows版)
|
||||
2. 双击`blivechat.exe`运行服务器,或者用命令行可以指定host和端口号:
|
||||
```bat
|
||||
blivechat.exe --host 127.0.0.1 --port 80
|
||||
blivechat.exe --host 127.0.0.1 --port 12450
|
||||
```
|
||||
3. 用浏览器打开[http://localhost](http://localhost),输入房间ID,进入房间,复制房间URL
|
||||
4. (可选)用样式生成器生成样式,复制CSS
|
||||
3. 用浏览器打开[http://localhost:12450](http://localhost:12450),输入房间ID,保存配置,复制房间URL
|
||||
4. 用样式生成器生成样式,复制CSS
|
||||
5. 在OBS中添加浏览器源,输入URL和自定义CSS,或者可以在首页的样式设置里输入CSS
|
||||
|
||||
### 源代码版
|
||||
1. 编译前端(需要安装NPM):
|
||||
1. 编译前端(需要安装Node.js和npm):
|
||||
```sh
|
||||
cd frontend
|
||||
npm i
|
||||
npm run build
|
||||
```
|
||||
2. 运行服务器:
|
||||
2. 运行服务器(需要Python3.6以上版本):
|
||||
```sh
|
||||
pip3 install -r requirements.txt
|
||||
python3 main.py
|
||||
```
|
||||
或者可以指定host和端口号:
|
||||
```sh
|
||||
python3 main.py --host 127.0.0.1 --port 80
|
||||
python3 main.py --host 127.0.0.1 --port 12450
|
||||
```
|
||||
3. 用浏览器打开[http://localhost](http://localhost),输入房间ID,进入房间,复制房间URL
|
||||
4. (可选)用样式生成器生成样式,复制CSS
|
||||
3. 用浏览器打开[http://localhost:12450](http://localhost:12450),输入房间ID,保存配置,复制房间URL
|
||||
4. 用样式生成器生成样式,复制CSS
|
||||
5. 在OBS中添加浏览器源,输入URL和自定义CSS,或者可以在首页的样式设置里输入CSS
|
||||
|
||||
### Docker
|
||||
1. ```sh
|
||||
docker run -d -p 12450:12450 xfgryujk/blivechat:latest
|
||||
```
|
||||
2. 以下略
|
||||
|
||||
2
blivedm
2
blivedm
Submodule blivedm updated: 003d89e976...1613238420
24493
frontend/package-lock.json
generated
24493
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,9 @@
|
||||
"core-js": "^2.6.5",
|
||||
"downloadjs": "^1.4.7",
|
||||
"element-ui": "^2.9.1",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash": "^4.17.13",
|
||||
"vue": "^2.6.10",
|
||||
"vue-i18n": "^8.11.2",
|
||||
"vue-router": "^3.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import {mergeConfig} from '@/utils'
|
||||
import stylegen from '@/views/StyleGenerator/stylegen'
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
minGiftPrice: 6.911, // $1
|
||||
minGiftPrice: 7, // $1
|
||||
mergeSimilarDanmaku: true,
|
||||
showDanmaku: true,
|
||||
showGift: true,
|
||||
maxSpeed: 4,
|
||||
maxNumber: 60,
|
||||
|
||||
blockGiftDanmaku: true,
|
||||
blockLevel: 0,
|
||||
@@ -18,7 +18,7 @@ export const DEFAULT_CONFIG = {
|
||||
blockUsers: '',
|
||||
blockMedalLevel: 0,
|
||||
|
||||
css: stylegen.getStyle(stylegen.DEFAULT_CONFIG)
|
||||
css: ''
|
||||
}
|
||||
|
||||
export function setLocalConfig (config) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div id="event-text" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{title}}</div>
|
||||
<div id="detail-text" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{content}}</div>
|
||||
</div>
|
||||
<div id="timestamp" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{time}}</div>
|
||||
<div 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">
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
<script>
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import utils from '@/utils'
|
||||
|
||||
export default {
|
||||
name: 'LegacyPaidMessage',
|
||||
@@ -32,7 +33,12 @@ export default {
|
||||
authorName: String,
|
||||
title: String,
|
||||
content: String,
|
||||
time: String
|
||||
time: Date
|
||||
},
|
||||
computed: {
|
||||
timeText() {
|
||||
return utils.getTimeTextMinSec(this.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
<div id="header-content" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="header-content-primary-column" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="author-name" class="style-scope yt-live-chat-paid-message-renderer">{{authorName}}</div>
|
||||
<div id="purchase-amount" class="style-scope yt-live-chat-paid-message-renderer">CN¥{{price}}</div>
|
||||
<div id="purchase-amount" class="style-scope yt-live-chat-paid-message-renderer">{{priceText}}</div>
|
||||
</div>
|
||||
<span id="timestamp" class="style-scope yt-live-chat-paid-message-renderer">{{time}}</span>
|
||||
<span id="timestamp" class="style-scope yt-live-chat-paid-message-renderer">{{timeText}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
<div id="content" class="style-scope yt-live-chat-paid-message-renderer" v-show="content">
|
||||
<div id="message" dir="auto" class="style-scope yt-live-chat-paid-message-renderer">
|
||||
{{content}}
|
||||
</div>
|
||||
@@ -33,6 +33,7 @@
|
||||
<script>
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import * as constants from './constants'
|
||||
import utils from '@/utils'
|
||||
|
||||
export default {
|
||||
name: 'PaidMessage',
|
||||
@@ -43,12 +44,18 @@ export default {
|
||||
avatarUrl: String,
|
||||
authorName: String,
|
||||
price: Number, // 价格,人民币
|
||||
time: String,
|
||||
time: Date,
|
||||
content: String
|
||||
},
|
||||
computed: {
|
||||
color() {
|
||||
return constants.getPriceConfig(this.price).colors
|
||||
},
|
||||
priceText() {
|
||||
return 'CN¥' + utils.formatCurrency(this.price)
|
||||
},
|
||||
timeText() {
|
||||
return utils.getTimeTextMinSec(this.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:imgUrl="avatarUrl"
|
||||
></img-shadow>
|
||||
<div id="content" class="style-scope yt-live-chat-text-message-renderer">
|
||||
<span id="timestamp" class="style-scope yt-live-chat-text-message-renderer">{{time}}</span>
|
||||
<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}}
|
||||
@@ -29,9 +29,11 @@
|
||||
import ImgShadow from './ImgShadow.vue'
|
||||
import AuthorBadge from './AuthorBadge.vue'
|
||||
import * as constants from './constants'
|
||||
import utils from '@/utils'
|
||||
|
||||
const REPEATED_MARK_COLOR_START = [64, 158, 255]
|
||||
const REPEATED_MARK_COLOR_END = [245, 108, 108]
|
||||
// HSL
|
||||
const REPEATED_MARK_COLOR_START = [210, 100.0, 62.5]
|
||||
const REPEATED_MARK_COLOR_END = [360, 87.3, 69.2]
|
||||
|
||||
export default {
|
||||
name: 'TextMessage',
|
||||
@@ -41,7 +43,7 @@ export default {
|
||||
},
|
||||
props: {
|
||||
avatarUrl: String,
|
||||
time: String,
|
||||
time: Date,
|
||||
authorName: String,
|
||||
authorType: Number,
|
||||
content: String,
|
||||
@@ -49,6 +51,9 @@ export default {
|
||||
repeated: Number
|
||||
},
|
||||
computed: {
|
||||
timeText() {
|
||||
return utils.getTimeTextMinSec(this.time)
|
||||
},
|
||||
authorTypeText() {
|
||||
return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
|
||||
},
|
||||
@@ -65,7 +70,7 @@ export default {
|
||||
color[i] = REPEATED_MARK_COLOR_START[i] + (REPEATED_MARK_COLOR_END[i] - REPEATED_MARK_COLOR_START[i]) * t
|
||||
}
|
||||
}
|
||||
return `rgb(${color.join(', ')})`
|
||||
return `hsl(${color[0]}, ${color[1]}%, ${color[2]}%)`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import ImgShadow from './ImgShadow.vue'
|
||||
import LegacyPaidMessage from './LegacyPaidMessage.vue'
|
||||
import PaidMessage from './PaidMessage.vue'
|
||||
import * as constants from './constants'
|
||||
import {formatCurrency} from '@/utils'
|
||||
|
||||
export default {
|
||||
name: 'Ticker',
|
||||
@@ -106,9 +107,9 @@ export default {
|
||||
},
|
||||
getText(message) {
|
||||
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
|
||||
return '会员'
|
||||
return 'Member'
|
||||
}
|
||||
return `CN¥${message.price}`
|
||||
return 'CN¥' + formatCurrency(message.price)
|
||||
},
|
||||
updateProgress() {
|
||||
let curTime = new Date()
|
||||
|
||||
@@ -19,11 +19,13 @@ export const GUARD_LEVEL_TO_TEXT = [
|
||||
|
||||
export const MESSAGE_TYPE_TEXT = 0
|
||||
export const MESSAGE_TYPE_MEMBER = 1
|
||||
export const MESSAGE_TYPE_GIFT = 2
|
||||
export const MESSAGE_TYPE_SUPER_CHAT = 2
|
||||
|
||||
// 美元 -> 人民币 汇率
|
||||
const EXCHANGE_RATE = 7
|
||||
export const PRICE_CONFIGS = [
|
||||
{ // $100红
|
||||
price: 1245, // >= 1245,小电视飞船
|
||||
price: 100 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(230,33,23,1)',
|
||||
headerBg: 'rgba(208,0,0,1)',
|
||||
@@ -35,7 +37,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 60
|
||||
},
|
||||
{ // $50品红
|
||||
price: 450, // 450~1245,摩天大楼
|
||||
price: 50 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(233,30,99,1)',
|
||||
headerBg: 'rgba(194,24,91,1)',
|
||||
@@ -47,7 +49,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 30
|
||||
},
|
||||
{ // $20橙
|
||||
price: 100, // 100~450,节奏风暴、天空之翼
|
||||
price: 20 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(245,124,0,1)',
|
||||
headerBg: 'rgba(230,81,0,1)',
|
||||
@@ -59,7 +61,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 10
|
||||
},
|
||||
{ // $10黄
|
||||
price: 52, // 52~100,疯狂打call
|
||||
price: 10 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(255,202,40,1)',
|
||||
headerBg: 'rgba(255,179,0,1)',
|
||||
@@ -71,7 +73,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 5
|
||||
},
|
||||
{ // $5绿
|
||||
price: 28, // 28~52,礼花
|
||||
price: 5 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(29,233,182,1)',
|
||||
headerBg: 'rgba(0,191,165,1)',
|
||||
@@ -83,7 +85,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 2
|
||||
},
|
||||
{ // $2浅蓝
|
||||
price: 9.9, // 9.9~28,B坷垃
|
||||
price: 2 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(0,229,255,1)',
|
||||
headerBg: 'rgba(0,184,212,1)',
|
||||
@@ -95,7 +97,7 @@ export const PRICE_CONFIGS = [
|
||||
pinTime: 0
|
||||
},
|
||||
{ // $1蓝
|
||||
price: 0, // 0~9.9,丢人
|
||||
price: 1 * EXCHANGE_RATE,
|
||||
colors: {
|
||||
contentBg: 'rgba(30,136,229,1)',
|
||||
headerBg: 'rgba(21,101,192,1)',
|
||||
|
||||
110
frontend/src/lang/en.js
Normal file
110
frontend/src/lang/en.js
Normal file
@@ -0,0 +1,110 @@
|
||||
export default {
|
||||
sidebar: {
|
||||
home: 'Home',
|
||||
stylegen: 'Style generator',
|
||||
projectAddress: 'Project address'
|
||||
},
|
||||
home: {
|
||||
roomIdEmpty: "Room ID can't be empty",
|
||||
roomIdInteger: 'Room ID must be positive integer',
|
||||
|
||||
general: 'General',
|
||||
roomId: 'Room ID',
|
||||
showDanmaku: 'Show messages',
|
||||
showGift: 'Show Super Chats',
|
||||
mergeSimilarDanmaku: 'Merge similar messages',
|
||||
minGiftPrice: 'Min price of Super Chats to show (CNY)',
|
||||
maxSpeed: 'Max speed of messages (/second, 0 for unlimited)',
|
||||
maxNumber: 'Max number of messages',
|
||||
|
||||
block: 'Block',
|
||||
giftDanmaku: 'Block system messages (gift effect)',
|
||||
blockLevel: 'Block user level lower than',
|
||||
informalUser: 'Block informal users',
|
||||
unverifiedUser: 'Block unverified users',
|
||||
blockKeywords: 'Block keywords',
|
||||
onePerLine: 'One per line',
|
||||
blockUsers: 'Block users',
|
||||
blockMedalLevel: 'Block medal level lower than',
|
||||
|
||||
style: 'Style',
|
||||
|
||||
roomUrl: 'Room URL',
|
||||
copy: 'Copy',
|
||||
saveConfig: 'Save config',
|
||||
enterRoom: 'Enter room',
|
||||
exportConfig: 'Export config',
|
||||
importConfig: 'Import config',
|
||||
|
||||
failedToSave: 'Failed to save: ',
|
||||
successfullySaved: 'Successfully saved',
|
||||
failedToParseConfig: 'Failed to parse config: '
|
||||
},
|
||||
stylegen: {
|
||||
outlines: 'Outlines',
|
||||
showOutlines: 'Show outlines',
|
||||
outlineSize: 'Outline size',
|
||||
outlineColor: 'Outline color',
|
||||
|
||||
avatars: 'Avatars',
|
||||
showAvatars: 'Show avatars',
|
||||
avatarSize: 'Avatar size',
|
||||
|
||||
userNames: 'User names',
|
||||
font: 'Font',
|
||||
fontSize: 'Font size',
|
||||
lineHeight: 'Line height (0 for default)',
|
||||
normalColor: 'Normal color',
|
||||
ownerColor: 'Owner color',
|
||||
moderatorColor: 'Moderator color',
|
||||
memberColor: 'Member color',
|
||||
showBadges: 'Show badges',
|
||||
showColon: 'Show colon after name',
|
||||
|
||||
messages: 'Messages',
|
||||
color: 'Color',
|
||||
onNewLine: 'On new line',
|
||||
|
||||
time: 'Timestamps',
|
||||
showTime: 'Show timestamps',
|
||||
|
||||
backgrounds: 'Backgrounds',
|
||||
bgColor: 'Background color',
|
||||
useBarsInsteadOfBg: 'Use bars instead of backgrounds',
|
||||
messageBgColor: 'Message background color',
|
||||
ownerMessageBgColor: 'Owner background color',
|
||||
moderatorMessageBgColor: 'Moderator background color',
|
||||
memberMessageBgColor: 'Member background color',
|
||||
|
||||
scAndNewMember: 'Super Chat / New member',
|
||||
firstLineFont: 'First line font',
|
||||
firstLineFontSize: 'First line font size',
|
||||
firstLineLineHeight: 'First line line height (0 for default)',
|
||||
firstLineColor: 'First line color',
|
||||
secondLineFont: 'Second line font',
|
||||
secondLineFontSize: 'Second line font size',
|
||||
secondLineLineHeight: 'Second line line height (0 for default)',
|
||||
secondLineColor: 'Second line color',
|
||||
scContentLineFont: 'Super Chat content font',
|
||||
scContentLineFontSize: 'Super Chat content font size',
|
||||
scContentLineLineHeight: 'Super Chat content line height (0 for default)',
|
||||
scContentLineColor: 'Super Chat content color',
|
||||
showNewMemberBg: 'Show new member background',
|
||||
showScTicker: 'Show Super Chat ticker',
|
||||
showOtherThings: 'Show everything other than Super Chat ticker',
|
||||
|
||||
animation: 'Animation',
|
||||
animateIn: 'Animate in',
|
||||
fadeInTime: 'Fade in time (miliseconds)',
|
||||
animateOut: 'Animate out (remove old messages)',
|
||||
animateOutWaitTime: 'Wait time (seconds)',
|
||||
fadeOutTime: 'Fade out time (miliseconds)',
|
||||
slide: 'Slide',
|
||||
reverseSlide: 'Reverse slide',
|
||||
playAnimation: 'Play animation',
|
||||
|
||||
result: 'Result',
|
||||
copy: 'Copy',
|
||||
resetConfig: 'Reset config'
|
||||
}
|
||||
}
|
||||
110
frontend/src/lang/ja.js
Normal file
110
frontend/src/lang/ja.js
Normal file
@@ -0,0 +1,110 @@
|
||||
export default {
|
||||
sidebar: {
|
||||
home: 'トップページ',
|
||||
stylegen: 'スタイルジェネレータ',
|
||||
projectAddress: 'プロジェクトアドレス'
|
||||
},
|
||||
home: {
|
||||
roomIdEmpty: 'ルームのIDを空白にすることはできません',
|
||||
roomIdInteger: 'ルームは正の整数でなければなりません',
|
||||
|
||||
general: '常規',
|
||||
roomId: 'ルームID',
|
||||
showDanmaku: 'コメントを表示する',
|
||||
showGift: 'スーパーチャットと新メンバーを表示する',
|
||||
mergeSimilarDanmaku: '同じコメントを合併する',
|
||||
minGiftPrice: '最低表示スーパーチャット価格(CNY)',
|
||||
maxSpeed: 'コメントの最大速度(バー/秒、0は無制限)',
|
||||
maxNumber: '最大コメント数',
|
||||
|
||||
block: 'ブロック',
|
||||
giftDanmaku: 'ブロックシステムコメント(プレゼント効果)',
|
||||
blockLevel: 'ブロックユーザーレベルがx未満',
|
||||
informalUser: 'ブロック非公式ユーザー',
|
||||
unverifiedUser: 'ブロック認証されていないユーザー',
|
||||
blockKeywords: 'ブロックキーワード',
|
||||
onePerLine: '1行に1つずつ',
|
||||
blockUsers: 'ブロックユーザー',
|
||||
blockMedalLevel: 'ブロック勲章等級がx未満',
|
||||
|
||||
style: 'スタイル',
|
||||
|
||||
roomUrl: 'ルームのURL',
|
||||
copy: 'コピー',
|
||||
saveConfig: 'コンフィグを保存する',
|
||||
enterRoom: 'ルームに入る',
|
||||
exportConfig: 'コンフィグの導出',
|
||||
importConfig: 'コンフィグの導入',
|
||||
|
||||
failedToSave: '保存に失敗しました:',
|
||||
successfullySaved: '保存に成功しました',
|
||||
failedToParseConfig: 'コンフィグ解析に失敗しました'
|
||||
},
|
||||
stylegen: {
|
||||
outlines: 'アウトライン',
|
||||
showOutlines: 'アウトラインを表示する',
|
||||
outlineSize: 'アウトラインのサイズ',
|
||||
outlineColor: 'アウトラインの色',
|
||||
|
||||
avatars: 'アイコン',
|
||||
showAvatars: 'アイコンを表示する',
|
||||
avatarSize: 'アイコンのサイズ',
|
||||
|
||||
userNames: 'ユーザー名',
|
||||
font: 'フォント',
|
||||
fontSize: 'フォントサイズ',
|
||||
lineHeight: '行の高さ(0はデフォルト)',
|
||||
normalColor: 'ノーマルの色',
|
||||
ownerColor: 'オーナーの色',
|
||||
moderatorColor: '管理者の色',
|
||||
memberColor: 'メンバーの色',
|
||||
showBadges: '勲章を見せる',
|
||||
showColon: 'ユーザー名の後にコロンが表示されます',
|
||||
|
||||
messages: 'コメント',
|
||||
color: '色',
|
||||
onNewLine: '新しい行に表示する',
|
||||
|
||||
time: '時間',
|
||||
showTime: '時間を表示する',
|
||||
|
||||
backgrounds: '背景',
|
||||
bgColor: '背景色',
|
||||
useBarsInsteadOfBg: '背景に代わります',
|
||||
messageBgColor: 'コメント背景色',
|
||||
ownerMessageBgColor: 'オーナーコメント背景色',
|
||||
moderatorMessageBgColor: '管理者コメント背景色',
|
||||
memberMessageBgColor: 'メンバーコメント背景色',
|
||||
|
||||
scAndNewMember: 'スーパーチャット、新メンバー',
|
||||
firstLineFont: '1行目のフォント',
|
||||
firstLineFontSize: '1行目のフォントサイズ',
|
||||
firstLineLineHeight: '1行目の高さ(0はデフォルト)',
|
||||
firstLineColor: '1行目の色',
|
||||
secondLineFont: '2行目のフォント',
|
||||
secondLineFontSize: '2行目のフォントサイズ',
|
||||
secondLineLineHeight: '2行目の高さ(0はデフォルト)',
|
||||
secondLineColor: '2行目の色',
|
||||
scContentLineFont: 'スーパーチャットのコンテンツフォント',
|
||||
scContentLineFontSize: 'スーパーチャットコンテンツフォントサイズ',
|
||||
scContentLineLineHeight: 'スーパーチャットコンテンツライン高さ(0がデフォルト)',
|
||||
scContentLineColor: 'スーパーチャットコンテンツライン色',
|
||||
showNewMemberBg: '新メンバーの背景を表示する',
|
||||
showScTicker: 'スーパーチャットチカーの表示',
|
||||
showOtherThings: 'スーパーチャットチカー以外のコンテンツを表示します',
|
||||
|
||||
animation: 'アニメーション',
|
||||
animateIn: '入場アニメーション',
|
||||
fadeInTime: 'フェードイン時間(ミリ秒)',
|
||||
animateOut: '古いコメントを除去する',
|
||||
animateOutWaitTime: '待ち時間(秒)',
|
||||
fadeOutTime: 'フェードアウト時間(ミリ秒)',
|
||||
slide: '滑る',
|
||||
reverseSlide: '逆の滑る',
|
||||
playAnimation: 'アニメーションを再生する',
|
||||
|
||||
result: '結果',
|
||||
copy: 'コピー',
|
||||
resetConfig: 'デフォルトに戻す'
|
||||
}
|
||||
}
|
||||
110
frontend/src/lang/zh.js
Normal file
110
frontend/src/lang/zh.js
Normal file
@@ -0,0 +1,110 @@
|
||||
export default {
|
||||
sidebar: {
|
||||
home: '首页',
|
||||
stylegen: '样式生成器',
|
||||
projectAddress: '项目地址'
|
||||
},
|
||||
home: {
|
||||
roomIdEmpty: '房间ID不能为空',
|
||||
roomIdInteger: '房间ID必须为正整数',
|
||||
|
||||
general: '常规',
|
||||
roomId: '房间ID',
|
||||
showDanmaku: '显示弹幕',
|
||||
showGift: '显示礼物和新舰长',
|
||||
mergeSimilarDanmaku: '合并相似弹幕',
|
||||
minGiftPrice: '最低显示礼物价格(元)',
|
||||
maxSpeed: '弹幕最大速度(条/秒,0为无限制)',
|
||||
maxNumber: '最大弹幕数',
|
||||
|
||||
block: '屏蔽',
|
||||
giftDanmaku: '屏蔽礼物弹幕',
|
||||
blockLevel: '屏蔽用户等级低于',
|
||||
informalUser: '屏蔽非正式会员',
|
||||
unverifiedUser: '屏蔽未绑定手机用户',
|
||||
blockKeywords: '屏蔽关键词',
|
||||
onePerLine: '一行一个',
|
||||
blockUsers: '屏蔽用户',
|
||||
blockMedalLevel: '屏蔽当前直播间勋章等级低于',
|
||||
|
||||
style: '样式',
|
||||
|
||||
roomUrl: '房间URL',
|
||||
copy: '复制',
|
||||
saveConfig: '保存配置',
|
||||
enterRoom: '进入房间',
|
||||
exportConfig: '导出配置',
|
||||
importConfig: '导入配置',
|
||||
|
||||
failedToSave: '保存失败:',
|
||||
successfullySaved: '保存成功',
|
||||
failedToParseConfig: '配置解析失败:'
|
||||
},
|
||||
stylegen: {
|
||||
outlines: '描边',
|
||||
showOutlines: '显示描边',
|
||||
outlineSize: '描边尺寸',
|
||||
outlineColor: '描边颜色',
|
||||
|
||||
avatars: '头像',
|
||||
showAvatars: '显示头像',
|
||||
avatarSize: '头像尺寸',
|
||||
|
||||
userNames: '用户名',
|
||||
font: '字体',
|
||||
fontSize: '字体尺寸',
|
||||
lineHeight: '行高(0为默认)',
|
||||
normalColor: '普通颜色',
|
||||
ownerColor: '主播颜色',
|
||||
moderatorColor: '房管颜色',
|
||||
memberColor: '舰长颜色',
|
||||
showBadges: '显示勋章',
|
||||
showColon: '用户名后显示冒号',
|
||||
|
||||
messages: '消息',
|
||||
color: '颜色',
|
||||
onNewLine: '在新行显示',
|
||||
|
||||
time: '时间',
|
||||
showTime: '显示时间',
|
||||
|
||||
backgrounds: '背景',
|
||||
bgColor: '背景色',
|
||||
useBarsInsteadOfBg: '用条代替背景',
|
||||
messageBgColor: '消息背景色',
|
||||
ownerMessageBgColor: '主播消息背景色',
|
||||
moderatorMessageBgColor: '房管消息背景色',
|
||||
memberMessageBgColor: '舰长消息背景色',
|
||||
|
||||
scAndNewMember: '礼物、舰长',
|
||||
firstLineFont: '第一行字体',
|
||||
firstLineFontSize: '第一行字体尺寸',
|
||||
firstLineLineHeight: '第一行行高(0为默认)',
|
||||
firstLineColor: '第一行颜色',
|
||||
secondLineFont: '第二行字体',
|
||||
secondLineFontSize: '第二行字体尺寸',
|
||||
secondLineLineHeight: '第二行行高(0为默认)',
|
||||
secondLineColor: '第二行颜色',
|
||||
scContentLineFont: 'Super Chat内容字体',
|
||||
scContentLineFontSize: 'Super Chat内容字体尺寸',
|
||||
scContentLineLineHeight: 'Super Chat内容行高(0为默认)',
|
||||
scContentLineColor: 'Super Chat内容颜色',
|
||||
showNewMemberBg: '显示新舰长背景',
|
||||
showScTicker: '显示Super Chat贴纸',
|
||||
showOtherThings: '显示Super Chat贴纸之外的内容',
|
||||
|
||||
animation: '动画',
|
||||
animateIn: '进入动画',
|
||||
fadeInTime: '淡入时间(毫秒)',
|
||||
animateOut: '移除旧消息',
|
||||
animateOutWaitTime: '等待时间(秒)',
|
||||
fadeOutTime: '淡出时间(毫秒)',
|
||||
slide: '滑动',
|
||||
reverseSlide: '反向滑动',
|
||||
playAnimation: '播放动画',
|
||||
|
||||
result: '结果',
|
||||
copy: '复制',
|
||||
resetConfig: '恢复默认设置'
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,40 @@
|
||||
:default-active="$route.path"
|
||||
>
|
||||
<el-menu-item index="/">
|
||||
<i class="el-icon-s-home"></i>首页
|
||||
<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>样式生成器
|
||||
<i class="el-icon-brush"></i>{{$t('sidebar.stylegen')}}
|
||||
</el-menu-item>
|
||||
<a href="https://github.com/xfgryujk/blivechat" target="_blank">
|
||||
<el-menu-item>
|
||||
<i class="el-icon-share"></i>项目地址
|
||||
<i class="el-icon-share"></i>{{$t('sidebar.projectAddress')}}
|
||||
</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-submenu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Sidebar'
|
||||
name: 'Sidebar',
|
||||
methods: {
|
||||
onSelectLanguage(locale) {
|
||||
window.localStorage.lang = this.$i18n.locale = locale
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<el-container class="app-wrapper">
|
||||
<el-aside width="200px" class="sidebar-container">
|
||||
<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}">
|
||||
<div class="logo-container">
|
||||
<router-link to="/">
|
||||
<img src="@/assets/img/logo.png" class="sidebar-logo">
|
||||
@@ -10,6 +11,7 @@
|
||||
<sidebar></sidebar>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
<el-button v-show="isMobile" class="menu-button" icon="el-icon-s-unfold" @click="hideSidebar = false"></el-button>
|
||||
<router-view></router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
@@ -22,11 +24,33 @@ export default {
|
||||
name: 'Layout',
|
||||
components: {
|
||||
Sidebar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isMobile: false,
|
||||
hideSidebar: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('resize', this.onResize)
|
||||
this.onResize()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.onResize)
|
||||
},
|
||||
methods: {
|
||||
onResize() {
|
||||
this.isMobile = document.body.clientWidth <= 992
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html {
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
||||
}
|
||||
|
||||
html, body, #app, .app-wrapper, .sidebar-container {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -39,11 +63,35 @@ a, a:focus, a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.drawer-bg {
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
background-color: #304156;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-wrapper.mobile .sidebar-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition-duration: 0.3s;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.app-wrapper.mobile .sidebar-container.hide-sidebar {
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
transform: translate3d(-210px, 0, 0);
|
||||
}
|
||||
|
||||
.sidebar-container .logo-container {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 axios from 'axios'
|
||||
|
||||
import App from './App.vue'
|
||||
@@ -11,13 +12,18 @@ import StyleGenerator from './views/StyleGenerator'
|
||||
import Room from './views/Room.vue'
|
||||
import NotFound from './views/NotFound.vue'
|
||||
|
||||
import zh from './lang/zh'
|
||||
import ja from './lang/ja'
|
||||
import en from './lang/en'
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 开发时使用localhost:80
|
||||
axios.defaults.baseURL = 'http://localhost'
|
||||
// 开发时使用localhost:12450
|
||||
axios.defaults.baseURL = 'http://localhost:12450'
|
||||
}
|
||||
|
||||
Vue.use(VueRouter)
|
||||
Vue.use(ElementUI)
|
||||
Vue.use(VueI18n)
|
||||
|
||||
Vue.config.ignoredElements = [
|
||||
/^yt-/
|
||||
@@ -39,7 +45,27 @@ const router = new VueRouter({
|
||||
]
|
||||
})
|
||||
|
||||
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
|
||||
router,
|
||||
i18n
|
||||
}).$mount('#app')
|
||||
|
||||
@@ -6,6 +6,20 @@ export function mergeConfig (config, defaultConfig) {
|
||||
return res
|
||||
}
|
||||
|
||||
export default {
|
||||
mergeConfig
|
||||
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}`
|
||||
}
|
||||
|
||||
export default {
|
||||
mergeConfig,
|
||||
formatCurrency,
|
||||
getTimeTextMinSec
|
||||
}
|
||||
|
||||
@@ -1,57 +1,60 @@
|
||||
<template>
|
||||
<el-form :model="form" ref="form" label-width="150px" :rules="{
|
||||
roomId: [
|
||||
{required: true, message: '房间ID不能为空', trigger: 'blur'},
|
||||
{type: 'integer', min: 1, message: '房间ID必须为正整数', trigger: 'blur'}
|
||||
{required: true, message: $t('home.roomIdEmpty'), trigger: 'blur'},
|
||||
{type: 'integer', min: 1, message: $t('home.roomIdInteger'), trigger: 'blur'}
|
||||
]
|
||||
}">
|
||||
<el-tabs>
|
||||
<el-tab-pane label="常规">
|
||||
<el-form-item label="房间ID" required prop="roomId">
|
||||
<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="显示弹幕">
|
||||
<el-form-item :label="$t('home.showDanmaku')">
|
||||
<el-switch v-model="form.showDanmaku"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示礼物和新舰长">
|
||||
<el-form-item :label="$t('home.showGift')">
|
||||
<el-switch v-model="form.showGift"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="合并相似弹幕">
|
||||
<el-form-item :label="$t('home.mergeSimilarDanmaku')">
|
||||
<el-switch v-model="form.mergeSimilarDanmaku"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="最低显示礼物价格(元)">
|
||||
<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="弹幕最大速度(条/秒,0为无限制)">
|
||||
<el-form-item :label="$t('home.maxSpeed')">
|
||||
<el-input v-model.number="form.maxSpeed" 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>
|
||||
|
||||
<el-tab-pane label="屏蔽">
|
||||
<el-form-item label="礼物弹幕">
|
||||
<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="用户等级低于">
|
||||
<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="非正式会员">
|
||||
<el-form-item :label="$t('home.informalUser')">
|
||||
<el-switch v-model="form.blockNewbie"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="未绑定手机用户">
|
||||
<el-form-item :label="$t('home.unverifiedUser')">
|
||||
<el-switch v-model="form.blockNotMobileVerified"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="屏蔽关键词">
|
||||
<el-input v-model="form.blockKeywords" type="textarea" :rows="5" placeholder="一行一个"></el-input>
|
||||
<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="屏蔽用户">
|
||||
<el-input v-model="form.blockUsers" type="textarea" :rows="5" placeholder="一行一个"></el-input>
|
||||
<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="当前直播间勋章等级低于">
|
||||
<el-form-item :label="$t('home.blockMedalLevel')">
|
||||
<el-slider v-model="form.blockMedalLevel" show-input :min="0" :max="20"></el-slider>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="样式">
|
||||
<el-tab-pane :label="$t('home.style')">
|
||||
<el-form-item label="CSS">
|
||||
<el-input v-model="form.css" type="textarea" :rows="20"></el-input>
|
||||
</el-form-item>
|
||||
@@ -59,15 +62,15 @@
|
||||
</el-tabs>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<el-form-item label="房间URL" v-show="roomUrl">
|
||||
<el-form-item :label="$t('home.roomUrl')" v-show="roomUrl">
|
||||
<el-input ref="roomUrlInput" readonly :value="roomUrl" style="width: calc(100% - 6em); margin-right: 1em;"></el-input>
|
||||
<el-button type="primary" @click="copyUrl">复制</el-button>
|
||||
<el-button type="primary" @click="copyUrl">{{$t('home.copy')}}</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveConfig">保存配置</el-button>
|
||||
<el-button type="primary" :disabled="!roomUrl" @click="enterRoom">进入房间</el-button>
|
||||
<el-button type="primary" @click="exportConfig">导出配置</el-button>
|
||||
<el-button type="primary" @click="importConfig">导入配置</el-button>
|
||||
<el-button type="primary" @click="saveConfig">{{$t('home.saveConfig')}}</el-button>
|
||||
<el-button type="primary" :disabled="!roomUrl" @click="enterRoom">{{$t('home.enterRoom')}}</el-button>
|
||||
<el-button type="primary" @click="exportConfig">{{$t('home.exportConfig')}}</el-button>
|
||||
<el-button type="primary" @click="importConfig">{{$t('home.importConfig')}}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
@@ -109,10 +112,10 @@ export default {
|
||||
window.localStorage.configId = (await config.createRemoteConfig(this.form)).id
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error('保存失败:' + e)
|
||||
this.$message.error(this.$t('home.failedToSave') + e)
|
||||
return
|
||||
}
|
||||
this.$message({message: '保存成功', type: 'success'})
|
||||
this.$message({message: this.$t('home.successfullySaved'), type: 'success'})
|
||||
|
||||
let resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId},
|
||||
query: {config_id: window.localStorage.configId}})
|
||||
@@ -141,7 +144,7 @@ export default {
|
||||
try {
|
||||
cfg = JSON.parse(reader.result)
|
||||
} catch (e) {
|
||||
this.$message.error('配置解析失败:' + e)
|
||||
this.$message.error(this.$t('home.failedToParseConfig') + e)
|
||||
return
|
||||
}
|
||||
cfg = mergeConfig(cfg, config.DEFAULT_CONFIG)
|
||||
|
||||
@@ -7,10 +7,13 @@ import config from '@/api/config'
|
||||
import ChatRenderer from '@/components/ChatRenderer'
|
||||
import * as constants from '@/components/ChatRenderer/constants'
|
||||
|
||||
const COMMAND_JOIN_ROOM = 0
|
||||
const COMMAND_ADD_TEXT = 1
|
||||
const COMMAND_ADD_GIFT = 2
|
||||
const COMMAND_ADD_MEMBER = 3
|
||||
// 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',
|
||||
@@ -33,11 +36,11 @@ export default {
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
// 开发时使用localhost:80
|
||||
const url = process.env.NODE_ENV === 'development' ? 'ws://localhost/chat' : `ws://${window.location.host}/chat`
|
||||
// 开发时使用localhost:12450
|
||||
const url = process.env.NODE_ENV === 'development' ? 'ws://localhost:12450/chat' : `ws://${window.location.host}/chat`
|
||||
this.websocket = new WebSocket(url)
|
||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||
this.websocket.onmessage = this.onWsMessage.bind(this)
|
||||
this.websocket.onopen = this.onWsOpen
|
||||
this.websocket.onmessage = this.onWsMessage
|
||||
|
||||
if (this.$route.query.config_id) {
|
||||
try {
|
||||
@@ -88,7 +91,7 @@ export default {
|
||||
message = {
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
time: time,
|
||||
authorName: data.authorName,
|
||||
authorType: data.authorType,
|
||||
content: data.content,
|
||||
@@ -101,31 +104,51 @@ export default {
|
||||
break
|
||||
}
|
||||
let price = data.totalCoin / 1000
|
||||
if (price < this.config.minGiftPrice) // 丢人
|
||||
if (price < this.config.minGiftPrice) { // 丢人
|
||||
break
|
||||
}
|
||||
message = {
|
||||
type: constants.MESSAGE_TYPE_GIFT,
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
authorName: data.authorName,
|
||||
price: price,
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
content: `Sent ${data.giftName}x${data.giftNum}`
|
||||
time: time,
|
||||
content: '' // 有了SC,礼物不需要内容了
|
||||
}
|
||||
break
|
||||
}
|
||||
case COMMAND_ADD_MEMBER:
|
||||
if (!this.config.showGift) {
|
||||
if (!this.config.showGift || !this.filterSuperChatMessage(data)) {
|
||||
break
|
||||
}
|
||||
message = {
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
avatarUrl: data.avatarUrl,
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
time: time,
|
||||
authorName: data.authorName,
|
||||
title: 'NEW MEMBER!',
|
||||
content: `Welcome ${data.authorName}`
|
||||
content: `Welcome ${data.authorName}!`
|
||||
}
|
||||
break
|
||||
case COMMAND_ADD_SUPER_CHAT:
|
||||
if (!this.config.showGift) {
|
||||
break
|
||||
}
|
||||
if (data.price < this.config.minGiftPrice) { // 丢人
|
||||
break
|
||||
}
|
||||
message = {
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
avatarUrl: data.avatarUrl,
|
||||
authorName: data.authorName,
|
||||
price: data.price,
|
||||
time: time,
|
||||
content: data.content
|
||||
}
|
||||
break
|
||||
case COMMAND_DEL_SUPER_CHAT:
|
||||
// TODO 删除SC
|
||||
break
|
||||
}
|
||||
if (message) {
|
||||
this.addMessageBuffer(message)
|
||||
@@ -155,6 +178,19 @@ export default {
|
||||
}
|
||||
return true
|
||||
},
|
||||
filterSuperChatMessage(data) {
|
||||
for (let keyword of this.config.blockKeywords) {
|
||||
if (data.content.indexOf(keyword) !== -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (let user of this.config.blockUsers) {
|
||||
if (data.authorName === user) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
mergeSimilar(content) {
|
||||
if (!this.config.mergeSimilarDanmaku) {
|
||||
return false
|
||||
@@ -189,8 +225,8 @@ export default {
|
||||
}
|
||||
// 防止同时添加和删除项目时所有的项目重新渲染 https://github.com/vuejs/vue/issues/6857
|
||||
this.$nextTick(() => {
|
||||
if (this.messages.length > 50) {
|
||||
this.messages.splice(0, this.messages.length - 50)
|
||||
if (this.messages.length > this.config.maxNumber) {
|
||||
this.messages.splice(0, this.messages.length - this.config.maxNumber)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -2,195 +2,195 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form label-width="150px" size="mini">
|
||||
<h3>描边</h3>
|
||||
<el-form-item label="显示描边">
|
||||
<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="描边尺寸">
|
||||
<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="描边颜色">
|
||||
<el-form-item :label="$t('stylegen.outlineColor')">
|
||||
<el-color-picker v-model="form.outlineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
|
||||
<h3>头像</h3>
|
||||
<el-form-item label="显示头像">
|
||||
<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="头像尺寸">
|
||||
<el-form-item :label="$t('stylegen.avatarSize')">
|
||||
<el-input v-model.number="form.avatarSize" type="number" min="0"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<h3>用户名</h3>
|
||||
<el-form-item label="字体">
|
||||
<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="字体尺寸">
|
||||
<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="行高(0为默认)">
|
||||
<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="普通颜色">
|
||||
<el-form-item :label="$t('stylegen.normalColor')">
|
||||
<el-color-picker v-model="form.userNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="主播颜色">
|
||||
<el-form-item :label="$t('stylegen.ownerColor')">
|
||||
<el-color-picker v-model="form.ownerUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="房管颜色">
|
||||
<el-form-item :label="$t('stylegen.moderatorColor')">
|
||||
<el-color-picker v-model="form.moderatorUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="舰长颜色">
|
||||
<el-form-item :label="$t('stylegen.memberColor')">
|
||||
<el-color-picker v-model="form.memberUserNameColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示勋章">
|
||||
<el-form-item :label="$t('stylegen.showBadges')">
|
||||
<el-switch v-model="form.showBadges"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名后显示冒号">
|
||||
<el-form-item :label="$t('stylegen.showColon')">
|
||||
<el-switch v-model="form.showColon"></el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<h3>消息</h3>
|
||||
<el-form-item label="字体">
|
||||
<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="字体尺寸">
|
||||
<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="行高(0为默认)">
|
||||
<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="颜色">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.messageColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="在新行显示">
|
||||
<el-form-item :label="$t('stylegen.onNewLine')">
|
||||
<el-switch v-model="form.messageOnNewLine"></el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<h3>时间</h3>
|
||||
<el-form-item label="显示时间">
|
||||
<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="字体">
|
||||
<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="字体尺寸">
|
||||
<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="行高(0为默认)">
|
||||
<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="颜色">
|
||||
<el-form-item :label="$t('stylegen.color')">
|
||||
<el-color-picker v-model="form.timeColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
|
||||
<h3>背景</h3>
|
||||
<el-form-item label="背景色">
|
||||
<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="用条代替背景">
|
||||
<el-form-item :label="$t('stylegen.useBarsInsteadOfBg')">
|
||||
<el-switch v-model="form.useBarsInsteadOfBg"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="消息背景色">
|
||||
<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="主播消息背景色">
|
||||
<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="房管消息背景色">
|
||||
<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="舰长消息背景色">
|
||||
<el-form-item :label="$t('stylegen.memberMessageBgColor')">
|
||||
<el-color-picker v-model="form.memberMessageBgColor" show-alpha></el-color-picker>
|
||||
</el-form-item>
|
||||
|
||||
<h3>礼物、舰长</h3>
|
||||
<el-form-item label="第一行字体">
|
||||
<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="第一行字体尺寸">
|
||||
<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="第一行行高(0为默认)">
|
||||
<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="第一行颜色">
|
||||
<el-form-item :label="$t('stylegen.firstLineColor')">
|
||||
<el-color-picker v-model="form.firstLineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="第二行字体">
|
||||
<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="第二行字体尺寸">
|
||||
<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="第二行行高(0为默认)">
|
||||
<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="第二行颜色">
|
||||
<el-form-item :label="$t('stylegen.secondLineColor')">
|
||||
<el-color-picker v-model="form.secondLineColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="SuperChat内容字体">
|
||||
<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="SuperChat内容字体尺寸">
|
||||
<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="SuperChat内容行高(0为默认)">
|
||||
<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="SuperChat内容颜色">
|
||||
<el-form-item :label="$t('stylegen.scContentLineColor')">
|
||||
<el-color-picker v-model="form.scContentColor"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示新舰长背景">
|
||||
<el-form-item :label="$t('stylegen.showNewMemberBg')">
|
||||
<el-switch v-model="form.showNewMemberBg"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示SuperChat贴纸">
|
||||
<el-form-item :label="$t('stylegen.showScTicker')">
|
||||
<el-switch v-model="form.showScTicker"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示SuperChat贴纸之外的内容">
|
||||
<el-form-item :label="$t('stylegen.showOtherThings')">
|
||||
<el-switch v-model="form.showOtherThings"></el-switch>
|
||||
</el-form-item>
|
||||
|
||||
<h3>动画</h3>
|
||||
<el-form-item label="进入动画">
|
||||
<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="淡入时间(毫秒)">
|
||||
<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="移除旧消息">
|
||||
<el-form-item :label="$t('stylegen.animateOut')">
|
||||
<el-switch v-model="form.animateOut"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="等待时间(秒)">
|
||||
<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="淡出时间(毫秒)">
|
||||
<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="滑动">
|
||||
<el-form-item :label="$t('stylegen.slide')">
|
||||
<el-switch v-model="form.slide"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="反向滑动">
|
||||
<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">播放动画</el-button>
|
||||
<el-button type="primary" @click="playAnimation">{{$t('stylegen.playAnimation')}}</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<h3>结果</h3>
|
||||
<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">复制</el-button>
|
||||
<el-button @click="resetConfig">恢复默认设置</el-button>
|
||||
<el-button type="primary" @click="copyResult">{{$t('stylegen.copy')}}</el-button>
|
||||
<el-button @click="resetConfig">{{$t('stylegen.resetConfig')}}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div id="example-container">
|
||||
<div id="example-container" ref="exampleContainer">
|
||||
<div id="fakebody">
|
||||
<chat-renderer :paidMessages="examplePaidMessages" :messages="messages" :css="exampleCss"></chat-renderer>
|
||||
<chat-renderer :paidMessages="examplePaidMessages" :messages="messages" :css="exampleCss" ref="example"></chat-renderer>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
@@ -211,7 +211,7 @@ let textMessageTemplate = {
|
||||
addTime: time,
|
||||
type: constants.MESSAGE_TYPE_TEXT,
|
||||
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
time: time,
|
||||
authorName: '',
|
||||
authorType: constants.AUTHRO_TYPE_NORMAL,
|
||||
content: '',
|
||||
@@ -223,7 +223,7 @@ let legacyPaidMessageTemplate = {
|
||||
addTime: time,
|
||||
type: constants.MESSAGE_TYPE_MEMBER,
|
||||
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
time: time,
|
||||
authorName: '',
|
||||
title: 'NEW MEMBER!',
|
||||
content: ''
|
||||
@@ -231,11 +231,11 @@ let legacyPaidMessageTemplate = {
|
||||
let paidMessageTemplate = {
|
||||
id: 0,
|
||||
addTime: time,
|
||||
type: constants.MESSAGE_TYPE_GIFT,
|
||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
|
||||
authorName: '',
|
||||
price: 0,
|
||||
time: `${time.getMinutes()}:${time.getSeconds()}`,
|
||||
time: time,
|
||||
content: ''
|
||||
}
|
||||
let nextId = 0
|
||||
@@ -263,14 +263,14 @@ const EXAMPLE_MESSAGES = [
|
||||
}, {
|
||||
...legacyPaidMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: 'Paryi',
|
||||
content: 'Welcome Paryi'
|
||||
authorName: '吾乐KANA',
|
||||
content: 'Welcome 吾乐KANA!'
|
||||
}, {
|
||||
...paidMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: '石油佬',
|
||||
price: 1245,
|
||||
content: 'Sent 小电视飞船x1'
|
||||
authorName: '无火的残渣',
|
||||
price: 66600,
|
||||
content: 'Sent 小电视飞船x100'
|
||||
}, {
|
||||
...textMessageTemplate,
|
||||
id: nextId++,
|
||||
@@ -280,7 +280,7 @@ const EXAMPLE_MESSAGES = [
|
||||
}, {
|
||||
...paidMessageTemplate,
|
||||
id: nextId++,
|
||||
authorName: '臭DD',
|
||||
authorName: '夏色祭保護協会会長',
|
||||
price: 28,
|
||||
content: 'Sent 礼花x1'
|
||||
}
|
||||
@@ -314,6 +314,10 @@ export default {
|
||||
return stylegen.getStyle(this.form)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let observer = new MutationObserver(() => this.$refs.example.scrollToBottom())
|
||||
observer.observe(this.$refs.exampleContainer, {attributes: true})
|
||||
},
|
||||
methods: {
|
||||
getFontSuggestions(query, callback) {
|
||||
let res = this.FONTS.map(font => {return {value: font}})
|
||||
@@ -357,8 +361,8 @@ export default {
|
||||
#example-container {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
width: 400px;
|
||||
left: calc(210px + 40px + (100vw - 210px - 40px) / 2);
|
||||
width: calc((100vw - 210px - 40px) / 2 - 40px - 30px);
|
||||
height: calc(100vh - 110px);
|
||||
|
||||
background-color: #444;
|
||||
@@ -380,6 +384,13 @@ export default {
|
||||
background-position:0 0, 16px 0, 16px -16px, 0px 16px;
|
||||
|
||||
padding: 25px;
|
||||
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-wrapper.mobile #example-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#fakebody {
|
||||
|
||||
@@ -63,6 +63,8 @@ export const DEFAULT_CONFIG = {
|
||||
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)
|
||||
@@ -105,7 +107,7 @@ yt-live-chat-author-chip #author-name {
|
||||
/* Outlines */
|
||||
yt-live-chat-renderer * {
|
||||
${getShowOutlinesStyle(config)}
|
||||
font-family: "${config.messageFont}", sans-serif;
|
||||
font-family: "${config.messageFont}"${FALLBACK_FONTS};
|
||||
font-size: ${config.messageFontSize}px !important;
|
||||
line-height: ${config.messageLineHeight}px !important;
|
||||
}
|
||||
@@ -116,11 +118,11 @@ yt-live-chat-legacy-paid-message-renderer #content {
|
||||
}
|
||||
|
||||
/* Hide scrollbar. */
|
||||
yt-live-chat-item-list-renderer #items{
|
||||
yt-live-chat-item-list-renderer #items {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
yt-live-chat-item-list-renderer #item-scroller{
|
||||
yt-live-chat-item-list-renderer #item-scroller {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
@@ -164,7 +166,7 @@ yt-live-chat-text-message-renderer #chat-badges {
|
||||
yt-live-chat-text-message-renderer #timestamp {
|
||||
display: ${config.showTime ? 'inline' : 'none'} !important;
|
||||
${config.timeColor ? `color: ${config.timeColor} !important;` : ''}
|
||||
font-family: "${config.timeFont}", sans-serif;
|
||||
font-family: "${config.timeFont}"${FALLBACK_FONTS};
|
||||
font-size: ${config.timeFontSize}px !important;
|
||||
line-height: ${config.timeLineHeight || config.timeFontSize}px !important;
|
||||
}
|
||||
@@ -188,7 +190,7 @@ yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="memb
|
||||
/* Channel names. */
|
||||
yt-live-chat-text-message-renderer #author-name {
|
||||
${config.userNameColor ? `color: ${config.userNameColor} !important;` : ''}
|
||||
font-family: "${config.userNameFont}", sans-serif;
|
||||
font-family: "${config.userNameFont}"${FALLBACK_FONTS};
|
||||
font-size: ${config.userNameFontSize}px !important;
|
||||
line-height: ${config.userNameLineHeight || config.userNameFontSize}px !important;
|
||||
}
|
||||
@@ -199,7 +201,7 @@ ${getShowColonStyle(config)}
|
||||
yt-live-chat-text-message-renderer #message,
|
||||
yt-live-chat-text-message-renderer #message * {
|
||||
${config.messageColor ? `color: ${config.messageColor} !important;` : ''}
|
||||
font-family: "${config.messageFont}", sans-serif;
|
||||
font-family: "${config.messageFont}"${FALLBACK_FONTS};
|
||||
font-size: ${config.messageFontSize}px !important;
|
||||
line-height: ${config.messageLineHeight || config.messageFontSize}px !important;
|
||||
}
|
||||
@@ -214,7 +216,7 @@ yt-live-chat-paid-message-renderer #author-name *,
|
||||
yt-live-chat-legacy-paid-message-renderer #event-text,
|
||||
yt-live-chat-legacy-paid-message-renderer #event-text * {
|
||||
${config.firstLineColor ? `color: ${config.firstLineColor} !important;` : ''}
|
||||
font-family: "${config.firstLineFont}", sans-serif;
|
||||
font-family: "${config.firstLineFont}"${FALLBACK_FONTS};
|
||||
font-size: ${config.firstLineFontSize}px !important;
|
||||
line-height: ${config.firstLineLineHeight || config.firstLineFontSize}px !important;
|
||||
}
|
||||
@@ -224,7 +226,7 @@ yt-live-chat-paid-message-renderer #purchase-amount *,
|
||||
yt-live-chat-legacy-paid-message-renderer #detail-text,
|
||||
yt-live-chat-legacy-paid-message-renderer #detail-text * {
|
||||
${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
|
||||
font-family: "${config.secondLineFont}", sans-serif;
|
||||
font-family: "${config.secondLineFont}"${FALLBACK_FONTS};
|
||||
font-size: ${config.secondLineFontSize}px !important;
|
||||
line-height: ${config.secondLineLineHeight || config.secondLineFontSize}px !important;
|
||||
}
|
||||
@@ -232,7 +234,7 @@ yt-live-chat-legacy-paid-message-renderer #detail-text * {
|
||||
yt-live-chat-paid-message-renderer #content,
|
||||
yt-live-chat-paid-message-renderer #content * {
|
||||
${config.scContentColor ? `color: ${config.scContentColor} !important;` : ''}
|
||||
font-family: "${config.scContentFont}", sans-serif;
|
||||
font-family: "${config.scContentFont}"${FALLBACK_FONTS};
|
||||
font-size: ${config.scContentFontSize}px !important;
|
||||
line-height: ${config.scContentLineHeight || config.scContentFontSize}px !important;
|
||||
}
|
||||
@@ -273,7 +275,7 @@ yt-live-chat-ticker-paid-message-item-renderer *,
|
||||
yt-live-chat-ticker-sponsor-item-renderer,
|
||||
yt-live-chat-ticker-sponsor-item-renderer * {
|
||||
${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
|
||||
font-family: "${config.secondLineFont}", sans-serif;
|
||||
font-family: "${config.secondLineFont}"${FALLBACK_FONTS};
|
||||
}
|
||||
|
||||
yt-live-chat-mode-change-message-renderer,
|
||||
@@ -382,11 +384,11 @@ function getAnimationStyle (config) {
|
||||
: ` transform: translateX(${config.reverseSlide ? 16 : -16}px);`
|
||||
} }`)
|
||||
curTime += config.fadeInTime
|
||||
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none;}`)
|
||||
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;}`)
|
||||
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);`
|
||||
|
||||
21
main.py
21
main.py
@@ -1,13 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import webbrowser
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
|
||||
import update
|
||||
import views.chat
|
||||
import views.config
|
||||
import views.main
|
||||
@@ -20,7 +22,7 @@ WEB_ROOT = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='用于OBS的仿YouTube风格的bilibili直播聊天层')
|
||||
parser.add_argument('--host', help='服务器host,默认为127.0.0.1', default='127.0.0.1')
|
||||
parser.add_argument('--port', help='服务器端口,默认为80', type=int, default=80)
|
||||
parser.add_argument('--port', help='服务器端口,默认为12450', type=int, default=12450)
|
||||
parser.add_argument('--debug', help='调试模式', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -31,6 +33,8 @@ def main():
|
||||
level=logging.INFO if not args.debug else logging.DEBUG
|
||||
)
|
||||
|
||||
asyncio.ensure_future(update.check_update())
|
||||
|
||||
app = tornado.web.Application(
|
||||
[
|
||||
(r'/chat', views.chat.ChatHandler),
|
||||
@@ -45,10 +49,15 @@ def main():
|
||||
debug=args.debug,
|
||||
autoreload=False
|
||||
)
|
||||
app.listen(args.port, args.host)
|
||||
logger.info('服务器启动:%s:%d', args.host, args.port)
|
||||
url = 'http://localhost' if args.port == 80 else f'http://localhost:{args.port}'
|
||||
webbrowser.open(url)
|
||||
try:
|
||||
app.listen(args.port, args.host)
|
||||
except OSError:
|
||||
logger.warning('Address is used %s:%d', args.host, args.port)
|
||||
return
|
||||
finally:
|
||||
url = 'http://localhost' if args.port == 80 else f'http://localhost:{args.port}'
|
||||
webbrowser.open(url)
|
||||
logger.info('Server started: %s:%d', args.host, args.port)
|
||||
tornado.ioloop.IOLoop.current().start()
|
||||
|
||||
|
||||
|
||||
15
update.py
Normal file
15
update.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import aiohttp
|
||||
|
||||
VERSION = 'v1.2.0'
|
||||
|
||||
|
||||
async def check_update():
|
||||
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'])
|
||||
@@ -17,10 +17,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(enum.IntEnum):
|
||||
JOIN_ROOM = 0
|
||||
ADD_TEXT = 1
|
||||
ADD_GIFT = 2
|
||||
ADD_MEMBER = 3
|
||||
HEARTBEAT = 0
|
||||
JOIN_ROOM = 1
|
||||
ADD_TEXT = 2
|
||||
ADD_GIFT = 3
|
||||
ADD_MEMBER = 4
|
||||
ADD_SUPER_CHAT = 5
|
||||
DEL_SUPER_CHAT = 6
|
||||
|
||||
|
||||
DEFAULT_AVATAR_URL = 'https://static.hdslb.com/images/member/noface.gif'
|
||||
@@ -48,8 +51,8 @@ async def get_avatar_url(user_id):
|
||||
return DEFAULT_AVATAR_URL
|
||||
|
||||
if _last_avatar_failed_time is not None:
|
||||
if (cur_time - _last_avatar_failed_time).total_seconds() < 5 * 60 + 3:
|
||||
# 5分钟以内被ban,解封大约要15分钟
|
||||
if (cur_time - _last_avatar_failed_time).total_seconds() < 3 * 60 + 3:
|
||||
# 3分钟以内被ban,解封大约要15分钟
|
||||
return DEFAULT_AVATAR_URL
|
||||
else:
|
||||
_last_avatar_failed_time = None
|
||||
@@ -59,7 +62,7 @@ async def get_avatar_url(user_id):
|
||||
async with _http_session.get('https://api.bilibili.com/x/space/acc/info',
|
||||
params={'mid': user_id}) as r:
|
||||
if r.status != 200: # 可能会被B站ban
|
||||
logger.warning('获取头像失败:status=%d %s uid=%d', r.status, r.reason, user_id)
|
||||
logger.warning('Failed to fetch avatar: status=%d %s uid=%d', r.status, r.reason, user_id)
|
||||
_last_avatar_failed_time = cur_time
|
||||
return DEFAULT_AVATAR_URL
|
||||
data = await r.json()
|
||||
@@ -95,7 +98,7 @@ asyncio.ensure_future(_fetch_avatar_loop())
|
||||
|
||||
class Room(blivedm.BLiveClient):
|
||||
def __init__(self, room_id):
|
||||
super().__init__(room_id, session=_http_session)
|
||||
super().__init__(room_id, session=_http_session, heartbeat_interval=10)
|
||||
self.clients: List['ChatHandler'] = []
|
||||
|
||||
def stop_and_close(self):
|
||||
@@ -158,6 +161,21 @@ class Room(blivedm.BLiveClient):
|
||||
'authorName': message.username
|
||||
})
|
||||
|
||||
async def _on_super_chat(self, message: blivedm.SuperChatMessage):
|
||||
self.send_message(Command.ADD_SUPER_CHAT, {
|
||||
'avatarUrl': message.face,
|
||||
'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
|
||||
})
|
||||
|
||||
|
||||
class RoomManager:
|
||||
def __init__(self):
|
||||
@@ -167,7 +185,7 @@ class RoomManager:
|
||||
if room_id in self._rooms:
|
||||
room = self._rooms[room_id]
|
||||
else:
|
||||
logger.info('创建房间%d', room_id)
|
||||
logger.info('Creating room %d', room_id)
|
||||
room = Room(room_id)
|
||||
self._rooms[room_id] = room
|
||||
room.start()
|
||||
@@ -182,7 +200,7 @@ class RoomManager:
|
||||
room = self._rooms[room_id]
|
||||
room.clients.remove(client)
|
||||
if not room.clients:
|
||||
logger.info('移除房间%d', room_id)
|
||||
logger.info('Removing room %d', room_id)
|
||||
room.stop_and_close()
|
||||
del self._rooms[room_id]
|
||||
|
||||
@@ -239,7 +257,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
|
||||
self.room_id = None
|
||||
|
||||
def open(self):
|
||||
logger.info('Websocket连接 %s', self.request.remote_ip)
|
||||
logger.info('Websocket connected %s', self.request.remote_ip)
|
||||
|
||||
def on_message(self, message):
|
||||
if self.room_id is not None:
|
||||
@@ -247,13 +265,13 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
|
||||
body = json.loads(message)
|
||||
if body['cmd'] == Command.JOIN_ROOM:
|
||||
self.room_id = int(body['data']['roomId'])
|
||||
logger.info('客户端%s加入房间%d', self.request.remote_ip, self.room_id)
|
||||
logger.info('Client %s is joining room %d', self.request.remote_ip, self.room_id)
|
||||
room_manager.add_client(self.room_id, self)
|
||||
else:
|
||||
logger.warning('未知的命令: %s data: %s', body['cmd'], body['data'])
|
||||
logger.warning('Unknown cmd: %s data: %s', body['cmd'], body['data'])
|
||||
|
||||
def on_close(self):
|
||||
logger.info('Websocket断开 %s room: %s', self.request.remote_ip, self.room_id)
|
||||
logger.info('Websocket disconnected %s room: %s', self.request.remote_ip, self.room_id)
|
||||
if self.room_id is not None:
|
||||
room_manager.del_client(self.room_id, self)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ configs: Dict[str, dict] = {}
|
||||
|
||||
ALLOWED_FIELDS = (
|
||||
'showDanmaku', 'showGift', 'mergeSimilarDanmaku', 'minGiftPrice', 'maxSpeed',
|
||||
'blockGiftDanmaku', 'blockLevel', 'blockNewbie', 'blockNotMobileVerified',
|
||||
'maxNumber', 'blockGiftDanmaku', 'blockLevel', 'blockNewbie', 'blockNotMobileVerified',
|
||||
'blockKeywords', 'blockUsers', 'blockMedalLevel', 'css'
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user