// ==UserScript==
// @name YouTube User Block
// @name:en YouTube User Block
// @namespace https://github.com/ChanthMiao/
// @icon https://visualpharm.com/assets/42/Invisible-595b40b65ba036ed117d2e78.svg
// @version 1.6.2
// @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
// @grant GM_addValueChangeListener
// @grant GM_notification
// @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)) {
unblockUser(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", uiText.unblock);
} else {
block.setAttribute("title", uiText.block);
}
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", uiText.unblock);
btn.setAttribute("aria-pressed", true);
} else {
comment.setAttribute("user-blocked", false);
btnDiv.setAttribute("title", uiText.block);
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 unblockUser(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);
});
sendNotification(uiText.resetSuccess);
}
function blockExport() {
var link = document.createElement("a");
var text = encodeURIComponent(JSON.stringify(blockList, null, 2));
link.setAttribute("download", "blockList.json");
link.setAttribute("href", "data:application/json;charset=utf-8," + text);
link.click();
}
const distinct = (value, index, self) => { return self.indexOf(value) === index; };
function handleFileSelect(e) {
var files = e.target.files;
var file = files[0];
if (file.type != "application/json") {
sendNotification(uiText.InvaildFile);
return;
}
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);
});
sendNotification(uiText.ImportSuccess);
} else {
sendNotification(uiText.InvaildFile);
}
} catch (e) {
sendNotification(e);
}
}
reader.readAsText(file);
}
function blockImport() {
var input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "application/json");
input.addEventListener("change", handleFileSelect);
input.click();
}
function registerCrossTabHandler() {
if (GM_addValueChangeListener) {
GM_addValueChangeListener("blockList", (name, oldValue, newValue, remote) => {
// 监听来自其他Tab的数据变动
if (remote) {
blockList = newValue;
document.querySelectorAll("ytd-comment-renderer[user-blocked]").forEach((v, i) => {
commentHandler(v);
});
}
});
} else if (!GM_getValue("API_CHECKED")) {
GM_setValue("API_CHECKED", true);
sendNotification(uiText.apiChecked);
}
}
function sendNotification(msg) {
if (typeof GM_notification === 'function') {
// 使用Tampermonkey提供的通知API
GM_notification({ text: msg, title: scriptName, image: icon64, timeout: msgTimeout })
} else {
if (!("Notification" in window)) {
// 不支持html5的通知机制,回退至alert
setTimeout(() => { alert(msg); }, 1);
} else if (Notification.permission === "granted") {
// 已授权,创建通知
var ntf = new Notification(scriptName, { body: msg, lang: lang, icon: icon64 });
if (msgTimeout) {
setTimeout(() => { ntf.close() }, msgTimeout);
}
} else if (Notification.permission !== "denied") {
// 未明确拒绝,尝试申请通知权限
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
var ntf = new Notification(scriptName, { body: msg, lang: lang, icon: icon64 });
if (msgTimeout) {
setTimeout(() => { ntf.close() }, msgTimeout);
}
}
})
}
}
}
/**
* @type {string[]}
*/
var blockList = GM_getValue("blockList");
if (!blockList) {
blockList = [];
}
const scriptName = "YouTube User Block"
const icon64 = "data:image/png;base64,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"
const msgTimeout = 8000; //ms
const lang = (navigator.language || navigator.userLanguage);
var uiText = {
menuR: "重置",
menuI: "导入",
menuE: "导出",
block: "拉黑",
unblock: "解封",
resetSuccess: "黑名单重置成功!",
ImportSuccess: "黑名单导入成功",
InvaildFile: "错误,文件格式非法!",
apiChecked: "GM_addValueChangeListener未定义,无法同步跨标签操作!"
};
switch (lang) {
case "zh-CN":
// do nothing
break;
case "en-US":
default:
// fallback to english.
uiText = {
menuR: "Reset",
menuI: "Import",
menuE: "Export",
block: "block",
unblock: "unblock",
resetSuccess: "Blocklist resets successfully!",
ImportSuccess: "Blocklist Import successfully!",
InvaildFile: "Error, invaild file format!",
apiChecked: "GM_addValueChangeListener undefined, unable to sync cross-tab operations!"
};
}
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 };
createNodeListener(targetNode, config, callback_top);
registerCrossTabHandler();
GM_registerMenuCommand(uiText.menuR, blockReset, "R");
GM_registerMenuCommand(uiText.menuI, blockImport, "I");
GM_registerMenuCommand(uiText.menuE, blockExport, "E");
})();