// ==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 }); } })();