您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
屏蔽B站主页推荐流中的版权内容卡片(电影、课堂、电视剧、国创 等等)
// ==UserScript== // @name Bilibili 主页推荐卡片版权内容屏蔽 // @namespace https://github.com/rainpenber/mktk-my-tampermonkey-scripts/tree/main/scripts/bilibili_feedcard_filter // @version 1.0.0 // @description 屏蔽B站主页推荐流中的版权内容卡片(电影、课堂、电视剧、国创 等等) // @author Rainpenber // @license MIT // @match https://www.bilibili.com/ // @match https://www.bilibili.com/?* // @icon https://www.bilibili.com/favicon.ico // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_addValueChangeListener // @run-at document-end // ==/UserScript== (function () { 'use strict'; const CATEGORY_LABELS = [ '电影', '课堂', '电视剧', '国创', '综艺', '纪录片', '漫画', '直播', '番剧', ]; const STORAGE_KEY = 'bili_filter_blocked_categories_v1'; function readBlocked() { const saved = GM_getValue(STORAGE_KEY, []); if (Array.isArray(saved)) return new Set(saved); return new Set(); } function writeBlocked(blockedSet) { GM_setValue(STORAGE_KEY, Array.from(blockedSet)); } function hideCard(card) { if (!card) return; card.style.display = 'none'; card.setAttribute('data-bili-filter-hidden', '1'); } function showCard(card) { if (!card) return; card.style.display = ''; card.removeAttribute('data-bili-filter-hidden'); } function getCardTitleElement(root) { // 结构:div.container.is-version8 内部的 div.floor-single-card > div.floor-card-inner > div.cover-container > a > div.badge > span.floor-title // 这里尽量兼容 class 变化,使用层级选择器并校验类名包含关系 const badgeSpan = root.querySelector( ':scope div.floor-card-inner div.cover-container a div.badge span' ); return badgeSpan; } function getCardCategoryText(card) { try { const span = getCardTitleElement(card); if (!span) return ''; return (span.textContent || '').trim(); } catch (_) { return ''; } } function isTargetCard(node) { if (!(node instanceof HTMLElement)) return false; if (!node.classList) return false; // 只处理 floor-single-card return node.classList.contains('floor-single-card'); } function filterOneCard(card, blockedSet) { const category = getCardCategoryText(card); if (!category) { // 无法识别类别则不处理 return; } if (blockedSet.has(category)) { hideCard(card); } else { showCard(card); } } function scanAndFilter(container, blockedSet) { if (!container) return; const cards = container.querySelectorAll('div.floor-single-card'); cards.forEach((card) => filterOneCard(card, blockedSet)); } function setupObserver(container, blockedSetRef) { if (!container) return; const observer = new MutationObserver((mutations) => { for (const m of mutations) { if (m.type === 'childList') { m.addedNodes.forEach((node) => { if (isTargetCard(node)) { filterOneCard(node, blockedSetRef.current); } else if (node instanceof HTMLElement) { // 子树中可能也有新增卡片 const innerCards = node.querySelectorAll('div.floor-single-card'); innerCards.forEach((c) => filterOneCard(c, blockedSetRef.current)); } }); } } }); observer.observe(container, { childList: true, subtree: true }); return observer; } function ensureStyles() { GM_addStyle(` .bili-filter-dialog-mask { position: fixed; inset: 0; background: rgba(0,0,0,.25); z-index: 99998; } .bili-filter-dialog { position: fixed; z-index: 99999; width: 360px; max-width: 92vw; background: #fff; color: #222; border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.18); left: 50%; top: 64px; transform: translateX(-50%); font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, PingFang SC, Microsoft YaHei, sans-serif; } .bili-filter-dialog header { padding: 12px 16px; font-weight: 600; border-bottom: 1px solid rgba(0,0,0,.06); display: flex; align-items: center; justify-content: space-between; } .bili-filter-dialog .content { padding: 12px 16px; max-height: 60vh; overflow: auto; } .bili-filter-dialog .actions { padding: 12px 16px; display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid rgba(0,0,0,.06); } .bili-filter-chip-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .bili-filter-chip { display: flex; align-items: center; gap: 6px; padding: 8px 10px; border: 1px solid #e5e7eb; border-radius: 8px; cursor: pointer; user-select: none; } .bili-filter-chip input { pointer-events: none; } .bili-filter-btn { padding: 6px 12px; border-radius: 6px; border: 1px solid #e5e7eb; background: #fff; cursor: pointer; } .bili-filter-btn.primary { background: #00AEEC; color: #fff; border-color: #00AEEC; } .bili-filter-close { cursor: pointer; opacity: .7; } .bili-filter-close:hover { opacity: 1; } `); } function openDialog(blockedSet, onChange, onClose) { ensureStyles(); const mask = document.createElement('div'); mask.className = 'bili-filter-dialog-mask'; const dialog = document.createElement('div'); dialog.className = 'bili-filter-dialog'; const header = document.createElement('header'); header.innerHTML = '<span>屏蔽推荐类型</span><span class="bili-filter-close">✕</span>'; const content = document.createElement('div'); content.className = 'content'; const grid = document.createElement('div'); grid.className = 'bili-filter-chip-grid'; const checkboxRefs = new Map(); CATEGORY_LABELS.forEach((label) => { const chip = document.createElement('label'); chip.className = 'bili-filter-chip'; const input = document.createElement('input'); input.type = 'checkbox'; input.checked = blockedSet.has(label); const span = document.createElement('span'); span.textContent = label; chip.appendChild(input); chip.appendChild(span); chip.addEventListener('click', (e) => { // label 默认会切换 checked,此处延迟读取 setTimeout(() => { if (input.checked) { blockedSet.add(label); } else { blockedSet.delete(label); } onChange(new Set(blockedSet)); }, 0); }); grid.appendChild(chip); checkboxRefs.set(label, input); }); content.appendChild(grid); const actions = document.createElement('div'); actions.className = 'actions'; const btnSelectAll = document.createElement('button'); btnSelectAll.className = 'bili-filter-btn'; btnSelectAll.textContent = '一键全选'; btnSelectAll.addEventListener('click', () => { CATEGORY_LABELS.forEach((l) => { blockedSet.add(l); const ref = checkboxRefs.get(l); if (ref) ref.checked = true; }); onChange(new Set(blockedSet)); }); const btnDisableAll = document.createElement('button'); btnDisableAll.className = 'bili-filter-btn'; btnDisableAll.textContent = '一键禁用'; btnDisableAll.addEventListener('click', () => { blockedSet.clear(); CATEGORY_LABELS.forEach((l) => { const ref = checkboxRefs.get(l); if (ref) ref.checked = false; }); onChange(new Set(blockedSet)); }); const btnOk = document.createElement('button'); btnOk.className = 'bili-filter-btn primary'; btnOk.textContent = '完成'; btnOk.addEventListener('click', close); actions.appendChild(btnSelectAll); actions.appendChild(btnDisableAll); actions.appendChild(btnOk); dialog.appendChild(header); dialog.appendChild(content); dialog.appendChild(actions); function close() { document.body.removeChild(mask); document.body.removeChild(dialog); onClose && onClose(); } mask.addEventListener('click', close); header.querySelector('.bili-filter-close')?.addEventListener('click', close); document.body.appendChild(mask); document.body.appendChild(dialog); } function main() { const container = document.querySelector('div.container.is-version8'); if (!container) { // 等待首页容器出现 const readyObserver = new MutationObserver(() => { const c = document.querySelector('div.container.is-version8'); if (c) { readyObserver.disconnect(); initWithContainer(c); } }); readyObserver.observe(document.documentElement || document.body, { childList: true, subtree: true, }); return; } initWithContainer(container); } function initWithContainer(container) { const blockedSet = readBlocked(); const blockedRef = { current: blockedSet }; // 初始扫描 scanAndFilter(container, blockedRef.current); // 观察新增 const obs = setupObserver(container, blockedRef); // 存储变更联动 GM_addValueChangeListener(STORAGE_KEY, (_k, _o, n) => { const next = new Set(Array.isArray(n) ? n : []); blockedRef.current = next; scanAndFilter(container, blockedRef.current); }); // 菜单项 GM_registerMenuCommand('设置屏蔽的推荐类型', () => { const working = new Set(blockedRef.current); openDialog(working, (updated) => { blockedRef.current = new Set(updated); writeBlocked(blockedRef.current); scanAndFilter(container, blockedRef.current); }); }); } // 尝试尽早启动 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();