// ==UserScript== // @name Bilibili-BlackList // @namespace https://github.com/HeavenTTT/bilibili-blacklist // @version 1.1.8 // @author HeavenTTT // @description Bilibili UP屏蔽插件 - 屏蔽UP主视频卡片,支持精确匹配和正则匹配,支持视频页面、分类页面、搜索页面等。 // @match *://*.bilibili.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @icon https://www.bilibili.com/favicon.ico // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/533940/Bilibili-BlackList.user.js // @updateURL https://update.greasyfork.icu/scripts/533940/Bilibili-BlackList.meta.js // ==/UserScript== (function () { "use strict"; /* * Bilibili-BlackList -- Bilibili UP屏蔽插件 * 脚本大部分代码由AI生成,作者一点都不懂JavaScript,出现bug请联系Gemini / ChatGPT / DeepSeek * this script is mainly generated by AI, the author doesn't know JavaScript at all, if there are bugs, please contact Gemini / ChatGPT / DeepSeek * 感谢你的使用 * Thank you for using this script * * 本段注释为VS code 自动生成 this is a comment generated by VS code */ //#region 常量和全局变量 // 从存储中获取黑名单 // 默认精确匹配黑名单(区分大小写) let exactMatchBlacklist = GM_getValue("exactBlacklist", [ "绝区零", "崩坏星穹铁道", "崩坏3", "原神", "米哈游miHoYo", ]); // 默认正则匹配黑名单(不区分大小写) let regexMatchBlacklist = GM_getValue("regexBlacklist", [ "王者荣耀", "和平精英", "PUBG", "绝地求生", "吃鸡", ]); // 默认标签名黑名单 let tagNameBlacklist = GM_getValue("tNameBlacklist", ["手机游戏"]); // 从存储中获取全局配置 let globalPluginConfig = GM_getValue("globalConfig", { flagInfo: true, // 启用/禁用按UP主名/标题屏蔽 flagAD: true, // 启用/禁用屏蔽一般广告 flagTName: true, // 启用/禁用按标签名屏蔽(需要API调用) flagCM: true, // 启用/禁用屏蔽cm.bilibili.com软广 flagKirby: true, // 启用/禁用被屏蔽视频的卡比覆盖模式 processQueueInterval: 200, // 处理队列中单个卡片的延迟时间(毫秒) blockScanInterval: 200, // BlockCard扫描新卡片的间隔时间(毫秒) flagHideOnLoad: true, // 启用/禁用页面加载时自动隐藏 flagVertical: true, // 启用/禁用屏蔽竖屏视频 verticalScaleThreshold: 0.7 || 0.7, // 竖屏视频的宽高比阈值(0-1) }); // UI元素(稍后初始化) let tempUnblockButton; let managerPanel; let exactMatchListElement; let regexMatchListElement; let tagNameListElement; let configListElement; let blockCountTitleElement; let blockCountDisplayElement = null; // 内部状态变量 let isShowAllVideos = false; // 是否显示全部视频卡片 let isBlockingOperationInProgress = false; // 是否正在执行BlockCard扫描操作 let lastBlockScanExecutionTime = 0; // 上次执行BlockCard扫描的时间戳 let blockedVideoCards = new Set(); // 存储已屏蔽的视频卡片元素 let processedVideoCards = new WeakSet(); // 记录已处理过的卡片(避免重复处理,包括 UP主/标题检查和 tname 获取) let videoCardProcessQueue = new Set(); // 存储待处理的卡片,用于统一的队列处理 let isVideoCardQueueProcessing = false; // 是否正在处理队列 let isPageCurrentlyActive = true; // 页面是否可见 let observerRetryCount = 0; // 观察器重试计数 let countBlockInfo = 0; // 已屏蔽视频计数 let countBlockAD = 0; // 已屏蔽广告计数 let countBlockTName = 0; // 已屏蔽标签名计数 let countBlockCM = 0; // 已屏蔽cm.bilibili.com软广计数 // 用于不同页面URL选择器 // 用于不同页面UP主名称选择器 const UP_NAME_SELECTORS = [ ".bili-video-card__info--author", // 主页 ".bili-video-card__author", // 分类页面 -> span title ".name", // 视频播放页 ]; // 用于不同页面视频标题选择器 const VIDEO_TITLE_SELECTORS = [ ".bili-video-card__info--tit", // 主页 ".bili-video-card__title", // 分类页面 -> span title ".title", // 视频播放页 ]; //#endregion //#region 存储管理 // 将黑名单保存到存储中 function saveBlacklistsToStorage() { GM_setValue("exactBlacklist", exactMatchBlacklist); GM_setValue("regexBlacklist", regexMatchBlacklist); GM_setValue("tNameBlacklist", tagNameBlacklist); } // 将全局配置保存到存储中 function saveGlobalConfigToStorage() { GM_setValue("globalConfig", globalPluginConfig); } //#endregion //#region 核心屏蔽功能 /** * 为视频卡片添加屏蔽按钮容器。 * @param {string} upName - UP主名称。 * @param {HTMLElement} cardElement - 视频卡片元素。 * @returns {HTMLElement} 创建的容器元素。 */ function addBlockContainerToCard(upName, cardElement) { if (!cardElement.querySelector(".bilibili-blacklist-block-container")) { const container = document.createElement("div"); container.classList.add("bilibili-blacklist-block-container"); if (!cardElement.querySelector(".bilibili-blacklist-block-btn")) { const blockButton = createBlockUpButton(upName, cardElement); if (isCurrentPageVideo()) { // 视频播放页面的视频卡片结构特殊,需要调整位置 cardElement.querySelector(".card-box").style.position = "relative"; cardElement.querySelector(".card-box").appendChild(container); } else if (isCurrentPageCategory()) { // 分类页面的视频卡片结构特殊,需要调整位置 cardElement.querySelector(".bili-video-card").appendChild(container); } else { cardElement.appendChild(container); } container.appendChild(blockButton); } return cardElement.querySelector(".bilibili-blacklist-block-container"); } return cardElement.querySelector(".bilibili-blacklist-block-container"); } /** * 隐藏给定的视频卡片。 * @param {HTMLElement} cardElement - 要隐藏的视频卡片元素。 * @param {string} tpye - 隐藏类型,默认为"info"。 * @returns {void} * */ function hideVideoCard(cardElement, type = "none") { const realCardToBlock = getRealVideoCardElement(cardElement); if (!blockedVideoCards.has(realCardToBlock)) { blockedVideoCards.add(realCardToBlock); } else { return; } if (!realCardToBlock) { console.warn( "[bililili-blacklist] hideVideoCard: realCardToBlock is null" ); return; } if (type === "info") { countBlockInfo++; } if (type === "ad") { countBlockAD++; } if (type === "tname") { countBlockTName++; } if (type === "cm") { countBlockCM++; } if (type === "vertical") { countBlockTName++; } //console.log(tpye); if (globalPluginConfig.flagKirby) { addKirbyOverlayToCard(cardElement); } else { realCardToBlock.style.display = "none"; } } /** * 获取应该被屏蔽的卡片的真正父元素。 * @param {HTMLElement} cardElement - 视频卡片元素。 * @returns {HTMLElement} 应用显示更改的实际元素。 */ function getRealVideoCardElement(cardElement) { // 搜索页面的视频卡片父元素是上一级 if (isCurrentPageSearch()) { return cardElement.parentElement; } // 主页视频卡片可能有多层父元素 if (isCurrentPageMain()) { if (cardElement.parentElement.classList.contains("bili-feed-card")) { cardElement = cardElement.parentElement; if (cardElement.parentElement.classList.contains("feed-card")) { cardElement = cardElement.parentElement; } } } return cardElement; } /** * 根据当前页面选择所有视频卡片。 * @returns {NodeListOf | null} 视频卡片元素的NodeList,如果不是识别的页面则返回null。 */ function queryAllVideoCards() { if (isCurrentPageMain()) { return document.querySelectorAll(".bili-video-card"); } else if (isCurrentPageVideo()) { return document.querySelectorAll(".video-page-card-small"); } else if (isCurrentPageCategory()) { return document.querySelectorAll(".feed-card"); } else if (isCurrentPageSearch()) { return document.querySelectorAll(".bili-video-card"); } return null; } /** * 扫描并处理视频卡片进行屏蔽。 */ function scanAndBlockVideoCards() { const now = Date.now(); // 限制扫描频率,防止性能问题 if ( isBlockingOperationInProgress || now - lastBlockScanExecutionTime < globalPluginConfig.blockScanInterval ) { return; } isBlockingOperationInProgress = true; lastBlockScanExecutionTime = now; try { const videoCards = queryAllVideoCards(); if (!videoCards) return; videoCards.forEach((card) => { // 如果卡片已经处理过,则跳过 if (processedVideoCards.has(card)) { return; } const { upName, videoTitle } = getVideoCardInfo(card); // 如果获取到UP主名称和视频标题,则添加屏蔽按钮 if (upName && videoTitle) { addBlockContainerToCard(upName, card); // --- 根据 flagHideOnLoad 开关决定是否立即隐藏卡片 --- const realCard = getRealVideoCardElement(card); if (globalPluginConfig.flagHideOnLoad && !isShowAllVideos) { // 只有在“显示全部”模式关闭时才执行 if (globalPluginConfig.flagKirby) { addKirbyOverlayToCard(card); // 卡比模式下添加遮罩 realCard.style.display = "block"; // 确保卡片本身是显示的 } else { realCard.style.display = "none"; // 非卡比模式下直接隐藏 } } } // --- 立即隐藏卡片的逻辑结束 --- // 将卡片添加到处理队列 videoCardProcessQueue.add(card); }); // 如果队列中有待处理的卡片且当前未在处理中,则开始处理队列 if (videoCardProcessQueue.size > 0 && !isVideoCardQueueProcessing) { processVideoCardQueue(); } // 刷新屏蔽计数显示 refreshBlockCountDisplay(); // 修正主页布局 fixMainPageLayout(); } finally { isBlockingOperationInProgress = false; } } /** * 修正主页在屏蔽后的布局。 */ function fixMainPageLayout() { if (!isCurrentPageMain()) return; const container = document.querySelector( ".recommended-container_floor-aside .container" ); if (container) { const children = container.children; let visibleIndex = 0; // 调整可见卡片的边距,使布局更紧凑 for (let i = 0; i < children.length; i++) { const child = children[i]; if (child.style.display !== "none") { if (visibleIndex <= 6) { child.style.marginTop = "0px"; } else if (visibleIndex < 12) { child.style.marginTop = "24px"; } else { break; } visibleIndex++; } } } } /** * 切换所有被屏蔽视频卡片的显示。 */ function toggleShowAllBlockedVideos() { isShowAllVideos = !isShowAllVideos; blockedVideoCards.forEach((card) => { if (globalPluginConfig.flagKirby) { const kirbyOverlay = card.querySelector("#bilibili-blacklist-kirby"); if (kirbyOverlay) { kirbyOverlay.style.display = isShowAllVideos ? "none" : "block"; } card.style.display = "block"; } else { card.style.display = isShowAllVideos ? "block" : "none"; } }); tempUnblockButton.textContent = isShowAllVideos ? "恢复屏蔽" : "取消屏蔽"; tempUnblockButton.style.background = isShowAllVideos ? "#dddddd" : "#fb7299"; } /** * 从视频卡片中检索UP主名称和视频标题。 * @param {HTMLElement} cardElement - 视频卡片元素。 * @returns {{upName: string, videoTitle: string}} 包含UP主名称和视频标题的对象。 */ function getVideoCardInfo(cardElement) { let upName = ""; let videoTitle = ""; const upNameElements = cardElement.querySelectorAll( UP_NAME_SELECTORS.join(", ") ); if (upNameElements.length > 0) { upName = upNameElements[0].textContent.trim(); if (isCurrentPageCategory()) { // 分类页面的UP主名称可能包含其他信息,需要进一步处理 upName = upName.split(" · ")[0].trim(); } } const titleElements = cardElement.querySelectorAll( VIDEO_TITLE_SELECTORS.join(", ") ); if (titleElements.length > 0) { videoTitle = titleElements[0].textContent.trim(); } return { upName, videoTitle }; } /** * 检查UP主名称或标题是否在黑名单中。 * @param {string} upName - 要检查的UP主名称。 * @param {string} title - 要检查的视频标题。 * @returns {boolean} 如果在黑名单中则返回true,否则返回false。 */ function isBlacklisted(upName, title) { const lowerCaseUpName = upName.toLowerCase(); // 检查精确匹配黑名单 if ( exactMatchBlacklist.some((item) => item.toLowerCase() === lowerCaseUpName) ) { return true; } // 检查正则匹配黑名单 if ( regexMatchBlacklist.some((regex) => new RegExp(regex, "i").test(upName)) ) { return true; } if ( regexMatchBlacklist.some((regex) => new RegExp(regex, "i").test(title)) ) { return true; } return false; } /** * 将UP主名称添加到精确匹配黑名单并刷新。 * @param {string} upName - 要添加的UP主名称。 * @param {HTMLElement} [cardElement=null] - 添加后要隐藏的视频卡片元素。 */ function addToExactBlacklist(upName, cardElement = null) { try { if (!upName) return; if (!exactMatchBlacklist.includes(upName)) { exactMatchBlacklist.push(upName); saveBlacklistsToStorage(); refreshAllPanelTabs(); if (cardElement) { hideVideoCard(cardElement); } } } catch (e) { console.error("[bilibili-blacklist] 添加黑名单出错:", e); } } /** * 从精确匹配黑名单中移除UP主名称。 * @param {string} upName - 要移除的UP主名称。 */ function removeFromExactBlacklist(upName) { try { if (exactMatchBlacklist.includes(upName)) { const index = exactMatchBlacklist.indexOf(upName); exactMatchBlacklist.splice(index, 1); saveBlacklistsToStorage(); refreshExactMatchList(); } } catch (e) { console.error("[bilibili-blacklist] 移除黑名单出错:", e); } } /** * 将标签名添加到黑名单并刷新。 * @param {string} tagName - 要添加的标签名。 * @param {HTMLElement} [cardElement=null] - 添加后要隐藏的视频卡片元素。 */ function addToTagNameBlacklist(tagName, cardElement = null) { try { if (!tagName) { return; } if (!tagNameBlacklist.includes(tagName)) { tagNameBlacklist.push(tagName); saveBlacklistsToStorage(); refreshAllPanelTabs(); if (cardElement) { hideVideoCard(cardElement); } } } catch (e) { console.error("[bilibili-blacklist] 添加标签黑名单出错:", e); } } /** * 从黑名单中移除标签名。 * @param {string} tagName - 要移除的标签名。 */ function removeFromTagNameBlacklist(tagName) { try { if (tagNameBlacklist.includes(tagName)) { const index = tagNameBlacklist.indexOf(tagName); tagNameBlacklist.splice(index, 1); saveBlacklistsToStorage(); refreshTagNameList(); } } catch (e) { console.error("[bilibili-blacklist] 移除标签黑名单出错:", e); } } //#endregion //#region 视频数据获取 /** * 获取视频卡片的链接。 * @param {HTMLElement} cardElement - 视频卡片元素。 * @returns {string|null} 视频链接,如果未找到则返回null。 */ function getCardHrefLink(cardElement) { const hrefLink = cardElement.querySelector("a"); if (hrefLink) { return hrefLink.getAttribute("href"); } return null; } function checkLinkCM(link) { if (!link) return false; // 如果是cm.bilibili.com的链接,且启用了CM广告屏蔽,则隐藏卡片 if (link.match(/cm.bilibili.com/) && globalPluginConfig.flagCM) { return true; } return false; } /** * 从视频卡片的链接中提取BV ID。 * 还处理cm.bilibili.com广告的屏蔽。 * @param {HTMLElement} cardElement - 视频卡片元素。 * @returns {string|null} BV ID,如果未找到/被屏蔽则返回null。 */ function getLinkBvId(link) { try { if (!link) { return null; } else { const bv = link.match(/BV\w+/); return bv ? bv[0] : null; } } catch (e) { return null; } } /** * 使用BV ID从Bilibili API获取视频信息。 * @param {string} bvid - 视频的BV ID。 * @returns {Promise} 解析为视频数据或null的Promise。 */ async function getBilibiliVideoApiData(bvid) { if (!bvid || bvid.length >= 24) { return null; } const url = `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`; try { const response = await fetch(url); const json = await response.json(); if (json.code === 0) { return json.data; } else { return null; } } catch (error) { console.error("[bilibili-blacklist] API 请求失败:", error); } } /** * 检查卡片是否包含任何黑名单标签。 * @param {HTMLElement} cardElement - 视频卡片元素。 * @returns {boolean} 如果有任何标签被列入黑名单,则返回true,否则返回false。 */ function isCardBlacklistedByTagName(cardElement) { const tnameGroup = cardElement.querySelector( ".bilibili-blacklist-tname-group" ); if (tnameGroup) { const tnameElements = tnameGroup.querySelectorAll( ".bilibili-blacklist-tname" ); for (const tnameElement of tnameElements) { const tname = tnameElement.textContent.trim(); if (tagNameBlacklist.includes(tname)) { return true; } } } return false; } /** * 处理视频卡片队列进行屏蔽。 */ async function processVideoCardQueue() { if (isVideoCardQueueProcessing) return; isVideoCardQueueProcessing = true; while (videoCardProcessQueue.size > 0) { // 如果页面不可见,则暂停处理 if (!isPageCurrentlyActive) { await sleep(1000); continue; } const iterator = videoCardProcessQueue.values(); const card = iterator.next().value; videoCardProcessQueue.delete(card); if (!card || processedVideoCards.has(card)) { continue; } let shouldHide = false; let blockType = "none"; // 如果启用了标签屏蔽且当前卡片未被隐藏 const link = getCardHrefLink(card); if (checkLinkCM(link)) { shouldHide = true; blockType = "cm"; } const { upName, videoTitle } = getVideoCardInfo(card); if (upName && videoTitle && !shouldHide) { // 如果UP主名称或标题在黑名单中,且启用了信息屏蔽 if (isBlacklisted(upName, videoTitle) && globalPluginConfig.flagInfo) { shouldHide = true; blockType = "info"; } } else { // 如果无法获取UP主名称和标题,但卡片已被隐藏或有Kirby覆盖,则也认为应该隐藏 if ( getRealVideoCardElement(card).style.display === "none" && !globalPluginConfig.flagKirby ) { shouldHide = true; } else if ( getRealVideoCardElement(card).querySelector( "#bilibili-blacklist-kirby" ) ) { shouldHide = true; } } if ( (globalPluginConfig.flagTName || globalPluginConfig.flagVertical) && !shouldHide ) { const bvId = getLinkBvId(link); // 如果存在BV ID且卡片尚未添加标签组 if (bvId && !card.querySelector(".bilibili-blacklist-tname-group")) { const data = await getBilibiliVideoApiData(bvId); if (data) { const container = card.querySelector( ".bilibili-blacklist-block-container" ); if (container) { const tnameGroup = document.createElement("div"); tnameGroup.className = "bilibili-blacklist-tname-group"; let hasTname = false; if (data.tname) { const btn = createTNameBlockButton(data.tname, card); tnameGroup.appendChild(btn); hasTname = true; } if (data.tname_v2) { const tnameElement = createTNameBlockButton( data.tname_v2, card ); tnameGroup.appendChild(tnameElement); hasTname = true; } if (hasTname) { container.appendChild(tnameGroup); } } if (isCardBlacklistedByTagName(card)) { shouldHide = true; blockType = "tname"; } // 如果启用了垂直视频屏蔽 if ( data.dimension.width && data.dimension.height && !shouldHide && globalPluginConfig.flagVertical ) { const dimension = data.dimension.width / data.dimension.height; if (dimension < globalPluginConfig.verticalScaleThreshold) { shouldHide = true; blockType = "vertical"; } } } } } if (shouldHide) { hideVideoCard(card, blockType); } else { const realCardToDisplay = getRealVideoCardElement(card); if (blockedVideoCards.has(realCardToDisplay)) { blockedVideoCards.delete(realCardToDisplay); } if (globalPluginConfig.flagKirby) { removeKirbyOverlay(card); } realCardToDisplay.style.display = "block"; } processedVideoCards.add(card); // 标记卡片已处理 await sleep(globalPluginConfig.processQueueInterval || 100); } isVideoCardQueueProcessing = false; refreshBlockCountDisplay(); } // 异步等待函数 function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } //#endregion //#region 页面可见性和焦点监听器 // 监听页面可见性变化 document.addEventListener("visibilitychange", () => { isPageCurrentlyActive = !document.hidden; }); // 监听窗口焦点获取 window.addEventListener("focus", () => { isPageCurrentlyActive = true; }); // 监听窗口焦点失去 window.addEventListener("blur", () => { isPageCurrentlyActive = false; }); //#endregion //#region UI元素创建和管理 /** * 为UP主创建屏蔽按钮,显示在视频卡片上。 * @param {string} upName - UP主名称。 * @param {HTMLElement} cardElement - 视频卡片元素。 * @returns {HTMLDivElement} 创建的按钮元素。 */ function createBlockUpButton(upName, cardElement) { const button = document.createElement("div"); button.className = "bilibili-blacklist-block-btn"; button.innerHTML = "屏蔽"; button.title = `屏蔽: ${upName}`; button.addEventListener("click", (e) => { e.stopPropagation(); // 阻止事件冒泡,防止触发视频点击事件 addToExactBlacklist(upName, cardElement); }); return button; } /** * 为标签名创建屏蔽按钮,显示在视频卡片上。 * @param {string} tagName - 标签名。 * @param {HTMLElement} cardElement - 视频卡片元素。 * @returns {HTMLSpanElement} 创建的按钮元素。 */ function createTNameBlockButton(tagName, cardElement) { const button = document.createElement("span"); button.className = "bilibili-blacklist-tname"; button.innerHTML = `${tagName}`; button.title = `屏蔽: ${tagName}`; button.addEventListener("click", (e) => { e.stopPropagation(); // 阻止事件冒泡 addToTagNameBlacklist(tagName, cardElement); }); return button; } /** * 将黑名单管理器按钮添加到右侧导航条。 */ function addBlacklistManagerButton() { const rightEntry = document.querySelector(".right-entry"); if (!rightEntry) { console.warn("[bilibili-blacklist] 未找到右侧导航栏"); return; } else if ( !rightEntry.querySelector("#bilibili-blacklist-manager-button") ) { const listItem = document.createElement("li"); listItem.id = "bilibili-blacklist-manager-button"; listItem.style.cursor = "pointer"; listItem.className = "v-popover-wrap"; const button = document.createElement("div"); button.className = "right-entry-item"; button.style.display = "flex"; button.style.flexDirection = "column"; button.style.alignItems = "center"; button.style.justifyContent = "center"; const icon = document.createElement("div"); icon.className = "right-entry__outside"; icon.innerHTML = getKirbySVG(); // 获取卡比SVG图标 icon.style.marginBottom = "-5px"; blockCountDisplayElement = document.createElement("span"); blockCountDisplayElement.textContent = `0`; button.appendChild(icon); button.appendChild(blockCountDisplayElement); listItem.appendChild(button); // 将按钮插入到导航栏的特定位置 if (rightEntry.children.length > 1) { rightEntry.insertBefore(listItem, rightEntry.children[1]); } else { rightEntry.appendChild(listItem); } // 点击按钮显示/隐藏管理面板 listItem.addEventListener("click", () => { if (managerPanel.style.display === "none") { managerPanel.style.display = "flex"; } else { managerPanel.style.display = "none"; } }); } } /** * 更新已屏蔽视频的显示计数。 */ function refreshBlockCountDisplay() { if (blockCountDisplayElement) { blockCountDisplayElement.textContent = `${blockedVideoCards.size}`; } countBlockInfo; if (blockCountTitleElement) { blockCountTitleElement.textContent = `已屏蔽视频 (${blockedVideoCards.size} = ${countBlockInfo} + ${countBlockAD} + ${countBlockCM} + ${countBlockTName})`; } } // 辅助函数:创建通用按钮 function createPanelButton(text, bgColor, onClick) { const button = document.createElement("button"); button.textContent = text; button.style.padding = "4px 8px"; button.style.background = bgColor; button.style.color = "#fff"; button.style.border = "none"; button.style.borderRadius = "4px"; button.style.cursor = "pointer"; button.addEventListener("click", onClick); return button; } // 辅助函数:为黑名单面板创建列表项 function createBlacklistListItem(contentText, onRemoveClick) { const item = document.createElement("li"); item.style.display = "flex"; item.style.justifyContent = "space-between"; item.style.alignItems = "center"; item.style.padding = "8px 0"; item.style.borderBottom = "1px solid #f1f2f3"; const content = document.createElement("span"); content.textContent = contentText; content.style.flex = "1"; const removeBtn = createPanelButton("移除", "#f56c6c", onRemoveClick); item.appendChild(content); item.appendChild(removeBtn); return item; } /** * 刷新面板中的精确匹配黑名单显示。 */ function refreshExactMatchList() { if (!exactMatchListElement) { if (!isBlacklistPanelCreated()) { return; } exactMatchListElement = document.querySelector( "#bilibili-blacklist-exact-list" ); if (!exactMatchListElement) { console.warn("[Bilibili-Blacklist] exactMatchListElement 未定义"); return; } } exactMatchListElement.innerHTML = ""; exactMatchBlacklist.forEach((upName) => { const item = createBlacklistListItem(upName, () => { removeFromExactBlacklist(upName); }); exactMatchListElement.appendChild(item); }); // 反转列表顺序,使最新添加的显示在顶部 Array.from(exactMatchListElement.children) .reverse() .forEach((item) => exactMatchListElement.appendChild(item)); if (exactMatchBlacklist.length === 0) { const empty = document.createElement("div"); empty.textContent = "暂无精确匹配屏蔽UP主"; empty.style.textAlign = "center"; empty.style.padding = "16px"; empty.style.color = "#999"; exactMatchListElement.appendChild(empty); } } /** * 刷新面板中的正则匹配黑名单显示。 */ function refreshRegexMatchList() { if (!regexMatchListElement) { if (!isBlacklistPanelCreated()) { return; } regexMatchListElement = document.querySelector( "#bilibili-blacklist-regex-list" ); if (!regexMatchListElement) { console.warn("[Bilibili-Blacklist] regexMatchListElement 未定义"); return; } } regexMatchListElement.innerHTML = ""; regexMatchBlacklist.forEach((regex, index) => { const item = createBlacklistListItem(regex, () => { regexMatchBlacklist.splice(index, 1); saveBlacklistsToStorage(); refreshRegexMatchList(); }); regexMatchListElement.appendChild(item); }); // 反转列表顺序,使最新添加的显示在顶部 Array.from(regexMatchListElement.children) .reverse() .forEach((item) => regexMatchListElement.appendChild(item)); if (regexMatchBlacklist.length === 0) { const empty = document.createElement("div"); empty.textContent = "暂无正则匹配屏蔽规则"; empty.style.textAlign = "center"; empty.style.padding = "16px"; empty.style.color = "#999"; regexMatchListElement.appendChild(empty); } } /** * 刷新面板中的标签名黑名单显示。 */ function refreshTagNameList() { if (!tagNameListElement) { if (!isBlacklistPanelCreated()) { return; } tagNameListElement = document.querySelector( "#bilibili-blacklist-tname-list" ); if (!tagNameListElement) { console.warn("[Bilibili-Blacklist] tagNameListElement 未定义"); return; } } tagNameListElement.innerHTML = ""; tagNameBlacklist.forEach((tagName) => { const item = createBlacklistListItem(tagName, () => { removeFromTagNameBlacklist(tagName); }); tagNameListElement.appendChild(item); }); // 反转列表顺序,使最新添加的显示在顶部 Array.from(tagNameListElement.children) .reverse() .forEach((item) => tagNameListElement.appendChild(item)); if (tagNameBlacklist.length === 0) { const empty = document.createElement("div"); empty.textContent = "暂无标签屏蔽规则"; empty.style.textAlign = "center"; empty.style.padding = "16px"; empty.style.color = "#999"; tagNameListElement.appendChild(empty); } } // 辅助函数:为设置创建切换按钮 function createSettingToggleButton(labelText, configKey, title = null) { const container = document.createElement("div"); container.style.display = "flex"; container.style.alignItems = "center"; container.style.marginBottom = "8px"; container.style.gap = "8px"; container.title = title; // 设置鼠标悬停提示 const label = document.createElement("span"); label.textContent = labelText; label.style.flex = "1"; const button = document.createElement("button"); button.style.padding = "6px 12px"; button.style.border = "none"; button.style.borderRadius = "4px"; button.style.cursor = "pointer"; button.style.color = "#fff"; function refreshButtonAppearance() { button.textContent = globalPluginConfig[configKey] ? "开启" : "关闭"; button.style.backgroundColor = globalPluginConfig[configKey] ? "#fb7299" : "#909399"; } button.addEventListener("click", () => { globalPluginConfig[configKey] = !globalPluginConfig[configKey]; refreshButtonAppearance(); saveGlobalConfigToStorage(); }); refreshButtonAppearance(); // 初始化按钮外观 container.appendChild(label); container.appendChild(button); return container; } // 辅助函数:为设置创建输入文本 function createSettingInput(labelText, configKey, title = null) { // 卡片扫描间隔设置 const Container = document.createElement("div"); Container.style.display = "flex"; Container.style.alignItems = "center"; Container.style.marginTop = "16px"; Container.style.gap = "8px"; Container.title = title; const Label = document.createElement("span"); Label.textContent = labelText; Label.style.flex = "1"; const Input = document.createElement("input"); Input.type = "number"; Input.min = "0"; Input.value = globalPluginConfig[configKey]; Input.style.width = "100px"; Input.style.padding = "6px"; Input.style.border = "1px solid #ddd"; Input.style.borderRadius = "4px"; const Button = document.createElement("button"); Button.textContent = "保存"; Button.style.padding = "6px 12px"; Button.style.backgroundColor = "#fb7299"; Button.style.color = "#fff"; Button.style.border = "none"; Button.style.borderRadius = "4px"; Button.style.cursor = "pointer"; Button.addEventListener("click", () => { const val = parseFloat(Input.value, 10); if (!isNaN(val) && val >= 0) { globalPluginConfig[configKey] = val; saveGlobalConfigToStorage(); } else { alert("请输入有效的非负数字!"); } }); Container.appendChild(Label); Container.appendChild(Input); Container.appendChild(Button); return Container; } /** * 刷新面板中的配置设置显示。 */ function refreshConfigSettings() { if (!configListElement) { if (!isBlacklistPanelCreated()) { return; } configListElement = document.querySelector( "#bilibili-blacklist-config-list" ); if (!configListElement) { console.warn("[Bilibili-Blacklist] configListElement 未定义"); return; } } configListElement.innerHTML = ""; // 临时开关按钮 const tempToggleContainer = document.createElement("div"); tempToggleContainer.style.display = "flex"; tempToggleContainer.style.alignItems = "center"; tempToggleContainer.style.marginBottom = "8px"; tempToggleContainer.style.gap = "8px"; tempToggleContainer.style.margin = "20px 0"; const tempToggleLabel = document.createElement("span"); tempToggleLabel.textContent = "临时开关"; tempToggleLabel.style.flex = "1"; tempUnblockButton = document.createElement("button"); tempUnblockButton.textContent = isShowAllVideos ? "恢复屏蔽" : "取消屏蔽"; tempUnblockButton.style.background = isShowAllVideos ? "#dddddd" : "#fb7299"; tempUnblockButton.style.padding = "6px 12px"; tempUnblockButton.style.border = "none"; tempUnblockButton.style.cursor = "pointer"; tempUnblockButton.style.color = "#fff"; tempUnblockButton.addEventListener("click", toggleShowAllBlockedVideos); tempToggleContainer.appendChild(tempToggleLabel); tempToggleContainer.appendChild(tempUnblockButton); configListElement.appendChild(tempToggleContainer); const title = document.createElement("h4"); title.textContent = "全局配置开关(部分功能刷新后生效)"; title.style.fontWeight = "bold"; title.style.marginBottom = "12px"; configListElement.appendChild(title); // 添加配置切换按钮 configListElement.appendChild( createSettingToggleButton( "屏蔽标题/Up主名", "flagInfo", "屏蔽标题/Up主名" ) ); configListElement.appendChild( createSettingToggleButton( "屏蔽分类标签", "flagTName", "通过请求API获取分类标签" ) ); configListElement.appendChild( createSettingToggleButton( "屏蔽竖屏视频", "flagVertical", "通过请求API获取视频分辨率" ) ); configListElement.appendChild( createSettingToggleButton("屏蔽主页推荐", "flagAD", "直播/广告/分区推送") ); configListElement.appendChild( createSettingToggleButton( "屏蔽主页视频软广", "flagCM", "cm.bilibili.com软广" ) ); //分割线 const hr = document.createElement("hr"); hr.style.margin = "12px 0"; hr.style.border = "none"; hr.style.borderTop = "2px solid #ddd"; configListElement.appendChild(hr); configListElement.appendChild( createSettingToggleButton("遮挡被屏蔽视频", "flagKirby", "更加温和的方式") ); configListElement.appendChild( createSettingToggleButton( "加载时立即隐藏卡片", "flagHideOnLoad", "新卡片加载出来时是否立即隐藏,待处理完成后再决定显示或继续屏蔽。关闭此功能可能会导致卡片先显示后隐藏的闪烁。" ) ); configListElement.appendChild( createSettingInput( "卡片扫描间隔 (ms):", "blockScanInterval", "扫描新卡片的间隔时间,单位 ms。值越小,新卡片隐藏越快,但可能会增加CPU负担。建议值 200ms。" ) ); configListElement.appendChild( createSettingInput( "视频信息API请求间隔 (ms):", "processQueueInterval", "每个视频获取分类标签/视频分辨率时的API请求间隔时间,单位 ms。间隔时间越长,越不容易触发B站API限速。建议值 200ms。" ) ); configListElement.appendChild( createSettingInput( "竖屏视频比例阈值:", "verticalScaleThreshold", "获取的视频API信息后,判断视频是否为竖屏(长 除于 宽)的阈值。建议值 0.7。" ) ); } /** * 刷新黑名单管理面板中的所有标签页。 */ function refreshAllPanelTabs() { refreshExactMatchList(); refreshRegexMatchList(); refreshTagNameList(); refreshConfigSettings(); } /** * 检查黑名单管理面板是否已创建并存在于DOM中。 * 如果找到,则设置全局 `managerPanel` 引用。 * @returns {boolean} 如果面板存在则返回true,否则返回false。 */ function isBlacklistPanelCreated() { const panelInDom = document.querySelector( "#bilibili-blacklist-manager-panel" ); if (panelInDom) { if (!managerPanel) { managerPanel = panelInDom; } return true; } return false; } /** * 创建黑名单管理面板。 */ function createBlacklistPanel() { if (isBlacklistPanelCreated()) { return; } managerPanel = document.createElement("div"); managerPanel.id = "bilibili-blacklist-manager-panel"; // 确保ID唯一 // 设置面板样式 managerPanel.style.position = "fixed"; managerPanel.style.top = "50%"; managerPanel.style.left = "50%"; managerPanel.style.transform = "translate(-50%, -50%)"; managerPanel.style.width = "500px"; managerPanel.style.maxHeight = "80vh"; managerPanel.style.backgroundColor = "#fff"; managerPanel.style.borderRadius = "8px"; managerPanel.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)"; managerPanel.style.zIndex = "99999"; managerPanel.style.overflow = "hidden"; managerPanel.style.display = "none"; // 默认隐藏 managerPanel.style.flexDirection = "column"; managerPanel.style.backgroundColor = "#ffffffee"; // 半透明背景 // 创建标签容器 const tabContainer = document.createElement("div"); tabContainer.style.display = "flex"; tabContainer.style.borderBottom = "1px solid #f1f2f3"; // 创建各个标签页的内容区域 const exactContent = document.createElement("div"); exactContent.style.padding = "16px"; exactContent.style.overflowY = "auto"; exactContent.style.flex = "1"; exactContent.style.display = "block"; // 默认显示精确匹配 const regexContent = document.createElement("div"); regexContent.style.padding = "16px"; regexContent.style.overflowY = "auto"; regexContent.style.flex = "1"; regexContent.style.display = "none"; const tnameContent = document.createElement("div"); tnameContent.style.padding = "16px"; tnameContent.style.overflowY = "auto"; tnameContent.style.flex = "1"; tnameContent.style.display = "none"; const configContent = document.createElement("div"); configContent.style.padding = "16px"; configContent.style.overflowY = "auto"; configContent.style.flex = "1"; configContent.style.display = "none"; // 定义标签页数据 const tabs = [ { name: "精确匹配(Up名字)", content: exactContent }, { name: "正则匹配(Up/标题)", content: regexContent }, { name: "屏蔽分类", content: tnameContent }, { name: "插件配置", content: configContent }, ]; tabs.forEach((tabData) => { const tab = document.createElement("div"); tab.textContent = tabData.name; tab.style.padding = "12px 16px"; tab.style.cursor = "pointer"; tab.style.fontWeight = "500"; tab.style.borderBottom = tabData.content.style.display === "block" ? "2px solid #fb7299" : "none"; // 标签点击事件,切换内容显示 tab.addEventListener("click", () => { tabs.forEach(({ tab: t, content: c }) => { t.style.borderBottom = "none"; c.style.display = "none"; }); tab.style.borderBottom = "2px solid #fb7299"; tabData.content.style.display = "block"; }); tabData.tab = tab; // 保存对标签元素的引用 tabContainer.appendChild(tab); }); // 创建面板头部 const header = document.createElement("div"); header.style.padding = "16px"; header.style.borderBottom = "1px solid #f1f2f3"; header.style.display = "flex"; header.style.justifyContent = "space-between"; header.style.alignItems = "center"; blockCountTitleElement = document.createElement("h3"); blockCountTitleElement.style.margin = "0"; blockCountTitleElement.style.fontWeight = "500"; blockCountTitleElement.title = "总数 =(UP/标题 + 广告 + CM + 分类/竖屏)"; const closeBtn = document.createElement("button"); closeBtn.textContent = "×"; closeBtn.style.background = "none"; closeBtn.style.border = "none"; closeBtn.style.cursor = "pointer"; closeBtn.style.padding = "0 8px"; closeBtn.addEventListener("click", () => { managerPanel.style.display = "none"; }); header.appendChild(blockCountTitleElement); header.appendChild(closeBtn); const contentContainer = document.createElement("div"); contentContainer.style.display = "flex"; contentContainer.style.flexDirection = "column"; contentContainer.style.flex = "1"; contentContainer.style.overflow = "hidden"; // 精确匹配添加输入框和按钮 const addExactContainer = document.createElement("div"); addExactContainer.style.display = "flex"; addExactContainer.style.marginBottom = "16px"; addExactContainer.style.gap = "8px"; const exactInput = document.createElement("input"); exactInput.type = "text"; exactInput.placeholder = "输入要屏蔽的UP主名称"; exactInput.style.flex = "1"; exactInput.style.padding = "8px"; exactInput.style.border = "1px solid #ddd"; exactInput.style.borderRadius = "4px"; const addExactBtn = document.createElement("button"); addExactBtn.textContent = "添加"; addExactBtn.style.padding = "8px 16px"; addExactBtn.style.background = "#fb7299"; addExactBtn.style.color = "#fff"; addExactBtn.style.border = "none"; addExactBtn.style.borderRadius = "4px"; addExactBtn.style.cursor = "pointer"; addExactBtn.addEventListener("click", () => { const upName = exactInput.value.trim(); if (upName) { addToExactBlacklist(upName); exactInput.value = ""; } }); addExactContainer.appendChild(exactInput); addExactContainer.appendChild(addExactBtn); exactContent.appendChild(addExactContainer); // 正则匹配添加输入框和按钮 const addRegexContainer = document.createElement("div"); addRegexContainer.style.display = "flex"; addRegexContainer.style.marginBottom = "16px"; addRegexContainer.style.gap = "8px"; const regexInput = document.createElement("input"); regexInput.type = "text"; regexInput.placeholder = "输入正则表达式 (如: 小小.*Official)"; regexInput.style.flex = "1"; regexInput.style.padding = "8px"; regexInput.style.border = "1px solid #ddd"; regexInput.style.borderRadius = "4px"; const addRegexBtn = document.createElement("button"); addRegexBtn.textContent = "添加"; addRegexBtn.style.padding = "8px 16px"; addRegexBtn.style.background = "#fb7299"; addRegexBtn.style.color = "#fff"; addRegexBtn.style.border = "none"; addRegexBtn.style.borderRadius = "4px"; addRegexBtn.style.cursor = "pointer"; addRegexBtn.addEventListener("click", () => { const regex = regexInput.value.trim(); if (regex && !regexMatchBlacklist.includes(regex)) { try { new RegExp(regex); // 验证正则表达式 regexMatchBlacklist.push(regex); saveBlacklistsToStorage(); regexInput.value = ""; refreshRegexMatchList(); } catch (e) { alert("无效的正则表达式: " + e.message); } } }); addRegexContainer.appendChild(regexInput); addRegexContainer.appendChild(addRegexBtn); regexContent.appendChild(addRegexContainer); // 创建列表元素 exactMatchListElement = document.createElement("ul"); exactMatchListElement.id = "bilibili-blacklist-exact-list"; exactMatchListElement.style.listStyle = "none"; exactMatchListElement.style.padding = "0"; exactMatchListElement.style.margin = "0"; regexMatchListElement = document.createElement("ul"); regexMatchListElement.id = "bilibili-blacklist-regex-list"; regexMatchListElement.style.listStyle = "none"; regexMatchListElement.style.padding = "0"; regexMatchListElement.style.margin = "0"; tagNameListElement = document.createElement("ul"); tagNameListElement.id = "bilibili-blacklist-tname-list"; tagNameListElement.style.listStyle = "none"; tagNameListElement.style.padding = "0"; tagNameListElement.style.margin = "0"; configListElement = document.createElement("ul"); configListElement.id = "bilibili-blacklist-config-list"; configListElement.style.listStyle = "none"; configListElement.style.padding = "0"; configListElement.style.margin = "0"; refreshAllPanelTabs(); // 初始化所有标签页内容 exactContent.appendChild(exactMatchListElement); regexContent.appendChild(regexMatchListElement); tnameContent.appendChild(tagNameListElement); configContent.appendChild(configListElement); contentContainer.appendChild(exactContent); contentContainer.appendChild(regexContent); contentContainer.appendChild(tnameContent); contentContainer.appendChild(configContent); managerPanel.appendChild(tabContainer); managerPanel.appendChild(header); managerPanel.appendChild(contentContainer); document.body.appendChild(managerPanel); return managerPanel; } /** * 为插件添加全局CSS样式。 */ GM_addStyle(` .bilibili-blacklist-block-container { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 20px; margin-top: 5px; padding: 0 5px; font-size: 12px; flex-direction: row; justify-content: space-between; align-items: center; gap: 3px; z-index: 9999; pointer-events: none; text-align:center; } .bili-video-card:hover .bilibili-blacklist-block-container, .card-box:hover .bilibili-blacklist-block-container { display: flex !important; pointer-events: none; } .card-box .bilibili-blacklist-block-container { flex-direction: column; align-items: flex-start; height: 100%; } .card-box .bilibili-blacklist-tname-group { flex-direction: column; align-items: flex-end; bottom: 0; } .card-box .bilibili-blacklist-tname-group .bilibili-blacklist-tname { background-color:rgba(255, 255, 255, 0.87); color: #9499A0; border: 1px solid #9499A0; } .bilibili-blacklist-block-btn { position: static; display: flex; width: 40px; height: 20px; justify-content: center; align-items: center; pointer-events: auto !important; background-color: #fb7299dd; color: white; border-radius: 10%; cursor: pointer; text-align: center; } .bilibili-blacklist-tname-group { display: flex; flex-direction: row; padding:0 5px; gap: 3px; align-items: center; margin-left: auto; max-width: 80%; pointer-events: none; } .bilibili-blacklist-tname { background-color: #fb7299dd; color: white; height: 20px; padding: 0 5px; border-radius: 10%; cursor: pointer; border-radius: 2px; pointer-events: auto; text-align: center; display: flex; justify-content: center; align-items: center; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } /* 修复视频卡片布局 */ .bili-video-card__cover { contain: layout !important; } /* 面板样式 */ #bilibili-blacklist-manager-panel { font-size: 15px; } /* 按钮悬停效果 */ #bilibili-blacklist-manager-panel button { transition: background-color 0.2s; } #bilibili-blacklist-manager-panel button:hover { opacity: 0.9; } /* 管理按钮悬停效果 */ #bilibili-blacklist-manager-button:hover svg { transform: scale(1.1); } #bilibili-blacklist-manager-button svg { transition: transform 0.2s; } /* 输入框聚焦效果 */ #"bilibili-blacklist-manager-panel input:focus { outline: none; border-color: #fb7299 !important; } /*灰度效果*/ .bilibili-blacklist-grayscale { filter: grayscale(95%); } `); /** * 返回卡比图标的SVG代码。 * @returns {string} SVG字符串。 */ function getKirbySVG() { return ` `; } /** * 为视频卡片添加卡比主题的覆盖层。 * @param {HTMLElement} cardElement - 视频卡片元素。 */ function addKirbyOverlayToCard(cardElement) { const kirbyWrapper = document.createElement("div"); // 如果已经有Kirby覆盖层,则不重复添加 if (cardElement.querySelector("#bilibili-blacklist-kirby") != null) return; kirbyWrapper.innerHTML = getKirbySVG(); kirbyWrapper.id = "bilibili-blacklist-kirby"; const justifyContent = isCurrentPageVideo() ? "flex-start" : "center"; const alignItems = isCurrentPageVideo() ? "flex-start" : "center"; Object.assign(kirbyWrapper.style, { position: "absolute", top: "0", left: "0", width: "100%", height: "100%", pointerEvents: "none", display: "flex", justifyContent: `${justifyContent}`, alignItems: `${alignItems}`, zIndex: "10", backgroundColor: "rgba(255, 255, 255, 0.7)", backdropFilter: "blur(5px)", WebkitBackdropFilter: "blur(5px)", // 兼容性 borderRadius: "inherit", border: "1px solid rgba(255, 255, 255, 0.5)", }); const svg = kirbyWrapper.querySelector("svg"); if (svg) { const cardRect = cardElement.getBoundingClientRect(); const size = Math.min(cardRect.width, cardRect.height) * 1.0; svg.setAttribute("width", `${size}px`); svg.setAttribute("height", `${size}px`); svg.setAttribute("bottom", `${cardRect.height - size}px`); svg.style.opacity = "0.15"; svg.style.filter = "none"; if (isCurrentPageVideo()) { svg.style.marginTop = "-10px"; // 视频播放页的微调 } else { svg.style.marginTop = "-40px"; // 其他页面的微调 } } // 确保卡片有position属性以便子元素绝对定位 const cardStyle = getComputedStyle(cardElement); if (cardStyle.position === "static" || !cardStyle.position) { cardElement.style.position = "relative"; } cardElement.appendChild(kirbyWrapper); } /** * 从视频卡片中移除卡比覆盖层。 * @param {HTMLElement} cardElement - 视频卡片元素。 */ function removeKirbyOverlay(cardElement) { const kirbyWrapper = cardElement.querySelector("#bilibili-blacklist-kirby"); if (kirbyWrapper) { kirbyWrapper.remove(); } } //#endregion //#region 变动观察器 // MutationObserver 检测动态加载的新内容 const contentObserver = new MutationObserver((mutations) => { let shouldCheck = false; // 对视频播放页进行优化,只在实际添加了可见元素时触发扫描 if (isCurrentPageVideo()) { mutations.forEach((mutation) => { if (mutation.addedNodes.length > 0) { shouldCheck = Array.from(mutation.addedNodes).some((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return false; // 检查节点是否有实际的尺寸,避免不必要的扫描 const hasVisibleContent = node.offsetWidth > 0 || node.offsetHeight > 0 || node.querySelector("[offsetWidth], [offsetHeight]"); return hasVisibleContent; }); } }); } else { // 其他页面只要有节点添加就触发 mutations.forEach((mutation) => { if (mutation.addedNodes.length > 0) { shouldCheck = true; } }); } if (shouldCheck) { // 使用setTimeout延迟扫描,避免短时间内多次触发 setTimeout(() => { scanAndBlockVideoCards(); if (isCurrentPageMain()) { blockMainPageAds(); // 主页广告屏蔽 } if (isCurrentPageVideo()) { blockVideoPageAds(); // 视频页广告屏蔽 } if (!document.getElementById("bilibili-blacklist-manager-button")) { addBlacklistManagerButton(); // 确保管理按钮存在 } }, globalPluginConfig.blockScanInterval); } }); /** * 在指定容器上初始化MutationObserver。 * @param {string} containerIdOrSelector - 要观察的容器的ID或CSS选择器。 * @returns {boolean} 如果观察器成功初始化则返回true,否则返回false。 */ function initializeObserver(containerIdOrSelector) { const rootNode = document.getElementById(containerIdOrSelector) || document.querySelector(containerIdOrSelector) || document.documentElement; // 默认观察整个文档 if (rootNode) { contentObserver.observe(rootNode, { childList: true, subtree: true, }); return true; } else { // 如果未找到根节点,则进行重试 setTimeout(() => initializeObserver(containerIdOrSelector), 500); console.warn("[bilibili-blacklist] 未找到根节点,正在重试..."); observerRetryCount++; if (observerRetryCount > 10) { console.error("[bilibili-blacklist] 重试次数过多,停止重试。"); return false; } } } //#endregion //#region 页面检测和初始化 /** * 根据当前页面初始化脚本。 */ function initializeScript() { // 重置状态变量 isBlockingOperationInProgress = false; lastBlockScanExecutionTime = 0; blockedVideoCards = new Set(); videoCardProcessQueue = new Set(); processedVideoCards = new WeakSet(); // 根据当前页面URL判断并初始化 if (isCurrentPageMain()) { initializeMainPage(); blockMainPageAds(); } else if (isCurrentPageSearch()) { initializeSearchPage(); blockMainPageAds(); // 搜索页也进行主页广告屏蔽 } else if (isCurrentPageVideo()) { initializeVideoPage(); } else if (isCurrentPageCategory()) { initializeCategoryPage(); } else if (isCurrentUserSpace()) { initializeUserSpace(); } else { return; // 不支持的页面不进行初始化 } createBlacklistPanel(); // 创建管理面板 console.log("[bilibili-blacklist] 脚本已加载🥔"); } // 监听DOMContentLoaded并检查readyState以进行早期初始化 document.addEventListener("DOMContentLoaded", initializeScript); if ( document.readyState === "complete" || document.readyState === "interactive" ) { initializeScript(); } /** * 检查当前页面是否为Bilibili主页。 * @returns {boolean} 如果是主页则返回true,否则返回false。 */ function isCurrentPageMain() { return location.pathname === "/"; } /** * 初始化主页特有的功能。 */ function initializeMainPage() { initializeObserver("feedchannel-main"); // 观察主页内容区域 console.log("[bilibili-blacklist] 主页已加载🍓"); } /** * 检查当前页面是否为Bilibili搜索结果页。 * @returns {boolean} 如果是搜索页则返回true,否则返回false。 */ function isCurrentPageSearch() { return location.hostname === "search.bilibili.com"; } /** * 初始化搜索页特有的功能。 */ function initializeSearchPage() { initializeObserver("i_cecream"); // 观察搜索结果内容区域 console.log("[bilibili-blacklist] 搜索页已加载🍉"); } /** * 检查当前页面是否为Bilibili视频播放页。 * @returns {boolean} 如果是视频播放页则返回true,否则返回false。 */ function isCurrentPageVideo() { return location.pathname.startsWith("/video/"); } /** * 初始化视频播放页特有的功能。 */ function initializeVideoPage() { initializeObserver("right-container"); // 观察视频播放页右侧推荐区域 console.log("[bilibili-blacklist] 播放页已加载🍇"); } /** * 检查当前页面是否为Bilibili分类页。 * @returns {boolean} 如果是分类页则返回true,否则返回false。 */ function isCurrentPageCategory() { return location.pathname.startsWith("/c/"); } /** * 初始化分类页特有的功能。 */ function initializeCategoryPage() { initializeObserver("app"); // 观察整个app容器 console.log("[bilibili-blacklist] 分类页已加载🍊"); } /** * 检查当前页面是否为Bilibili用户空间页。 * @returns {boolean} 如果是用户空间页则返回true,否则返回false。 */ function isCurrentUserSpace() { return location.hostname === "space.bilibili.com"; } /** * 初始化用户空间页特有的功能。 */ function initializeUserSpace() { console.log("[bilibili-blacklist] 用户空间已加载🍎"); const upNameSelector = "#h-name, .nickname"; // UP主名称的选择器 // 创建一个MutationObserver来等待UP主名称元素加载 const observerForUpName = new MutationObserver((mutations, observer) => { const upNameElement = document.querySelector(upNameSelector); if (upNameElement) { observer.disconnect(); // 找到元素后停止观察 addBlockButtonToUserSpace(upNameElement); } }); observerForUpName.observe(document.body, { childList: true, subtree: true, }); // 立即检查一次,如果元素已经存在则直接处理 const initialUpNameElement = document.querySelector(upNameSelector); if (initialUpNameElement) { observerForUpName.disconnect(); addBlockButtonToUserSpace(initialUpNameElement); } } /** * 在用户空间页面上的UP主名称元素添加屏蔽/取消屏蔽按钮。 * @param {HTMLElement} upNameElement - 包含UP主名称的元素。 */ function addBlockButtonToUserSpace(upNameElement) { const upName = upNameElement.textContent.trim(); // 避免重复添加按钮 if (upNameElement.querySelector(".bilibili-blacklist-up-block-btn")) { return; } // 调整UP主名称元素的样式,以便容纳按钮 upNameElement.style.display = "inline-flex"; upNameElement.style.alignItems = "center"; const button = document.createElement("button"); button.className = "bilibili-blacklist-up-block-btn"; button.textContent = "屏蔽"; button.style.color = "#fff"; button.style.width = "100px"; button.style.height = "30px"; button.style.marginLeft = "10px"; button.style.borderRadius = "5px"; button.style.border = "1px solid #fb7299"; // 刷新按钮状态和页面灰度效果 const refreshButtonStatus = () => { const blocked = isBlacklisted(upName); if (blocked) { button.textContent = "已屏蔽"; button.style.backgroundColor = "#dddddd"; button.style.border = "1px solid #ccc"; upNameElement.style.textDecoration = "line-through"; // 添加删除线 document.body.classList.add("bilibili-blacklist-grayscale"); // 添加灰度滤镜 } else { button.textContent = "屏蔽"; button.style.backgroundColor = "#fb7299"; button.style.border = "1px solid #fb7299"; upNameElement.style.textDecoration = "none"; // 移除删除线 document.body.classList.remove("bilibili-blacklist-grayscale"); // 移除灰度滤镜 } }; button.addEventListener("click", (e) => { e.stopPropagation(); const blocked = isBlacklisted(upName); if (blocked) { removeFromExactBlacklist(upName); } else { addToExactBlacklist(upName); } refreshButtonStatus(); // 更新按钮状态 }); refreshButtonStatus(); // 设置按钮初始状态 upNameElement.appendChild(button); } //#endregion //#region 广告屏蔽 /** * 屏蔽主页上的广告。 */ function blockMainPageAds() { if (!globalPluginConfig.flagAD) return; // 如果广告屏蔽未启用,则直接返回 const adSelectors = [ ".floor-single-card", // 分区推荐 ".bili-live-card", // 直播推广 ".btn-ad", // 广告按钮 ]; adSelectors.forEach((selector) => { document.querySelectorAll(selector).forEach((adCard) => { hideVideoCard(adCard, "ad"); // 隐藏广告卡片 }); }); } /** * 屏蔽视频播放页上的广告。 */ function blockVideoPageAds() { if (!globalPluginConfig.flagAD) return; // 如果广告屏蔽未启用,则直接返回 const adSelectors = [ ".video-card-ad-small", // 右上角推广 ".slide-ad-exp", // 大推广 ".video-page-game-card-small", // 游戏推广 ".activity-m-v1", // 活动推广 ".video-page-special-card-small", // 特殊卡片推广 ".ad-floor-exp", // 广告地板 ".btn-ad", // 广告按钮 ]; adSelectors.forEach((selector) => { document.querySelectorAll(selector).forEach((adCard) => { hideVideoCard(adCard, "ad"); // 隐藏广告卡片 }); }); } //#endregion })();