// ==UserScript== // @name Keyword-based Tweet Filtering for Threads // @name:zh-TW Threads 關鍵字過濾推文 // @name:zh-CN Threads 关键字过滤推文 // @namespace http://tampermonkey.net/ // @version 4.0 // @description As long as any section of a tweet—such as the main content, hashtags, or username—matches a keyword, the entire tweet will be hidden. Supports adding, listing, and individually deleting keywords. Supports quick blocking, listing, and individually deleting blocked users. Menu can be switched between Chinese and English. // @description:zh-TW 只要推文主體、標籤、用戶名等任一區塊命中關鍵字,整則推文一起隱藏。支援關鍵字新增、清單、單獨刪除。支援快速封鎖、清單、單獨刪除。中英菜單切換。 // @description:zh-CN 只要推文主体、标签、用户名等任一区块命中关键字,整则推文一起隐藏。支援关键字新增、清单、单独删除。支援快速封锁、清单、单独删除。中英菜单切换。 // @author chatgpt // @match https://www.threads.net/* // @match https://www.threads.com/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // ===== 多語言支援 ===== const LANGS = { zh: { addKeyword: '新增關鍵字', keywordList: '關鍵字清單/刪除', clearKeywords: '清除所有關鍵字', blockList: '封鎖名單管理', clearBlocks: '清除所有封鎖用戶', langSwitch: '語言 中文', blockUser: '封鎖用戶', confirmBlock: username => `確定要封鎖 @${username} 嗎?\n(此用戶所有推文將被隱藏)`, blocked: username => `已封鎖 @${username}!`, addKeywordPrompt: '請輸入要新增的關鍵字(可用半形或全形逗號分隔,一次可多個):', addedKeyword: '已新增關鍵字!', noKeyword: '目前沒有設定任何關鍵字。', keywordListMsg: (list) => `目前關鍵字如下:\n${list}\n請輸入要刪除的關鍵字編號(可多個,用逗號分隔),或留空取消:`, deletedKeyword: '已刪除指定關鍵字!', clearedKeyword: '已清除所有關鍵字!', noBlockUser: '目前沒有封鎖任何用戶。', blockListMsg: (list) => `目前封鎖用戶如下:\n${list}\n請輸入要解除封鎖的用戶編號(可多個,用逗號分隔),或留空取消:`, unblocked: '已解除指定用戶封鎖!', clearedBlock: '已清除所有封鎖用戶!', }, en: { addKeyword: 'Add Keyword', keywordList: 'Keyword List/Delete', clearKeywords: 'Clear All Keywords', blockList: 'Blocked Users', clearBlocks: 'Clear All Blocked Users', langSwitch: 'language EN', blockUser: 'Block User', confirmBlock: username => `Are you sure to block @${username}?\n(All posts from this user will be hidden)`, blocked: username => `@${username} has been blocked!`, addKeywordPrompt: 'Enter keywords to add (comma or Chinese comma separated, multiple allowed):', addedKeyword: 'Keyword(s) added!', noKeyword: 'No keywords set.', keywordListMsg: (list) => `Current keywords:\n${list}\nEnter the number(s) to delete (comma separated), or leave blank to cancel:`, deletedKeyword: 'Selected keyword(s) deleted!', clearedKeyword: 'All keywords cleared!', noBlockUser: 'No users blocked.', blockListMsg: (list) => `Blocked users:\n${list}\nEnter the number(s) to unblock (comma separated), or leave blank to cancel:`, unblocked: 'Selected user(s) unblocked!', clearedBlock: 'All blocked users cleared!', } }; function getLang() { return GM_getValue('lang', (navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en'); } function setLang(lang) { GM_setValue('lang', lang); } function t(key, ...args) { const lang = getLang(); const str = LANGS[lang][key]; return typeof str === 'function' ? str(...args) : str; } // 關鍵字相關 function getKeywords() { return GM_getValue('keywords', []); } function setKeywords(keywords) { GM_setValue('keywords', keywords); } // 封鎖用戶相關 function getBlockedUsers() { return GM_getValue('blockedUsers', []); } function setBlockedUsers(users) { GM_setValue('blockedUsers', users); } // 取得所有推文主容器 function getAllPostContainers() { return document.querySelectorAll('div[data-pressable-container][class*=" "]'); } // 在推文主容器下,找所有可能含有文字的區塊 function getAllTextBlocks(container) { return container.querySelectorAll('span[dir="auto"]:not([translate="no"]), a[role="link"], span, div'); } // 取得用戶名稱(Threads 通常在 a[href^="/@"] 內) function getUsername(container) { let a = container.querySelector('a[href^="/@"]'); if (a) { let username = a.getAttribute('href').replace('/', '').replace('@', ''); return username; } return null; } // ====== 這裡是唯一修改的地方 ====== // 過濾推文(不區分大小寫) function filterPosts() { let keywords = getKeywords().map(k => k.toLowerCase()); let blockedUsers = getBlockedUsers(); let containers = getAllPostContainers(); containers.forEach(container => { let blocks = getAllTextBlocks(container); let matched = false; // 關鍵字過濾(不區分大小寫) blocks.forEach(block => { let text = (block.innerText || block.textContent || "").trim().toLowerCase(); if (text && keywords.some(keyword => keyword && text.includes(keyword))) { matched = true; } }); // 封鎖用戶過濾 let username = getUsername(container); if (username && blockedUsers.includes(username)) { matched = true; } if (matched) { container.style.display = 'none'; } else { container.style.display = ''; } }); } // ====== 修改結束 ====== // 插入封鎖用戶按鈕(插在「分享」按鈕右邊) function insertBlockButtons() { let shareSvgs = document.querySelectorAll('svg[aria-label="分享"], svg[aria-label="Share"]'); let blockedUsers = getBlockedUsers(); shareSvgs.forEach(svg => { let shareBtnDiv = svg.closest('div[role="button"]'); if (!shareBtnDiv) return; // 找到推文主容器 let container = shareBtnDiv; for (let i = 0; i < 10; i++) { if (!container) break; if (container.hasAttribute('data-pressable-container')) break; container = container.parentElement; } if (!container || !container.hasAttribute('data-pressable-container')) return; // 避免重複插入 if (container.querySelector('.tm-block-user-btn')) return; let username = getUsername(container); if (!username) return; // 建立封鎖按鈕 let blockBtn = document.createElement('button'); blockBtn.className = 'tm-block-user-btn'; blockBtn.title = t('blockUser'); blockBtn.style.marginLeft = '8px'; blockBtn.style.background = 'none'; blockBtn.style.border = 'none'; blockBtn.style.cursor = 'pointer'; blockBtn.style.fontSize = '18px'; blockBtn.style.color = '#d00'; blockBtn.textContent = '🚫'; // 這裡改成 textContent blockBtn.onclick = function(e) { e.stopPropagation(); if (confirm(t('confirmBlock', username))) { let users = getBlockedUsers(); if (!users.includes(username)) { users.push(username); setBlockedUsers(users); alert(t('blocked', username)); filterPosts(); } } }; shareBtnDiv.parentNode.insertBefore(blockBtn, shareBtnDiv.nextSibling); }); } // observer 只監控新節點 const observer = new MutationObserver(mutations => { let needFilter = false; for (const m of mutations) { if (m.addedNodes && m.addedNodes.length > 0) { needFilter = true; break; } } if (needFilter) { filterPosts(); insertBlockButtons(); } }); observer.observe(document.body, { childList: true, subtree: true }); // 初始執行一次 filterPosts(); insertBlockButtons(); // 新增關鍵字 GM_registerMenuCommand(t('addKeyword'), () => { let input = prompt(t('addKeywordPrompt')); if (input !== null) { let arr = input.split(/,|,/).map(s => s.trim()).filter(Boolean); let keywords = getKeywords(); let newKeywords = [...keywords]; arr.forEach(k => { if (!newKeywords.includes(k)) newKeywords.push(k); }); setKeywords(newKeywords); alert(t('addedKeyword')); location.reload(); } }); // 關鍵字清單與單獨刪除 GM_registerMenuCommand(t('keywordList'), () => { let keywords = getKeywords(); if (keywords.length === 0) { alert(t('noKeyword')); return; } let msg = t('keywordListMsg', keywords.map((k, i) => `${i+1}. ${k}`).join('\n')); let input = prompt(msg, ''); if (input !== null && input.trim() !== '') { let idxArr = input.split(/,|,/).map(s => parseInt(s.trim(), 10) - 1).filter(i => !isNaN(i) && i >= 0 && i < keywords.length); if (idxArr.length > 0) { let newKeywords = keywords.filter((k, i) => !idxArr.includes(i)); setKeywords(newKeywords); alert(t('deletedKeyword')); location.reload(); } } }); // 清除所有關鍵字 GM_registerMenuCommand(t('clearKeywords'), () => { setKeywords([]); alert(t('clearedKeyword')); location.reload(); }); // 封鎖名單管理 GM_registerMenuCommand(t('blockList'), () => { let users = getBlockedUsers(); if (users.length === 0) { alert(t('noBlockUser')); return; } let msg = t('blockListMsg', users.map((u, i) => `${i+1}. @${u}`).join('\n')); let input = prompt(msg, ''); if (input !== null && input.trim() !== '') { let idxArr = input.split(/,|,/).map(s => parseInt(s.trim(), 10) - 1).filter(i => !isNaN(i) && i >= 0 && i < users.length); if (idxArr.length > 0) { let newUsers = users.filter((u, i) => !idxArr.includes(i)); setBlockedUsers(newUsers); alert(t('unblocked')); location.reload(); } } }); // 清除所有封鎖用戶 GM_registerMenuCommand(t('clearBlocks'), () => { setBlockedUsers([]); alert(t('clearedBlock')); location.reload(); }); // ===== 語言切換按鈕(放在最後) ===== GM_registerMenuCommand(t('langSwitch'), () => { let current = getLang(); let next = current === 'zh' ? 'en' : 'zh'; setLang(next); location.reload(); }); })();