您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Block and filter streamers, channels, categories, and chat messages on Kick.
// ==UserScript== // @name uKickBlock — Block & Filter Streamers, Channels, Categories, and Chat on Kick // @namespace https://github.com/berkaygediz/uKickBlock // @version 1.1.0 // @description Block and filter streamers, channels, categories, and chat messages on Kick. // @author berkaygediz // @match https://kick.com/* // @match https://www.kick.com/* // @grant GM_getValue // @grant GM_setValue // @license GPL-3.0 // @homepageURL https://github.com/berkaygediz/uKickBlock // @supportURL https://github.com/berkaygediz/uKickBlock/issues // ==/UserScript== (function () { "use strict"; function normalizeData(str) { return str?.toLowerCase().trim() || ""; } // ===== Chrome Extension (callback) ===== /* async function getBlockedChannels() { return new Promise((resolve) => { chrome.storage.local.get("blockedChannels", (result) => { try { const list = JSON.parse(result.blockedChannels || "[]"); resolve(list.map((x) => x.trim().toLowerCase())); } catch { resolve([]); } }); }); } async function saveBlockedChannels(list) { return new Promise((resolve) => { chrome.storage.local.set( { blockedChannels: JSON.stringify(list) }, resolve ); }); } */ // ===== Firefox Extension (Promise) ===== /* async function getBlockedChannels() { try { const result = await browser.storage.local.get("blockedChannels"); const list = JSON.parse(result.blockedChannels || "[]"); return list.map((x) => x.trim().toLowerCase()); } catch { return []; } } async function saveBlockedChannels(list) { await browser.storage.local.set({ blockedChannels: JSON.stringify(list), }); } */ // ===== Tampermonkey UserScript (GM) ===== async function getBlockedChannels() { try { const val = await GM_getValue("blockedChannels", "[]"); const list = JSON.parse(val); return list.map((x) => x.trim().toLowerCase()); } catch { return []; } } async function saveBlockedChannels(list) { await GM_setValue("blockedChannels", JSON.stringify(list)); } async function blockChannel(username) { username = normalizeData(username); const blocked = await getBlockedChannels(); if (!blocked.includes(username)) { blocked.push(username); await saveBlockedChannels(blocked); } } async function unblockChannel(username) { username = normalizeData(username); let blocked = await getBlockedChannels(); blocked = blocked.filter((u) => u !== username); await saveBlockedChannels(blocked); } // ==== Chrome Extension ==== getBlockedCategories & saveBlockedCategories /* async function getBlockedCategories() { return new Promise((resolve) => { try { chrome.storage.local.get("blockedCategories", (result) => { if (chrome.runtime.lastError) { console.error(chrome.runtime.lastError); resolve([]); return; } try { const list = JSON.parse(result.blockedCategories || "[]"); resolve(list.map(normalizeData)); } catch { resolve([]); } }); } catch (err) { console.error(err); resolve([]); } }); } async function saveBlockedCategories(list) { return new Promise((resolve) => { try { const data = JSON.stringify(list); chrome.storage.local.set({ blockedCategories: data }, () => { if (chrome.runtime.lastError) { console.error(chrome.runtime.lastError); } resolve(); }); } catch (err) { console.error(err); resolve(); } }); } */ // ==== Firefox Extension ==== getBlockedCategories & saveBlockedCategories /* async function getBlockedCategories() { try { const result = await browser.storage.local.get("blockedCategories"); const list = JSON.parse(result.blockedCategories || "[]"); return list.map(normalizeData); } catch (err) { console.error(err); return []; } } async function saveBlockedCategories(list) { try { const data = JSON.stringify(list); await browser.storage.local.set({ blockedCategories: data }); } catch (err) { console.error(err); } } */ // ==== Tampermonkey ==== getBlockedCategories & saveBlockedCategories async function getBlockedCategories() { try { const val = await GM_getValue("blockedCategories", "[]"); const list = JSON.parse(val); return list.map(normalizeData); } catch (err) { console.error(err); return []; } } async function saveBlockedCategories(list) { try { const data = JSON.stringify(list); await GM_setValue("blockedCategories", data); } catch (err) { console.error(err); } } // ==== Chrome Extension ==== /* async function blockCategory(categoryName) { const blocked = await getBlockedCategories(); const normalizedCategory = normalizeData(categoryName); if (!blocked.includes(normalizedCategory)) { blocked.push(normalizedCategory); await new Promise((resolve) => { chrome.storage.local.set( { blockedCategories: JSON.stringify(blocked) }, resolve ); }); } } */ // ==== Firefox Extension ==== /* async function blockCategory(categoryName) { const blocked = await getBlockedCategories(); const normalizedCategory = normalizeData(categoryName); if (!blocked.includes(normalizedCategory)) { blocked.push(normalizedCategory); await browser.storage.local.set({ blockedCategories: JSON.stringify(blocked), }); } } */ // ==== Tampermonkey/Violentmonkey UserScript ==== async function blockCategory(categoryName) { const blocked = await getBlockedCategories(); const normalizedCategory = normalizeData(categoryName); if (!blocked.includes(normalizedCategory)) { blocked.push(normalizedCategory); await GM_setValue("blockedCategories", JSON.stringify(blocked)); } } async function processCategoryCards() { const blockedCategories = await getBlockedCategories(); const blockedNormalized = blockedCategories.map((cat) => normalizeData(cat).toLowerCase().trim() ); document.querySelectorAll('[class*="group/card"]').forEach((card) => { const nameEl = card.querySelector('[data-testid^="category-"]'); if (!nameEl) return; const categoryName = normalizeData(nameEl.textContent) .toLowerCase() .trim(); if (blockedNormalized.includes(categoryName)) { card.style.display = "none"; return; } if (card.querySelector(".category-block-btn")) return; const imageWrapper = card.querySelector( 'a[href^="/category/"] > div.relative' ); if (!imageWrapper) return; const btn = document.createElement("button"); btn.textContent = "✖"; btn.title = `Hide category: ${nameEl.textContent}`; btn.className = "category-block-btn"; btn.style.cssText = ` position: absolute; top: 6px; right: 6px; background: rgba(0, 0, 0, 0.7); color: white; border: none; border-radius: 50%; width: 20px; height: 20px; font-size: 14px; cursor: pointer; z-index: 9999; `; btn.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); await blockCategory(categoryName); card.style.display = "none"; }); imageWrapper.style.position = "relative"; imageWrapper.appendChild(btn); }); } async function removeBlockedCategoryCards() { const blockedCategories = await getBlockedCategories(); document.querySelectorAll('[class*="group/card"]').forEach((card) => { const nameEl = card.querySelector('[data-testid^="category-"]'); if (!nameEl) return; const categoryName = normalizeData(nameEl.textContent) .toLowerCase() .trim(); const blockedNormalized = blockedCategories.map((cat) => normalizeData(cat).toLowerCase().trim() ); if (blockedNormalized.includes(categoryName)) { card.style.display = "none"; } else { card.style.display = ""; } }); } async function removeBlockedCards() { const blocked = await getBlockedChannels(); document.querySelectorAll('[class*="group/card"]').forEach((card) => { const anchor = card.querySelector('a[href^="/"]'); if (!anchor) return; const username = normalizeData(anchor.getAttribute("href").split("/")[1]); card.style.display = blocked.includes(username) ? "none" : ""; }); document .querySelectorAll("div.flex.w-full.shrink-0.grow-0.flex-col") .forEach((card) => { const anchor = card.querySelector('a[href^="/"]'); if (!anchor) return; const username = normalizeData( anchor.getAttribute("href").split("/")[1] ); card.style.display = blocked.includes(username) ? "none" : ""; }); // Block channel stream const usernameEl = document.getElementById("channel-username"); if (!usernameEl) { return; } const currentUsername = normalizeData(usernameEl.textContent); const videoPlayer = document.getElementById("video-player"); if (videoPlayer) { if (blocked.includes(currentUsername)) { videoPlayer.style.display = "none"; if (typeof videoPlayer.pause === "function") videoPlayer.pause(); } else { videoPlayer.style.display = ""; } } } // Sidebar, recommended channels async function removeSidebarBlockedChannels() { const blocked = await getBlockedChannels(); document .querySelectorAll('[data-testid^="sidebar-recommended-channel-"]') .forEach((item) => { const anchor = item.querySelector('a[href^="/"]') || item.closest('a[href^="/"]'); if (!anchor) return; const username = normalizeData( anchor.getAttribute("href").split("/")[1] ); anchor.style.display = blocked.includes(username) ? "none" : ""; }); } async function addBlockButtonOnChannelPage() { const usernameEl = document.getElementById("channel-username"); if (!usernameEl) return; if (document.getElementById("channelPageBlockBtn")) return; const username = usernameEl.textContent.trim(); const btn = document.createElement("button"); btn.id = "channelPageBlockBtn"; btn.textContent = "X"; btn.title = "Block this channel"; Object.assign(btn.style, { marginLeft: "8px", color: "white", backgroundColor: "red", border: "none", borderRadius: "4px", cursor: "pointer", fontSize: "16px", padding: "2px 8px", userSelect: "none", verticalAlign: "middle", lineHeight: "1", }); btn.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); await blockChannel(username); alert(`${username} blocked!`); location.reload(); }); const parent = usernameEl.parentElement; parent.style.display = "inline-flex"; parent.style.alignItems = "center"; parent.appendChild(btn); const blocked = await getBlockedChannels(); const videoPlayer = document.getElementById("video-player"); if (videoPlayer) { if (blocked.includes(normalizeData(username))) { videoPlayer.style.display = "none"; if (typeof videoPlayer.pause === "function") videoPlayer.pause(); } else { videoPlayer.style.display = ""; } } } async function processCards() { document.querySelectorAll('[class*="group/card"]').forEach((card) => { if (card.querySelector(".block-btn")) return; const anchor = card.querySelector('a[href^="/"]'); if (!anchor) return; const username = anchor.getAttribute("href").split("/")[1]; const titleEl = card.querySelector("a[title]"); if (!titleEl) return; const btn = createBlockButtonAbsolute(username); btn.classList.add("block-btn"); titleEl.parentElement.appendChild(btn); }); document .querySelectorAll("div.flex.w-full.shrink-0.grow-0.flex-col") .forEach((card) => { if (card.querySelector(".block-btn")) return; const anchor = card.querySelector('a[href^="/"]'); if (!anchor) return; const username = anchor.getAttribute("href").split("/")[1]; const followBtn = card.querySelector('button[aria-label="Takip Et"]'); if (!followBtn) { return; } const btn = createBlockButton(username); btn.classList.add("block-btn"); btn.style.marginLeft = "8px"; followBtn.insertAdjacentElement("afterend", btn); }); } async function processSidebarChannels() { const blocked = await getBlockedChannels(); document .querySelectorAll('[data-testid^="sidebar-recommended-channel-"]') .forEach((anchor) => { const username = anchor.getAttribute("href")?.split("/")[1]; if (!username) return; if (blocked.includes(normalizeData(username))) { anchor.style.display = "none"; return; } if (anchor.querySelector(".sidebar-block-btn")) return; const btn = document.createElement("button"); btn.textContent = "✕"; btn.className = "sidebar-block-btn"; btn.title = "Block this channel"; btn.style.cssText = ` position: absolute; top: 6px; right: 6px; background: rgba(255, 0, 0, 0.7); color: white; border: none; border-radius: 25%; width: 25px; height: 25px; font-size: 14px; display: none; cursor: pointer; z-index: 9999; `; btn.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); await blockChannel(username); await removeSidebarBlockedChannels(); await processSidebarChannels(); }); anchor.style.position = "relative"; anchor.addEventListener("mouseenter", () => { btn.style.display = "block"; }); anchor.addEventListener("mouseleave", () => { btn.style.display = "none"; }); anchor.appendChild(btn); }); } async function observeBlockedChatMessages() { let blockedUsers = await getBlockedChannels(); function normalize(name) { return name.trim().toLowerCase(); } const waitForChatContainer = () => new Promise((resolve) => { const check = () => { const container = document.querySelector("#chatroom-messages"); if (container) return resolve(container); requestAnimationFrame(check); }; check(); }); function hideChatMessage(node, username) { const content = node.querySelector('div[class*="betterhover"]'); if (content) { content.innerHTML = `<span style="color: gray; font-style: italic;">[${username} is blocked]</span>`; content.style.opacity = "0.3"; } } function processChatNode(node) { const userButton = node.querySelector("button[title]"); if (!userButton) return; const usernameChatter = userButton.getAttribute("title"); const normalizedChatter = normalize(usernameChatter); const isBlocked = blockedUsers.some( (blockedName) => normalize(blockedName) === normalizedChatter ); if (isBlocked) { hideChatMessage(node, usernameChatter); } } const chatContainer = await waitForChatContainer(); function debounce(fn, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }; } const processAddedNodes = debounce((mutationsList) => { for (const mutation of mutationsList) { for (const addedNode of mutation.addedNodes) { if (!(addedNode instanceof HTMLElement)) continue; if (addedNode.hasAttribute("data-index")) { processChatNode(addedNode); } else { addedNode.querySelectorAll("[data-index]").forEach(processChatNode); } } } }, 0); const chatObserver = new MutationObserver(processAddedNodes); chatObserver.observe(chatContainer, { childList: true, subtree: true, }); setTimeout(() => { chatContainer.querySelectorAll("[data-index]").forEach(processChatNode); }, 1000); async function refreshBlockedUsers() { blockedUsers = await getBlockedChannels(); chatContainer.querySelectorAll("[data-index]").forEach(processChatNode); } window.refreshBlockedUsers = refreshBlockedUsers; } async function observeChatUsernames() { const chatContainer = document.getElementById("chatroom-messages"); if (!chatContainer) return; function debounce(fn, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }; } async function addBlockButtonsToNodes(nodes) { nodes.forEach((msg) => { if (msg.querySelector(".username-block-btn")) return; const userButton = msg.querySelector("button[title]"); if (!userButton) return; const btn = document.createElement("button"); btn.textContent = "X"; btn.title = "Block this channel"; btn.className = "username-block-btn"; Object.assign(btn.style, { marginLeft: "6px", backgroundColor: "red", color: "white", border: "none", borderRadius: "3px", cursor: "pointer", fontSize: "10px", padding: "0 4px", userSelect: "none", verticalAlign: "middle", }); btn.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); const username = userButton.getAttribute("title").trim(); await blockChannel(username); await removeBlockedCards(); if (window.refreshBlockedUsers) { await window.refreshBlockedUsers(); } console.log(`${username} blocked!`); }); userButton.parentElement.appendChild(btn); }); } await addBlockButtonsToNodes( Array.from(chatContainer.querySelectorAll("[data-index]")) ); const observer = new MutationObserver( debounce((mutationsList) => { let addedNodes = []; for (const mutation of mutationsList) { mutation.addedNodes.forEach((node) => { if (node instanceof HTMLElement) { if (node.hasAttribute("data-index")) { addedNodes.push(node); } else { addedNodes.push(...node.querySelectorAll("[data-index]")); } } }); } if (addedNodes.length) addBlockButtonsToNodes(addedNodes); }, 100) ); observer.observe(chatContainer, { childList: true, subtree: true }); } // search page function createBlockButton(username) { const btn = document.createElement("button"); btn.textContent = "X"; btn.title = "Block this channel"; Object.assign(btn.style, { marginLeft: "8px", color: "white", backgroundColor: "red", border: "none", borderRadius: "4px", cursor: "pointer", fontSize: "12px", padding: "2px 6px", userSelect: "none", }); btn.addEventListener("click", async (e) => { e.stopPropagation(); e.preventDefault(); await blockChannel(username); await removeBlockedCards(); await processCards(); await processSidebarChannels(); }); return btn; } function createBlockButtonAbsolute(username) { const btn = document.createElement("button"); btn.textContent = "X"; btn.title = "Block this channel"; btn.style.cssText = ` position: absolute; top: 6px; right: 6px; background: rgba(255, 0, 0, 0.7); color: white; border: none; border-radius: 25%; width: 25px; height: 25px; font-size: 14px; cursor: pointer; z-index: 9999; `; btn.addEventListener("click", async (e) => { e.stopPropagation(); e.preventDefault(); await blockChannel(username); await removeBlockedCards(); await processCards(); await processSidebarChannels(); }); return btn; } // Menu for Tampermonkey/Violentmonkey/Userscript async function createToggleButtonAndPanel() { const toggleBtn = document.createElement("div"); toggleBtn.id = "kickToggleBtn"; toggleBtn.textContent = "K"; Object.assign(toggleBtn.style, { position: "fixed", top: "15px", right: "5px", width: "26px", height: "26px", lineHeight: "26px", textAlign: "center", fontSize: "16px", fontWeight: "bold", backgroundColor: "#00B660", color: "white", borderRadius: "50%", cursor: "pointer", zIndex: "10000", boxShadow: "0 0 5px rgba(0,0,0,0.3)", userSelect: "none", }); document.body.appendChild(toggleBtn); // Panel const panel = document.createElement("div"); panel.id = "kickControlPanel"; Object.assign(panel.style, { position: "fixed", top: "50px", right: "5px", backgroundColor: "#1e1e1e", color: "white", padding: "20px", borderRadius: "8px", zIndex: "9999", fontSize: "14px", fontFamily: "sans-serif", boxShadow: "0 0 15px rgba(0,0,0,0.7)", display: "none", width: "320px", maxHeight: "80vh", overflowY: "auto", boxSizing: "border-box", }); panel.innerHTML = ` <h1 style="color:#00b660; font-size:20px; margin-bottom:5px;">🛡️ uKickBlock</h1> <div style="color:#888; font-size:12px; margin-bottom:15px; font-style: italic;"> For Violentmonkey/Tampermonkey users </div> <section style="margin-bottom:25px;"> <h2 style="color:#b2ff59; font-size:16px; margin-bottom:8px;">Channels</h2> <div style="display:flex; gap:8px; margin-bottom:8px; flex-wrap: wrap;"> <button id="exportChannelsBtn" style="flex:1;">📤 Export</button> <button id="importChannelsBtn" style="flex:1;">📥 Import</button> <button id="clearChannelsBtn" style="flex:1;">🧹 Clear All</button> <button id="refreshChannelsBtn" style="flex:1;">🔄 Refresh</button> </div> <input type="text" id="channelInput" placeholder="Add channel..." style="width:100%; padding:8px; border-radius:4px; border:none; background:#333; color:#fff; font-size:14px;" /> <button id="addChannelBtn" style="width:100%; margin-top:8px; background:#00b660; border:none; border-radius:4px; color:#fff; cursor:pointer; padding:8px;">➕ Add Channel</button> <ul id="channelList" style="list-style:none; padding:0; margin-top:12px; max-height:150px; overflow-y:auto;"></ul> </section> <section> <h2 style="color:#b2ff59; font-size:16px; margin-bottom:8px;">Categories</h2> <div style="display:flex; gap:8px; margin-bottom:8px; flex-wrap: wrap;"> <button id="exportCategoriesBtn" style="flex:1;">📤 Export</button> <button id="importCategoriesBtn" style="flex:1;">📥 Import</button> <button id="clearCategoriesBtn" style="flex:1;">🧹 Clear All</button> <button id="refreshCategoriesBtn" style="flex:1;">🔄 Refresh</button> </div> <input type="text" id="categoryInput" placeholder="Add category..." style="width:100%; padding:8px; border-radius:4px; border:none; background:#333; color:#fff; font-size:14px;" /> <button id="addCategoryBtn" style="width:100%; margin-top:8px; background:#00b660; border:none; border-radius:4px; color:#fff; cursor:pointer; padding:8px;">➕ Add Category</button> <ul id="categoryList" style="list-style:none; padding:0; margin-top:12px; max-height:150px; overflow-y:auto;"></ul> </section> `; document.body.appendChild(panel); toggleBtn.addEventListener("click", () => { panel.style.display = panel.style.display === "none" ? "block" : "none"; }); function addListItem(listEl, name) { const li = document.createElement("li"); li.style = "background:#2a2a2a; padding:6px 10px; margin-bottom:6px; border-radius:4px; display:flex; justify-content:space-between; align-items:center; font-size:14px;"; li.textContent = name; const btn = document.createElement("button"); btn.textContent = "X"; btn.style = "background:#d9534f; border:none; border-radius:4px; color:white; cursor:pointer; padding:3px 8px; font-size:13px;"; btn.addEventListener("click", async () => { if (listEl.id === "channelList") { let channels = await getBlockedChannels(); channels = channels.filter((c) => c !== name); await saveBlockedChannels(channels); renderChannels(); } else { let categories = await getBlockedCategories(); categories = categories.filter((c) => c !== name); await saveBlockedCategories(categories); renderCategories(); } }); li.appendChild(btn); listEl.appendChild(li); } async function renderChannels() { const list = panel.querySelector("#channelList"); list.innerHTML = ""; const channels = await getBlockedChannels(); channels.forEach((name) => addListItem(list, name)); } async function renderCategories() { const list = panel.querySelector("#categoryList"); list.innerHTML = ""; const categories = await getBlockedCategories(); categories.forEach((name) => addListItem(list, name)); } // Add panel .querySelector("#addChannelBtn") .addEventListener("click", async () => { const input = panel.querySelector("#channelInput"); const val = input.value.trim(); if (!val) return alert("Please enter a channel name"); let channels = await getBlockedChannels(); const normalized = normalizeData(val); if (channels.includes(normalized)) return alert("Already blocked"); channels.push(normalized); await saveBlockedChannels(channels); input.value = ""; renderChannels(); }); panel .querySelector("#addCategoryBtn") .addEventListener("click", async () => { const input = panel.querySelector("#categoryInput"); const val = input.value.trim(); if (!val) return alert("Please enter a category name"); let categories = await getBlockedCategories(); const normalized = normalizeData(val); if (categories.includes(normalized)) return alert("Already blocked"); categories.push(normalized); await saveBlockedCategories(categories); input.value = ""; renderCategories(); }); // Clear panel .querySelector("#clearChannelsBtn") .addEventListener("click", async () => { if (confirm("Clear all blocked channels?")) { await saveBlockedChannels([]); renderChannels(); } }); panel .querySelector("#clearCategoriesBtn") .addEventListener("click", async () => { if (confirm("Clear all blocked categories?")) { await saveBlockedCategories([]); renderCategories(); } }); // Export panel .querySelector("#exportChannelsBtn") .addEventListener("click", async () => { const data = await getBlockedChannels(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "blocked_channels.json"; a.click(); URL.revokeObjectURL(url); }); panel .querySelector("#exportCategoriesBtn") .addEventListener("click", async () => { const data = await getBlockedCategories(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "blocked_categories.json"; a.click(); URL.revokeObjectURL(url); }); // Import panel.querySelector("#importChannelsBtn").addEventListener("click", () => { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const text = await file.text(); try { const parsed = JSON.parse(text); if (Array.isArray(parsed)) { await saveBlockedChannels(parsed.map(normalizeData)); renderChannels(); alert("Imported successfully."); } else alert("Invalid format."); } catch { alert("Invalid JSON."); } }; input.click(); }); panel .querySelector("#importCategoriesBtn") .addEventListener("click", () => { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const text = await file.text(); try { const parsed = JSON.parse(text); if (Array.isArray(parsed)) { await saveBlockedCategories(parsed.map(normalizeData)); renderCategories(); alert("Imported successfully."); } else alert("Invalid format."); } catch { alert("Invalid JSON."); } }; input.click(); }); // Refresh panel .querySelector("#refreshChannelsBtn") .addEventListener("click", renderChannels); panel .querySelector("#refreshCategoriesBtn") .addEventListener("click", renderCategories); renderChannels(); renderCategories(); } function debounce(fn, delay = 25) { let timer; return () => { clearTimeout(timer); timer = setTimeout(() => fn(), delay); }; } // ==== Chrome Extension ==== /* (async () => { const { enabled = true } = await chrome.storage.local.get("enabled"); let observer = null; async function startProcessing() { await processCards(); await processSidebarChannels(); await processCategoryCards(); await removeBlockedCards(); await removeSidebarBlockedChannels(); await removeBlockedCategoryCards(); await addBlockButtonOnChannelPage(); await observeBlockedChatMessages(); await observeChatUsernames(); } function startObserver() { if (observer) return; observer = new MutationObserver( debounce(async () => { if (!(await isEnabled())) return; await startProcessing(); }, 50) ); observer.observe(document.body, { childList: true, subtree: true }); } function stopObserver() { if (observer) { observer.disconnect(); observer = null; } } async function isEnabled() { const { enabled = true } = await chrome.storage.local.get("enabled"); return enabled; } if (enabled) { startObserver(); await startProcessing(); } chrome.storage.onChanged.addListener((changes, areaName) => { if (areaName === "local" && "enabled" in changes) { const newValue = changes.enabled.newValue; if (newValue) { startObserver(); startProcessing(); } else { stopObserver(); } } }); })(); */ // ==== Firefox Extension ==== /* (async () => { const { enabled = true } = await browser.storage.local.get("enabled"); let observer = null; async function startProcessing() { await processCards(); await processSidebarChannels(); await processCategoryCards(); await removeBlockedCards(); await removeSidebarBlockedChannels(); await removeBlockedCategoryCards(); await addBlockButtonOnChannelPage(); await observeBlockedChatMessages(); await observeChatUsernames(); } function startObserver() { if (observer) return; observer = new MutationObserver( debounce(async () => { if (!(await isEnabled())) return; await startProcessing(); }, 50) ); observer.observe(document.body, { childList: true, subtree: true }); } function stopObserver() { if (observer) { observer.disconnect(); observer = null; } } async function isEnabled() { const { enabled = true } = await browser.storage.local.get("enabled"); return enabled; } if (enabled) { startObserver(); await startProcessing(); } browser.storage.onChanged.addListener((changes, areaName) => { if (areaName === "local" && "enabled" in changes) { const newValue = changes.enabled.newValue; if (newValue) { startObserver(); startProcessing(); } else { stopObserver(); } } }); })(); */ // ==== Tampermonkey/Violentmonkey UserScript ==== (async () => { let observer = null; async function startProcessing() { await processCards(); await processSidebarChannels(); await processCategoryCards(); await removeBlockedCards(); await removeSidebarBlockedChannels(); await removeBlockedCategoryCards(); await addBlockButtonOnChannelPage(); await observeBlockedChatMessages(); await observeChatUsernames(); } function startObserver() { if (observer) return; observer = new MutationObserver( debounce(async () => { await startProcessing(); }, 50) ); observer.observe(document.body, { childList: true, subtree: true }); } startObserver(); await createToggleButtonAndPanel(); await startProcessing(); })(); })();