// ==UserScript== // @name Twitter/X 廣告與詐騙過濾器 // @namespace http://tampermonkey.net/ // @version 11.2 // @description 過濾推特廣告與詐騙。修正「圖案+文字」的順序讀取問題,確保 Emoji 在前或在後都能精準過濾,可以自行加入關鍵字和布想要的圖案會自動篩除。 // @author 程式夥伴 // @match https://twitter.com/* // @match https://x.com/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_setClipboard // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/559382/TwitterX%20%E5%BB%A3%E5%91%8A%E8%88%87%E8%A9%90%E9%A8%99%E9%81%8E%E6%BF%BE%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/559382/TwitterX%20%E5%BB%A3%E5%91%8A%E8%88%87%E8%A9%90%E9%A8%99%E9%81%8E%E6%BF%BE%E5%99%A8.meta.js // ==/UserScript== (function() { 'use strict'; // --- 1. 救援清單 --- const recoveredKeywords = ""; // 讀取設定 let blockKeywords = GM_getValue('block_keywords_v3', recoveredKeywords).split(',').map(k => k.trim().toLowerCase()).filter(k => k); let hideAds = GM_getValue('hide_ads', true); let debugMode = GM_getValue('debug_mode', false); // --- 2. CSS 樣式 --- GM_addStyle(` .filtered-placeholder { background-color: #16181c; color: #71767b; font-size: 13px; text-align: center; padding: 10px 0; margin: 5px 0; border-radius: 8px; border: 1px dashed #2f3336; display: flex; justify-content: center; align-items: center; gap: 10px; } .view-btn { background: rgba(29, 155, 240, 0.1); color: #1d9bf0; border: 1px solid #1d9bf0; padding: 2px 8px; border-radius: 12px; cursor: pointer; font-size: 11px; font-weight: bold; } .view-btn:hover { background: #1d9bf0; color: white; } #filter-panel-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 99999; display: flex; justify-content: center; align-items: center; } #filter-panel { background: #000; color: #e7e9ea; padding: 20px; border-radius: 16px; width: 450px; max-width: 90%; border: 1px solid #333; box-shadow: 0 0 20px rgba(255,255,255,0.1); font-family: sans-serif; } #keyword-list { display: flex; flex-wrap: wrap; gap: 8px; max-height: 250px; overflow-y: auto; margin-bottom: 15px; padding: 8px; background: #111; border-radius: 8px; border: 1px solid #333; } .kw-tag { background: #1d9bf0; color: white; padding: 4px 10px; border-radius: 20px; font-size: 13px; display: flex; align-items: center; gap: 6px; } .kw-tag span { cursor: pointer; font-weight: bold; opacity: 0.7; } .input-group { display: flex; gap: 10px; margin-bottom: 20px; } #new-kw-input { flex: 1; padding: 10px; background: #222; color: #fff; border: 1px solid #444; border-radius: 4px; } #add-kw-btn { background: #00ba7c; color: white; border: none; padding: 8px 15px; cursor: pointer; border-radius: 4px; font-weight:bold;} .panel-actions { display: flex; justify-content: flex-end; gap: 10px; border-top: 1px solid #333; padding-top: 15px; } `); // --- 3. UI 介面 --- function createManagerUI() { if (document.getElementById('filter-panel-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'filter-panel-overlay'; overlay.innerHTML = `

🚫 關鍵字過濾管理 (v10.0)

`; document.body.appendChild(overlay); const listDiv = document.getElementById('keyword-list'); const input = document.getElementById('new-kw-input'); let tempKeywords = [...blockKeywords]; function renderTags() { listDiv.innerHTML = ''; if (tempKeywords.length === 0) listDiv.innerHTML = '
無關鍵字
'; tempKeywords.forEach((kw, index) => { const tag = document.createElement('div'); tag.className = 'kw-tag'; tag.innerHTML = `${kw} `; tag.querySelector('span').onclick = () => { tempKeywords.splice(index, 1); renderTags(); }; listDiv.appendChild(tag); }); } function addKeyword(val) { val = val.trim().toLowerCase(); if (val && !tempKeywords.includes(val)) { tempKeywords.push(val); return true; } return false; } document.getElementById('export-btn').onclick = () => { GM_setClipboard(tempKeywords.join(',')); alert('✅ 已複製清單'); }; document.getElementById('import-btn').onclick = () => { const d = prompt("貼上清單(逗號分隔)"); if(d) { d.split(',').forEach(w=>addKeyword(w)); renderTags(); } }; document.getElementById('add-kw-btn').onclick = () => { if(addKeyword(input.value)) input.value=''; renderTags(); }; input.onkeypress = (e) => { if(e.key==='Enter'){ if(addKeyword(input.value)) input.value=''; renderTags(); }}; document.getElementById('close-btn').onclick = () => overlay.remove(); document.getElementById('save-btn').onclick = () => { blockKeywords = tempKeywords; GM_setValue('block_keywords_v3', blockKeywords.join(',')); overlay.remove(); location.reload(); }; renderTags(); } function registerMenus() { GM_registerMenuCommand(`⚙️ 管理關鍵字`, createManagerUI); const adStatus = hideAds ? "✅" : "❌"; GM_registerMenuCommand(`📢 隱藏廣告 (${adStatus})`, () => { GM_setValue('hide_ads', !hideAds); location.reload(); }); const debugStatus = debugMode ? "✅" : "❌"; GM_registerMenuCommand(`🐞 除錯模式 (${debugStatus})`, () => { GM_setValue('debug_mode', !debugMode); alert(`除錯模式已${!debugMode ? "開啟" : "關閉"}`); location.reload(); }); } registerMenus(); // --- 4. 核心邏輯 (v11.2: 依序讀取 DOM) --- function normalize(str) { if (!str) return ""; return str.toLowerCase().replace(/\s+/g, ''); } // 遞迴掃描 DOM,依照視覺順序獲取文字與圖片含義 function getVisualTextContent(node) { if (node.nodeType === Node.TEXT_NODE) { return node.textContent; } if (node.nodeType === Node.ELEMENT_NODE) { // 如果是圖片 (Emoji) if (node.tagName === 'IMG' && node.alt) { return node.alt; } // 如果是 SVG (圖標) if (node.tagName === 'SVG') { return node.getAttribute('aria-label') || ""; } // 忽略隱藏元素 (避免讀到沒顯示的 metadata) // 但 Twitter 的結構比較複雜,這裡暫時不做太嚴格的 style 檢查以免漏掉 let result = ""; for (let child of node.childNodes) { result += getVisualTextContent(child); } return result; } return ""; } function processTweets() { const tweets = document.querySelectorAll('[data-testid="cellInnerDiv"]:not(.processed)'); tweets.forEach(tweetCell => { let shouldBlock = false; let blockReason = ""; // 使用新的讀取器,確保順序正確 const visualText = getVisualTextContent(tweetCell); const cleanContent = normalize(visualText); if (debugMode) { console.log(`v11掃描: ${cleanContent.substring(0, 100)}...`); } // A. 廣告過濾 if (hideAds) { const adIndicators = tweetCell.querySelectorAll('[dir="ltr"] > span'); for (let span of adIndicators) { if (['Ad', 'Promoted', '推廣', '廣告'].includes(span.innerText)) { shouldBlock = true; blockReason = "廣告"; break; } } } // B. 關鍵字過濾 if (!shouldBlock && blockKeywords.length > 0) { const hitKeyword = blockKeywords.find(keyword => { const cleanKeyword = normalize(keyword); // 檢查包含關係 return cleanContent.includes(cleanKeyword); }); if (hitKeyword) { shouldBlock = true; blockReason = `關鍵字: ${hitKeyword}`; } } // C. 執行過濾 (隱藏 + 偷看按鈕) if (shouldBlock) { tweetCell.classList.add('processed'); const children = Array.from(tweetCell.children); children.forEach(child => { if (!child.classList.contains('filtered-placeholder')) { child.style.display = 'none'; child.classList.add('original-content-hidden'); } }); if (!tweetCell.querySelector('.filtered-placeholder')) { const placeholder = document.createElement('div'); placeholder.className = 'filtered-placeholder'; placeholder.innerHTML = ` 🚫 已過濾 (${blockReason}) `; const btn = placeholder.querySelector('.view-btn'); btn.onclick = function(e) { e.stopPropagation(); const hiddenContents = tweetCell.querySelectorAll('.original-content-hidden'); const isCurrentlyHidden = hiddenContents[0].style.display === 'none'; if (isCurrentlyHidden) { hiddenContents.forEach(el => el.style.display = ''); btn.innerText = '🙈 隱藏'; btn.style.background = '#333'; btn.style.color = '#ccc'; } else { hiddenContents.forEach(el => el.style.display = 'none'); btn.innerText = '👀 查看'; btn.style.background = ''; btn.style.color = ''; } }; tweetCell.appendChild(placeholder); } } }); } const observer = new MutationObserver((mutations) => { let shouldProcess = false; for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { shouldProcess = true; break; } } if (shouldProcess) processTweets(); }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(processTweets, 1000); })();