屏蔽微博、知乎、B站、X(Twitter)含关键词的内容,支持自定义管理。内容默认折叠,点击可查看原内容,查看后可收起。支持导入/导出配置。
// ==UserScript==
// @name XBlock
// @namespace http://tampermonkey.net/
// @version 2.6.4
// @description 屏蔽微博、知乎、B站、X(Twitter)含关键词的内容,支持自定义管理。内容默认折叠,点击可查看原内容,查看后可收起。支持导入/导出配置。
// @author werflala
// @match https://www.zhihu.com/*
// @match https://www.xiaohongshu.com/*
// @match https://www.bilibili.com/
// @match https://www.bilibili.com/?*
// @match https://www.bilibili.com/v/*
// @match https://search.bilibili.com/*
// @match https://weibo.com/*
// @match https://www.weibo.com/*
// @match https://s.weibo.com/*
// @match https://twitter.com/*
// @match https://x.com/*
// @icon data:image/jpeg;base64,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
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 1. 通用默认关键词
const DEFAULT_KEYWORDS_COMMON = [
'coser','COSER','漫展','JK'
];
const DEFAULT_KEYWORDS_X = [
'sao的很','sao货','太涩了','30+'
];
const STORAGE_KEY = 'keyword_blocker_words';
const DISABLED_SITES_KEY = 'keyword_blocker_disabled_sites';
const MASK_BAR_MASTER_KEY = 'keyword_blocker_mask_bar_master';
const KEYWORD_MASK_VISIBILITY_KEY = 'keyword_blocker_keyword_mask_visibility';
function isMasterMaskSwitchOn() {
try {
const saved = localStorage.getItem(MASK_BAR_MASTER_KEY);
return saved ? JSON.parse(saved) === true : false;
} catch (e) {
return false;
}
}
function saveMasterMaskSwitch(enabled) {
localStorage.setItem(MASK_BAR_MASTER_KEY, JSON.stringify(!!enabled));
}
function loadKeywordMaskVisibility() {
try {
const saved = localStorage.getItem(KEYWORD_MASK_VISIBILITY_KEY);
const data = saved ? JSON.parse(saved) : {};
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
} catch (e) {
return {};
}
}
function saveKeywordMaskVisibility(map) {
localStorage.setItem(KEYWORD_MASK_VISIBILITY_KEY, JSON.stringify(map));
}
let KEYWORD_MASK_VISIBILITY = loadKeywordMaskVisibility();
function getKeywordMaskBarVisible(keyword) {
if (Object.prototype.hasOwnProperty.call(KEYWORD_MASK_VISIBILITY, keyword)) {
return !!KEYWORD_MASK_VISIBILITY[keyword];
}
return !isMasterMaskSwitchOn();
}
function setKeywordMaskBarVisible(keyword, visible) {
KEYWORD_MASK_VISIBILITY[keyword] = !!visible;
saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
}
function removeKeywordMaskBarVisible(keyword) {
if (Object.prototype.hasOwnProperty.call(KEYWORD_MASK_VISIBILITY, keyword)) {
delete KEYWORD_MASK_VISIBILITY[keyword];
saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
}
}
let PENDING_MASTER_MASK_SWITCH = null;
let PENDING_KEYWORD_MASK_VISIBILITY = null;
function getKeywordMaskBarVisibleForUI(keyword) {
const visibilityMap = PENDING_KEYWORD_MASK_VISIBILITY || KEYWORD_MASK_VISIBILITY;
const masterSwitch = PENDING_MASTER_MASK_SWITCH === null ? isMasterMaskSwitchOn() : PENDING_MASTER_MASK_SWITCH;
if (Object.prototype.hasOwnProperty.call(visibilityMap, keyword)) {
return !!visibilityMap[keyword];
}
return !masterSwitch;
}
// 获取当前网站类型
function getCurrentSite() {
const hostname = window.location.hostname;
if (hostname.includes('zhihu.com')) return 'zhihu';
if (hostname.includes('xiaohongshu.com')) return 'xiaohongshu';
if (hostname.includes('bilibili.com')) return 'bilibili';
if (hostname.includes('weibo.com')) return 'weibo';
if (hostname.includes('x.com') || hostname.includes('twitter.com')) return 'twitter';
return 'unknown';
}
// 加载关键词
function loadKeywords() {
let keywords = [];
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
keywords = JSON.parse(saved);
} else {
keywords = [...DEFAULT_KEYWORDS_COMMON];
}
if (getCurrentSite() === 'twitter') {
const isOldData = !keywords.includes('30+');
if (isOldData) {
const newXWords = DEFAULT_KEYWORDS_X.filter(word => !keywords.includes(word));
keywords.unshift(...newXWords);
saveKeywords(keywords);
}
}
return keywords;
} catch (e) {
console.error('加载屏蔽词失败:', e);
return [...DEFAULT_KEYWORDS_COMMON, ...DEFAULT_KEYWORDS_X];
}
}
function saveKeywords(keywords) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(keywords));
}
// 禁用网站管理
function saveDisabledSites(sites) {
localStorage.setItem(DISABLED_SITES_KEY, JSON.stringify(sites));
}
function loadDisabledSites() {
try {
const saved = localStorage.getItem(DISABLED_SITES_KEY);
return saved ? JSON.parse(saved) : [];
} catch (e) {
return [];
}
}
function isCurrentSiteDisabled() {
const disabledSites = loadDisabledSites();
return disabledSites.includes(getCurrentSite());
}
function disableCurrentSite() {
const disabledSites = loadDisabledSites();
const currentSite = getCurrentSite();
if (!disabledSites.includes(currentSite)) {
disabledSites.push(currentSite);
saveDisabledSites(disabledSites);
}
}
function enableCurrentSite() {
const disabledSites = loadDisabledSites();
const currentSite = getCurrentSite();
const index = disabledSites.indexOf(currentSite);
if (index > -1) {
disabledSites.splice(index, 1);
saveDisabledSites(disabledSites);
}
}
let BLOCK_KEYWORDS = loadKeywords();
// 网站特定的配置
const siteConfigs = {
zhihu: {
containerSelector: '.ContentItem',
titleSelector: '.ContentItem-title a',
logPrefix: '知乎'
},
xiaohongshu: {
containerSelector: 'section.note-item',
titleSelector: 'a.title, .title',
logPrefix: '小红书'
},
bilibili: {
containerSelector: '.bili-feed-card, .bili-video-card',
titleSelector: '.bili-video-card__info--tit, .bili-video-card__info--tit a',
logPrefix: 'B站'
},
weibo: {
containerSelector: '.wbpro-scroller-item',
titleSelector: '.wbpro-feed-content .detail_wbtext_4CRf9',
logPrefix: '微博'
},
twitter: {
containerSelector: 'div[data-testid="cellInnerDiv"]',
titleSelector: 'div[data-testid="tweetText"]',
logPrefix: 'X(Twitter)'
}
};
// --- UI 创建部分 ---
function createManagementUI() {
const style = document.createElement('style');
style.textContent = `
/* 非 Twitter 的浮动按钮样式隔离类名 */
.kb-floating-btn { position: fixed; left: 12px; top: 50%; transform: translateY(-50%); z-index: 10000; background: #1890ff; color: white; border: none; border-radius: 5px; padding: 8px 6px; cursor: pointer; font-size: 12px; line-height: 1.1; box-shadow: 0 2px 6px rgba(0,0,0,0.15); transition: all 0.3s ease; writing-mode: vertical-lr; text-orientation: mixed; }
.kb-floating-btn:hover { background: #40a9ff; transform: translateY(-50%) scale(1.05); }
#keyword-blocker-panel { position: fixed; left: -350px; top: 50%; transform: translateY(-50%); z-index: 9999; width: 320px; max-height: 80vh; background: white; border: 1px solid #d9d9d9; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.15); transition: left 0.3s ease; font-family: sans-serif; display: flex; flex-direction: column; }
#keyword-blocker-panel.show { left: 20px; }
.kb-panel-header { padding: 16px 40px 10px 16px; border-bottom: 1px solid #f0f0f0; background: #fafafa; border-radius: 8px 8px 0 0; flex-shrink: 0; }
.kb-title-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.kb-panel-title { margin: 0; font-size: 16px; font-weight: 500; color: #262626; flex: 1; }
.kb-input-group { display: flex; gap: 8px; }
.kb-input { flex: 1; padding: 8px 12px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 14px; outline: none; }
.kb-input:focus { border-color: #1890ff; }
.kb-btn { padding: 8px 16px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; flex-shrink: 0; }
.kb-list-container { flex: 1; overflow-y: auto; min-height: 0; }
.kb-list { list-style: none; margin: 0; padding: 0; }
.kb-list-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #f0f0f0; }
.kb-keyword { flex: 1; font-size: 14px; color: #262626; word-break: break-all; margin-right: 8px; }
.kb-action-group { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.kb-visibility-btn { padding: 4px 8px; background: #f5f5f5; color: #666; border: 1px solid #d9d9d9; border-radius: 3px; cursor: pointer; font-size: 12px; flex-shrink: 0; transition: all 0.2s; }
.kb-visibility-btn.active { background: #e6f4ff; color: #1890ff; border-color: #91caff; }
.kb-delete-btn { padding: 4px 8px; background: #ff4d4f; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; flex-shrink: 0; }
.kb-stats { padding: 12px 16px; background: #f9f9f9; border-top: 1px solid #f0f0f0; font-size: 12px; color: #666; text-align: center; border-radius: 0 0 8px 8px; flex-shrink: 0; }
.kb-close-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 18px; cursor: pointer; color: #999; }
.kb-disable-site-btn { padding: 4px 8px; background: #8c8c8c; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 10px; margin-left: 12px; }
.kb-confirm-group { display: flex; gap: 8px; }
.kb-confirm-btn { padding: 4px 8px; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; color: white;}
.kb-confirm-delete { background: #ff4d4f; }
.kb-confirm-cancel { background: #8c8c8c; }
/* 数据管理行样式 */
.kb-data-row { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; }
.kb-data-btn { padding: 4px 10px; background: #fff; border: 1px solid #d9d9d9; border-radius: 4px; cursor: pointer; font-size: 12px; color: #666; transition: all 0.2s; }
.kb-data-btn:hover { color: #1890ff; border-color: #1890ff; }
.kb-setting-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 10px; }
.kb-setting-label { font-size: 12px; color: #666; }
.kb-setting-btn { padding: 4px 10px; background: #fff; border: 1px solid #d9d9d9; border-radius: 4px; cursor: pointer; font-size: 12px; color: #666; transition: all 0.2s; }
.kb-setting-btn:hover { color: #1890ff; border-color: #1890ff; }
.kb-setting-btn.active { color: #1890ff; border-color: #1890ff; background: #e6f4ff; }
.kb-save-btn { padding: 5px 12px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
.kb-save-btn:hover { background: #40a9ff; }
/* --- 遮罩层与收起按钮样式 --- */
.kb-mask-overlay {
background: #f7f7f7;
border: 1px dashed #d9d9d9;
border-radius: 8px;
padding: 4px 10px;
margin: 5px 0;
cursor: pointer;
text-align: center;
font-size: 13px;
color: #999;
user-select: none;
transition: all 0.2s;
display: flex;
justify-content: center;
align-items: center;
min-height: 24px;
width: 100%;
box-sizing: border-box;
}
.kb-mask-overlay:hover {
background: #f0f0f0;
color: #666;
border-color: #bbb;
}
.kb-mask-keyword {
font-weight: bold;
color: #ff4d4f;
margin: 0 4px;
background: rgba(255, 77, 79, 0.1);
padding: 0 4px;
border-radius: 3px;
}
.kb-hidden-content {
display: none !important;
}
/* 收起按钮 */
.kb-remask-btn {
display: block;
width: 100%;
text-align: center;
padding: 2px 0;
margin-top: 5px;
background: #f5f5f5;
color: #888;
font-size: 12px;
cursor: pointer;
border-radius: 4px;
border: 1px solid #eee;
transition: all 0.2s;
user-select: none;
}
.kb-remask-btn:hover {
background: #e6e6e6;
color: #555;
}
`;
document.head.appendChild(style);
// 为了避免 Twitter 元素继承了浮动按钮的 CSS 导致样式错乱,我们给予它独立的 ID
const isTwitter = getCurrentSite() === 'twitter';
const toggleBtnId = isTwitter ? 'keyword-blocker-toggle-twitter' : 'keyword-blocker-toggle';
const toggleBtn = document.createElement(isTwitter ? 'a' : 'button');
toggleBtn.id = toggleBtnId;
if (isTwitter) {
document.body.classList.add('kb-twitter-mode');
toggleBtn.style.display = 'none';
document.body.appendChild(toggleBtn);
ensureTwitterTogglePlacement();
} else {
toggleBtn.className = 'kb-floating-btn';
toggleBtn.textContent = '屏蔽词';
document.body.appendChild(toggleBtn);
}
const panel = document.createElement('div');
panel.id = 'keyword-blocker-panel';
const siteNames = {'zhihu':'知乎','xiaohongshu':'小红书','bilibili':'B站','weibo':'微博','twitter':'X / Twitter'};
const siteName = siteNames[getCurrentSite()] || '当前网站';
const isDisabled = isCurrentSiteDisabled();
const masterMaskSwitchOn = isMasterMaskSwitchOn();
panel.innerHTML = `
<button class="kb-close-btn" id="kb-close">×</button>
<div class="kb-panel-header">
<div class="kb-title-row">
<h3 class="kb-panel-title">${isDisabled ? '⚠️ 屏蔽已暂停' : '屏蔽词管理'}</h3>
<button class="kb-disable-site-btn" id="kb-disable-site">${isDisabled ? '启用屏蔽' : '暂停屏蔽'}</button>
</div>
<div class="kb-input-group">
<input type="text" id="kb-input" class="kb-input" placeholder="添加关键词..." />
<button id="kb-add-btn" class="kb-btn">新增</button>
</div>
<div class="kb-setting-row">
<span class="kb-setting-label">屏蔽条总开关(开=默认隐藏)</span>
<button id="kb-mask-master-toggle" class="kb-setting-btn ${masterMaskSwitchOn ? 'active' : ''}">${masterMaskSwitchOn ? '开启' : '关闭'}</button>
</div>
<div class="kb-setting-row">
<span class="kb-setting-label">切换后点保存才生效</span>
<button id="kb-save-visibility-btn" class="kb-save-btn">保存并刷新</button>
</div>
<div class="kb-data-row">
<button id="kb-export-btn" class="kb-data-btn">导出配置</button>
<button id="kb-import-btn" class="kb-data-btn">导入配置</button>
<input type="file" id="kb-import-input" style="display:none" accept=".json">
</div>
</div>
<div class="kb-list-container"><ul id="kb-list" class="kb-list"></ul></div>
<div class="kb-stats">当前 ${siteName} 共有 <span id="kb-count">0</span> 个屏蔽词</div>
`;
document.body.appendChild(panel);
return { toggleBtn, panel };
}
// X (Twitter) 原生导航项克隆对齐逻辑,完全隔离非Twitter的CSS,保证继承100%原生样式
function ensureTwitterTogglePlacement() {
if (getCurrentSite() !== 'twitter') return;
try {
const currentToggle = document.getElementById('keyword-blocker-toggle-twitter');
if (!currentToggle) return;
const nav = document.querySelector('nav[aria-label="主要"]') || document.querySelector('nav[role="navigation"]');
const bookmarkLink = nav?.querySelector('a[href="/i/bookmarks"]');
if (!nav || !bookmarkLink || !bookmarkLink.firstElementChild) return;
const signature = bookmarkLink.className + '|' + (bookmarkLink.firstElementChild.className || '');
if (currentToggle.dataset.kbTwitterSignature !== signature) {
// 原汁原味克隆节点,这样就不会丢掉任何原生 flex 排版和边距类名了
const clonedLink = bookmarkLink.cloneNode(true);
clonedLink.id = 'keyword-blocker-toggle-twitter';
clonedLink.setAttribute('href', '#');
clonedLink.setAttribute('aria-label', '屏蔽词');
clonedLink.removeAttribute('data-testid');
// 清空克隆带来的所有可能导致问题的内联样式
clonedLink.style.cssText = '';
const textWrap = clonedLink.querySelector('div[dir="ltr"]');
const textSpans = textWrap ? textWrap.querySelectorAll('span') : [];
if (textSpans[0]) textSpans[0].textContent = '屏蔽词';
// 替换 SVG,恢复为你 2.6.0 原本的那个未加修饰的图标代码
const svg = clonedLink.querySelector('svg');
if (svg) {
svg.setAttribute('viewBox', '0 0 24 24');
svg.innerHTML = '<g><path d="M3 7.75C3 6.78 3.78 6 4.75 6h14.5c.97 0 1.75.78 1.75 1.75S20.22 9.5 19.25 9.5H4.75C3.78 9.5 3 8.72 3 7.75zm0 8.5C3 15.28 3.78 14.5 4.75 14.5h14.5c.97 0 1.75.78 1.75 1.75S20.22 18 19.25 18H4.75C3.78 18 3 17.22 3 16.25z"></path><circle cx="8" cy="7.75" r="2.25" fill="white"></circle><circle cx="16" cy="16.25" r="2.25" fill="white"></circle><path d="M8 4.5a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5zm0 1.5a1.75 1.75 0 1 1 0 3.5A1.75 1.75 0 0 1 8 6zm8 7a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5zm0 1.5a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5z"></path></g>';
}
clonedLink.dataset.kbTwitterSignature = signature;
clonedLink.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation(); // 关键修正:阻止事件冒泡到全局,避免瞬间被 document.onclick 再次关掉
const panel = document.getElementById('keyword-blocker-panel');
if (panel) panel.classList.toggle('show');
});
currentToggle.replaceWith(clonedLink);
}
// 重新获取并定位到书签的下一行
const finalToggle = document.getElementById('keyword-blocker-toggle-twitter');
if (bookmarkLink.nextElementSibling !== finalToggle) {
nav.insertBefore(finalToggle, bookmarkLink.nextSibling);
}
finalToggle.style.display = ''; // 确保可见
} catch (e) {
console.error('X 按钮定位失败:', e);
}
}
function renderKeywordList() {
const list = document.getElementById('kb-list');
const count = document.getElementById('kb-count');
if (!list) return;
list.innerHTML = '';
count.textContent = BLOCK_KEYWORDS.length;
BLOCK_KEYWORDS.forEach((keyword, index) => {
const li = document.createElement('li');
li.className = 'kb-list-item';
li.dataset.index = index;
const isVisible = getKeywordMaskBarVisibleForUI(keyword);
li.innerHTML = `<span class="kb-keyword">${keyword}</span><div class="kb-action-group"><button class="kb-visibility-btn ${isVisible ? 'active' : ''}" data-index="${index}">${isVisible ? '显示条' : '隐藏条'}</button><button class="kb-delete-btn" data-index="${index}">删除</button></div>`;
list.appendChild(li);
});
}
// UI 事件绑定
function initUIEvents() {
const toggleBtnId = getCurrentSite() === 'twitter' ? 'keyword-blocker-toggle-twitter' : 'keyword-blocker-toggle';
const toggleBtn = document.getElementById(toggleBtnId);
const panel = document.getElementById('keyword-blocker-panel');
const closeBtn = document.getElementById('kb-close');
const addBtn = document.getElementById('kb-add-btn');
const input = document.getElementById('kb-input');
const list = document.getElementById('kb-list');
const disableSiteBtn = document.getElementById('kb-disable-site');
const exportBtn = document.getElementById('kb-export-btn');
const importBtn = document.getElementById('kb-import-btn');
const importInput = document.getElementById('kb-import-input');
const maskMasterToggleBtn = document.getElementById('kb-mask-master-toggle');
const saveVisibilityBtn = document.getElementById('kb-save-visibility-btn');
if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
const toggle = () => {
panel.classList.toggle('show');
const currentToggleBtn = document.getElementById(toggleBtnId);
if (getCurrentSite() !== 'twitter' && currentToggleBtn) {
currentToggleBtn.style.display = panel.classList.contains('show') ? 'none' : 'block';
}
};
if (toggleBtn) toggleBtn.onclick = toggle;
closeBtn.onclick = toggle;
document.onclick = (e) => {
// 每次点击都需要重新获取当前 DOM 里的按钮(防止 Twitter 下因重绘导致的旧节点失效)
const currentToggleBtn = document.getElementById(toggleBtnId);
if (panel.classList.contains('show') && !panel.contains(e.target) && (!currentToggleBtn || !currentToggleBtn.contains(e.target))) {
toggle();
}
};
addBtn.onclick = (e) => {
e.stopPropagation();
const val = input.value.trim();
if(!val) return;
const words = val.split(/[,,/]/).map(w => w.trim()).filter(w => w && !BLOCK_KEYWORDS.includes(w));
if(words.length) {
BLOCK_KEYWORDS.unshift(...words);
saveKeywords(BLOCK_KEYWORDS);
renderKeywordList();
input.value = '';
}
};
input.onkeypress = (e) => { if(e.key === 'Enter') addBtn.click(); };
PENDING_MASTER_MASK_SWITCH = isMasterMaskSwitchOn();
PENDING_KEYWORD_MASK_VISIBILITY = { ...KEYWORD_MASK_VISIBILITY };
if (maskMasterToggleBtn) {
maskMasterToggleBtn.onclick = (e) => {
e.stopPropagation();
const nextState = !PENDING_MASTER_MASK_SWITCH;
PENDING_MASTER_MASK_SWITCH = nextState;
maskMasterToggleBtn.textContent = nextState ? '开启' : '关闭';
maskMasterToggleBtn.classList.toggle('active', nextState);
renderKeywordList();
};
}
if (saveVisibilityBtn) {
saveVisibilityBtn.onclick = (e) => {
e.stopPropagation();
saveMasterMaskSwitch(PENDING_MASTER_MASK_SWITCH);
KEYWORD_MASK_VISIBILITY = { ...PENDING_KEYWORD_MASK_VISIBILITY };
saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
location.reload();
};
}
list.onclick = (e) => {
e.stopPropagation();
const idx = parseInt(e.target.dataset.index);
if (Number.isNaN(idx)) return;
const keyword = BLOCK_KEYWORDS[idx];
if (e.target.classList.contains('kb-visibility-btn')) {
const nextVisible = !getKeywordMaskBarVisibleForUI(keyword);
PENDING_KEYWORD_MASK_VISIBILITY[keyword] = nextVisible;
renderKeywordList();
} else if (e.target.classList.contains('kb-delete-btn')) {
const li = e.target.closest('li');
li.innerHTML = `<span class="kb-keyword">${keyword}</span><div class="kb-action-group"><button class="kb-confirm-btn kb-confirm-delete" data-index="${idx}">确认</button><button class="kb-confirm-btn kb-confirm-cancel" data-index="${idx}">取消</button></div>`;
} else if (e.target.classList.contains('kb-confirm-delete')) {
delete PENDING_KEYWORD_MASK_VISIBILITY[keyword];
removeKeywordMaskBarVisible(keyword);
BLOCK_KEYWORDS.splice(idx, 1);
saveKeywords(BLOCK_KEYWORDS);
renderKeywordList();
} else if (e.target.classList.contains('kb-confirm-cancel')) {
renderKeywordList();
}
};
disableSiteBtn.onclick = (e) => {
e.stopPropagation();
if(isCurrentSiteDisabled()) {
enableCurrentSite();
alert('已启用屏蔽,请刷新生效');
} else {
disableCurrentSite();
alert('已暂停当前网站屏蔽,请刷新页面生效');
}
toggle();
};
exportBtn.onclick = (e) => {
e.stopPropagation();
try {
const data = {
keywords: BLOCK_KEYWORDS,
maskBarMasterSwitch: isMasterMaskSwitchOn(),
keywordMaskVisibility: KEYWORD_MASK_VISIBILITY
};
const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'keyword-blocker-data.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err) {
console.error('导出失败:', err);
alert('导出失败,请查看控制台');
}
};
importBtn.onclick = (e) => {
e.stopPropagation();
importInput.click();
};
importInput.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const parsed = JSON.parse(event.target.result);
let importedKeywords = [];
if (Array.isArray(parsed)) {
importedKeywords = parsed;
} else if (parsed && Array.isArray(parsed.keywords)) {
importedKeywords = parsed.keywords;
if (typeof parsed.maskBarMasterSwitch === 'boolean') {
saveMasterMaskSwitch(parsed.maskBarMasterSwitch);
}
if (parsed.keywordMaskVisibility && typeof parsed.keywordMaskVisibility === 'object' && !Array.isArray(parsed.keywordMaskVisibility)) {
KEYWORD_MASK_VISIBILITY = { ...KEYWORD_MASK_VISIBILITY, ...parsed.keywordMaskVisibility };
PENDING_KEYWORD_MASK_VISIBILITY = { ...KEYWORD_MASK_VISIBILITY };
saveKeywordMaskVisibility(KEYWORD_MASK_VISIBILITY);
}
} else {
alert('导入失败:JSON 格式不正确');
return;
}
const oldLength = BLOCK_KEYWORDS.length;
BLOCK_KEYWORDS = [...new Set([...BLOCK_KEYWORDS, ...importedKeywords])];
saveKeywords(BLOCK_KEYWORDS);
renderKeywordList();
const addedCount = BLOCK_KEYWORDS.length - oldLength;
alert(`导入成功:新增 ${addedCount} 个关键词,当前共 ${BLOCK_KEYWORDS.length} 个关键词`);
location.reload();
} catch (err) {
console.error('导入失败:', err);
alert('导入失败:请确保是正确的 JSON 文件');
}
importInput.value = '';
};
reader.readAsText(file);
};
}
// --- 核心屏蔽逻辑 ---
function createMaskElement(keyword, onClick) {
const mask = document.createElement('div');
mask.className = 'kb-mask-overlay';
mask.innerHTML = `<span>🙈 已折叠包含 <span class="kb-mask-keyword">${keyword}</span> 的内容 (点击查看)</span>`;
mask.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
onClick(mask);
};
return mask;
}
function createRemaskButton(onRemask) {
const btn = document.createElement('div');
btn.className = 'kb-remask-btn';
btn.innerText = '🔼 收起 (重新屏蔽)';
btn.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
onRemask();
btn.remove();
};
return btn;
}
function processContentElement(element, config) {
if (element.getAttribute('data-kb-status') === 'processed') return;
const site = getCurrentSite();
if (site === 'bilibili') {
if (element.classList.contains('bili-video-card') && element.classList.contains('is-rcmd') && !element.classList.contains('enable-no-interest')) {
element.style.display = 'none';
return;
}
}
const titleEl = element.querySelector(config.titleSelector);
const text = titleEl ? titleEl.textContent : element.textContent;
const matchedKeyword = BLOCK_KEYWORDS.find(k => text.includes(k));
if (!matchedKeyword) return;
element.setAttribute('data-kb-status', 'processed');
let targetContent = element;
let insertParent = element.parentNode;
if (site === 'zhihu' && element.closest('.Card.TopstoryItem')) {
targetContent = element.closest('.Card.TopstoryItem');
insertParent = targetContent.parentNode;
targetContent.setAttribute('data-kb-status', 'processed');
} else if (site === 'bilibili') {
targetContent = element.closest('.feed-card') || element.closest('.bili-feed-card') || element;
insertParent = targetContent.parentNode;
} else if (site === 'twitter') {
targetContent = element;
insertParent = element;
}
const showMaskBar = getKeywordMaskBarVisible(matchedKeyword);
if (!showMaskBar) {
targetContent.style.display = 'none';
return;
}
const hideTarget = () => {
if (site === 'twitter') {
Array.from(targetContent.children).forEach(child => {
if (!child.classList.contains('kb-mask-overlay') && !child.classList.contains('kb-remask-btn')) {
child.style.display = 'none';
}
});
} else {
targetContent.classList.add('kb-hidden-content');
}
};
const showTarget = () => {
if (site === 'twitter') {
Array.from(targetContent.children).forEach(child => {
if (!child.classList.contains('kb-mask-overlay') && !child.classList.contains('kb-remask-btn')) {
child.style.display = '';
}
});
} else {
targetContent.classList.remove('kb-hidden-content');
}
};
let mask = null;
const handleRemask = () => {
hideTarget();
if (mask) mask.style.display = 'flex';
const existingRemaskBtn = targetContent.querySelector('.kb-remask-btn');
if (existingRemaskBtn) existingRemaskBtn.remove();
};
const handleReveal = () => {
showTarget();
if (mask) mask.style.display = 'none';
const existingRemaskBtn = targetContent.querySelector('.kb-remask-btn');
if (existingRemaskBtn) existingRemaskBtn.remove();
const remaskBtn = createRemaskButton(handleRemask);
targetContent.appendChild(remaskBtn);
};
mask = createMaskElement(matchedKeyword, handleReveal);
hideTarget();
if (site === 'twitter') {
targetContent.appendChild(mask);
} else if (insertParent) {
insertParent.insertBefore(mask, targetContent);
}
}
function processAllContent() {
const site = getCurrentSite();
const config = siteConfigs[site];
if (!config) return;
const elements = document.querySelectorAll(config.containerSelector);
elements.forEach(el => processContentElement(el, config));
}
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
const debouncedProcess = debounce(processAllContent, 500);
// 主程序
function init() {
if (isCurrentSiteDisabled()) {
console.log(`屏蔽器: ${getCurrentSite()} 已禁用`);
if (document.body) createManagementUI();
else window.addEventListener('DOMContentLoaded', createManagementUI);
if(document.body) initUIEvents();
else window.addEventListener('load', initUIEvents);
return;
}
console.log(`屏蔽器已启动: ${getCurrentSite()}`);
setInterval(() => {
processAllContent();
if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
}, 2000);
const observerConfig = { childList: true, subtree: true };
const observer = new MutationObserver((mutations) => {
let shouldRun = false;
for(let m of mutations) {
if(m.addedNodes.length) shouldRun = true;
}
if(shouldRun) {
debouncedProcess();
if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
}
});
const startObserver = () => {
if (document.body) {
observer.observe(document.body, observerConfig);
createManagementUI();
renderKeywordList();
initUIEvents();
processAllContent();
if (getCurrentSite() === 'twitter') ensureTwitterTogglePlacement();
} else {
setTimeout(startObserver, 1000);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startObserver);
} else {
startObserver();
}
window.addEventListener('scroll', () => {
setTimeout(debouncedProcess, 300);
}, { passive: true });
}
init();
})();