// ==UserScript==
// @name B站屏蔽
// @namespace Shurlormes
// @version 2.0
// @description B站屏蔽,视频、up、评论等。
// @author Shurlormes
// @match *://www.bilibili.com/*
// @icon https://www.bilibili.com/favicon.ico?v=1
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @license GPL-3.0
// ==/UserScript==
(function() {
'use strict';
const ADD_BTN_STYLE = 'width: 55px; background-color: #056de8; border-radius: 4px; color: white; padding: 4px 11px 4px 9px; line-height: 16px; border: 0;';
const REMOVE_BTN_STYLE = 'width: 55px; background-color: #FE2929; border-radius: 4px; color: white; padding: 4px 11px 4px 9px; line-height: 16px; border: 0;';
const INPUT_STYLE = 'border: 1px #6d757a solid; border-radius: 4px; line-height: 20px;';
const STATIC_TD_STYLE = "border: 1px #6d757a solid; text-align: center; padding: 5px;";
const BLOCK_BTN_STYLE = 'cursor: pointer; position: relative;left: 5px;';
const LIVE_BLOCK_BTN_STYLE = 'cursor: pointer; position: relative; left: 5px; top: -5px;';
const BLOCK_BTN_TITLE = '屏蔽';
const BLOCK_BTN_TXT = '🚫';
const TITLE_BLOCK_INPUT_ID = 'shurlormes-title-block-input';
const TITLE_BLOCK_ADD_BTN_ID = 'shurlormes-title-block-add-btn';
const TITLE_BLOCK_TABLE_ID = 'shurlormes-title-block-table';
const BLOCK_DATA_REMOVE_BTN_CLASS = 'shurlormes-block-data-remove-btn';
const BLOCK_DATA_REMOVE_BTN_KEY_ATTR = 'shurlormes-block-data-remove-btn-key';
const BLOCK_DATA_REMOVE_BTN_TYPE_ATTR = 'shurlormes-block-data-remove-btn-type';
const APPENDED_BLOCK_BTN_CLASS = 'shurlormes-appended-block-btn';
const USER_BLOCK_BTN_CLASS = 'shurlormes-user-block-btn';
const USER_BLOCK_BTN_USER_ID_ATTR = 'shurlormes-user-block-btn-user-id';
const USER_BLOCK_BTN_USERNAME_ATTR = 'shurlormes-user-block-btn-username';
const TITLE_BLOCK_KEY_PREFIX = 'b-title-';
const USER_BLOCK_KEY_PREFIX = 'b-user-';
const TITLE_BLACK_SET = new Set();
const USER_BLACK_MAP = new Map();
const TYPE_TITLE_BLACK = 0;
const TYPE_USER_BLACK = 1;
const TYPE_BLACK_DATA = [TITLE_BLACK_SET, USER_BLACK_MAP]
const TYPE_BLACK_PREFIX = [TITLE_BLOCK_KEY_PREFIX, USER_BLOCK_KEY_PREFIX]
//执行间隔,单位毫秒
const INTERVAL_TIME = 500;
//首页换一换
const INDEX_FEED_CARD_CLASS = 'feed-card';
//首页视频
const INDEX_BILI_VIDEO_CARD_CLASS = 'bili-video-card is-rcmd';
//首页视频 标题
const INDEX_BILI_VIDEO_CARD_TITLE_CLASS = 'bili-video-card__info--tit';
//首页视频 用户信息
const INDEX_BILI_VIDEO_CARD_OWNER_CLASS = 'bili-video-card__info--owner';
const INDEX_BILI_VIDEO_CARD_AUTHOR_CLASS = 'bili-video-card__info--author';
const INDEX_BILI_VIDEO_CARD_AD_CLASS = 'bili-video-card__info--ad';
//首页每层独立卡
const INDEX_FLOOR_SINGLE_CARD_CLASS = 'floor-single-card';
//首页每层独立卡 标题
const INDEX_FLOOR_SINGLE_CARD_TITLE_CLASS = 'title';
const INDEX_FLOOR_SINGLE_USER_TITLE_CLASS = 'sub-title';
//首页直播
const INDEX_BILI_LIVE_CARD_CLASS = 'bili-live-card is-rcmd';
//首页直播标题
const INDEX_BILI_LIVE_CARD_TITLE_CLASS = 'bili-live-card__info--tit';
//首页直播用户信息
const INDEX_BILI_LIVE_CARD_UNAME_CLASS = 'bili-live-card__info--uname';
const TYPE_VIDEO = 0;
const TYPE_FLOOR = 1;
const TYPE_LIVE = 2;
const TYPE_CARD_CLASS = [INDEX_BILI_VIDEO_CARD_CLASS, INDEX_FLOOR_SINGLE_CARD_CLASS, INDEX_BILI_LIVE_CARD_CLASS];
const TYPE_TITLE_CLASS = [INDEX_BILI_VIDEO_CARD_TITLE_CLASS, INDEX_FLOOR_SINGLE_CARD_TITLE_CLASS, INDEX_BILI_LIVE_CARD_TITLE_CLASS];
const TYPE_USER_CLASS = [INDEX_BILI_VIDEO_CARD_OWNER_CLASS, INDEX_FLOOR_SINGLE_USER_TITLE_CLASS, INDEX_BILI_LIVE_CARD_UNAME_CLASS];
let indexBlockByType = function(type) {
let cards = document.getElementsByClassName(TYPE_CARD_CLASS[type]);
if(cards.length > 0) {
let deleteArray = [];
for (let i = 0; i < cards.length; i++) {
let card = cards[i];
//屏蔽过滤判断
if(isAd(card) || userFilter(card) || titleKeywordsFilter(card, type)) {
deleteArray.push(card);
}
}
doBlock(deleteArray);
}
}
let isAd = function(element) {
return element.getElementsByClassName(INDEX_BILI_VIDEO_CARD_AD_CLASS).length > 0;
}
let userFilter = function(card) {
let blockBtn = card.getElementsByClassName(USER_BLOCK_BTN_CLASS);
if(blockBtn.length > 0) {
return USER_BLACK_MAP.has(blockBtn[0].getAttribute(USER_BLOCK_BTN_USER_ID_ATTR))
}
return false;
}
let titleKeywordsFilter = function(card, type) {
let title = '';
let cardTitle = card.getElementsByClassName(TYPE_TITLE_CLASS[type]);
if(cardTitle.length > 0) {
if(type === TYPE_VIDEO || type === TYPE_FLOOR) {
let titleElement = cardTitle[0];
title = titleElement.getAttribute('title')
} else {
let titleA = cardTitle[0].getElementsByTagName('a');
if(titleA.length > 0) {
let titleElement = titleA[0].lastChild;
if(titleElement) {
title = titleElement.innerText;
}
}
}
}
for (let keywords of TITLE_BLACK_SET) {
if(title.indexOf(keywords) !== -1) {
return true;
}
}
return false;
}
let doBlock = function(deleteArray) {
if(deleteArray.length > 0) {
for (let i = 0; i < deleteArray.length; i++) {
let deleteItem = deleteArray[i];
let deleteItemParent = deleteItem.parentElement;
if(deleteItemParent.className.indexOf(INDEX_FEED_CARD_CLASS) !== -1) {
deleteItemParent.remove();
} else {
deleteItem.remove();
}
}
}
}
let indexBlock = function() {
indexBlockByType(TYPE_VIDEO);
indexBlockByType(TYPE_FLOOR);
indexBlockByType(TYPE_LIVE);
}
let fillBlackData = function() {
if(localStorage.length > 0){
for(let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i);
if(key.indexOf(TITLE_BLOCK_KEY_PREFIX) !== -1) {
TITLE_BLACK_SET.add(key.replaceAll(TITLE_BLOCK_KEY_PREFIX, ''));
} else if(key.indexOf(USER_BLOCK_KEY_PREFIX) !== -1) {
USER_BLACK_MAP.set(key.replaceAll(USER_BLOCK_KEY_PREFIX, ''), localStorage.getItem(key));
}
}
}
}
let appendUserBlockBtnByType = function(type) {
let userATag = document.querySelectorAll(`.${TYPE_USER_CLASS[type]}:not(.${APPENDED_BLOCK_BTN_CLASS})`);
if(userATag.length > 0) {
for (let i = 0; i < userATag.length; i++) {
let aTag = userATag[i];
let href = aTag.getAttribute('href');
if(href.indexOf('https:') === -1) {
href = 'https:' + href;
}
if(href.indexOf('https://space') === -1) {
continue;
}
const userUrl = new URL(href);
const userId = userUrl.pathname.replace('/', '');
let username = '';
if(type === TYPE_VIDEO) {
let author = aTag.getElementsByClassName(INDEX_BILI_VIDEO_CARD_AUTHOR_CLASS);
if(author.length > 0) {
username = author[0].getAttribute('title');
}
} else {
let author = aTag.lastChild;
username = author.innerText;
}
let blockBtn = document.createElement("span");
blockBtn.setAttribute(USER_BLOCK_BTN_USER_ID_ATTR, userId);
blockBtn.setAttribute(USER_BLOCK_BTN_USERNAME_ATTR, username);
blockBtn.style = type !== TYPE_LIVE ? BLOCK_BTN_STYLE : LIVE_BLOCK_BTN_STYLE;
blockBtn.title = BLOCK_BTN_TITLE;
blockBtn.innerText = BLOCK_BTN_TXT;
blockBtn.onclick = userBlockBtnClickEvent;
blockBtn.classList.add(USER_BLOCK_BTN_CLASS);
aTag.parentElement.appendChild(blockBtn);
aTag.classList.add(APPENDED_BLOCK_BTN_CLASS);
if (type === TYPE_FLOOR) {
aTag.classList.remove('flex');
}
}
}
}
let userBlockBtnClickEvent = function(e) {
const target = e.target;
const userId = target.getAttribute(USER_BLOCK_BTN_USER_ID_ATTR);
const username = target.getAttribute(USER_BLOCK_BTN_USERNAME_ATTR);
USER_BLACK_MAP.set(userId, username);
localStorage.setItem(USER_BLOCK_KEY_PREFIX + userId, username);
}
let appendUserBlockBtn = function() {
appendUserBlockBtnByType(TYPE_VIDEO);
appendUserBlockBtnByType(TYPE_FLOOR);
appendUserBlockBtnByType(TYPE_LIVE);
}
//入口
let mainEvent = function() {
fillBlackData();
indexBlock();
appendUserBlockBtn();
}
setInterval(mainEvent, INTERVAL_TIME);
//弹出层,代码参考:https://www.jianshu.com/p/79970121dbe2
const popup = (function(){
class Popup {
// 构造函数中定义公共要使用的div
constructor() {
// 定义所有弹窗都需要使用的遮罩
this.mask = document.createElement('div')
// 设置样式
this.setStyle(this.mask, {
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, .2)',
position: 'fixed',
left: 0,
top: 0,
'z-index': 999
})
// 创建中间显示内容的水平并垂直居中的div
this.content = document.createElement('div')
// 设置样式
this.setStyle(this.content, {
width: '600px',
height: '400px',
backgroundColor: '#fff',
boxShadow: '0 0 2px #999',
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(-50%,-50%)',
borderRadius: '3px'
})
// 将这个小div放在遮罩中
this.mask.appendChild(this.content)
}
// 中间有弹框的 - 适用于alert和confirm
middleBox(param) {
// 先清空中间小div的内容 - 防止调用多次,出现混乱
this.content.innerHTML = ''
// 定义标题和内容变量
let title = param.title ? param.title : '默认标题内容';
// 将遮罩放在body中显示
document.body.appendChild(this.mask)
// 给中间的小div设置默认的排版
// 上面标题部分
this.title = document.createElement('div')
// 设置样式
this.setStyle(this.title, {
width: '100%',
height: '50px',
borderBottom: '1px solid #ccc',
lineHeight: '50px',
paddingLeft: '20px',
boxSizing: 'border-box',
color: '#050505'
})
// 设置默认标题内容
this.title.innerText = title
// 将标题部分放在中间div中
this.content.appendChild(this.title)
// 关闭按钮
this.closeBtn = document.createElement('a')
// 设置内容
this.closeBtn.innerText = '×'
// 设置href属性
this.closeBtn.setAttribute('href', 'javascript:;')
// 设置样式
this.setStyle(this.closeBtn, {
textDecoration: 'none',
color: '#666',
position: 'absolute',
right: '10px',
top: '6px',
fontSize: '25px'
})
// 将关闭按钮放在中间小div中
this.content.appendChild(this.closeBtn)
// 下面具体放内容的部分
this.description = document.createElement('div')
// 将默认内容放在中间的小div中
this.content.appendChild(this.description)
// 设置样式
this.setStyle(this.description, {
color: '#666',
paddingLeft: '20px',
lineHeight: '50px'
})
}
// 弹出提示框
alert(param) {
this.middleBox(param)
this.dialogContent = document.createElement('div')
this.setStyle(this.dialogContent,{
"padding":"15px",
"max-height":"400px"
})
this.dialogContent.innerHTML = param.content;
this.content.appendChild(this.dialogContent);
// 关闭按钮和确定按钮的点击事件
this.closeBtn.onclick = () => this.close()
}
dialog(param) {
this.middleBox(param)
this.btn = document.createElement('button');
// 添加内容
this.btn.innerText = param.confirmTxt ? param.confirmTxt : '确定';
// 设置内容
this.setStyle(this.btn, {
backgroundColor: 'rgb(30, 159, 255)',
position: 'absolute',
right: '10px',
bottom: '10px',
outline: 'none',
border: 'none',
color: '#fff',
fontSize: '16px',
borderRadius: '2px',
padding: '0 10px',
height: '30px',
lineHeight: '30px'
});
// 右下角的确定按钮
let confirm = function(){}
if(param.confirm && {}.toString.call(param.confirm) === '[object Function]') {
confirm = param.confirm;
}
// 将按钮放在div中
this.content.appendChild(this.btn)
this.dialogContent = document.createElement('div')
this.setStyle(this.dialogContent,{
"padding":"15px",
"max-height":"400px"
})
this.dialogContent.innerHTML = param.content;
this.content.appendChild(this.dialogContent);
// 确定按钮的点击事件
this.btn.onclick = () => {
confirm()
this.close()
}
this.closeBtn.onclick = () => this.close()
}
close(timerId) {
// 如果有定时器,就停止定时器
if(timerId) clearInterval(timerId)
// 将遮罩从body中删除
document.body.removeChild(this.mask)
}
// 设置样式的函数
setStyle(ele, styleObj) {
for(let attr in styleObj){
ele.style[attr] = styleObj[attr];
}
}
}
let popup = null;
return (function() {
if(!popup) {
popup = new Popup()
}
return popup;
})()
})()
let generateTr = function(key, text, type) {
let showText = `<span>${text}</span>`;
if(type === TYPE_USER_BLACK) {
showText = `<a href="https://space.bilibili.com/${key}" target="_blank">${text}</a>`
}
return `<tr>
<td style="${STATIC_TD_STYLE}">
${showText}
</td>
<td style="${STATIC_TD_STYLE}">
<button class="${BLOCK_DATA_REMOVE_BTN_CLASS}" ${BLOCK_DATA_REMOVE_BTN_KEY_ATTR}="${key}"
${BLOCK_DATA_REMOVE_BTN_TYPE_ATTR}="${type}" style="${REMOVE_BTN_STYLE}">删除</button>
</td>
</tr>`;
}
let generateTrFromBlackData = function(type) {
let content = '';
if(TYPE_BLACK_DATA[type].size > 0) {
for (let data of TYPE_BLACK_DATA[type]) {
if(type === TYPE_TITLE_BLACK) {
content = content + generateTr(data, data, type);
} else {
content = content + generateTr(data[0], data[1], type);
}
}
}
return content;
}
let bindTitleBlockRemoveClickEvent = function() {
let btns = document.getElementsByClassName(BLOCK_DATA_REMOVE_BTN_CLASS);
for (let i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', function(e) {
let target = e.target;
let key = target.getAttribute(BLOCK_DATA_REMOVE_BTN_KEY_ATTR)
let type = target.getAttribute(BLOCK_DATA_REMOVE_BTN_TYPE_ATTR)
localStorage.removeItem(TYPE_BLACK_PREFIX[type] + key);
TYPE_BLACK_DATA[type].delete(key);
target.parentElement.parentElement.remove();
});
}
}
GM_registerMenuCommand('屏蔽关键词', function() {
let content = `
<div>
<div>
<span>标题包含关键词: </span>
<input id="${TITLE_BLOCK_INPUT_ID}" style="${INPUT_STYLE}" />
<button id="${TITLE_BLOCK_ADD_BTN_ID}" style="${ADD_BTN_STYLE}">添加</button>
</div>
<div style="margin-top: 5px; height: 280px; overflow: auto">
<table id="${TITLE_BLOCK_TABLE_ID}" style="width: 98%;">
<tr>
<th style="${STATIC_TD_STYLE}">关键词</th>
<th style="${STATIC_TD_STYLE} width: 80px;">操作</th>
</tr>`;
content = content + generateTrFromBlackData(TYPE_TITLE_BLACK) + `
</table>
</div>
</div>
`;
popup.alert({title: '已屏蔽关键词', content: content});
bindTitleBlockRemoveClickEvent();
document.getElementById(TITLE_BLOCK_ADD_BTN_ID).addEventListener('click', function () {
let titleInput = document.getElementById(TITLE_BLOCK_INPUT_ID);
let text = titleInput.value.trim();
if(text.length < 1 || TITLE_BLACK_SET.has(text)) {
return ;
}
titleInput.value = '';
localStorage.setItem(TITLE_BLOCK_KEY_PREFIX + text, 1);
TITLE_BLACK_SET.add(text);
let tr = generateTr(text, text, TYPE_TITLE_BLACK);
document.getElementById(TITLE_BLOCK_TABLE_ID).innerHTML += tr;
bindTitleBlockRemoveClickEvent();
});
});
GM_registerMenuCommand('屏蔽用户', function() {
let content = `
<div>
<div style="margin-top: 5px; height: 280px; overflow: auto">
<table id="${TITLE_BLOCK_TABLE_ID}" style="width: 98%;">
<tr>
<th style="${STATIC_TD_STYLE}">用户名</th>
<th style="${STATIC_TD_STYLE} width: 80px;">操作</th>
</tr>`;
content = content + generateTrFromBlackData(TYPE_USER_BLACK) + `
</table>
</div>
</div>
`;
popup.alert({title: '已屏蔽用户', content: content});
bindTitleBlockRemoveClickEvent();
});
})();