// ==UserScript==
// @name Bilibili直播SC过滤
// @namespace https://github.com/journey-ad
// @version 0.3.2
// @description 通过UID、关键词或正则表达式过滤哔站直播间的SC
// @author journey-ad
// @icon https://www.google.com/s2/favicons?domain=bilibili.com
// @include /https?:\/\/live\.bilibili\.com\/(blanc\/)?\d+\??.*/
// @require https://cdn.jsdelivr.net/npm/vue@2
// @require https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/ajax-hook@2.0.3/dist/ajaxhook.min.js
// @require https://greasyfork.org/scripts/417560-bliveproxy/code/bliveproxy.js?version=984333
// @grant none
// @license MIT
// @run-at document-end
// @downloadURL https://update.greasyfork.icu/scripts/437580/Bilibili%E7%9B%B4%E6%92%ADSC%E8%BF%87%E6%BB%A4.user.js
// @updateURL https://update.greasyfork.icu/scripts/437580/Bilibili%E7%9B%B4%E6%92%ADSC%E8%BF%87%E6%BB%A4.meta.js
// ==/UserScript==
(function () {
'use strict';
const __SCRIPT_VERSION = '0.3.2';
let store = null,
blockUser = [], // 屏蔽用户列表
uidList = [], // 屏蔽用户UID列表
blockContent = [], // 屏蔽内容列表
pattern = null // 最终生成的正则表达式
function initApp() {
store = new MyStorage('__SC_BLOCK_DATA') // 初始化存储池
blockUser = store.get('blockUser', blockUser)
blockContent = store.get('blockContent', blockContent)
updateBlockList() // 更新屏蔽列表
// hook初始化接口,过滤sc数据
ah.proxy({
onRequest: (config, handler) => {
// Ajax-hook库的bug
// 什么都不做也要加上onRequest方法,不然会丢掉header导致csrf校验失败
handler.next(config);
},
onResponse: (response, handler) => {
if (response.config.url.includes('/xlive/web-room/v1/index/getInfoByRoom')) {
// console.log('======HOOK=======', response)
const _resp = JSON.parse(response.response)
// 过滤初始化数据的sc
if (_resp?.data?.super_chat_info?.message_list) {
_resp.data.super_chat_info.message_list = scFilter(_resp.data.super_chat_info.message_list)
}
response.response = JSON.stringify(_resp)
}
handler.next(response);
},
onError: (error, handler) => {
// 触发b站自己的xhr请求错误处理逻辑,避免一些未预期的行为 如无法显示大航海列表
handler?.xhrProxy?.onerror?.()
handler.next(error)
}
})
if (window?.__SSR_INITIAL_STATE__?.baseInfoRoom?.super_chat_info?.message_list) {
window.__SSR_INITIAL_STATE__.baseInfoRoom.super_chat_info.message_list = scFilter(window.__SSR_INITIAL_STATE__.baseInfoRoom.super_chat_info.message_list)
}
if (window?.__NEPTUNE_IS_MY_WAIFU__?.roomInfoRes?.data?.super_chat_info?.message_list) {
window.__NEPTUNE_IS_MY_WAIFU__.roomInfoRes.data.super_chat_info.message_list = scFilter(window.__NEPTUNE_IS_MY_WAIFU__.roomInfoRes.data.super_chat_info.message_list)
}
function scFilter(list) {
return list.filter(item => {
return !check({ type: 'SC', uid: item.uid, name: item.user_info.uname, msg: item.message })
})
}
// 通过sc右上角菜单屏蔽
$(document).on('click', '#pay-note-panel-vm .card-list .card-item-box', function (e) {
// 等一会详情dom加载,应该有更好的方法
setTimeout(() => {
$('#pay-note-panel-vm .detail-info .card-detail').on('click', '.more', function (e) {
// 已经添加过屏蔽按钮
if ($('#pay-note-panel-vm .card-detail .danmaku-menu .add-blocklist').length > 0) return
const cardEl = $(this).closest('.card-detail')[0]
const cardVM = cardEl.__vue__
const scData = cardVM?.currentCardData || null // 从挂载的vue实例拿到sc数据
if (!scData) return
const { uid, userInfo, message } = scData
const { uname } = userInfo
const roomid = window.BilibiliLive.ROOMID // 从全局变量拿到直播间号
const menuEl = $(cardEl).find('.danmaku-menu')
const menuItem = $(`
`)
menuItem.find('.add-blocklist').data('scData', { uid, uname, message, roomid })
// 插入菜单项
menuEl.append(menuItem)
$('#pay-note-panel-vm .card-detail .danmaku-menu').one('click', '.add-blocklist', function (e) {
cardVM.showInfo = false
const scData = $(this).data('scData')
// 添加屏蔽
addBlock('user', scData)
// 隐藏这个uid所有sc
hideSC(uid)
})
})
}, 200);
})
}
function initSettingPanel() {
const cssText = `.sc-block-setting {
display: none;
position: absolute;
right: 3%;
bottom: 32px;
width: 94%;
background: #fff;
border-radius: 6px;
padding: 6px 6px;
box-sizing: border-box;
color: #444;
font-size: 12px;
border: 1px solid #e7e7e7;
box-shadow: 0px 1px 10px #e9e9e9;
z-index: 100;
}
.sc-block-setting * {
box-sizing: border-box;
}
.sc-block-setting ::-webkit-scrollbar {
width: 4px !important;
height: 4px !important;
}
.sc-block-setting ::-webkit-scrollbar-button {
width: 0;
height: 0;
}
.sc-block-setting ::-webkit-scrollbar-thumb {
background: #e1e1e1 !important;
border-radius: 4px;
}
.sc-block-setting fieldset {
margin: 0;
padding: 0 4px;
border: 1px solid #efefef;
}
.sc-block-setting fieldset legend {
padding: 0 4px;
margin-bottom: 2px;
}
.sc-block-setting input[type="text"] {
display: block;
appearance: none;
width: 100%;
height: 22px;
line-height: 22px;
padding: 0 6px;
border: 1px solid #999;
border-radius: 2px;
outline: 0;
}
.sc-block-setting input[type="text"]::-webkit-input-placeholder {
color: #ccc;
}
.sc-block-setting input[type="text"]:focus {
border-color: #4caf50;
}
.sc-block-setting button {
display: flex;
justify-content: center;
align-items: center;
appearance: none;
margin-left: 5px;
height: 22px;
padding: 0 7px;
font-size: 12px;
color: #137cbd;
background: #fff;
border: 1px solid #23ade5;
border-radius: 3px;
line-height: 1;
user-select: none;
cursor: pointer;
transition: all 0.12s ease-in-out;
}
.sc-block-setting button:hover {
color: #fff;
background: #23ade5;
}
.sc-block-setting .header-bar {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
padding: 0 0 6px;
border-bottom: 1px solid #efefef;
}
.sc-block-setting .header-bar h4 {
font-size: 14px;
margin: 0;
}
.sc-block-setting .header-bar .btn {
cursor: pointer;
user-select: none;
font-size: 0;
margin-right: 2px;
}
.sc-block-setting .header-bar .btn.setting-btn svg {
fill: #222;
}
.sc-block-setting .header-bar .btn svg {
fill: #444;
}
.sc-block-setting .header-bar .setting-btn {
margin-left: auto;
margin-right: 8px;
}
.sc-block-setting .setting-content {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
overflow: hidden;
}
.sc-block-setting .setting-content .setting-item,
.sc-block-setting .setting-content .func-item {
float: left;
display: flex;
align-items: center;
height: 24px;
margin-top: 4px;
}
.sc-block-setting .setting-content .setting-item {
margin: 0 5px;
}
.sc-block-setting .setting-content .setting-item input[type="checkbox"] {
cursor: pointer;
}
.sc-block-setting .setting-content .setting-item label {
margin-left: 2px;
cursor: pointer;
user-select: none;
}
.sc-block-setting .section {
margin-top: 10px;
}
.sc-block-setting .section .empty {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
color: #5e5e5e;
}
.sc-block-setting .keyword-wrap {
display: flex;
justify-content: space-between;
align-items: center;
}
.sc-block-setting .keyword-wrap .add-keyword,
.sc-block-setting .keyword-wrap .add-uid {
flex: none;
}
.sc-block-setting .block-list {
min-height: 50px;
max-height: 122px;
margin-top: 8px;
overflow-y: auto;
}
.sc-block-setting .block-list.list-content .block-item {
display: flex;
justify-content: space-between;
align-items: center;
width: 250px;
white-space: nowrap;
text-overflow: ellipsis;
}
.sc-block-setting .block-list.list-content .block-item.is-regex {
color: #ff9800;
background: #fff9c5;
}
.sc-block-setting .block-list.list-content .block-item.is-regex::after {
content: "[正则]";
position: absolute;
top: 50%;
right: 28px;
transform: translateY(-50%);
color: #ccc;
}
.sc-block-setting .block-list.list-content .block-item.is-regex span {
width: 190px;
}
.sc-block-setting .block-list.list-content .block-item span {
width: 210px;
}
.sc-block-setting .block-list.list-content .block-item button {
margin-right: 6px;
}
.sc-block-setting .block-list.list-user .block-item button {
position: absolute;
right: 6px;
top: 12px;
}
.sc-block-setting .block-list .block-item {
position: relative;
padding: 2px 6px;
padding-right: 0;
}
.sc-block-setting .block-list .block-item:nth-of-type(odd) {
background-color: #f9f9f9;
border-top: 1px solid #FAFAFA;
}
.sc-block-setting .block-list .block-item:hover button {
opacity: 1;
}
.sc-block-setting .block-list .block-item a {
color: #23ade5;
cursor: pointer;
}
.sc-block-setting .block-list .block-item span {
margin-right: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sc-block-setting .block-list .block-item button {
flex: none;
width: 16px;
height: 16px;
border-radius: 50%;
font-size: 10px;
margin-left: 4px;
opacity: 0;
transition: 0.2s opacity ease-in-out;
}
.sc-block-setting .block-list .block-item .user-info,
.sc-block-setting .block-list .block-item .meta-info {
display: flex;
justify-content: flex-start;
max-width: 220px;
line-height: 16px;
}
.sc-block-setting .block-list .block-item .meta-info {
font-size: 11px;
color: #9f9f9f;
}
.sc-block-setting .block-list .block-item .meta-info a {
color: #9f9f9f;
}
.sc-block-setting .block-list .block-item .uid {
flex: none;
}
.sc-block-setting .block-list .block-item .message {
height: 24px;
line-height: 24px;
width: 235px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}`;
const template = `
`;
const appConf = {
data: {
uid: '', // 屏蔽UID
keyword: '', // 屏蔽关键词
blockUser, // 屏蔽用户列表
blockContent, // 屏蔽内容列表
roomid: window.BilibiliLive.ROOMID, // 直播间号
token: '', // CSRF Token
showSetting: false, // 是否显示扩展设置
// 设置项
setting: {
showRef: false, // 显示屏蔽来源
danmaku: true, // 同时过滤弹幕
syncSite: false, // 同时添加到网站
},
},
watch: {
setting: {
handler() {
this.saveSetting();
},
deep: true,
}
},
created() {
this.setting = store.get('setting', this.setting);
this.handleBroadcast();
const token = document.cookie.match(/bili_jct=([0-9a-fA-F]{32})/);
if (token) {
this.token = token[1]
} else {
return this.toast('找不到令牌', 'error');
}
},
methods: {
closeSetting() {
$('#sc-block-setting-vm').fadeOut(200);
this.saveSetting();
},
toggleSetingPanel() {
this.showSetting = !this.showSetting;
},
saveSetting() {
store.set('setting', this.setting);
},
handleAdd(type) {
if (type === 'content') {
if (this.keyword === '') {
return;
}
if (this.setting.syncSite) {
this.addSiteShield('content', this.keyword);
}
addBlock('content', this.keyword);
this.keyword = '';
} else if (type === 'uid') {
if (this.uid === '') {
return;
}
if (this.setting.syncSite) {
this.addSiteShield('uid', this.uid);
}
this.fetchUserInfo(this.uid)
.then(({ name }) => {
addBlock('user', {
uid: this.uid,
uname: name,
roomid: this.roomid,
message: '[通过UID手动屏蔽]'
})
this.uid = '';
})
.catch(err => {
this.toast(err.message, 'error');
})
}
},
handleRemove(type, index) {
if (type === 'content') {
removeBlock('content', index);
} else if (type === 'user') {
removeBlock('user', index);
}
},
// 同步屏蔽列表
handleSyncSiteShield() {
this.fetchSiteShield(this.roomid)
.then(({ users, keywords }) => {
const newBlockUser = users.map(user => {
return {
uid: user.uid,
uname: user.uname,
roomid: this.roomid,
message: '[同步自网站屏蔽列表]'
}
})
addBlock('user', newBlockUser);
addBlock('content', keywords);
this.toast('同步完成', 'info');
})
.catch(err => {
this.toast(err.message, 'error');
})
},
// 重置屏蔽列表
reset() {
blockUser = [];
blockContent = [];
this.blockUser = blockUser;
this.blockContent = blockContent;
updateBlockList();
this.toast('记录已清空', 'info');
},
// 获取用户信息
fetchUserInfo(uid) {
return new Promise((resolve, reject) => {
fetch('https://api.bilibili.com/x/space/acc/info?mid=' + uid, {
method: "GET",
mode: "cors",
credentials: "include"
})
.then(res => res.json())
.then(resp => {
// console.log(resp)
if (resp.code === 0) {
const { mid, name } = resp.data;
resolve({ mid, name })
} else {
reject(new Error(resp.message))
}
})
.catch(err => {
reject(err)
})
})
},
// 获取网站屏蔽列表
fetchSiteShield(roomid) {
return new Promise((resolve, reject) => {
fetch('https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByUser?room_id=' + roomid, {
method: "GET",
mode: "cors",
credentials: "include"
})
.then(res => res.json())
.then(resp => {
if (resp.code === 0) {
const { shield_user_list: users, keyword_list: keywords } = resp.data.shield_info;
resolve({ users, keywords })
} else {
reject(new Error(resp.message))
}
})
.catch(err => {
reject(err)
})
})
},
// 添加至网站屏蔽
addSiteShield(type = 'uid', data) {
let api = '', body = ''
if (type === 'uid') {
api = 'https://api.live.bilibili.com/liveact/shield_user';
body = new URLSearchParams({
roomid: this.roomid,
uid: data,
type: 1,
csrf_token: this.token,
csrf: this.token,
visit_id: ''
})
} else if (type === 'content') {
api = 'https://api.live.bilibili.com/xlive/web-ucenter/v1/banned/AddShieldKeyword';
const formData = new FormData();
formData.append('keyword', data);
formData.append('csrf', this.token);
formData.append('csrf_token', this.token);
body = formData;
}
fetch(api, {
body,
method: "POST",
mode: "cors",
credentials: "include"
})
.then(res => res.json())
.then(resp => {
console.log(resp)
})
.catch(err => {
console.error(err)
})
},
// 处理广播过滤
handleBroadcast() {
// 弹幕
bliveproxy.addCommandHandler('DANMU_MSG', command => {
// 设置里不处理弹幕
if (!this.setting.danmaku) return;
let info = command.info
const msg = info[1]
const [uid, name] = info[2]
if (check({ type: '弹幕', uid, name, msg })) {
command.cmd = "NULL"
}
})
// SC
bliveproxy.addCommandHandler('SUPER_CHAT_MESSAGE', command => {
const { roomid, data } = command
const { uid, message: msg, user_info } = data
const name = user_info.uname
if (check({ type: 'SC', uid, name, msg })) {
command.cmd = "NULL"
}
})
},
...Utils
},
filters: {
dateFmt(ts, format) {
const dateData = new Date(ts * 1000);
const date = {
"M+": dateData.getMonth() + 1,
"d+": dateData.getDate(),
"h+": dateData.getHours(),
"m+": dateData.getMinutes(),
"s+": dateData.getSeconds(),
"q+": Math.floor((dateData.getMonth() + 3) / 3),
"S+": dateData.getMilliseconds()
};
if (/(y+)/i.test(format)) {
format = format.replace(RegExp.$1, String(dateData.getFullYear()).substr(4 - RegExp.$1.length));
}
for (let k in date) {
if (new RegExp('(' + k + ')').test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length == 1 ?
date[k] : ("00" + date[k]).substr(String(date[k]).length));
}
}
return format;
}
}
}
Utils.addStyle(cssText);
const $settingPanel = new Vue(appConf)
const $btn = $('SC屏蔽助手')
$btn.css({ fontSize: '12px', margin: '0 5px', cursor: 'pointer', lineHeight: '24px', userSelect: 'none' })
$btn.on('click', function () {
$('#sc-block-setting-vm').toggle(200)
})
new MutationObserver((mutations, observer) => {
if (Utils.get('.icon-right-part')) {
observer.disconnect();
$('#control-panel-ctnr-box .icon-right-part').prepend($btn)
$('#control-panel-ctnr-box .control-panel-icon-row').append(template)
$settingPanel.$mount('#sc-block-setting-vm')
}
})
.observe(Utils.get('#control-panel-ctnr-box') || document.body, { childList: true, subtree: true });
}
// 检查是否在屏蔽名单内
function check({ type = '弹幕', uid, name, msg }) {
const content = `[${type}]${name}: ${msg}`
// 检查uid
if (uidList.includes(uid)) {
console.warn('UID blocked', uid, content)
return true
}
// 检查名字和内容
if (pattern) {
const match = content.match(pattern)
if (match) {
console.warn('Content blocked', uid, content, `======> ${match[0]}`)
return true
} else {
return false
}
}
}
// 隐藏指定uid发送的sc
function hideSC(uids) {
if (Utils.typeOf(uids) !== 'array') uids = [uids]
$('.card-list .card-item-box').each((_, item) => {
const uid = item.__vue__.itemData.uid
if (uids.includes(uid)) {
item.__vue__.itemData.show = false
}
})
// 关闭已打开的sc详情
// 详情窗口打开时会监听window对象的click事件,并调用closeMask方法来关闭详情窗口
// 这里直接给body派发一个click事件触发它
$('body').trigger('click')
}
// 添加屏蔽
function addBlock(type, data) {
if (!Array.isArray(data)) {
data = [data]
}
data.forEach(item => {
const ts = Date.now() / 1000 | 0
switch (type) {
case 'user':
const { uid, uname, message, roomid } = item
if (!uidList.includes(parseInt(uid))) {
blockUser.unshift({
uid: parseInt(uid), // uid
uname, // 名字
roomid, // 直播间号
ts, // 时间戳
ref: message // sc内容
})
} else {
if (data.length === 1) {
Utils.toast('已在屏蔽名单中', 'warn')
}
}
break;
case 'content':
if (!blockContent.includes(item)) {
blockContent.unshift(item)
} else {
if (data.length === 1) {
Utils.toast('已在屏蔽名单中', 'warn')
}
}
break;
default:
break;
}
})
if (data.length === 1) Utils.toast('添加屏蔽成功', 'success');
updateBlockList()
}
// 移除屏蔽项
function removeBlock(type, index) {
switch (type) {
case 'user':
blockUser.splice(index, 1)
break;
case 'content':
blockContent.splice(index, 1)
pattern = Utils.generatePattern(blockContent)
break;
default:
break;
}
Utils.toast('移除屏蔽成功', 'info', 1000);
updateBlockList()
}
// 更新屏蔽列表
function updateBlockList() {
// 屏蔽用户uid列表
uidList = blockUser.map(item => parseInt(item.uid))
// 生成过滤正则
pattern = Utils.generatePattern(blockContent)
store.set('blockUser', blockUser)
store.set('blockContent', blockContent)
}
// ====================== 工具 ======================
// 存储池管理
class MyStorage {
constructor(key) {
if (!key) throw new Error('cache key is required')
this.key = key
this.data = {}
try {
const _cached = JSON.parse(localStorage.getItem(this.key)) || {}
this.data = _cached || {}
} catch (e) {
this.data = {}
}
}
get(key, defaultValue) {
return this.data[key] || defaultValue
}
set(key, data) {
this.data[key] = data
this.save()
}
remove(key) {
delete this.data[key]
this.save()
}
clear() {
this.data = {}
this.save()
}
has(key) {
return this.data[key] !== undefined
}
save() {
localStorage.setItem(this.key, JSON.stringify(this.data))
}
}
// 封装一些工具
const Utils = {
create(nodeType, config, appendTo) {
const element = document.createElement(nodeType);
config && this.set(element, config);
if (appendTo) appendTo.appendChild(element);
return element;
},
set(element, config, appendTo) {
if (config) {
for (const [key, value] of Object.entries(config)) {
element[key] = value;
}
}
if (appendTo) appendTo.appendChild(element);
return element;
},
get(selector) {
if (selector instanceof Array) {
return selector.map(item => this.get(item));
}
return document.body.querySelector(selector);
},
toast(msg, type = 'success', duration = 3000) {
const classMap = {
success: 'success',
warn: 'caution',
error: 'error',
info: 'info'
}
let toast = this.create('div', {
innerHTML: `${msg}
`
});
document.querySelector('#aside-area-vm').appendChild(toast);
toast.firstChild.style.marginLeft = -toast.firstChild.offsetWidth / 2 + 'px';
setTimeout(() => document.querySelector('#aside-area-vm').removeChild(toast), duration);
},
// 检测是否处于iframe内嵌环境
inIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
},
typeOf(val) {
const typed = Object.prototype.toString.call(val)
switch (typed) {
case '[object Object]':
return 'object'
case '[object Array]':
return 'array'
case '[object String]':
return 'string'
case '[object Number]':
return 'number'
case '[object Boolean]':
return 'boolean'
case '[object Function]':
return 'function'
case '[object RegExp]':
return 'regex'
case '[object Null]':
return 'null'
case '[object Undefined]':
return 'undefined'
default:
if (val instanceof Element) {
return 'element'
}
return 'unknown'
}
},
// 根据传入关键词列表生成正则表达式
generatePattern(list) {
if (!list || !list.length) return null
const keys = list.map(item => {
if (this.isVaildRegex(item)) {
// 如果字符串为有效的正则表达式则将其内容作为关键词
return this.getRegex(item).source
} else {
// 作为普通字符串,为避免生成最终正则时产生歧义,先将其转义
return this.escapeRegex(item)
}
})
let pattern = null
try {
// 生成正则表达式
pattern = new RegExp(keys.join('|'), 'i');
console.log('pattern', pattern);
} catch (e) {
console.error(e)
}
return pattern
},
// 检测字符串是否为有效的正则表达式 eg: /^\d+$/
isVaildRegex(str) {
// 非字符串直接返回false
if (this.typeOf(str) !== 'string') return false
// 字符串长度小于3不可能是正则
if (str.length < 3) return false
// 如果字符串以/开头,且以/结尾,则可能是正则
if (/^\/.+\/[gimuy]*$/.test(str)) {
try {
return new RegExp(str)
} catch (e) {
return false
}
}
return false
},
// 根据传入字符串获取正则表达式对象
getRegex(regex) {
try {
regex = regex.trim();
let parts = regex.split('/');
if (regex[0] !== '/' || parts.length < 3) {
regex = regex.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); //escap common string
return new RegExp(regex);
}
const option = parts[parts.length - 1];
const lastIndex = regex.lastIndexOf('/');
regex = regex.substring(1, lastIndex);
return new RegExp(regex, option);
} catch (e) {
return null
}
},
// 转义正则表达式中的特殊字符
escapeRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
},
// 加载js或css,返回函数包裹的promise实例,用于顺序加载队列
loadSource(src) {
return () => {
return new Promise(function (resolve, reject) {
const TYPE = src.split('.').pop()
let s = null;
let r = false;
if (TYPE === 'js') {
s = document.createElement('script');
s.type = 'text/javascript';
s.src = src;
s.async = true;
} else if (TYPE === 'css') {
s = document.createElement('link');
s.rel = 'stylesheet';
s.type = 'text/css';
s.href = src;
}
s.onerror = function (err) {
reject(err, s);
};
s.onload = s.onreadystatechange = function () {
// console.log(this.readyState); // uncomment this line to see which ready states are called.
if (!r && (!this.readyState || this.readyState == 'complete')) {
r = true;
console.log(src)
resolve();
}
};
const t = document.getElementsByTagName('script')[0];
t.parentElement.insertBefore(s, t);
});
}
},
// 添加css
addStyle(css) {
if (typeof GM_addStyle != "undefined") {
GM_addStyle(css);
} else if (typeof PRO_addStyle != "undefined") {
PRO_addStyle(css);
} else {
const node = document.createElement("style");
node.type = "text/css";
node.appendChild(document.createTextNode(css));
const heads = document.getElementsByTagName("head");
if (heads.length > 0) {
heads[0].appendChild(node);
} else {
// no head yet, stick it whereever
document.documentElement.appendChild(node);
}
}
}
}
// ====================== 初始化 ======================
initApp() // 初始化插件
initSettingPanel() // 初始化设置面板
})();