您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一个可以快速导入Bilibili弹幕屏蔽词的油猴脚本,网页端导入并同步至移动端
// ==UserScript== // @name B站弹幕屏蔽词导入器 // @namespace https://github.com/xingguang2333/BiliBlocklistImporter/ // @version 1.0.1 // @description 一个可以快速导入Bilibili弹幕屏蔽词的油猴脚本,网页端导入并同步至移动端 // @author StarsOcean // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/ // @icon https://www.bilibili.com/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_notification // @grant GM_download // @connect * // @license GPLv3 // ==/UserScript== (function() { 'use strict'; // 配置项 const CONFIG = { CHECK_INTERVAL: 300, // 操作间隔时间(ms) MAX_WAIT_TIME: 5000, // 元素等待最大时间(ms) RETRY_TIMES: 3, // 失败重试次数 FIRST_USE_KEY: 'BHELPER_PRO_FIRST_USE' // 本地存储键名 }; // 元素选择器 const SELECTORS = { DROPDOWN_WRAP: '.bui-dropdown-wrap', // 设置面板容器 CURRENT_SETTING: '.bui-dropdown-name', // 当前设置显示 MENU_ITEM: '.bui-dropdown-item', // 菜单项 BLOCK_INPUT: '.bpx-player-block-add-input', // 规则输入框 ADD_BUTTON: '.bui-area.bui-button-gray', // 添加按钮 SETTING_BUTTON: '.video-settings-button' // 设置按钮 }; // 样式注入 GM_addStyle(` .bili-helper-pro { position: fixed; right: 25px; bottom: 25px; z-index: 2147483647; } .main-btn-pro { width: 56px; height: 56px; background: #00a1d6; border-radius: 50%; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .main-btn-pro:hover { transform: scale(1.15) rotate(15deg); background: #0091c8; } .menu-pro { position: absolute; right: 0; bottom: 70px; width: 200px; background: #fff; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); opacity: 0; transform: translateY(10px); transition: all 0.3s ease; } .menu-pro.show { opacity: 1; transform: translateY(0); } .menu-item-pro { padding: 14px 20px; font-size: 14px; color: #1a1a1a; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; } .menu-item-pro:hover { background: #f5f5f7; padding-left: 25px; } .input-modal-pro { /* 保持原有输入框样式 */ } .input-modal-pro { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); z-index: 2147483647; min-width: 300px; } .url-input { width: 100%; padding: 8px; margin: 10px 0; border: 1px solid #ddd; } .btn-group { display: flex; gap: 10px; margin-top: 15px; } .confirm-btn, .cancel-btn { flex:1; padding: 8px; background: #00a1d6; color: white; border: none; border-radius: 4px; } .cancel-btn { background: #f0f0f0; color: #333; } .close-btn { margin-top: 15px; padding: 8px 16px; background: #00a1d6; color: white; border-radius: 4px; } .about-modal-pro { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); z-index: 2147483647; min-width: 400px; max-width: 500px; font-family: Arial, sans-serif; cursor: move; } .about-modal-pro h3 { margin: 0 0 15px; font-size: 20px; color: #00a1d6; } .about-modal-pro p { margin: 10px 0; font-size: 14px; color: #333; } .about-modal-pro ul { margin: 10px 0; padding-left: 20px; } .about-modal-pro ul li { margin: 5px 0; font-size: 14px; color: #555; } .about-modal-pro a { color: #00a1d6; text-decoration: none; } .about-modal-pro a:hover { text-decoration: underline; } .close-btn { position: absolute; top: 10px; right: 10px; background: #f0f0f0; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; color: #333; } .close-btn:hover { background: #e0e0e0; } .guide-modal-pro { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); z-index: 2147483647; min-width: 500px; max-width: 600px; font-family: Arial, sans-serif; cursor: move; } .guide-modal-pro h2 { color: #00a1d6; margin: 0 0 15px; border-bottom: 2px solid #f0f0f0; padding-bottom: 10px; } .guide-content { max-height: 60vh; overflow-y: auto; padding-right: 10px; } .dont-show-again { display: flex; align-items: center; margin: 15px 0; font-size: 14px; } .dont-show-again input { margin-right: 8px; } .progress-container { position: fixed; left: 20px; bottom: 20px; width: 300px; background: rgba(255,255,255,0.9); border-radius: 8px; padding: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 2147483646; display: none; } .progress-bar { height: 12px; background: #e0e0e0; border-radius: 6px; overflow: hidden; margin: 8px 0; } .progress-fill { height: 100%; background: #00a1d6; width: 0%; transition: width 0.3s ease; } .progress-text { font-size: 12px; color: #666; text-align: center; } `); // 主界面 const createUI = () => { const container = document.createElement('div'); container.className = 'bili-helper-pro'; const mainBtn = document.createElement('div'); mainBtn.className = 'main-btn-pro'; mainBtn.innerHTML = '🛡️'; const menu = document.createElement('div'); menu.className = 'menu-pro'; // 菜单项 const menuItems = [ { name: '使用必看', icon: '📘', action: showGuide }, { name: 'TXT导入', icon: '📁', action: handleLocalImport }, { name: '在线导入', icon: '🌐', action: handleWebImport }, { name: '屏蔽词库', icon: '📚', action: () => { // 在新标签页打开规则仓库 window.open( 'https://github.com/xingguang2333/BiliBlocklistImporter/tree/main/Blocklist', '_blank', 'noopener,noreferrer' ); // 可选:添加点击统计 console.log('[统计] 屏蔽词库菜单点击'); } }, { name: '关于', icon: 'ℹ️', action: showAbout } ]; menuItems.forEach(item => { const btn = document.createElement('div'); btn.className = 'menu-item-pro'; btn.innerHTML = `${item.icon} ${item.name}`; btn.addEventListener('click', item.action); menu.appendChild(btn); }); // 事件绑定 mainBtn.addEventListener('click', (e) => { e.stopPropagation(); menu.classList.toggle('show'); }); document.addEventListener('click', () => { menu.classList.remove('show'); }); container.appendChild(menu); container.appendChild(mainBtn); document.body.appendChild(container); const progressContainer = document.createElement('div'); progressContainer.className = 'progress-container'; progressContainer.innerHTML = ` <div class="progress-bar"> <div class="progress-fill"></div> </div> <div class="progress-text">准备就绪</div> `; document.body.appendChild(progressContainer); }; // 本地导入处理 const handleLocalImport = async () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.txt'; input.onchange = async (e) => { const file = e.target.files[0]; try { const text = await file.text(); await processRules(text); showToast('本地导入成功!', 'success'); } catch (err) { showToast(`导入失败: ${err}`, 'error'); } }; input.click(); }; // 在线导入处理 const handleWebImport = () => { const modal = document.createElement('div'); modal.className = 'input-modal-pro'; modal.innerHTML = ` <h3>在线导入</h3> <input type="url" placeholder="输入TXT文件URL" class="url-input"> <div class="btn-group"> <button class="confirm-btn">确定</button> <button class="cancel-btn">取消</button> </div> `; modal.querySelector('.confirm-btn').onclick = async () => { const url = modal.querySelector('.url-input').value; if (!url) return; try { const text = await fetchWithRetry(url, CONFIG.RETRY_TIMES); await processRules(text); showToast('在线导入成功!', 'success'); } catch (err) { showToast(`导入失败: ${err}`, 'error'); } modal.remove(); }; modal.querySelector('.cancel-btn').onclick = () => modal.remove(); document.body.appendChild(modal); }; const processRules = async (text) => { const progressContainer = document.querySelector('.progress-container'); const progressFill = document.querySelector('.progress-fill'); const progressText = document.querySelector('.progress-text'); try { // 显示进度条 progressContainer.style.display = 'block'; progressFill.style.width = '0%'; progressText.textContent = '初始化中...'; // 展开屏蔽设置 if (!await checkCurrentSetting()) { await expandSettingsPanel(); await selectBlockSetting(); } // 执行规则导入 const rules = text.split('\n').filter(l => l.trim()); let successCount = 0; const total = rules.length; for (let i = 0; i < rules.length; i++) { const rule = rules[i]; try { await waitForElement(SELECTORS.BLOCK_INPUT, CONFIG.MAX_WAIT_TIME); const input = document.querySelector(SELECTORS.BLOCK_INPUT); const addBtn = document.querySelector(SELECTORS.ADD_BUTTON); if (!input || !addBtn) throw new Error('页面元素未找到'); input.value = rule.trim(); addBtn.click(); successCount++; // 更新进度 const progress = ((i + 1) / total * 100).toFixed(1); progressFill.style.width = `${progress}%`; progressText.textContent = `处理中 ${i + 1}/${total} (${progress}%)`; await wait(CONFIG.CHECK_INTERVAL); } catch (err) { console.error(`规则 "${rule}" 导入失败:`, err); } } // 完成时更新状态 progressFill.style.width = '100%'; progressText.textContent = `完成!成功导入 ${successCount}/${total} 条规则`; // 3秒后隐藏进度条 setTimeout(() => { progressContainer.style.display = 'none'; }, 3000); } catch (error) { // 错误处理 progressContainer.style.display = 'none'; showToast(`导入中断: ${error.message}`, 'error'); throw error; } }; // 辅助函数 const checkCurrentSetting = async () => { const current = document.querySelector(SELECTORS.CURRENT_SETTING); return current?.textContent.includes('屏蔽设定'); }; const expandSettingsPanel = async () => { const dropdown = document.querySelector(SELECTORS.DROPDOWN_WRAP); if (dropdown) { dropdown.classList.add('bui-dropdown-unfold'); await wait(800); } }; const selectBlockSetting = async () => { const items = await waitForElements(SELECTORS.MENU_ITEM); const target = Array.from(items).find(el => el.textContent.match(/屏蔽设定|block setting/i) ); if (target) { target.click(); await wait(1000); } }; const wait = (ms) => new Promise(r => setTimeout(r, ms)); const waitForElement = (selector, timeout = 5000) => { return new Promise((resolve, reject) => { const start = Date.now(); const check = () => { const el = document.querySelector(selector); if (el) return resolve(el); if (Date.now() - start > timeout) { reject(new Error(`Element ${selector} not found`)); } else { setTimeout(check, 100); } }; check(); }); }; const waitForElements = (selector) => waitForElement(selector).then(() => document.querySelectorAll(selector) ); const fetchWithRetry = async (url, retries = 3) => { for (let i = 0; i < retries; i++) { try { return await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(url)}`, onload: (res) => res.status === 200 ? resolve(res.responseText) : reject(new Error(`HTTP ${res.status}`)), onerror: reject }); }); } catch (err) { if (i === retries - 1) throw err; await wait(1000 * (i + 1)); } } }; const showToast = (message, type = 'info') => { const colors = { info: '#2196F3', success: '#4CAF50', error: '#F44336' }; const toast = document.createElement('div'); toast.textContent = message; toast.style.bottom = '150px'; toast.style.cssText = ` position: fixed; bottom: 150px; right: 30px; background: ${colors[type]}; color: white; padding: 12px 24px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); animation: slideIn 0.3s ease-out; `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); }; const showGuide = (isFirstTime = false) => { // 添加首页元素存在性检测 if (!document.querySelector(SELECTORS.SETTING_BUTTON) && window.location.pathname === '/') { console.log('检测到当前为首页,跳过播放器元素检测'); } const modal = document.createElement('div'); modal.className = 'guide-modal-pro'; modal.innerHTML = ` <h2>📖 使用必读指南</h2> <div class="guide-content"> <p><strong>重要提示:</strong>使用本插件前请仔细阅读以下内容</p> <ul> <li>🔒 本插件仅用于学习交流,请勿用于商业用途</li> <li>⚙️ 导入规则前请确保格式为每行一个关键词</li> <li>🔄 更新插件后建议清除旧规则重新导入</li> <li>⚠️ 必须在视频页面导入才有效果</li> </ul> <p><strong>你可以用下面方式获得规则:</strong></p> <ol> <li><a href="https://github.com/xingguang2333/BiliBlocklistImporter/tree/main/Blocklist" target="_blank">屏蔽词库</a></li> <li></li> <li>热心b友分享,并把他们保存在一个txt文件,一行一个屏蔽词</li> </ol> </div> ${isFirstTime ? ` <div class="dont-show-again"> <input type="checkbox" id="dontShow"> <label for="dontShow">不再显示此提示</label> </div> ` : ''} <button class="bui-button bui-button-blue" style="width:100%"> ${isFirstTime ? '我已知晓,开始使用' : '关闭'} </button> <button class="close-btn">×</button> `; // 关闭功能 const closeModal = () => { if (isFirstTime) { const dontShow = modal.querySelector('#dontShow').checked; if (dontShow) { localStorage.setItem(CONFIG.FIRST_USE_KEY, '1'); } } modal.remove(); }; modal.querySelector('button.bui-button').onclick = closeModal; modal.querySelector('.close-btn').onclick = closeModal; // 拖动功能(复用about窗口的拖动逻辑) let isDragging = false; let offsetX, offsetY; modal.addEventListener('mousedown', (e) => { if (!e.target.closest('button, input, a')) { isDragging = true; offsetX = e.clientX - modal.offsetLeft; offsetY = e.clientY - modal.offsetTop; } }); document.addEventListener('mousemove', (e) => { if (isDragging) { modal.style.left = `${e.clientX - offsetX}px`; modal.style.top = `${e.clientY - offsetY}px`; } }); document.addEventListener('mouseup', () => isDragging = false); document.body.appendChild(modal); }; // 新增:首次访问检测 const checkFirstUse = () => { if (!localStorage.getItem(CONFIG.FIRST_USE_KEY)) { showGuide(true); } }; const showAbout = () => { const modal = document.createElement('div'); modal.className = 'about-modal-pro'; modal.innerHTML = ` <h3>B站弹幕屏蔽词导入 v1.0</h3> <p>一个可以快速导入Bilibili弹幕屏蔽词的油猴脚本,网页端导入并同步至移动端。A Tampermonkey script that can quickly import Bilibili subtitle blocking words, import on the web page and synchronize to the mobile terminal.</p> <p>相关链接:</p> <ul> <li><a href="https://moestars.top" target="_blank">个人网站</a></li> <li><a href="https://github.com/xingguang2333/BiliBlocklistImporter/" target="_blank">GitHub</a></li> <li><a href="https://greasyfork.org/zh-CN/scripts/your-script" target="_blank">GreasyFork</a></li> <li><a href="https://github.com/xingguang2333/BiliBlocklistImporter/tree/main/Blocklist" target="_blank">屏蔽词库</a></li> </ul> <button class="close-btn">×</button> `; // 关闭按钮 modal.querySelector('.close-btn').onclick = () => modal.remove(); // 拖动功能 let isDragging = false; let offsetX, offsetY; modal.addEventListener('mousedown', (e) => { if (e.target.tagName.toLowerCase() !== 'a') { // 避免拖动时误点击链接 isDragging = true; offsetX = e.clientX - modal.offsetLeft; offsetY = e.clientY - modal.offsetTop; } }); document.addEventListener('mousemove', (e) => { if (isDragging) { modal.style.left = `${e.clientX - offsetX}px`; modal.style.top = `${e.clientY - offsetY}px`; } }); document.addEventListener('mouseup', () => { isDragging = false; }); document.body.appendChild(modal); }; // 初始化 (function init() { const checkDOMLoaded = () => { if (document.readyState === 'complete' || document.readyState === 'interactive') { createUI(); checkFirstUse(); } else { document.addEventListener('DOMContentLoaded', () => { createUI(); checkFirstUse(); }); } }; // 无论当前页面类型都执行初始化 checkDOMLoaded(); })(); })();