完成配置保存和读取

This commit is contained in:
John Smith 2019-06-12 13:55:49 +08:00
parent 22e8f88533
commit 920e6fff96
11 changed files with 434 additions and 252 deletions

275
frontend/src/api/config.js Normal file
View File

@ -0,0 +1,275 @@
import axios from 'axios'
const DEFAULT_CSS = `@import url("https://fonts.googleapis.com/css?family=Changa%20One");
@import url("https://fonts.googleapis.com/css?family=Imprima");
/* @import url("https://fonts.lug.ustc.edu.cn/css?family=Changa%20One"); */
/* @import url("https://fonts.lug.ustc.edu.cn/css?family=Imprima"); */
/* Background colors*/
body {
overflow: hidden;
background-color: rgba(0,0,0,0);
}
/* Transparent background. */
yt-live-chat-renderer {
background-color: transparent !important;
}
yt-live-chat-text-message-renderer,
yt-live-chat-text-message-renderer[is-highlighted] {
background-color: transparent !important;
}
yt-live-chat-text-message-renderer[author-type="owner"],
yt-live-chat-text-message-renderer[author-type="owner"][is-highlighted] {
background-color: transparent !important;
}
yt-live-chat-text-message-renderer[author-type="moderator"],
yt-live-chat-text-message-renderer[author-type="moderator"][is-highlighted] {
background-color: transparent !important;
}
yt-live-chat-text-message-renderer[author-type="member"],
yt-live-chat-text-message-renderer[author-type="member"][is-highlighted] {
background-color: transparent !important;
}
yt-live-chat-author-chip #author-name {
background-color: transparent !important;
}
/* Outlines */
yt-live-chat-renderer * {
text-shadow: -2px -2px #000000,-2px -1px #000000,-2px 0px #000000,-2px 1px #000000,-2px 2px #000000,-1px -2px #000000,-1px -1px #000000,-1px 0px #000000,-1px 1px #000000,-1px 2px #000000,0px -2px #000000,0px -1px #000000,0px 0px #000000,0px 1px #000000,0px 2px #000000,1px -2px #000000,1px -1px #000000,1px 0px #000000,1px 1px #000000,1px 2px #000000,2px -2px #000000,2px -1px #000000,2px 0px #000000,2px 1px #000000,2px 2px #000000;
font-family: "Imprima";
font-size: 18px !important;
line-height: 18px !important;
}
yt-live-chat-text-message-renderer #content,
yt-live-chat-legacy-paid-message-renderer #content {
overflow: initial !important;
}
/* Hide scrollbar. */
yt-live-chat-item-list-renderer #items{
overflow: hidden !important;
}
yt-live-chat-item-list-renderer #item-scroller{
overflow: hidden !important;
}
/* Hide header and input. */
yt-live-chat-header-renderer,
yt-live-chat-message-input-renderer {
display: none !important;
}
/* Reduce side padding. */
yt-live-chat-text-message-renderer,
yt-live-chat-legacy-paid-message-renderer {
padding-left: 4px !important;
padding-right: 4px !important;
}
yt-live-chat-paid-message-renderer #header {
padding-left: 4px !important;
padding-right: 4px !important;
}
/* Avatars. */
yt-live-chat-text-message-renderer #author-photo,
yt-live-chat-paid-message-renderer #author-photo,
yt-live-chat-legacy-paid-message-renderer #author-photo {
width: 24px !important;
height: 24px !important;
border-radius: 24px !important;
margin-right: 6px !important;
background-size: cover;
}
/* Hide badges. */
yt-live-chat-text-message-renderer #author-badges {
display: none !important;
vertical-align: text-top !important;
}
/* Timestamps. */
yt-live-chat-text-message-renderer #timestamp {
color: #999999 !important;
font-family: "Imprima";
font-size: 16px !important;
line-height: 16px !important;
}
/* Badges. */
yt-live-chat-text-message-renderer #author-name[type="owner"],
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
color: #ffd600 !important;
}
yt-live-chat-text-message-renderer #author-name[type="moderator"],
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
color: #5e84f1 !important;
}
yt-live-chat-text-message-renderer #author-name[type="member"],
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
color: #0f9d58 !important;
}
/* Channel names. */
yt-live-chat-text-message-renderer #author-name {
color: #cccccc !important;
font-family: "Changa One";
font-size: 20px !important;
line-height: 20px !important;
}
yt-live-chat-text-message-renderer #author-name::after {
content: ":";
margin-left: 2px;
}
/* Messages. */
yt-live-chat-text-message-renderer #message,
yt-live-chat-text-message-renderer #message * {
color: #ffffff !important;
font-family: "Imprima";
font-size: 18px !important;
line-height: 18px !important;
}
/* SuperChat/Fan Funding Messages. */
yt-live-chat-paid-message-renderer #author-name,
yt-live-chat-paid-message-renderer #author-name *,
yt-live-chat-legacy-paid-message-renderer #event-text,
yt-live-chat-legacy-paid-message-renderer #event-text * {
color: #ffffff !important;
font-family: "Changa One";
font-size: 20px !important;
line-height: 20px !important;
}
yt-live-chat-paid-message-renderer #purchase-amount,
yt-live-chat-paid-message-renderer #purchase-amount *,
yt-live-chat-legacy-paid-message-renderer #detail-text,
yt-live-chat-legacy-paid-message-renderer #detail-text * {
color: #ffffff !important;
font-family: "Imprima";
font-size: 18px !important;
line-height: 18px !important;
}
yt-live-chat-paid-message-renderer #content,
yt-live-chat-paid-message-renderer #content * {
color: #ffffff !important;
font-family: "Imprima";
font-size: 18px !important;
line-height: 18px !important;
}
yt-live-chat-paid-message-renderer {
margin: 4px 0 !important;
}
yt-live-chat-legacy-paid-message-renderer {
background-color: #0f9d58 !important;
margin: 4px 0 !important;
}
yt-live-chat-text-message-renderer a,
yt-live-chat-legacy-paid-message-renderer a {
text-decoration: none !important;
}
yt-live-chat-text-message-renderer[is-deleted],
yt-live-chat-legacy-paid-message-renderer[is-deleted] {
display: none !important;
}
yt-live-chat-ticker-renderer {
background-color: transparent !important;
box-shadow: none !important;
}
yt-live-chat-ticker-renderer {
display: none !important;
}
yt-live-chat-ticker-paid-message-item-renderer,
yt-live-chat-ticker-paid-message-item-renderer *,
yt-live-chat-ticker-sponsor-item-renderer,
yt-live-chat-ticker-sponsor-item-renderer * {
color: #ffffff !important;
font-family: "Imprima";
}
yt-live-chat-mode-change-message-renderer,
yt-live-chat-viewer-engagement-message-renderer,
yt-live-chat-restricted-participation-renderer {
display: none !important;
}
`
export const DEFAULT_CONFIG = {
minGiftPrice: 6.911, // $1
mergeSimilarDanmaku: true,
blockGiftDanmaku: true,
blockLevel: 0,
blockNewbie: true,
blockNotMobileVerified: true,
blockKeywords: '',
blockUsers: '',
css: DEFAULT_CSS
}
function mergeConfig (config) {
let res = {}
for (let i in DEFAULT_CONFIG) {
res[i] = i in config ? config[i] : DEFAULT_CONFIG[i]
}
return res
}
export function setLocalConfig (config) {
config = mergeConfig(config)
window.localStorage.config = JSON.stringify(config)
}
export function getLocalConfig () {
if (!window.localStorage.config) {
return DEFAULT_CONFIG
}
return mergeConfig(JSON.parse(window.localStorage.config))
}
export async function createRemoteConfig (config) {
config = mergeConfig(config)
return (await axios.post('/config', config)).data
}
export async function setRemoteConfig (id, config) {
config = mergeConfig(config)
return (await axios.put(`/config/${id}`, config)).data
}
export async function getRemoteConfig (id) {
let config = (await axios.get(`/config/${id}`)).data
return mergeConfig(config)
}
export default {
DEFAULT_CONFIG,
setLocalConfig,
getLocalConfig,
createRemoteConfig,
setRemoteConfig,
getRemoteConfig
}

View File

@ -207,220 +207,3 @@ yt-live-chat-ticker-paid-message-item-renderer #fake-avatar {
border-radius: 24px;
}
/* 以下为自动生成https://chatv2.septapus.com/ */
/* @import url("https://fonts.googleapis.com/css?family=Changa%20One"); */
/* @import url("https://fonts.googleapis.com/css?family=Imprima"); */
@import url("https://fonts.lug.ustc.edu.cn/css?family=Changa%20One");
@import url("https://fonts.lug.ustc.edu.cn/css?family=Imprima");
/* Background colors*/
body {
overflow: hidden;
background-color: rgba(0,0,0,0);
}
/* Transparent background. */
yt-live-chat-renderer {
background-color: transparent !important;
}
yt-live-chat-text-message-renderer,
yt-live-chat-text-message-renderer[is-highlighted] {
background-color: transparent !important;
}
yt-live-chat-text-message-renderer[author-type="owner"],
yt-live-chat-text-message-renderer[author-type="owner"][is-highlighted] {
background-color: transparent !important;
}
yt-live-chat-text-message-renderer[author-type="moderator"],
yt-live-chat-text-message-renderer[author-type="moderator"][is-highlighted] {
background-color: transparent !important;
}
yt-live-chat-text-message-renderer[author-type="member"],
yt-live-chat-text-message-renderer[author-type="member"][is-highlighted] {
background-color: transparent !important;
}
yt-live-chat-author-chip #author-name {
background-color: transparent !important;
}
/* Outlines */
yt-live-chat-renderer * {
text-shadow: -2px -2px #000000,-2px -1px #000000,-2px 0px #000000,-2px 1px #000000,-2px 2px #000000,-1px -2px #000000,-1px -1px #000000,-1px 0px #000000,-1px 1px #000000,-1px 2px #000000,0px -2px #000000,0px -1px #000000,0px 0px #000000,0px 1px #000000,0px 2px #000000,1px -2px #000000,1px -1px #000000,1px 0px #000000,1px 1px #000000,1px 2px #000000,2px -2px #000000,2px -1px #000000,2px 0px #000000,2px 1px #000000,2px 2px #000000;
font-family: "Imprima";
font-size: 18px !important;
line-height: 18px !important;
}
yt-live-chat-text-message-renderer #content,
yt-live-chat-legacy-paid-message-renderer #content {
overflow: initial !important;
}
/* Hide scrollbar. */
yt-live-chat-item-list-renderer #items{
overflow: hidden !important;
}
yt-live-chat-item-list-renderer #item-scroller{
overflow: hidden !important;
}
/* Hide header and input. */
yt-live-chat-header-renderer,
yt-live-chat-message-input-renderer {
display: none !important;
}
/* Reduce side padding. */
yt-live-chat-text-message-renderer,
yt-live-chat-legacy-paid-message-renderer {
padding-left: 4px !important;
padding-right: 4px !important;
}
yt-live-chat-paid-message-renderer #header {
padding-left: 4px !important;
padding-right: 4px !important;
}
/* Avatars. */
yt-live-chat-text-message-renderer #author-photo,
yt-live-chat-paid-message-renderer #author-photo,
yt-live-chat-legacy-paid-message-renderer #author-photo {
width: 24px !important;
height: 24px !important;
border-radius: 24px !important;
margin-right: 6px !important;
background-size: cover;
}
/* Hide badges. */
yt-live-chat-text-message-renderer #author-badges {
display: none !important;
vertical-align: text-top !important;
}
/* Timestamps. */
yt-live-chat-text-message-renderer #timestamp {
color: #999999 !important;
font-family: "Imprima";
font-size: 16px !important;
line-height: 16px !important;
}
/* Badges. */
yt-live-chat-text-message-renderer #author-name[type="owner"],
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
color: #ffd600 !important;
}
yt-live-chat-text-message-renderer #author-name[type="moderator"],
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
color: #5e84f1 !important;
}
yt-live-chat-text-message-renderer #author-name[type="member"],
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
color: #0f9d58 !important;
}
/* Channel names. */
yt-live-chat-text-message-renderer #author-name {
color: #cccccc !important;
font-family: "Changa One";
font-size: 20px !important;
line-height: 20px !important;
}
yt-live-chat-text-message-renderer #author-name::after {
content: ":";
margin-left: 2px;
}
/* Messages. */
yt-live-chat-text-message-renderer #message,
yt-live-chat-text-message-renderer #message * {
color: #ffffff !important;
font-family: "Imprima";
font-size: 18px !important;
line-height: 18px !important;
}
/* SuperChat/Fan Funding Messages. */
yt-live-chat-paid-message-renderer #author-name,
yt-live-chat-paid-message-renderer #author-name *,
yt-live-chat-legacy-paid-message-renderer #event-text,
yt-live-chat-legacy-paid-message-renderer #event-text * {
color: #ffffff !important;
font-family: "Changa One";
font-size: 20px !important;
line-height: 20px !important;
}
yt-live-chat-paid-message-renderer #purchase-amount,
yt-live-chat-paid-message-renderer #purchase-amount *,
yt-live-chat-legacy-paid-message-renderer #detail-text,
yt-live-chat-legacy-paid-message-renderer #detail-text * {
color: #ffffff !important;
font-family: "Imprima";
font-size: 18px !important;
line-height: 18px !important;
}
yt-live-chat-paid-message-renderer #content,
yt-live-chat-paid-message-renderer #content * {
color: #ffffff !important;
font-family: "Imprima";
font-size: 18px !important;
line-height: 18px !important;
}
yt-live-chat-paid-message-renderer {
margin: 4px 0 !important;
}
yt-live-chat-legacy-paid-message-renderer {
background-color: #0f9d58 !important;
margin: 4px 0 !important;
}
yt-live-chat-text-message-renderer a,
yt-live-chat-legacy-paid-message-renderer a {
text-decoration: none !important;
}
yt-live-chat-text-message-renderer[is-deleted],
yt-live-chat-legacy-paid-message-renderer[is-deleted] {
display: none !important;
}
yt-live-chat-ticker-renderer {
background-color: transparent !important;
box-shadow: none !important;
}
yt-live-chat-ticker-renderer {
display: none !important;
}
yt-live-chat-ticker-paid-message-item-renderer,
yt-live-chat-ticker-paid-message-item-renderer *,
yt-live-chat-ticker-sponsor-item-renderer,
yt-live-chat-ticker-sponsor-item-renderer * {
color: #ffffff !important;
font-family: "Imprima";
}
yt-live-chat-mode-change-message-renderer,
yt-live-chat-viewer-engagement-message-renderer,
yt-live-chat-restricted-participation-renderer {
display: none !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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 axios from 'axios'
import App from './App.vue'
import Layout from './layout'
@ -10,6 +11,11 @@ import StyleGenerator from './views/StyleGenerator.vue'
import Room from './views/Room'
import NotFound from './views/NotFound.vue'
if (process.env.NODE_ENV === 'development') {
// 开发时使用localhost:80
axios.defaults.baseURL = 'http://localhost'
}
Vue.use(VueRouter)
Vue.use(ElementUI)

View File

@ -43,52 +43,68 @@
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-divider></el-divider>
<el-form-item label="房间URL" 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-form-item>
<el-form-item>
<el-button type="primary" @click="enterRoom">进入房间</el-button>
<el-button type="primary" @click="popupRoom">弹出房间</el-button>
<el-button type="primary" @click="saveConfig()">保存配置</el-button>
<el-button type="primary" :disabled="!roomUrl" @click="enterRoom">进入房间</el-button>
</el-form-item>
</el-form>
</template>
<script>
import config from '@/api/config'
export default {
name: 'Home',
data() {
return {
form: {
roomId: parseInt(window.localStorage.roomId || '1'),
minGiftPrice: 6.911, // $1
mergeSimilarDanmaku: true,
blockGiftDanmaku: true,
blockLevel: 0,
blockNewbie: true,
blockNotMobileVerified: true,
blockKeywords: '',
blockUsers: '',
css: ''
}
...config.getLocalConfig()
},
roomUrl: ''
}
},
methods: {
saveConfig(callback) {
this.$refs.form.validate(valid => {
saveConfig() {
this.$refs.form.validate(async valid => {
if (!valid) {
return
}
window.localStorage.roomId = this.form.roomId
callback()
config.setLocalConfig(this.form)
try {
if (window.localStorage.configId) {
try {
await config.setRemoteConfig(window.localStorage.configId, this.form)
} catch (e) { // 404
window.localStorage.configId = (await config.createRemoteConfig(this.form)).id
}
} else {
window.localStorage.configId = (await config.createRemoteConfig(this.form)).id
}
} catch (e) {
this.$message.error('保存失败:' + e)
return
}
this.$message({message: '保存成功', type: 'success'})
let resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId},
query: {config_id: window.localStorage.configId}})
this.roomUrl = `http://${window.location.host}${resolved.href}`
})
},
enterRoom() {
this.saveConfig(() => this.$router.push({name: 'room', params: {roomId: this.form.roomId}}))
window.open(this.roomUrl, `room ${this.form.roomId}`, 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
},
popupRoom() {
this.saveConfig(() => {
let resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId}})
window.open(resolved.href, `room ${this.form.roomId}`, 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
})
copyUrl() {
this.$refs.roomUrlInput.select()
document.execCommand('Copy')
}
}
}

View File

@ -33,6 +33,7 @@
</template>
<script>
import config from '@/api/config'
import TextMessage from './TextMessage.vue'
import LegacyPaidMessage from './LegacyPaidMessage.vue'
import PaidMessage from './PaidMessage.vue'
@ -50,13 +51,18 @@ export default {
PaidMessage
},
data() {
let styleElement = document.createElement('style')
styleElement.innerText = config.DEFAULT_CONFIG.css
document.head.appendChild(styleElement)
return {
config: config.DEFAULT_CONFIG,
styleElement,
websocket: null,
messages: [],
nextId: 0
}
},
created() {
async created() {
// 使localhost:80
const url = process.env.NODE_ENV === 'development' ? 'ws://localhost/chat' : `ws://${window.location.host}/chat`
this.websocket = new WebSocket(url)
@ -85,7 +91,7 @@ export default {
break
case COMMAND_ADD_GIFT:
price = body.data.totalCoin / 1000
if (price < 6.911) //
if (price < this.config.minGiftPrice) //
break
message = {
id: this.nextId++,
@ -112,8 +118,18 @@ export default {
this.messages.shift()
}
}
if (this.$route.query.config_id) {
try {
this.config = await config.getRemoteConfig(this.$route.query.config_id)
this.styleElement.innerText = this.config.css
} catch (e) {
this.$message.error('获取配置失败:' + e)
}
}
},
beforeDestroy() {
document.head.removeChild(this.styleElement)
this.websocket.close()
},
updated() {

18
main.py
View File

@ -7,20 +7,15 @@ import os
import tornado.ioloop
import tornado.web
import chat
import views.chat
import views.config
import views.main
logger = logging.getLogger(__name__)
WEB_ROOT = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
# noinspection PyAbstractClass
class MainHandler(tornado.web.StaticFileHandler):
"""为了使用Vue Router的history模式把所有请求转发到index.html"""
async def get(self, *args, **kwargs):
await super().get('index.html', *args, **kwargs)
def main():
parser = argparse.ArgumentParser(description='用于OBS的仿YouTube风格的bilibili直播聊天层')
parser.add_argument('--host', help='服务器host默认为127.0.0.1', default='127.0.0.1')
@ -37,10 +32,13 @@ def main():
app = tornado.web.Application(
[
(r'/chat', chat.ChatHandler),
(r'/chat', views.chat.ChatHandler),
(r'/config', views.config.ConfigsHandler),
(r'/config/(.+)', views.config.ConfigHandler),
(r'/((css|img|js)/.*)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
(r'/(favicon\.ico)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
(r'/.*', MainHandler, {'path': WEB_ROOT})
(r'/.*', views.main.MainHandler, {'path': WEB_ROOT})
],
websocket_ping_interval=30,
debug=args.debug,

28
views/base.py Normal file
View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import json
import tornado.web
# noinspection PyAbstractClass
class ApiHandler(tornado.web.RequestHandler):
def set_default_headers(self):
# 跨域测试用
if not self.application.settings['debug']:
return
self.set_header('Access-Control-Allow-Origin', '*')
self.set_header('Access-Control-Allow-Methods', 'OPTIONS, PUT, POST, GET, DELETE')
if 'Access-Control-Request-Headers' in self.request.headers:
self.set_header('Access-Control-Allow-Headers',
self.request.headers['Access-Control-Request-Headers'])
def prepare(self):
if self.request.headers.get('Content-Type', '').startswith('application/json'):
self.json_args = json.loads(self.request.body)
else:
self.json_args = None
async def options(self, *_args, **_kwargs):
# 跨域测试用
self.set_status(204 if self.application.settings['debug'] else 405)

50
views/config.py Normal file
View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
import uuid
import views.base
from typing import *
configs: Dict[str, dict] = {}
ALLOWED_FIELDS = (
'minGiftPrice', 'mergeSimilarDanmaku', 'blockGiftDanmaku', 'blockLevel',
'blockNewbie', 'blockNotMobileVerified', 'blockKeywords', 'blockUsers',
'css'
)
# noinspection PyAbstractClass
class ConfigsHandler(views.base.ApiHandler):
async def post(self):
config_id = str(uuid.uuid4())
config = {
name: self.json_args[name] for name in ALLOWED_FIELDS
}
config['id'] = config_id
configs[config_id] = config
self.set_status(201)
self.write(config)
if len(configs) > 10000:
for _, key in zip(range(100), configs):
del configs[key]
# noinspection PyAbstractClass
class ConfigHandler(views.base.ApiHandler):
async def put(self, config_id):
config = configs.get(config_id, None)
if config is None:
self.set_status(404)
return
for name in ALLOWED_FIELDS:
config[name] = self.json_args[name]
self.write(config)
async def get(self, config_id):
config = configs.get(config_id, None)
if config is None:
self.set_status(404)
return
self.write(config)

10
views/main.py Normal file
View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
import tornado.web
# noinspection PyAbstractClass
class MainHandler(tornado.web.StaticFileHandler):
"""为了使用Vue Router的history模式把所有请求转发到index.html"""
async def get(self, *args, **kwargs):
await super().get('index.html', *args, **kwargs)