您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支援直播间封锁、推荐区过滤、主页悬浮封锁按钮、封锁清单管理功能
// ==UserScript== // @name TikTok Live Blocking & Filtering // @name:zh-TW TikTok 直播封鎖過濾 // @name:zh-CN TikTok 直播封锁过滤 // @namespace https://www.tampermonkey.net/ // @version 2.4 // @description Supports live room blocking, recommendation feed filtering, floating block button on homepage, and block list management features. // @description:zh-TW 支援直播間封鎖、推薦區過濾、主頁懸浮封鎖按鈕、封鎖清單管理功能 // @description:zh-CN 支援直播间封锁、推荐区过滤、主页悬浮封锁按钮、封锁清单管理功能 // @author ChatGPT // @match https://www.tiktok.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== (function () { 'use strict'; // ======= ✅ Toast 提示功能 ======= function toast(msg) { const div = document.createElement('div'); div.textContent = msg; div.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.75); color: white; padding: 10px 14px; border-radius: 6px; z-index: 10000; font-size: 14px; user-select: none; pointer-events: none; opacity: 0; transition: opacity 0.3s ease-in-out; `; document.body.appendChild(div); requestAnimationFrame(() => div.style.opacity = '1'); setTimeout(() => { div.style.opacity = '0'; div.addEventListener('transitionend', () => div.remove()); }, 2500); } // ======= 🟢 開關按鈕功能( ON/OFF ) ======= const SCRIPT_ENABLED_KEY = 'script_enabled'; let scriptEnabled = GM_getValue(SCRIPT_ENABLED_KEY, true); function insertToggleButton() { const targetAnchor = document.querySelector('a.tiktok-104tlrh.link-a11y-focus'); if (!targetAnchor) return; if (document.getElementById('tiktok-script-toggle-btn')) return; const btn = document.createElement('button'); btn.id = 'tiktok-script-toggle-btn'; btn.textContent = scriptEnabled ? 'ON' : 'OFF'; btn.style.cssText = ` margin-left: 8px; padding: 4px 10px; font-size: 16px; border-radius: 5px; border: none; cursor: pointer; background-color: ${scriptEnabled ? '#52c41a' : '#ff4d4f'}; color: white; box-shadow: 0 2px 6px rgba(0,0,0,0.15); user-select: none; transition: background-color 0.3s ease; `; btn.addEventListener('click', () => { scriptEnabled = !scriptEnabled; GM_setValue(SCRIPT_ENABLED_KEY, scriptEnabled); location.reload(); }); targetAnchor.parentElement.style.position = 'relative'; targetAnchor.insertAdjacentElement('afterend', btn); } function tryInsertToggleButton() { insertToggleButton(); if (!document.getElementById('tiktok-script-toggle-btn')) { setTimeout(tryInsertToggleButton, 1000); } } tryInsertToggleButton(); if (!scriptEnabled) { console.log('⚠️ TikTok 直播封鎖過濾腳本已被用戶關閉,停止執行'); return; } // ======= 🔒 封鎖邏輯處理與封鎖名單 ======= const BLOCK_BTN_CLASS = 'tiktok-block-btn'; let blockedList = GM_getValue('blocked_list', []); function getBlockedList() { return blockedList; } function setBlockedList(list) { blockedList = list; GM_setValue('blocked_list', list); } function getStreamerIDFromPath(path) { const match = path.match(/^\/@([^/]+)\/live/); return match ? match[1] : null; } function getStreamerID() { return getStreamerIDFromPath(window.location.pathname); } function addBlock() { const streamerID = getStreamerID(); if (!streamerID) return toast('❌ 無法取得直播主ID'); if (blockedList.includes(streamerID)) return toast(`⚠️ 直播主 ${streamerID} 已在封鎖名單中`); blockedList.push(streamerID); setBlockedList(blockedList); toast(`✅ 已將直播主 ${streamerID} 加入封鎖名單`); } // ======= 📌 插入直播間封鎖按鈕 ======= function insertLiveBlockButton() { if (document.querySelector(`button.${BLOCK_BTN_CLASS}`)) return; const wrapper = document.querySelector('div.tiktok-tk1gy.e1f21nov0'); if (!wrapper) return; const idBlock = wrapper.querySelector('div.tiktok-79elbk.e1w7pjwm0'); if (!idBlock) return; idBlock.style.position = 'relative'; const btn = document.createElement('button'); btn.className = BLOCK_BTN_CLASS; btn.textContent = '🚫 封鎖直播主'; btn.style.cssText = ` position: absolute; top: 50%; left: 100%; transform: translate(10px, -50%); background-color: #ff4d4f; color: white; border: none; padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 14px; z-index: 9999; white-space: nowrap; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); `; btn.addEventListener('click', addBlock); idBlock.appendChild(btn); } // ======= 👀 判斷是否已封鎖 ======= function isStreamerBlocked(streamerID) { return blockedList.includes(streamerID); } // ======= 🧹 隱藏推薦直播卡片(側欄與主頁) ======= function hideBlockedRecommendations() { if (blockedList.length === 0) return; const sideAnchors = document.querySelectorAll('a.tiktok-1usxus4.e1kktrof6.link-a11y-focus[href*="/@"]'); sideAnchors.forEach(anchor => { const href = decodeURIComponent(anchor.getAttribute('href') || ''); const streamerID = getStreamerIDFromPath(href); if (streamerID && isStreamerBlocked(streamerID)) { const card = anchor.closest('div[data-e2e="live-side-nav-item"]'); if (card) card.remove(); } }); const mainArea = document.querySelector('div.tiktok-i9gxme.e1q7cfv81'); if (!mainArea) return; const mainAnchors = mainArea.querySelectorAll('a[href*="/@"][href*="/live"]'); mainAnchors.forEach(anchor => { const href = decodeURIComponent(anchor.getAttribute('href') || ''); const streamerID = getStreamerIDFromPath(href); if (streamerID && isStreamerBlocked(streamerID)) { const card = anchor.closest('div.tiktok-17fk2p9.eomcb1m0'); if (card) card.remove(); } }); } // ======= 🧱 主頁推薦區封鎖按鈕注入 ======= function injectBlockButtonsToMainCards() { if (blockedList.length === 0) return; const mainArea = document.querySelector('div.tiktok-i9gxme.e1q7cfv81'); if (!mainArea) return; const cards = mainArea.querySelectorAll('div.tiktok-17fk2p9.eomcb1m0'); cards.forEach(card => { if (card.querySelector(`button.${BLOCK_BTN_CLASS}`)) return; const anchor = card.querySelector('a[href*="/@"][href*="/live"]'); if (!anchor) return; const href = decodeURIComponent(anchor.getAttribute('href') || ''); const streamerID = getStreamerIDFromPath(href); if (!streamerID) return; if (isStreamerBlocked(streamerID)) { card.remove(); return; } const btn = document.createElement('button'); btn.textContent = '🚫 封鎖'; btn.className = BLOCK_BTN_CLASS; btn.style.cssText = ` position: absolute; top: 8px; right: 8px; z-index: 9999; background-color: #ff4d4f; color: white; border: none; padding: 4px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); `; btn.onclick = () => { if (!blockedList.includes(streamerID)) { blockedList.push(streamerID); setBlockedList(blockedList); toast(`✅ 已封鎖直播主 @${streamerID}`); } card.remove(); }; card.style.position = 'relative'; card.appendChild(btn); }); } // ======= 🧾 封鎖清單管理與清空 ======= function showBlockedListAndEdit() { const PAGE_SIZE = 500; // 每頁最多顯示筆數 let currentPage = 0; // 預設從第 0 頁開始 function renderPage() { const list = getBlockedList(); // 取得目前封鎖清單 const totalPages = Math.ceil(list.length / PAGE_SIZE); // 計算總頁數 if (list.length === 0) return toast('封鎖清單目前為空'); const start = currentPage * PAGE_SIZE; // 當前頁起始索引 const end = Math.min(start + PAGE_SIZE, list.length); // 當前頁結束索引 const listStr = list.slice(start, end).map((id, i) => `${start + i + 1}. ${id}`).join('\n'); // 列出當前頁的項目 // 顯示 prompt 提示用戶輸入 const input = prompt( `📄 封鎖清單(第 ${currentPage + 1} 頁 / 共 ${totalPages} 頁)\n\n${listStr}\n\n` + `輸入欲剃除的「編號」(可用空格或逗號分隔)\n` + `輸入 > / < 可翻頁(下一頁 / 上一頁):` ); // 若使用者關閉對話框,則不做任何處理 if (input === null) return; const trimmed = input.trim(); // 處理翻頁邏輯 if (trimmed === '>') { if (currentPage + 1 < totalPages) currentPage++; return renderPage(); } else if (trimmed === '<') { if (currentPage > 0) currentPage--; return renderPage(); } // 使用者輸入欲剃除的編號 let indexes = trimmed.split(/[\s,]+/).map(s => parseInt(s.trim())) .filter(n => !isNaN(n) && n >= 1 && n <= list.length); if (indexes.length === 0) { toast('⚠️ 無有效編號,未變更'); return; } // 移除重複並由大至小排序,避免刪除時索引錯位 indexes = [...new Set(indexes)].sort((a, b) => b - a); const newList = [...list]; for (const idx of indexes) { newList.splice(idx - 1, 1); } // 儲存新的封鎖清單 setBlockedList(newList); toast(`✅ 已剃除 ${indexes.length} 位直播主`); } renderPage(); } // 清空封鎖清單 function clearBlockedList() { setBlockedList([]); toast('✅ 封鎖清單已清空'); } // 註冊功能至油猴選單 GM_registerMenuCommand('編輯封鎖清單', showBlockedListAndEdit); GM_registerMenuCommand('清除所有封鎖用戶', clearBlockedList); // ======= 🔁 自動重試載入錯誤頁面 ======= function autoRetryIfCrashed() { const errorContainer = document.querySelector('div.tiktok-17btlil'); const errorIcon = errorContainer?.querySelector('svg'); const retryButton = errorContainer?.querySelector('button.tiktok-1xrybjt.ebef5j00'); if (errorContainer && errorIcon && retryButton) { console.log('⚠️ 偵測到頁面掛掉,嘗試點擊「重試」按鈕...'); retryButton.click(); } } // ======= 🧠 MutationObserver 觀察頁面變化 ======= const observer = new MutationObserver(() => { const isLive = /^\/@[^/]+\/live/.test(window.location.pathname); if (isLive) insertLiveBlockButton(); injectBlockButtonsToMainCards(); hideBlockedRecommendations(); autoRetryIfCrashed(); }); observer.observe(document.body, { childList: true, subtree: true, }); })();