Compare commits

...

31 Commits

Author SHA1 Message Date
John Smith
60e6b5ec99 隐藏空SC内容、更新版本号 2019-09-23 22:28:11 +08:00
John Smith
695a111c9a SC使用官方功能 2019-09-23 22:22:27 +08:00
John Smith
0160549dbe 重复标记用HSL、代码优化 2019-09-22 17:18:34 +08:00
John Smith
3fd0d9ab28 更新JS包 2019-09-17 23:53:26 +08:00
John Smith
b1061ae103 更新blivedm 2019-09-17 23:08:03 +08:00
John Smith
6e737ce33f 样式生成器添加梗 2019-09-16 22:21:44 +08:00
John Smith
10db6b9ae2 添加最大弹幕数 2019-09-16 22:15:32 +08:00
John Smith
1514bfed5b 自动检查更新 2019-09-16 20:48:03 +08:00
John Smith
dc264d40f0 SC配置按美元汇率 2019-09-15 23:31:23 +08:00
John Smith
104e647f61 样式生成器添加默认字体 2019-07-18 09:15:27 +08:00
John Smith
d5d7fdde6d 日志改用英语 2019-07-18 09:08:03 +08:00
John Smith
23aa88d564 时间补齐2位 2019-07-18 08:31:58 +08:00
John Smith
ac1bda0392 修改一堆文案 2019-07-15 21:30:52 +08:00
John Smith
5cdb786966 去掉了点击预览会消失的CSS 2019-07-15 19:50:51 +08:00
John Smith
2b5ca2e219 Merge pull request #8 from simon300000/honest
「尝试」优化了样式生成器的CSS,加入了一个只有好孩子才能看到的小按钮
2019-07-15 19:30:39 +08:00
John Smith
fc77b246c2 我傻了 2019-07-15 18:01:38 +08:00
John Smith
083832cb3e Merge pull request #7 from simon300000/patch-css
增加element字体CSS
2019-07-15 17:58:42 +08:00
John Smith
804db2fec1 全局应用字体 2019-07-15 17:56:03 +08:00
simon3000
769964d5db 增加element字体CSS 2019-07-15 17:56:03 +08:00
John Smith
aba0efca34 Merge pull request #9 from xfgryujk/dependabot/npm_and_yarn/frontend/lodash-4.17.13
Bump lodash from 4.17.11 to 4.17.13 in /frontend
2019-07-15 17:35:06 +08:00
dependabot[bot]
9ff18fe8b5 Bump lodash from 4.17.11 to 4.17.13 in /frontend
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.13.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.13)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-15 09:33:24 +00:00
simon3000
ee78cbf90e 改用refs直接执行function 2019-07-13 01:51:58 +02:00
simon3000
ffc3c9d8a0 将监听的event名称改为exampleResize 2019-07-13 01:16:57 +02:00
simon3000
a69b6b6406 给ChatRenderer加上demo prop, 为true时自动resize 2019-07-13 01:14:55 +02:00
simon3000
1252402e5e Merge branch 'patch-scroll-on-resize' into honest 2019-07-13 01:03:45 +02:00
simon3000
53be587189 自动resize 2019-07-11 23:14:11 +02:00
simon3000
d3cc8b0c77 👿 2019-07-11 22:52:06 +02:00
simon3000
4f4d117ce0 使用calc优化CSS生成器的浏览部分 2019-07-11 22:51:41 +02:00
simon3000
0d1e38346e 增加element字体CSS 2019-07-11 22:15:46 +02:00
simon3000
273dc0f43f 窗口缩放时滚动 2019-07-11 21:50:28 +02:00
John Smith
26f19b6630 Update README.md 2019-07-11 11:35:17 +08:00
23 changed files with 18659 additions and 6188 deletions

View File

@@ -21,8 +21,8 @@
```bat
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
### 源代码版
@@ -41,8 +41,8 @@
```sh
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

Submodule blivedm updated: 9eb953720c...1613238420

24493
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
"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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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~28B坷垃
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)',

View File

@@ -15,16 +15,17 @@ export default {
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: 'System messages (gift effect)',
blockLevel: 'User level lower than',
informalUser: 'Informal users',
unverifiedUser: 'Unverified users',
giftDanmaku: 'Block system messages (gift effect)',
blockLevel: 'Block user level lower than',
informalUser: 'Block informal users',
unverifiedUser: 'Block unverified users',
blockKeywords: 'Block keywords',
onePerLine: 'One per line',
blockUsers: 'Block users',
blockMedalLevel: 'Medal level lower than',
blockMedalLevel: 'Block medal level lower than',
style: 'Style',

View File

@@ -15,16 +15,17 @@
mergeSimilarDanmaku: '同じコメントを合併する',
minGiftPrice: '最低表示スーパーチャット価格CNY',
maxSpeed: 'コメントの最大速度(バー/秒、0は無制限',
maxNumber: '最大コメント数',
block: 'ブロック',
giftDanmaku: 'システムコメント(プレゼント効果)',
blockLevel: 'ユーザーレベルがx未満',
informalUser: '非公式ユーザー',
unverifiedUser: '認証されていないユーザー',
blockKeywords: 'キーワード',
giftDanmaku: 'ブロックシステムコメント(プレゼント効果)',
blockLevel: 'ブロックユーザーレベルがx未満',
informalUser: 'ブロック非公式ユーザー',
unverifiedUser: 'ブロック認証されていないユーザー',
blockKeywords: 'ブロックキーワード',
onePerLine: '1行に1つずつ',
blockUsers: 'ユーザー',
blockMedalLevel: '勲章等級がx未満',
blockUsers: 'ブロックユーザー',
blockMedalLevel: 'ブロック勲章等級がx未満',
style: 'スタイル',

View File

@@ -15,16 +15,17 @@ export default {
mergeSimilarDanmaku: '合并相似弹幕',
minGiftPrice: '最低显示礼物价格(元)',
maxSpeed: '弹幕最大速度(条/秒0为无限制',
maxNumber: '最大弹幕数',
block: '屏蔽',
giftDanmaku: '礼物弹幕',
blockLevel: '用户等级低于',
informalUser: '非正式会员',
unverifiedUser: '未绑定手机用户',
giftDanmaku: '屏蔽礼物弹幕',
blockLevel: '屏蔽用户等级低于',
informalUser: '屏蔽非正式会员',
unverifiedUser: '屏蔽未绑定手机用户',
blockKeywords: '屏蔽关键词',
onePerLine: '一行一个',
blockUsers: '屏蔽用户',
blockMedalLevel: '当前直播间勋章等级低于',
blockMedalLevel: '屏蔽当前直播间勋章等级低于',
style: '样式',

View File

@@ -47,6 +47,10 @@ export default {
</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%;
}

View File

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

View File

@@ -25,6 +25,9 @@
<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="$t('home.block')">

View File

@@ -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',
@@ -36,8 +39,8 @@ export default {
// 开发时使用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)
}
})
},

View File

@@ -188,9 +188,9 @@
</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,9 @@ export default {
background-position:0 0, 16px 0, 16px -16px, 0px 16px;
padding: 25px;
resize: both;
overflow: hidden;
}
.app-wrapper.mobile #example-container {

View File

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

19
main.py
View File

@@ -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
@@ -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
View 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'])

View File

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

View File

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