您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 YT 菜单中添加“屏蔽频道”选项。自动隐藏被屏蔽频道的视频。
当前为
// ==UserScript== // @name deTube Block Channels // @name:de deTube Kanäle blockieren // @name:es deTube Bloquear canales // @name:fr deTube Bloquer des chaînes // @name:it deTube Blocca canali // @name:pt deTube Bloquear canais // @name:ru deTube Блокировать каналы // @name:ja deTube チャンネルをブロック // @name:ko deTube 채널 차단 // @name:zh-CN deTube 屏蔽频道 // @name:zh-TW deTube 封鎖頻道 // @name:nl deTube Kanalen blokkeren // @name:pl deTube Blokuj kanały // @name:sv deTube Blockera kanaler // @name:da deTube Bloker kanaler // @name:no deTube Blokker kanaler // @name:fi deTube Estä kanavat // @name:tr deTube Kanalları Engelle // @name:ar deTube حظر القنوات // @name:he deTube חסום ערוצים // @name:hi deTube चैनल ब्लॉक करें // @name:th deTube บล็อกช่อง // @name:vi deTube Chặn kênh // @version 0.1.1 // @description Adds a "Block Channel" option to YT menus. Hide videos from blocked channels automatically. // @description:de Fügt eine Option "Kanal blockieren" zu YT-Menüs hinzu. Blendet Videos blockierter Kanäle automatisch aus. // @description:es Agrega una opción "Bloquear canal" a los menús de YT. Oculta automáticamente videos de canales bloqueados. // @description:fr Ajoute une option "Bloquer la chaîne" dans les menus de YT. Masque automatiquement les vidéos des chaînes bloquées. // @description:it Aggiunge un'opzione "Blocca canale" ai menu di YT. Nasconde automaticamente i video dei canali bloccati. // @description:pt Adiciona a opção "Bloquear canal" aos menus do YT. Oculta automaticamente vídeos de canais bloqueados. // @description:ru Добавляет опцию «Блокировать канал» в меню YT. Автоматически скрывает видео заблокированных каналов. // @description:ja YT のメニューに「チャンネルをブロック」オプションを追加します。ブロックしたチャンネルの動画を自動的に非表示にします。 // @description:ko YT 메뉴에 "채널 차단" 옵션을 추가합니다. 차단된 채널의 동영상을 자동으로 숨깁니다. // @description:zh-CN 在 YT 菜单中添加“屏蔽频道”选项。自动隐藏被屏蔽频道的视频。 // @description:zh-TW 在 YT 選單中新增「封鎖頻道」選項。自動隱藏來自被封鎖頻道的影片。 // @description:nl Voegt een "Kanaal blokkeren" optie toe aan YT-menu's. Verbergt video's van geblokkeerde kanalen automatisch. // @description:pl Dodaje opcję "Zablokuj kanał" do menu YT. Automatycznie ukrywa filmy z zablokowanych kanałów. // @description:sv Lägger till ett alternativ "Blockera kanal" i YT-menyer. Döljer automatiskt videor från blockerade kanaler. // @description:da Tilføjer en "Bloker kanal" mulighed til YT-menuer. Skjuler automatisk videoer fra blokerede kanaler. // @description:no Legger til "Blokker kanal"-valg i YT-menyer. Skjuler automatisk videoer fra blokkerte kanaler. // @description:fi Lisää "Estä kanava" -valinnan YTn valikoihin. Piilottaa automaattisesti estettyjen kanavien videot. // @description:tr YT menülerine "Kanalı Engelle" seçeneği ekler. Engellenen kanalların videolarını otomatik gizler. // @description:ar يضيف خيار "حظر القناة" إلى قوائم YT. يخفي تلقائيًا مقاطع الفيديو من القنوات المحظورة. // @description:he מוסיף אפשרות "חסום ערוץ" לתפריטי YT. מסתיר באופן אוטומטי סרטונים מערוצים חסומים. // @description:hi YT मेनू में "चैनल ब्लॉक करें" विकल्प जोड़ता है। ब्लॉक किए गए चैनलों के वीडियो को स्वचालित रूप से छुपाता है। // @description:th เพิ่มตัวเลือก "บล็อกช่อง" ในเมนู YT ซ่อนวิดีโอจากช่องที่ถูกบล็อกโดยอัตโนมัติ // @description:vi Thêm tùy chọn "Chặn kênh" vào menu YT. Tự động ẩn video từ các kênh bị chặn. // @author polymegos // @namespace https://github.com/polymegos/deTube_channel_blocker // @supportURL https://github.com/polymegos/deTube_channel_blocker/issues // @license MIT // @match *://www.youtube.com/* // @match *://www.youtube-nocookie.com/* // @match *://m.youtube.com/* // @match *://music.youtube.com/* // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @compatible firefox // @compatible edge // @compatible safari // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY = 'detube_blocked_channels_store'; let blocked = new Set(); let lastRenderer = null; // Shorts blocker persistent state const SHORTS_STORAGE_KEY = 'detube_shorts_block_enabled'; let shortsEnabled = false; let shortsUrlObserver = null; let shortsDomObserver = null; const log = (...a) => console.log('%c[deTube Block Channels]', 'color: green; font-weight: bold;', ...a); async function loadBlocked() { // Load blocked channels from storage const raw = await GM_getValue(STORAGE_KEY, '[]'); try { blocked = new Set(JSON.parse(raw)); log('Loaded blocked:', [...blocked]); } catch(e){ blocked = new Set(); log('Load-error', e); } } // Shorts blocking const SHORTS_BLOCK_SELECTORS = [ 'ytd-reel-shelf-renderer', 'a[title="Shorts"]', 'div#dismissible.style-scope.ytd-rich-shelf-renderer' ]; function redirectIfShortsURL(url) { const shortsRegex = /^https:\/\/www\.youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})(\?.*)?$/; const match = url.match(shortsRegex); if (match) { const videoId = match[1]; const query = window.location.search || ''; const newUrl = `https://www.youtube.com/watch?v=${videoId}${query}`; window.location.replace(newUrl); } } function removeShortsElements() { if (!shortsEnabled) return; SHORTS_BLOCK_SELECTORS.forEach(sel => { document.querySelectorAll(sel).forEach(el => el.remove()); }); } function setupShortsBlocking(enable) { // Tear down previous observers if (shortsUrlObserver) { try { shortsUrlObserver.disconnect(); } catch(_){} shortsUrlObserver = null; } if (shortsDomObserver) { try { shortsDomObserver.disconnect(); } catch(_){} shortsDomObserver = null; } shortsEnabled = !!enable; if (!shortsEnabled) return; // Redirect current /shorts/ if needed redirectIfShortsURL(window.location.href); // Observe SPA URL changes let lastUrl = location.href; shortsUrlObserver = new MutationObserver(() => { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; redirectIfShortsURL(currentUrl); } }); shortsUrlObserver.observe(document, { subtree: true, childList: true }); // Observe DOM for Shorts UI and remove them const initDomObs = () => { if (document.body) { shortsDomObserver = new MutationObserver(removeShortsElements); shortsDomObserver.observe(document.body, { childList: true, subtree: true }); removeShortsElements(); } else { requestAnimationFrame(initDomObs); } }; initDomObs(); } async function saveBlocked() { // Persist blocked channels await GM_setValue(STORAGE_KEY, JSON.stringify([...blocked])); log('Saved blocked list:', [...blocked]); } async function loadShortsSetting() { try { const raw = await GM_getValue(SHORTS_STORAGE_KEY, 'false'); shortsEnabled = String(raw) === 'true'; log('Loaded shorts setting:', shortsEnabled); } catch (e) { shortsEnabled = false; log('Load-error shorts', e); } } async function saveShortsSetting() { await GM_setValue(SHORTS_STORAGE_KEY, shortsEnabled ? 'true' : 'false'); log('Saved shorts setting:', shortsEnabled); } function tagVideo(el) { // Tag matching for videos const selectorsToTry = [ // Generic '#channel-name a', 'ytd-channel-name a', 'a[href*="/@"]', 'a[href*="/channel/"]', 'a[href*="/c/"]', 'a[href*="/user/"]', // Lookup (sidebar/related etc.) '.yt-lockup-byline a', '.yt-lockup-metadata-view-model-wiz__title a', 'span.yt-core-attributed-string.yt-content-metadata-view-model-wiz__metadata-text', // Homepage '.yt-lockup-metadata-view-model-wiz__metadata .yt-core-attributed-string__link', '.yt-content-metadata-view-model-wiz__metadata-row .yt-core-attributed-string__link', // Search '#text-container a.yt-simple-endpoint.style-scope.yt-formatted-string', // Fallbacks 'yt-formatted-string a', 'yt-formatted-string', '.yt-lockup-metadata-view-model-wiz__title', '.yt-lockup-metadata-view-model-wiz', ]; for (const selector of selectorsToTry) { const candidate = el.querySelector(selector); if (candidate && candidate.textContent.trim()) { const name = candidate.textContent.trim(); el.dataset.detube = name; log(`[+] Tagged video with channel: "${name}" using selector "${selector}"`); return true; } } log('[!] Could not find channel name with any selector inside:', el); return false; } function tagEmAll() { const els = document.querySelectorAll([ 'yt-lockup-view-model', 'ytd-video-renderer', 'ytd-compact-video-renderer', 'ytd-grid-video-renderer', 'ytd-rich-item-renderer' ].join(',')); let count = 0; for (let el of els) if (tagVideo(el)) count++; log(`Tagged ${count}/${els.length} videos.`); } function removeBlockedVideos() { const videoSelectors = [ 'yt-lockup-view-model', 'ytd-grid-video-renderer', 'ytd-video-renderer', 'ytd-compact-video-renderer', 'ytd-rich-item-renderer' ]; document.querySelectorAll(videoSelectors.join(',')).forEach(item => { // Ensure we have a tag if (!item.dataset.detube) { tagVideo(item); } const name = item.dataset.detube && item.dataset.detube.trim(); if (name && blocked.has(name)) { item.remove(); } }); } function applyCSS() { let s = document.getElementById('detube_style_v4'); if (!s) { s = document.createElement('style'); s.id = 'detube_style_v4'; document.head.append(s); } const baseTargets = [ 'yt-lockup-view-model', 'ytd-video-renderer', 'ytd-compact-video-renderer', 'ytd-grid-video-renderer', 'ytd-rich-item-renderer' ]; const rules = [...blocked].map(n => `${baseTargets.map(t => `${t}[data-detube="${CSS.escape(n)}"]`).join(', ')} { display: none !important; }` ).join('\n'); s.textContent = rules; log(`Applied ${blocked.size} CSS rules.`); } function observeMenus() { const observer = new MutationObserver(() => { const menu = document.querySelector('yt-list-view-model'); if (menu && lastRenderer) { // Re-tag in case the dataset wasn't updated yet tagVideo(lastRenderer); const channel = lastRenderer.dataset.detube; // Get channel name from dataset if (channel) { injectButton(channel); // Refresh channel name every time lastRenderer = null; // Reset, prevent same renderer reuse } else { log('[!] Menu opened but no channel found on lastRenderer.'); } } }); observer.observe(document.body, { childList: true, subtree: true }); } function injectButton(channel) { const menu = document.querySelector('yt-list-view-model'); if (!menu) { log('[!] Menu not found for injection'); return; } // Remove any previous injected button const oldButton = menu.querySelector('.detube-block-button'); if (oldButton) oldButton.remove(); const button = document.createElement('yt-list-item-view-model'); button.className = 'detube-block-button'; button.setAttribute('role', 'menuitem'); button.setAttribute('tabindex', '0'); const labelDiv = document.createElement('div'); labelDiv.className = 'yt-list-item-view-model-wiz__label yt-list-item-view-model-wiz__container yt-list-item-view-model-wiz__container--compact yt-list-item-view-model-wiz__container--tappable yt-list-item-view-model-wiz__container--in-popup'; const textWrapper = document.createElement('div'); textWrapper.className = 'yt-list-item-view-model-wiz__text-wrapper'; const titleWrapper = document.createElement('div'); titleWrapper.className = 'yt-list-item-view-model-wiz__title-wrapper'; const span = document.createElement('span'); span.className = 'yt-core-attributed-string yt-list-item-view-model-wiz__title'; span.setAttribute('role', 'text'); span.textContent = ` 🚫 Block ${channel}`; // This is hilarious titleWrapper.appendChild(span); textWrapper.appendChild(titleWrapper); labelDiv.appendChild(textWrapper); button.appendChild(labelDiv); button.addEventListener('click', () => { blocked.add(channel); saveBlocked(); applyCSS(); tagEmAll(); log(`[>] Blocked channel: ${channel}`); }); menu.appendChild(button); log(`[+] Injected block button for "${channel}"`); } function createManagementButton() { const button = document.createElement('button'); button.id = 'detube-manage-btn'; button.title = 'Manage Blocked Channels'; button.textContent = '🚫'; button.style.cssText = ` background: none; border: none; color: var(--yt-spec-text-primary); cursor: pointer; font-size: 20px; padding: 6px 10px; border-radius: 50%; margin-left: 8px; transition: background-color 0.2s ease; display: inline-flex; align-items: center; justify-content: center; `; button.addEventListener('mouseenter', () => { button.style.backgroundColor = 'rgba(0,0,0,0.1)'; }); button.addEventListener('mouseleave', () => { button.style.backgroundColor = 'transparent'; }); button.addEventListener('click', openBlockedChannelsTab); return button; } function injectManagementButton() { // Manager button to view list of blocked channels const tryInject = () => { const masthead = document.querySelector('ytd-masthead #end') || document.querySelector('ytd-masthead'); if (masthead && !document.getElementById('detube-manage-btn')) { const managementButton = createManagementButton(); managementButton.style.marginLeft = '8px'; masthead.appendChild(managementButton); log('[+] Management button injected'); } }; tryInject(); const mastheadObserver = new MutationObserver(tryInject); mastheadObserver.observe(document.body, { childList: true, subtree: true }); setTimeout(() => mastheadObserver.disconnect(), 30000); } function generateBlockedChannelsHTML() { // Best way to manage is to direct away to local page, generate HTML for blocked channels overview const blockedArray = [...blocked].sort(); const channelItems = blockedArray.map(channel => ` <div class="channel-item" data-channel="${channel.replace(/"/g, '"')}"> <span class="channel-name">${channel.replace(/</g, '<').replace(/>/g, '>')}</span> <button class="unblock-btn" onclick="unblockChannel('${channel.replace(/'/g, "\\'")}')"> <span>✕</span> </button> </div> `).join(''); return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>deTube - Blocked Channels Manager</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } @media (prefers-color-scheme: light) { body { background: linear-gradient(135deg, #f2f2f2 0%, #ffffff 100%); } .container { background: rgba(255, 255, 255, 0.95); color: #2c3e50; } .header { background: linear-gradient(135deg, #ff6b6b, #ee5a24); color: white; } .stat-number { color: #ee5a24; } .stat-label { color: #666; } .channel-item { background: white; border-left: 4px solid #ee5a24; } .channel-name { color: #2c3e50; } .channels-list::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #ff6b6b, #ee5a24); } .channels-list::-webkit-scrollbar-track { background: #e1e1e1; } .footer { color: #666; border-top: 1px solid rgba(0, 0, 0, 0.1); } .empty-state { color: #666; } } @media (prefers-color-scheme: dark) { body { background: linear-gradient(135deg, #222222 0%, #333333 100%); color: #f5f5f5; } .container { background: rgba(30, 30, 30, 0.95); color: #f5f5f5; } .header { background: linear-gradient(135deg, #ff6b6b, #ee5a24); color: white; } .stat-number { color: #ff9f43; } .stat-label { color: #ccc; } .channel-item { background: #2e2e2e; border-left: 4px solid #ff6b6b; } .channel-name { color: #f5f5f5; } .channels-list::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #ff6b6b, #ee5a24); } .channels-list::-webkit-scrollbar-track { background: #414141; } .footer { color: #999; border-top: 1px solid rgba(255, 255, 255, 0.1); } .empty-state { color: #aaa; } } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; padding: 20px; } .container { max-width: 800px; margin: 0 auto; border-radius: 5px; backdrop-filter: blur(10px); overflow: hidden; } .header { padding: 30px; text-align: center; } .header h1 { font-size: 2.5rem; margin-bottom: 10px; font-weight: 700; } .header p { opacity: 0.9; font-size: 1.1rem; } .stats { display: flex; justify-content: space-around; padding: 20px; border-bottom: 1px solid rgba(0, 0, 0, 0.1); } .stat { text-align: center; } .stat-number { font-size: 2rem; font-weight: bold; color: #ee5a24; } .stat-label { color: #666; font-size: 0.9rem; margin-top: 5px; } .controls { padding: 20px; display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; } /* Toggle switch */ .toggle { display: inline-flex; align-items: center; gap: 10px; font-weight: 600; font-size: 13px; padding: 10px 16px; border-radius: 25px; background: rgba(0,0,0,0.06); } .switch { position: relative; display: inline-block; width: 46px; height: 26px; } .switch input { position: absolute; opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; transition: .3s; border-radius: 26px; } .switch { cursor: pointer; } .slider:before { position: absolute; content: ''; height: 20px; width: 20px; left: 3px; bottom: 3px; background: white; transition: .3s; border-radius: 50%; } input:checked + .slider { background: linear-gradient(135deg, #ff6b6b, #ee5a24); } input:checked + .slider:before { transform: translateX(20px); } .btn { background: linear-gradient(135deg, #ff6b6b, #ee5a24); color: white; border: none; padding: 12px 24px; border-radius: 25px; cursor: pointer; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(238, 90, 36, 0.3); } .btn:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(238, 90, 36, 0.4); } .btn.danger { background: linear-gradient(135deg, #e74c3c, #c0392b); box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3); } .btn.danger:hover { box-shadow: 0 8px 25px rgba(231, 76, 60, 0.4); } .channels-list { padding: 20px; max-height: 60vh; overflow-y: auto; } .channels-list::-webkit-scrollbar { width: 8px; } .channels-list::-webkit-scrollbar-track { border-radius: 10px; } .channels-list::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #ff6b6b, #ee5a24); border-radius: 10px; } .channel-item { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; margin-bottom: 10px; border-radius: 15px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); transition: all 0.3s ease; border-left: 4px solid #ee5a24; max-height: 80px; overflow: hidden; } .channel-item:hover { transform: translateX(5px); box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); } .channel-name { font-weight: 600; flex: 1; padding-right: 15px; } .channel-item.removing { opacity: 0; transform: translateX(20px); max-height: 0; padding-top: 0; padding-bottom: 0; margin-bottom: 0; } .unblock-btn { background: linear-gradient(135deg, #e74c3c, #c0392b); color: white; border: none; padding: 8px 16px; border-radius: 20px; cursor: pointer; font-size: 0.9rem; font-weight: 600; transition: all 0.3s ease; display: flex; align-items: center; gap: 5px; } .unblock-btn:hover { background: linear-gradient(135deg, #c0392b, #a93226); transform: scale(1.05); } .empty-state { text-align: center; padding: 60px 20px; color: #666; } .empty-state svg { width: 80px; height: 80px; margin-bottom: 20px; opacity: 0.5; } .footer { text-align: center; padding: 20px; color: #666; font-size: 0.9rem; border-top: 1px solid rgba(0, 0, 0, 0.1); } @media (max-width: 600px) { .container { margin: 10px; border-radius: 15px; } .header h1 { font-size: 2rem; } .stats { flex-direction: column; gap: 15px; } .channel-item { flex-direction: column; align-items: flex-start; gap: 10px; } .unblock-btn { align-self: flex-end; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>🚫 deTube Channel Blocker</h1> </div> <div class="controls"> <button class="btn" onclick="refreshPage()">Refresh</button> <button class="btn danger" onclick="clearAllChannels()" ${blockedArray.length === 0 ? 'disabled' : ''}> Clear All (${blockedArray.length}) </button> <div class="toggle" title="Toggle blocking of Shorts (persisted)"> <label class="switch"> <input id="shorts-toggle" type="checkbox" ${shortsEnabled ? 'checked' : ''} /> <span class="slider"></span> </label> <span>Block Shorts</span> </div> </div> <div class="channels-list"> ${blockedArray.length === 0 ? ` <div class="empty-state"> <svg viewBox="0 0 24 24" fill="currentColor"> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.58L19 8l-9 9z"/> </svg> <h3>No blocked channels yet!</h3> <p>Start blocking channels by right-clicking on a video's three-dot menu and selecting "Block Channel"</p> </div> ` : channelItems} </div> </div> <script> function unblockChannel(channelName) { if (!confirm('Are you sure you want to unblock "' + channelName + '"?')) return; const item = document.querySelector('.channel-item[data-channel="' + channelName.replace(/"/g, '\\"') + '"]'); const finish = () => { window.name = JSON.stringify({ action: 'unblock', channel: channelName }); // Request UI rebuild from parent, i.e., refresh page try { refreshPage(); } catch(_) {} }; if (item) { let done = false; const onEnd = () => { if (done) return; done = true; item.removeEventListener('transitionend', onEnd); finish(); }; item.addEventListener('transitionend', onEnd); setTimeout(onEnd, 400); requestAnimationFrame(() => item.classList.add('removing')); } else { finish(); } } function clearAllChannels() { if (!confirm('Are you sure you want to unblock all ${blockedArray.length} channels? This cannot be undone.')) return; const items = Array.from(document.querySelectorAll('.channel-item')); if (items.length === 0) { window.name = JSON.stringify({ action: 'clearAll' }); return; } items.forEach((el, i) => setTimeout(() => el.classList.add('removing'), i * 25)); setTimeout(() => { window.name = JSON.stringify({ action: 'clearAll' }); }, 300 + items.length * 25); } function refreshPage() { try { const pending = JSON.parse(window.name || 'null'); if (pending && pending.action && pending.action !== 'refreshManager') { // Defer refresh, avoid running over pending unblock / clearAll / toggle setTimeout(() => { window.name = JSON.stringify({ action: 'refreshManager' }); }, 600); return; } } catch (_) { /* idc about parse errors */ } window.name = JSON.stringify({ action: 'refreshManager' }); } // Shorts toggle handling document.addEventListener('DOMContentLoaded', () => { const t = document.getElementById('shorts-toggle'); if (t) { t.addEventListener('change', () => { window.name = JSON.stringify({ action: 'toggleShorts', enabled: t.checked }); }); } }); </script> </body> </html>`; } function openBlockedChannelsTab() { // Blocked channels management tab const html = generateBlockedChannelsHTML(); const blob = new Blob([html], { type: 'text/html' }); const url = URL.createObjectURL(blob); const newTab = window.open(url, '_blank'); // Monitor the new tab for actions const checkForActions = setInterval(() => { try { if (newTab.closed) { clearInterval(checkForActions); URL.revokeObjectURL(url); return; } if (newTab.window && newTab.window.name) { const action = JSON.parse(newTab.window.name); if (action.action === 'unblock' && action.channel) { blocked.delete(action.channel); saveBlocked(); applyCSS(); tagEmAll(); log(`[>] Unblocked channel: ${action.channel}`); newTab.window.name = ''; // Clear the action } else if (action.action === 'clearAll') { blocked.clear(); saveBlocked(); applyCSS(); tagEmAll(); log('[>] Cleared all blocked channels'); newTab.window.name = ''; // Clear the action again } else if (action.action === 'refreshManager') { // Rebuild the manager UI from current state and navigate the tab to it const freshUrl = URL.createObjectURL(new Blob([generateBlockedChannelsHTML()], { type: 'text/html' })); try { newTab.location.href = freshUrl; } catch (_) {} // Clear, avoids repeated triggers of refreshManager after navigating to the new URL try { newTab.window.name = ''; } catch (_) {} } else if (action.action === 'toggleShorts') { shortsEnabled = !!action.enabled; saveShortsSetting(); setupShortsBlocking(shortsEnabled); log(`[>] Shorts blocking: ${shortsEnabled ? 'ENABLED' : 'DISABLED'}`); newTab.window.name = ''; } } } catch (e) { // Cross-origin errors are really to be expected } }, 500); // Stop checking after ~5 minutes setTimeout(() => { clearInterval(checkForActions); URL.revokeObjectURL(url); }, 300000); } function observeNewVideos() { const observer = new MutationObserver(mutations => { let newVideosFound = false; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof HTMLElement)) continue; if (node.matches('yt-lockup-view-model') || node.querySelector('yt-lockup-view-model')) { newVideosFound = true; } } } if (newVideosFound) { tagEmAll(); applyCSS(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // Run on "(...).transpose()" clicks document.body.addEventListener('click', e => { const dot = e.target.closest('div.yt-spec-touch-feedback-shape__fill'); if (!dot) return; const renderer = dot.closest('yt-lockup-view-model'); if (!renderer) return; // Re-tag renderer with fresh channel name if (tagVideo(renderer)) { lastRenderer = renderer; log('Three-dot clicked for:', renderer.dataset.detube); } else { log('Could not tag renderer.'); } }, true); (async () => { log('Initializing...'); await loadBlocked(); await loadShortsSetting(); tagEmAll(); removeBlockedVideos(); applyCSS(); observeMenus(); injectManagementButton(); observeNewVideos(); setupShortsBlocking(shortsEnabled); const observer = new MutationObserver(() => { removeBlockedVideos(); if (shortsEnabled) removeShortsElements(); }); observer.observe(document.body, { childList: true, subtree: true, }); window.addEventListener('yt-navigate-finish', removeBlockedVideos); window.addEventListener('yt-navigate-finish', () => { if (shortsEnabled) removeShortsElements(); }); log('Ready. Await three-dot click, open menu, then see log/injected.'); })(); })();