// ==UserScript==
// @name Twitter自用增强
// @namespace http://viayoo.com/bhivy
// @version 3.4
// @description 自用脚本,干掉部分机械人发言,过滤推特(Twitter)评论区,去广告,尝试解锁敏感限制。
// @author Via && Deepseek
// @match https://x.com/*
// @exclude https://x.com/*/status/*/video/*
// @exclude https://x.com/*/status/*/photo/*
// @exclude https://x.com/*/status/*/mediaViewer*
// @exclude https://x.com/settings
// @exclude https://x.com/i/*/creators/
// @exclude https://x.com/i/bookmarks
// @exclude https://x.com/i/premium-business
// @exclude https://x.com/*/lists
// @exclude https://x.com/i/follow_people
// @exclude https://x.com/i/chat
// @exclude https://x.com/notifications
// @exclude https://x.com/i/grok
// @icon data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA0pJREFUWAntVk1oE1EQnnlJbFK3KUq9VJPYWgQVD/5QD0qpfweL1YJQoZAULBRPggp6kB78PQn14kHx0jRB0UO9REVFb1YqVBEsbZW2SbVS0B6apEnbbMbZ6qbZdTempqCHPAjvzcw3P5mdmfcAiquYgX+cAVwu/+5AdDMQnSPCHUhQA0hf+Rxy2OjicIvzm+qnKhito0qpb2wvJhWeJgCPP7oPELeHvdJ1VSGf3eOPnSWga0S0Qo9HxEkEusDBuNjbEca8G291nlBxmgDc/ukuIvAJxI6wr+yKCsq1ewLxQ2lZfpQLo8oQ4ZXdCkfnACrGWpyDCl+oQmVn5xuVPU102e2P3qoJkFOhzVb9S7KSnL5jJs/mI+As01PJFPSlZeFSZZoAGBRXBZyq9lk5NrC+e7pJ5en30c+JWk59pZ5vRDOuhAD381c/H/FKz1SMNgCE16rg505r5TT0uLqme93d0fbq+1SeLSeU83Ke0RHYFPGVPcjQfNDUwIa7M665+dQAEEjZoMwZMcEF9RxIDAgBQ2mCcqJ0Z0b+h4MNbZ4RnyOSDbNmE2iRk5jCNgIIckFoZAs4IgfLGrlKGjkzS16iwj6pV9I4mUvCPf73JVytH9nRJj24QHrqU8NCIWrMaGqAC+Ut/3ZzAS63cx4v2K/x/IvQBOCwWzu5KmJGwEJ5PIgeG9nQBDDcXPpFoDjJ7ThvBC6EZxXWkJG+JgAFwGM4KBAOcibeGCn8FQ/hyajXPmSk+1sACogn4hYk7OdiHDFSWipPkPWSmY6mCzIghEEuxJvcEYUvxIdhX2mvmSHDDPBF9AJRnDZTyp+P40671JYLbxiAohDxSTfQIg4oNxgPzCWPHaWQBViOf2jGqVwBaEaxGbAqOFMrp+SefC8eNhoFIY5lXzpmtnMGUB2IbU3JdIqVW9m5zcxINn/hAYKiIexdaTh4srHKORMAP0b28PNgJyGt5gvHzQVYx91QpVcwpRFl/p63HSR1DLbid1OcTpAJQOG7u+KH+aI5Qwj13IsamU5vkUSIc8uGLDa8OtoivV8U5HcydFLtT7hlSDVy2nfxI2Ibg9awuVU8IeJAOMF5m2B6jFs1tM5R9rS3GRP5uSuiihn4DzPwA7z7GDH+43gqAAAAAElFTkSuQmCC
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const api = {
getValue: (key, def) => {
if (typeof GM_getValue !== 'undefined') return GM_getValue(key, def);
const val = localStorage.getItem(key);
return val === null ? def : JSON.parse(val);
},
setValue: (key, val) => {
if (typeof GM_setValue !== 'undefined') { GM_setValue(key, val); }
else { localStorage.setItem(key, JSON.stringify(val)); }
},
setClipboard: (text) => {
if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); }
else { navigator.clipboard.writeText(text); }
}
};
const DEFAULT_CONFIG = {
badUsers: ["1-5线",".约炮","@ruantang992","@xiaonm88","MrMinstrel","chu男无偿","上门服务","上门破处","免费约p","免费破处","同城上门","同城无偿","听主人的话","处男免费","处男无偿","宇宙第一骚","找萢友","寻固炮","寻男大固炮","想找单男","找个搭子","找个男大弟弟","母狗找主人","每晚准时大秀","每晚准时色播","约.炮.","约炮."],
badWords: ["/.*快手.*(.{4,8}).*(?:牛[逼皮]|审核|竟然|过审|[藏忍顶]不住|看[立爽]了)/","/30\\+.*(体制内|太[色瑟涩骚sao]).*(?:\\@|顶不住)/","/(?:dd?|个?|线上|线下)+的?(?:姐姐|弟弟|哥哥|妹妹).{0,6}/i","/(?:主人|爸爸)快来(?:领|找)我.{0,6}$/","/(?:同城|附近).*(?:满足|草|操|艹|男大|女大|体育生|母狗)/","/(?:喜欢我|颠勺|大|这个骚货|线下我就曰|线下骚货)+/","/(?:她好(瑟|色|骚|[Ss][Aa][Oo])|我不行了|刷了半天|就她的主页能打)+/","/(?:小|母)狗(?:求|要|想|主人|爸爸|哥哥|抱抱|操|艹|草|抱走){2,}/","/(?:急需?|找)(?:一个|一位)?(?:主人|爸爸|固[萢跑炮狍]|搭子).{0,6}$/","/(?:推特|第一骚)\\s*\\@.+/","/(?:法我|被法|滴滴|想|来|一){3,}/","/(?:骚|[Ss][Aa][Oo]|比她|货){2,}.*没人比她.{2,4}\\s*\\@/","/.{10,21}\\s*玩的就是[反|返]差\\s*\\@/","/^(?:来|有)个(?:真人|单男|母狗|骚逼).+一下/","/^(?:男大|女大)大?(?:哥哥|弟弟|姐姐|妹妹)快?来$/","/^(?:线上|线下)?(?:蹲|找)一?个(?:男|女)?(?:长期|疼人|临时|温柔)?(?:固[萢跑炮狍]|搭子|主人|爸爸|哥哥|弟弟|主|[Ss])/","/^(?=.*我是真人)(?=.*线[上下][的吗])/","/^..找..(?:主人|主|爸爸|固[萢跑炮狍]|搭子).{0,6}$/","/^[\\p{Emoji}\\n]+$/u","/^刚(?:分手|被|操|做|完|艹|草)想被(?:操|艹|草|爱).{0,4}/","/^有?万达广场附近的吗$/","/^有没有(?:单男|母狗|骚逼).{0,6}$/","/^线下[Dd][Dd]$/","/^谁来当我(?:主人|爸爸|狗狗|小狗|骚母狗).{0,6}$/","/pan\\.(?:quark\\.cn|xunlei\\.com)|drive\\.uc\\.cn/","/体制内[的得]?.{3,8}[的得]很\\s*\\@/","/她太.{1,6}了.*我真顶不住\\s*\\@/","/小(?:狗|🐶|[Mm])(?:在线|想[和跟])(?:等|找|大家|主人|主|爸爸|固[萢跑炮狍]|搭子|调|你).{0,6}/","/晚上开课.+/","/有(哥哥|弟弟).*线(上|下)(约|吗)/i","/有没有离[得的]近[得的].{0,4}$/","/比她(?:骚|好看)的没/","/线下(?:骚|[Ss][Aa][Oo]|比她|货)+/","/(去|在|直到)快手.*(.{4,8}).*(绝了|审核|尺度|推荐|正常|藏了).*[??。]/","@designksh1","@ruantang992","@xiaonm88"]
};
let config = Object.assign({ disabledDefaultWords: [], disabledDefaultUsers: [] }, api.getValue('filterConfig', DEFAULT_CONFIG));
config.badUsers = (config.badUsers || []).filter(u => !DEFAULT_CONFIG.badUsers.includes(u));
config.badWords = (config.badWords || []).filter(w => !DEFAULT_CONFIG.badWords.includes(w));
const compileRules = (words) => words.map(r => {
try {
if (r.startsWith('/') && r.lastIndexOf('/') > 0) {
const idx = r.lastIndexOf('/');
return new RegExp(r.substring(1, idx), r.substring(idx + 1));
}
return new RegExp(r);
} catch (e) { return null; }
}).filter(Boolean);
let compiledRules = compileRules([
...config.badWords,
...DEFAULT_CONFIG.badWords.filter(w => !config.disabledDefaultWords.includes(w))
]);
let PROCESSED_TWEETS = new WeakSet();
const FILTER_CACHE = new Map();
const cleanText = (text) => {
if (!text) return "";
return text.replace(/[\u0000-\u001F\u007F-\u009F\u00AD\u061C\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF\u200D\u200C\u180E\u202F\u205F\u2060\uFE00-\uFE0F]/g, '').trim();
};
const isSpamBot = (tweetTextEl, tweetNode) => {
if (!tweetTextEl) return false;
const pureText = cleanText(tweetTextEl.innerText || "");
const nameEl = tweetNode?.querySelector('[data-testid="User-Name"]');
const nickName = nameEl ? cleanText(nameEl.innerText.split(/[\n·]/)[0]) : "";
const nameEmojiEls = nameEl ? nameEl.querySelectorAll('img[src*="emoji"]') : [];
const tweetEmojiEls = tweetTextEl.querySelectorAll('img[src*="emoji"]');
const spamEmojiList = ["⬆️","🍓","🌸","💊","👠","💎","💯","🍆","🔮","🍑","🌈","🚪","🅱️","🔞","❤"];
let nameEmojiCount = 0;
for (let img of nameEmojiEls) { if (spamEmojiList.includes(img.getAttribute('alt'))) nameEmojiCount++; }
const nameSpamStrict = /(?:约[ .]?炮|chu男|破处|处男|寻|找|蹲|求|想找|一个)(?:免费|无偿|单男|固[萢跑炮狍]|搭子|主[人子]|哥哥|弟弟|男大|女大|线上|线下)|(?:每晚|准时)(?:大秀|色播|直播)|(?:母狗|骚货|第一骚|体育生|女大|男大)(?:求|找|寻|固[萢跑炮狍]|[萢跑狍]友|搭子|主[人子])|[(\uff08](?:饥渴|处女|寂寞|男大来)|(?:附近的|来|DD)|(?:资源|小马开大车|萝莉|幼钕){2,}/i;
const nameSpamSuspect = /(?:同城|附近|寻|蹲|约见?|无|真实|免费|偿|个|丝丝|小猫|茜茜|萝莉|学姐|乔|欣|萌|成人|限制|破除|破限|AI|上门|速配|服务|满足|线下|联系|野战|安排|哥哥|弟弟|男大|女大|单男|体育生|固[萢跑炮狍]|[萢跑狍]友){2,}|(?:|1\-5|对接|主页|置顶|无套路|[首原]创|真实|全国|覆盖|可靠|同城|约见|附近|牵线|头像|[点看]我){2,}/i;
if (nickName) {
if (nameSpamStrict.test(nickName)) return true;
if (nameEmojiCount > 0 && nameSpamSuspect.test(nickName)) return true;
}
let totalSpamEmojiCount = nameEmojiCount;
for (let img of tweetEmojiEls) { if (spamEmojiList.includes(img.getAttribute('alt'))) totalSpamEmojiCount++; }
if (totalSpamEmojiCount >= 6) return true;
const hasPunctuation = /[\u3002\uff0c\uff1f\uff01\uff1b\uff1a\u3001.?!,;:]/.test(pureText);
if (tweetEmojiEls.length > 0 && !hasPunctuation && pureText.length < 4) return true;
if (tweetEmojiEls.length >= 4 && tweetEmojiEls.length <= 7 && /^[0-9a-zA-Z\s]*$/.test(pureText)) return true;
const specialSymbols = /[𖬺༄𖬚✫꙳⸝𖨋༺꙼꙽༻ꦿ⬆↑⇑ꪕꜥ꩕ꦼꥏ⦹⦸ꥎꦾ꩔Ꜥꪑ⧉ꥐ꧀꩖ꧏ꧑꩗꧁❀©ꥑ༘⋆࿐𓆩𓆪ꕤ༉✧ꦂ𓂃࿔𓏸𓃠]/g;
const symbolMatches = pureText.match(specialSymbols);
if (symbolMatches && symbolMatches.length >= 3) return true;
return false;
};
const twitterContentUnblocker = (() => {
const TARGET_ENDPOINTS = ['UserTweets', 'TweetDetail', 'SearchTimeline', 'HomeTimeline'];
const rewriteResponse = (rawText) => {
try {
return rawText
.replace(/"possibly_sensitive":\s*true/g, '"possibly_sensitive":false')
.replace(/"profile_interstitial_type":\s*"sensitive_media"/g, '"profile_interstitial_type":""')
.replace(/"mediaVisibilityResults":/g, '"mediaVisibilityResults_bypass":');
} catch (e) { return rawText; }
};
return () => {
const XHR = XMLHttpRequest.prototype;
const send = XHR.send;
const open = XHR.open;
XHR.open = function(method, url) { this._url = url; return open.apply(this, arguments);};
XHR.send = function() {
if (this._url && TARGET_ENDPOINTS.some(ep => this._url.includes(ep))) {
const responseTextGetter = Object.getOwnPropertyDescriptor(XHR, 'responseText').get;
Object.defineProperty(this, 'responseText', {
get: function() {
const originalText = responseTextGetter.call(this);
return typeof originalText === 'string' ? rewriteResponse(originalText) : originalText;
},
configurable: true
});
}
return send.apply(this, arguments);
};
};
})();
const twitterAdCleaner = (() => {
const PROCESSED_NODES = new WeakSet();
const AD_SELECTORS = ["div[data-testid='placementTracking']", "button[aria-label='Grok actions']","button[aria-label='Grok 操作']", "aside[role='complementary']"];
const isAdText = (t) => ['Promoted', '广告', '推廣', '広告', 'Sponsorisé'].includes(t);
const processNode = (node) => {
if (!node || node.nodeType !== 1 || PROCESSED_NODES.has(node)) return;
try {
if (node.getAttribute('data-testid') === 'cellInnerDiv') {
const hasAdSignal = node.querySelector(AD_SELECTORS[0]);
const adLabel = Array.from(node.querySelectorAll('article span')).find(s => isAdText(s.textContent));
if (hasAdSignal || adLabel) {
node.setAttribute('data-filtered', 'true');
PROCESSED_NODES.add(node);
return;
}
}
if (node.matches?.(AD_SELECTORS[1]) || node.matches?.(AD_SELECTORS[2])) {
const isGrok = node.matches(AD_SELECTORS[1]);
const isAdSidebar = node.innerText && isAdText(node.innerText.split('\n')[0]);
if (isGrok || isAdSidebar) {
node.setAttribute('data-filtered', 'true');
}
}
} catch (e) {}
};
return () => {
const observer = new MutationObserver((m) => {
requestAnimationFrame(() => m.forEach(mu => mu.addedNodes.forEach(processNode)));
});
observer.observe(document.body, { childList: true, subtree: true });
document.querySelectorAll('div[data-testid="cellInnerDiv"]').forEach(processNode);
};
})();
const quickFilterUI = (tweetData, tweetNode) => {
const existing = document.getElementById('tf-quick-panel');
if (existing) existing.remove();
const box = document.createElement('div');
box.id = 'tf-quick-panel';
const shadow = box.attachShadow({mode: 'closed'});
const style = document.createElement('style');
style.textContent = `
:host { position: fixed; inset: 0; z-index: 1000000; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); font-family: -apple-system, system-ui; }
.card { background: white; width: 320px; padding: 20px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }
@media (prefers-color-scheme: dark) { .card { background: #15202b; color: white; } }
h4 { margin: 0 0 15px 0; font-size: 16px; }
.opt { background: rgba(128,128,128,0.1); padding: 10px; border-radius: 8px; margin-bottom: 8px; cursor: pointer; font-size: 13px; word-break: break-all; border: 1px solid transparent; }
.opt:hover { border-color: #1d9bf0; background: rgba(29,155,240,0.1); }
textarea { width: 100%; height: 60px; margin-top: 5px; border-radius: 8px; border: 1px solid #ccc; padding: 5px; font-size: 12px; resize: none; box-sizing: border-box; }
.btns { display: flex; gap: 8px; margin-top: 15px; }
button { flex: 1; padding: 8px; border-radius: 20px; border: none; font-weight: bold; cursor: pointer; }
.add { background: #1d9bf0; color: white; }
.cancel { background: #eee; color: #333; }
`;
const cleanVal = cleanText(tweetData.text);
const regexVal = `/${cleanVal.substring(0, 10).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*/`;
const getFilterName = (name) => {
if (!name) return "";
const parts = name.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]|\u200D/);
if (parts.length <= 1) return name;
let maxPart = "";
for (let i = 0; i < parts.length; i++) {
const p = parts[i].trim();
if (p.length >= maxPart.length) maxPart = p;
}
return maxPart || name;
};
const filterName = getFilterName(tweetData.userName);
shadow.appendChild(style);
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
快速过滤
屏蔽用户ID: ${tweetData.userId}
屏蔽名称: ${filterName}
提取正则: ${regexVal}
自定义规则:
`;
shadow.appendChild(card);
document.body.appendChild(box);
const close = () => box.remove();
shadow.getElementById('cbtn').onclick = close;
const syncToMainPanel = (type, value) => {
const mainPlugin = document.getElementById('tf-plugin');
if (mainPlugin && mainPlugin.shadowRoot) {
const targetTextarea = mainPlugin.shadowRoot.getElementById(type);
if (targetTextarea) {
const currentLines = targetTextarea.value.split('\n').map(s => s.trim()).filter(s => s);
if (!currentLines.includes(value)) {
currentLines.push(value);
targetTextarea.value = currentLines.join('\n');
}
}
}
};
shadow.getElementById('addU').onclick = () => { shadow.getElementById('customR').value = tweetData.userId; shadow.getElementById('abtn').click(); };
shadow.getElementById('addN').onclick = () => {
const val = filterName;
if (!DEFAULT_CONFIG.badUsers.includes(val) && !config.badUsers.includes(val)) {
config.badUsers.push(val);
syncToMainPanel('users', val);
}
api.setValue('filterConfig', config);
if (tweetNode) {
const pCell = tweetNode.closest('[data-testid="cellInnerDiv"]') || tweetNode;
pCell.setAttribute('data-filtered', 'true');
}
close();
};
shadow.getElementById('addR').onclick = () => { shadow.getElementById('customR').value = regexVal; shadow.getElementById('abtn').click(); };
shadow.getElementById('abtn').onclick = () => {
const val = shadow.getElementById('customR').value.trim();
if (!val) return;
if (val.startsWith('@')) {
if (!DEFAULT_CONFIG.badUsers.includes(val) && !config.badUsers.includes(val)) {
config.badUsers.push(val);
syncToMainPanel('users', val);
}
config.disabledDefaultUsers = config.disabledDefaultUsers.filter(x => x !== val);
} else {
if (!DEFAULT_CONFIG.badWords.includes(val) && !config.badWords.includes(val)) {
config.badWords.push(val);
syncToMainPanel('words', val);
}
config.disabledDefaultWords = config.disabledDefaultWords.filter(x => x !== val);
compiledRules = compileRules(config.badWords);
}
api.setValue('filterConfig', config);
if (tweetNode) {
const pCell = tweetNode.closest('[data-testid="cellInnerDiv"]') || tweetNode;
pCell.setAttribute('data-filtered', 'true');
}
close();
};
};
const addFilterBtn = (tweet, data) => {
const group = tweet.querySelector('div[role="group"]:last-child');
if (!group || group.querySelector('.tf-qbtn')) return;
const insert = () => {
const btn = document.createElement('div');
const reference = group.firstElementChild;
btn.className = (reference ? reference.className : 'css-175oi2r r-18u37iz r-1h0z5md r-13awgt0') + ' tf-qbtn';
btn.style = 'display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0.6;';
btn.innerHTML = '';
btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); quickFilterUI(data, tweet); };
group.appendChild(btn);
};
if (group.children.length > 0) {
insert();
} else {
setTimeout(insert, 150);
}
};
const processFilterNode = (tweet) => {
if (!tweet || PROCESSED_TWEETS.has(tweet)) return;
const pCell = tweet.closest('[data-testid="cellInnerDiv"]') || tweet;
const textEl = tweet.querySelector('[data-testid="tweetText"]');
const tweetLink = tweet.querySelector('a[href*="/status/"]');
const tweetId = tweetLink ? tweetLink.getAttribute('href').split('/status/')[1]?.split('?')[0] : null;
const avatar = tweet.querySelector('[data-testid^="UserAvatar-Container-"]');
const userId = avatar ? '@' + avatar.getAttribute('data-testid').replace('UserAvatar-Container-', '') : null;
const nameContainer = tweet.querySelector('[data-testid="User-Name"]');
const userName = nameContainer ? nameContainer.innerText.split('\n')[0] : null;
const rawText = textEl ? textEl.innerText : "";
if (tweetId && FILTER_CACHE.get(tweetId)) {
pCell.setAttribute('data-filtered', 'true');
PROCESSED_TWEETS.add(tweet); return;
}
const activeBadUsers = [
...config.badUsers,
...DEFAULT_CONFIG.badUsers.filter(u => !config.disabledDefaultUsers.includes(u))
];
const isSpam = isSpamBot(textEl, tweet);
const shouldHide = isSpam || activeBadUsers.some(u => {
const t = u.toLowerCase();
return (userId && userId.toLowerCase() === t) || (userName && userName.toLowerCase().includes(t));
}) || (textEl && compiledRules.some(r => r.test(cleanText(rawText))));
if (shouldHide) {
pCell.setAttribute('data-filtered', 'true');
if (tweetId) {
FILTER_CACHE.set(tweetId, true);
if (FILTER_CACHE.size > 1000) {
const firstKey = FILTER_CACHE.keys().next().value;
FILTER_CACHE.delete(firstKey);
}
}
} else {
addFilterBtn(tweet, { userId, userName, text: rawText });
}
PROCESSED_TWEETS.add(tweet);
};
const filterObserver = new MutationObserver((mutations) => {
if (shouldExclude()) return;
requestAnimationFrame(() => {
for (let i = 0; i < mutations.length; i++) {
mutations[i].addedNodes.forEach(node => {
if (node.nodeType === 1) {
const tweet = node.matches('article[data-testid="tweet"]') ? node : node.querySelector('article[data-testid="tweet"]');
if (tweet) processFilterNode(tweet);
else if (node.getAttribute?.('data-testid') === 'cellInnerDiv') {
setTimeout(() => { const r = node.querySelector('article[data-testid="tweet"]'); if (r) processFilterNode(r); }, 100);
}
}
});
}
});
});
const createUI = () => {
const globalStyle = document.createElement('style');
globalStyle.textContent = `[data-filtered="true"] { display: none !important; }`;
document.head.appendChild(globalStyle);
const container = document.createElement('div');
container.id = 'tf-plugin';
document.body.appendChild(container);
const shadow = container.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
#mask { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: none; z-index: 999998; backdrop-filter: blur(4px); }
#fab { position: fixed; right: 0; top: 50%; transform: translateY(-50%); width: 42px; height: 42px; background: rgba(255, 255, 255, 0.6); color: #000; border-radius: 21px 0 0 21px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: -2px 0 12px rgba(0,0,0,0.1); z-index: 999997; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); padding-left: 4px; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.3); border-right: none; }
#fab.idle { opacity: 0.3; transform: translateY(-50%) translateX(25px); }
#panel { display: none; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 360px; max-width: 90vw; height: 500px; max-height: 85vh; background: rgba(255,255,255,0.8); backdrop-filter: blur(16px); border-radius: 24px; z-index: 999999; border: 1px solid rgba(255,255,255,0.4); font-family: -apple-system, system-ui; box-shadow: 0 20px 60px rgba(0,0,0,0.25); overflow: hidden; }
.header { display: flex; justify-content: space-between; align-items: center; padding: 20px 20px 10px 20px; }
.header h3 { margin: 0; font-size: 18px; font-weight: 800; }
.close-btn { cursor: pointer; padding: 4px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s; }
.close-btn:hover { background: rgba(0,0,0,0.1); }
.tabs { display: flex; padding: 0 20px; border-bottom: 1px solid rgba(0,0,0,0.05); gap: 15px; }
.tab { font-size: 13px; font-weight: bold; color: #536471; padding-bottom: 8px; cursor: pointer; border-bottom: 2px solid transparent; }
.tab.active { color: #1d9bf0; border-bottom-color: #1d9bf0; }
.view-slider { display: flex; width: 300%; height: calc(100% - 95px); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.page { width: 33.333%; height: 100%; box-sizing: border-box; padding: 15px 20px; overflow-y: auto; display: flex; flex-direction: column; }
label { display: block; font-size: 12px; font-weight: bold; margin-bottom: 4px; color: #536471; }
textarea { width: 100%; height: 140px; border: 1px solid rgba(0,0,0,0.1); border-radius: 12px; padding: 10px; margin-bottom: 12px; box-sizing: border-box; resize: none; outline: none; transition: all 0.2s; background: rgba(255,255,255,0.5); font-size: 13px; }
textarea:focus { border-color: #1d9bf0; background: #fff; box-shadow: 0 0 0 3px rgba(29,155,240,0.1); }
.miui-list { display: flex; flex-direction: column; gap: 8px; }
.miui-item { display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.02); padding: 10px 12px; border-radius: 12px; border: 1px solid rgba(0,0,0,0.03); }
.miui-text { font-size: 13px; font-family: monospace; word-break: break-all; max-width: 75%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.miui-switch { position: relative; width: 44px; height: 24px; background: #e3e3e3; border-radius: 12px; cursor: pointer; transition: background 0.2s; -webkit-tap-highlight-color: transparent; }
.miui-switch::after { content: ''; position: absolute; width: 20px; height: 20px; border-radius: 50%; background: #fff; top: 2px; left: 2px; transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
.miui-switch.checked { background: #00cc66; }
.miui-switch.checked::after { transform: translateX(20px); }
.btn-group { display: flex; flex-direction: column; gap: 10px; margin-top: auto; }
button { padding: 11px; border-radius: 12px; border: none; font-weight: bold; cursor: pointer; transition: all 0.2s; font-size: 13px; }
.save { background: #1d9bf0; color: #fff; }
.save:hover { background: #1a8cd8; }
.backup-btn { background: rgba(0,0,0,0.05); color: #000; }
.backup-btn:hover { background: rgba(0,0,0,0.08); }
.reset { background: #f4212e; color: #fff; }
.reset:hover { background: #dc1f29; }
@media (prefers-color-scheme: dark) {
#fab { background: rgba(0, 0, 0, 0.6); color: #fff; }
#panel { background: rgba(21, 32, 43, 0.85); color: #fff; border: 1px solid rgba(255,255,255,0.1); }
.close-btn:hover { background: rgba(255,255,255,0.1); }
.tabs { border-bottom-color: rgba(255,255,255,0.05); }
.tab { color: #8b98a5; }
.tab.active { color: #1d9bf0; }
label { color: #8b98a5; }
textarea { background: rgba(0,0,0,0.3); color: #fff; border-color: rgba(255,255,255,0.1); }
textarea:focus { background: rgba(0,0,0,0.5); }
.miui-item { background: rgba(255,255,255,0.03); border-color: rgba(255,255,255,0.02); }
.miui-switch { background: #3e4144; }
.backup-btn { background: rgba(255,255,255,0.1); color: #fff; }
.backup-btn:hover { background: rgba(255,255,255,0.15); }
}
`;
shadow.appendChild(style);
const div = document.createElement('div');
div.innerHTML = `
`;
shadow.appendChild(div);
const panel = shadow.getElementById('panel'), fab = shadow.getElementById('fab'), mask = shadow.getElementById('mask');
const slider = shadow.getElementById('slider'), tabs = shadow.querySelectorAll('.tab'), defContainer = shadow.getElementById('defaultRulesList');
let hideTimer;
const resetTimer = () => {
fab.classList.remove('idle');
clearTimeout(hideTimer);
hideTimer = setTimeout(() => { if (panel.style.display !== 'block') fab.classList.add('idle'); }, 3000);
};
const toggleUI = (show) => {
panel.style.display = show ? 'block' : 'none';
mask.style.display = show ? 'block' : 'none';
if (!show) resetTimer();
};
const updateVisibility = () => { container.style.display = shouldExclude() ? 'none' : 'block'; };
tabs.forEach(t => {
t.onclick = () => {
tabs.forEach(tab => tab.classList.remove('active'));
t.classList.add('active');
slider.style.transform = `translateX(-${t.getAttribute('data-idx') * 33.333}%)`;
};
});
const renderDefaultRules = () => {
defContainer.innerHTML = '';
const createSection = (title, items, type) => {
const label = document.createElement('label');
label.innerText = title;
label.style.marginTop = '10px';
defContainer.appendChild(label);
items.forEach(item => {
const row = document.createElement('div');
row.className = 'miui-item';
const text = document.createElement('div');
text.className = 'miui-text';
text.innerText = item;
text.title = item;
const sw = document.createElement('div');
const isDisabled = type === 'user' ? config.disabledDefaultUsers.includes(item) : config.disabledDefaultWords.includes(item);
sw.className = `miui-switch${isDisabled ? '' : ' checked'}`;
sw.onclick = () => {
if (type === 'user') {
if (config.disabledDefaultUsers.includes(item)) {
config.disabledDefaultUsers = config.disabledDefaultUsers.filter(x => x !== item);
} else {
config.disabledDefaultUsers.push(item);
}
} else {
if (config.disabledDefaultWords.includes(item)) {
config.disabledDefaultWords = config.disabledDefaultWords.filter(x => x !== item);
} else {
config.disabledDefaultWords.push(item);
}
}
api.setValue('filterConfig', config);
sw.classList.toggle('checked');
};
row.appendChild(text);
row.appendChild(sw);
defContainer.appendChild(row);
});
};
if(DEFAULT_CONFIG.badUsers.length) createSection('默认屏蔽用户', DEFAULT_CONFIG.badUsers, 'user');
if(DEFAULT_CONFIG.badWords.length) createSection('默认屏蔽关键词/正则', DEFAULT_CONFIG.badWords, 'word');
};
renderDefaultRules();
fab.onclick = () => toggleUI(true);
mask.onclick = () => toggleUI(false);
shadow.getElementById('closeX').onclick = () => toggleUI(false);
fab.onmouseenter = () => fab.classList.remove('idle');
fab.onmouseleave = resetTimer;
shadow.getElementById('saveBtn').onclick = () => {
const inputUsers = shadow.getElementById('users').value.split('\n').map(s => s.trim()).filter(s => s);
const inputWords = shadow.getElementById('words').value.split('\n').map(s => s.trim()).filter(s => s);
config.badUsers = inputUsers.filter(u => !DEFAULT_CONFIG.badUsers.includes(u));
config.badWords = inputWords.filter(w => !DEFAULT_CONFIG.badWords.includes(w));
api.setValue('filterConfig', config);
location.reload();
};
shadow.getElementById('copyBtn').onclick = () => { api.setClipboard(JSON.stringify(config)); alert('已复制'); };
shadow.getElementById('resetBtn').onclick = () => {
if (confirm("确定要恢复默认规则吗?(您的自定义规则将被保留)")) {
config.disabledDefaultUsers = [];
config.disabledDefaultWords = [];
api.setValue('filterConfig', config);
location.reload();
}
};
shadow.getElementById('importBtn').onclick = () => {
const j = prompt("请粘贴导出的 JSON 规则:");
if (j) {
try {
const p = JSON.parse(j);
if (p.badUsers || p.badWords) {
const newData = {
badUsers: Array.isArray(p.badUsers) ? p.badUsers : [],
badWords: Array.isArray(p.badWords) ? p.badWords : [],
disabledDefaultUsers: Array.isArray(p.disabledDefaultUsers) ? p.disabledDefaultUsers : [],
disabledDefaultWords: Array.isArray(p.disabledDefaultWords) ? p.disabledDefaultWords : []
};
api.setValue('filterConfig', newData);
location.reload();
} else {
alert("规则格式不匹配(需包含badUsers或badWords)");
}
} catch (e) {
alert("JSON解析失败,请检查格式");
}
}
};
window.addEventListener('popstate', updateVisibility);
document.addEventListener('click', () => setTimeout(updateVisibility, 200));
resetTimer();
updateVisibility();
};
const shouldExclude = (() => {
const ex = /https:\/\/x\.com\/(?:.*\/status\/.*\/(?:video|photo|mediaViewer.*)|settings|i\/.*\/creators\/|i\/bookmarks|i\/premium-business|.*\/lists|i\/follow_people|i\/chat|notifications|i\/grok)/;
let lastUrl = ''; let lastResult = false;
return () => {
const curr = location.href; if (curr === lastUrl) return lastResult;
const prevResult = lastResult;
lastUrl = curr; lastResult = ex.test(curr);
if (lastResult) {
filterObserver.disconnect();
} else {
filterObserver.observe(document.body, { childList: true, subtree: true });
if (prevResult === true) {
PROCESSED_TWEETS = new WeakSet();
requestAnimationFrame(() => {
document.querySelectorAll('article[data-testid="tweet"]').forEach(processFilterNode);
});
}
}
return lastResult;
};
})();
twitterContentUnblocker();
const checkReady = setInterval(() => {
const main = document.querySelector('main');
if (main) { clearInterval(checkReady); createUI(); twitterAdCleaner(); if (!shouldExclude()) document.querySelectorAll('article[data-testid="tweet"]').forEach(processFilterNode); }
}, 1000);
})();