mirror of
https://github.com/xfgryujk/blivechat.git
synced 2025-03-26 20:10:43 +08:00
Merge branch 'dev'
This commit is contained in:
commit
659fddc4d5
@ -17,5 +17,5 @@ README.md
|
|||||||
|
|
||||||
# runtime data
|
# runtime data
|
||||||
data/*
|
data/*
|
||||||
!data/config.ini
|
!data/config.example.ini
|
||||||
log/*
|
log/*
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -105,5 +105,6 @@ venv.bak/
|
|||||||
|
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
data/database.db
|
data/*
|
||||||
*.log*
|
!data/config.example.ini
|
||||||
|
log/*
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# 运行时
|
# 运行时
|
||||||
FROM python:3.6.8-slim-stretch
|
FROM python:3.7.10-slim-stretch
|
||||||
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak \
|
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 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-updates main contrib non-free">>/etc/apt/sources.list \
|
||||||
|
78
api/chat.py
78
api/chat.py
@ -32,7 +32,7 @@ class Command(enum.IntEnum):
|
|||||||
UPDATE_TRANSLATION = 7
|
UPDATE_TRANSLATION = 7
|
||||||
|
|
||||||
|
|
||||||
_http_session = aiohttp.ClientSession()
|
_http_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
|
||||||
|
|
||||||
room_manager: Optional['RoomManager'] = None
|
room_manager: Optional['RoomManager'] = None
|
||||||
|
|
||||||
@ -43,6 +43,8 @@ def init():
|
|||||||
|
|
||||||
|
|
||||||
class Room(blivedm.BLiveClient):
|
class Room(blivedm.BLiveClient):
|
||||||
|
HEARTBEAT_INTERVAL = 10
|
||||||
|
|
||||||
# 重新定义parse_XXX是为了减少对字段名的依赖,防止B站改字段名
|
# 重新定义parse_XXX是为了减少对字段名的依赖,防止B站改字段名
|
||||||
def __parse_danmaku(self, command):
|
def __parse_danmaku(self, command):
|
||||||
info = command['info']
|
info = command['info']
|
||||||
@ -97,7 +99,7 @@ class Room(blivedm.BLiveClient):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, room_id):
|
def __init__(self, room_id):
|
||||||
super().__init__(room_id, session=_http_session, heartbeat_interval=10)
|
super().__init__(room_id, session=_http_session, heartbeat_interval=self.HEARTBEAT_INTERVAL)
|
||||||
self.clients: List['ChatHandler'] = []
|
self.clients: List['ChatHandler'] = []
|
||||||
self.auto_translate_count = 0
|
self.auto_translate_count = 0
|
||||||
|
|
||||||
@ -365,34 +367,68 @@ class RoomManager:
|
|||||||
|
|
||||||
# noinspection PyAbstractClass
|
# noinspection PyAbstractClass
|
||||||
class ChatHandler(tornado.websocket.WebSocketHandler):
|
class ChatHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
HEARTBEAT_INTERVAL = 10
|
||||||
|
RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + 5
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._close_on_timeout_future = None
|
self._heartbeat_timer_handle = None
|
||||||
|
self._receive_timeout_timer_handle = None
|
||||||
|
|
||||||
self.room_id = None
|
self.room_id = None
|
||||||
self.auto_translate = False
|
self.auto_translate = False
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
logger.info('Websocket connected %s', self.request.remote_ip)
|
logger.info('Websocket connected %s', self.request.remote_ip)
|
||||||
self._close_on_timeout_future = asyncio.ensure_future(self._close_on_timeout())
|
self._heartbeat_timer_handle = asyncio.get_event_loop().call_later(
|
||||||
|
self.HEARTBEAT_INTERVAL, self._on_send_heartbeat
|
||||||
|
)
|
||||||
|
self._refresh_receive_timeout_timer()
|
||||||
|
|
||||||
async def _close_on_timeout(self):
|
def _on_send_heartbeat(self):
|
||||||
try:
|
self.send_message(Command.HEARTBEAT, {})
|
||||||
# 超过一定时间还没加入房间则断开
|
self._heartbeat_timer_handle = asyncio.get_event_loop().call_later(
|
||||||
await asyncio.sleep(10)
|
self.HEARTBEAT_INTERVAL, self._on_send_heartbeat
|
||||||
logger.warning('Client %s joining room timed out', self.request.remote_ip)
|
)
|
||||||
self.close()
|
|
||||||
except (asyncio.CancelledError, tornado.websocket.WebSocketClosedError):
|
def _refresh_receive_timeout_timer(self):
|
||||||
pass
|
if self._receive_timeout_timer_handle is not None:
|
||||||
|
self._receive_timeout_timer_handle.cancel()
|
||||||
|
self._receive_timeout_timer_handle = asyncio.get_event_loop().call_later(
|
||||||
|
self.RECEIVE_TIMEOUT, self._on_receive_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_receive_timeout(self):
|
||||||
|
logger.warning('Client %s timed out', self.request.remote_ip)
|
||||||
|
self._receive_timeout_timer_handle = None
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
logger.info('Websocket disconnected %s room: %s', self.request.remote_ip, str(self.room_id))
|
||||||
|
if self.has_joined_room:
|
||||||
|
room_manager.del_client(self.room_id, self)
|
||||||
|
if self._heartbeat_timer_handle is not None:
|
||||||
|
self._heartbeat_timer_handle.cancel()
|
||||||
|
self._heartbeat_timer_handle = None
|
||||||
|
if self._receive_timeout_timer_handle is not None:
|
||||||
|
self._receive_timeout_timer_handle.cancel()
|
||||||
|
self._receive_timeout_timer_handle = None
|
||||||
|
|
||||||
def on_message(self, message):
|
def on_message(self, message):
|
||||||
try:
|
try:
|
||||||
|
# 超时没有加入房间也断开
|
||||||
|
if self.has_joined_room:
|
||||||
|
self._refresh_receive_timeout_timer()
|
||||||
|
|
||||||
body = json.loads(message)
|
body = json.loads(message)
|
||||||
cmd = body['cmd']
|
cmd = body['cmd']
|
||||||
if cmd == Command.HEARTBEAT:
|
if cmd == Command.HEARTBEAT:
|
||||||
return
|
pass
|
||||||
elif cmd == Command.JOIN_ROOM:
|
elif cmd == Command.JOIN_ROOM:
|
||||||
if self.has_joined_room:
|
if self.has_joined_room:
|
||||||
return
|
return
|
||||||
|
self._refresh_receive_timeout_timer()
|
||||||
|
|
||||||
self.room_id = int(body['data']['roomId'])
|
self.room_id = int(body['data']['roomId'])
|
||||||
logger.info('Client %s is joining room %d', self.request.remote_ip, self.room_id)
|
logger.info('Client %s is joining room %d', self.request.remote_ip, self.room_id)
|
||||||
try:
|
try:
|
||||||
@ -402,21 +438,11 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
asyncio.ensure_future(room_manager.add_client(self.room_id, self))
|
asyncio.ensure_future(room_manager.add_client(self.room_id, self))
|
||||||
self._close_on_timeout_future.cancel()
|
|
||||||
self._close_on_timeout_future = None
|
|
||||||
else:
|
else:
|
||||||
logger.warning('Unknown cmd, client: %s, cmd: %d, body: %s', self.request.remote_ip, cmd, body)
|
logger.warning('Unknown cmd, client: %s, cmd: %d, body: %s', self.request.remote_ip, cmd, body)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('on_message error, client: %s, message: %s', self.request.remote_ip, message)
|
logger.exception('on_message error, client: %s, message: %s', self.request.remote_ip, message)
|
||||||
|
|
||||||
def on_close(self):
|
|
||||||
logger.info('Websocket disconnected %s room: %s', self.request.remote_ip, str(self.room_id))
|
|
||||||
if self.has_joined_room:
|
|
||||||
room_manager.del_client(self.room_id, self)
|
|
||||||
if self._close_on_timeout_future is not None:
|
|
||||||
self._close_on_timeout_future.cancel()
|
|
||||||
self._close_on_timeout_future = None
|
|
||||||
|
|
||||||
# 跨域测试用
|
# 跨域测试用
|
||||||
def check_origin(self, origin):
|
def check_origin(self, origin):
|
||||||
if self.application.settings['debug']:
|
if self.application.settings['debug']:
|
||||||
@ -432,7 +458,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
|
|||||||
try:
|
try:
|
||||||
self.write_message(body)
|
self.write_message(body)
|
||||||
except tornado.websocket.WebSocketClosedError:
|
except tornado.websocket.WebSocketClosedError:
|
||||||
self.on_close()
|
self.close()
|
||||||
|
|
||||||
async def on_join_room(self):
|
async def on_join_room(self):
|
||||||
if self.application.settings['debug']:
|
if self.application.settings['debug']:
|
||||||
@ -550,7 +576,7 @@ class RoomInfoHandler(api.base.ApiHandler):
|
|||||||
res.status, res.reason)
|
res.status, res.reason)
|
||||||
return room_id, 0
|
return room_id, 0
|
||||||
data = await res.json()
|
data = await res.json()
|
||||||
except aiohttp.ClientConnectionError:
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||||
logger.exception('room %d _get_room_info failed', room_id)
|
logger.exception('room %d _get_room_info failed', room_id)
|
||||||
return room_id, 0
|
return room_id, 0
|
||||||
|
|
||||||
@ -574,7 +600,7 @@ class RoomInfoHandler(api.base.ApiHandler):
|
|||||||
# res.status, res.reason)
|
# res.status, res.reason)
|
||||||
# return cls._host_server_list_cache
|
# return cls._host_server_list_cache
|
||||||
# data = await res.json()
|
# data = await res.json()
|
||||||
# except aiohttp.ClientConnectionError:
|
# except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||||
# logger.exception('room %d _get_server_host_list failed', room_id)
|
# logger.exception('room %d _get_server_host_list failed', room_id)
|
||||||
# return cls._host_server_list_cache
|
# return cls._host_server_list_cache
|
||||||
#
|
#
|
||||||
|
2
blivedm
2
blivedm
@ -1 +1 @@
|
|||||||
Subproject commit 8d8cc8c2706d62bbfa74cbc36f536b9717fe8f36
|
Subproject commit 4669b2c1c9a1654db340d02ff16c9f88be661d9f
|
104
config.py
104
config.py
@ -7,7 +7,10 @@ from typing import *
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_PATH = os.path.join('data', 'config.ini')
|
CONFIG_PATH_LIST = [
|
||||||
|
os.path.join('data', 'config.ini'),
|
||||||
|
os.path.join('data', 'config.example.ini')
|
||||||
|
]
|
||||||
|
|
||||||
_config: Optional['AppConfig'] = None
|
_config: Optional['AppConfig'] = None
|
||||||
|
|
||||||
@ -21,8 +24,16 @@ def init():
|
|||||||
|
|
||||||
|
|
||||||
def reload():
|
def reload():
|
||||||
|
config_path = ''
|
||||||
|
for path in CONFIG_PATH_LIST:
|
||||||
|
if os.path.exists(path):
|
||||||
|
config_path = path
|
||||||
|
break
|
||||||
|
if config_path == '':
|
||||||
|
return False
|
||||||
|
|
||||||
config = AppConfig()
|
config = AppConfig()
|
||||||
if not config.load(CONFIG_PATH):
|
if not config.load(config_path):
|
||||||
return False
|
return False
|
||||||
global _config
|
global _config
|
||||||
_config = config
|
_config = config
|
||||||
@ -36,31 +47,86 @@ def get_config():
|
|||||||
class AppConfig:
|
class AppConfig:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.database_url = 'sqlite:///data/database.db'
|
self.database_url = 'sqlite:///data/database.db'
|
||||||
self.enable_translate = True
|
|
||||||
self.allow_translate_rooms = {}
|
|
||||||
self.tornado_xheaders = False
|
self.tornado_xheaders = False
|
||||||
self.loader_url = ''
|
self.loader_url = ''
|
||||||
|
|
||||||
|
self.fetch_avatar_interval = 3.5
|
||||||
|
self.fetch_avatar_max_queue_size = 2
|
||||||
|
self.avatar_cache_size = 50000
|
||||||
|
|
||||||
|
self.enable_translate = True
|
||||||
|
self.allow_translate_rooms = set()
|
||||||
|
self.translation_cache_size = 50000
|
||||||
|
self.translator_configs = []
|
||||||
|
|
||||||
def load(self, path):
|
def load(self, path):
|
||||||
try:
|
try:
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(path, 'utf-8')
|
config.read(path, 'utf-8')
|
||||||
|
|
||||||
app_section = config['app']
|
self._load_app_config(config)
|
||||||
self.database_url = app_section['database_url']
|
self._load_translator_configs(config)
|
||||||
self.enable_translate = app_section.getboolean('enable_translate')
|
except Exception:
|
||||||
|
|
||||||
allow_translate_rooms = app_section['allow_translate_rooms']
|
|
||||||
if allow_translate_rooms == '':
|
|
||||||
self.allow_translate_rooms = {}
|
|
||||||
else:
|
|
||||||
allow_translate_rooms = allow_translate_rooms.split(',')
|
|
||||||
self.allow_translate_rooms = set(map(lambda id_: int(id_.strip()), allow_translate_rooms))
|
|
||||||
|
|
||||||
self.tornado_xheaders = app_section.getboolean('tornado_xheaders')
|
|
||||||
self.loader_url = app_section['loader_url']
|
|
||||||
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
logger.exception('Failed to load config:')
|
logger.exception('Failed to load config:')
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _load_app_config(self, config):
|
||||||
|
app_section = config['app']
|
||||||
|
self.database_url = app_section['database_url']
|
||||||
|
self.tornado_xheaders = app_section.getboolean('tornado_xheaders')
|
||||||
|
self.loader_url = app_section['loader_url']
|
||||||
|
|
||||||
|
self.fetch_avatar_interval = app_section.getfloat('fetch_avatar_interval')
|
||||||
|
self.fetch_avatar_max_queue_size = app_section.getint('fetch_avatar_max_queue_size')
|
||||||
|
self.avatar_cache_size = app_section.getint('avatar_cache_size')
|
||||||
|
|
||||||
|
self.enable_translate = app_section.getboolean('enable_translate')
|
||||||
|
self.allow_translate_rooms = _str_to_list(app_section['allow_translate_rooms'], int, set)
|
||||||
|
self.translation_cache_size = app_section.getint('translation_cache_size')
|
||||||
|
|
||||||
|
def _load_translator_configs(self, config):
|
||||||
|
app_section = config['app']
|
||||||
|
section_names = _str_to_list(app_section['translator_configs'])
|
||||||
|
translator_configs = []
|
||||||
|
for section_name in section_names:
|
||||||
|
section = config[section_name]
|
||||||
|
type_ = section['type']
|
||||||
|
|
||||||
|
translator_config = {
|
||||||
|
'type': type_,
|
||||||
|
'query_interval': section.getfloat('query_interval'),
|
||||||
|
'max_queue_size': section.getint('max_queue_size')
|
||||||
|
}
|
||||||
|
if type_ == 'TencentTranslateFree':
|
||||||
|
translator_config['source_language'] = section['source_language']
|
||||||
|
translator_config['target_language'] = section['target_language']
|
||||||
|
elif type_ == 'BilibiliTranslateFree':
|
||||||
|
pass
|
||||||
|
elif type_ == 'TencentTranslate':
|
||||||
|
translator_config['source_language'] = section['source_language']
|
||||||
|
translator_config['target_language'] = section['target_language']
|
||||||
|
translator_config['secret_id'] = section['secret_id']
|
||||||
|
translator_config['secret_key'] = section['secret_key']
|
||||||
|
translator_config['region'] = section['region']
|
||||||
|
elif type_ == 'BaiduTranslate':
|
||||||
|
translator_config['source_language'] = section['source_language']
|
||||||
|
translator_config['target_language'] = section['target_language']
|
||||||
|
translator_config['app_id'] = section['app_id']
|
||||||
|
translator_config['secret'] = section['secret']
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Invalid translator type: {type_}')
|
||||||
|
|
||||||
|
translator_configs.append(translator_config)
|
||||||
|
self.translator_configs = translator_configs
|
||||||
|
|
||||||
|
|
||||||
|
def _str_to_list(value, item_type: Type=str, container_type: Type=list):
|
||||||
|
value = value.strip()
|
||||||
|
if value == '':
|
||||||
|
return container_type()
|
||||||
|
items = value.split(',')
|
||||||
|
items = map(lambda item: item.strip(), items)
|
||||||
|
if item_type is not str:
|
||||||
|
items = map(lambda item: item_type(item), items)
|
||||||
|
return container_type(items)
|
||||||
|
142
data/config.example.ini
Normal file
142
data/config.example.ini
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
# 如果要修改配置,可以复制此文件并重命名为“config.ini”再修改
|
||||||
|
# If you want to modify the configuration, copy this file and rename it to "config.ini" and edit
|
||||||
|
|
||||||
|
[app]
|
||||||
|
# 数据库配置,见https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
||||||
|
# See https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
||||||
|
database_url = sqlite:///data/database.db
|
||||||
|
|
||||||
|
# 如果使用了nginx之类的反向代理服务器,设置为true
|
||||||
|
# Set to true if you are using a reverse proxy server such as nginx
|
||||||
|
tornado_xheaders = false
|
||||||
|
|
||||||
|
# 加载器URL,本地使用时加载器可以让你先运行OBS再运行blivechat。如果为空,不使用加载器
|
||||||
|
# **自建服务器时强烈建议不使用加载器**,否则可能因为混合HTTP和HTTPS等原因加载不出来
|
||||||
|
# Use a loader so that you can run OBS before blivechat. If empty, no loader is used
|
||||||
|
loader_url = https://xfgryujk.sinacloud.net/blivechat/loader.html
|
||||||
|
|
||||||
|
|
||||||
|
# 获取头像间隔时间(秒)。如果小于3秒有很大概率被服务器拉黑
|
||||||
|
# Interval between fetching avatar (s). At least 3 seconds is recommended
|
||||||
|
fetch_avatar_interval = 3.5
|
||||||
|
|
||||||
|
# 获取头像最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
|
||||||
|
# Maximum queue length for fetching avatar
|
||||||
|
fetch_avatar_max_queue_size = 2
|
||||||
|
|
||||||
|
# 头像缓存数量
|
||||||
|
# Number of avatar caches
|
||||||
|
avatar_cache_size = 50000
|
||||||
|
|
||||||
|
|
||||||
|
# 允许自动翻译到日语
|
||||||
|
# Enable auto translate to Japanese
|
||||||
|
enable_translate = true
|
||||||
|
|
||||||
|
# 允许翻译的房间ID,以逗号分隔。如果为空,允许所有房间
|
||||||
|
# Comma separated room IDs in which translation are not allowed. If empty, all are allowed
|
||||||
|
# Example: allow_translate_rooms = 4895312,22347054,21693691
|
||||||
|
allow_translate_rooms =
|
||||||
|
|
||||||
|
# 翻译缓存数量
|
||||||
|
# Number of translation caches
|
||||||
|
translation_cache_size = 50000
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
# 以下是给字幕组看的,实在懒得翻译了_(:з」∠)_。如果你不了解以下参数的意思,使用默认值就好
|
||||||
|
# **The following is for translation team. Leave it default if you don't know its meaning**
|
||||||
|
# -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# 翻译器配置,索引到下面的配置节。可以以逗号分隔配置多个翻译器,翻译时会自动负载均衡
|
||||||
|
# 配置多个翻译器可以增加额度、增加QPS、容灾
|
||||||
|
# 不同配置可以使用同一个类型,但要使用不同的账号,否则还是会遇到额度、调用频率限制
|
||||||
|
translator_configs = tencent_translate_free,bilibili_translate_free
|
||||||
|
|
||||||
|
|
||||||
|
[tencent_translate_free]
|
||||||
|
# 类型:腾讯翻译白嫖版。使用了网页版的接口,**将来可能失效**
|
||||||
|
type = TencentTranslateFree
|
||||||
|
|
||||||
|
# 请求间隔时间(秒),等于 1 / QPS。目前没有遇到此接口有调用频率限制,10QPS应该够用了
|
||||||
|
query_interval = 0.1
|
||||||
|
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
|
||||||
|
max_queue_size = 100
|
||||||
|
|
||||||
|
# 自动:auto;中文:zh;日语:jp;英语:en;韩语:kr
|
||||||
|
# 完整语言列表见文档:https://cloud.tencent.com/document/product/551/15619
|
||||||
|
# 源语言
|
||||||
|
source_language = zh
|
||||||
|
# 目标语言
|
||||||
|
target_language = jp
|
||||||
|
|
||||||
|
|
||||||
|
[bilibili_translate_free]
|
||||||
|
# 类型:B站翻译白嫖版。使用了B站直播网页的接口,**将来可能失效**。目前B站翻译后端是百度翻译
|
||||||
|
type = BilibiliTranslateFree
|
||||||
|
|
||||||
|
# 请求间隔时间(秒),等于 1 / QPS。目前此接口频率限制是3秒一次
|
||||||
|
query_interval = 3.1
|
||||||
|
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
|
||||||
|
max_queue_size = 3
|
||||||
|
|
||||||
|
|
||||||
|
[tencent_translate]
|
||||||
|
# 文档:https://cloud.tencent.com/product/tmt
|
||||||
|
# 定价:https://cloud.tencent.com/document/product/551/35017
|
||||||
|
# * 文本翻译的每月免费额度为5百万字符
|
||||||
|
# * 文本翻译当月需付费字符数小于100百万字符(1亿字符)时,刊例价为58元/每百万字符
|
||||||
|
# * 文本翻译当月需付费字符数大于等于100百万字符(1亿字符)时,刊例价为50元/每百万字符
|
||||||
|
# 限制:https://cloud.tencent.com/document/product/551/32572
|
||||||
|
# * 文本翻译最高QPS为5
|
||||||
|
|
||||||
|
# 类型:腾讯翻译
|
||||||
|
type = TencentTranslate
|
||||||
|
|
||||||
|
# 请求间隔时间(秒),等于 1 / QPS。理论上最高QPS为5,实际测试是3
|
||||||
|
query_interval = 0.333
|
||||||
|
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
|
||||||
|
max_queue_size = 30
|
||||||
|
|
||||||
|
# 自动:auto;中文:zh;日语:jp;英语:en;韩语:kr
|
||||||
|
# 完整语言列表见文档:https://cloud.tencent.com/document/product/551/15619
|
||||||
|
# 源语言
|
||||||
|
source_language = zh
|
||||||
|
# 目标语言
|
||||||
|
target_language = jp
|
||||||
|
|
||||||
|
# 腾讯云API密钥
|
||||||
|
secret_id =
|
||||||
|
secret_key =
|
||||||
|
|
||||||
|
# 腾讯云地域参数,用来标识希望操作哪个地域的数据
|
||||||
|
# 北京:ap-beijing;上海:ap-shanghai;香港:ap-hongkong;首尔:ap-seoul
|
||||||
|
# 完整地域列表见文档:https://cloud.tencent.com/document/api/551/15615#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
|
||||||
|
region = ap-shanghai
|
||||||
|
|
||||||
|
|
||||||
|
[baidu_translate]
|
||||||
|
# 文档:https://fanyi-api.baidu.com/
|
||||||
|
# 定价:https://fanyi-api.baidu.com/product/112
|
||||||
|
# * 标准版完全免费,不限使用字符量(QPS=1)
|
||||||
|
# * 高级版每月前200万字符免费,超出后仅收取超出部分费用(QPS=10),49元/百万字符
|
||||||
|
# * 尊享版每月前200万字符免费,超出后仅收取超出部分费用(QPS=100),49元/百万字符
|
||||||
|
|
||||||
|
# 类型:百度翻译
|
||||||
|
type = BaiduTranslate
|
||||||
|
|
||||||
|
# 请求间隔时间(秒),等于 1 / QPS
|
||||||
|
query_interval = 1.5
|
||||||
|
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
|
||||||
|
max_queue_size = 9
|
||||||
|
|
||||||
|
# 自动:auto;中文:zh;日语:jp;英语:en;韩语:kor
|
||||||
|
# 完整语言列表见文档:https://fanyi-api.baidu.com/doc/21
|
||||||
|
# 源语言
|
||||||
|
source_language = zh
|
||||||
|
# 目标语言
|
||||||
|
target_language = jp
|
||||||
|
|
||||||
|
# 百度翻译开放平台应用ID和密钥
|
||||||
|
app_id =
|
||||||
|
secret =
|
@ -1,22 +0,0 @@
|
|||||||
[app]
|
|
||||||
# 数据库配置,见https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
|
||||||
# See https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
|
||||||
database_url = sqlite:///data/database.db
|
|
||||||
|
|
||||||
# 允许自动翻译到日语
|
|
||||||
# Enable auto translate to Japanese
|
|
||||||
enable_translate = true
|
|
||||||
|
|
||||||
# 允许翻译的房间ID,以逗号分隔。如果为空,允许所有房间
|
|
||||||
# Comma separated room IDs in which translation are not allowed. If empty, all are allowed
|
|
||||||
# Example: allow_translate_rooms = 4895312,22347054,21693691
|
|
||||||
allow_translate_rooms =
|
|
||||||
|
|
||||||
# 如果使用了nginx之类的反向代理服务器,设置为true
|
|
||||||
# Set to true if you are using a reverse proxy server such as nginx
|
|
||||||
tornado_xheaders = false
|
|
||||||
|
|
||||||
# 加载器URL,本地使用时加载器可以让你先运行OBS再运行blivechat。如果为空,不使用加载器
|
|
||||||
# **自建服务器时强烈建议不使用加载器**,否则可能因为混合HTTP和HTTPS等原因加载不出来
|
|
||||||
# Use a loader so that you can run OBS before blivechat. If empty, no loader is used
|
|
||||||
loader_url = https://xfgryujk.sinacloud.net/blivechat/loader.html
|
|
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -19,3 +19,6 @@ yarn-error.log*
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
'@vue/app'
|
'@vue/cli-plugin-babel/preset'
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
[
|
[
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.21.1",
|
||||||
"core-js": "^2.6.5",
|
"core-js": "^3.6.5",
|
||||||
"downloadjs": "^1.4.7",
|
"downloadjs": "^1.4.7",
|
||||||
"element-ui": "^2.9.1",
|
"element-ui": "^2.9.1",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
@ -19,13 +19,13 @@
|
|||||||
"vue-router": "^3.0.6"
|
"vue-router": "^3.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^3.7.0",
|
"@vue/cli-plugin-babel": "^4.5.12",
|
||||||
"@vue/cli-plugin-eslint": "^3.7.0",
|
"@vue/cli-plugin-eslint": "^4.5.12",
|
||||||
"@vue/cli-service": "^4.2.2",
|
"@vue/cli-service": "~4.5.12",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-plugin-component": "^1.1.1",
|
"babel-plugin-component": "^1.1.1",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-vue": "^5.0.0",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
"vue-template-compiler": "^2.5.21"
|
"vue-template-compiler": "^2.5.21"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
@ -6,8 +6,8 @@ import * as avatar from './avatar'
|
|||||||
|
|
||||||
const HEADER_SIZE = 16
|
const HEADER_SIZE = 16
|
||||||
|
|
||||||
// const WS_BODY_PROTOCOL_VERSION_NORMAL = 0
|
// const WS_BODY_PROTOCOL_VERSION_INFLATE = 0
|
||||||
// const WS_BODY_PROTOCOL_VERSION_INT = 1 // 用于心跳包
|
// const WS_BODY_PROTOCOL_VERSION_NORMAL = 1
|
||||||
const WS_BODY_PROTOCOL_VERSION_DEFLATE = 2
|
const WS_BODY_PROTOCOL_VERSION_DEFLATE = 2
|
||||||
|
|
||||||
// const OP_HANDSHAKE = 0
|
// const OP_HANDSHAKE = 0
|
||||||
@ -32,6 +32,9 @@ const OP_AUTH_REPLY = 8
|
|||||||
// const MinBusinessOp = 1000
|
// const MinBusinessOp = 1000
|
||||||
// const MaxBusinessOp = 10000
|
// const MaxBusinessOp = 10000
|
||||||
|
|
||||||
|
const HEARTBEAT_INTERVAL = 10 * 1000
|
||||||
|
const RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + 5 * 1000
|
||||||
|
|
||||||
let textEncoder = new TextEncoder()
|
let textEncoder = new TextEncoder()
|
||||||
let textDecoder = new TextDecoder()
|
let textDecoder = new TextDecoder()
|
||||||
|
|
||||||
@ -55,6 +58,7 @@ export default class ChatClientDirect {
|
|||||||
this.retryCount = 0
|
this.retryCount = 0
|
||||||
this.isDestroying = false
|
this.isDestroying = false
|
||||||
this.heartbeatTimerId = null
|
this.heartbeatTimerId = null
|
||||||
|
this.receiveTimeoutTimerId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async start () {
|
async start () {
|
||||||
@ -120,15 +124,33 @@ export default class ChatClientDirect {
|
|||||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||||
this.websocket.onclose = this.onWsClose.bind(this)
|
this.websocket.onclose = this.onWsClose.bind(this)
|
||||||
this.websocket.onmessage = this.onWsMessage.bind(this)
|
this.websocket.onmessage = this.onWsMessage.bind(this)
|
||||||
this.heartbeatTimerId = window.setInterval(this.sendHeartbeat.bind(this), 10 * 1000)
|
}
|
||||||
|
|
||||||
|
onWsOpen () {
|
||||||
|
this.sendAuth()
|
||||||
|
this.heartbeatTimerId = window.setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
|
||||||
|
this.refreshReceiveTimeoutTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
sendHeartbeat () {
|
sendHeartbeat () {
|
||||||
this.websocket.send(this.makePacket({}, OP_HEARTBEAT))
|
this.websocket.send(this.makePacket({}, OP_HEARTBEAT))
|
||||||
}
|
}
|
||||||
|
|
||||||
onWsOpen () {
|
refreshReceiveTimeoutTimer() {
|
||||||
this.sendAuth()
|
if (this.receiveTimeoutTimerId) {
|
||||||
|
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||||
|
}
|
||||||
|
this.receiveTimeoutTimerId = window.setTimeout(this.onReceiveTimeout.bind(this), RECEIVE_TIMEOUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
onReceiveTimeout() {
|
||||||
|
window.console.warn('接收消息超时')
|
||||||
|
this.receiveTimeoutTimerId = null
|
||||||
|
|
||||||
|
// 直接丢弃阻塞的websocket,不等onclose回调了
|
||||||
|
this.websocket.onopen = this.websocket.onclose = this.websocket.onmessage = null
|
||||||
|
this.websocket.close()
|
||||||
|
this.onWsClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
onWsClose () {
|
onWsClose () {
|
||||||
@ -137,19 +159,26 @@ export default class ChatClientDirect {
|
|||||||
window.clearInterval(this.heartbeatTimerId)
|
window.clearInterval(this.heartbeatTimerId)
|
||||||
this.heartbeatTimerId = null
|
this.heartbeatTimerId = null
|
||||||
}
|
}
|
||||||
|
if (this.receiveTimeoutTimerId) {
|
||||||
|
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||||
|
this.receiveTimeoutTimerId = null
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isDestroying) {
|
if (this.isDestroying) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.console.log(`掉线重连中${++this.retryCount}`)
|
window.console.warn(`掉线重连中${++this.retryCount}`)
|
||||||
window.setTimeout(this.wsConnect.bind(this), 1000)
|
window.setTimeout(this.wsConnect.bind(this), 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
onWsMessage (event) {
|
onWsMessage (event) {
|
||||||
|
this.refreshReceiveTimeoutTimer()
|
||||||
this.retryCount = 0
|
this.retryCount = 0
|
||||||
if (!(event.data instanceof ArrayBuffer)) {
|
if (!(event.data instanceof ArrayBuffer)) {
|
||||||
window.console.warn('未知的websocket消息:', event.data)
|
window.console.warn('未知的websocket消息:', event.data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = new Uint8Array(event.data)
|
let data = new Uint8Array(event.data)
|
||||||
this.handlerMessage(data)
|
this.handlerMessage(data)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,9 @@ const COMMAND_ADD_SUPER_CHAT = 5
|
|||||||
const COMMAND_DEL_SUPER_CHAT = 6
|
const COMMAND_DEL_SUPER_CHAT = 6
|
||||||
const COMMAND_UPDATE_TRANSLATION = 7
|
const COMMAND_UPDATE_TRANSLATION = 7
|
||||||
|
|
||||||
|
const HEARTBEAT_INTERVAL = 10 * 1000
|
||||||
|
const RECEIVE_TIMEOUT = HEARTBEAT_INTERVAL + 5 * 1000
|
||||||
|
|
||||||
export default class ChatClientRelay {
|
export default class ChatClientRelay {
|
||||||
constructor (roomId, autoTranslate) {
|
constructor (roomId, autoTranslate) {
|
||||||
this.roomId = roomId
|
this.roomId = roomId
|
||||||
@ -23,6 +26,7 @@ export default class ChatClientRelay {
|
|||||||
this.retryCount = 0
|
this.retryCount = 0
|
||||||
this.isDestroying = false
|
this.isDestroying = false
|
||||||
this.heartbeatTimerId = null
|
this.heartbeatTimerId = null
|
||||||
|
this.receiveTimeoutTimerId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
start () {
|
start () {
|
||||||
@ -48,13 +52,6 @@ export default class ChatClientRelay {
|
|||||||
this.websocket.onopen = this.onWsOpen.bind(this)
|
this.websocket.onopen = this.onWsOpen.bind(this)
|
||||||
this.websocket.onclose = this.onWsClose.bind(this)
|
this.websocket.onclose = this.onWsClose.bind(this)
|
||||||
this.websocket.onmessage = this.onWsMessage.bind(this)
|
this.websocket.onmessage = this.onWsMessage.bind(this)
|
||||||
this.heartbeatTimerId = window.setInterval(this.sendHeartbeat.bind(this), 10 * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendHeartbeat () {
|
|
||||||
this.websocket.send(JSON.stringify({
|
|
||||||
cmd: COMMAND_HEARTBEAT
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onWsOpen () {
|
onWsOpen () {
|
||||||
@ -68,6 +65,31 @@ export default class ChatClientRelay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
this.heartbeatTimerId = window.setInterval(this.sendHeartbeat.bind(this), HEARTBEAT_INTERVAL)
|
||||||
|
this.refreshReceiveTimeoutTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
sendHeartbeat () {
|
||||||
|
this.websocket.send(JSON.stringify({
|
||||||
|
cmd: COMMAND_HEARTBEAT
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshReceiveTimeoutTimer() {
|
||||||
|
if (this.receiveTimeoutTimerId) {
|
||||||
|
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||||
|
}
|
||||||
|
this.receiveTimeoutTimerId = window.setTimeout(this.onReceiveTimeout.bind(this), RECEIVE_TIMEOUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
onReceiveTimeout() {
|
||||||
|
window.console.warn('接收消息超时')
|
||||||
|
this.receiveTimeoutTimerId = null
|
||||||
|
|
||||||
|
// 直接丢弃阻塞的websocket,不等onclose回调了
|
||||||
|
this.websocket.onopen = this.websocket.onclose = this.websocket.onmessage = null
|
||||||
|
this.websocket.close()
|
||||||
|
this.onWsClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
onWsClose () {
|
onWsClose () {
|
||||||
@ -76,16 +98,26 @@ export default class ChatClientRelay {
|
|||||||
window.clearInterval(this.heartbeatTimerId)
|
window.clearInterval(this.heartbeatTimerId)
|
||||||
this.heartbeatTimerId = null
|
this.heartbeatTimerId = null
|
||||||
}
|
}
|
||||||
|
if (this.receiveTimeoutTimerId) {
|
||||||
|
window.clearTimeout(this.receiveTimeoutTimerId)
|
||||||
|
this.receiveTimeoutTimerId = null
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isDestroying) {
|
if (this.isDestroying) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.console.log(`掉线重连中${++this.retryCount}`)
|
window.console.warn(`掉线重连中${++this.retryCount}`)
|
||||||
window.setTimeout(this.wsConnect.bind(this), 1000)
|
window.setTimeout(this.wsConnect.bind(this), 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
onWsMessage (event) {
|
onWsMessage (event) {
|
||||||
|
this.refreshReceiveTimeoutTimer()
|
||||||
|
|
||||||
let {cmd, data} = JSON.parse(event.data)
|
let {cmd, data} = JSON.parse(event.data)
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
|
case COMMAND_HEARTBEAT: {
|
||||||
|
break
|
||||||
|
}
|
||||||
case COMMAND_ADD_TEXT: {
|
case COMMAND_ADD_TEXT: {
|
||||||
if (!this.onAddText) {
|
if (!this.onAddText) {
|
||||||
break
|
break
|
||||||
|
211
frontend/src/api/chat/ChatClientTest.js
Normal file
211
frontend/src/api/chat/ChatClientTest.js
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import {getUuid4Hex} from '@/utils'
|
||||||
|
import * as constants from '@/components/ChatRenderer/constants'
|
||||||
|
import * as avatar from './avatar'
|
||||||
|
|
||||||
|
const NAMES = [
|
||||||
|
'xfgryujk', 'Simon', 'Il Harper', 'Kinori', 'shugen', 'yuyuyzl', '3Shain', '光羊', '黑炎', 'Misty', '孤梦星影',
|
||||||
|
'ジョナサン・ジョースター', 'ジョセフ・ジョースター', 'ディオ・ブランドー', '空條承太郎', '博丽灵梦', '雾雨魔理沙',
|
||||||
|
'Rick Astley'
|
||||||
|
]
|
||||||
|
|
||||||
|
const CONTENTS = [
|
||||||
|
'草', 'kksk', '8888888888', '888888888888888888888888888888', '老板大气,老板身体健康',
|
||||||
|
'The quick brown fox jumps over the lazy dog', "I can eat glass, it doesn't hurt me",
|
||||||
|
'我不做人了,JOJO', '無駄無駄無駄無駄無駄無駄無駄無駄', '欧啦欧啦欧啦欧啦欧啦欧啦欧啦欧啦', '逃げるんだよォ!',
|
||||||
|
'嚯,朝我走过来了吗,没有选择逃跑而是主动接近我么', '不要停下来啊', '已经没有什么好怕的了',
|
||||||
|
'I am the bone of my sword. Steel is my body, and fire is my blood.', '言いたいことがあるんだよ!',
|
||||||
|
'我忘不掉夏小姐了。如果不是知道了夏小姐,说不定我已经对这个世界没有留恋了', '迷えば、敗れる',
|
||||||
|
'Farewell, ashen one. May the flame guide thee', '竜神の剣を喰らえ!', '竜が我が敌を喰らう!',
|
||||||
|
'有一说一,这件事大家懂的都懂,不懂的,说了你也不明白,不如不说', '让我看看', '我柜子动了,我不玩了'
|
||||||
|
]
|
||||||
|
|
||||||
|
const AUTHOR_TYPES = [
|
||||||
|
{weight: 10, value: constants.AUTHRO_TYPE_NORMAL},
|
||||||
|
{weight: 5, value: constants.AUTHRO_TYPE_MEMBER},
|
||||||
|
{weight: 2, value: constants.AUTHRO_TYPE_ADMIN},
|
||||||
|
{weight: 1, value: constants.AUTHRO_TYPE_OWNER}
|
||||||
|
]
|
||||||
|
|
||||||
|
function randGuardInfo () {
|
||||||
|
let authorType = randomChoose(AUTHOR_TYPES)
|
||||||
|
let privilegeType
|
||||||
|
if (authorType === constants.AUTHRO_TYPE_MEMBER || authorType === constants.AUTHRO_TYPE_ADMIN) {
|
||||||
|
privilegeType = randInt(1, 3)
|
||||||
|
} else {
|
||||||
|
privilegeType = 0
|
||||||
|
}
|
||||||
|
return {authorType, privilegeType}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GIFT_INFO_LIST = [
|
||||||
|
{giftName: 'B坷垃', totalCoin: 9900},
|
||||||
|
{giftName: '礼花', totalCoin: 28000},
|
||||||
|
{giftName: '花式夸夸', totalCoin: 39000},
|
||||||
|
{giftName: '天空之翼', totalCoin: 100000},
|
||||||
|
{giftName: '摩天大楼', totalCoin: 450000},
|
||||||
|
{giftName: '小电视飞船', totalCoin: 1245000}
|
||||||
|
]
|
||||||
|
|
||||||
|
const SC_PRICES = [
|
||||||
|
30, 50, 100, 200, 500, 1000
|
||||||
|
]
|
||||||
|
|
||||||
|
const MESSAGE_GENERATORS = [
|
||||||
|
// 文字
|
||||||
|
{
|
||||||
|
weight: 20,
|
||||||
|
value() {
|
||||||
|
return {
|
||||||
|
type: constants.MESSAGE_TYPE_TEXT,
|
||||||
|
message: {
|
||||||
|
...randGuardInfo(),
|
||||||
|
avatarUrl: avatar.DEFAULT_AVATAR_URL,
|
||||||
|
timestamp: new Date().getTime() / 1000,
|
||||||
|
authorName: randomChoose(NAMES),
|
||||||
|
content: randomChoose(CONTENTS),
|
||||||
|
isGiftDanmaku: randInt(1, 10) <= 1,
|
||||||
|
authorLevel: randInt(0, 60),
|
||||||
|
isNewbie: randInt(1, 10) <= 9,
|
||||||
|
isMobileVerified: randInt(1, 10) <= 9,
|
||||||
|
medalLevel: randInt(0, 40),
|
||||||
|
id: getUuid4Hex(),
|
||||||
|
translation: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 礼物
|
||||||
|
{
|
||||||
|
weight: 1,
|
||||||
|
value() {
|
||||||
|
return {
|
||||||
|
type: constants.MESSAGE_TYPE_GIFT,
|
||||||
|
message: {
|
||||||
|
...randomChoose(GIFT_INFO_LIST),
|
||||||
|
id: getUuid4Hex(),
|
||||||
|
avatarUrl: avatar.DEFAULT_AVATAR_URL,
|
||||||
|
timestamp: new Date().getTime() / 1000,
|
||||||
|
authorName: randomChoose(NAMES),
|
||||||
|
num: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// SC
|
||||||
|
{
|
||||||
|
weight: 3,
|
||||||
|
value() {
|
||||||
|
return {
|
||||||
|
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||||
|
message: {
|
||||||
|
id: getUuid4Hex(),
|
||||||
|
avatarUrl: avatar.DEFAULT_AVATAR_URL,
|
||||||
|
timestamp: new Date().getTime() / 1000,
|
||||||
|
authorName: randomChoose(NAMES),
|
||||||
|
price: randomChoose(SC_PRICES),
|
||||||
|
content: randomChoose(CONTENTS),
|
||||||
|
translation: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 新舰长
|
||||||
|
{
|
||||||
|
weight: 1,
|
||||||
|
value() {
|
||||||
|
return {
|
||||||
|
type: constants.MESSAGE_TYPE_MEMBER,
|
||||||
|
message: {
|
||||||
|
id: getUuid4Hex(),
|
||||||
|
avatarUrl: avatar.DEFAULT_AVATAR_URL,
|
||||||
|
timestamp: new Date().getTime() / 1000,
|
||||||
|
authorName: randomChoose(NAMES),
|
||||||
|
privilegeType: randInt(1, 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function randomChoose (nodes) {
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
for (let node of nodes) {
|
||||||
|
if (node.weight === undefined || node.value === undefined) {
|
||||||
|
return nodes[randInt(0, nodes.length - 1)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalWeight = 0
|
||||||
|
for (let node of nodes) {
|
||||||
|
totalWeight += node.weight
|
||||||
|
}
|
||||||
|
let remainWeight = randInt(1, totalWeight)
|
||||||
|
for (let node of nodes) {
|
||||||
|
remainWeight -= node.weight
|
||||||
|
if (remainWeight > 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (node.value instanceof Array) {
|
||||||
|
return randomChoose(node.value)
|
||||||
|
}
|
||||||
|
return node.value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function randInt (min, max) {
|
||||||
|
return Math.floor(min + (max - min + 1) * Math.random())
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ChatClientTest {
|
||||||
|
constructor () {
|
||||||
|
this.minSleepTime = 800
|
||||||
|
this.maxSleepTime = 1200
|
||||||
|
|
||||||
|
this.onAddText = null
|
||||||
|
this.onAddGift = null
|
||||||
|
this.onAddMember = null
|
||||||
|
this.onAddSuperChat = null
|
||||||
|
this.onDelSuperChat = null
|
||||||
|
this.onUpdateTranslation = null
|
||||||
|
|
||||||
|
this.timerId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
start () {
|
||||||
|
this.refreshTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
stop () {
|
||||||
|
if (this.timerId) {
|
||||||
|
window.clearTimeout(this.timerId)
|
||||||
|
this.timerId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTimer () {
|
||||||
|
this.timerId = window.setTimeout(this.onTimeout.bind(this), randInt(this.minSleepTime, this.maxSleepTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
onTimeout () {
|
||||||
|
this.refreshTimer()
|
||||||
|
|
||||||
|
let {type, message} = randomChoose(MESSAGE_GENERATORS)()
|
||||||
|
switch (type) {
|
||||||
|
case constants.MESSAGE_TYPE_TEXT:
|
||||||
|
this.onAddText(message)
|
||||||
|
break
|
||||||
|
case constants.MESSAGE_TYPE_GIFT:
|
||||||
|
this.onAddGift(message)
|
||||||
|
break
|
||||||
|
case constants.MESSAGE_TYPE_MEMBER:
|
||||||
|
this.onAddMember(message)
|
||||||
|
break
|
||||||
|
case constants.MESSAGE_TYPE_SUPER_CHAT:
|
||||||
|
this.onAddSuperChat(message)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,8 +11,8 @@ export const DEFAULT_CONFIG = {
|
|||||||
|
|
||||||
blockGiftDanmaku: true,
|
blockGiftDanmaku: true,
|
||||||
blockLevel: 0,
|
blockLevel: 0,
|
||||||
blockNewbie: true,
|
blockNewbie: false,
|
||||||
blockNotMobileVerified: true,
|
blockNotMobileVerified: false,
|
||||||
blockKeywords: '',
|
blockKeywords: '',
|
||||||
blockUsers: '',
|
blockUsers: '',
|
||||||
blockMedalLevel: 0,
|
blockMedalLevel: 0,
|
||||||
@ -28,8 +28,9 @@ export function setLocalConfig (config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getLocalConfig () {
|
export function getLocalConfig () {
|
||||||
if (!window.localStorage.config) {
|
try {
|
||||||
return DEFAULT_CONFIG
|
return mergeConfig(JSON.parse(window.localStorage.config), DEFAULT_CONFIG)
|
||||||
|
} catch {
|
||||||
|
return {...DEFAULT_CONFIG}
|
||||||
}
|
}
|
||||||
return mergeConfig(JSON.parse(window.localStorage.config), DEFAULT_CONFIG)
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<yt-live-chat-ticker-renderer :hidden="showMessages.length === 0">
|
<yt-live-chat-ticker-renderer :hidden="showMessages.length === 0">
|
||||||
<div id="container" dir="ltr" class="style-scope yt-live-chat-ticker-renderer">
|
<div id="container" dir="ltr" class="style-scope yt-live-chat-ticker-renderer">
|
||||||
<div id="items" class="style-scope yt-live-chat-ticker-renderer">
|
<transition-group tag="div" :css="false" @enter="onTickerItemEnter" @leave="onTickerItemLeave"
|
||||||
|
id="items" class="style-scope yt-live-chat-ticker-renderer"
|
||||||
|
>
|
||||||
<yt-live-chat-ticker-paid-message-item-renderer v-for="message in showMessages" :key="message.raw.id"
|
<yt-live-chat-ticker-paid-message-item-renderer v-for="message in showMessages" :key="message.raw.id"
|
||||||
tabindex="0" class="style-scope yt-live-chat-ticker-renderer" style="overflow: hidden;"
|
tabindex="0" class="style-scope yt-live-chat-ticker-renderer" style="overflow: hidden;"
|
||||||
@click="onItemClick(message.raw)"
|
@click="onItemClick(message.raw)"
|
||||||
@ -19,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</yt-live-chat-ticker-paid-message-item-renderer>
|
</yt-live-chat-ticker-paid-message-item-renderer>
|
||||||
</div>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="pinnedMessage">
|
<template v-if="pinnedMessage">
|
||||||
<membership-item :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
|
<membership-item :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
|
||||||
@ -98,6 +100,32 @@ export default {
|
|||||||
window.clearInterval(this.updateTimerId)
|
window.clearInterval(this.updateTimerId)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async onTickerItemEnter(el, done) {
|
||||||
|
let width = el.clientWidth
|
||||||
|
if (width === 0) {
|
||||||
|
// CSS指定了不显示固定栏
|
||||||
|
done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
el.style.width = 0
|
||||||
|
await this.$nextTick()
|
||||||
|
el.style.width = `${width}px`
|
||||||
|
window.setTimeout(done, 200)
|
||||||
|
},
|
||||||
|
onTickerItemLeave(el, done) {
|
||||||
|
el.classList.add('sliding-down')
|
||||||
|
window.setTimeout(() => {
|
||||||
|
el.classList.add('collapsing')
|
||||||
|
el.style.width = 0
|
||||||
|
window.setTimeout(() => {
|
||||||
|
el.classList.remove('sliding-down')
|
||||||
|
el.classList.remove('collapsing')
|
||||||
|
el.style.width = 'auto'
|
||||||
|
done()
|
||||||
|
}, 200)
|
||||||
|
}, 200)
|
||||||
|
},
|
||||||
|
|
||||||
getShowAuthorName: constants.getShowAuthorName,
|
getShowAuthorName: constants.getShowAuthorName,
|
||||||
needToShow(message) {
|
needToShow(message) {
|
||||||
let pinTime = this.getPinTime(message)
|
let pinTime = this.getPinTime(message)
|
||||||
|
@ -137,7 +137,7 @@ export function getGiftShowContent (message, showGiftName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getShowAuthorName (message) {
|
export function getShowAuthorName (message) {
|
||||||
if (message.authorNamePronunciation) {
|
if (message.authorNamePronunciation && message.authorNamePronunciation !== message.authorName) {
|
||||||
return `${message.authorName}(${message.authorNamePronunciation})`
|
return `${message.authorName}(${message.authorNamePronunciation})`
|
||||||
}
|
}
|
||||||
return message.authorName
|
return message.authorName
|
||||||
|
@ -47,7 +47,21 @@ import MembershipItem from './MembershipItem.vue'
|
|||||||
import PaidMessage from './PaidMessage.vue'
|
import PaidMessage from './PaidMessage.vue'
|
||||||
import * as constants from './constants'
|
import * as constants from './constants'
|
||||||
|
|
||||||
|
// 只有要添加的消息需要平滑
|
||||||
|
const NEED_SMOOTH_MESSAGE_TYPES = [
|
||||||
|
constants.MESSAGE_TYPE_TEXT,
|
||||||
|
constants.MESSAGE_TYPE_GIFT,
|
||||||
|
constants.MESSAGE_TYPE_MEMBER,
|
||||||
|
constants.MESSAGE_TYPE_SUPER_CHAT
|
||||||
|
]
|
||||||
|
// 发送消息时间间隔范围
|
||||||
|
const MESSAGE_MIN_INTERVAL = 80
|
||||||
|
const MESSAGE_MAX_INTERVAL = 1000
|
||||||
|
|
||||||
|
// 每次发送消息后增加的动画时间,要比MESSAGE_MIN_INTERVAL稍微大一点,太小了动画不连续,太大了发送消息时会中断动画
|
||||||
|
// 84 = ceil((1000 / 60) * 5)
|
||||||
const CHAT_SMOOTH_ANIMATION_TIME_MS = 84
|
const CHAT_SMOOTH_ANIMATION_TIME_MS = 84
|
||||||
|
// 滚动条距离底部小于多少像素则认为在底部
|
||||||
const SCROLLED_TO_BOTTOM_EPSILON = 15
|
const SCROLLED_TO_BOTTOM_EPSILON = 15
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -59,7 +73,6 @@ export default {
|
|||||||
PaidMessage
|
PaidMessage
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
css: String,
|
|
||||||
maxNumber: {
|
maxNumber: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: chatConfig.DEFAULT_CONFIG.maxNumber
|
default: chatConfig.DEFAULT_CONFIG.maxNumber
|
||||||
@ -70,15 +83,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
let styleElement = document.createElement('style')
|
|
||||||
document.head.appendChild(styleElement)
|
|
||||||
return {
|
return {
|
||||||
MESSAGE_TYPE_TEXT: constants.MESSAGE_TYPE_TEXT,
|
MESSAGE_TYPE_TEXT: constants.MESSAGE_TYPE_TEXT,
|
||||||
MESSAGE_TYPE_GIFT: constants.MESSAGE_TYPE_GIFT,
|
MESSAGE_TYPE_GIFT: constants.MESSAGE_TYPE_GIFT,
|
||||||
MESSAGE_TYPE_MEMBER: constants.MESSAGE_TYPE_MEMBER,
|
MESSAGE_TYPE_MEMBER: constants.MESSAGE_TYPE_MEMBER,
|
||||||
MESSAGE_TYPE_SUPER_CHAT: constants.MESSAGE_TYPE_SUPER_CHAT,
|
MESSAGE_TYPE_SUPER_CHAT: constants.MESSAGE_TYPE_SUPER_CHAT,
|
||||||
|
|
||||||
styleElement,
|
|
||||||
messages: [], // 显示的消息
|
messages: [], // 显示的消息
|
||||||
paidMessages: [], // 固定在上方的消息
|
paidMessages: [], // 固定在上方的消息
|
||||||
|
|
||||||
@ -108,19 +118,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
css(val) {
|
|
||||||
this.styleElement.innerText = val
|
|
||||||
},
|
|
||||||
canScrollToBottom(val) {
|
canScrollToBottom(val) {
|
||||||
this.cantScrollStartTime = val ? null : new Date()
|
this.cantScrollStartTime = val ? null : new Date()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.styleElement.innerText = this.css
|
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
document.head.removeChild(this.styleElement)
|
|
||||||
if (this.emitSmoothedMessageTimerId) {
|
if (this.emitSmoothedMessageTimerId) {
|
||||||
window.clearTimeout(this.emitSmoothedMessageTimerId)
|
window.clearTimeout(this.emitSmoothedMessageTimerId)
|
||||||
this.emitSmoothedMessageTimerId = null
|
this.emitSmoothedMessageTimerId = null
|
||||||
@ -239,36 +244,53 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
enqueueMessages(messages) {
|
enqueueMessages(messages) {
|
||||||
if (this.lastEnqueueTime) {
|
// 估计进队列时间间隔
|
||||||
let interval = new Date() - this.lastEnqueueTime
|
if (!this.lastEnqueueTime) {
|
||||||
// 理论上B站发包间隔1S,如果不过滤间隔太短的会导致消息平滑失效
|
this.lastEnqueueTime = new Date()
|
||||||
if (interval > 100) {
|
} else {
|
||||||
|
let curTime = new Date()
|
||||||
|
let interval = curTime - this.lastEnqueueTime
|
||||||
|
// 让发消息速度变化不要太频繁
|
||||||
|
if (interval > 1000) {
|
||||||
this.enqueueIntervals.push(interval)
|
this.enqueueIntervals.push(interval)
|
||||||
if (this.enqueueIntervals.length > 5) {
|
if (this.enqueueIntervals.length > 5) {
|
||||||
this.enqueueIntervals.splice(0, this.enqueueIntervals.length - 5)
|
this.enqueueIntervals.splice(0, this.enqueueIntervals.length - 5)
|
||||||
}
|
}
|
||||||
this.estimatedEnqueueInterval = Math.max(...this.enqueueIntervals)
|
this.estimatedEnqueueInterval = Math.max(...this.enqueueIntervals)
|
||||||
|
this.lastEnqueueTime = curTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.lastEnqueueTime = new Date()
|
|
||||||
|
|
||||||
// 只有要显示的消息需要平滑
|
// 把messages分成messageGroup,每个组里最多有1个需要平滑的消息
|
||||||
let messageGroup = []
|
let messageGroup = []
|
||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
messageGroup.push(message)
|
messageGroup.push(message)
|
||||||
if (message.type !== constants.MESSAGE_TYPE_DEL && message.type !== constants.MESSAGE_TYPE_UPDATE) {
|
if (this.messageNeedSmooth(message)) {
|
||||||
this.smoothedMessageQueue.push(messageGroup)
|
this.smoothedMessageQueue.push(messageGroup)
|
||||||
messageGroup = []
|
messageGroup = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 还剩下不需要平滑的消息
|
||||||
if (messageGroup.length > 0) {
|
if (messageGroup.length > 0) {
|
||||||
|
if (this.smoothedMessageQueue.length > 0) {
|
||||||
|
// 和上一组合并
|
||||||
|
let lastMessageGroup = this.smoothedMessageQueue[this.smoothedMessageQueue.length - 1]
|
||||||
|
for (let message of messageGroup) {
|
||||||
|
lastMessageGroup.push(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 自己一个组
|
||||||
this.smoothedMessageQueue.push(messageGroup)
|
this.smoothedMessageQueue.push(messageGroup)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.emitSmoothedMessageTimerId) {
|
if (!this.emitSmoothedMessageTimerId) {
|
||||||
this.emitSmoothedMessageTimerId = window.setTimeout(this.emitSmoothedMessages)
|
this.emitSmoothedMessageTimerId = window.setTimeout(this.emitSmoothedMessages)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
messageNeedSmooth({type}) {
|
||||||
|
return NEED_SMOOTH_MESSAGE_TYPES.indexOf(type) !== -1
|
||||||
|
},
|
||||||
emitSmoothedMessages() {
|
emitSmoothedMessages() {
|
||||||
this.emitSmoothedMessageTimerId = null
|
this.emitSmoothedMessageTimerId = null
|
||||||
if (this.smoothedMessageQueue.length <= 0) {
|
if (this.smoothedMessageQueue.length <= 0) {
|
||||||
@ -280,21 +302,18 @@ export default {
|
|||||||
if (this.estimatedEnqueueInterval) {
|
if (this.estimatedEnqueueInterval) {
|
||||||
estimatedNextEnqueueRemainTime = Math.max(this.lastEnqueueTime - new Date() + this.estimatedEnqueueInterval, 1)
|
estimatedNextEnqueueRemainTime = Math.max(this.lastEnqueueTime - new Date() + this.estimatedEnqueueInterval, 1)
|
||||||
}
|
}
|
||||||
// 最快80ms/条,计算发送的消息数,保证在下次进队列之前消费队列到最多剩3条消息,不消费完是为了防止消息速度变慢时突然停顿
|
// 计算发送的消息数,保证在下次进队列之前发完
|
||||||
const MIN_SLEEP_TIME = 80
|
|
||||||
const MAX_SLEEP_TIME = 1000
|
|
||||||
const MAX_REMAIN_GROUP_NUM = 3
|
|
||||||
// 下次进队列之前应该发多少条消息
|
// 下次进队列之前应该发多少条消息
|
||||||
let shouldEmitGroupNum = Math.max(this.smoothedMessageQueue.length - MAX_REMAIN_GROUP_NUM, 0)
|
let shouldEmitGroupNum = Math.max(this.smoothedMessageQueue.length, 0)
|
||||||
// 下次进队列之前最多能发多少次
|
// 下次进队列之前最多能发多少次
|
||||||
let maxCanEmitCount = estimatedNextEnqueueRemainTime / MIN_SLEEP_TIME
|
let maxCanEmitCount = estimatedNextEnqueueRemainTime / MESSAGE_MIN_INTERVAL
|
||||||
// 这次发多少条消息
|
// 这次发多少条消息
|
||||||
let groupNumToEmit
|
let groupNumToEmit
|
||||||
if (shouldEmitGroupNum < maxCanEmitCount) {
|
if (shouldEmitGroupNum < maxCanEmitCount) {
|
||||||
// 队列中消息数很少,每次发1条也能发到最多剩3条
|
// 队列中消息数很少,每次发1条也能发完
|
||||||
groupNumToEmit = 1
|
groupNumToEmit = 1
|
||||||
} else {
|
} else {
|
||||||
// 每次发1条以上,保证按最快速度能发到最多剩3条
|
// 每次发1条以上,保证按最快速度能发完
|
||||||
groupNumToEmit = Math.ceil(shouldEmitGroupNum / maxCanEmitCount)
|
groupNumToEmit = Math.ceil(shouldEmitGroupNum / maxCanEmitCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,17 +333,17 @@ export default {
|
|||||||
// 消息没发完,计算下次发消息时间
|
// 消息没发完,计算下次发消息时间
|
||||||
let sleepTime
|
let sleepTime
|
||||||
if (groupNumToEmit === 1) {
|
if (groupNumToEmit === 1) {
|
||||||
// 队列中消息数很少,随便定个[MIN_SLEEP_TIME, MAX_SLEEP_TIME]的时间
|
// 队列中消息数很少,随便定个[MESSAGE_MIN_INTERVAL, MESSAGE_MAX_INTERVAL]的时间
|
||||||
sleepTime = estimatedNextEnqueueRemainTime / this.smoothedMessageQueue.length
|
sleepTime = estimatedNextEnqueueRemainTime / this.smoothedMessageQueue.length
|
||||||
sleepTime *= 0.5 + Math.random()
|
sleepTime *= 0.5 + Math.random()
|
||||||
if (sleepTime > MAX_SLEEP_TIME) {
|
if (sleepTime > MESSAGE_MAX_INTERVAL) {
|
||||||
sleepTime = MAX_SLEEP_TIME
|
sleepTime = MESSAGE_MAX_INTERVAL
|
||||||
} else if (sleepTime < MIN_SLEEP_TIME) {
|
} else if (sleepTime < MESSAGE_MIN_INTERVAL) {
|
||||||
sleepTime = MIN_SLEEP_TIME
|
sleepTime = MESSAGE_MIN_INTERVAL
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 按最快速度发
|
// 按最快速度发
|
||||||
sleepTime = MIN_SLEEP_TIME
|
sleepTime = MESSAGE_MIN_INTERVAL
|
||||||
}
|
}
|
||||||
this.emitSmoothedMessageTimerId = window.setTimeout(this.emitSmoothedMessages, sleepTime)
|
this.emitSmoothedMessageTimerId = window.setTimeout(this.emitSmoothedMessages, sleepTime)
|
||||||
},
|
},
|
||||||
@ -351,7 +370,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.maybeResizeScrollContainer(),
|
this.maybeResizeScrollContainer()
|
||||||
this.flushMessagesBuffer()
|
this.flushMessagesBuffer()
|
||||||
this.$nextTick(this.maybeScrollToBottom)
|
this.$nextTick(this.maybeScrollToBottom)
|
||||||
},
|
},
|
||||||
@ -361,8 +380,13 @@ export default {
|
|||||||
addTime: new Date() // 添加一个本地时间给Ticker用,防止本地时间和服务器时间相差很大的情况
|
addTime: new Date() // 添加一个本地时间给Ticker用,防止本地时间和服务器时间相差很大的情况
|
||||||
}
|
}
|
||||||
this.messagesBuffer.push(message)
|
this.messagesBuffer.push(message)
|
||||||
|
|
||||||
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
|
||||||
this.paidMessages.unshift(message)
|
this.paidMessages.unshift(message)
|
||||||
|
const MAX_PAID_MESSAGE_NUM = 100
|
||||||
|
if (this.paidMessages.length > MAX_PAID_MESSAGE_NUM) {
|
||||||
|
this.paidMessages.splice(MAX_PAID_MESSAGE_NUM, this.paidMessages.length - MAX_PAID_MESSAGE_NUM)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleDelMessage({id}) {
|
handleDelMessage({id}) {
|
||||||
@ -425,7 +449,8 @@ export default {
|
|||||||
}
|
}
|
||||||
this.messagesBuffer = []
|
this.messagesBuffer = []
|
||||||
// 等items高度变化
|
// 等items高度变化
|
||||||
this.$nextTick(this.showNewMessages)
|
await this.$nextTick()
|
||||||
|
this.showNewMessages()
|
||||||
},
|
},
|
||||||
showNewMessages() {
|
showNewMessages() {
|
||||||
let hasScrollBar = this.$refs.items.clientHeight > this.$refs.scroller.clientHeight
|
let hasScrollBar = this.$refs.items.clientHeight > this.$refs.scroller.clientHeight
|
||||||
|
@ -41,12 +41,19 @@ export default {
|
|||||||
roomUrl: 'Room URL',
|
roomUrl: 'Room URL',
|
||||||
copy: 'Copy',
|
copy: 'Copy',
|
||||||
enterRoom: 'Enter room',
|
enterRoom: 'Enter room',
|
||||||
|
enterTestRoom: 'Enter test room',
|
||||||
exportConfig: 'Export config',
|
exportConfig: 'Export config',
|
||||||
importConfig: 'Import config',
|
importConfig: 'Import config',
|
||||||
|
|
||||||
failedToParseConfig: 'Failed to parse config: '
|
failedToParseConfig: 'Failed to parse config: '
|
||||||
},
|
},
|
||||||
stylegen: {
|
stylegen: {
|
||||||
|
legacy: 'Classic',
|
||||||
|
lineLike: 'Line-like',
|
||||||
|
|
||||||
|
light: 'light',
|
||||||
|
dark: 'dark',
|
||||||
|
|
||||||
outlines: 'Outlines',
|
outlines: 'Outlines',
|
||||||
showOutlines: 'Show outlines',
|
showOutlines: 'Show outlines',
|
||||||
outlineSize: 'Outline size',
|
outlineSize: 'Outline size',
|
||||||
|
@ -41,12 +41,19 @@ export default {
|
|||||||
roomUrl: 'ルームのURL',
|
roomUrl: 'ルームのURL',
|
||||||
copy: 'コピー',
|
copy: 'コピー',
|
||||||
enterRoom: 'ルームに入る',
|
enterRoom: 'ルームに入る',
|
||||||
|
enterTestRoom: 'テストルームに入る',
|
||||||
exportConfig: 'コンフィグの導出',
|
exportConfig: 'コンフィグの導出',
|
||||||
importConfig: 'コンフィグの導入',
|
importConfig: 'コンフィグの導入',
|
||||||
|
|
||||||
failedToParseConfig: 'コンフィグ解析に失敗しました'
|
failedToParseConfig: 'コンフィグ解析に失敗しました'
|
||||||
},
|
},
|
||||||
stylegen: {
|
stylegen: {
|
||||||
|
legacy: '古典',
|
||||||
|
lineLike: 'Line風',
|
||||||
|
|
||||||
|
light: '明るい',
|
||||||
|
dark: '暗い',
|
||||||
|
|
||||||
outlines: 'アウトライン',
|
outlines: 'アウトライン',
|
||||||
showOutlines: 'アウトラインを表示する',
|
showOutlines: 'アウトラインを表示する',
|
||||||
outlineSize: 'アウトラインのサイズ',
|
outlineSize: 'アウトラインのサイズ',
|
||||||
|
@ -41,12 +41,19 @@ export default {
|
|||||||
roomUrl: '房间URL',
|
roomUrl: '房间URL',
|
||||||
copy: '复制',
|
copy: '复制',
|
||||||
enterRoom: '进入房间',
|
enterRoom: '进入房间',
|
||||||
|
enterTestRoom: '进入测试房间',
|
||||||
exportConfig: '导出配置',
|
exportConfig: '导出配置',
|
||||||
importConfig: '导入配置',
|
importConfig: '导入配置',
|
||||||
|
|
||||||
failedToParseConfig: '配置解析失败:'
|
failedToParseConfig: '配置解析失败:'
|
||||||
},
|
},
|
||||||
stylegen: {
|
stylegen: {
|
||||||
|
legacy: '经典',
|
||||||
|
lineLike: '仿微信',
|
||||||
|
|
||||||
|
light: '明亮',
|
||||||
|
dark: '黑暗',
|
||||||
|
|
||||||
outlines: '描边',
|
outlines: '描边',
|
||||||
showOutlines: '显示描边',
|
showOutlines: '显示描边',
|
||||||
outlineSize: '描边尺寸',
|
outlineSize: '描边尺寸',
|
||||||
@ -77,7 +84,7 @@ export default {
|
|||||||
|
|
||||||
backgrounds: '背景',
|
backgrounds: '背景',
|
||||||
bgColor: '背景色',
|
bgColor: '背景色',
|
||||||
useBarsInsteadOfBg: '用条代替背景',
|
useBarsInsteadOfBg: '用条代替消息背景',
|
||||||
messageBgColor: '消息背景色',
|
messageBgColor: '消息背景色',
|
||||||
ownerMessageBgColor: '主播消息背景色',
|
ownerMessageBgColor: '主播消息背景色',
|
||||||
moderatorMessageBgColor: '房管消息背景色',
|
moderatorMessageBgColor: '房管消息背景色',
|
||||||
@ -104,7 +111,7 @@ export default {
|
|||||||
animateIn: '进入动画',
|
animateIn: '进入动画',
|
||||||
fadeInTime: '淡入时间(毫秒)',
|
fadeInTime: '淡入时间(毫秒)',
|
||||||
animateOut: '移除旧消息',
|
animateOut: '移除旧消息',
|
||||||
animateOutWaitTime: '等待时间(秒)',
|
animateOutWaitTime: '移除前等待时间(秒)',
|
||||||
fadeOutTime: '淡出时间(毫秒)',
|
fadeOutTime: '淡出时间(毫秒)',
|
||||||
slide: '滑动',
|
slide: '滑动',
|
||||||
reverseSlide: '反向滑动',
|
reverseSlide: '反向滑动',
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a href="http://link.bilibili.com/ctool/vtuber" target="_blank">
|
<a href="http://link.bilibili.com/ctool/vtuber" target="_blank">
|
||||||
<el-menu-item>
|
<el-menu-item>
|
||||||
<i class="el-icon-share"></i>{{$t('sidebar.giftRecordOfficial')}}
|
<i class="el-icon-link"></i>{{$t('sidebar.giftRecordOfficial')}}
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</a>
|
</a>
|
||||||
<el-submenu index="null">
|
<el-submenu index="null">
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="version">
|
<div class="version">
|
||||||
v1.5.1
|
v1.5.2-beta
|
||||||
</div>
|
</div>
|
||||||
<sidebar></sidebar>
|
<sidebar></sidebar>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
@ -53,7 +53,7 @@ export default {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "\5FAE\8F6F\96C5\9ED1", "微软雅黑", Arial, sans-serif;
|
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "\5FAE \8F6F \96C5 \9ED1 ", "微软雅黑", Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body, #app, .app-wrapper, .sidebar-container {
|
html, body, #app, .app-wrapper, .sidebar-container {
|
||||||
@ -62,6 +62,7 @@ html, body, #app, .app-wrapper, .sidebar-container {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
background-color: #f6f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
a, a:focus, a:hover {
|
a, a:focus, a:hover {
|
||||||
|
@ -2,9 +2,9 @@ import Vue from 'vue'
|
|||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import VueI18n from 'vue-i18n'
|
import VueI18n from 'vue-i18n'
|
||||||
import {
|
import {
|
||||||
Aside, Autocomplete, Badge, Button, Col, ColorPicker, Container, Divider, Form, FormItem, Image,
|
Aside, Autocomplete, Badge, Button, Card, Col, ColorPicker, Container, Divider, Form, FormItem, Image,
|
||||||
Input, Main, Menu, MenuItem, Message, Radio, RadioGroup, Row, Scrollbar, Slider, Submenu, Switch,
|
Input, Main, Menu, MenuItem, Message, Option, OptionGroup, Radio, RadioGroup, Row, Select, Scrollbar,
|
||||||
TabPane, Tabs, Tooltip
|
Slider, Submenu, Switch, TabPane, Tabs, Tooltip
|
||||||
} from 'element-ui'
|
} from 'element-ui'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ if (process.env.NODE_ENV === 'development') {
|
|||||||
// 开发时使用localhost:12450
|
// 开发时使用localhost:12450
|
||||||
axios.defaults.baseURL = 'http://localhost:12450'
|
axios.defaults.baseURL = 'http://localhost:12450'
|
||||||
}
|
}
|
||||||
|
axios.defaults.timeout = 10 * 1000
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
Vue.use(VueI18n)
|
Vue.use(VueI18n)
|
||||||
@ -32,6 +33,7 @@ Vue.use(Aside)
|
|||||||
Vue.use(Autocomplete)
|
Vue.use(Autocomplete)
|
||||||
Vue.use(Badge)
|
Vue.use(Badge)
|
||||||
Vue.use(Button)
|
Vue.use(Button)
|
||||||
|
Vue.use(Card)
|
||||||
Vue.use(Col)
|
Vue.use(Col)
|
||||||
Vue.use(ColorPicker)
|
Vue.use(ColorPicker)
|
||||||
Vue.use(Container)
|
Vue.use(Container)
|
||||||
@ -43,9 +45,12 @@ Vue.use(Input)
|
|||||||
Vue.use(Main)
|
Vue.use(Main)
|
||||||
Vue.use(Menu)
|
Vue.use(Menu)
|
||||||
Vue.use(MenuItem)
|
Vue.use(MenuItem)
|
||||||
|
Vue.use(Option)
|
||||||
|
Vue.use(OptionGroup)
|
||||||
Vue.use(Radio)
|
Vue.use(Radio)
|
||||||
Vue.use(RadioGroup)
|
Vue.use(RadioGroup)
|
||||||
Vue.use(Row)
|
Vue.use(Row)
|
||||||
|
Vue.use(Select)
|
||||||
Vue.use(Scrollbar)
|
Vue.use(Scrollbar)
|
||||||
Vue.use(Slider)
|
Vue.use(Slider)
|
||||||
Vue.use(Submenu)
|
Vue.use(Submenu)
|
||||||
@ -71,7 +76,19 @@ const router = new VueRouter({
|
|||||||
{path: 'help', name: 'help', component: Help}
|
{path: 'help', name: 'help', component: Help}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{path: '/room/:roomId', name: 'room', component: Room},
|
{path: '/room/test', name: 'test_room', component: Room, props: route => ({strConfig: route.query})},
|
||||||
|
{
|
||||||
|
path: '/room/:roomId',
|
||||||
|
name: 'room',
|
||||||
|
component: Room,
|
||||||
|
props(route) {
|
||||||
|
let roomId = parseInt(route.params.roomId)
|
||||||
|
if (isNaN(roomId)) {
|
||||||
|
roomId = null
|
||||||
|
}
|
||||||
|
return {roomId, strConfig: route.query}
|
||||||
|
}
|
||||||
|
},
|
||||||
{path: '*', component: NotFound}
|
{path: '*', component: NotFound}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1>{{$t('help.help')}}</h1>
|
<h1>{{$t('help.help')}}</h1>
|
||||||
<p>{{$t('help.p1')}}</p>
|
<p>{{$t('help.p1')}}</p>
|
||||||
<p><el-image src="/static/img/tutorial/tutorial-1.png"></el-image></p>
|
<p class="img-container"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-1.png"></el-image></p>
|
||||||
<p>{{$t('help.p2')}}</p>
|
<p>{{$t('help.p2')}}</p>
|
||||||
<p><el-image src="/static/img/tutorial/tutorial-2.png"></el-image></p>
|
<p class="img-container large-img"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-2.png"></el-image></p>
|
||||||
<p>{{$t('help.p3')}}</p>
|
<p>{{$t('help.p3')}}</p>
|
||||||
<p><el-image src="/static/img/tutorial/tutorial-3.png"></el-image></p>
|
<p class="img-container large-img"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-3.png"></el-image></p>
|
||||||
<p>{{$t('help.p4')}}</p>
|
<p>{{$t('help.p4')}}</p>
|
||||||
<p><el-image src="/static/img/tutorial/tutorial-4.png"></el-image></p>
|
<p class="img-container"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-4.png"></el-image></p>
|
||||||
<p>{{$t('help.p5')}}</p>
|
<p>{{$t('help.p5')}}</p>
|
||||||
<p><el-image src="/static/img/tutorial/tutorial-5.png"></el-image></p>
|
<p class="img-container large-img"><el-image fit="scale-down" src="/static/img/tutorial/tutorial-5.png"></el-image></p>
|
||||||
<p><br><br><br><br><br><br><br><br>--------------------------------------------------------------------------------------------------------</p>
|
<p><br><br><br><br><br><br><br><br>--------------------------------------------------------------------------------------------------------</p>
|
||||||
<p>喜欢的话可以推荐给别人,专栏求支持_(:з」∠)_ <a href="https://www.bilibili.com/read/cv4594365" target="_blank">https://www.bilibili.com/read/cv4594365</a></p>
|
<p>喜欢的话可以推荐给别人,专栏求支持_(:з」∠)_ <a href="https://www.bilibili.com/read/cv4594365" target="_blank">https://www.bilibili.com/read/cv4594365</a></p>
|
||||||
</div>
|
</div>
|
||||||
@ -21,3 +21,13 @@ export default {
|
|||||||
name: 'Help'
|
name: 'Help'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.img-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-container.large-img .el-image {
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,90 +1,140 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form :model="form" ref="form" label-width="150px" :rules="{
|
<div>
|
||||||
roomId: [
|
<p>
|
||||||
{required: true, message: $t('home.roomIdEmpty'), trigger: 'blur'},
|
<el-form :model="form" ref="form" label-width="150px" :rules="{
|
||||||
{type: 'integer', min: 1, message: $t('home.roomIdInteger'), trigger: 'blur'}
|
roomId: [
|
||||||
]
|
{required: true, message: $t('home.roomIdEmpty'), trigger: 'blur'},
|
||||||
}">
|
{type: 'integer', min: 1, message: $t('home.roomIdInteger'), trigger: 'blur'}
|
||||||
<el-tabs>
|
]
|
||||||
<el-tab-pane :label="$t('home.general')">
|
}">
|
||||||
<el-form-item :label="$t('home.roomId')" required prop="roomId">
|
<el-tabs type="border-card">
|
||||||
<el-input v-model.number="form.roomId" type="number" min="1"></el-input>
|
<el-tab-pane :label="$t('home.general')">
|
||||||
</el-form-item>
|
<el-form-item :label="$t('home.roomId')" required prop="roomId">
|
||||||
<el-form-item :label="$t('home.showDanmaku')">
|
<el-input v-model.number="form.roomId" type="number" min="1"></el-input>
|
||||||
<el-switch v-model="form.showDanmaku"></el-switch>
|
</el-form-item>
|
||||||
</el-form-item>
|
<el-row :gutter="20">
|
||||||
<el-form-item :label="$t('home.showGift')">
|
<el-col :xs="24" :sm="8">
|
||||||
<el-switch v-model="form.showGift"></el-switch>
|
<el-form-item :label="$t('home.showDanmaku')">
|
||||||
</el-form-item>
|
<el-switch v-model="form.showDanmaku"></el-switch>
|
||||||
<el-form-item :label="$t('home.showGiftName')">
|
</el-form-item>
|
||||||
<el-switch v-model="form.showGiftName"></el-switch>
|
</el-col>
|
||||||
</el-form-item>
|
<el-col :xs="24" :sm="8">
|
||||||
<el-form-item :label="$t('home.mergeSimilarDanmaku')">
|
<el-form-item :label="$t('home.showGift')">
|
||||||
<el-switch v-model="form.mergeSimilarDanmaku"></el-switch>
|
<el-switch v-model="form.showGift"></el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('home.mergeGift')">
|
</el-col>
|
||||||
<el-switch v-model="form.mergeGift"></el-switch>
|
<el-col :xs="24" :sm="8">
|
||||||
</el-form-item>
|
<el-form-item :label="$t('home.showGiftName')">
|
||||||
<el-form-item :label="$t('home.minGiftPrice')">
|
<el-switch v-model="form.showGiftName"></el-switch>
|
||||||
<el-input v-model.number="form.minGiftPrice" type="number" min="0"></el-input>
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-col>
|
||||||
<el-form-item :label="$t('home.maxNumber')">
|
</el-row>
|
||||||
<el-input v-model.number="form.maxNumber" type="number" min="1"></el-input>
|
<el-row :gutter="20">
|
||||||
</el-form-item>
|
<el-col :xs="24" :sm="8">
|
||||||
</el-tab-pane>
|
<el-form-item :label="$t('home.mergeSimilarDanmaku')">
|
||||||
|
<el-switch v-model="form.mergeSimilarDanmaku"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="8">
|
||||||
|
<el-form-item :label="$t('home.mergeGift')">
|
||||||
|
<el-switch v-model="form.mergeGift"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="8">
|
||||||
|
<el-form-item :label="$t('home.minGiftPrice')">
|
||||||
|
<el-input v-model.number="form.minGiftPrice" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="8">
|
||||||
|
<el-form-item :label="$t('home.maxNumber')">
|
||||||
|
<el-input v-model.number="form.maxNumber" type="number" min="1"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane :label="$t('home.block')">
|
<el-tab-pane :label="$t('home.block')">
|
||||||
<el-form-item :label="$t('home.giftDanmaku')">
|
<el-row :gutter="20">
|
||||||
<el-switch v-model="form.blockGiftDanmaku"></el-switch>
|
<el-col :xs="24" :sm="8">
|
||||||
</el-form-item>
|
<el-form-item :label="$t('home.giftDanmaku')">
|
||||||
<el-form-item :label="$t('home.blockLevel')">
|
<el-switch v-model="form.blockGiftDanmaku"></el-switch>
|
||||||
<el-slider v-model="form.blockLevel" show-input :min="0" :max="60"></el-slider>
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-col>
|
||||||
<el-form-item :label="$t('home.informalUser')">
|
<el-col :xs="24" :sm="8">
|
||||||
<el-switch v-model="form.blockNewbie"></el-switch>
|
<el-form-item :label="$t('home.informalUser')">
|
||||||
</el-form-item>
|
<el-switch v-model="form.blockNewbie"></el-switch>
|
||||||
<el-form-item :label="$t('home.unverifiedUser')">
|
</el-form-item>
|
||||||
<el-switch v-model="form.blockNotMobileVerified"></el-switch>
|
</el-col>
|
||||||
</el-form-item>
|
<el-col :xs="24" :sm="8">
|
||||||
<el-form-item :label="$t('home.blockKeywords')">
|
<el-form-item :label="$t('home.unverifiedUser')">
|
||||||
<el-input v-model="form.blockKeywords" type="textarea" :rows="5" :placeholder="$t('home.onePerLine')"></el-input>
|
<el-switch v-model="form.blockNotMobileVerified"></el-switch>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('home.blockUsers')">
|
</el-col>
|
||||||
<el-input v-model="form.blockUsers" type="textarea" :rows="5" :placeholder="$t('home.onePerLine')"></el-input>
|
</el-row>
|
||||||
</el-form-item>
|
<el-row :gutter="20">
|
||||||
<el-form-item :label="$t('home.blockMedalLevel')">
|
<el-col :xs="24" :sm="12">
|
||||||
<el-slider v-model="form.blockMedalLevel" show-input :min="0" :max="40"></el-slider>
|
<el-form-item :label="$t('home.blockLevel')">
|
||||||
</el-form-item>
|
<el-slider v-model="form.blockLevel" show-input :min="0" :max="60"></el-slider>
|
||||||
</el-tab-pane>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('home.blockMedalLevel')">
|
||||||
|
<el-slider v-model="form.blockMedalLevel" show-input :min="0" :max="40"></el-slider>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item :label="$t('home.blockKeywords')">
|
||||||
|
<el-input v-model="form.blockKeywords" type="textarea" :rows="5" :placeholder="$t('home.onePerLine')"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('home.blockUsers')">
|
||||||
|
<el-input v-model="form.blockUsers" type="textarea" :rows="5" :placeholder="$t('home.onePerLine')"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane :label="$t('home.advanced')">
|
<el-tab-pane :label="$t('home.advanced')">
|
||||||
<el-form-item :label="$t('home.relayMessagesByServer')">
|
<el-row :gutter="20">
|
||||||
<el-switch v-model="form.relayMessagesByServer"></el-switch>
|
<el-col :xs="24" :sm="8">
|
||||||
</el-form-item>
|
<el-form-item :label="$t('home.relayMessagesByServer')">
|
||||||
<el-form-item :label="$t('home.autoTranslate')">
|
<el-switch v-model="form.relayMessagesByServer"></el-switch>
|
||||||
<el-switch v-model="form.autoTranslate" :disabled="!serverConfig.enableTranslate || !form.relayMessagesByServer"></el-switch>
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-col>
|
||||||
<el-form-item :label="$t('home.giftUsernamePronunciation')">
|
<el-col :xs="24" :sm="8">
|
||||||
<el-radio-group v-model="form.giftUsernamePronunciation">
|
<el-form-item :label="$t('home.autoTranslate')">
|
||||||
<el-radio label="">{{$t('home.dontShow')}}</el-radio>
|
<el-switch v-model="form.autoTranslate" :disabled="!serverConfig.enableTranslate || !form.relayMessagesByServer"></el-switch>
|
||||||
<el-radio label="pinyin">{{$t('home.pinyin')}}</el-radio>
|
</el-form-item>
|
||||||
<el-radio label="kana">{{$t('home.kana')}}</el-radio>
|
</el-col>
|
||||||
</el-radio-group>
|
</el-row>
|
||||||
</el-form-item>
|
<el-form-item :label="$t('home.giftUsernamePronunciation')">
|
||||||
</el-tab-pane>
|
<el-radio-group v-model="form.giftUsernamePronunciation">
|
||||||
</el-tabs>
|
<el-radio label="">{{$t('home.dontShow')}}</el-radio>
|
||||||
|
<el-radio label="pinyin">{{$t('home.pinyin')}}</el-radio>
|
||||||
|
<el-radio label="kana">{{$t('home.kana')}}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-form>
|
||||||
|
</p>
|
||||||
|
|
||||||
<el-divider></el-divider>
|
<p>
|
||||||
<el-form-item :label="$t('home.roomUrl')">
|
<el-card>
|
||||||
<el-input ref="roomUrlInput" readonly :value="obsRoomUrl" style="width: calc(100% - 8em); margin-right: 1em;"></el-input>
|
<el-form :model="form" label-width="150px">
|
||||||
<el-button type="primary" @click="copyUrl">{{$t('home.copy')}}</el-button>
|
<el-form-item :label="$t('home.roomUrl')">
|
||||||
</el-form-item>
|
<el-input ref="roomUrlInput" readonly :value="obsRoomUrl" style="width: calc(100% - 8em); margin-right: 1em;"></el-input>
|
||||||
<el-form-item>
|
<el-button type="primary" @click="copyUrl">{{$t('home.copy')}}</el-button>
|
||||||
<el-button type="primary" :disabled="!roomUrl" @click="enterRoom">{{$t('home.enterRoom')}}</el-button>
|
</el-form-item>
|
||||||
<el-button type="primary" @click="exportConfig">{{$t('home.exportConfig')}}</el-button>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="importConfig">{{$t('home.importConfig')}}</el-button>
|
<el-button type="primary" :disabled="!roomUrl" @click="enterRoom">{{$t('home.enterRoom')}}</el-button>
|
||||||
</el-form-item>
|
<el-button :disabled="!roomUrl" @click="enterTestRoom">{{$t('home.enterTestRoom')}}</el-button>
|
||||||
</el-form>
|
<el-button @click="exportConfig">{{$t('home.exportConfig')}}</el-button>
|
||||||
|
<el-button @click="importConfig">{{$t('home.importConfig')}}</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -111,13 +161,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
roomUrl() {
|
roomUrl() {
|
||||||
if (this.form.roomId === '') {
|
return this.getRoomUrl(false)
|
||||||
return ''
|
|
||||||
}
|
|
||||||
let query = {...this.form}
|
|
||||||
delete query.roomId
|
|
||||||
let resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId}, query})
|
|
||||||
return `${window.location.protocol}//${window.location.host}${resolved.href}`
|
|
||||||
},
|
},
|
||||||
obsRoomUrl() {
|
obsRoomUrl() {
|
||||||
if (this.roomUrl === '') {
|
if (this.roomUrl === '') {
|
||||||
@ -151,6 +195,23 @@ export default {
|
|||||||
enterRoom() {
|
enterRoom() {
|
||||||
window.open(this.roomUrl, `room ${this.form.roomId}`, 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
|
window.open(this.roomUrl, `room ${this.form.roomId}`, 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
|
||||||
},
|
},
|
||||||
|
enterTestRoom() {
|
||||||
|
window.open(this.getRoomUrl(true), 'test room', 'menubar=0,location=0,scrollbars=0,toolbar=0,width=600,height=600')
|
||||||
|
},
|
||||||
|
getRoomUrl(isTestRoom) {
|
||||||
|
if (isTestRoom && this.form.roomId === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
let query = {...this.form}
|
||||||
|
delete query.roomId
|
||||||
|
let resolved
|
||||||
|
if (isTestRoom) {
|
||||||
|
resolved = this.$router.resolve({name: 'test_room', query})
|
||||||
|
} else {
|
||||||
|
resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId}, query})
|
||||||
|
}
|
||||||
|
return `${window.location.protocol}//${window.location.host}${resolved.href}`
|
||||||
|
},
|
||||||
copyUrl() {
|
copyUrl() {
|
||||||
this.$refs.roomUrlInput.select()
|
this.$refs.roomUrlInput.select()
|
||||||
document.execCommand('Copy')
|
document.execCommand('Copy')
|
||||||
@ -183,9 +244,3 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.el-form {
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import {mergeConfig, toBool, toInt} from '@/utils'
|
import {mergeConfig, toBool, toInt} from '@/utils'
|
||||||
import * as pronunciation from '@/utils/pronunciation'
|
import * as pronunciation from '@/utils/pronunciation'
|
||||||
import * as chatConfig from '@/api/chatConfig'
|
import * as chatConfig from '@/api/chatConfig'
|
||||||
|
import ChatClientTest from '@/api/chat/ChatClientTest'
|
||||||
import ChatClientDirect from '@/api/chat/ChatClientDirect'
|
import ChatClientDirect from '@/api/chat/ChatClientDirect'
|
||||||
import ChatClientRelay from '@/api/chat/ChatClientRelay'
|
import ChatClientRelay from '@/api/chat/ChatClientRelay'
|
||||||
import ChatRenderer from '@/components/ChatRenderer'
|
import ChatRenderer from '@/components/ChatRenderer'
|
||||||
@ -16,6 +17,16 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
ChatRenderer
|
ChatRenderer
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
roomId: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
strConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
config: {...chatConfig.DEFAULT_CONFIG},
|
config: {...chatConfig.DEFAULT_CONFIG},
|
||||||
@ -54,9 +65,9 @@ export default {
|
|||||||
initConfig() {
|
initConfig() {
|
||||||
let cfg = {}
|
let cfg = {}
|
||||||
// 留空的使用默认值
|
// 留空的使用默认值
|
||||||
for (let i in this.$route.query) {
|
for (let i in this.strConfig) {
|
||||||
if (this.$route.query[i] !== '') {
|
if (this.strConfig[i] !== '') {
|
||||||
cfg[i] = this.$route.query[i]
|
cfg[i] = this.strConfig[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg = mergeConfig(cfg, chatConfig.DEFAULT_CONFIG)
|
cfg = mergeConfig(cfg, chatConfig.DEFAULT_CONFIG)
|
||||||
@ -79,11 +90,14 @@ export default {
|
|||||||
this.config = cfg
|
this.config = cfg
|
||||||
},
|
},
|
||||||
initChatClient() {
|
initChatClient() {
|
||||||
let roomId = parseInt(this.$route.params.roomId)
|
if (this.roomId === null) {
|
||||||
if (!this.config.relayMessagesByServer) {
|
this.chatClient = new ChatClientTest()
|
||||||
this.chatClient = new ChatClientDirect(roomId)
|
|
||||||
} else {
|
} else {
|
||||||
this.chatClient = new ChatClientRelay(roomId, this.config.autoTranslate)
|
if (!this.config.relayMessagesByServer) {
|
||||||
|
this.chatClient = new ChatClientDirect(this.roomId)
|
||||||
|
} else {
|
||||||
|
this.chatClient = new ChatClientRelay(this.roomId, this.config.autoTranslate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.chatClient.onAddText = this.onAddText
|
this.chatClient.onAddText = this.onAddText
|
||||||
this.chatClient.onAddGift = this.onAddGift
|
this.chatClient.onAddGift = this.onAddGift
|
||||||
@ -94,6 +108,13 @@ export default {
|
|||||||
this.chatClient.start()
|
this.chatClient.start()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.chatClient.start()
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
this.chatClient.stop()
|
||||||
|
},
|
||||||
|
|
||||||
onAddText(data) {
|
onAddText(data) {
|
||||||
if (!this.config.showDanmaku || !this.filterTextMessage(data) || this.mergeSimilarText(data.content)) {
|
if (!this.config.showDanmaku || !this.filterTextMessage(data) || this.mergeSimilarText(data.content)) {
|
||||||
return
|
return
|
||||||
|
33
frontend/src/views/StyleGenerator/FontSelect.vue
Normal file
33
frontend/src/views/StyleGenerator/FontSelect.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<el-select :value="value" @input="val => $emit('input', val)" filterable allow-create default-first-option>
|
||||||
|
<el-option-group>
|
||||||
|
<el-option v-for="font in LOCAL_FONTS" :key="font" :value="font"></el-option>
|
||||||
|
</el-option-group>
|
||||||
|
<el-option-group>
|
||||||
|
<el-option v-for="font in NETWORK_FONTS" :key="font" :value="font"></el-option>
|
||||||
|
</el-option-group>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as fonts from './fonts'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FontSelect',
|
||||||
|
props: {
|
||||||
|
value: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
LOCAL_FONTS: fonts.LOCAL_FONTS,
|
||||||
|
NETWORK_FONTS: fonts.NETWORK_FONTS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-select {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
</style>
|
689
frontend/src/views/StyleGenerator/Legacy.vue
Normal file
689
frontend/src/views/StyleGenerator/Legacy.vue
Normal file
@ -0,0 +1,689 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-form label-width="150px" size="mini">
|
||||||
|
<h3>{{$t('stylegen.outlines')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showOutlines')">
|
||||||
|
<el-switch v-model="form.showOutlines"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.outlineColor')">
|
||||||
|
<el-color-picker v-model="form.outlineColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item :label="$t('stylegen.outlineSize')">
|
||||||
|
<el-input v-model.number="form.outlineSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.avatars')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showAvatars')">
|
||||||
|
<el-switch v-model="form.showAvatars"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.avatarSize')">
|
||||||
|
<el-input v-model.number="form.avatarSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.userNames')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showUserNames')">
|
||||||
|
<el-switch v-model="form.showUserNames"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.font')">
|
||||||
|
<font-select v-model="form.userNameFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fontSize')">
|
||||||
|
<el-input v-model.number="form.userNameFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||||
|
<el-input v-model.number="form.userNameLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.normalColor')">
|
||||||
|
<el-color-picker v-model="form.userNameColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.memberColor')">
|
||||||
|
<el-color-picker v-model="form.memberUserNameColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.moderatorColor')">
|
||||||
|
<el-color-picker v-model="form.moderatorUserNameColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.ownerColor')">
|
||||||
|
<el-color-picker v-model="form.ownerUserNameColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showBadges')">
|
||||||
|
<el-switch v-model="form.showBadges"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showColon')">
|
||||||
|
<el-switch v-model="form.showColon"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.messages')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.font')">
|
||||||
|
<font-select v-model="form.messageFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.color')">
|
||||||
|
<el-color-picker v-model="form.messageColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fontSize')">
|
||||||
|
<el-input v-model.number="form.messageFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||||
|
<el-input v-model.number="form.messageLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item :label="$t('stylegen.onNewLine')">
|
||||||
|
<el-switch v-model="form.messageOnNewLine"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.time')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-form-item :label="$t('stylegen.showTime')">
|
||||||
|
<el-switch v-model="form.showTime"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.font')">
|
||||||
|
<font-select v-model="form.timeFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.color')">
|
||||||
|
<el-color-picker v-model="form.timeColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fontSize')">
|
||||||
|
<el-input v-model.number="form.timeFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||||
|
<el-input v-model.number="form.timeLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.backgrounds')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.bgColor')">
|
||||||
|
<el-color-picker v-model="form.bgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.useBarsInsteadOfBg')">
|
||||||
|
<el-switch v-model="form.useBarsInsteadOfBg"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.messageBgColor')">
|
||||||
|
<el-color-picker v-model="form.messageBgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.memberMessageBgColor')">
|
||||||
|
<el-color-picker v-model="form.memberMessageBgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.moderatorMessageBgColor')">
|
||||||
|
<el-color-picker v-model="form.moderatorMessageBgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.ownerMessageBgColor')">
|
||||||
|
<el-color-picker v-model="form.ownerMessageBgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.scAndNewMember')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.firstLineFont')">
|
||||||
|
<font-select v-model="form.firstLineFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.firstLineColor')">
|
||||||
|
<el-color-picker v-model="form.firstLineColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.firstLineFontSize')">
|
||||||
|
<el-input v-model.number="form.firstLineFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.firstLineLineHeight')">
|
||||||
|
<el-input v-model.number="form.firstLineLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.secondLineFont')">
|
||||||
|
<font-select v-model="form.secondLineFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.secondLineColor')">
|
||||||
|
<el-color-picker v-model="form.secondLineColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.secondLineFontSize')">
|
||||||
|
<el-input v-model.number="form.secondLineFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.secondLineLineHeight')">
|
||||||
|
<el-input v-model.number="form.secondLineLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.scContentLineFont')">
|
||||||
|
<font-select v-model="form.scContentFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.scContentLineColor')">
|
||||||
|
<el-color-picker v-model="form.scContentColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.scContentLineFontSize')">
|
||||||
|
<el-input v-model.number="form.scContentFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.scContentLineLineHeight')">
|
||||||
|
<el-input v-model.number="form.scContentLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('stylegen.showNewMemberBg')">
|
||||||
|
<el-switch v-model="form.showNewMemberBg"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showScTicker')">
|
||||||
|
<el-switch v-model="form.showScTicker"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showOtherThings')">
|
||||||
|
<el-switch v-model="form.showOtherThings"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.animation')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.animateIn')">
|
||||||
|
<el-switch v-model="form.animateIn"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fadeInTime')">
|
||||||
|
<el-input v-model.number="form.fadeInTime" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.animateOut')">
|
||||||
|
<el-switch v-model="form.animateOut"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fadeOutTime')">
|
||||||
|
<el-input v-model.number="form.fadeOutTime" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item :label="$t('stylegen.animateOutWaitTime')">
|
||||||
|
<el-input v-model.number="form.animateOutWaitTime" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.slide')">
|
||||||
|
<el-switch v-model="form.slide"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.reverseSlide')">
|
||||||
|
<el-switch v-model="form.reverseSlide"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import FontSelect from './FontSelect'
|
||||||
|
import * as common from './common'
|
||||||
|
import {mergeConfig} from '@/utils'
|
||||||
|
|
||||||
|
export const DEFAULT_CONFIG = {
|
||||||
|
showOutlines: true,
|
||||||
|
outlineSize: 2,
|
||||||
|
outlineColor: '#000000',
|
||||||
|
|
||||||
|
showAvatars: true,
|
||||||
|
avatarSize: 24,
|
||||||
|
|
||||||
|
showUserNames: true,
|
||||||
|
userNameFont: 'Changa One',
|
||||||
|
userNameFontSize: 20,
|
||||||
|
userNameLineHeight: 0,
|
||||||
|
userNameColor: '#cccccc',
|
||||||
|
ownerUserNameColor: '#ffd600',
|
||||||
|
moderatorUserNameColor: '#5e84f1',
|
||||||
|
memberUserNameColor: '#0f9d58',
|
||||||
|
showBadges: true,
|
||||||
|
showColon: true,
|
||||||
|
|
||||||
|
messageFont: 'Imprima',
|
||||||
|
messageFontSize: 18,
|
||||||
|
messageLineHeight: 0,
|
||||||
|
messageColor: '#ffffff',
|
||||||
|
messageOnNewLine: false,
|
||||||
|
|
||||||
|
showTime: false,
|
||||||
|
timeFont: 'Imprima',
|
||||||
|
timeFontSize: 16,
|
||||||
|
timeLineHeight: 0,
|
||||||
|
timeColor: '#999999',
|
||||||
|
|
||||||
|
bgColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
useBarsInsteadOfBg: false,
|
||||||
|
messageBgColor: 'rgba(204, 204, 204, 0)',
|
||||||
|
ownerMessageBgColor: 'rgba(255, 214, 0, 0)',
|
||||||
|
moderatorMessageBgColor: 'rgba(94, 132, 241, 0)',
|
||||||
|
memberMessageBgColor: 'rgba(15, 157, 88, 0)',
|
||||||
|
|
||||||
|
firstLineFont: 'Changa One',
|
||||||
|
firstLineFontSize: 20,
|
||||||
|
firstLineLineHeight: 0,
|
||||||
|
firstLineColor: '#ffffff',
|
||||||
|
secondLineFont: 'Imprima',
|
||||||
|
secondLineFontSize: 18,
|
||||||
|
secondLineLineHeight: 0,
|
||||||
|
secondLineColor: '#ffffff',
|
||||||
|
scContentFont: 'Imprima',
|
||||||
|
scContentFontSize: 18,
|
||||||
|
scContentLineHeight: 0,
|
||||||
|
scContentColor: '#ffffff',
|
||||||
|
showNewMemberBg: true,
|
||||||
|
showScTicker: false,
|
||||||
|
showOtherThings: true,
|
||||||
|
|
||||||
|
animateIn: false,
|
||||||
|
fadeInTime: 200, // ms
|
||||||
|
animateOut: false,
|
||||||
|
animateOutWaitTime: 30, // s
|
||||||
|
fadeOutTime: 200, // ms
|
||||||
|
slide: false,
|
||||||
|
reverseSlide: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Legacy',
|
||||||
|
components: {
|
||||||
|
FontSelect
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: this.loadConfig()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
result() {
|
||||||
|
return `${this.importStyle}
|
||||||
|
|
||||||
|
${common.COMMON_STYLE}
|
||||||
|
|
||||||
|
${this.paddingStyle}
|
||||||
|
|
||||||
|
${this.outlineStyle}
|
||||||
|
|
||||||
|
${this.avatarStyle}
|
||||||
|
|
||||||
|
${this.userNameStyle}
|
||||||
|
|
||||||
|
${this.messageStyle}
|
||||||
|
|
||||||
|
${this.timeStyle}
|
||||||
|
|
||||||
|
${this.backgroundStyle}
|
||||||
|
|
||||||
|
${this.scAndNewMemberStyle}
|
||||||
|
|
||||||
|
${this.animationStyle}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
importStyle() {
|
||||||
|
let allFonts = []
|
||||||
|
for (let name of ['userNameFont', 'messageFont', 'timeFont', 'firstLineFont', 'secondLineFont', 'scContentFont']) {
|
||||||
|
allFonts.push(this.form[name])
|
||||||
|
}
|
||||||
|
return common.getImportStyle(allFonts)
|
||||||
|
},
|
||||||
|
paddingStyle() {
|
||||||
|
return `/* Reduce side padding */
|
||||||
|
yt-live-chat-text-message-renderer {
|
||||||
|
padding-left: ${this.form.useBarsInsteadOfBg ? 20 : 4}px !important;
|
||||||
|
padding-right: 4px !important;
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
outlineStyle() {
|
||||||
|
return `/* Outlines */
|
||||||
|
yt-live-chat-renderer * {
|
||||||
|
${this.showOutlinesStyle}
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.messageFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.messageFontSize}px !important;
|
||||||
|
line-height: ${this.form.messageLineHeight || this.form.messageFontSize}px !important;
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
showOutlinesStyle () {
|
||||||
|
if (!this.form.showOutlines || !this.form.outlineSize) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
let shadow = []
|
||||||
|
for (let x = -this.form.outlineSize; x <= this.form.outlineSize; x += Math.ceil(this.form.outlineSize / 4)) {
|
||||||
|
for (let y = -this.form.outlineSize; y <= this.form.outlineSize; y += Math.ceil(this.form.outlineSize / 4)) {
|
||||||
|
shadow.push(`${x}px ${y}px ${this.form.outlineColor}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `text-shadow: ${shadow.join(', ')};`
|
||||||
|
},
|
||||||
|
avatarStyle() {
|
||||||
|
return common.getAvatarStyle(this.form)
|
||||||
|
},
|
||||||
|
userNameStyle() {
|
||||||
|
return `/* Channel names */
|
||||||
|
yt-live-chat-text-message-renderer #author-name[type="owner"],
|
||||||
|
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
|
||||||
|
${this.form.ownerUserNameColor ? `color: ${this.form.ownerUserNameColor} !important;` : ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer #author-name[type="moderator"],
|
||||||
|
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
|
||||||
|
${this.form.moderatorUserNameColor ? `color: ${this.form.moderatorUserNameColor} !important;` : ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer #author-name[type="member"],
|
||||||
|
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
|
||||||
|
${this.form.memberUserNameColor ? `color: ${this.form.memberUserNameColor} !important;` : ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer #author-name {
|
||||||
|
${this.form.showUserNames ? '' : 'display: none !important;'}
|
||||||
|
${this.form.userNameColor ? `color: ${this.form.userNameColor} !important;` : ''}
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.userNameFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.userNameFontSize}px !important;
|
||||||
|
line-height: ${this.form.userNameLineHeight || this.form.userNameFontSize}px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
${!this.form.showColon ? '' : `/* Show colon */
|
||||||
|
yt-live-chat-text-message-renderer #author-name::after {
|
||||||
|
content: ":";
|
||||||
|
margin-left: ${this.form.outlineSize}px;
|
||||||
|
}`}
|
||||||
|
|
||||||
|
/* Hide badges */
|
||||||
|
yt-live-chat-text-message-renderer #chat-badges {
|
||||||
|
${this.form.showBadges ? '' : 'display: none !important;'}
|
||||||
|
vertical-align: text-top !important;
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
messageStyle() {
|
||||||
|
return `/* Messages */
|
||||||
|
yt-live-chat-text-message-renderer #message,
|
||||||
|
yt-live-chat-text-message-renderer #message * {
|
||||||
|
${this.form.messageColor ? `color: ${this.form.messageColor} !important;` : ''}
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.messageFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.messageFontSize}px !important;
|
||||||
|
line-height: ${this.form.messageLineHeight || this.form.messageFontSize}px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
${!this.form.messageOnNewLine ? '' : `yt-live-chat-text-message-renderer #message {
|
||||||
|
display: block !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}`}`
|
||||||
|
},
|
||||||
|
timeStyle() {
|
||||||
|
return common.getTimeStyle(this.form)
|
||||||
|
},
|
||||||
|
backgroundStyle() {
|
||||||
|
return `/* Background colors */
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
${this.form.bgColor ? `background-color: ${this.form.bgColor};` : ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
${this.getBgStyleForAuthorType('', this.form.messageBgColor)}
|
||||||
|
|
||||||
|
${this.getBgStyleForAuthorType('owner', this.form.ownerMessageBgColor)}
|
||||||
|
|
||||||
|
${this.getBgStyleForAuthorType('moderator', this.form.moderatorMessageBgColor)}
|
||||||
|
|
||||||
|
${this.getBgStyleForAuthorType('member', this.form.memberMessageBgColor)}`
|
||||||
|
},
|
||||||
|
scAndNewMemberStyle() {
|
||||||
|
return `/* SuperChat/Fan Funding Messages */
|
||||||
|
yt-live-chat-paid-message-renderer {
|
||||||
|
margin: 4px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
${this.scAndNewMemberFontStyle}
|
||||||
|
|
||||||
|
yt-live-chat-membership-item-renderer #card,
|
||||||
|
yt-live-chat-membership-item-renderer #header {
|
||||||
|
${this.showNewMemberBgStyle}
|
||||||
|
}
|
||||||
|
|
||||||
|
${this.scTickerStyle}
|
||||||
|
|
||||||
|
${this.form.showOtherThings ? '' : `yt-live-chat-item-list-renderer {
|
||||||
|
display: none !important;
|
||||||
|
}`}`
|
||||||
|
},
|
||||||
|
scAndNewMemberFontStyle() {
|
||||||
|
return `yt-live-chat-paid-message-renderer #author-name,
|
||||||
|
yt-live-chat-paid-message-renderer #author-name *,
|
||||||
|
yt-live-chat-membership-item-renderer #header-content-inner-column,
|
||||||
|
yt-live-chat-membership-item-renderer #header-content-inner-column * {
|
||||||
|
${this.form.firstLineColor ? `color: ${this.form.firstLineColor} !important;` : ''}
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.firstLineFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.firstLineFontSize}px !important;
|
||||||
|
line-height: ${this.form.firstLineLineHeight || this.form.firstLineFontSize}px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-paid-message-renderer #purchase-amount,
|
||||||
|
yt-live-chat-paid-message-renderer #purchase-amount *,
|
||||||
|
yt-live-chat-membership-item-renderer #header-subtext,
|
||||||
|
yt-live-chat-membership-item-renderer #header-subtext * {
|
||||||
|
${this.form.secondLineColor ? `color: ${this.form.secondLineColor} !important;` : ''}
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.secondLineFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.secondLineFontSize}px !important;
|
||||||
|
line-height: ${this.form.secondLineLineHeight || this.form.secondLineFontSize}px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-paid-message-renderer #content,
|
||||||
|
yt-live-chat-paid-message-renderer #content * {
|
||||||
|
${this.form.scContentColor ? `color: ${this.form.scContentColor} !important;` : ''}
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.scContentFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.scContentFontSize}px !important;
|
||||||
|
line-height: ${this.form.scContentLineHeight || this.form.scContentFontSize}px !important;
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
showNewMemberBgStyle() {
|
||||||
|
if (this.form.showNewMemberBg) {
|
||||||
|
return `background-color: ${this.form.memberUserNameColor} !important;
|
||||||
|
margin: 4px 0 !important;`
|
||||||
|
} else {
|
||||||
|
return `background-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
margin: 0 !important;`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scTickerStyle() {
|
||||||
|
return `${this.form.showScTicker ? '' : `yt-live-chat-ticker-renderer {
|
||||||
|
display: none !important;
|
||||||
|
}`}
|
||||||
|
|
||||||
|
/* SuperChat Ticker */
|
||||||
|
yt-live-chat-ticker-paid-message-item-renderer,
|
||||||
|
yt-live-chat-ticker-paid-message-item-renderer *,
|
||||||
|
yt-live-chat-ticker-sponsor-item-renderer,
|
||||||
|
yt-live-chat-ticker-sponsor-item-renderer * {
|
||||||
|
${this.form.secondLineColor ? `color: ${this.form.secondLineColor} !important;` : ''}
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.secondLineFont)}"${common.FALLBACK_FONTS};
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
animationStyle() {
|
||||||
|
return common.getAnimationStyle(this.form)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
result(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
this.saveConfig()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$emit('input', this.result)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveConfig: _.debounce(function() {
|
||||||
|
let config = mergeConfig(this.form, DEFAULT_CONFIG)
|
||||||
|
window.localStorage.stylegenConfig = JSON.stringify(config)
|
||||||
|
}, 500),
|
||||||
|
loadConfig() {
|
||||||
|
try {
|
||||||
|
return mergeConfig(JSON.parse(window.localStorage.stylegenConfig), DEFAULT_CONFIG)
|
||||||
|
} catch {
|
||||||
|
return {...DEFAULT_CONFIG}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetConfig() {
|
||||||
|
this.form = {...DEFAULT_CONFIG}
|
||||||
|
},
|
||||||
|
|
||||||
|
getBgStyleForAuthorType(authorType, color) {
|
||||||
|
let typeSelector = authorType ? `[author-type="${authorType}"]` : ''
|
||||||
|
if (!this.form.useBarsInsteadOfBg) {
|
||||||
|
return `yt-live-chat-text-message-renderer${typeSelector},
|
||||||
|
yt-live-chat-text-message-renderer${typeSelector}[is-highlighted] {
|
||||||
|
${color ? `background-color: ${color} !important;` : ''}
|
||||||
|
}`
|
||||||
|
} else {
|
||||||
|
return `yt-live-chat-text-message-renderer${typeSelector}::after {
|
||||||
|
${color ? `border: 2px solid ${color};` : ''}
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
left: 8px;
|
||||||
|
top: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
width: 1px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 2px;
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
598
frontend/src/views/StyleGenerator/LineLike.vue
Normal file
598
frontend/src/views/StyleGenerator/LineLike.vue
Normal file
@ -0,0 +1,598 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-form label-width="150px" size="mini">
|
||||||
|
<h3>{{$t('stylegen.avatars')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showAvatars')">
|
||||||
|
<el-switch v-model="form.showAvatars"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.avatarSize')">
|
||||||
|
<el-input v-model.number="form.avatarSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.userNames')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showUserNames')">
|
||||||
|
<el-switch v-model="form.showUserNames"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.font')">
|
||||||
|
<font-select v-model="form.userNameFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fontSize')">
|
||||||
|
<el-input v-model.number="form.userNameFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||||
|
<el-input v-model.number="form.userNameLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.normalColor')">
|
||||||
|
<el-color-picker v-model="form.userNameColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.memberColor')">
|
||||||
|
<el-color-picker v-model="form.memberUserNameColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.moderatorColor')">
|
||||||
|
<el-color-picker v-model="form.moderatorUserNameColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.ownerColor')">
|
||||||
|
<el-color-picker v-model="form.ownerUserNameColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showBadges')">
|
||||||
|
<el-switch v-model="form.showBadges"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.messages')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.font')">
|
||||||
|
<font-select v-model="form.messageFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.color')">
|
||||||
|
<el-color-picker v-model="form.messageColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fontSize')">
|
||||||
|
<el-input v-model.number="form.messageFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||||
|
<el-input v-model.number="form.messageLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.time')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-form-item :label="$t('stylegen.showTime')">
|
||||||
|
<el-switch v-model="form.showTime"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.font')">
|
||||||
|
<font-select v-model="form.timeFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.color')">
|
||||||
|
<el-color-picker v-model="form.timeColor"></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fontSize')">
|
||||||
|
<el-input v-model.number="form.timeFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.lineHeight')">
|
||||||
|
<el-input v-model.number="form.timeLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.backgrounds')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.bgColor')">
|
||||||
|
<el-color-picker v-model="form.bgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.messageBgColor')">
|
||||||
|
<el-color-picker v-model="form.messageBgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.memberMessageBgColor')">
|
||||||
|
<el-color-picker v-model="form.memberMessageBgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.moderatorMessageBgColor')">
|
||||||
|
<el-color-picker v-model="form.moderatorMessageBgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.ownerMessageBgColor')">
|
||||||
|
<el-color-picker v-model="form.ownerMessageBgColor" show-alpha></el-color-picker>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.scAndNewMember')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.firstLineFont')">
|
||||||
|
<font-select v-model="form.firstLineFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.firstLineFontSize')">
|
||||||
|
<el-input v-model.number="form.firstLineFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.firstLineLineHeight')">
|
||||||
|
<el-input v-model.number="form.firstLineLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.secondLineFont')">
|
||||||
|
<font-select v-model="form.secondLineFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.secondLineFontSize')">
|
||||||
|
<el-input v-model.number="form.secondLineFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.secondLineLineHeight')">
|
||||||
|
<el-input v-model.number="form.secondLineLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.scContentLineFont')">
|
||||||
|
<font-select v-model="form.scContentFont"></font-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.scContentLineFontSize')">
|
||||||
|
<el-input v-model.number="form.scContentFontSize" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.scContentLineLineHeight')">
|
||||||
|
<el-input v-model.number="form.scContentLineHeight" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showScTicker')">
|
||||||
|
<el-switch v-model="form.showScTicker"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.showOtherThings')">
|
||||||
|
<el-switch v-model="form.showOtherThings"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<h3>{{$t('stylegen.animation')}}</h3>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.animateIn')">
|
||||||
|
<el-switch v-model="form.animateIn"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fadeInTime')">
|
||||||
|
<el-input v-model.number="form.fadeInTime" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.animateOut')">
|
||||||
|
<el-switch v-model="form.animateOut"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.fadeOutTime')">
|
||||||
|
<el-input v-model.number="form.fadeOutTime" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item :label="$t('stylegen.animateOutWaitTime')">
|
||||||
|
<el-input v-model.number="form.animateOutWaitTime" type="number" min="0"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.slide')">
|
||||||
|
<el-switch v-model="form.slide"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12">
|
||||||
|
<el-form-item :label="$t('stylegen.reverseSlide')">
|
||||||
|
<el-switch v-model="form.reverseSlide"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import FontSelect from './FontSelect'
|
||||||
|
import * as common from './common'
|
||||||
|
import {mergeConfig} from '@/utils'
|
||||||
|
|
||||||
|
export const DEFAULT_CONFIG = {
|
||||||
|
showAvatars: true,
|
||||||
|
avatarSize: 40,
|
||||||
|
|
||||||
|
showUserNames: true,
|
||||||
|
userNameFont: 'Noto Sans SC',
|
||||||
|
userNameFontSize: 20,
|
||||||
|
userNameLineHeight: 0,
|
||||||
|
userNameColor: '#cccccc',
|
||||||
|
ownerUserNameColor: '#ffd600',
|
||||||
|
moderatorUserNameColor: '#5e84f1',
|
||||||
|
memberUserNameColor: '#0f9d58',
|
||||||
|
showBadges: true,
|
||||||
|
|
||||||
|
messageFont: 'Noto Sans SC',
|
||||||
|
messageFontSize: 18,
|
||||||
|
messageLineHeight: 0,
|
||||||
|
messageColor: '#000000',
|
||||||
|
|
||||||
|
showTime: false,
|
||||||
|
timeFont: 'Noto Sans SC',
|
||||||
|
timeFontSize: 16,
|
||||||
|
timeLineHeight: 0,
|
||||||
|
timeColor: '#999999',
|
||||||
|
|
||||||
|
bgColor: 'rgba(0, 0, 0, 0)',
|
||||||
|
messageBgColor: '#ffffff',
|
||||||
|
ownerMessageBgColor: 'rgba(231, 199, 30, 1)',
|
||||||
|
moderatorMessageBgColor: 'rgba(41, 95, 251, 1)',
|
||||||
|
memberMessageBgColor: 'rgba(43, 234, 43, 1)',
|
||||||
|
|
||||||
|
firstLineFont: 'Noto Sans SC',
|
||||||
|
firstLineFontSize: 20,
|
||||||
|
firstLineLineHeight: 0,
|
||||||
|
secondLineFont: 'Noto Sans SC',
|
||||||
|
secondLineFontSize: 18,
|
||||||
|
secondLineLineHeight: 0,
|
||||||
|
scContentFont: 'Noto Sans SC',
|
||||||
|
scContentFontSize: 18,
|
||||||
|
scContentLineHeight: 0,
|
||||||
|
showScTicker: false,
|
||||||
|
showOtherThings: true,
|
||||||
|
|
||||||
|
animateIn: true,
|
||||||
|
fadeInTime: 200, // ms
|
||||||
|
animateOut: false,
|
||||||
|
animateOutWaitTime: 30, // s
|
||||||
|
fadeOutTime: 200, // ms
|
||||||
|
slide: true,
|
||||||
|
reverseSlide: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LineLike',
|
||||||
|
components: {
|
||||||
|
FontSelect
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: this.loadConfig()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
result() {
|
||||||
|
return `${this.importStyle}
|
||||||
|
|
||||||
|
${common.COMMON_STYLE}
|
||||||
|
|
||||||
|
${this.paddingStyle}
|
||||||
|
|
||||||
|
${this.avatarStyle}
|
||||||
|
|
||||||
|
${this.userNameStyle}
|
||||||
|
|
||||||
|
${this.messageStyle}
|
||||||
|
|
||||||
|
${this.timeStyle}
|
||||||
|
|
||||||
|
${this.backgroundStyle}
|
||||||
|
|
||||||
|
${this.scAndNewMemberStyle}
|
||||||
|
|
||||||
|
${this.animationStyle}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
importStyle() {
|
||||||
|
let allFonts = []
|
||||||
|
for (let name of ['userNameFont', 'messageFont', 'timeFont', 'firstLineFont', 'secondLineFont', 'scContentFont']) {
|
||||||
|
allFonts.push(this.form[name])
|
||||||
|
}
|
||||||
|
return common.getImportStyle(allFonts)
|
||||||
|
},
|
||||||
|
paddingStyle() {
|
||||||
|
return `/* Reduce side padding */
|
||||||
|
yt-live-chat-text-message-renderer {
|
||||||
|
padding-left: 4px !important;
|
||||||
|
padding-right: 4px !important;
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
avatarStyle() {
|
||||||
|
return common.getAvatarStyle(this.form)
|
||||||
|
},
|
||||||
|
userNameStyle() {
|
||||||
|
return `/* Channel names */
|
||||||
|
yt-live-chat-text-message-renderer yt-live-chat-author-chip {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer #author-name[type="owner"],
|
||||||
|
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
|
||||||
|
${this.form.ownerUserNameColor ? `color: ${this.form.ownerUserNameColor} !important;` : ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer #author-name[type="moderator"],
|
||||||
|
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
|
||||||
|
${this.form.moderatorUserNameColor ? `color: ${this.form.moderatorUserNameColor} !important;` : ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer #author-name[type="member"],
|
||||||
|
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
|
||||||
|
${this.form.memberUserNameColor ? `color: ${this.form.memberUserNameColor} !important;` : ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer #author-name {
|
||||||
|
${this.form.showUserNames ? '' : 'display: none !important;'}
|
||||||
|
${this.form.userNameColor ? `color: ${this.form.userNameColor} !important;` : ''}
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.userNameFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.userNameFontSize}px !important;
|
||||||
|
line-height: ${this.form.userNameLineHeight || this.form.userNameFontSize}px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide badges */
|
||||||
|
yt-live-chat-text-message-renderer #chat-badges {
|
||||||
|
${this.form.showBadges ? '' : 'display: none !important;'}
|
||||||
|
vertical-align: text-top !important;
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
messageStyle() {
|
||||||
|
return `/* Messages */
|
||||||
|
yt-live-chat-text-message-renderer #message,
|
||||||
|
yt-live-chat-text-message-renderer #message * {
|
||||||
|
${this.form.messageColor ? `color: ${this.form.messageColor} !important;` : ''}
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.messageFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.messageFontSize}px !important;
|
||||||
|
line-height: ${this.form.messageLineHeight || this.form.messageFontSize}px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer #message {
|
||||||
|
display: block !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The triangle beside dialog */
|
||||||
|
yt-live-chat-text-message-renderer #message::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: ${this.form.showUserNames ? ((this.form.userNameLineHeight || this.form.userNameFontSize) + 10) : 20}px;
|
||||||
|
left: ${this.form.showAvatars ? (this.form.avatarSize + this.form.avatarSize / 4 - 8) : -8}px;
|
||||||
|
border: 8px solid transparent;
|
||||||
|
border-right: 18px solid;
|
||||||
|
transform: rotate(35deg);
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
timeStyle() {
|
||||||
|
return common.getTimeStyle(this.form)
|
||||||
|
},
|
||||||
|
backgroundStyle() {
|
||||||
|
return `/* Background colors */
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
${this.form.bgColor ? `background-color: ${this.form.bgColor};` : ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
${this.getBgStyleForAuthorType('', this.form.messageBgColor)}
|
||||||
|
|
||||||
|
${this.getBgStyleForAuthorType('owner', this.form.ownerMessageBgColor)}
|
||||||
|
|
||||||
|
${this.getBgStyleForAuthorType('moderator', this.form.moderatorMessageBgColor)}
|
||||||
|
|
||||||
|
${this.getBgStyleForAuthorType('member', this.form.memberMessageBgColor)}`
|
||||||
|
},
|
||||||
|
scAndNewMemberStyle() {
|
||||||
|
return `/* SuperChat/Fan Funding Messages */
|
||||||
|
yt-live-chat-paid-message-renderer {
|
||||||
|
margin: 4px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
${this.scAndNewMemberFontStyle}
|
||||||
|
|
||||||
|
yt-live-chat-membership-item-renderer #card,
|
||||||
|
yt-live-chat-membership-item-renderer #header {
|
||||||
|
${this.showNewMemberBgStyle}
|
||||||
|
}
|
||||||
|
|
||||||
|
${this.scTickerStyle}
|
||||||
|
|
||||||
|
${this.form.showOtherThings ? '' : `yt-live-chat-item-list-renderer {
|
||||||
|
display: none !important;
|
||||||
|
}`}`
|
||||||
|
},
|
||||||
|
scAndNewMemberFontStyle() {
|
||||||
|
return `yt-live-chat-paid-message-renderer #author-name,
|
||||||
|
yt-live-chat-paid-message-renderer #author-name *,
|
||||||
|
yt-live-chat-membership-item-renderer #header-content-inner-column,
|
||||||
|
yt-live-chat-membership-item-renderer #header-content-inner-column * {
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.firstLineFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.firstLineFontSize}px !important;
|
||||||
|
line-height: ${this.form.firstLineLineHeight || this.form.firstLineFontSize}px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-paid-message-renderer #purchase-amount,
|
||||||
|
yt-live-chat-paid-message-renderer #purchase-amount *,
|
||||||
|
yt-live-chat-membership-item-renderer #header-subtext,
|
||||||
|
yt-live-chat-membership-item-renderer #header-subtext * {
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.secondLineFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.secondLineFontSize}px !important;
|
||||||
|
line-height: ${this.form.secondLineLineHeight || this.form.secondLineFontSize}px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-paid-message-renderer #content,
|
||||||
|
yt-live-chat-paid-message-renderer #content * {
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.scContentFont)}"${common.FALLBACK_FONTS};
|
||||||
|
font-size: ${this.form.scContentFontSize}px !important;
|
||||||
|
line-height: ${this.form.scContentLineHeight || this.form.scContentFontSize}px !important;
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
showNewMemberBgStyle() {
|
||||||
|
return `background-color: ${this.form.memberUserNameColor} !important;
|
||||||
|
margin: 4px 0 !important;`
|
||||||
|
},
|
||||||
|
scTickerStyle() {
|
||||||
|
return `${this.form.showScTicker ? '' : `yt-live-chat-ticker-renderer {
|
||||||
|
display: none !important;
|
||||||
|
}`}
|
||||||
|
|
||||||
|
/* SuperChat Ticker */
|
||||||
|
yt-live-chat-ticker-paid-message-item-renderer,
|
||||||
|
yt-live-chat-ticker-paid-message-item-renderer *,
|
||||||
|
yt-live-chat-ticker-sponsor-item-renderer,
|
||||||
|
yt-live-chat-ticker-sponsor-item-renderer * {
|
||||||
|
font-family: "${common.cssEscapeStr(this.form.secondLineFont)}"${common.FALLBACK_FONTS};
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
animationStyle() {
|
||||||
|
return common.getAnimationStyle(this.form)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
result(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
this.saveConfig()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$emit('input', this.result)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveConfig: _.debounce(function() {
|
||||||
|
let config = mergeConfig(this.form, DEFAULT_CONFIG)
|
||||||
|
window.localStorage.stylegenLineLikeConfig = JSON.stringify(config)
|
||||||
|
}, 500),
|
||||||
|
loadConfig() {
|
||||||
|
try {
|
||||||
|
return mergeConfig(JSON.parse(window.localStorage.stylegenLineLikeConfig), DEFAULT_CONFIG)
|
||||||
|
} catch {
|
||||||
|
return {...DEFAULT_CONFIG}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetConfig() {
|
||||||
|
this.form = {...DEFAULT_CONFIG}
|
||||||
|
},
|
||||||
|
|
||||||
|
getBgStyleForAuthorType(authorType, color) {
|
||||||
|
if (!color) {
|
||||||
|
color = '#ffffff'
|
||||||
|
}
|
||||||
|
let typeSelector = authorType ? `[author-type="${authorType}"]` : ''
|
||||||
|
return `yt-live-chat-text-message-renderer${typeSelector} #message {
|
||||||
|
background-color: ${color} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer${typeSelector} #message::before {
|
||||||
|
border-right-color: ${color};
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
162
frontend/src/views/StyleGenerator/common.js
Normal file
162
frontend/src/views/StyleGenerator/common.js
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import * as fonts from './fonts'
|
||||||
|
|
||||||
|
export const FALLBACK_FONTS = ', "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "\\5FAE \\8F6F \\96C5 \\9ED1 ", SimHei, Arial, sans-serif'
|
||||||
|
|
||||||
|
export const COMMON_STYLE = `/* Transparent background */
|
||||||
|
yt-live-chat-renderer {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-ticker-renderer {
|
||||||
|
background-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-author-chip #author-name {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar */
|
||||||
|
yt-live-chat-item-list-renderer #items {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-item-list-renderer #item-scroller {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer #content,
|
||||||
|
yt-live-chat-membership-item-renderer #content {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide header and input */
|
||||||
|
yt-live-chat-header-renderer,
|
||||||
|
yt-live-chat-message-input-renderer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide unimportant messages */
|
||||||
|
yt-live-chat-text-message-renderer[is-deleted],
|
||||||
|
yt-live-chat-membership-item-renderer[is-deleted] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-mode-change-message-renderer,
|
||||||
|
yt-live-chat-viewer-engagement-message-renderer,
|
||||||
|
yt-live-chat-restricted-participation-renderer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer a,
|
||||||
|
yt-live-chat-membership-item-renderer a {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}`
|
||||||
|
|
||||||
|
export function getImportStyle (allFonts) {
|
||||||
|
let fontsNeedToImport = new Set()
|
||||||
|
for (let font of allFonts) {
|
||||||
|
if (fonts.NETWORK_FONTS.indexOf(font) !== -1) {
|
||||||
|
fontsNeedToImport.add(font)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let res = []
|
||||||
|
for (let font of fontsNeedToImport) {
|
||||||
|
res.push(`@import url("https://fonts.googleapis.com/css?family=${encodeURIComponent(font)}");`)
|
||||||
|
}
|
||||||
|
return res.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvatarStyle (config) {
|
||||||
|
return `/* Avatars */
|
||||||
|
yt-live-chat-text-message-renderer #author-photo,
|
||||||
|
yt-live-chat-text-message-renderer #author-photo img,
|
||||||
|
yt-live-chat-paid-message-renderer #author-photo,
|
||||||
|
yt-live-chat-paid-message-renderer #author-photo img,
|
||||||
|
yt-live-chat-membership-item-renderer #author-photo,
|
||||||
|
yt-live-chat-membership-item-renderer #author-photo img {
|
||||||
|
${config.showAvatars ? '' : 'display: none !important;'}
|
||||||
|
width: ${config.avatarSize}px !important;
|
||||||
|
height: ${config.avatarSize}px !important;
|
||||||
|
border-radius: ${config.avatarSize}px !important;
|
||||||
|
margin-right: ${config.avatarSize / 4}px !important;
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimeStyle (config) {
|
||||||
|
return `/* Timestamps */
|
||||||
|
yt-live-chat-text-message-renderer #timestamp {
|
||||||
|
display: ${config.showTime ? 'inline' : 'none'} !important;
|
||||||
|
${config.timeColor ? `color: ${config.timeColor} !important;` : ''}
|
||||||
|
font-family: "${cssEscapeStr(config.timeFont)}"${FALLBACK_FONTS};
|
||||||
|
font-size: ${config.timeFontSize}px !important;
|
||||||
|
line-height: ${config.timeLineHeight || config.timeFontSize}px !important;
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAnimationStyle (config) {
|
||||||
|
if (!config.animateIn && !config.animateOut) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
let totalTime = 0
|
||||||
|
if (config.animateIn) {
|
||||||
|
totalTime += config.fadeInTime
|
||||||
|
}
|
||||||
|
if (config.animateOut) {
|
||||||
|
totalTime += config.animateOutWaitTime * 1000
|
||||||
|
totalTime += config.fadeOutTime
|
||||||
|
}
|
||||||
|
let keyframes = []
|
||||||
|
let curTime = 0
|
||||||
|
if (config.animateIn) {
|
||||||
|
keyframes.push(` 0% { opacity: 0;${!config.slide ? ''
|
||||||
|
: ` transform: translateX(${config.reverseSlide ? 16 : -16}px);`
|
||||||
|
} }`)
|
||||||
|
curTime += config.fadeInTime
|
||||||
|
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none; }`)
|
||||||
|
}
|
||||||
|
if (config.animateOut) {
|
||||||
|
curTime += config.animateOutWaitTime * 1000
|
||||||
|
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none; }`)
|
||||||
|
curTime += config.fadeOutTime
|
||||||
|
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 0;${!config.slide ? ''
|
||||||
|
: ` transform: translateX(${config.reverseSlide ? -16 : 16}px);`
|
||||||
|
} }`)
|
||||||
|
}
|
||||||
|
return `/* Animation */
|
||||||
|
@keyframes anim {
|
||||||
|
${keyframes.join('\n')}
|
||||||
|
}
|
||||||
|
|
||||||
|
yt-live-chat-text-message-renderer,
|
||||||
|
yt-live-chat-membership-item-renderer,
|
||||||
|
yt-live-chat-paid-message-renderer {
|
||||||
|
animation: anim ${totalTime}ms;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cssEscapeStr (str) {
|
||||||
|
let res = []
|
||||||
|
for (let char of str) {
|
||||||
|
res.push(cssEscapeChar(char))
|
||||||
|
}
|
||||||
|
return res.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function cssEscapeChar (char) {
|
||||||
|
if (!needEscapeChar(char)) {
|
||||||
|
return char
|
||||||
|
}
|
||||||
|
let hexCode = char.codePointAt(0).toString(16)
|
||||||
|
// https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
|
||||||
|
return `\\${hexCode} `
|
||||||
|
}
|
||||||
|
|
||||||
|
function needEscapeChar (char) {
|
||||||
|
let code = char.codePointAt(0)
|
||||||
|
if (0x20 <= code && code <= 0x7E) {
|
||||||
|
return char === '"' || char === '\\'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
@ -1,199 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :sm="24" :md="16">
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane :label="$t('stylegen.legacy')" name="legacy">
|
||||||
|
<legacy ref="legacy" v-model="subComponentResults.legacy"></legacy>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane :label="$t('stylegen.lineLike')" name="lineLike">
|
||||||
|
<line-like ref="lineLike" v-model="subComponentResults.lineLike"></line-like>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
<el-form label-width="150px" size="mini">
|
<el-form label-width="150px" size="mini">
|
||||||
<h3>{{$t('stylegen.outlines')}}</h3>
|
|
||||||
<el-form-item :label="$t('stylegen.showOutlines')">
|
|
||||||
<el-switch v-model="form.showOutlines"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.outlineSize')">
|
|
||||||
<el-input v-model.number="form.outlineSize" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.outlineColor')">
|
|
||||||
<el-color-picker v-model="form.outlineColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h3>{{$t('stylegen.avatars')}}</h3>
|
|
||||||
<el-form-item :label="$t('stylegen.showAvatars')">
|
|
||||||
<el-switch v-model="form.showAvatars"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.avatarSize')">
|
|
||||||
<el-input v-model.number="form.avatarSize" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h3>{{$t('stylegen.userNames')}}</h3>
|
|
||||||
<el-form-item :label="$t('stylegen.showUserNames')">
|
|
||||||
<el-switch v-model="form.showUserNames"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.font')">
|
|
||||||
<el-autocomplete v-model="form.userNameFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.fontSize')">
|
|
||||||
<el-input v-model.number="form.userNameFontSize" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
|
||||||
<el-input v-model.number="form.userNameLineHeight" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.normalColor')">
|
|
||||||
<el-color-picker v-model="form.userNameColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.ownerColor')">
|
|
||||||
<el-color-picker v-model="form.ownerUserNameColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.moderatorColor')">
|
|
||||||
<el-color-picker v-model="form.moderatorUserNameColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.memberColor')">
|
|
||||||
<el-color-picker v-model="form.memberUserNameColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.showBadges')">
|
|
||||||
<el-switch v-model="form.showBadges"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.showColon')">
|
|
||||||
<el-switch v-model="form.showColon"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h3>{{$t('stylegen.messages')}}</h3>
|
|
||||||
<el-form-item :label="$t('stylegen.font')">
|
|
||||||
<el-autocomplete v-model="form.messageFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.fontSize')">
|
|
||||||
<el-input v-model.number="form.messageFontSize" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
|
||||||
<el-input v-model.number="form.messageLineHeight" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.color')">
|
|
||||||
<el-color-picker v-model="form.messageColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.onNewLine')">
|
|
||||||
<el-switch v-model="form.messageOnNewLine"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h3>{{$t('stylegen.time')}}</h3>
|
|
||||||
<el-form-item :label="$t('stylegen.showTime')">
|
|
||||||
<el-switch v-model="form.showTime"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.font')">
|
|
||||||
<el-autocomplete v-model="form.timeFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.fontSize')">
|
|
||||||
<el-input v-model.number="form.timeFontSize" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.lineHeight')">
|
|
||||||
<el-input v-model.number="form.timeLineHeight" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.color')">
|
|
||||||
<el-color-picker v-model="form.timeColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h3>{{$t('stylegen.backgrounds')}}</h3>
|
|
||||||
<el-form-item :label="$t('stylegen.bgColor')">
|
|
||||||
<el-color-picker v-model="form.bgColor" show-alpha></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.useBarsInsteadOfBg')">
|
|
||||||
<el-switch v-model="form.useBarsInsteadOfBg"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.messageBgColor')">
|
|
||||||
<el-color-picker v-model="form.messageBgColor" show-alpha></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.ownerMessageBgColor')">
|
|
||||||
<el-color-picker v-model="form.ownerMessageBgColor" show-alpha></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.moderatorMessageBgColor')">
|
|
||||||
<el-color-picker v-model="form.moderatorMessageBgColor" show-alpha></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.memberMessageBgColor')">
|
|
||||||
<el-color-picker v-model="form.memberMessageBgColor" show-alpha></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h3>{{$t('stylegen.scAndNewMember')}}</h3>
|
|
||||||
<el-form-item :label="$t('stylegen.firstLineFont')">
|
|
||||||
<el-autocomplete v-model="form.firstLineFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.firstLineFontSize')">
|
|
||||||
<el-input v-model.number="form.firstLineFontSize" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.firstLineLineHeight')">
|
|
||||||
<el-input v-model.number="form.firstLineLineHeight" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.firstLineColor')">
|
|
||||||
<el-color-picker v-model="form.firstLineColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.secondLineFont')">
|
|
||||||
<el-autocomplete v-model="form.secondLineFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.secondLineFontSize')">
|
|
||||||
<el-input v-model.number="form.secondLineFontSize" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.secondLineLineHeight')">
|
|
||||||
<el-input v-model.number="form.secondLineLineHeight" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.secondLineColor')">
|
|
||||||
<el-color-picker v-model="form.secondLineColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.scContentLineFont')">
|
|
||||||
<el-autocomplete v-model="form.scContentFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.scContentLineFontSize')">
|
|
||||||
<el-input v-model.number="form.scContentFontSize" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.scContentLineLineHeight')">
|
|
||||||
<el-input v-model.number="form.scContentLineHeight" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.scContentLineColor')">
|
|
||||||
<el-color-picker v-model="form.scContentColor"></el-color-picker>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.showNewMemberBg')">
|
|
||||||
<el-switch v-model="form.showNewMemberBg"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.showScTicker')">
|
|
||||||
<el-switch v-model="form.showScTicker"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.showOtherThings')">
|
|
||||||
<el-switch v-model="form.showOtherThings"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h3>{{$t('stylegen.animation')}}</h3>
|
|
||||||
<el-form-item :label="$t('stylegen.animateIn')">
|
|
||||||
<el-switch v-model="form.animateIn"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.fadeInTime')">
|
|
||||||
<el-input v-model.number="form.fadeInTime" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.animateOut')">
|
|
||||||
<el-switch v-model="form.animateOut"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.animateOutWaitTime')">
|
|
||||||
<el-input v-model.number="form.animateOutWaitTime" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.fadeOutTime')">
|
|
||||||
<el-input v-model.number="form.fadeOutTime" type="number" min="0"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.slide')">
|
|
||||||
<el-switch v-model="form.slide"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('stylegen.reverseSlide')">
|
|
||||||
<el-switch v-model="form.reverseSlide"></el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" @click="playAnimation">{{$t('stylegen.playAnimation')}}</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<h3>{{$t('stylegen.result')}}</h3>
|
<h3>{{$t('stylegen.result')}}</h3>
|
||||||
<el-form-item label="CSS">
|
<el-card shadow="never">
|
||||||
<el-input v-model="result" ref="result" type="textarea" :rows="20"></el-input>
|
<el-form-item label="CSS">
|
||||||
</el-form-item>
|
<el-input v-model="inputResult" ref="result" type="textarea" :rows="20"></el-input>
|
||||||
<el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" @click="copyResult">{{$t('stylegen.copy')}}</el-button>
|
<el-form-item>
|
||||||
<el-button @click="resetConfig">{{$t('stylegen.resetConfig')}}</el-button>
|
<el-button type="primary" @click="copyResult">{{$t('stylegen.copy')}}</el-button>
|
||||||
</el-form-item>
|
<el-button @click="resetConfig">{{$t('stylegen.resetConfig')}}</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-card>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
|
||||||
<div ref="exampleContainer" id="example-container">
|
<el-col :sm="24" :md="8">
|
||||||
<div id="fakebody">
|
<div :style="{position: 'relative', top: `${exampleTop}px`}">
|
||||||
<chat-renderer ref="renderer" :css="exampleCss"></chat-renderer>
|
<el-form inline style="line-height: 40px">
|
||||||
|
<el-form-item :label="$t('stylegen.playAnimation')" style="margin: 0">
|
||||||
|
<el-switch v-model="playAnimation" @change="onPlayAnimationChange"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('stylegen.backgrounds')" style="margin: 0 0 0 30px">
|
||||||
|
<el-switch v-model="exampleBgLight" :active-text="$t('stylegen.light')" :inactive-text="$t('stylegen.dark')"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div id="example-container" :class="{light: exampleBgLight}">
|
||||||
|
<div id="fakebody">
|
||||||
|
<room ref="room"></room>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -203,172 +47,100 @@
|
|||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
import * as stylegen from './stylegen'
|
import Legacy from './Legacy'
|
||||||
import * as fonts from './fonts'
|
import LineLike from './LineLike'
|
||||||
import ChatRenderer from '@/components/ChatRenderer'
|
import Room from '@/views/Room'
|
||||||
import * as constants from '@/components/ChatRenderer/constants'
|
|
||||||
|
|
||||||
let time = new Date()
|
|
||||||
let textMessageTemplate = {
|
|
||||||
id: 0,
|
|
||||||
addTime: time,
|
|
||||||
type: constants.MESSAGE_TYPE_TEXT,
|
|
||||||
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
|
|
||||||
time: time,
|
|
||||||
authorName: '',
|
|
||||||
authorType: constants.AUTHRO_TYPE_NORMAL,
|
|
||||||
content: '',
|
|
||||||
privilegeType: 0,
|
|
||||||
repeated: 1,
|
|
||||||
translation: ''
|
|
||||||
}
|
|
||||||
let membershipItemTemplate = {
|
|
||||||
id: 0,
|
|
||||||
addTime: time,
|
|
||||||
type: constants.MESSAGE_TYPE_MEMBER,
|
|
||||||
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
|
|
||||||
time: time,
|
|
||||||
authorName: '',
|
|
||||||
privilegeType: 3,
|
|
||||||
title: 'New member'
|
|
||||||
}
|
|
||||||
let paidMessageTemplate = {
|
|
||||||
id: 0,
|
|
||||||
addTime: time,
|
|
||||||
type: constants.MESSAGE_TYPE_SUPER_CHAT,
|
|
||||||
avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
|
|
||||||
authorName: '',
|
|
||||||
price: 0,
|
|
||||||
time: time,
|
|
||||||
content: '',
|
|
||||||
translation: ''
|
|
||||||
}
|
|
||||||
let nextId = 0
|
|
||||||
const EXAMPLE_MESSAGES = [
|
|
||||||
{
|
|
||||||
...textMessageTemplate,
|
|
||||||
id: (nextId++).toString(),
|
|
||||||
authorName: 'mob路人',
|
|
||||||
content: '8888888888',
|
|
||||||
repeated: 12
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...textMessageTemplate,
|
|
||||||
id: (nextId++).toString(),
|
|
||||||
authorName: 'member舰长',
|
|
||||||
authorType: constants.AUTHRO_TYPE_MEMBER,
|
|
||||||
content: '草',
|
|
||||||
privilegeType: 3,
|
|
||||||
repeated: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...textMessageTemplate,
|
|
||||||
id: (nextId++).toString(),
|
|
||||||
authorName: 'admin房管',
|
|
||||||
authorType: constants.AUTHRO_TYPE_ADMIN,
|
|
||||||
content: 'kksk'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...membershipItemTemplate,
|
|
||||||
id: (nextId++).toString(),
|
|
||||||
authorName: '艾米亚official'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...paidMessageTemplate,
|
|
||||||
id: (nextId++).toString(),
|
|
||||||
authorName: '愛里紗メイプル',
|
|
||||||
price: 66600,
|
|
||||||
content: 'Sent 小电视飞船x100'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...textMessageTemplate,
|
|
||||||
id: (nextId++).toString(),
|
|
||||||
authorName: 'streamer主播',
|
|
||||||
authorType: constants.AUTHRO_TYPE_OWNER,
|
|
||||||
content: '老板大气,老板身体健康'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...paidMessageTemplate,
|
|
||||||
id: (nextId++).toString(),
|
|
||||||
authorName: 'AstralisUP',
|
|
||||||
price: 30,
|
|
||||||
content: '言いたいことがあるんだよ!'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'StyleGenerator',
|
name: 'StyleGenerator',
|
||||||
components: {
|
components: {
|
||||||
ChatRenderer
|
Legacy, LineLike, Room
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
let stylegenConfig = stylegen.getLocalConfig()
|
let styleElement = document.createElement('style')
|
||||||
let result = stylegen.getStyle(stylegenConfig)
|
document.head.appendChild(styleElement)
|
||||||
|
// 数据流:
|
||||||
|
// 输入框 --\
|
||||||
|
// 子组件 -> subComponentResults -> subComponentResult -> inputResult -> 防抖延迟0.5s后 -> debounceResult -> exampleCss
|
||||||
return {
|
return {
|
||||||
FONTS: [...fonts.LOCAL_FONTS, ...fonts.NETWORK_FONTS],
|
// 子组件的结果
|
||||||
|
subComponentResults: {
|
||||||
|
legacy: '',
|
||||||
|
lineLike: ''
|
||||||
|
},
|
||||||
|
activeTab: 'legacy',
|
||||||
|
// 输入框的结果
|
||||||
|
inputResult: '',
|
||||||
|
// 防抖后延迟变化的结果
|
||||||
|
debounceResult: '',
|
||||||
|
|
||||||
form: {...stylegenConfig},
|
styleElement,
|
||||||
result,
|
exampleTop: 0,
|
||||||
exampleCss: result.replace(/^body\b/gm, '#fakebody'),
|
playAnimation: true,
|
||||||
|
exampleBgLight: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
computedResult() {
|
// 子组件的结果
|
||||||
return stylegen.getStyle(this.form)
|
subComponentResult() {
|
||||||
|
return this.subComponentResults[this.activeTab]
|
||||||
|
},
|
||||||
|
// 应用到预览上的CSS
|
||||||
|
exampleCss() {
|
||||||
|
return this.debounceResult.replace(/^body\b/gm, '#fakebody')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
computedResult: _.debounce(function(val) {
|
subComponentResult(val) {
|
||||||
this.result = val
|
this.inputResult = val
|
||||||
stylegen.setLocalConfig(this.form)
|
},
|
||||||
|
inputResult: _.debounce(function(val) {
|
||||||
|
this.debounceResult = val
|
||||||
}, 500),
|
}, 500),
|
||||||
result(val) {
|
exampleCss(val) {
|
||||||
this.exampleCss = val.replace(/^body\b/gm, '#fakebody')
|
this.styleElement.innerText = val
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$refs.renderer.addMessages(EXAMPLE_MESSAGES)
|
this.debounceResult = this.inputResult = this.subComponentResult
|
||||||
|
|
||||||
let observer = new MutationObserver(() => this.$refs.renderer.scrollToBottom())
|
this.$parent.$el.addEventListener('scroll', this.onParentScroll)
|
||||||
observer.observe(this.$refs.exampleContainer, {attributes: true})
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$parent.$el.removeEventListener('scroll', this.onParentScroll)
|
||||||
|
|
||||||
|
document.head.removeChild(this.styleElement)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getFontSuggestions(query, callback) {
|
onParentScroll(event) {
|
||||||
let res = this.FONTS.map(font => {return {value: font}})
|
if (document.body.clientWidth <= 992) {
|
||||||
if (query) {
|
this.exampleTop = 0
|
||||||
query = query.toLowerCase()
|
} else {
|
||||||
res = res.filter(
|
this.exampleTop = event.target.scrollTop
|
||||||
font => font.value.toLowerCase().indexOf(query) !== -1
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
callback(res)
|
|
||||||
},
|
},
|
||||||
playAnimation() {
|
onPlayAnimationChange(value) {
|
||||||
this.$refs.renderer.clearMessages()
|
if (value) {
|
||||||
this.$nextTick(() => this.$refs.renderer.addMessages(EXAMPLE_MESSAGES))
|
this.$refs.room.start()
|
||||||
|
} else {
|
||||||
|
this.$refs.room.stop()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
copyResult() {
|
copyResult() {
|
||||||
this.$refs.result.select()
|
this.$refs.result.select()
|
||||||
document.execCommand('Copy')
|
document.execCommand('Copy')
|
||||||
},
|
},
|
||||||
resetConfig() {
|
resetConfig() {
|
||||||
this.form = {...stylegen.DEFAULT_CONFIG}
|
this.$refs[this.activeTab].resetConfig()
|
||||||
|
this.inputResult = this.subComponentResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.el-form {
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#example-container {
|
#example-container {
|
||||||
position: fixed;
|
height: calc(100vh - 150px);
|
||||||
top: 30px;
|
|
||||||
left: calc(210px + 40px + (100vw - 210px - 40px) / 2);
|
|
||||||
width: calc((100vw - 210px - 40px) / 2 - 40px - 30px);
|
|
||||||
height: calc(100vh - 110px);
|
|
||||||
|
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
background-image:
|
background-image:
|
||||||
@ -382,11 +154,11 @@ export default {
|
|||||||
-webkit-gradient(linear, 0 100%, 100% 0, color-stop(.75, transparent), color-stop(.75, #333)),
|
-webkit-gradient(linear, 0 100%, 100% 0, color-stop(.75, transparent), color-stop(.75, #333)),
|
||||||
-webkit-gradient(linear, 0 0, 100% 100%, color-stop(.75, transparent), color-stop(.75, #333));
|
-webkit-gradient(linear, 0 0, 100% 100%, color-stop(.75, transparent), color-stop(.75, #333));
|
||||||
|
|
||||||
-moz-background-size:32px 32px;
|
-moz-background-size: 32px 32px;
|
||||||
background-size:32px 32px;
|
background-size: 32px 32px;
|
||||||
-webkit-background-size:32px 32px;
|
-webkit-background-size: 32px 32px;
|
||||||
|
|
||||||
background-position:0 0, 16px 0, 16px -16px, 0px 16px;
|
background-position: 0 0, 16px 0, 16px -16px, 0px 16px;
|
||||||
|
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
|
|
||||||
@ -394,8 +166,18 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-wrapper.mobile #example-container {
|
#example-container.light {
|
||||||
display: none;
|
background-color: #ddd;
|
||||||
|
background-image:
|
||||||
|
-moz-linear-gradient(45deg, #eee 25%, transparent 25%),
|
||||||
|
-moz-linear-gradient(-45deg, #eee 25%, transparent 25%),
|
||||||
|
-moz-linear-gradient(45deg, transparent 75%, #eee 75%),
|
||||||
|
-moz-linear-gradient(-45deg, transparent 75%, #eee 75%);
|
||||||
|
background-image:
|
||||||
|
-webkit-gradient(linear, 0 100%, 100% 0, color-stop(.25, #eee), color-stop(.25, transparent)),
|
||||||
|
-webkit-gradient(linear, 0 0, 100% 100%, color-stop(.25, #eee), color-stop(.25, transparent)),
|
||||||
|
-webkit-gradient(linear, 0 100%, 100% 0, color-stop(.75, transparent), color-stop(.75, #eee)),
|
||||||
|
-webkit-gradient(linear, 0 0, 100% 100%, color-stop(.75, transparent), color-stop(.75, #eee));
|
||||||
}
|
}
|
||||||
|
|
||||||
#fakebody {
|
#fakebody {
|
||||||
|
@ -1,430 +0,0 @@
|
|||||||
import {mergeConfig} from '@/utils'
|
|
||||||
import * as fonts from './fonts'
|
|
||||||
|
|
||||||
export const DEFAULT_CONFIG = {
|
|
||||||
showOutlines: true,
|
|
||||||
outlineSize: 2,
|
|
||||||
outlineColor: '#000000',
|
|
||||||
|
|
||||||
showAvatars: true,
|
|
||||||
avatarSize: 24,
|
|
||||||
|
|
||||||
showUserNames: true,
|
|
||||||
userNameFont: 'Changa One',
|
|
||||||
userNameFontSize: 20,
|
|
||||||
userNameLineHeight: 0,
|
|
||||||
userNameColor: '#cccccc',
|
|
||||||
ownerUserNameColor: '#ffd600',
|
|
||||||
moderatorUserNameColor: '#5e84f1',
|
|
||||||
memberUserNameColor: '#0f9d58',
|
|
||||||
showBadges: true,
|
|
||||||
showColon: true,
|
|
||||||
|
|
||||||
messageFont: 'Imprima',
|
|
||||||
messageFontSize: 18,
|
|
||||||
messageLineHeight: 0,
|
|
||||||
messageColor: '#ffffff',
|
|
||||||
messageOnNewLine: false,
|
|
||||||
|
|
||||||
showTime: false,
|
|
||||||
timeFont: 'Imprima',
|
|
||||||
timeFontSize: 16,
|
|
||||||
timeLineHeight: 0,
|
|
||||||
timeColor: '#999999',
|
|
||||||
|
|
||||||
bgColor: 'rgba(0, 0, 0, 0)',
|
|
||||||
useBarsInsteadOfBg: false,
|
|
||||||
messageBgColor: 'rgba(204, 204, 204, 0)',
|
|
||||||
ownerMessageBgColor: 'rgba(255, 214, 0, 0)',
|
|
||||||
moderatorMessageBgColor: 'rgba(94, 132, 241, 0)',
|
|
||||||
memberMessageBgColor: 'rgba(15, 157, 88, 0)',
|
|
||||||
|
|
||||||
firstLineFont: 'Changa One',
|
|
||||||
firstLineFontSize: 20,
|
|
||||||
firstLineLineHeight: 0,
|
|
||||||
firstLineColor: '#ffffff',
|
|
||||||
secondLineFont: 'Imprima',
|
|
||||||
secondLineFontSize: 18,
|
|
||||||
secondLineLineHeight: 0,
|
|
||||||
secondLineColor: '#ffffff',
|
|
||||||
scContentFont: 'Imprima',
|
|
||||||
scContentFontSize: 18,
|
|
||||||
scContentLineHeight: 0,
|
|
||||||
scContentColor: '#ffffff',
|
|
||||||
showNewMemberBg: true,
|
|
||||||
showScTicker: false,
|
|
||||||
showOtherThings: true,
|
|
||||||
|
|
||||||
animateIn: false,
|
|
||||||
fadeInTime: 200, // ms
|
|
||||||
animateOut: false,
|
|
||||||
animateOutWaitTime: 30, // s
|
|
||||||
fadeOutTime: 200, // ms
|
|
||||||
slide: false,
|
|
||||||
reverseSlide: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const FALLBACK_FONTS = ', "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "\\5FAE\\8F6F\\96C5\\9ED1", SimHei, Arial, sans-serif'
|
|
||||||
|
|
||||||
export function setLocalConfig (config) {
|
|
||||||
config = mergeConfig(config, DEFAULT_CONFIG)
|
|
||||||
window.localStorage.stylegenConfig = JSON.stringify(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLocalConfig () {
|
|
||||||
if (!window.localStorage.stylegenConfig) {
|
|
||||||
return DEFAULT_CONFIG
|
|
||||||
}
|
|
||||||
return mergeConfig(JSON.parse(window.localStorage.stylegenConfig), DEFAULT_CONFIG)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStyle (config) {
|
|
||||||
config = mergeConfig(config, DEFAULT_CONFIG)
|
|
||||||
return `${getImports(config)}
|
|
||||||
|
|
||||||
/* Background colors */
|
|
||||||
body {
|
|
||||||
overflow: hidden;
|
|
||||||
${config.bgColor ? `background-color: ${config.bgColor};` : ''}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transparent background. */
|
|
||||||
yt-live-chat-renderer {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
${getMessageColorStyle('', config.messageBgColor, config.useBarsInsteadOfBg)}
|
|
||||||
|
|
||||||
${getMessageColorStyle('owner', config.ownerMessageBgColor, config.useBarsInsteadOfBg)}
|
|
||||||
|
|
||||||
${getMessageColorStyle('moderator', config.moderatorMessageBgColor, config.useBarsInsteadOfBg)}
|
|
||||||
|
|
||||||
${getMessageColorStyle('member', config.memberMessageBgColor, config.useBarsInsteadOfBg)}
|
|
||||||
|
|
||||||
yt-live-chat-author-chip #author-name {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Outlines */
|
|
||||||
yt-live-chat-renderer * {
|
|
||||||
${getShowOutlinesStyle(config)}
|
|
||||||
font-family: "${cssEscapeStr(config.messageFont)}"${FALLBACK_FONTS};
|
|
||||||
font-size: ${config.messageFontSize}px !important;
|
|
||||||
line-height: ${config.messageLineHeight || config.messageFontSize}px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-text-message-renderer #content,
|
|
||||||
yt-live-chat-membership-item-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 {
|
|
||||||
${getPaddingStyle(config)}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Avatars. */
|
|
||||||
yt-live-chat-text-message-renderer #author-photo,
|
|
||||||
yt-live-chat-text-message-renderer #author-photo img,
|
|
||||||
yt-live-chat-paid-message-renderer #author-photo,
|
|
||||||
yt-live-chat-paid-message-renderer #author-photo img,
|
|
||||||
yt-live-chat-membership-item-renderer #author-photo,
|
|
||||||
yt-live-chat-membership-item-renderer #author-photo img {
|
|
||||||
${config.showAvatars ? '' : 'display: none !important;'}
|
|
||||||
width: ${config.avatarSize}px !important;
|
|
||||||
height: ${config.avatarSize}px !important;
|
|
||||||
border-radius: ${config.avatarSize}px !important;
|
|
||||||
margin-right: ${config.avatarSize / 4}px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide badges. */
|
|
||||||
yt-live-chat-text-message-renderer #chat-badges {
|
|
||||||
${config.showBadges ? '' : 'display: none !important;'}
|
|
||||||
vertical-align: text-top !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Timestamps. */
|
|
||||||
yt-live-chat-text-message-renderer #timestamp {
|
|
||||||
display: ${config.showTime ? 'inline' : 'none'} !important;
|
|
||||||
${config.timeColor ? `color: ${config.timeColor} !important;` : ''}
|
|
||||||
font-family: "${cssEscapeStr(config.timeFont)}"${FALLBACK_FONTS};
|
|
||||||
font-size: ${config.timeFontSize}px !important;
|
|
||||||
line-height: ${config.timeLineHeight || config.timeFontSize}px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Badges. */
|
|
||||||
yt-live-chat-text-message-renderer #author-name[type="owner"],
|
|
||||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="owner"] {
|
|
||||||
${config.ownerUserNameColor ? `color: ${config.ownerUserNameColor} !important;` : ''}
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-text-message-renderer #author-name[type="moderator"],
|
|
||||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="moderator"] {
|
|
||||||
${config.moderatorUserNameColor ? `color: ${config.moderatorUserNameColor} !important;` : ''}
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-text-message-renderer #author-name[type="member"],
|
|
||||||
yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="member"] {
|
|
||||||
${config.memberUserNameColor ? `color: ${config.memberUserNameColor} !important;` : ''}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Channel names. */
|
|
||||||
yt-live-chat-text-message-renderer #author-name {
|
|
||||||
${config.showUserNames ? '' : 'display: none !important;'}
|
|
||||||
${config.userNameColor ? `color: ${config.userNameColor} !important;` : ''}
|
|
||||||
font-family: "${cssEscapeStr(config.userNameFont)}"${FALLBACK_FONTS};
|
|
||||||
font-size: ${config.userNameFontSize}px !important;
|
|
||||||
line-height: ${config.userNameLineHeight || config.userNameFontSize}px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
${getShowColonStyle(config)}
|
|
||||||
|
|
||||||
/* Messages. */
|
|
||||||
yt-live-chat-text-message-renderer #message,
|
|
||||||
yt-live-chat-text-message-renderer #message * {
|
|
||||||
${config.messageColor ? `color: ${config.messageColor} !important;` : ''}
|
|
||||||
font-family: "${cssEscapeStr(config.messageFont)}"${FALLBACK_FONTS};
|
|
||||||
font-size: ${config.messageFontSize}px !important;
|
|
||||||
line-height: ${config.messageLineHeight || config.messageFontSize}px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
${!config.messageOnNewLine ? '' : `yt-live-chat-text-message-renderer #message {
|
|
||||||
display: block !important;
|
|
||||||
}`}
|
|
||||||
|
|
||||||
/* SuperChat/Fan Funding Messages. */
|
|
||||||
yt-live-chat-paid-message-renderer #author-name,
|
|
||||||
yt-live-chat-paid-message-renderer #author-name *,
|
|
||||||
yt-live-chat-membership-item-renderer #header-content-inner-column,
|
|
||||||
yt-live-chat-membership-item-renderer #header-content-inner-column * {
|
|
||||||
${config.firstLineColor ? `color: ${config.firstLineColor} !important;` : ''}
|
|
||||||
font-family: "${cssEscapeStr(config.firstLineFont)}"${FALLBACK_FONTS};
|
|
||||||
font-size: ${config.firstLineFontSize}px !important;
|
|
||||||
line-height: ${config.firstLineLineHeight || config.firstLineFontSize}px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-paid-message-renderer #purchase-amount,
|
|
||||||
yt-live-chat-paid-message-renderer #purchase-amount *,
|
|
||||||
yt-live-chat-membership-item-renderer #header-subtext,
|
|
||||||
yt-live-chat-membership-item-renderer #header-subtext * {
|
|
||||||
${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
|
|
||||||
font-family: "${cssEscapeStr(config.secondLineFont)}"${FALLBACK_FONTS};
|
|
||||||
font-size: ${config.secondLineFontSize}px !important;
|
|
||||||
line-height: ${config.secondLineLineHeight || config.secondLineFontSize}px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-paid-message-renderer #content,
|
|
||||||
yt-live-chat-paid-message-renderer #content * {
|
|
||||||
${config.scContentColor ? `color: ${config.scContentColor} !important;` : ''}
|
|
||||||
font-family: "${cssEscapeStr(config.scContentFont)}"${FALLBACK_FONTS};
|
|
||||||
font-size: ${config.scContentFontSize}px !important;
|
|
||||||
line-height: ${config.scContentLineHeight || config.scContentFontSize}px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-paid-message-renderer {
|
|
||||||
margin: 4px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-membership-item-renderer #card,
|
|
||||||
yt-live-chat-membership-item-renderer #header {
|
|
||||||
${getShowNewMemberBgStyle(config)}
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-text-message-renderer a,
|
|
||||||
yt-live-chat-membership-item-renderer a {
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-text-message-renderer[is-deleted],
|
|
||||||
yt-live-chat-membership-item-renderer[is-deleted] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-ticker-renderer {
|
|
||||||
background-color: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
${config.showScTicker ? '' : `yt-live-chat-ticker-renderer {
|
|
||||||
display: none !important;
|
|
||||||
}`}
|
|
||||||
|
|
||||||
${config.showOtherThings ? '' : `yt-live-chat-item-list-renderer {
|
|
||||||
display: none !important;
|
|
||||||
}`}
|
|
||||||
|
|
||||||
yt-live-chat-ticker-paid-message-item-renderer,
|
|
||||||
yt-live-chat-ticker-paid-message-item-renderer *,
|
|
||||||
yt-live-chat-ticker-sponsor-item-renderer,
|
|
||||||
yt-live-chat-ticker-sponsor-item-renderer * {
|
|
||||||
${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
|
|
||||||
font-family: "${cssEscapeStr(config.secondLineFont)}"${FALLBACK_FONTS};
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-mode-change-message-renderer,
|
|
||||||
yt-live-chat-viewer-engagement-message-renderer,
|
|
||||||
yt-live-chat-restricted-participation-renderer {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
${getAnimationStyle(config)}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImports (config) {
|
|
||||||
let fontsNeedToImport = new Set()
|
|
||||||
for (let name of ['userNameFont', 'messageFont', 'timeFont', 'firstLineFont', 'secondLineFont', 'scContentFont']) {
|
|
||||||
let font = config[name]
|
|
||||||
if (fonts.NETWORK_FONTS.indexOf(font) !== -1) {
|
|
||||||
fontsNeedToImport.add(font)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let res = []
|
|
||||||
for (let font of fontsNeedToImport) {
|
|
||||||
res.push(`@import url("https://fonts.googleapis.com/css?family=${encodeURIComponent(font)}");`)
|
|
||||||
}
|
|
||||||
return res.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMessageColorStyle (authorType, color, useBarsInsteadOfBg) {
|
|
||||||
let typeSelector = authorType ? `[author-type="${authorType}"]` : ''
|
|
||||||
if (!useBarsInsteadOfBg) {
|
|
||||||
return `yt-live-chat-text-message-renderer${typeSelector},
|
|
||||||
yt-live-chat-text-message-renderer${typeSelector}[is-highlighted] {
|
|
||||||
${color ? `background-color: ${color} !important;` : ''}
|
|
||||||
}`
|
|
||||||
} else {
|
|
||||||
return `yt-live-chat-text-message-renderer${typeSelector}::after {
|
|
||||||
${color ? `border: 2px solid ${color};` : ''}
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
left: 8px;
|
|
||||||
top: 4px;
|
|
||||||
bottom: 4px;
|
|
||||||
width: 1px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 2px;
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getShowOutlinesStyle (config) {
|
|
||||||
if (!config.showOutlines || !config.outlineSize) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
let shadow = []
|
|
||||||
for (var x = -config.outlineSize; x <= config.outlineSize; x += Math.ceil(config.outlineSize / 4)) {
|
|
||||||
for (var y = -config.outlineSize; y <= config.outlineSize; y += Math.ceil(config.outlineSize / 4)) {
|
|
||||||
shadow.push(`${x}px ${y}px ${config.outlineColor}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `text-shadow: ${shadow.join(', ')};`
|
|
||||||
}
|
|
||||||
|
|
||||||
function cssEscapeStr (str) {
|
|
||||||
let res = []
|
|
||||||
for (let char of str) {
|
|
||||||
res.push(cssEscapeChar(char))
|
|
||||||
}
|
|
||||||
return res.join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function cssEscapeChar (char) {
|
|
||||||
if (!needEscapeChar(char)) {
|
|
||||||
return char
|
|
||||||
}
|
|
||||||
let hexCode = char.codePointAt(0).toString(16)
|
|
||||||
// https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
|
|
||||||
return `\\${hexCode} `
|
|
||||||
}
|
|
||||||
|
|
||||||
function needEscapeChar (char) {
|
|
||||||
let code = char.codePointAt(0)
|
|
||||||
if (0x20 <= code && code <= 0x7E) {
|
|
||||||
return char === '"'
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPaddingStyle (config) {
|
|
||||||
return `padding-left: ${config.useBarsInsteadOfBg ? 20 : 4}px !important;
|
|
||||||
padding-right: 4px !important;`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getShowColonStyle (config) {
|
|
||||||
if (!config.showColon) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return `yt-live-chat-text-message-renderer #author-name::after {
|
|
||||||
content: ":";
|
|
||||||
margin-left: ${config.outlineSize}px;
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getShowNewMemberBgStyle (config) {
|
|
||||||
if (config.showNewMemberBg) {
|
|
||||||
return `background-color: ${config.memberUserNameColor} !important;
|
|
||||||
margin: 4px 0 !important;`
|
|
||||||
} else {
|
|
||||||
return `background-color: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
margin: 0 !important;`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAnimationStyle (config) {
|
|
||||||
if (!config.animateIn && !config.animateOut) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
let totalTime = 0
|
|
||||||
if (config.animateIn) {
|
|
||||||
totalTime += config.fadeInTime
|
|
||||||
}
|
|
||||||
if (config.animateOut) {
|
|
||||||
totalTime += config.animateOutWaitTime * 1000
|
|
||||||
totalTime += config.fadeOutTime
|
|
||||||
}
|
|
||||||
let keyframes = []
|
|
||||||
let curTime = 0
|
|
||||||
if (config.animateIn) {
|
|
||||||
keyframes.push(` 0% { opacity: 0;${!config.slide ? ''
|
|
||||||
: ` transform: translateX(${config.reverseSlide ? 16 : -16}px);`
|
|
||||||
} }`)
|
|
||||||
curTime += config.fadeInTime
|
|
||||||
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none; }`)
|
|
||||||
}
|
|
||||||
if (config.animateOut) {
|
|
||||||
curTime += config.animateOutWaitTime * 1000
|
|
||||||
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 1; transform: none; }`)
|
|
||||||
curTime += config.fadeOutTime
|
|
||||||
keyframes.push(` ${(curTime / totalTime) * 100}% { opacity: 0;${!config.slide ? ''
|
|
||||||
: ` transform: translateX(${config.reverseSlide ? -16 : 16}px);`
|
|
||||||
} }`)
|
|
||||||
}
|
|
||||||
return `@keyframes anim {
|
|
||||||
${keyframes.join('\n')}
|
|
||||||
}
|
|
||||||
|
|
||||||
yt-live-chat-text-message-renderer,
|
|
||||||
yt-live-chat-membership-item-renderer,
|
|
||||||
yt-live-chat-paid-message-renderer {
|
|
||||||
animation: anim ${totalTime}ms;
|
|
||||||
animation-fill-mode: both;
|
|
||||||
}`
|
|
||||||
}
|
|
@ -10,6 +10,7 @@ import aiohttp
|
|||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
import sqlalchemy.exc
|
import sqlalchemy.exc
|
||||||
|
|
||||||
|
import config
|
||||||
import models.database
|
import models.database
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -18,18 +19,21 @@ logger = logging.getLogger(__name__)
|
|||||||
DEFAULT_AVATAR_URL = '//static.hdslb.com/images/member/noface.gif'
|
DEFAULT_AVATAR_URL = '//static.hdslb.com/images/member/noface.gif'
|
||||||
|
|
||||||
_main_event_loop = asyncio.get_event_loop()
|
_main_event_loop = asyncio.get_event_loop()
|
||||||
_http_session = aiohttp.ClientSession()
|
_http_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
|
||||||
# user_id -> avatar_url
|
# user_id -> avatar_url
|
||||||
_avatar_url_cache: Dict[int, str] = {}
|
_avatar_url_cache: Dict[int, str] = {}
|
||||||
# 正在获取头像的Future,user_id -> Future
|
# 正在获取头像的Future,user_id -> Future
|
||||||
_uid_fetch_future_map: Dict[int, asyncio.Future] = {}
|
_uid_fetch_future_map: Dict[int, asyncio.Future] = {}
|
||||||
# 正在获取头像的user_id队列
|
# 正在获取头像的user_id队列
|
||||||
_uid_queue_to_fetch = asyncio.Queue(15)
|
_uid_queue_to_fetch = None
|
||||||
# 上次被B站ban时间
|
# 上次被B站ban时间
|
||||||
_last_fetch_banned_time: Optional[datetime.datetime] = None
|
_last_fetch_banned_time: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
|
|
||||||
def init():
|
def init():
|
||||||
|
cfg = config.get_config()
|
||||||
|
global _uid_queue_to_fetch
|
||||||
|
_uid_queue_to_fetch = asyncio.Queue(cfg.fetch_avatar_max_queue_size)
|
||||||
asyncio.ensure_future(_get_avatar_url_from_web_consumer())
|
asyncio.ensure_future(_get_avatar_url_from_web_consumer())
|
||||||
|
|
||||||
|
|
||||||
@ -124,7 +128,8 @@ async def _get_avatar_url_from_web_consumer():
|
|||||||
asyncio.ensure_future(_get_avatar_url_from_web_coroutine(user_id, future))
|
asyncio.ensure_future(_get_avatar_url_from_web_coroutine(user_id, future))
|
||||||
|
|
||||||
# 限制频率,防止被B站ban
|
# 限制频率,防止被B站ban
|
||||||
await asyncio.sleep(0.2)
|
cfg = config.get_config()
|
||||||
|
await asyncio.sleep(cfg.fetch_avatar_interval)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('_get_avatar_url_from_web_consumer error:')
|
logger.exception('_get_avatar_url_from_web_consumer error:')
|
||||||
|
|
||||||
@ -178,7 +183,8 @@ def update_avatar_cache(user_id, avatar_url):
|
|||||||
|
|
||||||
def _update_avatar_cache_in_memory(user_id, avatar_url):
|
def _update_avatar_cache_in_memory(user_id, avatar_url):
|
||||||
_avatar_url_cache[user_id] = avatar_url
|
_avatar_url_cache[user_id] = avatar_url
|
||||||
while len(_avatar_url_cache) > 50000:
|
cfg = config.get_config()
|
||||||
|
while len(_avatar_url_cache) > cfg.avatar_cache_size:
|
||||||
_avatar_url_cache.pop(next(iter(_avatar_url_cache)), None)
|
_avatar_url_cache.pop(next(iter(_avatar_url_cache)), None)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
|
|
||||||
import yarl
|
|
||||||
from typing import *
|
from typing import *
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
NO_TRANSLATE_TEXTS = {
|
NO_TRANSLATE_TEXTS = {
|
||||||
@ -21,7 +23,7 @@ NO_TRANSLATE_TEXTS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_main_event_loop = asyncio.get_event_loop()
|
_main_event_loop = asyncio.get_event_loop()
|
||||||
_http_session = aiohttp.ClientSession()
|
_http_session = None
|
||||||
_translate_providers: List['TranslateProvider'] = []
|
_translate_providers: List['TranslateProvider'] = []
|
||||||
# text -> res
|
# text -> res
|
||||||
_translate_cache: Dict[str, str] = {}
|
_translate_cache: Dict[str, str] = {}
|
||||||
@ -34,17 +36,45 @@ def init():
|
|||||||
|
|
||||||
|
|
||||||
async def _do_init():
|
async def _do_init():
|
||||||
# 考虑优先级
|
global _http_session
|
||||||
providers = [
|
_http_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
|
||||||
TencentTranslate(),
|
|
||||||
YoudaoTranslate(),
|
cfg = config.get_config()
|
||||||
BilibiliTranslate()
|
if not cfg.enable_translate:
|
||||||
]
|
return
|
||||||
|
providers = []
|
||||||
|
for trans_cfg in cfg.translator_configs:
|
||||||
|
provider = create_translate_provider(trans_cfg)
|
||||||
|
if provider is not None:
|
||||||
|
providers.append(provider)
|
||||||
await asyncio.gather(*(provider.init() for provider in providers))
|
await asyncio.gather(*(provider.init() for provider in providers))
|
||||||
global _translate_providers
|
global _translate_providers
|
||||||
_translate_providers = providers
|
_translate_providers = providers
|
||||||
|
|
||||||
|
|
||||||
|
def create_translate_provider(cfg):
|
||||||
|
type_ = cfg['type']
|
||||||
|
if type_ == 'TencentTranslateFree':
|
||||||
|
return TencentTranslateFree(
|
||||||
|
cfg['query_interval'], cfg['max_queue_size'], cfg['source_language'],
|
||||||
|
cfg['target_language']
|
||||||
|
)
|
||||||
|
elif type_ == 'BilibiliTranslateFree':
|
||||||
|
return BilibiliTranslateFree(cfg['query_interval'], cfg['max_queue_size'])
|
||||||
|
elif type_ == 'TencentTranslate':
|
||||||
|
return TencentTranslate(
|
||||||
|
cfg['query_interval'], cfg['max_queue_size'], cfg['source_language'],
|
||||||
|
cfg['target_language'], cfg['secret_id'], cfg['secret_key'],
|
||||||
|
cfg['region']
|
||||||
|
)
|
||||||
|
elif type_ == 'BaiduTranslate':
|
||||||
|
return BaiduTranslate(
|
||||||
|
cfg['query_interval'], cfg['max_queue_size'], cfg['source_language'],
|
||||||
|
cfg['target_language'], cfg['app_id'], cfg['secret']
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def need_translate(text):
|
def need_translate(text):
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
# 没有中文,平时打不出的字不管
|
# 没有中文,平时打不出的字不管
|
||||||
@ -54,7 +84,7 @@ def need_translate(text):
|
|||||||
if any(0x3040 <= ord(c) <= 0x30FF for c in text):
|
if any(0x3040 <= ord(c) <= 0x30FF for c in text):
|
||||||
return False
|
return False
|
||||||
# 弹幕同传
|
# 弹幕同传
|
||||||
if text.startswith('【'):
|
if '【' in text:
|
||||||
return False
|
return False
|
||||||
# 中日双语
|
# 中日双语
|
||||||
if text in NO_TRANSLATE_TEXTS:
|
if text in NO_TRANSLATE_TEXTS:
|
||||||
@ -82,14 +112,25 @@ def translate(text) -> Awaitable[Optional[str]]:
|
|||||||
future.set_result(res)
|
future.set_result(res)
|
||||||
return future
|
return future
|
||||||
|
|
||||||
|
# 负载均衡,找等待时间最少的provider
|
||||||
|
min_wait_time = None
|
||||||
|
min_wait_time_provider = None
|
||||||
for provider in _translate_providers:
|
for provider in _translate_providers:
|
||||||
if provider.is_available:
|
if not provider.is_available:
|
||||||
_text_future_map[key] = future
|
continue
|
||||||
future.add_done_callback(functools.partial(_on_translate_done, key))
|
wait_time = provider.wait_time
|
||||||
provider.translate(text, future)
|
if min_wait_time is None or wait_time < min_wait_time:
|
||||||
return future
|
min_wait_time = wait_time
|
||||||
|
min_wait_time_provider = provider
|
||||||
|
|
||||||
future.set_result(None)
|
# 没有可用的
|
||||||
|
if min_wait_time_provider is None:
|
||||||
|
future.set_result(None)
|
||||||
|
return future
|
||||||
|
|
||||||
|
_text_future_map[key] = future
|
||||||
|
future.add_done_callback(functools.partial(_on_translate_done, key))
|
||||||
|
min_wait_time_provider.translate(text, future)
|
||||||
return future
|
return future
|
||||||
|
|
||||||
|
|
||||||
@ -103,7 +144,8 @@ def _on_translate_done(key, future):
|
|||||||
if res is None:
|
if res is None:
|
||||||
return
|
return
|
||||||
_translate_cache[key] = res
|
_translate_cache[key] = res
|
||||||
while len(_translate_cache) > 50000:
|
cfg = config.get_config()
|
||||||
|
while len(_translate_cache) > cfg.translation_cache_size:
|
||||||
_translate_cache.pop(next(iter(_translate_cache)), None)
|
_translate_cache.pop(next(iter(_translate_cache)), None)
|
||||||
|
|
||||||
|
|
||||||
@ -115,55 +157,111 @@ class TranslateProvider:
|
|||||||
def is_available(self):
|
def is_available(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wait_time(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
def translate(self, text, future):
|
def translate(self, text, future):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class TencentTranslate(TranslateProvider):
|
class FlowControlTranslateProvider(TranslateProvider):
|
||||||
def __init__(self):
|
def __init__(self, query_interval, max_queue_size):
|
||||||
# 过期时间1小时
|
self._query_interval = query_interval
|
||||||
|
# (text, future)
|
||||||
|
self._text_queue = asyncio.Queue(max_queue_size)
|
||||||
|
|
||||||
|
async def init(self):
|
||||||
|
asyncio.ensure_future(self._translate_consumer())
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_available(self):
|
||||||
|
return not self._text_queue.full()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wait_time(self):
|
||||||
|
return self._text_queue.qsize() * self._query_interval
|
||||||
|
|
||||||
|
def translate(self, text, future):
|
||||||
|
try:
|
||||||
|
self._text_queue.put_nowait((text, future))
|
||||||
|
except asyncio.QueueFull:
|
||||||
|
future.set_result(None)
|
||||||
|
|
||||||
|
async def _translate_consumer(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
text, future = await self._text_queue.get()
|
||||||
|
asyncio.ensure_future(self._translate_coroutine(text, future))
|
||||||
|
# 频率限制
|
||||||
|
await asyncio.sleep(self._query_interval)
|
||||||
|
except Exception:
|
||||||
|
logger.exception('FlowControlTranslateProvider error:')
|
||||||
|
|
||||||
|
async def _translate_coroutine(self, text, future):
|
||||||
|
try:
|
||||||
|
res = await self._do_translate(text)
|
||||||
|
except BaseException as e:
|
||||||
|
future.set_exception(e)
|
||||||
|
else:
|
||||||
|
future.set_result(res)
|
||||||
|
|
||||||
|
async def _do_translate(self, text):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class TencentTranslateFree(FlowControlTranslateProvider):
|
||||||
|
def __init__(self, query_interval, max_queue_size, source_language, target_language):
|
||||||
|
super().__init__(query_interval, max_queue_size)
|
||||||
|
self._source_language = source_language
|
||||||
|
self._target_language = target_language
|
||||||
|
|
||||||
self._qtv = ''
|
self._qtv = ''
|
||||||
self._qtk = ''
|
self._qtk = ''
|
||||||
self._reinit_future = None
|
self._reinit_future = None
|
||||||
# 连续失败的次数
|
# 连续失败的次数
|
||||||
self._fail_count = 0
|
self._fail_count = 0
|
||||||
self._cool_down_future = None
|
|
||||||
|
|
||||||
async def init(self):
|
async def init(self):
|
||||||
|
if not await super().init():
|
||||||
|
return False
|
||||||
|
if not await self._do_init():
|
||||||
|
return False
|
||||||
self._reinit_future = asyncio.ensure_future(self._reinit_coroutine())
|
self._reinit_future = asyncio.ensure_future(self._reinit_coroutine())
|
||||||
return await self._do_init()
|
return True
|
||||||
|
|
||||||
async def _do_init(self):
|
async def _do_init(self):
|
||||||
try:
|
try:
|
||||||
async with _http_session.get('https://fanyi.qq.com/') as r:
|
async with _http_session.get('https://fanyi.qq.com/') as r:
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
logger.warning('TencentTranslate init request failed: status=%d %s', r.status, r.reason)
|
logger.warning('TencentTranslateFree init request failed: status=%d %s', r.status, r.reason)
|
||||||
return False
|
return False
|
||||||
html = await r.text()
|
html = await r.text()
|
||||||
|
|
||||||
m = re.search(r"""\breauthuri\s*=\s*['"](.+?)['"]""", html)
|
m = re.search(r"""\breauthuri\s*=\s*['"](.+?)['"]""", html)
|
||||||
if m is None:
|
if m is None:
|
||||||
logger.exception('TencentTranslate init failed: reauthuri not found')
|
logger.exception('TencentTranslateFree init failed: reauthuri not found')
|
||||||
return False
|
return False
|
||||||
reauthuri = m[1]
|
reauthuri = m[1]
|
||||||
|
|
||||||
async with _http_session.post('https://fanyi.qq.com/api/' + reauthuri) as r:
|
async with _http_session.post('https://fanyi.qq.com/api/' + reauthuri) as r:
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
logger.warning('TencentTranslate init request failed: reauthuri=%s, status=%d %s',
|
logger.warning('TencentTranslateFree init request failed: reauthuri=%s, status=%d %s',
|
||||||
reauthuri, r.status, r.reason)
|
reauthuri, r.status, r.reason)
|
||||||
return False
|
return False
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||||
logger.exception('TencentTranslate init error:')
|
logger.exception('TencentTranslateFree init error:')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
qtv = data.get('qtv', None)
|
qtv = data.get('qtv', None)
|
||||||
if qtv is None:
|
if qtv is None:
|
||||||
logger.warning('TencentTranslate init failed: qtv not found')
|
logger.warning('TencentTranslateFree init failed: qtv not found')
|
||||||
return False
|
return False
|
||||||
qtk = data.get('qtk', None)
|
qtk = data.get('qtk', None)
|
||||||
if qtk is None:
|
if qtk is None:
|
||||||
logger.warning('TencentTranslate init failed: qtk not found')
|
logger.warning('TencentTranslateFree init failed: qtk not found')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._qtv = qtv
|
self._qtv = qtv
|
||||||
@ -174,23 +272,14 @@ class TencentTranslate(TranslateProvider):
|
|||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(30)
|
await asyncio.sleep(30)
|
||||||
while True:
|
logger.debug('TencentTranslateFree reinit')
|
||||||
logger.debug('TencentTranslate reinit')
|
asyncio.ensure_future(self._do_init())
|
||||||
try:
|
|
||||||
if await self._do_init():
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
logger.exception('TencentTranslate init error:')
|
|
||||||
await asyncio.sleep(3 * 60)
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_available(self):
|
def is_available(self):
|
||||||
return self._qtv != '' and self._qtk != ''
|
return self._qtv != '' and self._qtk != '' and super().is_available
|
||||||
|
|
||||||
def translate(self, text, future):
|
|
||||||
asyncio.ensure_future(self._translate_coroutine(text, future))
|
|
||||||
|
|
||||||
async def _translate_coroutine(self, text, future):
|
async def _translate_coroutine(self, text, future):
|
||||||
try:
|
try:
|
||||||
@ -213,202 +302,47 @@ class TencentTranslate(TranslateProvider):
|
|||||||
'Referer': 'https://fanyi.qq.com/'
|
'Referer': 'https://fanyi.qq.com/'
|
||||||
},
|
},
|
||||||
data={
|
data={
|
||||||
'source': 'zh',
|
'source': self._source_language,
|
||||||
'target': 'jp',
|
'target': self._target_language,
|
||||||
'sourceText': text,
|
'sourceText': text,
|
||||||
'qtv': self._qtv,
|
'qtv': self._qtv,
|
||||||
'qtk': self._qtk
|
'qtk': self._qtk
|
||||||
}
|
}
|
||||||
) as r:
|
) as r:
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
logger.warning('TencentTranslate request failed: status=%d %s', r.status, r.reason)
|
logger.warning('TencentTranslateFree request failed: status=%d %s', r.status, r.reason)
|
||||||
return None
|
return None
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||||
return None
|
return None
|
||||||
if data['errCode'] != 0:
|
if data['errCode'] != 0:
|
||||||
logger.warning('TencentTranslate failed: %d %s', data['errCode'], data['errMsg'])
|
logger.warning('TencentTranslateFree failed: %d %s', data['errCode'], data['errMsg'])
|
||||||
return None
|
return None
|
||||||
res = ''.join(record['targetText'] for record in data['translate']['records'])
|
res = ''.join(record['targetText'] for record in data['translate']['records'])
|
||||||
if res == '' and text.strip() != '':
|
if res == '' and text.strip() != '':
|
||||||
# qtv、qtk过期
|
# qtv、qtk过期
|
||||||
logger.warning('TencentTranslate result is empty %s', data)
|
logger.warning('TencentTranslateFree result is empty %s', data)
|
||||||
return None
|
return None
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _on_fail(self):
|
def _on_fail(self):
|
||||||
self._fail_count += 1
|
self._fail_count += 1
|
||||||
# 目前没有测试出被ban的情况,为了可靠性,连续失败20次时冷却并重新init
|
# 目前没有测试出被ban的情况,为了可靠性,连续失败20次时冷却直到下次重新init
|
||||||
if self._fail_count >= 20 and self._cool_down_future is None:
|
if self._fail_count >= 20:
|
||||||
self._cool_down_future = asyncio.ensure_future(self._cool_down())
|
self._cool_down()
|
||||||
|
|
||||||
async def _cool_down(self):
|
def _cool_down(self):
|
||||||
logger.info('TencentTranslate is cooling down')
|
logger.info('TencentTranslateFree is cooling down')
|
||||||
|
# 下次_do_init后恢复
|
||||||
self._qtv = self._qtk = ''
|
self._qtv = self._qtk = ''
|
||||||
try:
|
self._fail_count = 0
|
||||||
while True:
|
|
||||||
await asyncio.sleep(3 * 60)
|
|
||||||
logger.info('TencentTranslate reinit')
|
|
||||||
try:
|
|
||||||
if await self._do_init():
|
|
||||||
self._fail_count = 0
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
logger.exception('TencentTranslate init error:')
|
|
||||||
finally:
|
|
||||||
logger.info('TencentTranslate finished cooling down')
|
|
||||||
self._cool_down_future = None
|
|
||||||
|
|
||||||
|
|
||||||
class YoudaoTranslate(TranslateProvider):
|
class BilibiliTranslateFree(FlowControlTranslateProvider):
|
||||||
def __init__(self):
|
def __init__(self, query_interval, max_queue_size):
|
||||||
self._has_init = False
|
super().__init__(query_interval, max_queue_size)
|
||||||
self._cool_down_future = None
|
|
||||||
|
|
||||||
async def init(self):
|
|
||||||
# 获取cookie
|
|
||||||
try:
|
|
||||||
async with _http_session.get('http://fanyi.youdao.com/') as r:
|
|
||||||
if r.status >= 400:
|
|
||||||
logger.warning('YoudaoTranslate init request failed: status=%d %s', r.status, r.reason)
|
|
||||||
return False
|
|
||||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
cookies = _http_session.cookie_jar.filter_cookies(yarl.URL('http://fanyi.youdao.com/'))
|
|
||||||
res = 'JSESSIONID' in cookies and 'OUTFOX_SEARCH_USER_ID' in cookies
|
|
||||||
if res:
|
|
||||||
self._has_init = True
|
|
||||||
return res
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_available(self):
|
|
||||||
return self._has_init
|
|
||||||
|
|
||||||
def translate(self, text, future):
|
|
||||||
asyncio.ensure_future(self._translate_coroutine(text, future))
|
|
||||||
|
|
||||||
async def _translate_coroutine(self, text, future):
|
|
||||||
try:
|
|
||||||
res = await self._do_translate(text)
|
|
||||||
except BaseException as e:
|
|
||||||
future.set_exception(e)
|
|
||||||
else:
|
|
||||||
future.set_result(res)
|
|
||||||
|
|
||||||
async def _do_translate(self, text):
|
async def _do_translate(self, text):
|
||||||
try:
|
|
||||||
async with _http_session.post(
|
|
||||||
'http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule',
|
|
||||||
headers={
|
|
||||||
'Referer': 'http://fanyi.youdao.com/'
|
|
||||||
},
|
|
||||||
data={
|
|
||||||
'i': text,
|
|
||||||
'from': 'zh-CHS',
|
|
||||||
'to': 'ja',
|
|
||||||
'smartresult': 'dict',
|
|
||||||
'client': 'fanyideskweb',
|
|
||||||
**self._generate_salt(text),
|
|
||||||
'doctype': 'json',
|
|
||||||
'version': '2.1',
|
|
||||||
'keyfrom': 'fanyi.web',
|
|
||||||
'action': 'FY_BY_REALTlME'
|
|
||||||
}
|
|
||||||
) as r:
|
|
||||||
if r.status != 200:
|
|
||||||
logger.warning('YoudaoTranslate request failed: status=%d %s', r.status, r.reason)
|
|
||||||
return None
|
|
||||||
data = await r.json()
|
|
||||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
|
||||||
return None
|
|
||||||
except aiohttp.ContentTypeError:
|
|
||||||
# 被ban了
|
|
||||||
if self._cool_down_future is None:
|
|
||||||
self._cool_down_future = asyncio.ensure_future(self._cool_down())
|
|
||||||
return None
|
|
||||||
if data['errorCode'] != 0:
|
|
||||||
logger.warning('YoudaoTranslate failed: %d', data['errorCode'])
|
|
||||||
return None
|
|
||||||
|
|
||||||
res = []
|
|
||||||
for outer_result in data['translateResult']:
|
|
||||||
for inner_result in outer_result:
|
|
||||||
res.append(inner_result['tgt'])
|
|
||||||
return ''.join(res)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _generate_salt(text):
|
|
||||||
timestamp = int(time.time() * 1000)
|
|
||||||
salt = f'{timestamp}{random.randint(0, 9)}'
|
|
||||||
md5 = hashlib.md5()
|
|
||||||
md5.update(f'fanyideskweb{text}{salt}n%A-rKaT5fb[Gy?;N5@Tj'.encode())
|
|
||||||
sign = md5.hexdigest()
|
|
||||||
return {
|
|
||||||
'ts': timestamp,
|
|
||||||
'bv': '7bcd9ea3ff9b319782c2a557acee9179', # md5(navigator.appVersion)
|
|
||||||
'salt': salt,
|
|
||||||
'sign': sign
|
|
||||||
}
|
|
||||||
|
|
||||||
async def _cool_down(self):
|
|
||||||
logger.info('YoudaoTranslate is cooling down')
|
|
||||||
self._has_init = False
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(3 * 60)
|
|
||||||
try:
|
|
||||||
is_success = await self.init()
|
|
||||||
except Exception:
|
|
||||||
logger.exception('YoudaoTranslate init error:')
|
|
||||||
continue
|
|
||||||
if is_success:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
logger.info('YoudaoTranslate finished cooling down')
|
|
||||||
self._cool_down_future = None
|
|
||||||
|
|
||||||
|
|
||||||
# 目前B站后端是百度翻译
|
|
||||||
class BilibiliTranslate(TranslateProvider):
|
|
||||||
def __init__(self):
|
|
||||||
# 最长等待时间大约21秒,(text, future)
|
|
||||||
self._text_queue = asyncio.Queue(7)
|
|
||||||
|
|
||||||
async def init(self):
|
|
||||||
asyncio.ensure_future(self._translate_consumer())
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_available(self):
|
|
||||||
return not self._text_queue.full()
|
|
||||||
|
|
||||||
def translate(self, text, future):
|
|
||||||
try:
|
|
||||||
self._text_queue.put_nowait((text, future))
|
|
||||||
except asyncio.QueueFull:
|
|
||||||
future.set_result(None)
|
|
||||||
|
|
||||||
async def _translate_consumer(self):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
text, future = await self._text_queue.get()
|
|
||||||
asyncio.ensure_future(self._translate_coroutine(text, future))
|
|
||||||
# 频率限制一分钟20次
|
|
||||||
await asyncio.sleep(3.1)
|
|
||||||
except Exception:
|
|
||||||
logger.exception('BilibiliTranslate error:')
|
|
||||||
|
|
||||||
async def _translate_coroutine(self, text, future):
|
|
||||||
try:
|
|
||||||
res = await self._do_translate(text)
|
|
||||||
except BaseException as e:
|
|
||||||
future.set_exception(e)
|
|
||||||
else:
|
|
||||||
future.set_result(res)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _do_translate(text):
|
|
||||||
try:
|
try:
|
||||||
async with _http_session.get(
|
async with _http_session.get(
|
||||||
'https://api.live.bilibili.com/av/v1/SuperChat/messageTranslate',
|
'https://api.live.bilibili.com/av/v1/SuperChat/messageTranslate',
|
||||||
@ -421,12 +355,183 @@ class BilibiliTranslate(TranslateProvider):
|
|||||||
}
|
}
|
||||||
) as r:
|
) as r:
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
logger.warning('BilibiliTranslate request failed: status=%d %s', r.status, r.reason)
|
logger.warning('BilibiliTranslateFree request failed: status=%d %s', r.status, r.reason)
|
||||||
return None
|
return None
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||||
return None
|
return None
|
||||||
if data['code'] != 0:
|
if data['code'] != 0:
|
||||||
logger.warning('BilibiliTranslate failed: %d %s', data['code'], data['msg'])
|
logger.warning('BilibiliTranslateFree failed: %d %s', data['code'], data['msg'])
|
||||||
return None
|
return None
|
||||||
return data['data']['message_trans']
|
return data['data']['message_trans']
|
||||||
|
|
||||||
|
|
||||||
|
class TencentTranslate(FlowControlTranslateProvider):
|
||||||
|
def __init__(self, query_interval, max_queue_size, source_language, target_language,
|
||||||
|
secret_id, secret_key, region):
|
||||||
|
super().__init__(query_interval, max_queue_size)
|
||||||
|
self._source_language = source_language
|
||||||
|
self._target_language = target_language
|
||||||
|
self._secret_id = secret_id
|
||||||
|
self._secret_key = secret_key
|
||||||
|
self._region = region
|
||||||
|
|
||||||
|
self._cool_down_timer_handle = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_available(self):
|
||||||
|
return self._cool_down_timer_handle is None and super().is_available
|
||||||
|
|
||||||
|
async def _do_translate(self, text):
|
||||||
|
try:
|
||||||
|
async with self._request_tencent_cloud(
|
||||||
|
'TextTranslate',
|
||||||
|
'2018-03-21',
|
||||||
|
{
|
||||||
|
'SourceText': text,
|
||||||
|
'Source': self._source_language,
|
||||||
|
'Target': self._target_language,
|
||||||
|
'ProjectId': 0
|
||||||
|
}
|
||||||
|
) as r:
|
||||||
|
if r.status != 200:
|
||||||
|
logger.warning('TencentTranslate request failed: status=%d %s', r.status, r.reason)
|
||||||
|
return None
|
||||||
|
data = (await r.json())['Response']
|
||||||
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||||
|
return None
|
||||||
|
error = data.get('Error', None)
|
||||||
|
if error is not None:
|
||||||
|
logger.warning('TencentTranslate failed: %s %s, RequestId=%s', error['Code'],
|
||||||
|
error['Message'], data['RequestId'])
|
||||||
|
self._on_fail(error['Code'])
|
||||||
|
return None
|
||||||
|
return data['TargetText']
|
||||||
|
|
||||||
|
def _request_tencent_cloud(self, action, version, body):
|
||||||
|
body_bytes = json.dumps(body).encode('utf-8')
|
||||||
|
|
||||||
|
canonical_headers = 'content-type:application/json; charset=utf-8\nhost:tmt.tencentcloudapi.com\n'
|
||||||
|
signed_headers = 'content-type;host'
|
||||||
|
hashed_request_payload = hashlib.sha256(body_bytes).hexdigest()
|
||||||
|
canonical_request = f'POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_request_payload}'
|
||||||
|
|
||||||
|
request_timestamp = int(datetime.datetime.now().timestamp())
|
||||||
|
date = datetime.datetime.utcfromtimestamp(request_timestamp).strftime('%Y-%m-%d')
|
||||||
|
credential_scope = f'{date}/tmt/tc3_request'
|
||||||
|
hashed_canonical_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
|
||||||
|
string_to_sign = f'TC3-HMAC-SHA256\n{request_timestamp}\n{credential_scope}\n{hashed_canonical_request}'
|
||||||
|
|
||||||
|
def sign(key, msg):
|
||||||
|
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
|
||||||
|
|
||||||
|
secret_date = sign(('TC3' + self._secret_key).encode('utf-8'), date)
|
||||||
|
secret_service = sign(secret_date, 'tmt')
|
||||||
|
secret_signing = sign(secret_service, 'tc3_request')
|
||||||
|
signature = hmac.new(secret_signing, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
authorization = (
|
||||||
|
f'TC3-HMAC-SHA256 Credential={self._secret_id}/{credential_scope}, '
|
||||||
|
f'SignedHeaders={signed_headers}, Signature={signature}'
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': authorization,
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'X-TC-Action': action,
|
||||||
|
'X-TC-Version': version,
|
||||||
|
'X-TC-Timestamp': str(request_timestamp),
|
||||||
|
'X-TC-Region': self._region
|
||||||
|
}
|
||||||
|
|
||||||
|
return _http_session.post('https://tmt.tencentcloudapi.com/', headers=headers, data=body_bytes)
|
||||||
|
|
||||||
|
def _on_fail(self, code):
|
||||||
|
if self._cool_down_timer_handle is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
sleep_time = 0
|
||||||
|
if code == 'FailedOperation.NoFreeAmount':
|
||||||
|
# 下个月恢复免费额度
|
||||||
|
cur_time = datetime.datetime.now()
|
||||||
|
year = cur_time.year
|
||||||
|
month = cur_time.month + 1
|
||||||
|
if month > 12:
|
||||||
|
year += 1
|
||||||
|
month = 1
|
||||||
|
next_month_time = datetime.datetime(year, month, 1, minute=5)
|
||||||
|
sleep_time = (next_month_time - cur_time).total_seconds()
|
||||||
|
# Python 3.8之前不能超过一天
|
||||||
|
sleep_time = min(sleep_time, 24 * 60 * 60 - 1)
|
||||||
|
elif code in ('FailedOperation.ServiceIsolate', 'LimitExceeded'):
|
||||||
|
# 需要手动处理,等5分钟
|
||||||
|
sleep_time = 5 * 60
|
||||||
|
if sleep_time != 0:
|
||||||
|
self._cool_down_timer_handle = asyncio.get_event_loop().call_later(
|
||||||
|
sleep_time, self._on_cool_down_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_cool_down_timeout(self):
|
||||||
|
self._cool_down_timer_handle = None
|
||||||
|
|
||||||
|
|
||||||
|
class BaiduTranslate(FlowControlTranslateProvider):
|
||||||
|
def __init__(self, query_interval, max_queue_size, source_language, target_language,
|
||||||
|
app_id, secret):
|
||||||
|
super().__init__(query_interval, max_queue_size)
|
||||||
|
self._source_language = source_language
|
||||||
|
self._target_language = target_language
|
||||||
|
self._app_id = app_id
|
||||||
|
self._secret = secret
|
||||||
|
|
||||||
|
self._cool_down_timer_handle = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_available(self):
|
||||||
|
return self._cool_down_timer_handle is None and super().is_available
|
||||||
|
|
||||||
|
async def _do_translate(self, text):
|
||||||
|
try:
|
||||||
|
async with _http_session.post(
|
||||||
|
'https://fanyi-api.baidu.com/api/trans/vip/translate',
|
||||||
|
data=self._add_sign({
|
||||||
|
'q': text,
|
||||||
|
'from': self._source_language,
|
||||||
|
'to': self._target_language,
|
||||||
|
'appid': self._app_id,
|
||||||
|
'salt': random.randint(1, 999999999)
|
||||||
|
})
|
||||||
|
) as r:
|
||||||
|
if r.status != 200:
|
||||||
|
logger.warning('BaiduTranslate request failed: status=%d %s', r.status, r.reason)
|
||||||
|
return None
|
||||||
|
data = await r.json()
|
||||||
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||||
|
return None
|
||||||
|
error_code = data.get('error_code', None)
|
||||||
|
if error_code is not None:
|
||||||
|
logger.warning('BaiduTranslate failed: %s %s', error_code, data['error_msg'])
|
||||||
|
self._on_fail(error_code)
|
||||||
|
return None
|
||||||
|
return ''.join(result['dst'] for result in data['trans_result'])
|
||||||
|
|
||||||
|
def _add_sign(self, data):
|
||||||
|
str_to_sign = f"{self._app_id}{data['q']}{data['salt']}{self._secret}"
|
||||||
|
sign = hashlib.md5(str_to_sign.encode('utf-8')).hexdigest()
|
||||||
|
return {**data, 'sign': sign}
|
||||||
|
|
||||||
|
def _on_fail(self, code):
|
||||||
|
if self._cool_down_timer_handle is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
sleep_time = 0
|
||||||
|
if code == '54004':
|
||||||
|
# 账户余额不足,需要手动处理,等5分钟
|
||||||
|
sleep_time = 5 * 60
|
||||||
|
if sleep_time != 0:
|
||||||
|
self._cool_down_timer_handle = asyncio.get_event_loop().call_later(
|
||||||
|
sleep_time, self._on_cool_down_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_cool_down_timeout(self):
|
||||||
|
self._cool_down_timer_handle = None
|
||||||
|
@ -13,7 +13,7 @@ def check_update():
|
|||||||
|
|
||||||
async def _do_check_update():
|
async def _do_check_update():
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
|
||||||
async with session.get('https://api.github.com/repos/xfgryujk/blivechat/releases/latest') as r:
|
async with session.get('https://api.github.com/repos/xfgryujk/blivechat/releases/latest') as r:
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
if data['name'] != VERSION:
|
if data['name'] != VERSION:
|
||||||
|
Loading…
Reference in New Issue
Block a user