// ==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.2
// @description Adds a "Block Channel" and "Block Video" option to YT menus. Hide videos from blocked channels and videos automatically. Allow for blocking short-form content.
// @description:de Fügt "Kanal blockieren" und "Video blockieren" zu YT-Menüs hinzu. Blendet automatisch Videos blockierter Kanäle und einzelner Videos aus. Ermöglicht das Blockieren von Kurzvideos.
// @description:es Agrega "Bloquear canal" y "Bloquear video" a los menús de YT. Oculta automáticamente videos de canales y videos bloqueados. Permite bloquear contenido corto.
// @description:fr Ajoute "Bloquer la chaîne" et "Bloquer la vidéo" aux menus YT. Masque automatiquement les vidéos des chaînes et vidéos bloquées. Permet de bloquer les formats courts.
// @description:it Aggiunge "Blocca canale" e "Blocca video" ai menu di YT. Nasconde automaticamente i video dei canali e dei video bloccati. Consente di bloccare i contenuti brevi.
// @description:pt Adiciona "Bloquear canal" e "Bloquear vídeo" aos menus do YT. Oculta automaticamente vídeos de canais e vídeos bloqueados. Permite bloquear conteúdo curto.
// @description:ru Добавляет «Заблокировать канал» и «Заблокировать видео» в меню YT. Автоматически скрывает видео с заблокированных каналов и отдельные видео. Поддерживает блокировку коротких видео.
// @description:ja YTのメニューに「チャンネルをブロック」「動画をブロック」を追加します。ブロックしたチャンネルや動画を自動的に非表示にします。ショート動画のブロックにも対応。
// @description:ko YT 메뉴에 "채널 차단" 및 "동영상 차단" 옵션을 추가합니다. 차단된 채널과 동영상을 자동으로 숨깁니다. 쇼츠 콘텐츠도 차단할 수 있습니다.
// @description:zh-CN 在 YT 菜单中添加“屏蔽频道”和“屏蔽视频”选项。自动隐藏被屏蔽频道和视频的内容。支持屏蔽短视频内容。
// @description:zh-TW 在 YT 選單中新增「封鎖頻道」與「封鎖影片」選項。自動隱藏來自被封鎖頻道與影片的內容。支援封鎖短影片內容。
// @description:nl Voegt "Kanaal blokkeren" en "Video blokkeren" toe aan YT-menu's. Verbergt automatisch video's van geblokkeerde kanalen en geblokkeerde video's. Ondersteunt het blokkeren van korte content.
// @description:pl Dodaje "Zablokuj kanał" i "Zablokuj wideo" do menu YT. Automatycznie ukrywa filmy z zablokowanych kanałów i pojedyncze filmy. Umożliwia blokowanie krótkich treści.
// @description:sv Lägger till "Blockera kanal" och "Blockera video" i YT-menyer. Döljer automatiskt videor från blockerade kanaler och enskilda videor. Stödjer blockering av kortinnehåll.
// @description:da Tilføjer "Bloker kanal" og "Bloker video" til YT-menuer. Skjuler automatisk videoer fra blokerede kanaler og enkelte videoer. Understøtter blokering af kortindhold.
// @description:no Legger til "Blokker kanal" og "Blokker video" i YT-menyene. Skjuler automatisk videoer fra blokkerte kanaler og videoer. Støtter blokkering av kortinnhold.
// @description:fi Lisää "Estä kanava" ja "Estä video" YT-valikoihin. Piilottaa automaattisesti estettyjen kanavien ja videoiden sisällöt. Tukee lyhytsisällön estoa.
// @description:tr YT menülerine "Kanalı Engelle" ve "Videoyu Engelle" seçeneklerini ekler. Engellenen kanalların ve videoların içeriklerini otomatik gizler. Kısa içerik engelleme desteği sunar.
// @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à "Chặn video" vào menu YT. Tự động ẩn video từ các kênh và video bị chặn. Hỗ trợ chặn nội dung dạng ngắ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';
// Channel blocker persistence
const STORAGE_KEY = 'detube_blocked_channels_store';
let blocked = new Set();
let lastRenderer = null;
// Video blocker persistence
const VIDEOS_STORAGE_KEY = 'detube_blocked_videos_store_v1';
let blockedVideos = {};
// Shorts blocker persistence
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 map
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); }
}
async function loadBlockedVideos() {
// Load blocked videos map
try {
const raw = await GM_getValue(VIDEOS_STORAGE_KEY, '{}');
blockedVideos = JSON.parse(raw) || {};
if (typeof blockedVideos !== 'object' || Array.isArray(blockedVideos)) blockedVideos = {};
log('Loaded blocked videos:', Object.keys(blockedVideos));
} catch (e) {
blockedVideos = {};
log('Load-error videos', e);
}
}
// Block shorts if user toggle on
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 observers
if (shortsUrlObserver) { try { shortsUrlObserver.disconnect(); } catch(_){} shortsUrlObserver = null; }
if (shortsDomObserver) { try { shortsDomObserver.disconnect(); } catch(_){} shortsDomObserver = null; }
shortsEnabled = !!enable;
if (!shortsEnabled) return;
redirectIfShortsURL(window.location.href);
// Observe SPA URL evolution
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 saveBlockedVideos() {
// Persist blocked videos
await GM_setValue(VIDEOS_STORAGE_KEY, JSON.stringify(blockedVideos));
log('Saved blocked videos:', Object.keys(blockedVideos));
}
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 and channels
const selectorsToTry = [
// Generic
'#channel-name a',
'ytd-channel-name a',
'a[href*="/@"]',
'a[href*="/channel/"]',
'a[href*="/c/"]',
'a[href*="/user/"]',
// Sidebars
'.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;
}
// Literally "best-effort" video id and title extraction
function getVideoInfo(el) {
let id = '';
let title = '';
// Try common anchor patterns to get id
const a = el.querySelector('a[href*="/watch?v="]');
if (a) {
try {
const href = a.getAttribute('href') || '';
// Use relative /watch?v=... or absolute URL
const url = href.startsWith('http') ? new URL(href) : new URL(href, 'https://www.youtube.com');
id = url.searchParams.get('v') || '';
} catch (_) {}
}
if (!id) {
const lockup = el.querySelector('div[class*="content-id-"]');
if (lockup) {
const m = Array.from(lockup.classList).map(c => c.match(/^content-id-([A-Za-z0-9_-]{6,})$/)).find(Boolean);
if (m && m[1]) id = m[1];
}
}
// Title selectors
const titleSelectors = [
'a#video-title',
'h3 .yt-lockup-metadata-view-model-wiz__title span.yt-core-attributed-string',
'.yt-lockup-view-model-wiz__content-image span.yt-core-attributed-string',
'span.yt-core-attributed-string[role="text"]',
'a.yt-lockup-metadata-view-model-wiz__title span.yt-core-attributed-string',
];
for (const ts of titleSelectors) {
const n = el.querySelector(ts);
if (n && n.textContent && n.textContent.trim()) { title = n.textContent.trim(); break; }
}
// write it, cut it, paste it, save it, load it, check it, quick rewrite it
if (id) el.dataset.detubeVidId = id;
if (title) el.dataset.detubeVidTitle = title;
return { id, title };
}
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);
}
// Channel-based removal
const name = item.dataset.detube && item.dataset.detube.trim();
if (name && blocked.has(name)) { item.remove(); return; }
// Video-based removal
const { id } = getVideoInfo(item);
if (id && blockedVideos[id]) { item.remove(); return; }
});
}
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);
// Get channel name from storage
const channel = lastRenderer.dataset.detube;
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 oldVideoButton = menu.querySelector('.detube-block-video-button');
if (oldVideoButton) oldVideoButton.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}"`);
// Inject "Block Video" button just below
const videoInfo = lastRenderer ? getVideoInfo(lastRenderer) : { id: '', title: '' };
if (!videoInfo.id) {
log('[!] Could not determine video id for Block Video');
return;
}
const vBtn = document.createElement('yt-list-item-view-model');
vBtn.className = 'detube-block-video-button';
vBtn.setAttribute('role', 'menuitem');
vBtn.setAttribute('tabindex', '0');
const vLabelDiv = document.createElement('div');
vLabelDiv.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 vTextWrapper = document.createElement('div');
vTextWrapper.className = 'yt-list-item-view-model-wiz__text-wrapper';
const vTitleWrapper = document.createElement('div');
vTitleWrapper.className = 'yt-list-item-view-model-wiz__title-wrapper';
const vSpan = document.createElement('span');
vSpan.className = 'yt-core-attributed-string yt-list-item-view-model-wiz__title';
vSpan.setAttribute('role', 'text');
vSpan.textContent = ` 🚧 Block Video`;
vTitleWrapper.appendChild(vSpan);
vTextWrapper.appendChild(vTitleWrapper);
vLabelDiv.appendChild(vTextWrapper);
vBtn.appendChild(vLabelDiv);
vBtn.addEventListener('click', () => {
const id = videoInfo.id;
const title = videoInfo.title || id;
blockedVideos[id] = title;
saveBlockedVideos();
removeBlockedVideos();
log(`[>] Blocked video: ${title} (${id})`);
});
menu.appendChild(vBtn);
log(`[+] Injected block video button for id "${videoInfo.id}"`);
}
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 videosArray = Object.entries(blockedVideos)
.map(([id, title]) => ({ id, title: String(title || id) }))
.sort((a, b) => a.title.localeCompare(b.title));
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('');
const videoItems = videosArray.map(v => `
<div class="channel-item" data-video-id="${v.id.replace(/"/g, '"')}">
<span class="channel-name">${v.title.replace(/</g, '<').replace(/>/g, '>')}</span>
<button class="unblock-btn" onclick="unblockVideo('${v.id.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;
}
h2 {
margin-bottom: 10px;
}
@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: 40px 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 Blocker</h1>
</div>
<div class="controls">
<button class="btn" onclick="refreshPage()">Refresh</button>
<button class="btn danger" onclick="clearAll()" ${(blockedArray.length + videosArray.length) === 0 ? 'disabled' : ''}>
Clear All (${blockedArray.length + videosArray.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>
<button class="btn" onclick="exportData()">Export</button>
<button class="btn" onclick="triggerImport()">Import</button>
<input id="import-file" type="file" accept="application/json" style="display:none" />
</div>
<div class="channels-list">
<h2 style="padding: 0 20px;">Blocked Channels (${blockedArray.length})</h2>
${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>Use the three-dot menu on a video to select "Block Channel"</p>
</div>
` : channelItems}
<hr style="margin: 10px 0; border: none; border-top: 1px solid rgba(0,0,0,0.1);" />
<h2 style="padding: 0 20px;">Blocked Videos (${videosArray.length})</h2>
${videosArray.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 videos yet!</h3>
<p>Use the three-dot menu on a video to select "Block Video"</p>
</div>
` : videoItems}
</div>
</div>
<script>
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' });
}
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 });
// Refresh only after the animation completed and action was posted
setTimeout(() => { try { refreshPage(); } catch(_) {} }, 150);
};
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 unblockVideo(videoId) {
if (!confirm('Unblock this video?')) return;
const item = document.querySelector('.channel-item[data-video-id="' + videoId.replace(/"/g, '\\"') + '"]');
const finish = () => {
window.name = JSON.stringify({ action: 'unblockVideo', videoId });
setTimeout(() => { try { refreshPage(); } catch(_) {} }, 150);
};
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 clearAll() {
if (!confirm('Are you sure you want to clear all ${blockedArray.length} channels and ${videosArray.length} videos? 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));
const totalAnimMs = 300 + items.length * 25; // match CSS transition + stagger
setTimeout(() => {
// Post action after animations have (mostly) completed
window.name = JSON.stringify({ action: 'clearAll' });
// Then refresh the UI slightly after to allow parent to process
setTimeout(() => { try { refreshPage(); } catch(_) {} }, 150);
}, totalAnimMs);
}
function exportData() {
try {
const payload = {
version: 'detube-export-2',
blockedNames: ${JSON.stringify(blockedArray)},
blockedVideos: ${JSON.stringify(blockedVideos)}
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const stamp = new Date().toISOString().replace(/[:.]/g,'-');
a.download = 'detube-export-' + stamp + '.json';
document.body.appendChild(a);
a.click();
setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 0);
} catch (e) {
try { alert('Export failed: ' + e); } catch(_){ /* no-op */ }
}
}
function triggerImport() {
const input = document.getElementById('import-file');
if (!input) return;
input.value = '';
input.onchange = () => {
const file = input.files && input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const raw = String(reader.result || '').trim();
if (!raw) throw new Error('Empty file');
const data = JSON.parse(raw);
// Accept either legacy array, or object with channels/videos
let names = [];
let videos = {};
if (Array.isArray(data)) {
names = data;
} else if (data && typeof data === 'object') {
if (Array.isArray(data.blockedNames)) names = data.blockedNames;
if (data.blockedVideos && typeof data.blockedVideos === 'object') videos = data.blockedVideos;
}
if (!Array.isArray(names)) throw new Error('Invalid format for channels');
window.name = JSON.stringify({ action: 'importData', data: { blockedNames: names, blockedVideos: videos } });
// Ask parent to rebuild UI
try { refreshPage(); } catch(_) {}
} catch (e) {
try { alert('Import failed: ' + e); } catch(_){}
}
};
reader.readAsText(file);
};
input.click();
}
// 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 === 'unblockVideo' && action.videoId) {
try { delete blockedVideos[action.videoId]; } catch(_) {}
saveBlockedVideos();
removeBlockedVideos();
log(`[>] Unblocked video: ${action.videoId}`);
newTab.window.name = '';
} else if (action.action === 'importData' && action.data) {
try {
const arr = Array.isArray(action.data.blockedNames) ? action.data.blockedNames : [];
let added = 0, duplicates = 0, invalid = 0;
for (const n of arr) {
if (!n || typeof n !== 'string') { invalid++; continue; }
if (blocked.has(n)) { duplicates++; continue; }
blocked.add(n);
added++;
}
// Merge videos
const vids = action.data.blockedVideos && typeof action.data.blockedVideos === 'object' ? action.data.blockedVideos : {};
let vAdded = 0;
for (const [vid, title] of Object.entries(vids)) {
if (!vid || typeof vid !== 'string') continue;
if (!blockedVideos[vid]) { blockedVideos[vid] = String(title || vid); vAdded++; }
}
saveBlocked();
saveBlockedVideos();
applyCSS();
tagEmAll();
removeBlockedVideos();
log(`[>] Import merged: +${added} channels (+${vAdded} videos), dupes ${duplicates}, invalid ${invalid}`);
} catch (e) {
log('Import error:', e);
}
// Ask the manager page to refresh
try { newTab.window.name = JSON.stringify({ action: 'refreshManager' }); } catch(_) {}
} else if (action.action === 'clearAll') {
blocked.clear();
blockedVideos = {};
saveBlocked();
saveBlockedVideos();
applyCSS();
tagEmAll();
removeBlockedVideos();
log('[>] Cleared all blocked channels and videos');
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();
removeBlockedVideos();
}
});
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 loadBlockedVideos();
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.');
})();
})();