// ==UserScript==
// @name YTKit: YouTube Customization Suite
// @namespace https://github.com/SysAdminDoc/YTKit
// @version 1.2.0
// @description Ultimate YouTube customization with ad blocking, VLC streaming, video/channel hiding, playback enhancements, sticky video, and more.
// @author Matthew Parker
// @license MIT
// @match https://*.youtube.com/*
// @match https://*.youtube-nocookie.com/*
// @match https://youtu.be/*
// @exclude https://m.youtube.com/*
// @exclude https://studio.youtube.com/*
// @icon https://github.com/SysAdminDoc/YTKit/blob/main/assets/ytlogo.png?raw=true
// @grant GM_setValue
// @grant GM_getValue
// @grant GM.xmlHttpRequest
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect sponsor.ajay.app
// @connect raw.githubusercontent.com
// @connect cobalt.meowing.de
// @connect meowing.de
// @connect *
// @updateURL https://github.com/SysAdminDoc/YTKit/raw/refs/heads/main/YTKit.user.js
// @downloadURL https://github.com/SysAdminDoc/YTKit/raw/refs/heads/main/YTKit.user.js
// @run-at document-start
// ==/UserScript==
// AD BLOCKER BOOTSTRAP - Split Architecture
// PHASE 1: Proxy engine injected into REAL page context via ');
if (endIdx === -1) return [];
jsonStr = jsonStr.substring(0, endIdx);
const start = jsonStr.indexOf('{');
const end = jsonStr.lastIndexOf('}');
const ytInitialData = JSON.parse(jsonStr.substring(start, end + 1));
const tabs = ytInitialData?.contents?.twoColumnBrowseResultsRenderer?.tabs;
if (!tabs || !tabs[0]) return [];
const sectionList = tabs[0]?.tabRenderer?.content?.sectionListRenderer;
if (!sectionList) return [];
const items = sectionList?.contents?.[0]?.itemSectionRenderer?.contents?.[0]?.shelfRenderer?.content?.expandedShelfContentsRenderer?.items;
if (!items) return [];
return items.map(({ channelRenderer }) => ({
title: channelRenderer?.title?.simpleText,
handle: channelRenderer?.subscriberCountText?.simpleText
})).filter(s => s.title);
} catch (e) {
console.error('[YTKit] Failed to fetch subscriptions:', e);
return [];
}
},
_isSubscribed(channel) {
if (!channel) return true;
if (channel.startsWith('@')) {
return this._subscriptions.some(s => s.handle === channel);
}
return this._subscriptions.some(s => s.title === channel);
},
_validateFeedCard(cardNode) {
if (cardNode.tagName !== 'YTD-ITEM-SECTION-RENDERER') return;
const channelLink = cardNode.querySelector('ytd-shelf-renderer #title-container a[title]');
if (!channelLink) return;
const title = channelLink.getAttribute('title');
const handle = channelLink.getAttribute('href')?.slice(1);
if (!this._isSubscribed(title) && !this._isSubscribed(handle)) {
DebugManager.log('Content', 'Hiding collaboration from:', title);
cardNode.remove();
}
},
async init() {
if (window.location.pathname !== '/feed/subscriptions') return;
if (!this._initialized) {
this._subscriptions = await this._fetchSubscriptions();
this._initialized = true;
DebugManager.log('Content', `Loaded ${this._subscriptions.length} subscriptions`);
}
if (this._subscriptions.length === 0) return;
// Process existing items
document.querySelectorAll('ytd-item-section-renderer').forEach(card => this._validateFeedCard(card));
// Watch for new items
const feedSelector = 'ytd-section-list-renderer > div#contents';
const feed = document.querySelector(feedSelector);
if (feed) {
this._observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList' && m.addedNodes.length > 0) {
m.addedNodes.forEach(node => {
if (node.nodeType === 1) this._validateFeedCard(node);
});
}
}
});
this._observer.observe(feed, { childList: true });
}
// Re-run on navigation
addNavigateRule(this.id, () => {
if (window.location.pathname === '/feed/subscriptions') {
setTimeout(() => {
document.querySelectorAll('ytd-item-section-renderer').forEach(card => this._validateFeedCard(card));
}, 1000);
}
});
},
destroy() {
this._observer?.disconnect();
removeNavigateRule(this.id);
}
},
{
id: 'showVlcButton',
name: 'VLC Player Button',
description: 'Add button to stream video directly in VLC media player',
group: 'Downloads',
icon: 'play-circle',
isParent: true,
_createButton(parent) {
const btn = document.createElement('button');
btn.className = 'ytkit-vlc-btn';
btn.title = 'Stream in VLC Player (requires YTYT-Downloader)';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '20');
svg.setAttribute('height', '20');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z');
path.setAttribute('fill', 'white');
svg.appendChild(path);
btn.appendChild(svg);
btn.appendChild(document.createTextNode(' VLC'));
btn.style.cssText = `display:inline-flex;align-items:center;gap:6px;padding:0 16px;height:36px;margin-left:8px;border-radius:18px;border:none;background:#f97316;color:white;font-family:"Roboto","Arial",sans-serif;font-size:14px;font-weight:500;cursor:pointer;transition:background 0.2s;`;
btn.onmouseenter = () => { btn.style.background = '#ea580c'; };
btn.onmouseleave = () => { btn.style.background = '#f97316'; };
btn.addEventListener('click', () => {
showToast('🎬 Sending to VLC...', '#f97316');
window.location.href = 'ytvlc://' + encodeURIComponent(window.location.href);
});
parent.appendChild(btn);
},
init() {
registerPersistentButton('vlcButton', '#top-level-buttons-computed', '.ytkit-vlc-btn', this._createButton.bind(this), 'VLC');
startButtonChecker();
},
destroy() {
unregisterPersistentButton('vlcButton');
document.querySelector('.ytkit-vlc-btn')?.remove();
}
},
{
id: 'showLocalDownloadButton',
name: 'Local Download Button',
description: 'Add button to download video locally via yt-dlp',
group: 'Downloads',
icon: 'hard-drive-download',
_createButton(parent) {
const btn = document.createElement('button');
btn.className = 'ytkit-local-dl-btn';
btn.title = 'Download to PC (requires YTYT-Downloader)';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '20');
svg.setAttribute('height', '20');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z');
path.setAttribute('fill', 'white');
svg.appendChild(path);
btn.appendChild(svg);
btn.appendChild(document.createTextNode(' DL'));
btn.style.cssText = `display:inline-flex;align-items:center;gap:6px;padding:0 16px;height:36px;margin-left:8px;border-radius:18px;border:none;background:#22c55e;color:white;font-family:"Roboto","Arial",sans-serif;font-size:14px;font-weight:500;cursor:pointer;transition:background 0.2s;`;
btn.onmouseenter = () => { btn.style.background = '#16a34a'; };
btn.onmouseleave = () => { btn.style.background = '#22c55e'; };
btn.addEventListener('click', () => {
showToast('⬇️ Starting download...', '#22c55e');
window.location.href = 'ytdl://' + encodeURIComponent(window.location.href);
});
parent.appendChild(btn);
},
init() {
registerPersistentButton('localDownloadButton', '#top-level-buttons-computed', '.ytkit-local-dl-btn', this._createButton.bind(this), 'Download');
startButtonChecker();
},
destroy() {
unregisterPersistentButton('localDownloadButton');
document.querySelector('.ytkit-local-dl-btn')?.remove();
}
},
{
id: 'showMp3DownloadButton',
name: 'MP3 Download Button',
description: 'Add button to download audio as MP3 via yt-dlp',
group: 'Downloads',
icon: 'music',
_createButton(parent) {
const btn = document.createElement('button');
btn.className = 'ytkit-mp3-dl-btn';
btn.title = 'Download MP3 (requires YTYT-Downloader)';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '20');
svg.setAttribute('height', '20');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z');
path.setAttribute('fill', 'white');
svg.appendChild(path);
btn.appendChild(svg);
btn.appendChild(document.createTextNode(' MP3'));
btn.style.cssText = `display:inline-flex;align-items:center;gap:6px;padding:0 16px;height:36px;margin-left:8px;border-radius:18px;border:none;background:#8b5cf6;color:white;font-family:"Roboto","Arial",sans-serif;font-size:14px;font-weight:500;cursor:pointer;transition:background 0.2s;`;
btn.onmouseenter = () => { btn.style.background = '#7c3aed'; };
btn.onmouseleave = () => { btn.style.background = '#8b5cf6'; };
btn.addEventListener('click', () => {
showToast('🎵 Starting MP3 download...', '#8b5cf6');
window.location.href = 'ytdl://' + encodeURIComponent(window.location.href) + '?ytyt_audio_only=1';
});
parent.appendChild(btn);
},
init() {
registerPersistentButton('mp3DownloadButton', '#top-level-buttons-computed', '.ytkit-mp3-dl-btn', this._createButton.bind(this), 'MP3');
startButtonChecker();
},
destroy() {
unregisterPersistentButton('mp3DownloadButton');
document.querySelector('.ytkit-mp3-dl-btn')?.remove();
}
},
{
id: 'videoContextMenu',
name: 'Video Context Menu',
description: 'Right-click on video player for quick download options (video, audio, transcript)',
group: 'Downloads',
icon: 'menu',
_menu: null,
_styleElement: null,
_contextHandler: null,
_clickHandler: null,
_serverPort: 9547,
_injectStyles() {
if (this._styleElement) return;
this._styleElement = document.createElement('style');
this._styleElement.id = 'ytkit-context-menu-styles';
this._styleElement.textContent = `.ytkit-context-menu{position:fixed;z-index:999999;background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:6px 0;min-width:220px;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:"Roboto",Arial,sans-serif;font-size:14px;animation:ytkit-menu-fade 0.15s ease-out;} @keyframes ytkit-menu-fade{from{opacity:0;transform:scale(0.95);} to{opacity:1;transform:scale(1);} } .ytkit-context-menu-header{padding:8px 14px;color:#888;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;border-bottom:1px solid #333;margin-bottom:4px;} .ytkit-context-menu-item{display:flex;align-items:center;gap:12px;padding:10px 14px;color:#e0e0e0;cursor:pointer;transition:background 0.1s;} .ytkit-context-menu-item:hover{background:#2d2d44;} .ytkit-context-menu-item svg{width:18px;height:18px;flex-shrink:0;} .ytkit-context-menu-item.ytkit-item-video svg{color:#22c55e;} .ytkit-context-menu-item.ytkit-item-audio svg{color:#8b5cf6;} .ytkit-context-menu-item.ytkit-item-transcript svg{color:#3b82f6;} .ytkit-context-menu-item.ytkit-item-vlc svg{color:#f97316;} .ytkit-context-menu-item.ytkit-item-mpv svg{color:#ec4899;} .ytkit-context-menu-item.ytkit-item-embed svg{color:#06b6d4;} .ytkit-context-menu-item.ytkit-item-copy svg{color:#fbbf24;} .ytkit-context-menu-divider{height:1px;background:#333;margin:6px 0;} .ytkit-context-menu-item .ytkit-shortcut{margin-left:auto;color:#666;font-size:12px;}`;
document.head.appendChild(this._styleElement);
},
_createMenu() {
const menu = document.createElement('div');
menu.className = 'ytkit-context-menu';
menu.style.display = 'none';
const header = document.createElement('div');
header.className = 'ytkit-context-menu-header';
header.textContent = 'YTKit Downloads';
menu.appendChild(header);
const items = [
{ id: 'download-video', icon: 'download', label: 'Download Video (MP4)', class: 'ytkit-item-video', action: () => this._downloadVideo() },
{ id: 'download-audio', icon: 'music', label: 'Download Audio (MP3)', class: 'ytkit-item-audio', action: () => this._downloadAudio() },
{ id: 'download-transcript', icon: 'file-text', label: 'Download Transcript', class: 'ytkit-item-transcript', action: () => this._downloadTranscript() },
{ divider: true },
{ id: 'stream-vlc', icon: 'play-circle', label: 'Stream in VLC', class: 'ytkit-item-vlc', action: () => this._streamVLC() },
{ id: 'queue-vlc', icon: 'list-plus', label: 'Add to VLC Queue', class: 'ytkit-item-vlc-queue', action: () => this._addToVLCQueue() },
{ id: 'stream-mpv', icon: 'monitor', label: 'Stream in MPV', class: 'ytkit-item-mpv', action: () => this._streamMPV() },
{ id: 'embed-player', icon: 'tv', label: 'Use Embed Player', class: 'ytkit-item-embed', action: () => this._activateEmbed() },
{ divider: true },
{ id: 'copy-url', icon: 'link', label: 'Copy Video URL', class: 'ytkit-item-copy', action: () => this._copyURL() },
{ id: 'copy-id', icon: 'hash', label: 'Copy Video ID', class: 'ytkit-item-copy', action: () => this._copyID() },
];
items.forEach(item => {
if (item.divider) {
const divider = document.createElement('div');
divider.className = 'ytkit-context-menu-divider';
menu.appendChild(divider);
return;
}
const el = document.createElement('div');
el.className = `ytkit-context-menu-item ${item.class}`;
el.dataset.action = item.id;
// Icon SVG
const iconSvg = this._getIcon(item.icon);
el.appendChild(iconSvg);
// Label
const label = document.createElement('span');
label.textContent = item.label;
el.appendChild(label);
el.addEventListener('click', (e) => {
e.stopPropagation();
this._hideMenu();
item.action();
});
menu.appendChild(el);
});
document.body.appendChild(menu);
return menu;
},
_getIcon(name) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
// Build icons using DOM methods (Trusted Types compliant)
const ns = 'http://www.w3.org/2000/svg';
const createPath = (d) => {
const p = document.createElementNS(ns, 'path');
p.setAttribute('d', d);
return p;
};
const createLine = (x1, y1, x2, y2) => {
const l = document.createElementNS(ns, 'line');
l.setAttribute('x1', x1);
l.setAttribute('y1', y1);
l.setAttribute('x2', x2);
l.setAttribute('y2', y2);
return l;
};
const createCircle = (cx, cy, r) => {
const c = document.createElementNS(ns, 'circle');
c.setAttribute('cx', cx);
c.setAttribute('cy', cy);
c.setAttribute('r', r);
return c;
};
const createRect = (x, y, w, h, rx, ry) => {
const r = document.createElementNS(ns, 'rect');
r.setAttribute('x', x);
r.setAttribute('y', y);
r.setAttribute('width', w);
r.setAttribute('height', h);
if (rx) r.setAttribute('rx', rx);
if (ry) r.setAttribute('ry', ry);
return r;
};
const createPolyline = (points) => {
const p = document.createElementNS(ns, 'polyline');
p.setAttribute('points', points);
return p;
};
const createPolygon = (points) => {
const p = document.createElementNS(ns, 'polygon');
p.setAttribute('points', points);
return p;
};
switch (name) {
case 'download':
svg.appendChild(createPath('M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'));
svg.appendChild(createPolyline('7 10 12 15 17 10'));
svg.appendChild(createLine('12', '15', '12', '3'));
break;
case 'music':
svg.appendChild(createPath('M9 18V5l12-2v13'));
svg.appendChild(createCircle('6', '18', '3'));
svg.appendChild(createCircle('18', '16', '3'));
break;
case 'file-text':
svg.appendChild(createPath('M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'));
svg.appendChild(createPolyline('14 2 14 8 20 8'));
svg.appendChild(createLine('16', '13', '8', '13'));
svg.appendChild(createLine('16', '17', '8', '17'));
break;
case 'play-circle':
svg.appendChild(createCircle('12', '12', '10'));
svg.appendChild(createPolygon('10 8 16 12 10 16'));
break;
case 'monitor':
svg.appendChild(createRect('2', '3', '20', '14', '2', '2'));
svg.appendChild(createLine('8', '21', '16', '21'));
svg.appendChild(createLine('12', '17', '12', '21'));
break;
case 'tv':
svg.appendChild(createRect('2', '7', '20', '15', '2', '2'));
svg.appendChild(createPolyline('17 2 12 7 7 2'));
break;
case 'link':
svg.appendChild(createPath('M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'));
svg.appendChild(createPath('M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'));
break;
case 'hash':
svg.appendChild(createLine('4', '9', '20', '9'));
svg.appendChild(createLine('4', '15', '20', '15'));
svg.appendChild(createLine('10', '3', '8', '21'));
svg.appendChild(createLine('16', '3', '14', '21'));
break;
case 'list-plus':
svg.appendChild(createLine('8', '6', '21', '6'));
svg.appendChild(createLine('8', '12', '21', '12'));
svg.appendChild(createLine('8', '18', '21', '18'));
svg.appendChild(createLine('3', '6', '3.01', '6'));
svg.appendChild(createLine('3', '12', '3.01', '12'));
svg.appendChild(createLine('3', '18', '3.01', '18'));
// Plus sign
svg.appendChild(createLine('16', '5', '16', '7'));
svg.appendChild(createLine('15', '6', '17', '6'));
break;
}
return svg;
},
_showMenu(x, y) {
if (!this._menu) {
this._menu = this._createMenu();
}
// Position menu
this._menu.style.display = 'block';
// Adjust position if menu would go off screen
const rect = this._menu.getBoundingClientRect();
const maxX = window.innerWidth - rect.width - 10;
const maxY = window.innerHeight - rect.height - 10;
this._menu.style.left = Math.min(x, maxX) + 'px';
this._menu.style.top = Math.min(y, maxY) + 'px';
},
_hideMenu() {
if (this._menu) {
this._menu.style.display = 'none';
}
},
// Action handlers
_downloadVideo() {
const url = window.location.href;
showToast('⬇️ Starting video download...', '#22c55e');
window.location.href = 'ytdl://' + encodeURIComponent(url);
},
_downloadAudio() {
const url = window.location.href;
showToast('🎵 Starting audio download...', '#a855f7');
// Use ytdl with audio-only flag (assuming handler supports it)
window.location.href = 'ytdl://' + encodeURIComponent(url + '&ytkit_audio_only=1');
},
async _downloadTranscript() {
await TranscriptService.downloadTranscript();
},
_streamVLC() {
const url = window.location.href;
showToast('Sending to VLC...', '#f97316');
window.location.href = 'ytvlc://' + encodeURIComponent(url);
},
_streamMPV() {
const url = window.location.href;
showToast('🎬 Sending to MPV...', '#8b5cf6');
window.location.href = 'ytmpv://' + encodeURIComponent(url);
},
_addToVLCQueue() {
const url = window.location.href;
showToast('📋 Adding to VLC queue...', '#f97316');
window.location.href = 'ytvlcq://' + encodeURIComponent(url);
},
async _activateEmbed() {
if (embedFeature && typeof embedFeature.activateEmbed === 'function') {
embedFeature._injectStyles();
await embedFeature.activateEmbed(true);
}
},
_copyURL() {
navigator.clipboard.writeText(window.location.href).then(() => {
this._showToast('URL copied to clipboard');
});
},
_copyID() {
const videoId = getVideoId();
if (videoId) {
navigator.clipboard.writeText(videoId).then(() => {
this._showToast('Video ID copied: ' + videoId);
});
}
},
_showToast(message) {
showToast(message, '#22c55e');
},
init() {
this._injectStyles();
// Context menu handler - use capturing to intercept before YouTube
this._contextHandler = (e) => {
// Check if right-click is on video player area
const moviePlayer = document.querySelector('#movie_player');
if (!moviePlayer) return;
// Check if click target is within movie player
if (moviePlayer.contains(e.target) || e.target === moviePlayer) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
this._showMenu(e.clientX, e.clientY);
return false;
}
};
// Click handler to hide menu
this._clickHandler = (e) => {
if (this._menu && !this._menu.contains(e.target)) {
this._hideMenu();
}
};
// Use capturing phase to get the event before YouTube does
document.addEventListener('contextmenu', this._contextHandler, true);
document.addEventListener('click', this._clickHandler);
this._scrollHandler = () => this._hideMenu();
document.addEventListener('scroll', this._scrollHandler, { passive: true });
// Also add directly to movie_player when it appears
this._attachToPlayer = () => {
const moviePlayer = document.querySelector('#movie_player');
if (moviePlayer && !moviePlayer._ytkitContextMenu) {
moviePlayer._ytkitContextMenu = true;
moviePlayer.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
this._showMenu(e.clientX, e.clientY);
return false;
}, true);
}
};
// Try to attach now and on navigation
this._attachToPlayer();
addNavigateRule('contextMenuAttach', this._attachToPlayer);
},
destroy() {
if (this._contextHandler) {
document.removeEventListener('contextmenu', this._contextHandler, true);
}
if (this._clickHandler) {
document.removeEventListener('click', this._clickHandler);
}
if (this._scrollHandler) {
document.removeEventListener('scroll', this._scrollHandler);
}
removeNavigateRule('contextMenuAttach');
this._menu?.remove();
this._menu = null;
this._styleElement?.remove();
this._styleElement = null;
}
},
// ─── Auto-Resume Last Position ───
{
id: 'autoResumePosition',
name: 'Auto-Resume Position',
description: 'Resume videos from where you left off (saves position for partially watched videos)',
group: 'Video Player',
icon: 'play',
_ruleId: 'autoResumeRule',
_saveInterval: null,
_storageKey: 'ytkit_resume_positions',
_getPositions() {
try { return JSON.parse(GM_getValue(this._storageKey, '{}')); } catch { return {}; }
},
_setPositions(p) { GM_setValue(this._storageKey, JSON.stringify(p)); },
init() {
const self = this;
const threshold = appState.settings.autoResumeThreshold || 15;
const tryResume = () => {
if (!window.location.pathname.startsWith('/watch')) return;
const videoId = getVideoId();
if (!videoId) return;
const positions = self._getPositions();
const saved = positions[videoId];
if (!saved || saved < threshold) return;
const video = document.querySelector('video.html5-main-video');
if (!video || video.currentTime > threshold) return;
if (!isFinite(video.duration)) return; // Skip live streams
video.currentTime = saved;
showToast(`Resumed from ${Math.floor(saved / 60)}:${String(Math.floor(saved % 60)).padStart(2, '0')}`, '#3b82f6', { duration: 2 });
// Remove position after resuming
delete positions[videoId];
self._setPositions(positions);
};
addNavigateRule(this._ruleId, () => waitForPageContent(tryResume));
// Save position every 10 seconds
this._saveInterval = setInterval(() => {
if (!window.location.pathname.startsWith('/watch')) return;
const video = document.querySelector('video.html5-main-video');
if (!video || video.paused || video.duration < 60 || !isFinite(video.duration)) return;
const videoId = getVideoId();
if (!videoId) return;
// Don't save if near start or near end (within 10%)
if (video.currentTime < threshold || video.currentTime > video.duration * 0.9) return;
const positions = self._getPositions();
positions[videoId] = Math.floor(video.currentTime);
// Keep only last 200 entries
const keys = Object.keys(positions);
if (keys.length > 200) { keys.slice(0, keys.length - 200).forEach(k => delete positions[k]); }
self._setPositions(positions);
}, 10000);
},
destroy() {
removeNavigateRule(this._ruleId);
if (this._saveInterval) clearInterval(this._saveInterval);
}
},
{
id: 'autoResumeThreshold',
name: 'Resume Threshold',
description: 'Seconds into a video before saving resume position',
group: 'Video Player',
icon: 'clock',
isSubFeature: true,
parentId: 'autoResumePosition',
type: 'range',
settingKey: 'autoResumeThreshold',
min: 5,
max: 120,
step: 5,
formatValue: (v) => `${v}s`,
init() {},
destroy() {}
},
// ─── GPU Context Recovery (monitor switch fix) ───
{
id: 'gpuContextRecovery',
name: 'Monitor Switch Fix',
description: 'Automatically recovers video when moving browser between monitors (fixes black screen with audio)',
group: 'Video Player',
icon: 'monitor',
_healthPoll: null,
_recovering: false,
_canvas: null,
_ctx: null,
_lastTime: 0,
_blackCount: 0,
_stage: 0,
_isBlackFrame(video) {
if (!this._canvas) {
this._canvas = document.createElement('canvas');
this._canvas.width = 16;
this._canvas.height = 16;
this._ctx = this._canvas.getContext('2d', { willReadFrequently: true });
}
try {
this._ctx.drawImage(video, 0, 0, 16, 16);
const data = this._ctx.getImageData(0, 0, 16, 16).data;
let totalBrightness = 0;
for (let i = 0; i < data.length; i += 4) {
totalBrightness += data[i] + data[i + 1] + data[i + 2];
}
return (totalBrightness / (16 * 16)) < 15;
} catch (e) {
return false;
}
},
_recover() {
if (this._recovering) return;
this._recovering = true;
this._stage++;
const video = document.querySelector('video.html5-main-video');
const player = document.querySelector('#movie_player');
if (!video || !player) { this._recovering = false; return; }
const currentTime = video.currentTime;
DebugManager.log('GPU', `Black frame detected — recovery stage ${this._stage}`);
if (this._stage <= 1) {
// Stage 1: Pause → seekTo (forces keyframe decode) → play
try {
player.pauseVideo();
setTimeout(() => {
player.seekTo(currentTime, true);
setTimeout(() => {
player.playVideo();
this._recovering = false;
}, 200);
}, 100);
} catch(e) { this._recovering = false; }
} else if (this._stage <= 2) {
// Stage 2: Remove video src, force reload via loadVideoById
try {
const videoId = new URLSearchParams(window.location.search).get('v');
if (videoId && typeof player.loadVideoById === 'function') {
player.loadVideoById({ videoId, startSeconds: currentTime });
// loadVideoById auto-plays, give it time to reinit
setTimeout(() => {
this._recovering = false;
}, 1000);
} else {
// Fallback: nuke the video element's rendering
video.srcObject = video.srcObject;
this._recovering = false;
}
} catch(e) { this._recovering = false; }
} else if (this._stage <= 3) {
// Stage 3: Toggle hardware acceleration by forcing software rendering path
try {
// Remove will-change and transform hints to force software fallback
video.style.willChange = 'auto';
video.style.transform = 'none';
video.style.backfaceVisibility = 'hidden';
// Force layout
void video.offsetHeight;
// Re-seek to force new frame
player.seekTo(currentTime + 0.1, true);
setTimeout(() => {
// Restore
video.style.willChange = '';
video.style.transform = '';
video.style.backfaceVisibility = '';
this._recovering = false;
}, 500);
} catch(e) { this._recovering = false; }
} else {
// Stage 4: Full page-level reload as absolute last resort
DebugManager.log('GPU', 'All recovery stages failed — reloading player');
try {
// Use YouTube's navigation to "reload" without full page refresh
const url = window.location.href;
if (typeof player.loadVideoByUrl === 'function') {
player.loadVideoByUrl({ mediaContentUrl: `https://www.youtube.com/v/${new URLSearchParams(window.location.search).get('v')}`, startSeconds: currentTime });
} else {
window.location.replace(url);
}
} catch(e) {}
this._recovering = false;
this._stage = 0;
this._blackCount = 0;
}
},
init() {
this._healthPoll = setInterval(() => {
if (this._recovering) return;
const video = document.querySelector('video.html5-main-video');
if (!video || video.paused || video.ended || video.readyState < 3) {
this._blackCount = 0;
return;
}
const ct = video.currentTime;
if (ct === this._lastTime) return;
this._lastTime = ct;
if (this._isBlackFrame(video)) {
this._blackCount++;
if (this._blackCount >= 2) {
this._recover();
}
} else {
// Video is rendering — reset stages
this._blackCount = 0;
this._stage = 0;
}
}, 1500);
},
destroy() {
clearInterval(this._healthPoll);
this._healthPoll = null;
this._canvas = null;
this._ctx = null;
this._blackCount = 0;
this._recovering = false;
this._stage = 0;
}
},
// ALCHEMY-INSPIRED FEATURES
{
id: 'quickLinkMenu',
name: 'Logo Quick Links',
description: 'Hover over the YouTube logo to reveal a customizable dropdown menu',
group: 'Interface',
icon: 'menu',
_wrapper: null,
_styleEl: null,
_iconMap: {
'/feed/history': 'M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z',
'/playlist?list=WL': 'M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm4.2 14.2L11 13V7h1.5v5.2l4.5 2.7-.8 1.3z',
'/feed/library': 'M22,7H2v1h20V7z M13,12H2v-1h11V12z M13,16H2v-1h11V16z M15,19v-8l7,4L15,19z',
'/playlist?list=LL': 'M18.77,11h-4.23l1.52-4.94C16.38,5.03,15.54,4,14.38,4c-0.58,0-1.13,0.24-1.53,0.65L7,11H3v10h4h1h9.43 c1.06,0,1.98-0.67,2.26-1.68l1.29-4.73C21.35,13.41,20.41,11,18.77,11z M7,20H4v-8h3V20z M19.98,14.37l-1.29,4.73 C18.59,19.64,18.02,20,17.43,20H8v-8.61l5.83-5.97c0.15-0.15,0.35-0.23,0.55-0.23c0.41,0,0.72,0.37,0.6,0.77L13.46,11h1.08h4.23 c0.54,0,0.85,0.79,0.65,1.29L19.98,14.37z',
'/feed/subscriptions': 'M10 18v-6l5 3-5 3zm7-15H7v1h10V3zm3 3H4v1h16V6zm2 3H2v12h20V9zM3 20V10h18v10H3z',
'/': 'M12 2L3.5 9.25V22h6.25V15.5h4.5V22h6.25V9.25L12 2zm0 2.5l6.5 5.5V20h-2.25v-6.5h-8.5V20H5.5V10L12 4.5z',
'/feed/trending': 'M17.53 11.2c-.23-.3-.5-.56-.76-.82-.65-.6-1.4-1.03-2.03-1.66C13.3 7.26 13 5.64 13.41 4c-1.59.5-2.8 1.5-3.7 2.82-2.06 3.05-1.53 7.03 1.21 9.43.17.15.31.34.36.56.07.29-.03.58-.27.8-.24.22-.56.34-.88.27-.29-.06-.54-.27-.68-.53-.85-1.32-.95-2.88-.46-4.35a7.932 7.932 0 00-1.59 4.27c-.07.81.07 1.62.33 2.39.3.95.81 1.81 1.49 2.54 1.48 1.52 3.58 2.36 5.71 2.28 2.27-.09 4.33-1.25 5.53-3.09 1.33-2.04 1.6-4.77.37-6.92z',
'/feed/channels': 'M4 20h14v1H3V6h1v14zM6 3v14h15V3H6zm13 2v10H8V5h11z',
'_default': 'M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19V6.413L11.2071 14.2071L9.79289 12.7929L17.585 5H13V3H21Z',
},
_parseItems() {
const raw = appState.settings.quickLinkItems || '';
return raw.split('\n').map(line => {
const sep = line.indexOf('|');
if (sep === -1) return null;
const text = line.substring(0, sep).trim();
const url = line.substring(sep + 1).trim();
if (!text || !url) return null;
const icon = this._iconMap[url] || this._iconMap['_default'];
return { text, url, icon };
}).filter(Boolean);
},
_buildMenu(parentEl, dropId) {
const existing = parentEl.querySelector('#' + dropId);
if (existing) existing.remove();
const menu = document.createElement('div');
menu.id = dropId;
menu.className = 'ytkit-ql-drop';
this._parseItems().forEach(item => {
const a = document.createElement('a'); a.href = item.url; a.className = 'ytkit-ql-item';
TrustedHTML.setHTML(a, `${item.text}`);
menu.appendChild(a);
});
// Settings link — compact
const divider = document.createElement('div');
divider.style.cssText = 'height:1px;background:rgba(255,255,255,0.06);margin:3px 0;';
menu.appendChild(divider);
const gear = document.createElement('a');
gear.href = '#';
gear.className = 'ytkit-ql-item ytkit-ql-settings';
gear.onclick = (e) => { e.preventDefault(); document.body.classList.toggle('ytkit-panel-open'); };
TrustedHTML.setHTML(gear, `Settings`);
menu.appendChild(gear);
parentEl.appendChild(menu);
// JS hover with delayed hide
let hideTimer = null;
const show = () => { clearTimeout(hideTimer); menu.classList.add('ytkit-ql-visible'); };
const scheduleHide = () => { hideTimer = setTimeout(() => menu.classList.remove('ytkit-ql-visible'), 1500); };
parentEl.addEventListener('mouseenter', show);
parentEl.addEventListener('mouseleave', scheduleHide);
menu.addEventListener('mouseenter', show);
menu.addEventListener('mouseleave', scheduleHide);
return menu;
},
rebuildMenus() {
if (this._wrapper) this._buildMenu(this._wrapper, 'ytkit-ql-menu');
// Also rebuild watch page dropdown if present
const poLogoWrap = document.getElementById('ytkit-po-logo-wrap');
if (poLogoWrap) this._buildMenu(poLogoWrap, 'ytkit-po-drop');
},
init() {
const self = this;
self._styleEl = GM_addStyle(`#ytkit-ql-wrap{position:relative;display:inline-block} .ytkit-ql-drop{position:absolute;flex-direction:column;background:rgba(22,22,22,0.96);border:1px solid rgba(255,255,255,0.1);border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.7);padding:4px 0;z-index:9999;min-width:180px;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);opacity:0;visibility:hidden;pointer-events:none;transform:translateY(4px);transition:opacity 0.25s ease,visibility 0.25s ease,transform 0.25s ease;display:flex} .ytkit-ql-drop.ytkit-ql-visible{opacity:1;visibility:visible;pointer-events:auto;transform:translateY(0)} #ytkit-ql-menu{top:38px;left:0} #ytkit-po-drop{bottom:calc(100% + 6px);right:0} .ytkit-ql-item{display:flex;align-items:center;padding:7px 14px;color:#fff;text-decoration:none;font-size:13px;font-family:"Roboto","Arial",sans-serif;transition:background .15s;gap:10px} .ytkit-ql-item:hover{background:rgba(255,255,255,.08)} .ytkit-ql-icon{fill:#fff;width:18px;height:18px;flex-shrink:0} .ytkit-ql-settings{padding:5px 14px;opacity:0.4;font-size:11px} .ytkit-ql-settings .ytkit-ql-icon{width:14px;height:14px} .ytkit-ql-settings:hover{opacity:0.8}`);
waitForElement('ytd-topbar-logo-renderer', (logo) => {
if (document.getElementById('ytkit-ql-wrap')) return;
const wrapper = document.createElement('div'); wrapper.id = 'ytkit-ql-wrap';
logo.parentNode.insertBefore(wrapper, logo);
wrapper.appendChild(logo);
self._buildMenu(wrapper, 'ytkit-ql-menu');
self._wrapper = wrapper;
});
},
destroy() {
if (this._wrapper) {
const logo = this._wrapper.querySelector('ytd-topbar-logo-renderer');
if (logo) { this._wrapper.parentNode?.insertBefore(logo, this._wrapper); }
this._wrapper.remove(); this._wrapper = null;
}
this._styleEl?.remove(); this._styleEl = null;
}
},
{
id: 'quickLinkEditor',
name: 'Edit Quick Links',
description: 'Customize the logo dropdown menu. One link per line: Label | URL',
group: 'Interface',
icon: 'menu',
isSubFeature: true,
parentId: 'quickLinkMenu',
type: 'textarea',
placeholder: 'History | /feed/history\nWatch Later | /playlist?list=WL',
settingKey: 'quickLinkItems',
init() {
// Listen for setting changes to rebuild menus
document.addEventListener('ytkit-settings-changed', (e) => {
if (e.detail?.key === 'quickLinkItems') {
const ql = features.find(f => f.id === 'quickLinkMenu');
if (ql && ql.rebuildMenus) ql.rebuildMenus();
}
});
},
destroy() {}
},
];
function injectStyle(selector, featureId, isRawCss = false) {
const id = `yt-suite-style-${featureId}`;
document.getElementById(id)?.remove();
const style = document.createElement('style');
style.id = id;
style.textContent = isRawCss ? selector : `${selector} { display: none !important; }`;
document.head.appendChild(style);
return style;
}
// SECTION 3: HELPERS
function applyBotFilter() {
if (!window.location.pathname.startsWith('/watch')) return;
const messages = document.querySelectorAll('yt-live-chat-text-message-renderer:not([data-ytkit-bot-checked])');
messages.forEach(msg => {
msg.dataset.ytkitBotChecked = '1';
const authorName = msg.querySelector('#author-name')?.textContent.toLowerCase() || '';
if (authorName.includes('bot')) {
msg.style.display = 'none';
msg.classList.add('yt-suite-hidden-bot');
}
});
}
let _lastKeywordHash = '';
function applyKeywordFilter() {
if (!window.location.pathname.startsWith('/watch')) return;
const keywordsRaw = appState.settings.chatKeywordFilter;
const currentHash = keywordsRaw || '';
// If keywords changed, recheck all messages
if (currentHash !== _lastKeywordHash) {
_lastKeywordHash = currentHash;
document.querySelectorAll('yt-live-chat-text-message-renderer[data-ytkit-kw-checked]').forEach(el => {
delete el.dataset.ytkitKwChecked;
});
}
const messages = document.querySelectorAll('yt-live-chat-text-message-renderer:not([data-ytkit-kw-checked])');
if (!keywordsRaw || !keywordsRaw.trim()) {
messages.forEach(el => {
el.dataset.ytkitKwChecked = '1';
if (el.classList.contains('yt-suite-hidden-keyword')) {
el.style.display = '';
el.classList.remove('yt-suite-hidden-keyword');
}
});
return;
}
const keywords = keywordsRaw.toLowerCase().split(',').map(k => k.trim()).filter(Boolean);
messages.forEach(msg => {
msg.dataset.ytkitKwChecked = '1';
const messageText = msg.querySelector('#message')?.textContent.toLowerCase() || '';
const authorText = msg.querySelector('#author-name')?.textContent.toLowerCase() || '';
const shouldHide = keywords.some(k => messageText.includes(k) || authorText.includes(k));
if (shouldHide) {
msg.style.display = 'none';
msg.classList.add('yt-suite-hidden-keyword');
}
});
}
// SECTION 4: PREMIUM UI (Trusted Types Safe)
// SVG Icon Factory - Creates icons using DOM methods (Trusted Types safe)
function createSVG(viewBox, paths, options = {}) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', viewBox);
if (options.fill) svg.setAttribute('fill', options.fill);
else svg.setAttribute('fill', 'none');
if (options.stroke !== false) svg.setAttribute('stroke', options.stroke || 'currentColor');
if (options.strokeWidth) svg.setAttribute('stroke-width', options.strokeWidth);
if (options.strokeLinecap) svg.setAttribute('stroke-linecap', options.strokeLinecap);
if (options.strokeLinejoin) svg.setAttribute('stroke-linejoin', options.strokeLinejoin);
paths.forEach(p => {
if (p.type === 'path') {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', p.d);
if (p.fill) path.setAttribute('fill', p.fill);
svg.appendChild(path);
} else if (p.type === 'circle') {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', p.cx);
circle.setAttribute('cy', p.cy);
circle.setAttribute('r', p.r);
if (p.fill) circle.setAttribute('fill', p.fill);
svg.appendChild(circle);
} else if (p.type === 'rect') {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', p.x);
rect.setAttribute('y', p.y);
rect.setAttribute('width', p.width);
rect.setAttribute('height', p.height);
if (p.rx) rect.setAttribute('rx', p.rx);
svg.appendChild(rect);
} else if (p.type === 'line') {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', p.x1);
line.setAttribute('y1', p.y1);
line.setAttribute('x2', p.x2);
line.setAttribute('y2', p.y2);
svg.appendChild(line);
} else if (p.type === 'polyline') {
const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
polyline.setAttribute('points', p.points);
svg.appendChild(polyline);
} else if (p.type === 'polygon') {
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
polygon.setAttribute('points', p.points);
svg.appendChild(polygon);
}
});
return svg;
}
const _S = { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' };
const ICONS = {
settings: () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 3 },
{ type: 'path', d: 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z' }
], _S),
close: () => createSVG('0 0 24 24', [
{ type: 'line', x1: 18, y1: 6, x2: 6, y2: 18 },
{ type: 'line', x1: 6, y1: 6, x2: 18, y2: 18 }
], { strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }),
github: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z' }
], { fill: 'currentColor', stroke: false }),
upload: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' },
{ type: 'polyline', points: '17 8 12 3 7 8' },
{ type: 'line', x1: 12, y1: 3, x2: 12, y2: 15 }
], { strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }),
download: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' },
{ type: 'polyline', points: '7 10 12 15 17 10' },
{ type: 'line', x1: 12, y1: 15, x2: 12, y2: 3 }
], { strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }),
check: () => createSVG('0 0 24 24', [
{ type: 'polyline', points: '20 6 9 17 4 12' }
], { strokeWidth: '3', strokeLinecap: 'round', strokeLinejoin: 'round' }),
search: () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 11, cy: 11, r: 8 },
{ type: 'line', x1: 21, y1: 21, x2: 16.65, y2: 16.65 }
], { strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }),
chevronRight: () => createSVG('0 0 24 24', [
{ type: 'polyline', points: '9 18 15 12 9 6' }
], { strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }),
ytLogo: () => createSVG('0 0 28 20', [
{ type: 'path', d: 'M27.5 3.1s-.3-2.2-1.3-3.2C25.8-1 24.1-.1 23.6-.1 19.8 0 14 0 14 0S8.2 0 4.4-.1c-.5 0-1.6 0-2.6 1-1 .9-1.3 3.2-1.3 3.2S0 5.4 0 7.7v4.6c0 2.3.4 4.6.4 4.6s.3 2.2 1.3 3.2c1 .9 2.3 1 2.8 1.1 2.5.2 9.5.2 9.5.2s5.8 0 9.5-.2c.5-.1 1.8-0.2 2.8-1.1 1-.9 1.3-3.2 1.3-3.2s.4-2.3.4-4.6V7.7c0-2.3-.4-4.6-.4-4.6z', fill: '#FF0000' },
{ type: 'path', d: 'M11.2 14.6V5.4l8 4.6-8 4.6z', fill: 'white' }
], { stroke: false }),
// Category icons
interface: () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 18, height: 18, rx: 2 },
{ type: 'path', d: 'M3 9h18' },
{ type: 'path', d: 'M9 21V9' }
], _S),
appearance: () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 5 },
{ type: 'path', d: 'M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4' }
], _S),
content: () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 2, width: 20, height: 20, rx: 2 },
{ type: 'line', x1: 7, y1: 2, x2: 7, y2: 22 },
{ type: 'line', x1: 17, y1: 2, x2: 17, y2: 22 },
{ type: 'line', x1: 2, y1: 12, x2: 22, y2: 12 }
], _S),
player: () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 3, width: 20, height: 14, rx: 2 },
{ type: 'path', d: 'm10 8 5 3-5 3z' },
{ type: 'line', x1: 2, y1: 20, x2: 22, y2: 20 }
], _S),
sponsor: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z' }
], _S),
shield: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ type: 'path', d: 'M9 12l2 2 4-4' }
], _S),
quality: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M5.6 18.4l2.8-2.8M15.6 8.4l2.8-2.8' },
{ type: 'circle', cx: 12, cy: 12, r: 4 }
], _S),
clutter: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6' },
{ type: 'path', d: 'M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2' },
{ type: 'line', x1: 10, y1: 11, x2: 10, y2: 17 },
{ type: 'line', x1: 14, y1: 11, x2: 14, y2: 17 }
], _S),
livechat: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z' },
{ type: 'circle', cx: 12, cy: 10, r: 1, fill: 'currentColor' },
{ type: 'circle', cx: 8, cy: 10, r: 1, fill: 'currentColor' },
{ type: 'circle', cx: 16, cy: 10, r: 1, fill: 'currentColor' }
], _S),
actions: () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'path', d: 'M12 8v4l3 3' }
], _S),
controls: () => createSVG('0 0 24 24', [
{ type: 'line', x1: 4, y1: 21, x2: 4, y2: 14 },
{ type: 'line', x1: 4, y1: 10, x2: 4, y2: 3 },
{ type: 'line', x1: 12, y1: 21, x2: 12, y2: 12 },
{ type: 'line', x1: 12, y1: 8, x2: 12, y2: 3 },
{ type: 'line', x1: 20, y1: 21, x2: 20, y2: 16 },
{ type: 'line', x1: 20, y1: 12, x2: 20, y2: 3 },
{ type: 'circle', cx: 4, cy: 12, r: 2, fill: 'currentColor' },
{ type: 'circle', cx: 12, cy: 10, r: 2, fill: 'currentColor' },
{ type: 'circle', cx: 20, cy: 14, r: 2, fill: 'currentColor' }
], _S),
downloads: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' },
{ type: 'polyline', points: '7 10 12 15 17 10' },
{ type: 'line', x1: 12, y1: 15, x2: 12, y2: 3 }
], _S),
'list-plus': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 8, y1: 6, x2: 21, y2: 6 },
{ type: 'line', x1: 8, y1: 12, x2: 21, y2: 12 },
{ type: 'line', x1: 8, y1: 18, x2: 21, y2: 18 },
{ type: 'circle', cx: 3, cy: 6, r: 1, fill: 'currentColor' },
{ type: 'circle', cx: 3, cy: 12, r: 1, fill: 'currentColor' },
{ type: 'circle', cx: 3, cy: 18, r: 1, fill: 'currentColor' }
], _S),
// Feature Icons
'eye-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24' },
{ type: 'line', x1: 1, y1: 1, x2: 23, y2: 23 }
], _S),
'moon': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z' }
], _S),
'square': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 18, height: 18, rx: 2 }
], _S),
'video-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M10.66 6H14a2 2 0 0 1 2 2v2.34l1 1L22 8v8' },
{ type: 'path', d: 'M16 16a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2' },
{ type: 'line', x1: 2, y1: 2, x2: 22, y2: 22 }
], _S),
'external-link': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' },
{ type: 'polyline', points: '15 3 21 3 21 9' },
{ type: 'line', x1: 10, y1: 14, x2: 21, y2: 3 }
], _S),
'layout': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 18, height: 18, rx: 2 },
{ type: 'line', x1: 3, y1: 9, x2: 21, y2: 9 },
{ type: 'line', x1: 9, y1: 21, x2: 9, y2: 9 }
], _S),
'grid': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 7, height: 7 },
{ type: 'rect', x: 14, y: 3, width: 7, height: 7 },
{ type: 'rect', x: 14, y: 14, width: 7, height: 7 },
{ type: 'rect', x: 3, y: 14, width: 7, height: 7 }
], _S),
'folder-video': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z' },
{ type: 'polygon', points: '10 13 15 10.5 10 8 10 13' }
], _S),
'fullscreen': () => createSVG('0 0 24 24', [
{ type: 'polyline', points: '15 3 21 3 21 9' },
{ type: 'polyline', points: '9 21 3 21 3 15' },
{ type: 'polyline', points: '21 15 21 21 15 21' },
{ type: 'polyline', points: '3 9 3 3 9 3' }
], _S),
'arrows-horizontal': () => createSVG('0 0 24 24', [
{ type: 'polyline', points: '18 8 22 12 18 16' },
{ type: 'polyline', points: '6 8 2 12 6 16' },
{ type: 'line', x1: 2, y1: 12, x2: 22, y2: 12 }
], _S),
'youtube': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z' },
{ type: 'polygon', points: '9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02' }
], _S),
'tv': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 7, width: 20, height: 15, rx: 2 },
{ type: 'polyline', points: '17 2 12 7 7 2' }
], _S),
'home': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z' },
{ type: 'polyline', points: '9 22 9 12 15 12 15 22' }
], _S),
'sidebar': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 18, height: 18, rx: 2 },
{ type: 'line', x1: 9, y1: 3, x2: 9, y2: 21 }
], _S),
'skip-forward': () => createSVG('0 0 24 24', [
{ type: 'polygon', points: '5 4 15 12 5 20 5 4' },
{ type: 'line', x1: 19, y1: 5, x2: 19, y2: 19 }
], _S),
'play-circle': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'polygon', points: '10 8 16 12 10 16 10 8' }
], _S),
'monitor': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 3, width: 20, height: 14, rx: 2 },
{ type: 'line', x1: 8, y1: 21, x2: 16, y2: 21 },
{ type: 'line', x1: 12, y1: 17, x2: 12, y2: 21 }
], _S),
'menu': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 3, y1: 12, x2: 21, y2: 12 },
{ type: 'line', x1: 3, y1: 6, x2: 21, y2: 6 },
{ type: 'line', x1: 3, y1: 18, x2: 21, y2: 18 }
], _S),
'hash': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 4, y1: 9, x2: 20, y2: 9 },
{ type: 'line', x1: 4, y1: 15, x2: 20, y2: 15 },
{ type: 'line', x1: 10, y1: 3, x2: 8, y2: 21 },
{ type: 'line', x1: 16, y1: 3, x2: 14, y2: 21 }
], _S),
'file-text': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' },
{ type: 'polyline', points: '14 2 14 8 20 8' },
{ type: 'line', x1: 16, y1: 13, x2: 8, y2: 13 },
{ type: 'line', x1: 16, y1: 17, x2: 8, y2: 17 },
{ type: 'polyline', points: '10 9 9 9 8 9' }
], _S),
'link': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' },
{ type: 'path', d: 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71' }
], _S),
'music': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M9 18V5l12-2v13' },
{ type: 'circle', cx: 6, cy: 18, r: 3 },
{ type: 'circle', cx: 18, cy: 16, r: 3 }
], _S),
'hard-drive-download': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M12 2v8' },
{ type: 'path', d: 'm16 6-4 4-4-4' },
{ type: 'rect', x: 2, y: 14, width: 20, height: 8, rx: 2 },
{ type: 'line', x1: 6, y1: 18, x2: 6.01, y2: 18 }
], _S),
'users-x': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2' },
{ type: 'circle', cx: 9, cy: 7, r: 4 },
{ type: 'line', x1: 18, y1: 8, x2: 23, y2: 13 },
{ type: 'line', x1: 23, y1: 8, x2: 18, y2: 13 }
], _S),
'clock': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'polyline', points: '12 6 12 12 16 14' }
], _S),
'download-cloud': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242' },
{ type: 'path', d: 'M12 12v9' },
{ type: 'path', d: 'm8 17 4 4 4-4' }
], _S),
'filter': () => createSVG('0 0 24 24', [
{ type: 'polygon', points: '22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3' }
], _S),
'layout-grid': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 7, height: 7, rx: 1 },
{ type: 'rect', x: 14, y: 3, width: 7, height: 7, rx: 1 },
{ type: 'rect', x: 14, y: 14, width: 7, height: 7, rx: 1 },
{ type: 'rect', x: 3, y: 14, width: 7, height: 7, rx: 1 }
], _S),
'message-square': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z' }
], _S),
'play': () => createSVG('0 0 24 24', [
{ type: 'polygon', points: '5 3 19 12 5 21 5 3' }
], _S),
'sparkles': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'm12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3L12 3Z' },
{ type: 'path', d: 'M5 3v4' },
{ type: 'path', d: 'M19 17v4' },
{ type: 'path', d: 'M3 5h4' },
{ type: 'path', d: 'M17 19h4' }
], _S),
'square-x': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 18, height: 18, rx: 2 },
{ type: 'line', x1: 9, y1: 9, x2: 15, y2: 15 },
{ type: 'line', x1: 15, y1: 9, x2: 9, y2: 15 }
], _S),
'list': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 8, y1: 6, x2: 21, y2: 6 },
{ type: 'line', x1: 8, y1: 12, x2: 21, y2: 12 },
{ type: 'line', x1: 8, y1: 18, x2: 21, y2: 18 },
{ type: 'line', x1: 3, y1: 6, x2: 3.01, y2: 6 },
{ type: 'line', x1: 3, y1: 12, x2: 3.01, y2: 12 },
{ type: 'line', x1: 3, y1: 18, x2: 3.01, y2: 18 }
], _S),
'picture-in-picture-2': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 1, y: 1, width: 22, height: 22, rx: 2 },
{ type: 'rect', x: 10, y: 10, width: 12, height: 8, rx: 1 }
], _S),
'gauge': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M12 15l3.5-5' },
{ type: 'circle', cx: 12, cy: 15, r: 2 },
{ type: 'path', d: 'M2 12a10 10 0 0120 0' }
], _S)
};
const CATEGORY_CONFIG = {
'Interface': { icon: 'interface', color: '#60a5fa' },
'Appearance': { icon: 'appearance', color: '#f472b6' },
'Content': { icon: 'content', color: '#34d399' },
'Video Player': { icon: 'player', color: '#a78bfa' },
'Ad Blocker': { icon: 'shield', color: '#10b981' },
'SponsorBlock': { icon: 'sponsor', color: '#22d3ee' },
'Quality': { icon: 'quality', color: '#facc15' },
'Clutter': { icon: 'clutter', color: '#f87171' },
'Live Chat': { icon: 'livechat', color: '#4ade80' },
'Action Buttons': { icon: 'actions', color: '#c084fc' },
'Player Controls': { icon: 'controls', color: '#38bdf8' },
'Downloads': { icon: 'downloads', color: '#f97316' },
};
function injectSettingsButton() {
const handleDisplay = () => {
const isWatchPage = window.location.pathname.startsWith('/watch');
const createButton = (id) => {
const btn = document.createElement('button');
btn.id = id;
btn.className = 'ytkit-trigger-btn';
btn.title = 'YTKit Settings (Ctrl+Alt+Y)';
btn.appendChild(ICONS.settings());
btn.onclick = () => document.body.classList.toggle('ytkit-panel-open');
return btn;
};
if (isWatchPage) {
// Remove masthead button if we're on watch page
document.getElementById('ytkit-masthead-btn')?.remove();
// Only add watch button if it doesn't exist
if (document.getElementById('ytkit-watch-btn')) return;
waitForElement('#top-row #owner', (ownerDiv) => {
if (document.getElementById('ytkit-watch-btn')) return;
const btn = createButton('ytkit-watch-btn');
ownerDiv.prepend(btn);
});
} else {
// Remove watch button if we're not on watch page
document.getElementById('ytkit-watch-btn')?.remove();
// Only add masthead button if it doesn't exist
if (document.getElementById('ytkit-masthead-btn')) return;
waitForElement('ytd-masthead #end', (mastheadEnd) => {
if (document.getElementById('ytkit-masthead-btn')) return;
mastheadEnd.prepend(createButton('ytkit-masthead-btn'));
});
}
};
addNavigateRule("settingsButtonRule", handleDisplay);
}
function buildSettingsPanel() {
if (document.getElementById('ytkit-settings-panel')) return;
const categoryOrder = ['Interface', 'Appearance', 'Content', 'Video Player', 'Ad Blocker', 'SponsorBlock', 'Quality', 'Clutter', 'Live Chat', 'Action Buttons', 'Player Controls', 'Downloads'];
// Group labels: maps first category of each group → label text
const categoryGroupLabels = {
'Interface': 'Interface',
'Content': 'Content',
'Video Player': 'Player',
'Ad Blocker': 'Filtering',
'Live Chat': 'Controls',
};
const featuresByCategory = categoryOrder.reduce((acc, cat) => ({...acc, [cat]: []}), {});
features.forEach(f => { if (f.group && featuresByCategory[f.group]) featuresByCategory[f.group].push(f); });
// Create overlay
const overlay = document.createElement('div');
overlay.id = 'ytkit-overlay';
overlay.onclick = () => document.body.classList.remove('ytkit-panel-open');
// Create panel
const panel = document.createElement('div');
panel.id = 'ytkit-settings-panel';
panel.setAttribute('role', 'dialog');
// Header
const header = document.createElement('header');
header.className = 'ytkit-header';
const brand = document.createElement('div');
brand.className = 'ytkit-brand';
const logoWrap = document.createElement('div');
logoWrap.className = 'ytkit-logo';
logoWrap.appendChild(ICONS.ytLogo());
const title = document.createElement('h1');
title.className = 'ytkit-title';
const titleYT = document.createElement('span');
titleYT.className = 'ytkit-title-yt';
titleYT.textContent = 'YT';
const titleKit = document.createElement('span');
titleKit.className = 'ytkit-title-kit';
titleKit.textContent = 'Kit';
title.appendChild(titleYT);
title.appendChild(titleKit);
const badge = document.createElement('span');
badge.className = 'ytkit-badge';
badge.textContent = 'PRO';
brand.appendChild(logoWrap);
brand.appendChild(title);
brand.appendChild(badge);
const closeBtn = document.createElement('button');
closeBtn.className = 'ytkit-close';
closeBtn.title = 'Close (Esc)';
closeBtn.appendChild(ICONS.close());
closeBtn.onclick = () => document.body.classList.remove('ytkit-panel-open');
header.appendChild(brand);
header.appendChild(closeBtn);
// Body
const body = document.createElement('div');
body.className = 'ytkit-body';
// Sidebar
const sidebar = document.createElement('nav');
sidebar.className = 'ytkit-sidebar';
// Search box
const searchContainer = document.createElement('div');
searchContainer.className = 'ytkit-search-container';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'ytkit-search-input';
searchInput.placeholder = 'Search settings...';
searchInput.id = 'ytkit-search';
const searchIcon = ICONS.search();
searchIcon.setAttribute('class', 'ytkit-search-icon');
searchContainer.appendChild(searchIcon);
searchContainer.appendChild(searchInput);
sidebar.appendChild(searchContainer);
// Divider
const divider = document.createElement('div');
divider.className = 'ytkit-sidebar-divider';
sidebar.appendChild(divider);
categoryOrder.forEach((cat, index) => {
// Insert group label before first category of each group
if (categoryGroupLabels[cat]) {
const groupLabel = document.createElement('div');
groupLabel.className = 'ytkit-nav-group-label';
groupLabel.textContent = categoryGroupLabels[cat];
if (index > 0) groupLabel.style.marginTop = '10px';
sidebar.appendChild(groupLabel);
}
// Special handling for Ad Blocker sidebar
if (cat === 'Ad Blocker') {
const config = CATEGORY_CONFIG[cat];
const catId = cat.replace(/ /g, '-');
const btn = document.createElement('button');
btn.className = 'ytkit-nav-btn';
btn.dataset.tab = catId;
const iconWrap = document.createElement('span');
iconWrap.className = 'ytkit-nav-icon';
iconWrap.style.setProperty('--cat-color', config.color);
iconWrap.appendChild((ICONS.shield || ICONS.settings)());
const labelSpan = document.createElement('span');
labelSpan.className = 'ytkit-nav-label';
labelSpan.textContent = cat;
const countSpan = document.createElement('span');
countSpan.className = 'ytkit-nav-count';
const st = _rw.__ytab?.stats;
countSpan.textContent = st ? `${st.blocked}` : '0';
countSpan.title = 'Ads blocked this session';
// Live update — store reference so it can be cleared when panel is destroyed
const _adCountInterval = setInterval(() => {
// Stop updating if the element has been removed from the DOM
if (!countSpan.isConnected) { clearInterval(_adCountInterval); return; }
const s = _rw.__ytab?.stats;
if (s) countSpan.textContent = `${s.blocked}`;
}, 3000);
const arrowSpan = document.createElement('span');
arrowSpan.className = 'ytkit-nav-arrow';
arrowSpan.appendChild(ICONS.chevronRight());
btn.appendChild(iconWrap);
btn.appendChild(labelSpan);
btn.appendChild(countSpan);
btn.appendChild(arrowSpan);
sidebar.appendChild(btn);
return;
}
const categoryFeatures = featuresByCategory[cat];
if (!categoryFeatures || categoryFeatures.length === 0) return;
const config = CATEGORY_CONFIG[cat] || { icon: 'settings', color: '#60a5fa' };
const catId = cat.replace(/ /g, '-');
const enabledCount = categoryFeatures.filter(f => !f.isSubFeature && appState.settings[f.id]).length;
const totalCount = categoryFeatures.filter(f => !f.isSubFeature).length;
const btn = document.createElement('button');
btn.className = 'ytkit-nav-btn' + (index === 0 ? ' active' : '');
btn.dataset.tab = catId;
const iconWrap = document.createElement('span');
iconWrap.className = 'ytkit-nav-icon';
iconWrap.style.setProperty('--cat-color', config.color);
const iconFn = ICONS[config.icon] || ICONS.settings;
iconWrap.appendChild(iconFn());
const labelSpan = document.createElement('span');
labelSpan.className = 'ytkit-nav-label';
labelSpan.textContent = cat;
const countSpan = document.createElement('span');
countSpan.className = 'ytkit-nav-count';
countSpan.textContent = `${enabledCount}/${totalCount}`;
const arrowSpan = document.createElement('span');
arrowSpan.className = 'ytkit-nav-arrow';
arrowSpan.appendChild(ICONS.chevronRight());
btn.appendChild(iconWrap);
btn.appendChild(labelSpan);
btn.appendChild(countSpan);
btn.appendChild(arrowSpan);
sidebar.appendChild(btn);
});
// Content
const content = document.createElement('div');
content.className = 'ytkit-content';
// Ad Blocker Custom Pane
function buildAdBlockPane(config) {
const adblockFeature = features.find(f => f.id === 'ytAdBlock');
const subFeatures = features.filter(f => f.parentId === 'ytAdBlock');
const pane = document.createElement('section');
pane.id = 'ytkit-pane-Ad-Blocker';
pane.className = 'ytkit-pane';
// ── Header ──
const paneHeader = document.createElement('div');
paneHeader.className = 'ytkit-pane-header';
const paneTitle = document.createElement('div');
paneTitle.className = 'ytkit-pane-title';
const paneIcon = document.createElement('span');
paneIcon.className = 'ytkit-pane-icon';
paneIcon.style.setProperty('--cat-color', config.color);
paneIcon.appendChild((ICONS.shield || ICONS.settings)());
const paneTitleH2 = document.createElement('h2');
paneTitleH2.textContent = 'Ad Blocker';
paneTitle.appendChild(paneIcon);
paneTitle.appendChild(paneTitleH2);
// Master toggle
const toggleLabel = document.createElement('label');
toggleLabel.className = 'ytkit-toggle-all';
toggleLabel.style.marginLeft = 'auto';
const toggleText = document.createElement('span');
toggleText.textContent = 'Enabled';
const toggleSwitch = document.createElement('div');
toggleSwitch.className = 'ytkit-switch' + (appState.settings.ytAdBlock ? ' active' : '');
const toggleInput = document.createElement('input');
toggleInput.type = 'checkbox';
toggleInput.id = 'ytkit-toggle-ytAdBlock';
toggleInput.checked = appState.settings.ytAdBlock;
toggleInput.onchange = async () => {
appState.settings.ytAdBlock = toggleInput.checked;
toggleSwitch.classList.toggle('active', toggleInput.checked);
settingsManager.save(appState.settings);
if (toggleInput.checked) adblockFeature?.init?.(); else adblockFeature?.destroy?.();
updateAllToggleStates();
};
const toggleTrack = document.createElement('span');
toggleTrack.className = 'ytkit-switch-track';
const toggleThumb = document.createElement('span');
toggleThumb.className = 'ytkit-switch-thumb';
toggleTrack.appendChild(toggleThumb);
toggleSwitch.appendChild(toggleInput);
toggleSwitch.appendChild(toggleTrack);
toggleLabel.appendChild(toggleText);
toggleLabel.appendChild(toggleSwitch);
paneHeader.appendChild(paneTitle);
paneHeader.appendChild(toggleLabel);
pane.appendChild(paneHeader);
// ── Sub-feature toggles ──
const subGrid = document.createElement('div');
subGrid.className = 'ytkit-features-grid';
subFeatures.forEach(sf => { subGrid.appendChild(buildFeatureCard(sf, config.color, true)); });
pane.appendChild(subGrid);
// ── Shared styles for this pane ──
const sectionStyle = 'background:var(--ytkit-bg-elevated);border-radius:10px;padding:16px;margin-top:12px;';
const labelStyle = 'font-size:13px;font-weight:600;color:var(--ytkit-text);margin-bottom:8px;display:flex;align-items:center;gap:6px;';
const inputStyle = 'width:100%;background:var(--ytkit-bg-card);color:var(--ytkit-text);border:1px solid var(--ytkit-border);border-radius:6px;padding:8px 10px;font-size:13px;font-family:inherit;outline:none;transition:border-color 0.2s;';
const btnStyle = `background:${config.color};color:#000;border:none;padding:8px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:opacity 0.2s;`;
const btnSecStyle = 'background:var(--ytkit-bg-card);color:var(--ytkit-text);border:1px solid var(--ytkit-border);padding:8px 16px;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;transition:opacity 0.2s;';
// ── Stats Section ──
const statsSection = document.createElement('div');
statsSection.style.cssText = sectionStyle;
const statsLabel = document.createElement('div');
statsLabel.style.cssText = labelStyle;
statsLabel.textContent = 'Session Stats';
statsSection.appendChild(statsLabel);
const statsGrid = document.createElement('div');
statsGrid.style.cssText = 'display:grid;grid-template-columns:repeat(3,1fr);gap:10px;';
function makeStat(label, valueGetter, color) {
const box = document.createElement('div');
box.style.cssText = 'background:var(--ytkit-bg-card);padding:12px;border-radius:8px;text-align:center;';
const num = document.createElement('div');
num.style.cssText = `font-size:22px;font-weight:700;color:${color};font-variant-numeric:tabular-nums;`;
num.textContent = valueGetter();
num.dataset.statKey = label;
const lbl = document.createElement('div');
lbl.style.cssText = 'font-size:11px;color:var(--ytkit-text-muted);margin-top:4px;';
lbl.textContent = label;
box.appendChild(num);
box.appendChild(lbl);
return box;
}
const s = _rw.__ytab?.stats || { blocked: 0, pruned: 0, ssapSkipped: 0 };
statsGrid.appendChild(makeStat('Ads Blocked', () => s.blocked, config.color));
statsGrid.appendChild(makeStat('JSON Pruned', () => s.pruned, '#a78bfa'));
statsGrid.appendChild(makeStat('SSAP Skipped', () => s.ssapSkipped, '#f59e0b'));
statsSection.appendChild(statsGrid);
// Auto-refresh stats
let statsInterval = null;
const refreshStats = () => {
const st = _rw.__ytab?.stats || { blocked: 0, pruned: 0, ssapSkipped: 0 };
statsGrid.querySelectorAll('[data-stat-key]').forEach(el => {
const key = el.dataset.statKey;
if (key === 'Ads Blocked') el.textContent = st.blocked;
else if (key === 'JSON Pruned') el.textContent = st.pruned;
else if (key === 'SSAP Skipped') el.textContent = st.ssapSkipped;
});
};
// Start/stop interval when pane is visible
new MutationObserver(() => {
if (pane.classList.contains('active')) {
refreshStats();
if (!statsInterval) statsInterval = setInterval(refreshStats, 2000);
} else {
if (statsInterval) { clearInterval(statsInterval); statsInterval = null; }
}
}).observe(pane, { attributes: true, attributeFilter: ['class'] });
pane.appendChild(statsSection);
// ── Filter List Management ──
const filterSection = document.createElement('div');
filterSection.style.cssText = sectionStyle;
const filterLabel = document.createElement('div');
filterLabel.style.cssText = labelStyle;
filterLabel.textContent = 'Remote Filter List';
filterSection.appendChild(filterLabel);
// URL row
const urlRow = document.createElement('div');
urlRow.style.cssText = 'display:flex;gap:8px;align-items:center;';
const urlInput = document.createElement('input');
urlInput.type = 'text';
urlInput.style.cssText = inputStyle + 'flex:1;';
urlInput.value = appState.settings.adblockFilterUrl || '';
urlInput.placeholder = 'Filter list URL (.txt format)';
urlInput.spellcheck = false;
const saveUrlBtn = document.createElement('button');
saveUrlBtn.style.cssText = btnSecStyle;
saveUrlBtn.textContent = 'Save';
saveUrlBtn.onclick = async () => {
appState.settings.adblockFilterUrl = urlInput.value.trim();
settingsManager.save(appState.settings);
createToast('Filter URL saved', 'success');
};
urlRow.appendChild(urlInput);
urlRow.appendChild(saveUrlBtn);
filterSection.appendChild(urlRow);
// Info + Update row
const infoRow = document.createElement('div');
infoRow.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-top:10px;';
const filterInfo = document.createElement('div');
filterInfo.style.cssText = 'font-size:11px;color:var(--ytkit-text-muted);';
const cachedTime = GM_getValue('ytab_filter_update_time', 0);
const cachedCount = GM_getValue('ytab_cached_selector_count', 0);
filterInfo.textContent = cachedTime
? `${cachedCount} selectors | Updated ${new Date(cachedTime).toLocaleString()}`
: 'No filters loaded yet';
const updateBtn = document.createElement('button');
updateBtn.style.cssText = btnStyle;
updateBtn.textContent = 'Update Filters';
updateBtn.onclick = () => {
updateBtn.textContent = 'Fetching...';
updateBtn.style.opacity = '0.6';
const url = (appState.settings.adblockFilterUrl || '').trim();
if (!url) { createToast('No filter URL set', 'error'); updateBtn.textContent = 'Update Filters'; updateBtn.style.opacity = '1'; return; }
GM.xmlHttpRequest({
method: 'GET',
url: url + '?_=' + Date.now(),
timeout: 15000,
onload(resp) {
if (resp.status >= 200 && resp.status < 400) {
const text = resp.responseText || '';
const selectors = _rw.__ytab?.parseFilterList?.(text) || [];
const selectorStr = selectors.join(',\n');
GM_setValue('ytab_cached_selectors', selectorStr);
GM_setValue('ytab_filter_update_time', Date.now());
GM_setValue('ytab_cached_selector_count', selectors.length);
GM_setValue('ytab_raw_filters', text);
// Apply live
const custom = GM_getValue('ytab_custom_filters', '');
const combined = [selectorStr, custom].filter(Boolean).join(',\n');
_rw.__ytab?.updateCSS?.(combined);
filterInfo.textContent = `${selectors.length} selectors | Updated ${new Date().toLocaleString()}`;
createToast(`Filters updated: ${selectors.length} cosmetic selectors parsed`, 'success');
// Refresh preview if open
if (previewArea.style.display !== 'none') renderPreview();
} else {
createToast(`Filter fetch failed: HTTP ${resp.status}`, 'error');
}
updateBtn.textContent = 'Update Filters';
updateBtn.style.opacity = '1';
},
onerror() { createToast('Filter fetch failed (network error)', 'error'); updateBtn.textContent = 'Update Filters'; updateBtn.style.opacity = '1'; },
ontimeout() { createToast('Filter fetch timed out', 'error'); updateBtn.textContent = 'Update Filters'; updateBtn.style.opacity = '1'; }
});
};
infoRow.appendChild(filterInfo);
infoRow.appendChild(updateBtn);
filterSection.appendChild(infoRow);
// ── Bootstrap Status Indicator ──
const statusRow = document.createElement('div');
statusRow.style.cssText = 'display:flex;align-items:center;gap:6px;margin-top:10px;padding:8px 10px;background:var(--ytkit-bg-card);border-radius:6px;';
const statusDot = document.createElement('span');
const isActive = !!_rw.__ytab?.active;
statusDot.style.cssText = `width:8px;height:8px;border-radius:50%;background:${isActive ? config.color : '#ef4444'};flex-shrink:0;`;
const statusText = document.createElement('span');
statusText.style.cssText = 'font-size:11px;color:var(--ytkit-text-muted);';
statusText.textContent = isActive
? 'Proxy engines active (installed at document-start)'
: 'Proxies not installed - enable Ad Blocker and reload page';
statusRow.appendChild(statusDot);
statusRow.appendChild(statusText);
filterSection.appendChild(statusRow);
// ── Live Ad-Block Stats ──
if (isActive) {
const statsRow = document.createElement('div');
statsRow.style.cssText = 'display:flex;gap:12px;margin-top:8px;padding:8px 10px;background:var(--ytkit-bg-card);border-radius:6px;';
const abStats = _rw.__ytab?.stats || { blocked: 0, pruned: 0, ssapSkipped: 0 };
const statItems = [
{ label: 'Blocked', value: abStats.blocked, color: '#22c55e' },
{ label: 'Pruned', value: abStats.pruned, color: '#3b82f6' },
{ label: 'Skipped', value: abStats.ssapSkipped, color: '#f59e0b' }
];
statItems.forEach(s => {
const item = document.createElement('div');
item.style.cssText = 'display:flex;align-items:center;gap:4px;';
const dot = document.createElement('span');
dot.style.cssText = 'width:6px;height:6px;border-radius:50%;background:' + s.color + ';flex-shrink:0;';
const lbl = document.createElement('span');
lbl.style.cssText = 'font-size:11px;color:var(--ytkit-text-muted);';
lbl.textContent = s.label + ': ' + s.value;
item.appendChild(dot);
item.appendChild(lbl);
statsRow.appendChild(item);
});
filterSection.appendChild(statsRow);
// Auto-refresh stats every 5s while panel is open
const statsInterval = setInterval(() => {
if (!document.body.classList.contains('ytkit-panel-open')) {
clearInterval(statsInterval); return;
}
const live = _rw.__ytab?.stats || {};
const labels = statsRow.querySelectorAll('span:last-child');
if (labels[0]) labels[0].textContent = 'Blocked: ' + (live.blocked || 0);
if (labels[1]) labels[1].textContent = 'Pruned: ' + (live.pruned || 0);
if (labels[2]) labels[2].textContent = 'Skipped: ' + (live.ssapSkipped || 0);
}, 5000);
}
pane.appendChild(filterSection);
// ── Custom Filters Section ──
const customSection = document.createElement('div');
customSection.style.cssText = sectionStyle;
const customLabel = document.createElement('div');
customLabel.style.cssText = labelStyle;
customLabel.textContent = 'Custom Filters';
const customHint = document.createElement('span');
customHint.style.cssText = 'font-weight:400;color:var(--ytkit-text-muted);font-size:11px;';
customHint.textContent = '(CSS selectors, one per line)';
customLabel.appendChild(customHint);
customSection.appendChild(customLabel);
const customTextarea = document.createElement('textarea');
customTextarea.style.cssText = inputStyle + 'min-height:100px;resize:vertical;font-family:"Cascadia Code","Fira Code",monospace;font-size:12px;line-height:1.5;';
customTextarea.value = (GM_getValue('ytab_custom_filters', '') || '').replace(/,\n/g, '\n').replace(/,/g, '\n');
customTextarea.placeholder = 'ytd-merch-shelf-renderer\n.ytp-ad-overlay-slot\n#custom-ad-element';
customTextarea.spellcheck = false;
customSection.appendChild(customTextarea);
const customBtnRow = document.createElement('div');
customBtnRow.style.cssText = 'display:flex;gap:8px;margin-top:10px;';
const saveCustomBtn = document.createElement('button');
saveCustomBtn.style.cssText = btnStyle;
saveCustomBtn.textContent = 'Apply Filters';
saveCustomBtn.onclick = () => {
const lines = customTextarea.value.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('!') && !l.startsWith('//'));
const selectorStr = lines.join(',\n');
GM_setValue('ytab_custom_filters', selectorStr);
// Apply live
const remote = GM_getValue('ytab_cached_selectors', '');
const combined = [remote, selectorStr].filter(Boolean).join(',\n');
_rw.__ytab?.updateCSS?.(combined);
createToast(`${lines.length} custom filter${lines.length !== 1 ? 's' : ''} applied`, 'success');
};
const clearCustomBtn = document.createElement('button');
clearCustomBtn.style.cssText = btnSecStyle;
clearCustomBtn.textContent = 'Clear';
clearCustomBtn.onclick = () => {
customTextarea.value = '';
GM_setValue('ytab_custom_filters', '');
const remote = GM_getValue('ytab_cached_selectors', '');
_rw.__ytab?.updateCSS?.(remote);
createToast('Custom filters cleared', 'success');
};
customBtnRow.appendChild(saveCustomBtn);
customBtnRow.appendChild(clearCustomBtn);
customSection.appendChild(customBtnRow);
pane.appendChild(customSection);
// ── Active Filters Preview (collapsible) ──
const previewSection = document.createElement('div');
previewSection.style.cssText = sectionStyle;
const previewHeader = document.createElement('div');
previewHeader.style.cssText = 'display:flex;justify-content:space-between;align-items:center;cursor:pointer;';
const previewLabel = document.createElement('div');
previewLabel.style.cssText = labelStyle + 'margin-bottom:0;';
previewLabel.textContent = 'Active Filters Preview';
const previewToggle = document.createElement('span');
previewToggle.style.cssText = 'font-size:11px;color:var(--ytkit-text-muted);';
previewToggle.textContent = 'Show';
previewHeader.appendChild(previewLabel);
previewHeader.appendChild(previewToggle);
const previewArea = document.createElement('pre');
previewArea.style.cssText = 'display:none;margin-top:10px;padding:10px;background:var(--ytkit-bg-card);border-radius:6px;font-size:11px;color:var(--ytkit-text-muted);font-family:"Cascadia Code","Fira Code",monospace;max-height:300px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;line-height:1.6;';
function renderPreview() {
const remote = (GM_getValue('ytab_cached_selectors', '') || '').split(',\n').filter(Boolean);
const custom = (GM_getValue('ytab_custom_filters', '') || '').split(',\n').filter(Boolean);
let text = '';
if (remote.length) text += `/* Remote (${remote.length}) */\n` + remote.join('\n') + '\n\n';
if (custom.length) text += `/* Custom (${custom.length}) */\n` + custom.join('\n');
if (!text) text = 'No filters loaded. Click "Update Filters" to fetch from remote URL.';
previewArea.textContent = text;
}
previewHeader.onclick = () => {
const showing = previewArea.style.display !== 'none';
previewArea.style.display = showing ? 'none' : 'block';
previewToggle.textContent = showing ? 'Show' : 'Hide';
if (!showing) renderPreview();
};
previewSection.appendChild(previewHeader);
previewSection.appendChild(previewArea);
pane.appendChild(previewSection);
return pane;
}
categoryOrder.forEach((cat, index) => {
// Special handling for Ad Blocker
if (cat === 'Ad Blocker') {
const config = CATEGORY_CONFIG[cat];
content.appendChild(buildAdBlockPane(config));
return;
}
const categoryFeatures = featuresByCategory[cat];
if (!categoryFeatures || categoryFeatures.length === 0) return;
const config = CATEGORY_CONFIG[cat] || { icon: 'settings', color: '#60a5fa' };
const catId = cat.replace(/ /g, '-');
const pane = document.createElement('section');
pane.id = `ytkit-pane-${catId}`;
pane.className = 'ytkit-pane' + (index === 0 ? ' active' : '');
// Pane header
const paneHeader = document.createElement('div');
paneHeader.className = 'ytkit-pane-header';
const paneTitle = document.createElement('div');
paneTitle.className = 'ytkit-pane-title';
const paneIcon = document.createElement('span');
paneIcon.className = 'ytkit-pane-icon';
paneIcon.style.setProperty('--cat-color', config.color);
const paneIconFn = ICONS[config.icon] || ICONS.settings;
paneIcon.appendChild(paneIconFn());
const paneTitleH2 = document.createElement('h2');
paneTitleH2.textContent = cat;
paneTitle.appendChild(paneIcon);
paneTitle.appendChild(paneTitleH2);
const toggleAllLabel = document.createElement('label');
toggleAllLabel.className = 'ytkit-toggle-all';
const toggleAllText = document.createElement('span');
toggleAllText.textContent = 'Enable All';
const toggleAllSwitch = document.createElement('div');
toggleAllSwitch.className = 'ytkit-switch';
const toggleAllInput = document.createElement('input');
toggleAllInput.type = 'checkbox';
toggleAllInput.className = 'ytkit-toggle-all-cb';
toggleAllInput.dataset.category = catId;
const toggleAllTrack = document.createElement('span');
toggleAllTrack.className = 'ytkit-switch-track';
const toggleAllThumb = document.createElement('span');
toggleAllThumb.className = 'ytkit-switch-thumb';
toggleAllTrack.appendChild(toggleAllThumb);
toggleAllSwitch.appendChild(toggleAllInput);
toggleAllSwitch.appendChild(toggleAllTrack);
toggleAllLabel.appendChild(toggleAllText);
toggleAllLabel.appendChild(toggleAllSwitch);
paneHeader.appendChild(paneTitle);
// Reset group button
const resetBtn = document.createElement('button');
resetBtn.className = 'ytkit-reset-group-btn';
resetBtn.title = 'Reset this group to defaults';
resetBtn.textContent = 'Reset';
resetBtn.onclick = () => {
const categoryFeatures = featuresByCategory[cat];
const backup = {};
categoryFeatures.forEach(f => { backup[f.id] = appState.settings[f.id]; });
categoryFeatures.forEach(f => {
const defaultValue = settingsManager.defaults[f.id];
if (defaultValue !== undefined) {
appState.settings[f.id] = defaultValue;
try { f.destroy?.(); f._initialized = false; } catch(e) {}
if (defaultValue) {
try { f.init?.(); f._initialized = true; } catch(e) {}
}
}
});
settingsManager.save(appState.settings);
updateAllToggleStates();
// Update UI
categoryFeatures.forEach(f => {
const toggle = document.getElementById(`ytkit-toggle-${f.id}`);
if (toggle) {
toggle.checked = appState.settings[f.id];
const switchEl = toggle.closest('.ytkit-switch');
if (switchEl) switchEl.classList.toggle('active', toggle.checked);
}
});
createToast(`Reset "${cat}" to defaults`, 'success');
showToast(`"${cat}" reset to defaults`, '#f97316', { duration: 5, action: { text: 'Undo', onClick: async () => {
categoryFeatures.forEach(f => {
if (backup[f.id] !== undefined) {
appState.settings[f.id] = backup[f.id];
try { f.destroy?.(); f._initialized = false; } catch(e) {}
if (backup[f.id]) { try { f.init?.(); f._initialized = true; } catch(e) {} }
}
});
settingsManager.save(appState.settings);
updateAllToggleStates();
categoryFeatures.forEach(f => {
const t = document.getElementById(`ytkit-toggle-${f.id}`);
if (t) { t.checked = appState.settings[f.id]; const s = t.closest('.ytkit-switch'); if (s) s.classList.toggle('active', t.checked); }
});
showToast(`"${cat}" restored`, '#22c55e');
}}});
};
paneHeader.appendChild(resetBtn);
paneHeader.appendChild(toggleAllLabel);
pane.appendChild(paneHeader);
// Features grid
const grid = document.createElement('div');
grid.className = 'ytkit-features-grid';
const parentFeatures = categoryFeatures.filter(f => !f.isSubFeature);
const subFeatures = categoryFeatures.filter(f => f.isSubFeature);
// Sort features: dropdowns/selects first, then others
const sortedParentFeatures = [...parentFeatures].sort((a, b) => {
const aIsDropdown = a.type === 'select';
const bIsDropdown = b.type === 'select';
if (aIsDropdown && !bIsDropdown) return -1;
if (!aIsDropdown && bIsDropdown) return 1;
return 0;
});
sortedParentFeatures.forEach(f => {
const card = buildFeatureCard(f, config.color);
grid.appendChild(card);
// Add sub-features if any
const children = subFeatures.filter(sf => sf.parentId === f.id);
if (children.length > 0) {
const subContainer = document.createElement('div');
subContainer.className = 'ytkit-sub-features';
subContainer.dataset.parentId = f.id;
if (!appState.settings[f.id]) { subContainer.style.opacity = '0.35'; subContainer.style.pointerEvents = 'none'; }
children.forEach(sf => {
subContainer.appendChild(buildFeatureCard(sf, config.color, true));
});
grid.appendChild(subContainer);
}
});
pane.appendChild(grid);
content.appendChild(pane);
});
body.appendChild(sidebar);
body.appendChild(content);
// Footer
const footer = document.createElement('footer');
footer.className = 'ytkit-footer';
const footerLeft = document.createElement('div');
footerLeft.className = 'ytkit-footer-left';
const githubLink = document.createElement('a');
githubLink.href = 'https://github.com/SysAdminDoc/YTKit';
githubLink.target = '_blank';
githubLink.className = 'ytkit-github';
githubLink.title = 'View on GitHub';
githubLink.appendChild(ICONS.github());
// YTYT-Downloader Installer Button - Downloads a .bat launcher
const ytToolsBtn = document.createElement('button');
ytToolsBtn.className = 'ytkit-github';
ytToolsBtn.title = 'Download & run this script to setup local YouTube downloads (VLC/MPV streaming, yt-dlp)';
ytToolsBtn.style.cssText = 'background: linear-gradient(135deg, #f97316, #22c55e) !important; border: none; cursor: pointer;';
const dlIcon = ICONS.download();
dlIcon.style.color = 'white';
ytToolsBtn.appendChild(dlIcon);
ytToolsBtn.addEventListener('click', () => {
// Generate a .bat file that runs the PowerShell installer
const batContent = `@echo off
title YTYT-Downloader Installer
echo ========================================
echo YTYT-Downloader Installer
echo VLC/MPV Streaming ^& Local Downloads
echo ========================================
echo.
echo Downloading and running installer...
echo.
powershell -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/SysAdminDoc/YTYT-Downloader/refs/heads/main/src/Install-YTYT.ps1 | iex"
echo.
echo If the window closes immediately, right-click and Run as Administrator.
pause
`;
const blob = new Blob([batContent], { type: 'application/x-bat' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'Install-YTYT.bat';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast('📦 Installer downloaded! Double-click the .bat file to run.', '#22c55e');
});
const ytToolsLink = ytToolsBtn; // Alias for existing appendChild call
const versionSpan = document.createElement('span');
versionSpan.className = 'ytkit-version';
versionSpan.textContent = 'v1.1.0';
versionSpan.style.position = 'relative';
versionSpan.style.cursor = 'pointer';
// What's New badge
const CURRENT_VER = '1.1.0';
const lastSeenVer = GM_getValue('ytkit_last_seen_version', '');
if (lastSeenVer !== CURRENT_VER) {
const badge = document.createElement('span');
badge.id = 'ytkit-whats-new-badge';
badge.style.cssText = 'position:absolute;top:-3px;right:-8px;width:8px;height:8px;background:#ef4444;border-radius:50%;animation:ytkit-badge-pulse 2s infinite;';
versionSpan.appendChild(badge);
versionSpan.title = 'New in v1.1.0: Watch page alignment fixes, ad-block stats display, configurable Cobalt URL, performance optimizations';
versionSpan.onclick = () => {
GM_setValue('ytkit_last_seen_version', CURRENT_VER);
badge.remove();
showToast('v1.0.0: Watch page alignment fixes, live ad-block stats, configurable Cobalt URL, 35+ performance & robustness improvements', '#3b82f6', { duration: 6 });
};
}
const shortcutSpan = document.createElement('span');
shortcutSpan.className = 'ytkit-shortcut';
shortcutSpan.textContent = 'Ctrl+Alt+Y';
footerLeft.appendChild(githubLink);
footerLeft.appendChild(ytToolsLink);
footerLeft.appendChild(versionSpan);
footerLeft.appendChild(shortcutSpan);
const footerRight = document.createElement('div');
footerRight.className = 'ytkit-footer-right';
const importBtn = document.createElement('button');
importBtn.className = 'ytkit-btn ytkit-btn-secondary';
importBtn.id = 'ytkit-import';
importBtn.appendChild(ICONS.upload());
const importText = document.createElement('span');
importText.textContent = 'Import';
importBtn.appendChild(importText);
const exportBtn = document.createElement('button');
exportBtn.className = 'ytkit-btn ytkit-btn-primary';
exportBtn.id = 'ytkit-export';
exportBtn.appendChild(ICONS.download());
const exportText = document.createElement('span');
exportText.textContent = 'Export';
exportBtn.appendChild(exportText);
footerRight.appendChild(importBtn);
footerRight.appendChild(exportBtn);
footer.appendChild(footerLeft);
footer.appendChild(footerRight);
panel.appendChild(header);
panel.appendChild(body);
panel.appendChild(footer);
document.body.appendChild(overlay);
document.body.appendChild(panel);
updateAllToggleStates();
}
function buildFeatureCard(f, accentColor, isSubFeature = false) {
const card = document.createElement('div');
card.className = 'ytkit-feature-card' + (isSubFeature ? ' ytkit-sub-card' : '') + (f.type === 'textarea' ? ' ytkit-textarea-card' : '') + (f.type === 'select' ? ' ytkit-select-card' : '') + (f.type === 'info' ? ' ytkit-info-card' : '');
card.dataset.featureId = f.id;
if (accentColor) card.style.setProperty('--cat-color', accentColor);
// Apply enabled accent stripe for boolean features
const _cardIsEnabled = f._arrayKey
? (appState.settings[f._arrayKey] || []).includes(f._arrayValue)
: (f.type !== 'select' && f.type !== 'color' && f.type !== 'range' && appState.settings[f.id]);
if (_cardIsEnabled && !isSubFeature) card.classList.add('ytkit-card-enabled');
// Special styling for info cards
if (f.type === 'info') {
card.style.cssText = 'background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(34, 197, 94, 0.15)) !important; border: 1px solid rgba(249, 115, 22, 0.3) !important; grid-column: 1 / -1;';
}
const info = document.createElement('div');
info.className = 'ytkit-feature-info';
const name = document.createElement('h3');
name.className = 'ytkit-feature-name';
name.textContent = f.name;
const desc = document.createElement('p');
desc.className = 'ytkit-feature-desc';
desc.textContent = f.description;
info.appendChild(name);
info.appendChild(desc);
card.appendChild(info);
if (f.type === 'info') {
// info-type features have no interactive control
} else if (f.type === 'textarea') {
const textarea = document.createElement('textarea');
textarea.className = 'ytkit-input';
textarea.id = `ytkit-input-${f.id}`;
textarea.placeholder = f.placeholder || 'word1, word2, phrase';
textarea.value = appState.settings[f.settingKey || f.id] || appState.settings[f.id] || '';
// Auto-save on blur for textarea features
textarea.addEventListener('blur', () => {
const key = f.settingKey || f.id;
appState.settings[key] = textarea.value;
settingsManager.save(appState.settings);
document.dispatchEvent(new CustomEvent('ytkit-settings-changed', { detail: { key } }));
if (f.id === 'cobaltUrl' && textarea.value) {
GM_setValue('ytkit_cobalt_url', textarea.value);
}
});
card.appendChild(textarea);
} else if (f.type === 'select') {
const select = document.createElement('select');
select.className = 'ytkit-select';
select.id = `ytkit-select-${f.id}`;
select.style.cssText = `padding:8px 12px;border-radius:8px;background:var(--ytkit-bg-base);color:#fff;border:1px solid rgba(255,255,255,0.1);cursor:pointer;font-size:13px;min-width:150px;`;
const settingKey = f.settingKey || f.id;
const currentValue = String(appState.settings[settingKey] ?? Object.keys(f.options)[0]);
for (const [value, label] of Object.entries(f.options)) {
const option = document.createElement('option');
option.value = value;
option.textContent = label;
option.selected = value === currentValue;
select.appendChild(option);
}
card.appendChild(select);
} else if (f.type === 'range') {
const settingKey = f.settingKey || f.id;
const currentVal = appState.settings[settingKey] ?? f.min ?? 0;
const wrapper = document.createElement('div');
wrapper.style.cssText = 'display:flex;align-items:center;gap:10px;min-width:200px;';
const slider = document.createElement('input');
slider.type = 'range';
slider.min = f.min ?? 0;
slider.max = f.max ?? 100;
slider.step = f.step ?? 1;
slider.value = currentVal;
slider.className = 'ytkit-range';
slider.id = `ytkit-range-${f.id}`;
slider.style.cssText = 'flex:1;accent-color:#3b82f6;cursor:pointer;height:6px;';
const valDisplay = document.createElement('span');
valDisplay.className = 'ytkit-range-value';
valDisplay.style.cssText = 'min-width:45px;text-align:right;font-size:12px;color:var(--ytkit-text-secondary);font-weight:600;font-variant-numeric:tabular-nums;';
valDisplay.textContent = f.formatValue ? f.formatValue(currentVal) : currentVal;
slider.oninput = () => { valDisplay.textContent = f.formatValue ? f.formatValue(slider.value) : slider.value; };
wrapper.appendChild(slider);
wrapper.appendChild(valDisplay);
card.appendChild(wrapper);
} else if (f.type === 'color') {
const settingKey = f.settingKey || f.id;
const currentVal = appState.settings[settingKey] || '';
const wrapper = document.createElement('div');
wrapper.style.cssText = 'display:flex;align-items:center;gap:8px;';
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.id = `ytkit-color-${f.id}`;
colorInput.value = currentVal || '#3b82f6';
colorInput.style.cssText = 'width:36px;height:28px;border:1px solid rgba(255,255,255,0.15);border-radius:6px;cursor:pointer;background:transparent;padding:0;';
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
clearBtn.style.cssText = 'padding:4px 10px;border-radius:4px;background:var(--ytkit-bg-hover);border:1px solid rgba(255,255,255,0.1);color:#aaa;font-size:11px;cursor:pointer;';
clearBtn.onclick = () => { colorInput.value = '#3b82f6'; colorInput.dispatchEvent(new Event('change', { bubbles: true })); };
wrapper.appendChild(colorInput);
wrapper.appendChild(clearBtn);
card.appendChild(wrapper);
} else {
// For array-toggle sub-features, check array membership instead of boolean
const isEnabled = f._arrayKey
? (appState.settings[f._arrayKey] || []).includes(f._arrayValue)
: appState.settings[f.id];
const switchDiv = document.createElement('div');
switchDiv.className = 'ytkit-switch' + (isEnabled ? ' active' : '');
switchDiv.style.setProperty('--switch-color', accentColor);
const input = document.createElement('input');
input.type = 'checkbox';
input.className = 'ytkit-feature-cb';
input.id = `ytkit-toggle-${f.id}`;
input.checked = isEnabled;
const track = document.createElement('span');
track.className = 'ytkit-switch-track';
const thumb = document.createElement('span');
thumb.className = 'ytkit-switch-thumb';
const iconWrap = document.createElement('span');
iconWrap.className = 'ytkit-switch-icon';
iconWrap.appendChild(ICONS.check());
thumb.appendChild(iconWrap);
track.appendChild(thumb);
switchDiv.appendChild(input);
switchDiv.appendChild(track);
card.appendChild(switchDiv);
}
return card;
}
function createToast(message, type = 'success', duration = 3000) {
const colorMap = { success: '#22c55e', error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' };
showToast(message, colorMap[type] || '#22c55e', { duration: duration / 1000 });
}
function updateAllToggleStates() {
document.querySelectorAll('.ytkit-toggle-all-cb').forEach(cb => {
const catId = cb.dataset.category;
const pane = document.getElementById(`ytkit-pane-${catId}`);
if (!pane) return;
const featureToggles = pane.querySelectorAll('.ytkit-feature-cb');
const allChecked = featureToggles.length > 0 && Array.from(featureToggles).every(t => t.checked);
cb.checked = allChecked;
// Update switch visual state
const switchEl = cb.closest('.ytkit-switch');
if (switchEl) {
switchEl.classList.toggle('active', allChecked);
}
});
// Update nav counts
document.querySelectorAll('.ytkit-nav-btn').forEach(btn => {
const catId = btn.dataset.tab;
const pane = document.getElementById(`ytkit-pane-${catId}`);
if (!pane) return;
const featureToggles = pane.querySelectorAll('.ytkit-feature-card:not(.ytkit-sub-card) .ytkit-feature-cb');
const enabledCount = Array.from(featureToggles).filter(t => t.checked).length;
const totalCount = featureToggles.length;
const countEl = btn.querySelector('.ytkit-nav-count');
if (countEl) countEl.textContent = `${enabledCount}/${totalCount}`;
});
}
function handleFileImport(callback) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json,application/json';
fileInput.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = readerEvent => callback(readerEvent.target.result);
reader.readAsText(file);
};
fileInput.click();
}
function handleFileExport(filename, content) {
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = Object.assign(document.createElement('a'), { href: url, download: filename });
a.click();
URL.revokeObjectURL(url);
}
function attachUIEventListeners() {
const doc = document;
// Auto-close panel on SPA navigation — prevents overlay persisting on home/other pages
doc.addEventListener('yt-navigate-start', () => {
doc.body.classList.remove('ytkit-panel-open');
});
// Close panel
doc.addEventListener('click', (e) => {
if (e.target.closest('.ytkit-close') || e.target.matches('#ytkit-overlay')) {
doc.body.classList.remove('ytkit-panel-open');
}
});
// Tab navigation
doc.addEventListener('click', (e) => {
const navBtn = e.target.closest('.ytkit-nav-btn');
if (navBtn) {
doc.querySelectorAll('.ytkit-nav-btn').forEach(btn => btn.classList.remove('active'));
doc.querySelectorAll('.ytkit-pane').forEach(pane => pane.classList.remove('active'));
navBtn.classList.add('active');
const pane = doc.querySelector(`#ytkit-pane-${navBtn.dataset.tab}`);
if (pane) {
pane.classList.add('active');
pane.scrollTop = 0;
}
// Also scroll the main content area
const contentArea = doc.querySelector('.ytkit-content');
if (contentArea) contentArea.scrollTop = 0;
}
});
// Keyboard shortcuts
doc.addEventListener('keydown', (e) => {
if (e.key === "Escape" && doc.body.classList.contains('ytkit-panel-open')) {
doc.body.classList.remove('ytkit-panel-open');
}
if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'y') {
e.preventDefault();
e.stopPropagation();
doc.body.classList.toggle('ytkit-panel-open');
}
});
// Search functionality
doc.addEventListener('input', (e) => {
if (e.target.matches('#ytkit-search')) {
const query = e.target.value.toLowerCase().trim();
const allCards = doc.querySelectorAll('.ytkit-feature-card');
const allPanes = doc.querySelectorAll('.ytkit-pane');
const allNavBtns = doc.querySelectorAll('.ytkit-nav-btn');
// Clear all previous highlights
doc.querySelectorAll('.ytkit-feature-name, .ytkit-feature-desc').forEach(el => {
if (el._originalText !== undefined) el.textContent = el._originalText;
});
if (!query) {
// Reset to normal view
allCards.forEach(card => card.style.display = '');
allPanes.forEach(pane => pane.classList.remove('ytkit-search-active'));
doc.querySelectorAll('.ytkit-sub-features').forEach(sub => {
const parentId = sub.dataset.parentId;
const enabled = appState.settings[parentId];
sub.style.opacity = enabled ? '' : '0.35';
sub.style.pointerEvents = enabled ? '' : 'none';
});
// Restore normal tab behavior
if (!doc.querySelector('.ytkit-pane.active')) {
allPanes[0]?.classList.add('active');
allNavBtns[0]?.classList.add('active');
}
return;
}
// Show all panes for searching
allPanes.forEach(pane => pane.classList.add('ytkit-search-active'));
doc.querySelectorAll('.ytkit-sub-features').forEach(sub => { sub.style.opacity = ''; sub.style.pointerEvents = ''; });
// Helper to highlight text matches
const highlightText = (el, query) => {
if (!el) return;
if (el._originalText === undefined) el._originalText = el.textContent;
const text = el._originalText;
const idx = text.toLowerCase().indexOf(query);
if (idx === -1) { el.textContent = text; return; }
el.innerHTML = '';
el.appendChild(document.createTextNode(text.substring(0, idx)));
const mark = document.createElement('mark');
mark.style.cssText = 'background:#fbbf24;color:#000;border-radius:2px;padding:0 1px;';
mark.textContent = text.substring(idx, idx + query.length);
el.appendChild(mark);
el.appendChild(document.createTextNode(text.substring(idx + query.length)));
};
// Filter cards and highlight
let matchCount = 0;
allCards.forEach(card => {
const nameEl = card.querySelector('.ytkit-feature-name');
const descEl = card.querySelector('.ytkit-feature-desc');
const name = nameEl?.textContent.toLowerCase() || '';
const desc = descEl?.textContent.toLowerCase() || '';
const matches = name.includes(query) || desc.includes(query);
card.style.display = matches ? '' : 'none';
if (matches) {
matchCount++;
highlightText(nameEl, query);
highlightText(descEl, query);
}
});
// Update nav buttons with match counts
allNavBtns.forEach(btn => {
const catId = btn.dataset.tab;
const pane = doc.getElementById(`ytkit-pane-${catId}`);
if (pane) {
const visibleCards = pane.querySelectorAll('.ytkit-feature-card:not([style*="display: none"])').length;
const countEl = btn.querySelector('.ytkit-nav-count');
if (countEl && query) {
countEl.textContent = visibleCards > 0 ? `${visibleCards} match${visibleCards !== 1 ? 'es' : ''}` : '0';
countEl.style.color = visibleCards > 0 ? '#22c55e' : '#666';
}
}
});
}
});
// Clear search on tab click
doc.addEventListener('click', (e) => {
if (e.target.closest('.ytkit-nav-btn')) {
const searchInput = doc.getElementById('ytkit-search');
if (searchInput && searchInput.value) {
searchInput.value = '';
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
}
}
});
// Feature toggles
doc.addEventListener('change', (e) => {
if (e.target.matches('.ytkit-feature-cb')) {
const card = e.target.closest('[data-feature-id]');
const featureId = card.dataset.featureId;
const isEnabled = e.target.checked;
// Update switch visual
const switchEl = e.target.closest('.ytkit-switch');
if (switchEl) switchEl.classList.toggle('active', isEnabled);
// Update card enabled accent stripe
const cardEl = e.target.closest('.ytkit-feature-card');
if (cardEl && !cardEl.classList.contains('ytkit-sub-card')) {
cardEl.classList.toggle('ytkit-card-enabled', isEnabled);
}
const feature = features.find(f => f.id === featureId);
// Array-toggle sub-features: modify parent array instead of boolean
if (feature?._arrayKey) {
let arr = appState.settings[feature._arrayKey] || [];
if (!Array.isArray(arr)) arr = [];
if (isEnabled && !arr.includes(feature._arrayValue)) {
arr.push(feature._arrayValue);
} else if (!isEnabled) {
arr = arr.filter(v => v !== feature._arrayValue);
}
appState.settings[feature._arrayKey] = arr;
settingsManager.save(appState.settings);
// Re-init parent feature to apply changes
const parentFeature = features.find(f => f.id === feature.parentId);
if (parentFeature) {
try { parentFeature.destroy?.(); } catch(e) {}
if (appState.settings[parentFeature.id] !== false) {
try { parentFeature.init?.(); } catch(e) {}
}
}
} else {
appState.settings[featureId] = isEnabled;
settingsManager.save(appState.settings);
if (feature) {
isEnabled ? feature.init?.() : feature.destroy?.();
}
// If this is a sub-feature, reinit the parent to pick up the change
if (feature?.isSubFeature && feature.parentId) {
const parentFeature = features.find(f => f.id === feature.parentId);
if (parentFeature && appState.settings[parentFeature.id] !== false) {
try { parentFeature.destroy?.(); } catch(e) {}
try { parentFeature.init?.(); } catch(e) {}
}
}
}
// Toggle sub-features visibility (greyed out, not hidden)
const subContainer = doc.querySelector(`.ytkit-sub-features[data-parent-id="${featureId}"]`);
if (subContainer) {
subContainer.style.opacity = isEnabled ? '' : '0.35';
subContainer.style.pointerEvents = isEnabled ? '' : 'none';
}
updateAllToggleStates();
}
// Toggle all
if (e.target.matches('.ytkit-toggle-all-cb')) {
const isEnabled = e.target.checked;
const catId = e.target.dataset.category;
const pane = doc.getElementById(`ytkit-pane-${catId}`);
// Update the switch visual state
const switchEl = e.target.closest('.ytkit-switch');
if (switchEl) {
switchEl.classList.toggle('active', isEnabled);
}
if (pane) {
pane.querySelectorAll('.ytkit-feature-card:not(.ytkit-sub-card) .ytkit-feature-cb').forEach(cb => {
if (cb.checked !== isEnabled) {
cb.checked = isEnabled;
cb.dispatchEvent(new Event('change', { bubbles: true }));
}
});
}
}
});
// Textarea input
doc.addEventListener('input', (e) => {
if (e.target.matches('.ytkit-input')) {
const card = e.target.closest('[data-feature-id]');
const featureId = card.dataset.featureId;
appState.settings[featureId] = e.target.value;
settingsManager.save(appState.settings);
const feature = features.find(f => f.id === featureId);
if (feature) {
feature.destroy?.();
feature.init?.();
}
}
// Select dropdown
if (e.target.matches('.ytkit-select')) {
const card = e.target.closest('[data-feature-id]');
const featureId = card.dataset.featureId;
const feature = features.find(f => f.id === featureId);
// Use settingKey if specified, otherwise use featureId
const settingKey = feature?.settingKey || featureId;
const newValue = e.target.value;
appState.settings[settingKey] = newValue;
settingsManager.save(appState.settings);
// Reinitialize the feature to apply changes immediately
if (feature) {
if (typeof feature.destroy === 'function') {
try { feature.destroy(); feature._initialized = false; } catch (e) { /* ignore */ }
}
if (typeof feature.init === 'function') {
try { feature.init(); feature._initialized = true; } catch (e) { console.warn('[YTKit] Feature reinit error:', e); }
}
}
const selectedText = e.target.options[e.target.selectedIndex].text;
createToast(`${feature?.name || 'Setting'} changed to ${selectedText}`, 'success');
}
// Range slider
if (e.target.matches('.ytkit-range')) {
const card = e.target.closest('[data-feature-id]');
const featureId = card.dataset.featureId;
const feature = features.find(f => f.id === featureId);
const settingKey = feature?.settingKey || featureId;
const val = parseFloat(e.target.value);
appState.settings[settingKey] = val;
settingsManager.save(appState.settings);
if (feature) {
try { feature.destroy?.(); } catch(err) {}
try { feature.init?.(); } catch(err) {}
}
}
// Color picker
if (e.target.matches('[id^="ytkit-color-"]')) {
const card = e.target.closest('[data-feature-id]');
const featureId = card.dataset.featureId;
const feature = features.find(f => f.id === featureId);
const settingKey = feature?.settingKey || featureId;
appState.settings[settingKey] = e.target.value;
settingsManager.save(appState.settings);
if (feature) {
try { feature.destroy?.(); } catch(err) {}
try { feature.init?.(); } catch(err) {}
}
}
});
doc.addEventListener('click', (e) => {
if (e.target.closest('#ytkit-export')) {
const configString = settingsManager.exportAllSettings();
handleFileExport('ytkit_settings.json', configString);
createToast('Settings exported successfully', 'success');
}
if (e.target.closest('#ytkit-import')) {
handleFileImport(async (content) => {
const success = settingsManager.importAllSettings(content);
if (success) {
createToast('Settings imported! Reloading...', 'success');
setTimeout(() => location.reload(), 1000);
} else {
createToast('Import failed. Invalid file format.', 'error');
}
});
}
});
}
// SECTION 5: STYLES
function injectPanelStyles() {
GM_addStyle(`@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');:root{--ytkit-font:'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;--ytkit-bg-base:#0a0a0b;--ytkit-bg-elevated:#111113;--ytkit-bg-surface:#18181b;--ytkit-bg-hover:#1f1f23;--ytkit-bg-active:#27272a;--ytkit-border:#27272a;--ytkit-border-subtle:#1f1f23;--ytkit-text-primary:#fafafa;--ytkit-text-secondary:#a1a1aa;--ytkit-text-muted:#71717a;--ytkit-accent:#ff4e45;--ytkit-accent-soft:rgba(255,78,69,0.15);--ytkit-success:#22c55e;--ytkit-error:#ef4444;--ytkit-radius-sm:6px;--ytkit-radius-md:10px;--ytkit-radius-lg:14px;--ytkit-radius-xl:20px;--ytkit-shadow-sm:0 1px 2px rgba(0,0,0,0.3);--ytkit-shadow-md:0 4px 12px rgba(0,0,0,0.4);--ytkit-shadow-lg:0 8px 32px rgba(0,0,0,0.5);--ytkit-shadow-xl:0 24px 64px rgba(0,0,0,0.6);--ytkit-transition:200ms cubic-bezier(0.4,0,0.2,1);} .ytkit-vlc-btn,.ytkit-local-dl-btn,.ytkit-mp3-dl-btn,.ytkit-transcript-btn,.ytkit-mpv-btn,.ytkit-dlplay-btn,.ytkit-embed-btn{display:inline-flex !important;visibility:visible !important;opacity:1 !important;z-index:9999 !important;position:relative !important;} .ytkit-button-container{display:flex !important;gap:8px !important;margin:8px 0 !important;flex-wrap:wrap !important;visibility:visible !important;} .ytkit-trigger-btn{display:flex;align-items:center;justify-content:center;width:40px;height:40px;padding:0;margin:0 4px;background:transparent;border:none;border-radius:var(--ytkit-radius-md);cursor:pointer;transition:all var(--ytkit-transition);} .ytkit-trigger-btn svg{width:22px;height:22px;color:var(--yt-spec-icon-inactive,#aaa);transition:all var(--ytkit-transition);} .ytkit-trigger-btn:hover{background:var(--yt-spec-badge-chip-background,rgba(255,255,255,0.1));} .ytkit-trigger-btn:hover svg{color:var(--yt-spec-text-primary,#fff);transform:rotate(45deg);} #ytkit-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.75);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:99998;opacity:0;pointer-events:none;transition:opacity 300ms ease;} body.ytkit-panel-open #ytkit-overlay{opacity:1;pointer-events:auto;} #ytkit-settings-panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(0.96);z-index:99999;display:flex;flex-direction:column;width:95%;max-width:1100px;height:85vh;max-height:800px;background:var(--ytkit-bg-base);border:1px solid var(--ytkit-border);border-radius:var(--ytkit-radius-xl);box-shadow:var(--ytkit-shadow-xl),0 0 0 1px rgba(255,255,255,0.05) inset;font-family:var(--ytkit-font);color:var(--ytkit-text-primary);opacity:0;pointer-events:none;transition:all 300ms cubic-bezier(0.32,0.72,0,1);overflow:hidden;} body.ytkit-panel-open #ytkit-settings-panel{opacity:1;pointer-events:auto;transform:translate(-50%,-50%) scale(1);} .ytkit-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;background:linear-gradient(180deg,var(--ytkit-bg-elevated) 0%,var(--ytkit-bg-base) 100%);border-bottom:1px solid var(--ytkit-border);flex-shrink:0;} .ytkit-brand{display:flex;align-items:center;gap:12px;} .ytkit-logo{display:flex;align-items:center;justify-content:center;width:42px;height:42px;background:linear-gradient(135deg,#ff0000 0%,#cc0000 100%);border-radius:var(--ytkit-radius-md);box-shadow:0 4px 12px rgba(255,0,0,0.3);} .ytkit-yt-icon{width:26px;height:auto;} .ytkit-title{font-size:26px;font-weight:700;letter-spacing:-0.5px;margin:0;} .ytkit-title-yt{background:linear-gradient(135deg,#ff4e45 0%,#ff0000 50%,#ff4e45 100%);background-size:200% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;animation:ytkit-shimmer 3s linear infinite;} .ytkit-title-kit{color:var(--ytkit-text-primary);} @keyframes ytkit-shimmer{0%{background-position:0% center;} 100%{background-position:200% center;} } .ytkit-badge{padding:4px 10px;font-size:10px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;color:#fff;background:linear-gradient(135deg,#ff4e45,#ff0000);border-radius:100px;box-shadow:0 2px 8px rgba(255,78,69,0.4);} .ytkit-close{display:flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border);border-radius:var(--ytkit-radius-md);cursor:pointer;transition:all var(--ytkit-transition);} .ytkit-close svg{width:18px;height:18px;color:var(--ytkit-text-secondary);transition:color var(--ytkit-transition);} .ytkit-close:hover{background:var(--ytkit-error);border-color:var(--ytkit-error);} .ytkit-close:hover svg{color:#fff;} .ytkit-body{display:flex;flex:1;overflow:hidden;} .ytkit-sidebar{display:flex;flex-direction:column;width:240px;padding:16px 12px;background:var(--ytkit-bg-elevated);border-right:1px solid var(--ytkit-border);overflow-y:auto;flex-shrink:0;} .ytkit-search-container{position:relative;margin-bottom:12px;} .ytkit-search-input{width:100%;padding:10px 12px 10px 36px;background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border);border-radius:var(--ytkit-radius-md);color:var(--ytkit-text-primary);font-size:13px;transition:all var(--ytkit-transition);} .ytkit-search-input:focus{outline:none;border-color:var(--ytkit-accent);box-shadow:0 0 0 3px rgba(255,78,69,0.15);} .ytkit-search-input::placeholder{color:var(--ytkit-text-muted);} .ytkit-search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:var(--ytkit-text-muted);pointer-events:none;} .ytkit-sidebar-divider{height:1px;background:var(--ytkit-border);margin:8px 0 12px;} .ytkit-pane.ytkit-search-active{display:block;} .ytkit-pane.ytkit-search-active .ytkit-pane-header{display:none;} .ytkit-nav-btn{display:flex;align-items:center;gap:10px;width:100%;padding:10px 12px;margin-bottom:2px;background:transparent;border:none;border-radius:var(--ytkit-radius-md);cursor:pointer;transition:all var(--ytkit-transition);text-align:left;} .ytkit-nav-btn:hover{background:var(--ytkit-bg-hover);} .ytkit-nav-btn.active{background:var(--ytkit-bg-active);} .ytkit-nav-icon{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:var(--ytkit-bg-surface);border-radius:var(--ytkit-radius-sm);flex-shrink:0;transition:all var(--ytkit-transition);} .ytkit-nav-btn.active .ytkit-nav-icon{background:var(--cat-color,var(--ytkit-accent));box-shadow:0 2px 8px color-mix(in srgb,var(--cat-color,var(--ytkit-accent)) 40%,transparent);} .ytkit-nav-icon svg{width:16px;height:16px;color:var(--ytkit-text-secondary);transition:color var(--ytkit-transition);} .ytkit-nav-btn.active .ytkit-nav-icon svg{color:#fff;} .ytkit-nav-label{flex:1;font-size:13px;font-weight:500;color:var(--ytkit-text-secondary);transition:color var(--ytkit-transition);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} .ytkit-nav-btn.active .ytkit-nav-label{color:var(--ytkit-text-primary);} .ytkit-nav-count{font-size:11px;font-weight:600;color:var(--ytkit-text-muted);background:var(--ytkit-bg-surface);padding:2px 6px;border-radius:100px;transition:all var(--ytkit-transition);} .ytkit-nav-btn.active .ytkit-nav-count{background:rgba(255,255,255,0.15);color:var(--ytkit-text-primary);} .ytkit-nav-arrow{display:flex;opacity:0;transition:opacity var(--ytkit-transition);} .ytkit-nav-arrow svg{width:14px;height:14px;color:var(--ytkit-text-muted);} .ytkit-nav-btn.active .ytkit-nav-arrow{opacity:1;} .ytkit-nav-group-label{padding:4px 12px 2px;font-size:9.5px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:var(--ytkit-text-muted);user-select:none;pointer-events:none;} .ytkit-content{flex:1;padding:24px;overflow-y:auto;background:var(--ytkit-bg-base);} .ytkit-pane{display:none;animation:ytkit-fade-in 300ms ease;} .ytkit-pane.active{display:block;} .ytkit-pane.ytkit-vh-pane.active{display:flex;flex-direction:column;height:100%;max-height:calc(85vh - 180px);} #ytkit-vh-content{flex:1;overflow-y:auto;padding-right:8px;} @keyframes ytkit-fade-in{from{opacity:0;transform:translateX(8px);} to{opacity:1;transform:translateX(0);} } @keyframes ytkit-badge-pulse{0%,100%{opacity:1;transform:scale(1);} 50%{opacity:0.5;transform:scale(1.3);} } .ytkit-pane-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--ytkit-border);} .ytkit-pane-title{display:flex;align-items:center;gap:12px;} .ytkit-pane-icon{display:flex;align-items:center;justify-content:center;width:40px;height:40px;background:var(--cat-color,var(--ytkit-accent));border-radius:var(--ytkit-radius-md);box-shadow:0 4px 12px color-mix(in srgb,var(--cat-color,var(--ytkit-accent)) 30%,transparent);} .ytkit-pane-icon svg{width:20px;height:20px;color:#fff;} .ytkit-pane-title h2{font-size:20px;font-weight:600;margin:0;color:var(--ytkit-text-primary);} .ytkit-toggle-all{display:flex;align-items:center;gap:10px;cursor:pointer;} .ytkit-toggle-all span{font-size:13px;font-weight:500;color:var(--ytkit-text-secondary);} .ytkit-reset-group-btn{padding:6px 12px;margin-right:12px;background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border);border-radius:var(--ytkit-radius-sm);color:var(--ytkit-text-muted);font-size:12px;font-weight:500;cursor:pointer;transition:all var(--ytkit-transition);} .ytkit-reset-group-btn:hover{background:var(--ytkit-error);border-color:var(--ytkit-error);color:#fff;} .ytkit-features-grid{display:flex;flex-direction:column;gap:8px;} .ytkit-feature-card{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border-subtle);border-left:3px solid transparent;border-radius:var(--ytkit-radius-md);transition:all var(--ytkit-transition);} .ytkit-feature-card:hover{background:var(--ytkit-bg-hover);border-color:var(--ytkit-border);border-left-color:transparent;} .ytkit-feature-card.ytkit-card-enabled{border-left-color:var(--cat-color,var(--ytkit-accent));} .ytkit-sub-card{margin-left:24px;background:var(--ytkit-bg-elevated);border-left:2px solid var(--ytkit-accent-soft);} .ytkit-sub-features{display:flex;flex-direction:column;gap:8px;} .ytkit-feature-info{flex:1;min-width:0;padding-right:16px;} .ytkit-feature-name{font-size:14px;font-weight:600;color:var(--ytkit-text-primary);margin:0 0 4px 0;} .ytkit-feature-desc{font-size:12px;color:var(--ytkit-text-muted);margin:0;line-height:1.4;} .ytkit-textarea-card{flex-direction:column;align-items:stretch;gap:12px;} .ytkit-textarea-card .ytkit-feature-info{padding-right:0;} .ytkit-input{width:100%;padding:10px 12px;font-family:var(--ytkit-font);font-size:13px;color:var(--ytkit-text-primary);background:var(--ytkit-bg-base);border:1px solid var(--ytkit-border);border-radius:var(--ytkit-radius-sm);resize:vertical;min-height:60px;transition:all var(--ytkit-transition);} .ytkit-input:focus{outline:none;border-color:var(--ytkit-accent);box-shadow:0 0 0 3px var(--ytkit-accent-soft);} .ytkit-input::placeholder{color:var(--ytkit-text-muted);} .ytkit-switch{position:relative;width:44px;height:24px;flex-shrink:0;} .ytkit-switch input{position:absolute;opacity:0;width:100%;height:100%;cursor:pointer;z-index:1;margin:0;} .ytkit-switch-track{position:absolute;inset:0;background:var(--ytkit-bg-active);border-radius:100px;transition:all var(--ytkit-transition);} .ytkit-switch.active .ytkit-switch-track{background:var(--switch-color,var(--ytkit-accent));box-shadow:0 0 12px color-mix(in srgb,var(--switch-color,var(--ytkit-accent)) 50%,transparent);} .ytkit-switch-thumb{position:absolute;top:2px;left:2px;width:20px;height:20px;background:#fff;border-radius:50%;box-shadow:var(--ytkit-shadow-sm);transition:all var(--ytkit-transition);display:flex;align-items:center;justify-content:center;} .ytkit-switch.active .ytkit-switch-thumb{transform:translateX(20px);} .ytkit-switch-icon{display:flex;opacity:0;transform:scale(0.5);transition:all var(--ytkit-transition);} .ytkit-switch-icon svg{width:12px;height:12px;color:var(--switch-color,var(--ytkit-accent));} .ytkit-switch.active .ytkit-switch-icon{opacity:1;transform:scale(1);} .ytkit-footer{display:flex;align-items:center;justify-content:space-between;padding:14px 24px;background:var(--ytkit-bg-elevated);border-top:1px solid var(--ytkit-border);flex-shrink:0;} .ytkit-footer-left{display:flex;align-items:center;gap:16px;} .ytkit-github{display:flex;align-items:center;justify-content:center;width:32px;height:32px;color:var(--ytkit-text-muted);background:var(--ytkit-bg-surface);border-radius:var(--ytkit-radius-sm);transition:all var(--ytkit-transition);} .ytkit-github:hover{color:var(--ytkit-text-primary);background:var(--ytkit-bg-hover);} .ytkit-github svg{width:18px;height:18px;} .ytkit-version{font-size:12px;font-weight:600;color:var(--ytkit-text-muted);background:var(--ytkit-bg-surface);padding:4px 10px;border-radius:100px;} .ytkit-shortcut{font-size:11px;color:var(--ytkit-text-muted);background:var(--ytkit-bg-surface);padding:4px 8px;border-radius:var(--ytkit-radius-sm);font-family:ui-monospace,SFMono-Regular,'SF Mono',Menlo,monospace;} .ytkit-footer-right{display:flex;gap:10px;} .ytkit-btn{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;font-family:var(--ytkit-font);font-size:13px;font-weight:600;border:none;border-radius:var(--ytkit-radius-md);cursor:pointer;transition:all var(--ytkit-transition);} .ytkit-btn svg{width:16px;height:16px;} .ytkit-btn-secondary{color:var(--ytkit-text-secondary);background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border);} .ytkit-btn-secondary:hover{background:var(--ytkit-bg-hover);color:var(--ytkit-text-primary);} .ytkit-btn-primary{color:#fff;background:linear-gradient(135deg,#ff4e45,#e6423a);box-shadow:0 2px 8px rgba(255,78,69,0.3);} .ytkit-btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(255,78,69,0.4);} .ytkit-toast{position:fixed;bottom:-80px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:10px;padding:14px 20px;font-family:var(--ytkit-font);font-size:14px;font-weight:500;color:#fff;background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border);border-radius:var(--ytkit-radius-lg);box-shadow:var(--ytkit-shadow-lg);z-index:100000;transition:all 400ms cubic-bezier(0.68,-0.55,0.27,1.55);} .ytkit-toast.show{bottom:24px;} .ytkit-toast-success{border-color:var(--ytkit-success);box-shadow:0 4px 20px rgba(34,197,94,0.2);} .ytkit-toast-error{border-color:var(--ytkit-error);box-shadow:0 4px 20px rgba(239,68,68,0.2);}ytd-watch-metadata.watch-active-metadata{margin-top:180px !important;} ytd-live-chat-frame:not([style*="position"]){margin-top:-57px !important;width:402px !important;} .ytkit-sidebar::-webkit-scrollbar,.ytkit-content::-webkit-scrollbar{width:6px;} .ytkit-sidebar::-webkit-scrollbar-track,.ytkit-content::-webkit-scrollbar-track{background:transparent;} .ytkit-sidebar::-webkit-scrollbar-thumb,.ytkit-content::-webkit-scrollbar-thumb{background:var(--ytkit-border);border-radius:100px;} .ytkit-sidebar::-webkit-scrollbar-thumb:hover,.ytkit-content::-webkit-scrollbar-thumb:hover{background:var(--ytkit-text-muted);} .ytkit-css-editor{width:100%;min-height:150px;padding:12px;background:var(--ytkit-bg-base);border:1px solid var(--ytkit-border);border-radius:var(--ytkit-radius-md);color:var(--ytkit-text-primary);font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:13px;line-height:1.5;resize:vertical;} .ytkit-css-editor:focus{outline:none;border-color:var(--ytkit-accent);} .ytkit-bulk-bar{animation:slideDown 0.2s ease-out;} @keyframes slideDown{from{opacity:0;transform:translateY(-10px);} to{opacity:1;transform:translateY(0);} }`);
}
// SECTION 6: BOOTSTRAP
let _mainRan = false;
function main() {
if (_mainRan) return; // Guard against double-init (YouTube SPA can re-trigger)
_mainRan = true;
appState.settings = settingsManager.load();
appState.currentPage = getCurrentPage();
injectPanelStyles();
// Page Feature Dock — per-page floating toggle strip
// Page Quick Settings Modal
// A per-page context modal opened by a dedicated button next to the gear.
// Per-page feature config: id → label override (null = use feature.name)
const PAGE_MODAL_CONFIG = {
home: [
{ id: 'videosPerRow', label: 'Videos Per Row' },
{ id: 'hideNewsHome', label: 'Hide News' },
{ id: 'hidePlaylistsHome', label: 'Hide Playlists' },
],
subs: [
{ id: 'subscriptionsGrid', label: 'Dense Grid' },
{ id: 'fullWidthSubscriptions', label: 'Full Width' },
],
watch: [
{ id: 'stickyVideo', label: 'Theater Split' },
{ id: 'fitPlayerToWindow', label: 'Fit to Window' },
{ id: 'expandVideoWidth', label: 'Expand Width' },
{ id: 'skipSponsors', label: 'SponsorBlock' },
{ id: 'autoMaxResolution', label: 'Max Resolution' },
],
channel: [
{ id: 'redirectToVideosTab', label: 'Auto Videos Tab' },
],
};
const PAGE_LABELS = {
home: 'Home',
subs: 'Subscriptions',
watch: 'Watch',
channel: 'Channel',
};
const PAGE_MODAL_PAGE_MAP = {
[PageTypes.HOME]: 'home',
[PageTypes.SUBSCRIPTIONS]: 'subs',
[PageTypes.WATCH]: 'watch',
[PageTypes.CHANNEL]: 'channel',
};
let _pageModalOpen = false;
let _pageModalEl = null;
let _pageModalOverlay = null;
function closePageModal() {
if (!_pageModalOpen) return;
_pageModalOpen = false;
if (_pageModalEl) {
_pageModalEl.classList.remove('ytkit-pm-visible');
setTimeout(() => _pageModalEl?.remove(), 220);
_pageModalEl = null;
}
if (_pageModalOverlay) {
_pageModalOverlay.classList.remove('ytkit-pm-ov-visible');
setTimeout(() => _pageModalOverlay?.remove(), 220);
_pageModalOverlay = null;
}
document.querySelector('#ytkit-page-btn')?.classList.remove('active');
document.querySelector('#ytkit-page-btn-watch')?.classList.remove('active');
}
function openPageModal() {
if (_pageModalOpen) { closePageModal(); return; }
const pt = getCurrentPage();
const pageKey = PAGE_MODAL_PAGE_MAP[pt];
const featureList = pageKey ? (PAGE_MODAL_CONFIG[pageKey] || []) : [];
if (!featureList.length) return;
_pageModalOpen = true;
document.querySelector('#ytkit-page-btn')?.classList.add('active');
document.querySelector('#ytkit-page-btn-watch')?.classList.add('active');
// Overlay
const ov = document.createElement('div');
ov.className = 'ytkit-pm-overlay';
ov.addEventListener('click', closePageModal);
document.body.appendChild(ov);
_pageModalOverlay = ov;
requestAnimationFrame(() => ov.classList.add('ytkit-pm-ov-visible'));
// Modal panel
const modal = document.createElement('div');
modal.id = 'ytkit-page-modal';
modal.className = 'ytkit-pm';
modal.addEventListener('click', e => e.stopPropagation());
// Header
const header = document.createElement('div');
header.className = 'ytkit-pm-header';
const titleWrap = document.createElement('div');
titleWrap.className = 'ytkit-pm-title-wrap';
const pageBadge = document.createElement('span');
pageBadge.className = 'ytkit-pm-badge';
pageBadge.textContent = PAGE_LABELS[pageKey] || pageKey;
const titleText = document.createElement('h3');
titleText.className = 'ytkit-pm-title';
titleText.textContent = 'Quick Settings';
titleWrap.appendChild(pageBadge);
titleWrap.appendChild(titleText);
const closeBtn = document.createElement('button');
closeBtn.className = 'ytkit-pm-close';
closeBtn.title = 'Close';
closeBtn.appendChild(ICONS.close());
closeBtn.addEventListener('click', closePageModal);
header.appendChild(titleWrap);
header.appendChild(closeBtn);
modal.appendChild(header);
// Feature grid
const grid = document.createElement('div');
grid.className = 'ytkit-pm-grid';
featureList.forEach(({ id: fid, label }) => {
const feat = features.find(f => f.id === fid);
if (!feat) return;
const isOn = !!appState.settings[fid];
const card = document.createElement('button');
card.className = 'ytkit-pm-card' + (isOn ? ' on' : '');
card.dataset.fid = fid;
// Icon area
const iconWrap = document.createElement('div');
iconWrap.className = 'ytkit-pm-card-icon';
const iconFn = ICONS[feat.icon] || ICONS[feat.group?.toLowerCase()] || ICONS.settings;
iconWrap.appendChild(iconFn());
// Text
const textWrap = document.createElement('div');
textWrap.className = 'ytkit-pm-card-text';
const cardLabel = document.createElement('span');
cardLabel.className = 'ytkit-pm-card-label';
cardLabel.textContent = label || feat.name;
const cardDesc = document.createElement('span');
cardDesc.className = 'ytkit-pm-card-desc';
// Keep description short
const desc = feat.description || '';
cardDesc.textContent = desc.length > 72 ? desc.slice(0, 70) + '…' : desc;
textWrap.appendChild(cardLabel);
textWrap.appendChild(cardDesc);
// Toggle indicator
const toggle = document.createElement('div');
toggle.className = 'ytkit-pm-card-toggle';
const toggleTrack = document.createElement('div');
toggleTrack.className = 'ytkit-pm-toggle-track';
const toggleThumb = document.createElement('div');
toggleThumb.className = 'ytkit-pm-toggle-thumb';
toggleTrack.appendChild(toggleThumb);
toggle.appendChild(toggleTrack);
card.appendChild(iconWrap);
card.appendChild(textWrap);
card.appendChild(toggle);
card.addEventListener('click', () => {
const newVal = !appState.settings[fid];
appState.settings[fid] = newVal;
settingsManager.save(appState.settings);
try { newVal ? feat.init?.() : feat.destroy?.(); } catch(e) {}
card.classList.toggle('on', newVal);
// Update all matching dock pills if any remain
document.querySelectorAll(`.ytkit-dock-pill[data-fid="${fid}"]`).forEach(p => p.classList.toggle('on', newVal));
});
grid.appendChild(card);
});
modal.appendChild(grid);
// Footer: link to full settings
const footer = document.createElement('div');
footer.className = 'ytkit-pm-footer';
const fullBtn = document.createElement('button');
fullBtn.className = 'ytkit-pm-full-settings';
fullBtn.textContent = 'Open Full Settings →';
fullBtn.addEventListener('click', () => {
closePageModal();
document.body.classList.add('ytkit-panel-open');
});
footer.appendChild(fullBtn);
modal.appendChild(footer);
document.body.appendChild(modal);
_pageModalEl = modal;
requestAnimationFrame(() => modal.classList.add('ytkit-pm-visible'));
// Close on navigation
document.addEventListener('yt-navigate-start', closePageModal, { once: true });
}
function injectPageModalButton() {
const handleDisplay = () => {
const isWatch = window.location.pathname.startsWith('/watch');
// Clean up wrong-context button
if (isWatch) {
document.getElementById('ytkit-page-btn')?.remove();
} else {
document.getElementById('ytkit-page-btn-watch')?.remove();
}
const btnId = isWatch ? 'ytkit-page-btn-watch' : 'ytkit-page-btn';
if (document.getElementById(btnId)) return;
const pt = getCurrentPage();
const pageKey = PAGE_MODAL_PAGE_MAP[pt];
if (!pageKey || !PAGE_MODAL_CONFIG[pageKey]?.length) return;
const btn = document.createElement('button');
btn.id = btnId;
btn.className = 'ytkit-trigger-btn ytkit-page-trigger';
btn.title = 'YTKit Page Settings';
// Sliders icon
const svg = createSVG('0 0 24 24', [
{ type: 'line', x1: 4, y1: 21, x2: 4, y2: 14 },
{ type: 'line', x1: 4, y1: 10, x2: 4, y2: 3 },
{ type: 'line', x1: 12, y1: 21, x2: 12, y2: 12 },
{ type: 'line', x1: 12, y1: 8, x2: 12, y2: 3 },
{ type: 'line', x1: 20, y1: 21, x2: 20, y2: 16 },
{ type: 'line', x1: 20, y1: 12, x2: 20, y2: 3 },
{ type: 'line', x1: 1, y1: 14, x2: 7, y2: 14 },
{ type: 'line', x1: 9, y1: 8, x2: 15, y2: 8 },
{ type: 'line', x1: 17, y1: 16, x2: 23, y2: 16 },
], { strokeWidth: '2', strokeLinecap: 'round' });
btn.appendChild(svg);
btn.addEventListener('click', openPageModal);
if (isWatch) {
waitForElement('#top-row #owner', (ownerDiv) => {
if (document.getElementById(btnId)) return;
// Place right after the gear button
const gear = document.getElementById('ytkit-watch-btn');
if (gear) gear.after(btn);
else ownerDiv.prepend(btn);
});
} else {
waitForElement('ytd-masthead #end', (mastheadEnd) => {
if (document.getElementById(btnId)) return;
// Place right before the gear button
const gear = document.getElementById('ytkit-masthead-btn');
if (gear) mastheadEnd.insertBefore(btn, gear);
else mastheadEnd.prepend(btn);
});
}
};
addNavigateRule('_pageModalBtnRule', handleDisplay);
// Close modal on click-away (Escape key)
document.addEventListener('keydown', e => { if (e.key === 'Escape' && _pageModalOpen) closePageModal(); });
}
GM_addStyle(`.ytkit-pm-overlay{position:fixed;inset:0;z-index:99990;background:rgba(0,0,0,0);transition:background 0.2s ease;pointer-events:none;} .ytkit-pm-ov-visible{background:rgba(0,0,0,0.55);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);pointer-events:auto;} .ytkit-pm{position:fixed;top:60px;right:16px;width:400px;max-height:calc(100vh - 80px);overflow-y:auto;z-index:99991;background:linear-gradient(145deg,rgba(18,18,28,0.98),rgba(12,12,20,0.98));border:1px solid rgba(255,255,255,0.08);border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,0.7),0 0 0 1px rgba(255,255,255,0.04) inset;font-family:"Roboto",Arial,sans-serif;color:var(--yt-spec-text-primary,#fff);opacity:0;transform:translateY(-8px) scale(0.98);transition:opacity 0.2s ease,transform 0.2s ease;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,0.1) transparent;} .ytkit-pm::-webkit-scrollbar{width:4px;} .ytkit-pm::-webkit-scrollbar-track{background:transparent;} .ytkit-pm::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.12);border-radius:2px;} .ytkit-pm-visible{opacity:1;transform:translateY(0) scale(1);} .ytkit-pm-header{display:flex;align-items:center;justify-content:space-between;padding:16px 16px 12px;border-bottom:1px solid rgba(255,255,255,0.06);} .ytkit-pm-title-wrap{display:flex;align-items:center;gap:10px;} .ytkit-pm-badge{font-size:10px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#3b82f6;background:rgba(59,130,246,0.12);border:1px solid rgba(59,130,246,0.25);border-radius:6px;padding:2px 8px;} .ytkit-pm-title{margin:0;font-size:15px;font-weight:600;color:rgba(255,255,255,0.9);} .ytkit-pm-close{width:30px;height:30px;border-radius:8px;border:none;background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.5);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all 0.15s;flex-shrink:0;} .ytkit-pm-close:hover{background:rgba(255,255,255,0.12);color:#fff;} .ytkit-pm-close svg{width:14px;height:14px;} .ytkit-pm-grid{display:flex;flex-direction:column;gap:2px;padding:8px;} .ytkit-pm-card{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:10px;border:none;background:transparent;cursor:pointer;text-align:left;transition:background 0.15s;width:100%;} .ytkit-pm-card:hover{background:rgba(255,255,255,0.05);} .ytkit-pm-card.on{background:rgba(59,130,246,0.08);} .ytkit-pm-card.on:hover{background:rgba(59,130,246,0.13);} .ytkit-pm-card-icon{width:34px;height:34px;border-radius:9px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.45);transition:all 0.15s;} .ytkit-pm-card.on .ytkit-pm-card-icon{background:rgba(59,130,246,0.18);color:#60a5fa;} .ytkit-pm-card-icon svg{width:16px;height:16px;} .ytkit-pm-card-text{flex:1;min-width:0;} .ytkit-pm-card-label{display:block;font-size:13px;font-weight:500;color:rgba(255,255,255,0.8);line-height:1.3;} .ytkit-pm-card.on .ytkit-pm-card-label{color:#e0eaff;} .ytkit-pm-card-desc{display:block;font-size:11px;color:rgba(255,255,255,0.35);line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px;} .ytkit-pm-card-toggle{flex-shrink:0;} .ytkit-pm-toggle-track{width:34px;height:18px;border-radius:9px;background:rgba(255,255,255,0.1);position:relative;transition:background 0.2s;} .ytkit-pm-card.on .ytkit-pm-toggle-track{background:#2563eb;} .ytkit-pm-toggle-thumb{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:rgba(255,255,255,0.5);transition:transform 0.2s,background 0.2s;} .ytkit-pm-card.on .ytkit-pm-toggle-thumb{transform:translateX(16px);background:#fff;} .ytkit-pm-footer{padding:10px 16px 14px;border-top:1px solid rgba(255,255,255,0.06);} .ytkit-pm-full-settings{width:100%;padding:9px 16px;border-radius:8px;border:none;background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.55);font-size:12px;font-weight:500;cursor:pointer;transition:all 0.15s;letter-spacing:0.2px;} .ytkit-pm-full-settings:hover{background:rgba(255,255,255,0.1);color:rgba(255,255,255,0.85);} .ytkit-page-trigger.active svg{color:#3b82f6 !important;transform:none !important;} .ytkit-page-trigger:hover svg{transform:none !important;}`);
function buildPageDock() {
// Dock replaced by page modal — no-op kept for call-site compatibility
}
buildSettingsPanel();
injectSettingsButton();
buildPageDock();
injectPageModalButton();
attachUIEventListeners();
updateAllToggleStates();
// ── Safe Mode + Diagnostics ──
const isSafeMode = new URLSearchParams(window.location.search).get('ytkit') === 'safe' ||
GM_getValue('ytkit_safe_mode', false);
window.ytkit = {
safe() { GM_setValue('ytkit_safe_mode', true); location.reload(); },
unsafe() { GM_setValue('ytkit_safe_mode', false); location.reload(); },
debug(on) {
if (on === undefined) return DebugManager._enabled;
on ? DebugManager.enable() : DebugManager.disable();
console.log('[YTKit] Debug ' + (on ? 'enabled' : 'disabled'));
},
stats() {
const ab = _rw.__ytab?.stats || {};
console.table({ 'Ads Blocked': ab.blocked || 0, 'Responses Pruned': ab.pruned || 0, 'SSAP Skipped': ab.ssapSkipped || 0 });
return ab;
},
diagCSS() {
document.getElementById('ytab-cosmetic')?.remove();
document.getElementById('ytkit-opened-fix')?.remove();
console.log('[YTKit Diag] Removed ad-blocker cosmetic CSS + .opened fix');
},
diagAdblock(enable = false) {
GM_setValue('ytab_enabled', enable);
console.log(`[YTKit Diag] Ad blocker ${enable ? 'enabled' : 'disabled'} — reloading...`);
location.reload();
},
testOnly(id) {
const s = { ...appState.settings };
features.forEach(f => { if (!f._arrayKey) s[f.id] = false; });
s[id] = true;
settingsManager.save(s);
GM_setValue('ytkit_safe_mode', false);
location.reload();
},
disableAll() {
const s = { ...appState.settings };
features.forEach(f => { if (!f._arrayKey) s[f.id] = false; });
settingsManager.save(s);
location.reload();
},
list() {
const enabled = [], disabled = [];
features.forEach(f => {
if (f._arrayKey) return;
(appState.settings[f.id] ? enabled : disabled).push(f.id);
});
console.log(`%c[YTKit] ${enabled.length} enabled:`, 'color:#22c55e;font-weight:bold');
enabled.forEach(id => console.log(` ✓ ${id}`));
console.log(`%c[YTKit] ${disabled.length} disabled:`, 'color:#ef4444;font-weight:bold');
disabled.forEach(id => console.log(` ✗ ${id}`));
return { enabled, disabled };
},
settings: appState.settings,
features,
version: '1.0.0',
};
if (isSafeMode) {
console.log('%c[YTKit] SAFE MODE — All features disabled. ytkit.unsafe() to exit.', 'color:#f97316;font-weight:bold;font-size:16px;');
showToast('SAFE MODE — All features disabled. Console: ytkit.unsafe() to exit.', '#f97316', { duration: 10 });
} else {
// TIER 0: Critical — adblock, cosmetics, CSS-only, Theater Split.
// Must run synchronously before any page content paints.
// TIER 1: Normal — all other non-watch-page-specific features.
// Run in rAF to avoid blocking first paint.
// TIER 2: Watch-page-only — heavy features that aren't needed until
// the video is playing. Deferred 1500ms via requestIdleCallback.
const CRITICAL_IDS = new Set([
'ytAdBlock','adblockCosmeticHide','adblockSsapAutoSkip','adblockAntiDetect',
'stickyVideo','uiStyleManager',
]);
const LAZY_IDS = new Set([
// Only defer watch-page-only features that are heavy or network-bound
'skipSponsors',
'autoResumePosition','chapterProgressBar',
]);
const initFeature = (f) => {
if (f._arrayKey) return;
const isEnabled = (f.type === 'select' || f.type === 'color' || f.type === 'range')
? true : appState.settings[f.id];
if (!isEnabled) return;
if (f.pages && !f.pages.includes(appState.currentPage)) return;
if (f.dependsOn && !appState.settings[f.dependsOn]) return;
if (f._initialized) return;
try { f.init?.(); f._initialized = true; } catch(err) {
console.error(`[YTKit] Error initializing "${f.id}":`, err);
}
};
const critLog = [], normalLog = [], lazyLog = [];
const normal = [], lazy = [];
features.forEach(f => {
if (CRITICAL_IDS.has(f.id)) { initFeature(f); critLog.push(f.id); }
else if (LAZY_IDS.has(f.id)) lazy.push(f);
else normal.push(f);
});
// Tier 1: after first paint
requestAnimationFrame(() => {
normal.forEach(f => { initFeature(f); if (f._initialized) normalLog.push(f.id); });
console.log(`[YTKit] v1.0.0 | critical:${critLog.length} normal:${normalLog.length} (lazy pending)`);
});
// Tier 2: after page is interactive
const lazyInit = () => {
lazy.forEach(f => { initFeature(f); if (f._initialized) lazyLog.push(f.id); });
if (lazyLog.length) DebugManager.log('Init', `Lazy loaded: ${lazyLog.join(', ')}`);
};
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(lazyInit, { timeout: 2000 });
} else {
setTimeout(lazyInit, 1500);
}
}
// Show sub-features for enabled parents
document.querySelectorAll('.ytkit-sub-features').forEach(container => {
const parentId = container.dataset.parentId;
if (appState.settings[parentId]) {
container.style.display = '';
}
});
// Button injection is handled by startButtonChecker() called from each button feature's init()
const hasRun = settingsManager.getFirstRunStatus();
if (!hasRun) {
settingsManager.setFirstRunStatus(true);
}
// Track page changes for lazy loading (skip in safe mode)
if (!isSafeMode) {
document.addEventListener('yt-navigate-finish', () => {
const newPage = getCurrentPage();
if (newPage !== appState.currentPage) {
const oldPage = appState.currentPage;
appState.currentPage = newPage;
DebugManager.log('Navigation', `Page changed: ${oldPage} -> ${newPage}`);
// Re-initialize features that are page-specific
features.forEach(f => {
if (f._arrayKey) return;
const isEnabled = (f.type === 'select' || f.type === 'color' || f.type === 'range')
? true
: appState.settings[f.id];
if (isEnabled && f.pages) {
const wasActive = f.pages.includes(oldPage);
const shouldBeActive = f.pages.includes(newPage);
if (!wasActive && shouldBeActive && !f._initialized) {
try { f.init?.(); f._initialized = true; } catch(e) {}
} else if (wasActive && !shouldBeActive && f._initialized) {
try { f.destroy?.(); f._initialized = false; } catch(e) {}
}
}
});
}
});
} // end !isSafeMode
console.log(`%c[YTKit] v1.0.0 Initialized${isSafeMode ? ' (SAFE MODE)' : ''}`, 'color: #3b82f6; font-weight: bold; font-size: 14px;');
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
main();
} else {
window.addEventListener('DOMContentLoaded', main, { once: true });
}
})();