// ==UserScript==
// @name YouTube User Block
// @name:en YouTube User Block
// @namespace https://github.com/ChanthMiao/
// @icon https://www.gstatic.com/youtube/img/branding/favicon/favicon_144x144.png
// @version 1.5.1
// @description YouTube用户黑名单,拉黑指定用户(自动隐藏其评论,可手动查看)。
// @description:en YouTube User BlockList,blocking specific users (hide all their comments, support manual viewing).
// @author ChanthMiao
// @match *://www.youtube.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @compatible chrome
// @compatible firefox
// @license MIT
// ==/UserScript==
(function () {
'use strict';
function toggleButton(element) {
// Check to see if the button is pressed
var pressed = (element.getAttribute("aria-pressed") === "true");
const user = element.parentElement.parentElement.parentElement.parentElement.querySelector("#author-text > span").textContent.trim();
if (!pressed && !blockList.includes(user)) {
blockUser(user);
} else if (pressed && blockList.includes(user)) {
disblockUser(user);
}
}
function handleBtnClick(event) {
toggleButton(event.target);
}
function handleBtnKeyDown(event) {
// Check to see if space or enter were pressed
if (event.key === " " || event.key === "Enter" || event.key === "Spacebar") { // "Spacebar" for IE11 support
// Prevent the default action to stop scrolling when space is pressed
event.preventDefault();
toggleButton(event.target);
}
}
function createNodeListener(node, config, mutationCallback) {
const observer = new MutationObserver(mutationCallback);
observer.observe(node, config);
return observer;
}
function injectBlockButton(comment) {
const user = comment.querySelector("#author-text > span").textContent.trim();
const arc = comment.querySelector("#creator-heart");
var block = document.createElement("div");
block.setAttribute("id", "block-button");
if (blockList.includes(user)) {
block.setAttribute("title", "解封");
} else {
block.setAttribute("title", "拉黑");
}
var btn = document.createElement("button");
btn.setAttribute("id", "block-button-item");
btn.setAttribute("role", "button");
btn.setAttribute("aria-pressed", blockList.includes(user));
btn.addEventListener("tap", handleBtnClick, false);
btn.addEventListener("keydown", handleBtnKeyDown, false);
block.append(btn);
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("id", "block-button-icon");
svg.setAttribute("viewbox", "0 0 24 24");
svg.setAttribute("width", "16px");
svg.setAttribute("height", "16px");
svg.setAttribute("focusable", false);
svg.setAttribute("class", "style-scope yt-icon");
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
svg.setAttribute("style", "pointer-events: none;display: block;width: 100%;height: 100%;");
btn.append(svg);
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttribute("class", "style-scope yt-icon");
svg.append(g);
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M 8 4.667969 C 9.839844 4.667969 11.332031 6.160156 11.332031 8 C 11.332031 8.433594 11.246094 8.839844 11.09375 9.21875 L 13.039062 11.167969 C 14.046875 10.328125 14.839844 9.238281 15.328125 8 C 14.171875 5.074219 11.328125 3 7.992188 3 C 7.058594 3 6.167969 3.167969 5.339844 3.464844 L 6.78125 4.90625 C 7.160156 4.753906 7.566406 4.667969 8 4.667969 Z M 1.332031 2.847656 L 2.851562 4.367188 L 3.160156 4.671875 C 2.054688 5.535156 1.1875 6.679688 0.667969 8 C 1.820312 10.925781 4.667969 13 8 13 C 9.035156 13 10.019531 12.800781 10.921875 12.441406 L 11.199219 12.71875 L 13.152344 14.667969 L 14 13.820312 L 2.179688 2 Z M 5.019531 6.535156 L 6.054688 7.566406 C 6.019531 7.707031 6 7.851562 6 8 C 6 9.105469 6.894531 10 8 10 C 8.148438 10 8.292969 9.980469 8.433594 9.945312 L 9.464844 10.980469 C 9.019531 11.199219 8.527344 11.332031 8 11.332031 C 6.160156 11.332031 4.667969 9.839844 4.667969 8 C 4.667969 7.472656 4.800781 6.980469 5.019531 6.535156 Z M 7.894531 6.011719 L 9.992188 8.113281 L 10.007812 8.007812 C 10.007812 6.898438 9.113281 6.007812 8.007812 6.007812 Z M 7.894531 6.011719 ");
path.setAttribute("class", "style-scope yt-icon");
g.append(path);
arc.before(block);
}
function commentHandler(comment) {
const user = comment.querySelector("#author-text > span").textContent.trim();
var btnDiv = comment.querySelector("#block-button");
var btn = btnDiv.children[0];
if (blockList.includes(user)) {
comment.setAttribute("user-blocked", true);
btnDiv.setAttribute("title", "解封");
btn.setAttribute("aria-pressed", true);
} else {
comment.setAttribute("user-blocked", false);
btnDiv.setAttribute("title", "拉黑");
btn.setAttribute("aria-pressed", false);
}
}
function blockUser(user) {
blockList.push(user); // 添加黑名单,使后续dom由callback处理
GM_setValue("blockList", blockList);
// 更正已由callback处理过的dom
document.querySelectorAll("ytd-comment-renderer[user-blocked='false']").forEach((v, i) => {
commentHandler(v);
});
}
function disblockUser(user) {
blockList = blockList.filter(v => v != user); // 从黑名单移除用户,使后续dom由callback处理
GM_setValue("blockList", blockList);
// 更正已由callback处理过的dom
document.querySelectorAll("ytd-comment-renderer[user-blocked='true']").forEach((v, i) => {
commentHandler(v);
});
}
const callback = function (mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
var nodes = mutation.addedNodes;
nodes.forEach((v, i) => {
if (v.nodeName === 'YTD-COMMENT-RENDERER') {
if (!v.querySelector("#block-button")) {
injectBlockButton(v); // 注入拉黑按钮
}
commentHandler(v);
}
})
}
}
}
const callback_top = function (mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
var nodes = mutation.addedNodes;
nodes.forEach((v, i) => {
var comments_entry = document.querySelector('ytd-item-section-renderer > #contents:not([hooked])');
if (comments_entry) {
comments_entry.setAttribute("hooked", true);
observer.disconnect();
createNodeListener(comments_entry, config, callback);
}
})
}
}
}
function blockReset() {
blockList = [];
GM_setValue("blockList", blockList);
document.querySelectorAll("ytd-comment-renderer[user-blocked='true']").forEach((v, i) => {
commentHandler(v);
});
setTimeout(function () { alert('YouTube 用户黑名单已重置成功!'); }, 1);
}
function blockExport() {
var link = document.createElement("a");
var text = JSON.stringify(blockList, null, 2);
link.setAttribute("download", "blockList.json");
link.setAttribute("href", "data:text/json," + text);
link.click();
}
const distinct = (value, index, self) => { return self.indexOf(value) === index; };
function handleFileSelect(e) {
var files = e.target.files;
if (files.length < 1) {
setTimeout(function () { alert('请选择一份黑名单文件!'); }, 1);
return;
}
var file = files[0];
var reader = new FileReader();
reader.onload = (e) => {
try {
var content = JSON.parse(e.target.result);
if (Array.isArray(content)) {
blockList = content.concat(blockList).filter(distinct);//拼接,去重。
GM_setValue("blockList", blockList);
// 更正已由callback处理过的dom
document.querySelectorAll("ytd-comment-renderer[user-blocked='false']").forEach((v, i) => {
commentHandler(v);
});
setTimeout(function () { alert('导入成功!'); }, 1);
} else {
setTimeout(function () { alert('错误,格式非法!'); }, 1);
}
} catch (e) {
setTimeout(function () { alert(e); }, 1);
}
}
reader.readAsText(file);
}
function blockImport() {
var input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", ".json,text/json");
input.addEventListener("change", handleFileSelect);
input.click();
}
/**
* @type {string[]}
*/
var blockList = GM_getValue("blockList");
if (!blockList) {
blockList = [];
}
GM_addStyle("ytd-comment-renderer[user-blocked='true']{height:1.8rem;opacity:0.3;overflow:hidden;box-shadow:0 0 5px #F44336;}"
+ "ytd-comment-renderer[user-blocked='true']:hover{height:auto;transition:all 1s;-ms-transition:all 1s;-o-transition:all 1s;-moz-transition:all 1s;-webkit-transition:all 1s;opacity:1;}"
+ "#block-button{width:32px;height:32px;border:none;border-radius:50%;outline:none;}"
+ "#block-button-item{cursor:pointer;width:32px;height:32px;border:none;outline:none;padding: 8px;border-radius:50%;background:none;}"
+ "#block-button-item:active{width:32px;height:32px;background: radial-gradient(#ffffff,#E2E2E2);}"
+ "#block-button-icon{fill:grey;}"
+ "#block-button-item[aria-pressed=\"true\"]>#block-button-icon{fill:#065FD4;}");
const targetNode = document.querySelector('ytd-app');
const config = { attributes: false, childList: true, subtree: true };
const localobserver = createNodeListener(targetNode, config, callback_top);
GM_registerMenuCommand("重置", blockReset, "R");
GM_registerMenuCommand("导入", blockImport, "I");
GM_registerMenuCommand("导出", blockExport, "E");
})();