// ==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 = `