// ==UserScript== // @name B站推荐过滤器 // @namespace http://tampermonkey.net/ // @homepageURL https://github.com/StarsWhere/Bilibili-Video-Filter // @version 7.0.0 // @description Bilibili首页推荐过滤器:智能屏蔽广告、分类、直播和自定义关键词,支持自适应持续屏蔽、拖拽控制面板、暗黑模式切换,以及修复屏蔽后页面留白问题。优化UI交互,提升浏览体验。 // @author StarsWhere // @match *://www.bilibili.com/* // @exclude *://www.bilibili.com/video/* // @icon https://www.bilibili.com/favicon.ico // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function () { 'use strict'; // ===== 工具函数 ===== const debounce = (func, wait) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }; const throttle = (func, limit) => { let inThrottle; return (...args) => { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }; const safeExecute = (func, errorMessage) => { try { return func(); } catch (error) { console.warn(`[B站过滤插件] ${errorMessage}:`, error); return 0; } }; // ===== 选择器常量 ===== const SELECTORS = { CARD_CONTAINERS: '.feed-card, .bili-video-card, .bili-feed-card', LIVE_CARDS: '.bili-live-card, .floor-single-card', AD_INDICATORS: '.bili-ad, [ad-id], .ad-report, [data-report*="ad"], .bili-video-card__stats--text:is(:has-text("广告"), :has-text("推广"))', VIDEO_TITLES: '.bili-video-card__info--tit, .bili-video-card__info--title', VIDEO_AUTHORS: '.bili-video-card__info--author, .bili-video-card__info--owner', CATEGORY_TITLES: '.floor-title, .bili-grid-floor-header__title', LIVE_INDICATORS: '.bili-video-card__stats--item[title*="直播"], .live-tag, .bili-live-card__info--text, .recommend-card__live-status', CARD_SELECTORS: ['.feed-card', '.bili-video-card', '.bili-live-card', '.bili-feed-card', '.floor-single-card'] }; // ===== 配置管理器 ===== class ConfigManager { constructor() { this.config = {}; this.loadConfig(); } loadConfig() { this.config = { video: { enabled: GM_getValue('video.enabled', true), blacklist: GM_getValue('video.blacklist', []) }, category: { enabled: GM_getValue('category.enabled', true), blacklist: GM_getValue('category.blacklist', ['番剧', '直播', '国创', '综艺', '课堂', '电影', '电视剧', '纪录片', '漫画']) }, ad: GM_getValue('ad', true), live: GM_getValue('live', true), continuousBlock: GM_getValue('continuousBlock', false), floatBtnPosition: GM_getValue('floatBtnPosition', { x: 30, y: 100 }), darkMode: GM_getValue('darkMode', false) }; } get(path) { return path.split('.').reduce((acc, key) => acc?.[key], this.config); } setValue(key, value) { GM_setValue(key, value); const keys = key.split('.'); let current = this.config; for (let i = 0; i < keys.length - 1; i++) { if (!current[keys[i]]) current[keys[i]] = {}; current = current[keys[i]]; } current[keys[keys.length - 1]] = value; } reset() { const keysToDelete = [ 'video.enabled', 'video.blacklist', 'category.enabled', 'category.blacklist', 'ad', 'live', 'continuousBlock', 'floatBtnPosition', 'darkMode', 'videoEnabled', 'videoBlacklist', 'categoryEnabled', 'categoryBlacklist', 'adEnabled', 'liveEnabled' ]; keysToDelete.forEach(key => GM_setValue(key, undefined)); } } const configManager = new ConfigManager(); // ===== 自适应屏蔽器 ===== class AdaptiveBlocker { constructor() { this.baseInterval = 2000; this.maxInterval = 8000; this.minInterval = 500; this.currentInterval = this.baseInterval; this.lastBlockCount = 0; this.intervalId = null; this.isRunning = false; } adjustInterval(currentBlockCount) { if (currentBlockCount > this.lastBlockCount) { this.currentInterval = Math.max(this.minInterval, this.currentInterval * 0.7); } else { this.currentInterval = Math.min(this.maxInterval, this.currentInterval * 1.3); } this.lastBlockCount = currentBlockCount; } start() { if (this.isRunning) return; this.isRunning = true; const execute = () => { if (!this.isRunning) return; const blockedCount = runAllBlockers(); this.adjustInterval(blockedCount); this.intervalId = setTimeout(execute, this.currentInterval); }; execute(); } stop() { this.isRunning = false; clearTimeout(this.intervalId); this.intervalId = null; } } const adaptiveBlocker = new AdaptiveBlocker(); // ===== 状态指示器 ===== function showBlockingStatus(count = 0) { let indicator = document.querySelector('.block-status-indicator'); if (!indicator) { const indicatorContainer = document.createElement('div'); indicatorContainer.innerHTML = `
`; document.head.appendChild(indicatorContainer.querySelector('style')); indicator = indicatorContainer.querySelector('.block-status-indicator'); document.body.appendChild(indicator); } if (count > 0) indicator.textContent = `🛡️ 已屏蔽 ${count} 项`; indicator.classList.add('show'); setTimeout(() => indicator.classList.remove('show'), 1500); } // ===== 核心屏蔽功能 ===== function removeCardElement(element) { if (!element || element.dataset.blocked) return false; const card = element.closest(SELECTORS.CARD_SELECTORS.join(', ')); if (card && !card.dataset.blocked) { card.dataset.blocked = 'true'; card.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; card.style.opacity = '0'; card.style.transform = 'scale(0.95)'; setTimeout(() => card.remove(), 300); return true; } return false; } function blockAds() { if (!configManager.get('ad')) return 0; let blockedCount = 0; document.querySelectorAll(SELECTORS.AD_INDICATORS).forEach(indicator => { if (removeCardElement(indicator)) blockedCount++; }); const recommendItem = document.querySelector(".recommended-swipe"); if (recommendItem) { recommendItem.remove(); blockedCount++; } return blockedCount; } function blockCategories() { if (!configManager.get('category.enabled')) return 0; let blockedCount = 0; const blacklist = configManager.get('category.blacklist') || []; document.querySelectorAll('.floor-single-card, .bili-grid-floor-header').forEach(card => { if (card.dataset.blocked) return; const categoryElement = card.querySelector(SELECTORS.CATEGORY_TITLES); if (categoryElement && blacklist.some(keyword => categoryElement.textContent.trim().includes(keyword))) { const floorContainer = card.closest('.bili-grid-floor') || card; if (floorContainer && removeCardElement(floorContainer)) blockedCount++; } }); return blockedCount; } function blockVideos() { if (!configManager.get('video.enabled')) return 0; const blacklist = configManager.get('video.blacklist') || []; if (blacklist.length === 0) return 0; let blockedCount = 0; document.querySelectorAll(SELECTORS.CARD_CONTAINERS).forEach(card => { if (card.dataset.blocked) return; const title = card.querySelector(SELECTORS.VIDEO_TITLES)?.textContent.trim() || ''; const author = card.querySelector(SELECTORS.VIDEO_AUTHORS)?.textContent.trim() || ''; if (blacklist.some(keyword => title.includes(keyword) || author.includes(keyword))) { if (removeCardElement(card)) blockedCount++; } }); return blockedCount; } function blockLive() { if (!configManager.get('live')) return 0; let blockedCount = 0; document.querySelectorAll(`${SELECTORS.LIVE_CARDS}, ${SELECTORS.CARD_CONTAINERS}`).forEach(card => { if (card.dataset.blocked) return; const isLive = card.querySelector(SELECTORS.LIVE_INDICATORS) || card.textContent.includes('正在直播') || card.querySelector('.floor-title')?.textContent.includes('直播'); if (isLive && removeCardElement(card)) blockedCount++; }); return blockedCount; } const runAllBlockers = () => { const totalBlocked = [blockAds, blockCategories, blockVideos, blockLive] .reduce((sum, func) => sum + safeExecute(func, `${func.name}执行失败`), 0); if (totalBlocked > 0) showBlockingStatus(totalBlocked); return totalBlocked; }; const debouncedRunAllBlockers = debounce(runAllBlockers, 300); // ===== UI 组件 ===== function injectGlobalStyles() { const style = document.createElement('style'); style.textContent = ` :root { --panel-bg: rgba(255, 255, 255, 0.95); --panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.1); --text-primary: #18191c; --text-secondary: #5f6368; --text-tertiary: #888c92; --border-color: #e3e5e7; --hover-bg: #f1f2f3; --active-bg: #e7e8e9; --accent-blue: #00a1d6; --accent-blue-hover: #00b5e5; --accent-red: #fd4c5d; --accent-green: #52c41a; --accent-gray: #d9d9d9; --input-bg: #f1f2f3; --input-border: #e3e5e7; --tag-bg: #f1f2f3; --tag-text: #5f6368; --tag-hover-bg: #e7e8e9; --info-bg: #e6f7ff; --info-text: #0958d9; } body[data-theme="dark"] { --panel-bg: rgba(37, 37, 37, 0.95); --panel-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); --text-primary: #e7e9ea; --text-secondary: #b0b3b8; --text-tertiary: #8a8d91; --border-color: #4d4d4d; --hover-bg: #4a4a4a; --active-bg: #5a5a5a; --input-bg: #3a3b3c; --input-border: #4d4d4d; --tag-bg: #3a3b3c; --tag-text: #e4e6eb; --tag-hover-bg: #5a5a5a; --info-bg: #263e5e; --info-text: #69b1ff; } .filter-panel { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); background: var(--panel-bg); color: var(--text-primary); border-radius: 16px; box-shadow: var(--panel-shadow); padding: 24px; z-index: 10000; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; transition: opacity 0.3s, transform 0.3s; border: 1px solid var(--border-color); } .filter-panel.main-panel { width: 400px; } .filter-panel.sub-panel { width: 480px; max-height: 85vh; display: flex; flex-direction: column;} .panel-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 15px; border-bottom: 1px solid var(--border-color); margin-bottom: 20px; gap: 15px; } .panel-header h3 { margin: 0; font-size: 18px; display: flex; align-items: center; gap: 8px; } .panel-header .header-buttons { display: flex; align-items: center; gap: 8px; } .header-btn { cursor: pointer; font-size: 20px; color: var(--text-secondary); width: 32px; height: 32px; border-radius: 50%; transition: background .2s, color .2s; border: none; background: none; display: flex; align-items: center; justify-content: center; } .header-btn:hover { background: var(--hover-bg); color: var(--text-primary); } .switch-item { display: flex; justify-content: space-between; align-items: center; margin: 16px 0; padding: 10px; border-radius: 8px; transition: background-color 0.2s; } .switch-item:hover { background-color: var(--hover-bg); } .switch-item > div:first-child { display: flex; align-items: center; gap: 8px; } .switch-item span { font-size: 14px; } .manage-btn { color: var(--accent-blue); cursor: pointer; margin-left: 10px; padding: 6px 10px; border-radius: 6px; transition: background .2s; border: none; background: none; font-size: 13px; font-weight: 500; } .manage-btn:hover { background: var(--accent-blue); color: #fff; } .switch { position: relative; display: inline-block; width: 40px; height: 20px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--accent-gray); transition: .4s; border-radius: 20px; } .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; } .switch input:checked + .slider { background-color: var(--accent-blue); } .switch input:checked + .slider:before { transform: translateX(20px); } .continuous-block-section { background: var(--hover-bg); padding: 12px; border-radius: 8px; margin: 20px 0; } .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-left: 8px; transition: background-color 0.4s; } .status-indicator.status-active { background: var(--accent-green); } .status-indicator.status-inactive { background: var(--text-tertiary); } .action-buttons { display: flex; gap: 12px; margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border-color); } .action-btn { flex: 1; padding: 10px 14px; border: 1px solid var(--accent-blue); background: transparent; color: var(--accent-blue); border-radius: 8px; cursor: pointer; transition: all .2s; font-size: 14px; font-weight: 500; display: flex; align-items: center; justify-content: center; gap: 6px;} .action-btn:hover { background: var(--accent-blue); color: #fff; } .action-btn.primary { background: var(--accent-blue); color: #fff; } .action-btn.primary:hover { background: var(--accent-blue-hover); border-color: var(--accent-blue-hover); } .input-group { display: flex; gap: 10px; margin-bottom: 20px; } .input-field { flex: 1; padding: 12px; border: 1px solid var(--input-border); border-radius: 8px; font-size: 14px; background: var(--input-bg); color: var(--text-primary); } .input-field:focus { border-color: var(--accent-blue); outline: none; box-shadow: 0 0 0 2px rgba(0, 161, 214, 0.2); } .add-btn { padding: 12px 24px; background: var(--accent-blue); color: #fff; border: none; border-radius: 8px; cursor: pointer; transition: background .2s; font-weight: 500; } .add-btn:hover { background: var(--accent-blue-hover); } .item-list { overflow-y: auto; padding: 5px; margin: -5px; display: flex; flex-wrap: wrap; gap: 10px; align-content: flex-start; } .item-tag { display: flex; align-items: center; padding: 6px 12px; background: var(--tag-bg); color: var(--tag-text); border-radius: 16px; transition: background .2s; font-size: 13px; } .item-tag span { word-break: keep-all; white-space: nowrap; } .item-tag .delete-btn { color: var(--text-tertiary); background: none; border: none; cursor: pointer; padding: 4px; margin-left: 6px; font-size: 16px; line-height: 1; border-radius: 50%; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; } .item-tag .delete-btn:hover { background: var(--active-bg); color: var(--accent-red); } .stats-bar { padding: 12px; border-radius: 8px; margin-bottom: 20px; background: var(--info-bg); color: var(--info-text); font-size: 14px; } .master-float-btn { position: fixed; background: linear-gradient(135deg, #00a1d6, #0085b3); color: #fff; padding: 12px; border-radius: 50%; z-index: 9999; cursor: grab; box-shadow: 0 4px 12px rgba(0,0,0,0.3); transition: transform .2s, box-shadow .2s; user-select: none; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; font-size: 20px; -webkit-user-select: none; } .master-float-btn:hover { transform: scale(1.1); box-shadow: 0 6px 16px rgba(0,0,0,0.4); } .master-float-btn:active { cursor: grabbing; } .master-float-btn.dragging { opacity: .8; z-index: 10000; cursor: grabbing; transform: scale(1.05); } `; document.head.appendChild(style); } const ICONS = { shield: ``, close: `×`, sun: ``, moon: ``, settings: ``, list: ``, zap: ``, trash: `` }; function createMainPanel() { const panel = document.createElement('div'); panel.className = 'filter-panel main-panel'; panel.style.display = 'none'; const isDarkMode = configManager.get('darkMode'); panel.innerHTML = `

${ICONS.shield} 智能屏蔽控制中心 v7.0.0

${ICONS.settings}自适应持续屏蔽
智能调节屏蔽间隔,优化性能
${ICONS.list}视频关键词屏蔽
${ICONS.list}分类屏蔽
${ICONS.list}广告屏蔽
${ICONS.list}直播推荐屏蔽
`; // Event listeners panel.querySelector('.theme-toggle-btn').addEventListener('click', e => { const body = document.body; const currentTheme = body.dataset.theme === 'dark'; const newTheme = !currentTheme; body.dataset.theme = newTheme ? 'dark' : 'light'; configManager.setValue('darkMode', newTheme); e.currentTarget.innerHTML = newTheme ? ICONS.sun : ICONS.moon; }); panel.querySelector('#continuous-block').addEventListener('change', e => { configManager.setValue('continuousBlock', e.target.checked); panel.querySelector('.status-indicator').className = `status-indicator ${e.target.checked ? 'status-active' : 'status-inactive'}`; e.target.checked ? adaptiveBlocker.start() : adaptiveBlocker.stop(); }); panel.querySelector('#video-enabled').addEventListener('change', e => { configManager.setValue('video.enabled', e.target.checked); runAllBlockers(); }); panel.querySelector('#category-enabled').addEventListener('change', e => { configManager.setValue('category.enabled', e.target.checked); runAllBlockers(); }); panel.querySelector('#ad-enabled').addEventListener('change', e => { configManager.setValue('ad', e.target.checked); runAllBlockers(); }); panel.querySelector('#live-enabled').addEventListener('change', e => { configManager.setValue('live', e.target.checked); runAllBlockers(); }); panel.querySelectorAll('.manage-btn').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); showManagementPanel(btn.dataset.type); }); }); panel.querySelector('#run-once').addEventListener('click', runAllBlockers); panel.querySelector('#reset-config').addEventListener('click', () => { if (confirm('确定要重置所有配置吗?此操作不可撤销,页面将刷新。')) { configManager.reset(); location.reload(); } }); panel.querySelector('.close-btn').addEventListener('click', () => panel.style.display = 'none'); return panel; } function createSubPanel(type) { const isVideo = type === 'video'; const panel = document.createElement('div'); panel.className = 'filter-panel sub-panel'; panel.id = `${type}-management-panel`; const listClass = `${type}-item-list`; const inputClass = `${type}-management-input`; const countClass = `${type}-management-count`; panel.innerHTML = `

${isVideo ? '📝 视频关键词管理' : '🏷️ 分类屏蔽管理'}

当前屏蔽${isVideo ? '关键词' : '分类'}:${(configManager.get(`${type}.blacklist`) || []).length}
`; document.body.appendChild(panel); const list = panel.querySelector(`.${listClass}`); const countSpan = panel.querySelector(`.${countClass}`); const input = panel.querySelector(`.${inputClass}`); const updateList = () => { const blacklist = configManager.get(`${type}.blacklist`) || []; if (countSpan) countSpan.textContent = blacklist.length; list.innerHTML = blacklist.map(item => `
${item}
` ).join(''); list.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', () => { const itemToRemove = btn.dataset.item; let currentList = configManager.get(`${type}.blacklist`) || []; const newList = currentList.filter(i => i !== itemToRemove); configManager.setValue(`${type}.blacklist`, newList); updateList(); runAllBlockers(); }); }); }; const addItem = () => { if (!input) return; const item = input.value.trim(); const currentList = configManager.get(`${type}.blacklist`) || []; if (item && !currentList.includes(item)) { const newList = [...currentList, item]; configManager.setValue(`${type}.blacklist`, newList); input.value = ''; updateList(); runAllBlockers(); } input.focus(); }; panel.querySelector('.add-btn')?.addEventListener('click', addItem); input?.addEventListener('keypress', e => { if (e.key === 'Enter') addItem(); }); panel.querySelector('.close-btn')?.addEventListener('click', () => panel.remove()); updateList(); } const showManagementPanel = type => { const existingPanel = document.getElementById(`${type}-management-panel`); if (existingPanel) { existingPanel.remove(); } createSubPanel(type); }; function createDraggableFloatBtn(mainPanelElement) { const btn = document.createElement('div'); btn.className = 'master-float-btn'; btn.title = '拖动移动位置,点击打开控制面板'; btn.innerHTML = '🛡️'; const pos = configManager.get('floatBtnPosition'); btn.style.left = `${pos.x}px`; btn.style.bottom = `${pos.y}px`; let isDragging = false, hasMoved = false; let startX, startY, initialX, initialY; const handleStart = e => { e.preventDefault(); isDragging = true; hasMoved = false; btn.classList.add('dragging'); const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX; const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY; startX = clientX; startY = clientY; const rect = btn.getBoundingClientRect(); initialX = rect.left; initialY = window.innerHeight - rect.bottom; document.addEventListener(e.type.includes('touch') ? 'touchmove' : 'mousemove', handleMove, { passive: false }); document.addEventListener(e.type.includes('touch') ? 'touchend' : 'mouseup', handleEnd); }; const handleMove = e => { if (!isDragging) return; e.preventDefault(); const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX; const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY; const deltaX = clientX - startX; const deltaY = startY - clientY; if (!hasMoved && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) hasMoved = true; if (hasMoved) { let newX = initialX + deltaX, newY = initialY + deltaY; newX = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, newX)); newY = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, newY)); btn.style.left = `${newX}px`; btn.style.bottom = `${newY}px`; } }; const handleEnd = () => { if (!isDragging) return; isDragging = false; btn.classList.remove('dragging'); document.removeEventListener('mousemove', handleMove); document.removeEventListener('mouseup', handleEnd); document.removeEventListener('touchmove', handleMove); document.removeEventListener('touchend', handleEnd); if (hasMoved) { const rect = btn.getBoundingClientRect(); configManager.setValue('floatBtnPosition', { x: rect.left, y: window.innerHeight - rect.bottom }); } else { mainPanelElement.style.display = mainPanelElement.style.display === 'none' ? 'block' : 'none'; } }; btn.addEventListener('mousedown', handleStart); btn.addEventListener('touchstart', handleStart, { passive: false }); window.addEventListener('resize', throttle(() => { const rect = btn.getBoundingClientRect(); const x = Math.max(0, Math.min(window.innerWidth - btn.offsetWidth, rect.left)); const y = Math.max(0, Math.min(window.innerHeight - btn.offsetHeight, window.innerHeight - rect.bottom)); btn.style.left = `${x}px`; btn.style.bottom = `${y}px`; }, 100)); return btn; } // ===== 初始化 ===== function init() { injectGlobalStyles(); if (configManager.get('darkMode')) { document.body.dataset.theme = 'dark'; } const mainPanelElement = createMainPanel(); const floatBtnElement = createDraggableFloatBtn(mainPanelElement); document.body.appendChild(mainPanelElement); document.body.appendChild(floatBtnElement); document.addEventListener('click', e => { const isClickInsidePanel = e.target.closest('.filter-panel'); const isClickOnFloatBtn = floatBtnElement.contains(e.target); if (!isClickInsidePanel && !isClickOnFloatBtn) { mainPanelElement.style.display = 'none'; const subPanels = document.querySelectorAll('.sub-panel'); subPanels.forEach(p => p.remove()); } }); const observer = new MutationObserver(throttle(() => { if (!configManager.get('continuousBlock')) debouncedRunAllBlockers(); }, 500)); observer.observe(document.body, { childList: true, subtree: true }); if (document.readyState === 'complete') { runAllBlockers(); } else { window.addEventListener('load', runAllBlockers); } if (configManager.get('continuousBlock')) { adaptiveBlocker.start(); } window.addEventListener('beforeunload', () => { adaptiveBlocker.stop(); observer.disconnect(); }); console.log('[B站过滤插件] v7.0.0 初始化完成'); } init(); })();