// ==UserScript==
// @name YTKit: YouTube Customization Suite
// @namespace https://github.com/SysAdminDoc/YTKit
// @version 16
// @description Ultimate YouTube customization with ad blocking, VLC streaming, video/channel hiding, playback enhancements, sticky video, and more. Uses YTYT-Downloader for local downloads.
// @author Matthew Parker
// @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_getResourceText
// @grant GM_notification
// @grant GM_download
// @grant GM.xmlHttpRequest
// @grant GM_addStyle
// @grant GM_openInTab
// @connect sponsor.ajay.app
// @connect raw.githubusercontent.com
// @resource betterDarkMode https://github.com/SysAdminDoc/YTKit/raw/refs/heads/main/Themes/youtube-dark-theme.css
// @resource catppuccinMocha https://github.com/SysAdminDoc/YTKit/raw/refs/heads/main/Themes/youtube-catppuccin-theme.css
// @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)) {
console.log('[YTKit] 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;
console.log(`[YTKit] 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));
startButtonChecker();
},
destroy() {
unregisterPersistentButton('vlcButton');
document.querySelector('.ytkit-vlc-btn')?.remove();
}
},
{
id: 'showVlcQueueButton',
name: 'VLC Queue Button',
description: 'Add button to queue video in VLC (plays after current)',
group: 'Downloads',
icon: 'list-plus',
_createButton(parent) {
const btn = document.createElement('button');
btn.className = 'ytkit-vlc-queue-btn';
btn.title = 'Add to VLC Queue (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');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'white');
svg.setAttribute('stroke-width', '2');
// List icon with plus
const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line1.setAttribute('x1', '8'); line1.setAttribute('y1', '6');
line1.setAttribute('x2', '21'); line1.setAttribute('y2', '6');
const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line2.setAttribute('x1', '8'); line2.setAttribute('y1', '12');
line2.setAttribute('x2', '21'); line2.setAttribute('y2', '12');
const line3 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line3.setAttribute('x1', '8'); line3.setAttribute('y1', '18');
line3.setAttribute('x2', '21'); line3.setAttribute('y2', '18');
const plus1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
plus1.setAttribute('x1', '3'); plus1.setAttribute('y1', '12');
plus1.setAttribute('x2', '3'); plus1.setAttribute('y2', '12');
plus1.setAttribute('stroke-linecap', 'round');
const circle1 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle1.setAttribute('cx', '3'); circle1.setAttribute('cy', '6'); circle1.setAttribute('r', '1');
circle1.setAttribute('fill', 'white');
const circle2 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle2.setAttribute('cx', '3'); circle2.setAttribute('cy', '12'); circle2.setAttribute('r', '1');
circle2.setAttribute('fill', 'white');
const circle3 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle3.setAttribute('cx', '3'); circle3.setAttribute('cy', '18'); circle3.setAttribute('r', '1');
circle3.setAttribute('fill', 'white');
svg.appendChild(line1); svg.appendChild(line2); svg.appendChild(line3);
svg.appendChild(circle1); svg.appendChild(circle2); svg.appendChild(circle3);
btn.appendChild(svg);
btn.appendChild(document.createTextNode(' +Q'));
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:#ea580c;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 = '#c2410c'; };
btn.onmouseleave = () => { btn.style.background = '#ea580c'; };
btn.addEventListener('click', () => {
showToast('📋 Adding to VLC queue...', '#ea580c');
window.location.href = 'ytvlcq://' + encodeURIComponent(window.location.href);
});
parent.appendChild(btn);
},
init() {
registerPersistentButton('vlcQueueButton', '#top-level-buttons-computed', '.ytkit-vlc-queue-btn', this._createButton.bind(this));
startButtonChecker();
},
destroy() {
unregisterPersistentButton('vlcQueueButton');
document.querySelector('.ytkit-vlc-queue-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));
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));
startButtonChecker();
},
destroy() {
unregisterPersistentButton('mp3DownloadButton');
document.querySelector('.ytkit-mp3-dl-btn')?.remove();
}
},
{
id: 'showTranscriptButton',
name: 'Download Transcript Button',
description: 'Add button to download video transcript/captions as a text file',
group: 'Downloads',
icon: 'file-text',
async _downloadTranscript() {
await TranscriptService.downloadTranscript();
},
_createButton(parent) {
const btn = document.createElement('button');
btn.className = 'ytkit-transcript-btn';
btn.title = 'Download Transcript';
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');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'white');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
// File-text icon
const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path1.setAttribute('d', 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z');
const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
polyline.setAttribute('points', '14 2 14 8 20 8');
const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line1.setAttribute('x1', '16'); line1.setAttribute('y1', '13');
line1.setAttribute('x2', '8'); line1.setAttribute('y2', '13');
const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line2.setAttribute('x1', '16'); line2.setAttribute('y1', '17');
line2.setAttribute('x2', '8'); line2.setAttribute('y2', '17');
svg.appendChild(path1);
svg.appendChild(polyline);
svg.appendChild(line1);
svg.appendChild(line2);
btn.appendChild(svg);
btn.appendChild(document.createTextNode(' CC'));
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:#3b82f6;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 = '#2563eb'; };
btn.onmouseleave = () => { btn.style.background = '#3b82f6'; };
btn.addEventListener('click', () => this._downloadTranscript());
parent.appendChild(btn);
},
init() {
registerPersistentButton('transcriptButton', '#top-level-buttons-computed', '.ytkit-transcript-btn', this._createButton.bind(this));
startButtonChecker();
},
destroy() {
unregisterPersistentButton('transcriptButton');
document.querySelector('.ytkit-transcript-btn')?.remove();
}
},
{
id: 'showMpvButton',
name: 'MPV Player Button',
description: 'Add button to stream video in MPV player (for advanced users)',
group: 'Downloads',
icon: 'clapperboard',
_createButton(parent) {
const btn = document.createElement('button');
btn.className = 'ytkit-mpv-btn';
btn.title = 'Stream in MPV Player';
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', 'M4 8V4h16v4M12 4v16M8 20h8');
path.setAttribute('stroke', 'white');
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'none');
svg.appendChild(path);
btn.appendChild(svg);
btn.appendChild(document.createTextNode(' MPV'));
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('🎬 Sending to MPV...', '#8b5cf6');
window.location.href = 'ytmpv://' + encodeURIComponent(window.location.href);
});
parent.appendChild(btn);
},
init() {
registerPersistentButton('mpvButton', '#top-level-buttons-computed', '.ytkit-mpv-btn', this._createButton.bind(this));
startButtonChecker();
},
destroy() {
unregisterPersistentButton('mpvButton');
document.querySelector('.ytkit-mpv-btn')?.remove();
}
},
{
id: 'autoDownloadOnVisit',
name: 'Auto-Download Videos',
description: 'Automatically start download when visiting a video page',
group: 'Downloads',
icon: 'download',
_lastDownloaded: null,
_handleNavigation() {
if (!window.location.pathname.startsWith('/watch')) return;
const videoId = new URLSearchParams(window.location.search).get('v');
if (!videoId || videoId === this._lastDownloaded) return;
this._lastDownloaded = videoId;
// Small delay to let page load
setTimeout(() => {
const videoUrl = window.location.href;
console.log('[YTKit] Auto-downloading:', videoUrl);
window.location.href = 'ytdl://' + encodeURIComponent(videoUrl);
}, 2000);
},
init() {
addNavigateRule('autoDownload', this._handleNavigation.bind(this));
},
destroy() {
removeNavigateRule('autoDownload');
}
},
{
id: 'downloadQuality',
name: 'Download Quality',
description: 'Preferred video quality for downloads',
group: 'Downloads',
icon: 'settings-2',
type: 'select',
options: {
'2160': '4K (2160p)',
'1440': '2K (1440p)',
'1080': 'Full HD (1080p)',
'720': 'HD (720p)',
'480': 'SD (480p)',
'best': 'Best Available'
},
init() {},
destroy() {}
},
{
id: 'preferredMediaPlayer',
name: 'Preferred Media Player',
description: 'Default player for streaming videos',
group: 'Downloads',
icon: 'monitor-play',
type: 'select',
options: {
'vlc': 'VLC Media Player',
'mpv': 'MPV',
'potplayer': 'PotPlayer',
'mpc-hc': 'MPC-HC'
},
init() {},
destroy() {}
},
{
id: 'showDownloadPlayButton',
name: 'Download & Play Button',
description: 'Download video first, then open in VLC (better quality, works offline)',
group: 'Downloads',
icon: 'download',
_createButton(parent) {
const btn = document.createElement('button');
btn.className = 'ytkit-dlplay-btn';
btn.title = 'Download & Play in VLC';
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 2v8l3-3m-3 3l-3-3m-4 8a9 9 0 1018 0');
path.setAttribute('stroke', 'white');
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'none');
svg.appendChild(path);
btn.appendChild(svg);
btn.appendChild(document.createTextNode(' DL+Play'));
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:linear-gradient(135deg,#22c55e,#f97316);color:white;font-family:"Roboto","Arial",sans-serif;font-size:14px;font-weight:500;cursor:pointer;transition:opacity 0.2s;`;
btn.onmouseenter = () => { btn.style.opacity = '0.8'; };
btn.onmouseleave = () => { btn.style.opacity = '1'; };
btn.addEventListener('click', () => {
showToast('⬇️ Downloading & preparing to play...', '#22c55e');
window.location.href = 'ytdlplay://' + encodeURIComponent(window.location.href);
});
parent.appendChild(btn);
},
init() {
registerPersistentButton('downloadPlayButton', '#top-level-buttons-computed', '.ytkit-dlplay-btn', this._createButton.bind(this));
startButtonChecker();
},
destroy() {
unregisterPersistentButton('downloadPlayButton');
document.querySelector('.ytkit-dlplay-btn')?.remove();
}
},
{
id: 'subsVlcPlaylist',
name: 'Subscriptions VLC Button',
description: 'Add button on subscriptions page to queue all videos to VLC playlist',
group: 'Downloads',
icon: 'list-video',
_queuedVideos: new Set(),
_styleElement: null,
_getQueuedVideos() {
try {
const stored = localStorage.getItem('ytkit-queued-videos');
return stored ? new Set(JSON.parse(stored)) : new Set();
} catch {
return new Set();
}
},
_saveQueuedVideos() {
try {
localStorage.setItem('ytkit-queued-videos', JSON.stringify([...this._queuedVideos]));
} catch {}
},
_markVideoQueued(videoId, element) {
this._queuedVideos.add(videoId);
this._saveQueuedVideos();
if (element) {
element.classList.add('ytkit-video-queued');
// Add overlay badge
if (!element.querySelector('.ytkit-queued-badge')) {
const badge = document.createElement('div');
badge.className = 'ytkit-queued-badge';
badge.textContent = '✓ Queued';
const thumbnail = element.querySelector('ytd-thumbnail, #thumbnail');
if (thumbnail) {
thumbnail.style.position = 'relative';
thumbnail.appendChild(badge);
}
}
}
},
_isVideoQueued(videoId) {
return this._queuedVideos.has(videoId);
},
_getAllVideosOnPage() {
const videos = [];
// Find all video renderers on subscriptions page
const selectors = [
'ytd-rich-item-renderer',
'ytd-grid-video-renderer',
'ytd-video-renderer'
];
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(item => {
const link = item.querySelector('a#thumbnail, a.ytd-thumbnail');
if (link && link.href && link.href.includes('/watch?v=')) {
const match = link.href.match(/[?&]v=([^&]+)/);
if (match) {
videos.push({
id: match[1],
url: link.href,
element: item
});
}
}
});
});
return videos;
},
async _queueAllVideos() {
const videos = this._getAllVideosOnPage();
const unqueuedVideos = videos.filter(v => !this._isVideoQueued(v.id));
if (unqueuedVideos.length === 0) {
showToast('✅ All videos already queued!', '#22c55e');
return;
}
showToast(`📋 Queueing ${unqueuedVideos.length} videos to VLC...`, '#f97316');
// Queue videos with small delay between each
for (let i = 0; i < unqueuedVideos.length; i++) {
const video = unqueuedVideos[i];
// Mark as queued visually
this._markVideoQueued(video.id, video.element);
// Send to VLC queue
window.location.href = 'ytvlcq://' + encodeURIComponent(video.url);
// Small delay to allow protocol handler to process
if (i < unqueuedVideos.length - 1) {
await new Promise(r => setTimeout(r, 500));
}
}
showToast(`✅ Queued ${unqueuedVideos.length} videos to VLC!`, '#22c55e');
},
_clearQueueMarks() {
this._queuedVideos.clear();
this._saveQueuedVideos();
document.querySelectorAll('.ytkit-video-queued').forEach(el => {
el.classList.remove('ytkit-video-queued');
});
document.querySelectorAll('.ytkit-queued-badge').forEach(el => el.remove());
showToast('🗑️ Queue marks cleared', '#6b7280');
},
_applyQueuedMarks() {
const videos = this._getAllVideosOnPage();
videos.forEach(video => {
if (this._isVideoQueued(video.id)) {
this._markVideoQueued(video.id, video.element);
}
});
},
_createButton() {
if (document.querySelector('.ytkit-subs-vlc-btn')) return;
// Find the header area on subscriptions page
const headerContainer = document.querySelector('#title-container, #page-header, ytd-page-manager #header');
const buttonContainer = document.querySelector('#buttons, #header-buttons, #start #buttons');
// Try to find a suitable container
let container = buttonContainer || headerContainer;
if (!container) {
// Create our own container near the title
const title = document.querySelector('yt-page-header-renderer, #page-header');
if (title) {
container = document.createElement('div');
container.className = 'ytkit-subs-btn-container';
container.style.cssText = 'display:flex;gap:8px;margin-left:auto;padding:8px 16px;';
title.appendChild(container);
}
}
if (!container) return;
// Helper to create SVG elements
const ns = 'http://www.w3.org/2000/svg';
const createSvgElement = (tag, attrs) => {
const el = document.createElementNS(ns, tag);
for (const [k, v] of Object.entries(attrs)) {
el.setAttribute(k, v);
}
return el;
};
// Queue All button
const queueBtn = document.createElement('button');
queueBtn.className = 'ytkit-subs-vlc-btn';
queueBtn.title = 'Add all subscription videos to VLC queue';
// Build SVG using DOM
const queueSvg = createSvgElement('svg', { viewBox: '0 0 24 24', width: '20', height: '20', fill: 'none', stroke: 'currentColor', 'stroke-width': '2' });
queueSvg.appendChild(createSvgElement('line', { x1: '8', y1: '6', x2: '21', y2: '6' }));
queueSvg.appendChild(createSvgElement('line', { x1: '8', y1: '12', x2: '21', y2: '12' }));
queueSvg.appendChild(createSvgElement('line', { x1: '8', y1: '18', x2: '21', y2: '18' }));
const c1 = createSvgElement('circle', { cx: '3', cy: '6', r: '1.5' }); c1.setAttribute('fill', 'currentColor'); queueSvg.appendChild(c1);
const c2 = createSvgElement('circle', { cx: '3', cy: '12', r: '1.5' }); c2.setAttribute('fill', 'currentColor'); queueSvg.appendChild(c2);
const c3 = createSvgElement('circle', { cx: '3', cy: '18', r: '1.5' }); c3.setAttribute('fill', 'currentColor'); queueSvg.appendChild(c3);
queueBtn.appendChild(queueSvg);
const queueText = document.createElement('span');
queueText.textContent = 'Queue All to VLC';
queueBtn.appendChild(queueText);
queueBtn.style.cssText = `
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 20px;
border: none;
background: #f97316;
color: white;
font-family: "Roboto", Arial, sans-serif;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
`;
queueBtn.onmouseenter = () => { queueBtn.style.background = '#ea580c'; };
queueBtn.onmouseleave = () => { queueBtn.style.background = '#f97316'; };
queueBtn.addEventListener('click', () => this._queueAllVideos());
// Clear button
const clearBtn = document.createElement('button');
clearBtn.className = 'ytkit-subs-clear-btn';
clearBtn.title = 'Clear queue marks';
// Build clear SVG using DOM
const clearSvg = createSvgElement('svg', { viewBox: '0 0 24 24', width: '18', height: '18', fill: 'none', stroke: 'currentColor', 'stroke-width': '2' });
clearSvg.appendChild(createSvgElement('path', { d: 'M3 6h18' }));
clearSvg.appendChild(createSvgElement('path', { d: 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6' }));
clearSvg.appendChild(createSvgElement('path', { d: 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' }));
clearBtn.appendChild(clearSvg);
clearBtn.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: rgba(255,255,255,0.1);
color: white;
cursor: pointer;
transition: background 0.2s;
`;
clearBtn.onmouseenter = () => { clearBtn.style.background = 'rgba(255,255,255,0.2)'; };
clearBtn.onmouseleave = () => { clearBtn.style.background = 'rgba(255,255,255,0.1)'; };
clearBtn.addEventListener('click', () => this._clearQueueMarks());
container.appendChild(queueBtn);
container.appendChild(clearBtn);
},
_injectStyles() {
if (this._styleElement) return;
this._styleElement = document.createElement('style');
this._styleElement.textContent = `
.ytkit-video-queued ytd-thumbnail,
.ytkit-video-queued #thumbnail {
opacity: 0.6;
}
.ytkit-video-queued::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(249, 115, 22, 0.1);
pointer-events: none;
}
.ytkit-queued-badge {
position: absolute;
top: 8px;
left: 8px;
background: #22c55e;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
font-family: "Roboto", Arial, sans-serif;
z-index: 100;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.ytkit-subs-btn-container {
position: fixed;
top: 56px;
right: 24px;
z-index: 1000;
display: flex;
gap: 8px;
}
`;
document.head.appendChild(this._styleElement);
},
init() {
this._queuedVideos = this._getQueuedVideos();
this._injectStyles();
// Only activate on subscriptions page
const checkAndCreate = () => {
if (window.location.pathname === '/feed/subscriptions') {
setTimeout(() => {
this._createButton();
this._applyQueuedMarks();
}, 1000);
}
};
// Check on navigation
document.addEventListener('yt-navigate-finish', checkAndCreate);
checkAndCreate();
// Re-apply marks when new content loads
const observer = new MutationObserver(() => {
if (window.location.pathname === '/feed/subscriptions') {
this._applyQueuedMarks();
}
});
observer.observe(document.body, { childList: true, subtree: true });
this._observer = observer;
},
destroy() {
this._styleElement?.remove();
this._observer?.disconnect();
document.querySelector('.ytkit-subs-vlc-btn')?.remove();
document.querySelector('.ytkit-subs-clear-btn')?.remove();
document.querySelectorAll('.ytkit-queued-badge').forEach(el => el.remove());
}
},
{
id: 'enableEmbedPlayer',
name: 'Embed Player (Beta)',
description: 'Replace YouTube player with custom HTML5 player. Requires YTYT-Downloader embed server running.',
group: 'Downloads',
icon: 'monitor-play',
_serverPort: 9547,
_player: null,
_audioElement: null,
_sponsorSegments: [],
_skipTimer: null,
_keyboardHandler: null,
_styleElement: null,
_isActive: false,
_persistenceObserver: null,
_persistenceInterval: null,
async _checkServer() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`http://localhost:${this._serverPort}/status`, { signal: controller.signal });
clearTimeout(timeoutId);
const data = await response.json();
return data.success;
} catch {
return false;
}
},
async _getStreamUrls(videoId) {
try {
const response = await fetch(`http://localhost:${this._serverPort}/stream?id=${videoId}`);
return await response.json();
} catch (e) {
console.error('[YTKit Embed] Failed to get stream URLs:', e);
return null;
}
},
async _getSponsorSegments(videoId) {
try {
const response = await fetch(`http://localhost:${this._serverPort}/sponsorblock?id=${videoId}`);
const data = await response.json();
return data.success ? data.segments : [];
} catch {
return [];
}
},
_injectStyles() {
if (this._styleElement) return;
this._styleElement = document.createElement('style');
this._styleElement.id = 'ytkit-embed-styles';
this._styleElement.textContent = `
/* Embed player inherits all sizing from #movie_player */
#movie_player.ytkit-embed-active {
position: relative !important;
}
/* NUCLEAR OPTION: Hide ALL YouTube player internals */
#movie_player.ytkit-embed-active > *:not(.ytkit-embed-video):not(.ytkit-embed-overlay):not(.ytkit-embed-audio) {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
z-index: -1 !important;
}
/* Explicit hide for major containers */
#movie_player.ytkit-embed-active .html5-video-container,
#movie_player.ytkit-embed-active .html5-video-player,
#movie_player.ytkit-embed-active video.html5-main-video,
#movie_player.ytkit-embed-active .ytp-chrome-bottom,
#movie_player.ytkit-embed-active .ytp-chrome-top,
#movie_player.ytkit-embed-active .ytp-chrome-controls,
#movie_player.ytkit-embed-active .ytp-gradient-bottom,
#movie_player.ytkit-embed-active .ytp-gradient-top,
#movie_player.ytkit-embed-active .ytp-progress-bar-container,
#movie_player.ytkit-embed-active .ytp-progress-bar,
#movie_player.ytkit-embed-active .ytp-time-display,
#movie_player.ytkit-embed-active .ytp-left-controls,
#movie_player.ytkit-embed-active .ytp-right-controls,
#movie_player.ytkit-embed-active .ytp-spinner,
#movie_player.ytkit-embed-active .ytp-spinner-container,
#movie_player.ytkit-embed-active .ytp-cued-thumbnail-overlay,
#movie_player.ytkit-embed-active .ytp-pause-overlay,
#movie_player.ytkit-embed-active .ytp-player-content,
#movie_player.ytkit-embed-active .ytp-iv-player-content,
#movie_player.ytkit-embed-active .ytp-ce-element,
#movie_player.ytkit-embed-active .ytp-ce-covering-overlay,
#movie_player.ytkit-embed-active .ytp-endscreen-content,
#movie_player.ytkit-embed-active .ytp-title,
#movie_player.ytkit-embed-active .ytp-title-text,
#movie_player.ytkit-embed-active .ytp-share-panel,
#movie_player.ytkit-embed-active .annotation,
#movie_player.ytkit-embed-active .ytp-cards-teaser,
#movie_player.ytkit-embed-active .ytp-cards-button,
#movie_player.ytkit-embed-active .ytp-tooltip,
#movie_player.ytkit-embed-active .ytp-tooltip-text,
#movie_player.ytkit-embed-active .ytp-bezel-text-wrapper,
#movie_player.ytkit-embed-active .ytp-bezel,
#movie_player.ytkit-embed-active .ytp-bezel-text,
#movie_player.ytkit-embed-active .ytp-watermark,
#movie_player.ytkit-embed-active .ytp-chapter-hover-container,
#movie_player.ytkit-embed-active .ytp-scrubber-container,
#movie_player.ytkit-embed-active .ytp-swatch-background-color,
#movie_player.ytkit-embed-active .ytp-play-button,
#movie_player.ytkit-embed-active .ytp-volume-panel,
#movie_player.ytkit-embed-active .ytp-settings-button,
#movie_player.ytkit-embed-active .ytp-subtitles-button,
#movie_player.ytkit-embed-active .ytp-miniplayer-button,
#movie_player.ytkit-embed-active .ytp-size-button,
#movie_player.ytkit-embed-active .ytp-fullscreen-button {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
z-index: -1 !important;
}
/* Hide ads and overlays */
#movie_player.ytkit-embed-active .ytp-ad-module,
#movie_player.ytkit-embed-active .ytp-ad-overlay-container,
#movie_player.ytkit-embed-active .ytp-ad-player-overlay,
#movie_player.ytkit-embed-active .ytp-ad-text-overlay,
#movie_player.ytkit-embed-active .ytp-ad-skip-button-container,
#movie_player.ytkit-embed-active .ytp-ad-preview-container,
#movie_player.ytkit-embed-active .video-ads,
#movie_player.ytkit-embed-active .ytp-paid-content-overlay,
#movie_player.ytkit-embed-active .ytp-ad-info-dialog-container {
display: none !important;
}
/* The embed video fills #movie_player completely - HIGHEST z-index */
.ytkit-embed-video {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
z-index: 99999 !important;
background: #000 !important;
object-fit: contain !important;
}
/* Overlay container for UI elements - above video */
.ytkit-embed-overlay {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
z-index: 100000 !important;
pointer-events: none !important;
}
.ytkit-embed-overlay > * {
pointer-events: auto;
}
/* Title bar */
.ytkit-embed-title {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 12px 16px;
background: linear-gradient(to bottom, rgba(0,0,0,0.8), transparent);
color: white;
font-size: 14px;
font-weight: 500;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
#movie_player.ytkit-embed-active:hover .ytkit-embed-title {
opacity: 1;
}
/* Embed badge */
.ytkit-embed-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 8px;
background: rgba(59, 130, 246, 0.9);
color: white;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
#movie_player.ytkit-embed-active:hover .ytkit-embed-badge {
opacity: 1;
}
/* Skip button */
.ytkit-skip-indicator {
position: absolute;
bottom: 80px;
right: 16px;
padding: 10px 20px;
background: #00d400;
color: white;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
display: none;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
transition: transform 0.2s, background 0.2s;
z-index: 100;
}
.ytkit-skip-indicator:hover {
transform: scale(1.05);
background: #00b800;
}
/* Fit to window mode - embed inherits automatically via % sizing */
body.yt-suite-fit-to-window #movie_player.ytkit-embed-active .ytkit-embed-video {
width: 100% !important;
height: 100% !important;
}
/* Theater mode */
ytd-watch-flexy[theater] #movie_player.ytkit-embed-active .ytkit-embed-video {
width: 100% !important;
height: 100% !important;
}
/* Fullscreen */
#movie_player.ytkit-embed-active:fullscreen .ytkit-embed-video {
width: 100vw !important;
height: 100vh !important;
}
#movie_player.ytkit-embed-active:fullscreen .ytkit-skip-indicator {
bottom: 100px;
right: 24px;
}
`;
document.head.appendChild(this._styleElement);
},
_createPlayer(streamData) {
// Clean up any existing embed
this._cleanupPlayer();
const moviePlayer = document.querySelector('#movie_player');
if (!moviePlayer) {
console.error('[YTKit Embed] #movie_player not found');
return null;
}
// Mark player as embed active
moviePlayer.classList.add('ytkit-embed-active');
// Pause and clear YouTube's video
const ytVideo = moviePlayer.querySelector('video.html5-main-video');
if (ytVideo) {
ytVideo.pause();
ytVideo.muted = true;
// Remove src to stop buffering and free memory
try {
ytVideo.src = '';
ytVideo.load(); // Force release of media resources
} catch(e) {}
}
// Create our video element
const video = document.createElement('video');
video.className = 'ytkit-embed-video';
video.controls = true;
video.autoplay = true;
video.playsInline = true;
video.src = streamData.videoUrl;
// Handle separate audio stream
let audioElement = null;
if (streamData.audioUrl && streamData.audioUrl !== streamData.videoUrl) {
audioElement = document.createElement('audio');
audioElement.className = 'ytkit-embed-audio';
audioElement.src = streamData.audioUrl;
audioElement.style.display = 'none';
// Throttled sync - only run every 500ms instead of every timeupdate
let lastSyncTime = 0;
const syncAudio = () => {
const now = Date.now();
if (now - lastSyncTime < 500) return; // Throttle to 2 times/second
lastSyncTime = now;
if (Math.abs(audioElement.currentTime - video.currentTime) > 0.3) {
audioElement.currentTime = video.currentTime;
}
};
video.addEventListener('play', () => {
audioElement.currentTime = video.currentTime;
audioElement.play().catch(() => {});
});
video.addEventListener('pause', () => audioElement.pause());
video.addEventListener('seeked', () => { audioElement.currentTime = video.currentTime; });
video.addEventListener('seeking', () => { audioElement.currentTime = video.currentTime; });
video.addEventListener('ratechange', () => { audioElement.playbackRate = video.playbackRate; });
video.addEventListener('volumechange', () => {
audioElement.volume = video.volume;
audioElement.muted = video.muted;
});
video.addEventListener('timeupdate', syncAudio);
moviePlayer.appendChild(audioElement);
this._audioElement = audioElement;
}
// Create overlay container
const overlayContainer = document.createElement('div');
overlayContainer.className = 'ytkit-embed-overlay';
// Title overlay
const titleOverlay = document.createElement('div');
titleOverlay.className = 'ytkit-embed-title';
titleOverlay.textContent = streamData.title || 'YouTube Video';
// Skip button (for SponsorBlock)
const skipIndicator = document.createElement('div');
skipIndicator.className = 'ytkit-skip-indicator';
skipIndicator.textContent = 'Skip Sponsor ▸';
overlayContainer.appendChild(titleOverlay);
overlayContainer.appendChild(skipIndicator);
// Insert elements
moviePlayer.appendChild(video);
moviePlayer.appendChild(overlayContainer);
// Double-click for fullscreen
video.addEventListener('dblclick', (e) => {
e.preventDefault();
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
moviePlayer.requestFullscreen().catch(() => {});
}
});
// Keyboard shortcuts
this._keyboardHandler = (e) => {
if (document.activeElement.tagName === 'INPUT' ||
document.activeElement.tagName === 'TEXTAREA' ||
document.activeElement.isContentEditable) return;
const key = e.key.toLowerCase();
if (key === ' ' || key === 'k') {
e.preventDefault();
video.paused ? video.play() : video.pause();
} else if (key === 'f') {
e.preventDefault();
document.fullscreenElement ? document.exitFullscreen() : moviePlayer.requestFullscreen();
} else if (key === 'm') {
e.preventDefault();
video.muted = !video.muted;
} else if (key === 'arrowleft') {
e.preventDefault();
video.currentTime -= 5;
} else if (key === 'arrowright') {
e.preventDefault();
video.currentTime += 5;
} else if (key === 'j') {
e.preventDefault();
video.currentTime -= 10;
} else if (key === 'l') {
e.preventDefault();
video.currentTime += 10;
} else if (key === 'arrowup') {
e.preventDefault();
video.volume = Math.min(1, video.volume + 0.1);
} else if (key === 'arrowdown') {
e.preventDefault();
video.volume = Math.max(0, video.volume - 0.1);
} else if (key === '0') {
e.preventDefault();
video.currentTime = 0;
} else if (key >= '1' && key <= '9') {
e.preventDefault();
video.currentTime = video.duration * (parseInt(key) / 10);
}
};
document.addEventListener('keydown', this._keyboardHandler);
// PERSISTENCE: Watch for YouTube trying to restore its player
this._persistenceObserver = new MutationObserver((mutations) => {
const moviePlayer = document.querySelector('#movie_player');
if (!moviePlayer || !this._isActive) return;
// Ensure our class stays on
if (!moviePlayer.classList.contains('ytkit-embed-active')) {
moviePlayer.classList.add('ytkit-embed-active');
console.log('[YTKit Embed] Re-applied ytkit-embed-active class');
}
// Stop YouTube video if it tries to play
const ytVideo = moviePlayer.querySelector('video.html5-main-video');
if (ytVideo && !ytVideo.paused) {
ytVideo.pause();
try { ytVideo.currentTime = 0; } catch(e) {}
}
// Force hide any YouTube elements that become visible
const ytElements = moviePlayer.querySelectorAll('.html5-video-container, .ytp-chrome-bottom, .ytp-chrome-top, .ytp-gradient-bottom');
ytElements.forEach(el => {
if (el.style.display !== 'none' || el.style.visibility !== 'hidden') {
el.style.setProperty('display', 'none', 'important');
el.style.setProperty('visibility', 'hidden', 'important');
}
});
});
this._persistenceObserver.observe(moviePlayer, {
childList: true,
subtree: false, // Changed from true - less expensive
attributes: true,
attributeFilter: ['class']
});
// Use interval as backup but less frequently (was 500ms, now 2000ms)
// Stop after embed is stable for a while
let stableCount = 0;
this._persistenceInterval = setInterval(() => {
if (!this._isActive) {
clearInterval(this._persistenceInterval);
return;
}
const mp = document.querySelector('#movie_player');
if (mp && !mp.classList.contains('ytkit-embed-active')) {
mp.classList.add('ytkit-embed-active');
stableCount = 0; // Reset if we had to fix it
} else {
stableCount++;
// If stable for 30 checks (60 seconds), reduce to very slow checking
if (stableCount > 30 && this._persistenceInterval) {
clearInterval(this._persistenceInterval);
// Switch to very slow checking (every 10 seconds)
this._persistenceInterval = setInterval(() => {
if (!this._isActive) {
clearInterval(this._persistenceInterval);
return;
}
const mp2 = document.querySelector('#movie_player');
if (mp2 && !mp2.classList.contains('ytkit-embed-active')) {
mp2.classList.add('ytkit-embed-active');
}
}, 10000);
}
}
// Keep YouTube video paused and unloaded
const ytv = document.querySelector('#movie_player video.html5-main-video');
if (ytv) {
if (!ytv.paused) ytv.pause();
if (ytv.src && ytv.src !== '') {
try { ytv.src = ''; ytv.load(); } catch(e) {}
}
}
}, 2000);
this._player = video;
this._isActive = true;
console.log('[YTKit Embed] Player created successfully with persistence');
return video;
},
_setupSponsorSkip(video, segments) {
if (!segments || segments.length === 0) return;
this._sponsorSegments = segments;
const skipIndicator = document.querySelector('.ytkit-skip-indicator');
// Throttle sponsor check to every 500ms
let lastCheck = 0;
video.addEventListener('timeupdate', () => {
const now = Date.now();
if (now - lastCheck < 500) return;
lastCheck = now;
const currentTime = video.currentTime;
for (const seg of segments) {
if (currentTime >= seg.start && currentTime < seg.end) {
if (skipIndicator) {
skipIndicator.style.display = 'block';
skipIndicator.onclick = () => {
video.currentTime = seg.end + 0.1;
skipIndicator.style.display = 'none';
};
}
// Auto-skip if enabled
if (appState.settings.skipSponsors) {
video.currentTime = seg.end + 0.1;
console.log(`[YTKit Embed] Skipped ${seg.category}: ${seg.start}s - ${seg.end}s`);
}
return;
}
}
if (skipIndicator) skipIndicator.style.display = 'none';
});
console.log(`[YTKit Embed] SponsorBlock: ${segments.length} segments loaded`);
},
_cleanupPlayer() {
// Stop persistence mechanisms
if (this._persistenceObserver) {
this._persistenceObserver.disconnect();
this._persistenceObserver = null;
}
if (this._persistenceInterval) {
clearInterval(this._persistenceInterval);
this._persistenceInterval = null;
}
// Remove embed elements
document.querySelector('.ytkit-embed-video')?.remove();
document.querySelector('.ytkit-embed-audio')?.remove();
document.querySelector('.ytkit-embed-overlay')?.remove();
// Remove keyboard handler
if (this._keyboardHandler) {
document.removeEventListener('keydown', this._keyboardHandler);
this._keyboardHandler = null;
}
// Remove embed active class and restore YouTube player
const moviePlayer = document.querySelector('#movie_player');
if (moviePlayer) {
moviePlayer.classList.remove('ytkit-embed-active');
}
this._player = null;
this._audioElement = null;
this._isActive = false;
},
_createEmbedButton(parent) {
const self = this;
const btn = document.createElement('button');
btn.className = 'ytkit-embed-btn';
btn.title = 'Use Embed Player (requires local server)';
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 rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', '2');
rect.setAttribute('y', '3');
rect.setAttribute('width', '20');
rect.setAttribute('height', '14');
rect.setAttribute('rx', '2');
rect.setAttribute('stroke', 'white');
rect.setAttribute('stroke-width', '2');
rect.setAttribute('fill', 'none');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'm10 8 5 3-5 3z');
path.setAttribute('fill', 'white');
svg.appendChild(rect);
svg.appendChild(path);
btn.appendChild(svg.cloneNode(true));
btn.appendChild(document.createTextNode(' Embed'));
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:#3b82f6;color:white;font-family:"Roboto","Arial",sans-serif;font-size:14px;font-weight:500;cursor:pointer;transition:background 0.2s;`;
btn.onmouseenter = () => { if (!self._isActive) btn.style.background = '#2563eb'; };
btn.onmouseleave = () => { if (!self._isActive) btn.style.background = '#3b82f6'; };
// Store reference to svg for later use
btn._svgTemplate = svg;
btn.addEventListener('click', async () => {
// If already active, deactivate
if (self._isActive) {
self._cleanupPlayer();
btn.style.background = '#3b82f6';
while (btn.lastChild) btn.removeChild(btn.lastChild);
btn.appendChild(btn._svgTemplate.cloneNode(true));
btn.appendChild(document.createTextNode(' Embed'));
window.location.reload();
return;
}
// Show loading
while (btn.lastChild) btn.removeChild(btn.lastChild);
btn.appendChild(document.createTextNode('⏳ Loading...'));
btn.disabled = true;
const success = await self.activateEmbed(true);
if (success) {
while (btn.lastChild) btn.removeChild(btn.lastChild);
btn.appendChild(document.createTextNode('✓ Active'));
btn.style.background = '#22c55e';
} else {
while (btn.lastChild) btn.removeChild(btn.lastChild);
btn.appendChild(btn._svgTemplate.cloneNode(true));
btn.appendChild(document.createTextNode(' Embed'));
btn.style.background = '#3b82f6';
}
btn.disabled = false;
});
parent.appendChild(btn);
},
init() {
this._injectStyles();
registerPersistentButton('embedButton', '#top-level-buttons-computed', '.ytkit-embed-btn', this._createEmbedButton.bind(this));
startButtonChecker();
},
destroy() {
unregisterPersistentButton('embedButton');
this._cleanupPlayer();
this._styleElement?.remove();
this._styleElement = null;
document.querySelector('.ytkit-embed-btn')?.remove();
},
// Expose method for auto-embed feature to use
async activateEmbed(showAlerts = false) {
if (this._isActive) return true;
if (!window.location.pathname.startsWith('/watch')) return false;
const serverOk = await this._checkServer();
if (!serverOk) {
console.log('[YTKit Embed] Server not running');
if (showAlerts) {
alert('YTYT-Downloader embed server not running!\n\nMake sure the embed server is installed and running.\nRe-run the YTYT-Downloader installer if needed.');
}
return false;
}
const videoId = new URLSearchParams(window.location.search).get('v');
if (!videoId) return false;
const streamData = await this._getStreamUrls(videoId);
if (!streamData || !streamData.success) {
console.log('[YTKit Embed] Failed to get stream URLs');
if (showAlerts) {
alert('Failed to get stream URLs. Video may be restricted.');
}
return false;
}
const video = this._createPlayer(streamData);
if (video) {
const segments = await this._getSponsorSegments(videoId);
this._setupSponsorSkip(video, segments);
// Update button if it exists
const btn = document.querySelector('.ytkit-embed-btn');
if (btn) {
while (btn.lastChild) btn.removeChild(btn.lastChild);
btn.appendChild(document.createTextNode('✓ Active'));
btn.style.background = '#22c55e';
}
return true;
}
return false;
}
},
{
id: 'autoEmbedOnVisit',
name: 'Auto-Embed on Visit',
description: 'Automatically activate embed player when visiting videos (requires server running)',
group: 'Downloads',
icon: 'play',
_lastVideoId: null,
_observer: null,
_attempting: false,
async _tryEmbed() {
if (this._attempting) return;
if (!window.location.pathname.startsWith('/watch')) return;
const videoId = new URLSearchParams(window.location.search).get('v');
if (!videoId || videoId === this._lastVideoId) return;
// Check if movie_player exists
const moviePlayer = document.querySelector('#movie_player');
if (!moviePlayer) return;
this._attempting = true;
this._lastVideoId = videoId;
console.log('[YTKit] Auto-embed triggered for:', videoId);
// Find the enableEmbedPlayer feature and call its activateEmbed method
const embedFeature = features.find(f => f.id === 'enableEmbedPlayer');
if (embedFeature && typeof embedFeature.activateEmbed === 'function') {
embedFeature._injectStyles();
const success = await embedFeature.activateEmbed();
console.log('[YTKit] Auto-embed result:', success ? 'success' : 'failed');
}
this._attempting = false;
},
init() {
// Method 1: Navigation events
addNavigateRule('autoEmbedRule', this._tryEmbed.bind(this));
// Method 2: MutationObserver for instant detection of video player
this._observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList' && m.addedNodes.length > 0) {
for (const node of m.addedNodes) {
if (node.nodeType === 1) {
if (node.id === 'movie_player' ||
node.querySelector?.('#movie_player') ||
node.tagName === 'YTD-WATCH-FLEXY') {
// Video player appeared - try embed immediately
setTimeout(() => this._tryEmbed(), 0);
setTimeout(() => this._tryEmbed(), 100);
setTimeout(() => this._tryEmbed(), 300);
return;
}
}
}
}
// Also watch for video-id attribute changes
if (m.type === 'attributes' && m.attributeName === 'video-id') {
this._lastVideoId = null; // Reset to allow new embed
setTimeout(() => this._tryEmbed(), 0);
setTimeout(() => this._tryEmbed(), 100);
}
}
});
this._observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['video-id']
});
// Method 3: Aggressive initial attempts
this._tryEmbed();
setTimeout(() => this._tryEmbed(), 100);
setTimeout(() => this._tryEmbed(), 300);
setTimeout(() => this._tryEmbed(), 500);
setTimeout(() => this._tryEmbed(), 1000);
},
destroy() {
removeNavigateRule('autoEmbedRule');
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
this._lastVideoId = null;
this._attempting = false;
}
},
{
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() {
const embedFeature = features.find(f => f.id === 'enableEmbedPlayer');
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 = new URLSearchParams(window.location.search).get('v');
if (videoId) {
navigator.clipboard.writeText(videoId).then(() => {
this._showToast('Video ID copied: ' + videoId);
});
}
},
_showToast(message) {
const toast = document.createElement('div');
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #22c55e;
color: white;
padding: 12px 24px;
border-radius: 8px;
font-family: "Roboto", Arial, sans-serif;
font-size: 14px;
z-index: 999999;
animation: ytkit-toast-fade 2s ease-out forwards;
`;
// Add animation keyframes if not exists
if (!document.getElementById('ytkit-toast-animation')) {
const style = document.createElement('style');
style.id = 'ytkit-toast-animation';
style.textContent = `
@keyframes ytkit-toast-fade {
0% { opacity: 0; transform: translateX(-50%) translateY(20px); }
15% { opacity: 1; transform: translateX(-50%) translateY(0); }
85% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(-20px); }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
},
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);
document.addEventListener('scroll', () => this._hideMenu());
// 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);
}
removeNavigateRule('contextMenuAttach');
this._menu?.remove();
this._menu = null;
this._styleElement?.remove();
this._styleElement = null;
}
},
// ─── Advanced ───
{
id: 'debugMode',
name: 'Debug Mode',
description: 'Enable diagnostic logging and expose window.YTKit for troubleshooting',
group: 'Advanced',
icon: 'bug',
init() {
DebugManager.enable();
DebugManager.log('Init', 'Debug mode enabled');
},
destroy() {
DebugManager.disable();
}
},
{
id: 'keyboardShortcutsFeature',
name: 'Keyboard Shortcuts',
description: 'Enable custom keyboard shortcuts (Ctrl+Alt+Y for settings, Shift+H to hide video)',
group: 'Advanced',
icon: 'keyboard',
init() {
KeyboardManager.init();
// Register default shortcuts
const shortcuts = appState.settings.keyboardShortcuts || {};
// Open settings panel
if (shortcuts.openSettings) {
KeyboardManager.register(shortcuts.openSettings, () => {
document.body.classList.toggle('ytkit-panel-open');
}, 'Open YTKit Settings');
}
// Hide current video (only on watch page)
if (shortcuts.hideVideo) {
KeyboardManager.register(shortcuts.hideVideo, () => {
if (!window.location.pathname.startsWith('/watch')) return;
const videoId = new URLSearchParams(window.location.search).get('v');
if (videoId) {
const videoHiderFeature = features.find(f => f.id === 'hideVideosFromHome');
if (videoHiderFeature && typeof videoHiderFeature._hideVideo === 'function') {
const title = document.querySelector('h1.ytd-video-primary-info-renderer, h1.ytd-watch-metadata')?.textContent || 'Unknown';
const channelName = document.querySelector('#owner #channel-name, #upload-info #channel-name')?.textContent?.trim() || 'Unknown';
videoHiderFeature._hideVideo(videoId, title, channelName);
showToast('Video hidden (Shift+H)', '#ef4444', {
duration: 4,
action: {
text: 'Undo',
onClick: () => {
if (typeof videoHiderFeature._unhideVideo === 'function') {
videoHiderFeature._unhideVideo(videoId);
showToast('Video restored', '#22c55e');
}
}
}
});
}
}
}, 'Hide Current Video');
}
// Download video shortcut
if (shortcuts.downloadVideo) {
KeyboardManager.register(shortcuts.downloadVideo, () => {
if (!window.location.pathname.startsWith('/watch')) return;
showToast('Starting download...', '#22c55e');
window.location.href = 'ytdl://' + encodeURIComponent(window.location.href);
}, 'Download Video');
}
},
destroy() {
const shortcuts = appState.settings.keyboardShortcuts || {};
if (shortcuts.openSettings) KeyboardManager.unregister(shortcuts.openSettings);
if (shortcuts.hideVideo) KeyboardManager.unregister(shortcuts.hideVideo);
if (shortcuts.downloadVideo) KeyboardManager.unregister(shortcuts.downloadVideo);
}
},
// ─── Auto-Skip "Still Watching?" Prompt ───
{
id: 'autoSkipStillWatching',
name: 'Auto-Skip "Still Watching?"',
description: 'Automatically dismiss the "Video paused. Continue watching?" popup',
group: 'Playback',
icon: 'skip-forward',
_observer: null,
_checkInterval: null,
init() {
const dismissPopup = () => {
// Look for the "Still watching?" / "Continue watching?" dialog
const confirmButton = document.querySelector(
'yt-confirm-dialog-renderer #confirm-button, ' +
'.ytp-pause-overlay-container button, ' +
'ytd-popup-container yt-confirm-dialog-renderer button.yt-spec-button-shape-next--filled, ' +
'tp-yt-paper-dialog #confirm-button, ' +
'ytd-enforcement-message-view-model button'
);
if (confirmButton) {
const dialogText = confirmButton.closest('yt-confirm-dialog-renderer, tp-yt-paper-dialog')?.textContent?.toLowerCase() || '';
if (dialogText.includes('still watching') || dialogText.includes('continue watching') || dialogText.includes('video paused')) {
confirmButton.click();
showToast('Auto-dismissed "Still watching?" popup', '#22c55e');
StatsTracker.increment('stillWatchingDismissed');
DebugManager.log('StillWatching', 'Dismissed popup');
}
}
// Also check for the pause overlay
const pauseOverlay = document.querySelector('.ytp-pause-overlay');
if (pauseOverlay && pauseOverlay.style.display !== 'none') {
const playButton = document.querySelector('.ytp-play-button');
if (playButton) {
playButton.click();
showToast('Auto-resumed playback', '#22c55e');
}
}
};
// Check periodically
this._checkInterval = setInterval(dismissPopup, 2000);
// Also observe for new dialogs
this._observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
setTimeout(dismissPopup, 100);
}
}
});
this._observer.observe(document.body, {
childList: true,
subtree: true
});
},
destroy() {
if (this._checkInterval) clearInterval(this._checkInterval);
if (this._observer) this._observer.disconnect();
}
},
// ─── Per-Channel Settings ───
{
id: 'enablePerChannelSettings',
name: 'Per-Channel Settings',
description: 'Remember playback speed, volume, and quality per channel',
group: 'Playback',
icon: 'users',
_lastVideoId: null,
init() {
const applyChannelSettings = async () => {
if (!window.location.pathname.startsWith('/watch')) return;
const videoId = new URLSearchParams(window.location.search).get('v');
if (videoId === this._lastVideoId) return;
this._lastVideoId = videoId;
const channelId = ChannelSettingsManager.getCurrentChannelId();
if (!channelId) return;
const settings = await ChannelSettingsManager.getForChannel(channelId);
if (!settings) return;
const video = document.querySelector('video');
if (!video) return;
// Apply playback speed
if (settings.playbackSpeed && settings.playbackSpeed !== video.playbackRate) {
video.playbackRate = settings.playbackSpeed;
DebugManager.log('ChannelSettings', `Applied speed ${settings.playbackSpeed}x for channel ${channelId}`);
}
// Apply volume
if (settings.volume !== undefined && settings.volume !== video.volume) {
video.volume = settings.volume;
}
};
// Save current settings for channel
const saveChannelSettings = async () => {
if (!window.location.pathname.startsWith('/watch')) return;
const channelId = ChannelSettingsManager.getCurrentChannelId();
const channelName = ChannelSettingsManager.getCurrentChannelName();
if (!channelId) return;
const video = document.querySelector('video');
if (!video) return;
await ChannelSettingsManager.setForChannel(channelId, {
name: channelName,
playbackSpeed: video.playbackRate,
volume: video.volume
});
};
// Apply on navigation
addNavigateRule('perChannelSettings', () => {
setTimeout(applyChannelSettings, 1000);
});
// Save periodically when video is playing
this._saveInterval = setInterval(() => {
const video = document.querySelector('video');
if (video && !video.paused) {
saveChannelSettings();
}
}, 30000); // Save every 30 seconds while playing
},
destroy() {
removeNavigateRule('perChannelSettings');
if (this._saveInterval) clearInterval(this._saveInterval);
}
},
// ─── Statistics Dashboard ───
{
id: 'showStatisticsDashboard',
name: 'Statistics Dashboard',
description: 'Track videos watched, time saved from sponsors, videos hidden, and more',
group: 'Advanced',
icon: 'bar-chart',
init() {
// Track video watches
let lastVideoId = null;
addNavigateRule('statsVideoWatch', () => {
if (!window.location.pathname.startsWith('/watch')) return;
const videoId = new URLSearchParams(window.location.search).get('v');
if (videoId && videoId !== lastVideoId) {
lastVideoId = videoId;
StatsTracker.increment('videosWatched');
}
});
// Update time on YouTube
this._timeInterval = setInterval(() => {
StatsTracker.increment('totalTimeOnYouTube', 60); // Add 60 seconds
}, 60000);
},
destroy() {
removeNavigateRule('statsVideoWatch');
if (this._timeInterval) clearInterval(this._timeInterval);
}
},
// ─── Regex Keyword Filter ───
{
id: 'useRegexKeywordFilter',
name: 'Regex Keyword Filter',
description: 'Use regular expressions for advanced keyword filtering (e.g., /\\[.*\\]/ to hide bracketed titles)',
group: 'Video Hider',
icon: 'filter',
subFeatureOf: 'hideVideosFromHome',
init() {
// This modifies the behavior of hideVideosFromHome
// The actual regex matching is handled in _shouldHide
},
destroy() {
// Nothing to clean up
}
},
// ─── Custom CSS Injection ───
{
id: 'customCssEnabled',
name: 'Custom CSS',
description: 'Inject your own custom CSS rules for advanced customization',
group: 'Appearance',
icon: 'palette',
_styleElement: null,
init() {
const css = appState.settings.customCssCode || '';
if (css.trim()) {
this._styleElement = document.createElement('style');
this._styleElement.id = 'ytkit-custom-css';
this._styleElement.textContent = css;
document.head.appendChild(this._styleElement);
}
},
destroy() {
this._styleElement?.remove();
this._styleElement = null;
},
updateCss(newCss) {
if (this._styleElement) {
this._styleElement.textContent = newCss;
} else if (newCss.trim()) {
this._styleElement = document.createElement('style');
this._styleElement.id = 'ytkit-custom-css';
this._styleElement.textContent = newCss;
document.head.appendChild(this._styleElement);
}
}
},
{
id: 'customCssCode',
name: 'Custom CSS Code',
description: 'Enter your CSS rules here (applied when Custom CSS is enabled)',
group: 'Appearance',
icon: 'code',
type: 'textarea',
placeholder: '/* Your custom CSS */\n.ytp-chrome-bottom { opacity: 0.8; }',
init() {},
destroy() {}
},
// ─── IntersectionObserver Performance Mode ───
{
id: 'useIntersectionObserver',
name: 'Performance Mode',
description: 'Use IntersectionObserver to only process visible videos (improves performance on long feeds)',
group: 'Advanced',
icon: 'gauge',
init() {
VisibilityObserver.init();
// Add CSS containment for better rendering performance
const style = document.createElement('style');
style.id = 'ytkit-performance-css';
style.textContent = `
ytd-rich-item-renderer,
ytd-video-renderer,
ytd-grid-video-renderer,
ytd-compact-video-renderer {
contain: content;
}
ytd-thumbnail {
contain: content;
}
`;
document.head.appendChild(style);
this._styleElement = style;
},
destroy() {
VisibilityObserver.disconnect();
this._styleElement?.remove();
}
},
// ─── Settings Profiles ───
{
id: 'settingsProfiles',
name: 'Settings Profiles',
description: 'Save and load different configurations (Minimal, Privacy, Power User, Download Mode, Binge)',
group: 'Advanced',
icon: 'list-tree',
init() {
// Profiles are managed through the settings panel UI
// This feature just enables the functionality
},
destroy() {
// Nothing to clean up
}
},
];
// ══════════════════════════════════════════════════════════════════════════
// HELPER: Channel Settings Dialog
// ══════════════════════════════════════════════════════════════════════════
function showChannelSettingsDialog(channelId, channelName, existingSettings) {
// Remove existing dialog
document.getElementById('ytkit-channel-dialog')?.remove();
const dialog = document.createElement('div');
dialog.id = 'ytkit-channel-dialog';
dialog.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 16px;
padding: 24px;
z-index: 999999;
min-width: 350px;
box-shadow: 0 25px 50px rgba(0,0,0,0.5);
font-family: "Roboto", Arial, sans-serif;
color: #e2e8f0;
`;
const currentSpeed = existingSettings?.playbackSpeed || 1;
const currentVolume = existingSettings?.volume !== undefined ? Math.round(existingSettings.volume * 100) : 100;
TrustedHTML.setHTML(dialog, `
Channel Settings
Settings for: ${channelName || channelId}
`);
// Add backdrop
const backdrop = document.createElement('div');
backdrop.id = 'ytkit-channel-backdrop';
backdrop.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 999998;
`;
backdrop.onclick = () => {
dialog.remove();
backdrop.remove();
};
document.body.appendChild(backdrop);
document.body.appendChild(dialog);
// Volume display update
const volumeSlider = dialog.querySelector('#ytkit-channel-volume');
const volDisplay = dialog.querySelector('#ytkit-vol-display');
volumeSlider.oninput = () => {
volDisplay.textContent = volumeSlider.value + '%';
};
// Close button
dialog.querySelector('#ytkit-channel-close').onclick = () => {
dialog.remove();
backdrop.remove();
};
// Save button
dialog.querySelector('#ytkit-channel-save').onclick = async () => {
const speed = parseFloat(dialog.querySelector('#ytkit-channel-speed').value);
const volume = parseInt(volumeSlider.value) / 100;
await ChannelSettingsManager.setForChannel(channelId, {
name: channelName,
playbackSpeed: speed,
volume: volume
});
// Apply immediately
const video = document.querySelector('video');
if (video) {
video.playbackRate = speed;
video.volume = volume;
}
showToast(`Settings saved for ${channelName}`, '#22c55e');
dialog.remove();
backdrop.remove();
};
// Reset button
dialog.querySelector('#ytkit-channel-reset').onclick = async () => {
await ChannelSettingsManager.removeChannel(channelId);
showToast(`Settings reset for ${channelName}`, '#f97316');
dialog.remove();
backdrop.remove();
};
}
// ══════════════════════════════════════════════════════════════════════════
// HELPER: Statistics Dashboard Builder
// ══════════════════════════════════════════════════════════════════════════
async function buildStatisticsDashboard() {
const stats = await StatsTracker.getAll();
const container = document.createElement('div');
container.className = 'ytkit-stats-dashboard';
TrustedHTML.setHTML(container, `
${stats.videosWatched || 0}
Videos Watched
${stats.videosHidden || 0}
Videos Hidden
${stats.channelsBlocked || 0}
Channels Blocked
${StatsTracker.formatTime(stats.sponsorTimeSkipped || 0)}
Sponsor Time Skipped
${stats.downloadsInitiated || 0}
Downloads
${stats.vlcStreams || 0}
VLC Streams
${StatsTracker.formatTime(stats.totalTimeOnYouTube || 0)}
Total Time on YouTube
`);
container.querySelector('.ytkit-stats-reset').onclick = async () => {
if (confirm('Reset all statistics? This cannot be undone.')) {
await StatsTracker.reset();
showToast('Statistics reset', '#f97316');
// Refresh the dashboard
const parent = container.parentElement;
container.remove();
parent?.appendChild(await buildStatisticsDashboard());
}
};
return container;
}
// ══════════════════════════════════════════════════════════════════════════
// HELPER: Profiles Manager UI
// ══════════════════════════════════════════════════════════════════════════
async function buildProfilesUI() {
const profiles = await ProfilesManager.getAll();
const container = document.createElement('div');
container.className = 'ytkit-profiles-ui';
let html = 'Built-in Profiles
';
// Built-in profiles
for (const [key, profile] of Object.entries(profiles.builtIn)) {
html += `
${profile.name}
${profile.description}
`;
}
html += '
';
// Custom profiles section
html += 'Custom Profiles
';
if (Object.keys(profiles.custom).length === 0) {
html += '
No custom profiles yet.
';
} else {
for (const [key, profile] of Object.entries(profiles.custom)) {
html += `
${profile.name}
${profile.description}
`;
}
}
html += '
';
// Save current as profile button
html += `
`;
TrustedHTML.setHTML(container, html);
// Event listeners
container.querySelectorAll('.ytkit-profile-apply').forEach(btn => {
btn.onclick = async (e) => {
e.stopPropagation();
const item = btn.closest('.ytkit-profile-item');
const profileKey = item.dataset.profile;
const isBuiltIn = item.dataset.builtin === 'true';
if (confirm(`Apply the "${profileKey}" profile? This will change your current settings.`)) {
await ProfilesManager.applyProfile(profileKey, isBuiltIn);
showToast(`Profile "${profileKey}" applied! Refreshing...`, '#22c55e');
setTimeout(() => location.reload(), 1000);
}
};
});
container.querySelectorAll('.ytkit-profile-delete').forEach(btn => {
btn.onclick = async (e) => {
e.stopPropagation();
const item = btn.closest('.ytkit-profile-item');
const profileKey = item.dataset.profile;
if (confirm(`Delete the "${profileKey}" profile?`)) {
await ProfilesManager.deleteCustomProfile(profileKey);
showToast(`Profile "${profileKey}" deleted`, '#f97316');
// Refresh profiles UI
const parent = container.parentElement;
container.remove();
parent?.appendChild(await buildProfilesUI());
}
};
});
container.querySelector('#ytkit-save-profile').onclick = async () => {
const name = prompt('Enter a name for this profile:');
if (name && name.trim()) {
await ProfilesManager.saveCustomProfile(name.trim(), appState.settings);
showToast(`Profile "${name}" saved`, '#22c55e');
// Refresh profiles UI
const parent = container.parentElement;
container.remove();
parent?.appendChild(await buildProfilesUI());
}
};
return container;
}
// ══════════════════════════════════════════════════════════════════════════
// HELPER: Bulk Operations for Hidden Videos
// ══════════════════════════════════════════════════════════════════════════
function addBulkOperationsUI(container) {
const bulkBar = document.createElement('div');
bulkBar.className = 'ytkit-bulk-bar';
bulkBar.style.cssText = `
display: none;
padding: 12px;
background: linear-gradient(135deg, #1e3a5f 0%, #1e293b 100%);
border-radius: 8px;
margin-bottom: 12px;
align-items: center;
gap: 12px;
`;
TrustedHTML.setHTML(bulkBar, `
0 selected
`);
container.insertBefore(bulkBar, container.firstChild);
let selectedItems = new Set();
const updateBulkBar = () => {
bulkBar.style.display = selectedItems.size > 0 ? 'flex' : 'none';
bulkBar.querySelector('.ytkit-bulk-count').textContent = `${selectedItems.size} selected`;
};
bulkBar.querySelector('.ytkit-bulk-cancel').onclick = () => {
selectedItems.clear();
container.querySelectorAll('.ytkit-item-checkbox').forEach(cb => cb.checked = false);
updateBulkBar();
};
bulkBar.querySelector('.ytkit-bulk-unhide').onclick = () => {
if (selectedItems.size === 0) return;
const videoHiderFeature = features.find(f => f.id === 'hideVideosFromHome');
if (videoHiderFeature) {
selectedItems.forEach(id => {
if (typeof videoHiderFeature._unhideVideo === 'function') {
videoHiderFeature._unhideVideo(id);
}
});
showToast(`Unhid ${selectedItems.size} videos`, '#22c55e');
selectedItems.clear();
updateBulkBar();
// Trigger refresh of the list
container.dispatchEvent(new CustomEvent('ytkit-refresh-list'));
}
};
return {
addCheckbox: (item, id) => {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'ytkit-item-checkbox';
checkbox.style.cssText = 'margin-right: 8px; accent-color: #60a5fa;';
checkbox.onchange = () => {
if (checkbox.checked) {
selectedItems.add(id);
} else {
selectedItems.delete(id);
}
updateBulkBar();
};
item.insertBefore(checkbox, item.firstChild);
},
selectAll: () => {
container.querySelectorAll('.ytkit-item-checkbox').forEach(cb => {
cb.checked = true;
const id = cb.closest('[data-video-id]')?.dataset.videoId;
if (id) selectedItems.add(id);
});
updateBulkBar();
},
deselectAll: () => {
selectedItems.clear();
container.querySelectorAll('.ytkit-item-checkbox').forEach(cb => cb.checked = false);
updateBulkBar();
}
};
}
function injectStyle(selector, featureId, isRawCss = false) {
const style = document.createElement('style');
style.id = `yt-suite-style-${featureId}`;
style.textContent = isRawCss ? selector : `${selector} { display: none !important; }`;
document.head.appendChild(style);
return style;
}
// ══════════════════════════════════════════════════════════════════════════
// SECTION 3: HELPERS
// ══════════════════════════════════════════════════════════════════════════
let appState = {};
function applyBotFilter() {
if (!window.location.pathname.startsWith('/watch')) return;
const messages = document.querySelectorAll('yt-live-chat-text-message-renderer:not(.yt-suite-hidden-bot)');
messages.forEach(msg => {
const authorName = msg.querySelector('#author-name')?.textContent.toLowerCase() || '';
if (authorName.includes('bot')) {
msg.style.display = 'none';
msg.classList.add('yt-suite-hidden-bot');
}
});
}
function applyKeywordFilter() {
if (!window.location.pathname.startsWith('/watch')) return;
const keywordsRaw = appState.settings.chatKeywordFilter;
const messages = document.querySelectorAll('yt-live-chat-text-message-renderer');
if (!keywordsRaw || !keywordsRaw.trim()) {
messages.forEach(el => {
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 => {
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');
} else if (msg.classList.contains('yt-suite-hidden-keyword')) {
msg.style.display = '';
msg.classList.remove('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 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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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.2-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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
playback: () => createSVG('0 0 24 24', [
{ type: 'polygon', points: '5 3 19 12 5 21 5 3' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
actions: () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'path', d: 'M12 8v4l3 3' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
advanced: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M12 2L2 7l10 5 10-5-10-5z' },
{ type: 'path', d: 'M2 17l10 5 10-5' },
{ type: 'path', d: 'M2 12l10 5 10-5' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'list-video': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 10, y1: 6, x2: 21, y2: 6 },
{ type: 'line', x1: 10, y1: 12, x2: 21, y2: 12 },
{ type: 'line', x1: 10, y1: 18, x2: 21, y2: 18 },
{ type: 'polygon', points: '3 6 7 9 3 12 3 6', fill: 'currentColor' },
{ type: 'circle', cx: 5, cy: 18, r: 1.5, fill: 'currentColor' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
// Playback Enhancement Icons
gauge: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z' },
{ type: 'path', d: 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-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 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.26.604.852.997 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
brain: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 4.44-1.54' },
{ type: 'path', d: 'M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-4.44-1.54' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'thumbs-down': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
progress: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M5 12h14' },
{ type: 'path', d: 'M12 5v14' },
{ type: 'rect', x: 3, y: 3, width: 18, height: 18, rx: 2 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
bookmark: () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'minimize-2': () => createSVG('0 0 24 24', [
{ type: 'polyline', points: '4 14 10 14 10 20' },
{ type: 'polyline', points: '20 10 14 10 14 4' },
{ type: 'line', x1: 14, y1: 10, x2: 21, y2: 3 },
{ type: 'line', x1: 3, y1: 21, x2: 10, y2: 14 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'grid-3x3': () => 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: 3, y1: 15, x2: 21, y2: 15 },
{ type: 'line', x1: 9, y1: 3, x2: 9, y2: 21 },
{ type: 'line', x1: 15, y1: 3, x2: 15, y2: 21 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
// ─── Additional 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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'bell-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M13.73 21a2 2 0 0 1-3.46 0' },
{ type: 'path', d: 'M18.63 13A17.89 17.89 0 0 1 18 8' },
{ type: 'path', d: 'M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14' },
{ type: 'path', d: 'M18 8a6 6 0 0 0-9.33-5' },
{ type: 'line', x1: 1, y1: 1, x2: 23, y2: 23 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'bell-minus': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9' },
{ type: 'path', d: 'M13.73 21a2 2 0 0 1-3.46 0' },
{ type: 'line', x1: 8, y1: 2, x2: 16, y2: 2 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'sun-dim': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 4 },
{ type: 'path', d: 'M12 4h.01M12 20h.01M4 12h.01M20 12h.01M6.34 6.34h.01M17.66 6.34h.01M6.34 17.66h.01M17.66 17.66h.01' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'contrast': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'path', d: 'M12 2v20' },
{ type: 'path', d: 'M12 2a10 10 0 0 1 0 20', fill: 'currentColor' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'palette': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 13.5, cy: 6.5, r: 0.5, fill: 'currentColor' },
{ type: 'circle', cx: 17.5, cy: 10.5, r: 0.5, fill: 'currentColor' },
{ type: 'circle', cx: 8.5, cy: 7.5, r: 0.5, fill: 'currentColor' },
{ type: 'circle', cx: 6.5, cy: 12.5, r: 0.5, fill: 'currentColor' },
{ type: 'path', d: 'M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.555C21.965 6.012 17.461 2 12 2z' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'square': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 18, height: 18, rx: 2 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'user-square': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 18, height: 18, rx: 2 },
{ type: 'circle', cx: 12, cy: 10, r: 3 },
{ type: 'path', d: 'M7 21v-2a4 4 0 0 1 4-4h2a4 4 0 0 1 4 4v2' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'droplet-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M12 2v5' },
{ type: 'path', d: 'M6.8 11.2A6 6 0 0 0 12 22a6 6 0 0 0 5.3-8.8' },
{ type: 'path', d: 'M12 2l3.5 5.5' },
{ type: 'line', x1: 2, y1: 2, x2: 22, y2: 22 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'minimize': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M8 3v3a2 2 0 0 1-2 2H3' },
{ type: 'path', d: 'M21 8h-3a2 2 0 0 1-2-2V3' },
{ type: 'path', d: 'M3 16h3a2 2 0 0 1 2 2v3' },
{ type: 'path', d: 'M16 21v-3a2 2 0 0 1 2-2h3' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'pause': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 6, y: 4, width: 4, height: 16 },
{ type: 'rect', x: 14, y: 4, width: 4, height: 16 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'maximize': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M8 3H5a2 2 0 0 0-2 2v3' },
{ type: 'path', d: 'M21 8V5a2 2 0 0 0-2-2h-3' },
{ type: 'path', d: 'M3 16v3a2 2 0 0 0 2 2h3' },
{ type: 'path', d: 'M16 21h3a2 2 0 0 0 2-2v-3' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'badge': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'info-off': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'line', x1: 12, y1: 16, x2: 12, y2: 12 },
{ type: 'line', x1: 12, y1: 8, x2: 12.01, y2: 8 },
{ type: 'line', x1: 4, y1: 4, x2: 20, y2: 20 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'gamepad': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 6, y1: 12, x2: 10, y2: 12 },
{ type: 'line', x1: 8, y1: 10, x2: 8, y2: 14 },
{ type: 'line', x1: 15, y1: 13, x2: 15.01, y2: 13 },
{ type: 'line', x1: 18, y1: 11, x2: 18.01, y2: 11 },
{ type: 'rect', x: 2, y: 6, width: 20, height: 12, rx: 2 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'lock': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 11, width: 18, height: 11, rx: 2 },
{ type: 'path', d: 'M7 11V7a5 5 0 0 1 10 0v4' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'newspaper': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2' },
{ type: 'path', d: 'M18 14h-8M15 18h-5M10 6h8v4h-8V6Z' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'list-x': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 11, y1: 6, x2: 21, y2: 6 },
{ type: 'line', x1: 11, y1: 12, x2: 21, y2: 12 },
{ type: 'line', x1: 11, y1: 18, x2: 21, y2: 18 },
{ type: 'line', x1: 3, y1: 4, x2: 7, y2: 8 },
{ type: 'line', x1: 7, y1: 4, x2: 3, y2: 8 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'panel-right': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 3, width: 18, height: 18, rx: 2 },
{ type: 'line', x1: 15, y1: 3, x2: 15, y2: 21 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'cast': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6' },
{ type: 'path', d: 'M2 12a9 9 0 0 1 8 8' },
{ type: 'path', d: 'M2 16a5 5 0 0 1 4 4' },
{ type: 'line', x1: 2, y1: 20, x2: 2.01, y2: 20 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'file-minus': () => 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: 9, y1: 15, x2: 15, y2: 15 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'align-horizontal-justify-center': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 5, width: 6, height: 14, rx: 2 },
{ type: 'rect', x: 16, y: 7, width: 6, height: 10, rx: 2 },
{ type: 'line', x1: 12, y1: 2, x2: 12, y2: 22 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'pause-circle': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'line', x1: 10, y1: 15, x2: 10, y2: 9 },
{ type: 'line', x1: 14, y1: 15, x2: 14, y2: 9 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'chevrons-down': () => createSVG('0 0 24 24', [
{ type: 'polyline', points: '7 13 12 18 17 13' },
{ type: 'polyline', points: '7 6 12 11 17 6' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'plus-circle': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'line', x1: 12, y1: 8, x2: 12, y2: 16 },
{ type: 'line', x1: 8, y1: 12, x2: 16, y2: 12 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'mic-off': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 1, y1: 1, x2: 23, y2: 23 },
{ type: 'path', d: 'M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6' },
{ type: 'path', d: 'M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23' },
{ type: 'line', x1: 12, y1: 19, x2: 12, y2: 23 },
{ type: 'line', x1: 8, y1: 23, x2: 16, y2: 23 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'thumbs-up': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'monitor-play': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 3, width: 20, height: 14, rx: 2 },
{ type: 'polygon', points: '10 8 15 10 10 12 10 8' },
{ type: 'line', x1: 8, y1: 21, x2: 16, y2: 21 },
{ type: 'line', x1: 12, y1: 17, x2: 12, y2: 21 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'info': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'line', x1: 12, y1: 16, x2: 12, y2: 12 },
{ type: 'line', x1: 12, y1: 8, x2: 12.01, y2: 8 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'zap-off': () => createSVG('0 0 24 24', [
{ type: 'polyline', points: '12.41 6.75 13 2 10.57 4.92' },
{ type: 'polyline', points: '18.57 12.91 21 10 15.66 10' },
{ type: 'polyline', points: '8 8 3 14 12 14 11 22 16 16' },
{ type: 'line', x1: 1, y1: 1, x2: 23, y2: 23 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'trophy': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M6 9H4.5a2.5 2.5 0 0 1 0-5H6' },
{ type: 'path', d: 'M18 9h1.5a2.5 2.5 0 0 0 0-5H18' },
{ type: 'path', d: 'M4 22h16' },
{ type: 'path', d: 'M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22' },
{ type: 'path', d: 'M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22' },
{ type: 'path', d: 'M18 2H6v7a6 6 0 0 0 12 0V2Z' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'trending-up': () => createSVG('0 0 24 24', [
{ type: 'polyline', points: '23 6 13.5 15.5 8.5 10.5 1 18' },
{ type: 'polyline', points: '17 6 23 6 23 12' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'timer': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 14, r: 8 },
{ type: 'line', x1: 12, y1: 14, x2: 12, y2: 10 },
{ type: 'line', x1: 12, y1: 2, x2: 12, y2: 4 },
{ type: 'line', x1: 8, y1: 2, x2: 16, y2: 2 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'ticket': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z' },
{ type: 'path', d: 'M13 5v2' },
{ type: 'path', d: 'M13 17v2' },
{ type: 'path', d: 'M13 11v2' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'users': () => 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: 'path', d: 'M23 21v-2a4 4 0 0 0-3-3.87' },
{ type: 'path', d: 'M16 3.13a4 4 0 0 1 0 7.75' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'award': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 8, r: 6 },
{ type: 'path', d: 'M15.477 12.89 17 22l-5-3-5 3 1.523-9.11' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'bar-chart': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 12, y1: 20, x2: 12, y2: 10 },
{ type: 'line', x1: 18, y1: 20, x2: 18, y2: 4 },
{ type: 'line', x1: 6, y1: 20, x2: 6, y2: 16 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'bell-ring': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9' },
{ type: 'path', d: 'M13.73 21a2 2 0 0 1-3.46 0' },
{ type: 'path', d: 'M2 8c0-2.2.7-4.3 2-6' },
{ type: 'path', d: 'M22 8a10 10 0 0 0-2-6' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'bot': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 11, width: 18, height: 10, rx: 2 },
{ type: 'circle', cx: 12, cy: 5, r: 2 },
{ type: 'path', d: 'M12 7v4' },
{ type: 'line', x1: 8, y1: 16, x2: 8, y2: 16 },
{ type: 'line', x1: 16, y1: 16, x2: 16, y2: 16 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'captions-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M10.5 5H19a2 2 0 0 1 2 2v8.5' },
{ type: 'path', d: 'M17 11h-.5' },
{ type: 'path', d: 'M19 19H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2' },
{ type: 'path', d: 'M2 2 22 22' },
{ type: 'path', d: 'M7 11h4' },
{ type: 'path', d: 'M7 15h2.5' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'clapperboard': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M4 11v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8H4Z' },
{ type: 'path', d: 'm4 11-.88-2.87a2 2 0 0 1 1.33-2.5l11.48-3.5a2 2 0 0 1 2.5 1.32l.87 2.87L4 11.01Z' },
{ type: 'path', d: 'm6.6 4.99 3.38 4.2' },
{ type: 'path', d: 'm11.86 3.38 3.38 4.2' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'clock': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'polyline', points: '12 6 12 12 16 14' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'dollar-sign': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 12, y1: 2, x2: 12, y2: 22 },
{ type: 'path', d: 'M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'file-x': () => 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: 9.5, y1: 12.5, x2: 14.5, y2: 17.5 },
{ type: 'line', x1: 14.5, y1: 12.5, x2: 9.5, y2: 17.5 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'flag-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M8 2c3 0 5 2 8 2s4-1 4-1v11' },
{ type: 'path', d: 'M4 22V4' },
{ type: 'path', d: 'M4 15s1-1 4-1 5 2 8 2' },
{ type: 'line', x1: 2, y1: 2, x2: 22, y2: 22 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'gift': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 3, y: 8, width: 18, height: 4, rx: 1 },
{ type: 'path', d: 'M12 8v13' },
{ type: 'path', d: 'M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7' },
{ type: 'path', d: 'M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'heart': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'heart-off': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 2, y1: 2, x2: 22, y2: 22 },
{ type: 'path', d: 'M16.5 16.5 12 21l-7-7c-1.5-1.45-3-3.2-3-5.5a5.5 5.5 0 0 1 2.14-4.35' },
{ type: 'path', d: 'M8.76 3.1c1.15.22 2.13.78 3.24 1.9 1.5-1.5 2.74-2 4.5-2A5.5 5.5 0 0 1 22 8.5c0 2.12-1.3 3.78-2.67 5.17' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'list-tree': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M21 12h-8' },
{ type: 'path', d: 'M21 6H8' },
{ type: 'path', d: 'M21 18h-8' },
{ type: 'path', d: 'M3 6v4c0 1.1.9 2 2 2h3' },
{ type: 'path', d: 'M3 10v6c0 1.1.9 2 2 2h3' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'megaphone-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M9.26 9.26 3 11v3l14.14 3.14' },
{ type: 'path', d: 'M21 15.34V6l-7.31 2.03' },
{ type: 'path', d: 'M11.6 16.8a3 3 0 1 1-5.8-1.6' },
{ type: 'line', x1: 2, y1: 2, x2: 22, y2: 22 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'message-circle-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M20.5 14.9A9 9 0 0 0 9.1 3.5' },
{ type: 'path', d: 'M5.5 5.5A9 9 0 0 0 3 12c0 .78.1 1.53.28 2.25a9 9 0 0 0 .61 1.6l-1.7 5.47a.5.5 0 0 0 .61.63l5.58-1.48c.48.27.99.49 1.52.66' },
{ type: 'line', x1: 2, y1: 2, x2: 22, y2: 22 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'message-square-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M21 15V5a2 2 0 0 0-2-2H9' },
{ type: 'path', d: 'M3 3l18 18' },
{ type: 'path', d: 'M3 6v15l4-4h5' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'more-horizontal': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 1 },
{ type: 'circle', cx: 19, cy: 12, r: 1 },
{ type: 'circle', cx: 5, cy: 12, r: 1 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'more-vertical': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 1 },
{ type: 'circle', cx: 12, cy: 5, r: 1 },
{ type: 'circle', cx: 12, cy: 19, r: 1 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'panel-top': () => 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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'picture-in-picture': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 4, width: 20, height: 16, rx: 2 },
{ type: 'rect', x: 12, y: 12, width: 8, height: 6 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'pip': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 4, width: 20, height: 16, rx: 2 },
{ type: 'rect', x: 12, y: 12, width: 8, height: 6 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'pin-off': () => createSVG('0 0 24 24', [
{ type: 'line', x1: 2, y1: 2, x2: 22, y2: 22 },
{ type: 'line', x1: 12, y1: 17, x2: 12, y2: 22 },
{ type: 'path', d: 'M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V17h12' },
{ type: 'path', d: 'M15 9.34V6h1a2 2 0 0 0 0-4H7.89' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'play': () => createSVG('0 0 24 24', [
{ type: 'polygon', points: '5 3 19 12 5 21 5 3' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'repeat': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'm17 2 4 4-4 4' },
{ type: 'path', d: 'M3 11v-1a4 4 0 0 1 4-4h14' },
{ type: 'path', d: 'm7 22-4-4 4-4' },
{ type: 'path', d: 'M21 13v1a4 4 0 0 1-4 4H3' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'scissors': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 6, cy: 6, r: 3 },
{ type: 'circle', cx: 6, cy: 18, r: 3 },
{ type: 'line', x1: 20, y1: 4, x2: 8.12, y2: 15.88 },
{ type: 'line', x1: 14.47, y1: 14.48, x2: 20, y2: 20 },
{ type: 'line', x1: 8.12, y1: 8.12, x2: 12, y2: 12 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'scroll-text': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4' },
{ type: 'path', d: 'M19 17V5a2 2 0 0 0-2-2H4' },
{ type: 'path', d: 'M15 8h-5' },
{ type: 'path', d: 'M15 12h-5' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'settings-2': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M20 7h-9' },
{ type: 'path', d: 'M14 17H5' },
{ type: 'circle', cx: 17, cy: 17, r: 3 },
{ type: 'circle', cx: 7, cy: 7, r: 3 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'share': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 18, cy: 5, r: 3 },
{ type: 'circle', cx: 6, cy: 12, r: 3 },
{ type: 'circle', cx: 18, cy: 19, r: 3 },
{ type: 'line', x1: 8.59, y1: 13.51, x2: 15.42, y2: 17.49 },
{ type: 'line', x1: 15.41, y1: 6.51, x2: 8.59, y2: 10.49 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'shield-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M19.7 14a6.9 6.9 0 0 0 .3-2V5l-8-3-3.2 1.2' },
{ type: 'path', d: 'M4.7 4.7 4 5v7c0 6 8 10 8 10a20.3 20.3 0 0 0 5.62-4.38' },
{ type: 'line', x1: 2, y1: 2, x2: 22, y2: 22 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'shopping-bag': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z' },
{ type: 'line', x1: 3, y1: 6, x2: 21, y2: 6 },
{ type: 'path', d: 'M16 10a4 4 0 0 1-8 0' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'smile': () => createSVG('0 0 24 24', [
{ type: 'circle', cx: 12, cy: 12, r: 10 },
{ type: 'path', d: 'M8 14s1.5 2 4 2 4-2 4-2' },
{ type: 'line', x1: 9, y1: 9, x2: 9.01, y2: 9 },
{ type: 'line', x1: 15, y1: 9, x2: 15.01, y2: 9 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'smile-plus': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M22 11v1a10 10 0 1 1-9-10' },
{ type: 'path', d: 'M8 14s1.5 2 4 2 4-2 4-2' },
{ type: 'line', x1: 9, y1: 9, x2: 9.01, y2: 9 },
{ type: 'line', x1: 15, y1: 9, x2: 15.01, y2: 9 },
{ type: 'path', d: 'M16 5h6' },
{ type: 'path', d: 'M19 2v6' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'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 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'subtitles': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 4, width: 20, height: 16, rx: 2 },
{ type: 'line', x1: 6, y1: 12, x2: 9, y2: 12 },
{ type: 'line', x1: 6, y1: 16, x2: 13, y2: 16 },
{ type: 'line', x1: 12, y1: 12, x2: 18, y2: 12 },
{ type: 'line', x1: 16, y1: 16, x2: 18, y2: 16 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'tag-off': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M10.02 2.03 2.03 10.02a2 2 0 0 0 0 2.83l8.12 8.12a2 2 0 0 0 2.83 0l8.01-8.01a2.02 2.02 0 0 0 .38-2.29' },
{ type: 'path', d: 'M7.5 7.5a.5.5 0 1 0 1 0 .5.5 0 1 0-1 0Z' },
{ type: 'path', d: 'M21.95 12.05 12.05 21.95' },
{ type: 'line', x1: 2, y1: 2, x2: 22, y2: 22 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
// Keyboard/debug icons
'keyboard': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 2, y: 4, width: 20, height: 16, rx: 2 },
{ type: 'line', x1: 6, y1: 8, x2: 6, y2: 8 },
{ type: 'line', x1: 10, y1: 8, x2: 10, y2: 8 },
{ type: 'line', x1: 14, y1: 8, x2: 14, y2: 8 },
{ type: 'line', x1: 18, y1: 8, x2: 18, y2: 8 },
{ type: 'line', x1: 8, y1: 12, x2: 8, y2: 12 },
{ type: 'line', x1: 12, y1: 12, x2: 12, y2: 12 },
{ type: 'line', x1: 16, y1: 12, x2: 16, y2: 12 },
{ type: 'line', x1: 7, y1: 16, x2: 17, y2: 16 }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'bug': () => createSVG('0 0 24 24', [
{ type: 'rect', x: 8, y: 6, width: 8, height: 14, rx: 4 },
{ type: 'path', d: 'M19 7l-3 2' },
{ type: 'path', d: 'M5 7l3 2' },
{ type: 'path', d: 'M19 19l-3-2' },
{ type: 'path', d: 'M5 19l3-2' },
{ type: 'path', d: 'M20 13h-4' },
{ type: 'path', d: 'M4 13h4' },
{ type: 'path', d: 'M10 4l1 2' },
{ type: 'path', d: 'M14 4l-1 2' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
'undo': () => createSVG('0 0 24 24', [
{ type: 'path', d: 'M3 7v6h6' },
{ type: 'path', d: 'M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13' }
], { strokeWidth: '1.5', strokeLinecap: 'round', strokeLinejoin: 'round' }),
};
const CATEGORY_CONFIG = {
'Interface': { icon: 'interface', color: '#60a5fa' },
'Appearance': { icon: 'appearance', color: '#f472b6' },
'Content': { icon: 'content', color: '#34d399' },
'Video Hider': { icon: 'eye-off', color: '#ef4444' },
'Video Player': { icon: 'player', color: '#a78bfa' },
'Playback': { icon: 'playback', color: '#fb923c' },
'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' },
'Advanced': { icon: 'advanced', color: '#94a3b8' },
};
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');
const logo = document.getElementById('yt-suite-watch-logo');
if (logo && logo.parentElement === ownerDiv) {
ownerDiv.insertBefore(btn, logo.nextSibling);
} else {
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 Hider', 'Video Player', 'Playback', 'Ad Blocker', 'SponsorBlock', 'Quality', 'Clutter', 'Live Chat', 'Action Buttons', 'Player Controls', 'Downloads', 'Advanced'];
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) => {
// 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
setInterval(() => {
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;
}
// Special handling for Video Hider
if (cat === 'Video Hider') {
const config = CATEGORY_CONFIG[cat];
const catId = cat.replace(/ /g, '-');
const videoHiderFeature = features.find(f => f.id === 'hideVideosFromHome');
const videoCount = (typeof videoHiderFeature?._getHiddenVideos === 'function' ? videoHiderFeature._getHiddenVideos() : []).length;
const channelCount = (typeof videoHiderFeature?._getBlockedChannels === 'function' ? videoHiderFeature._getBlockedChannels() : []).length;
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);
const iconFn = ICONS['eye-off'] || 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 = `${videoCount + channelCount}`;
countSpan.title = `${videoCount} videos, ${channelCount} channels`;
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';
// Special builder for Video Hider pane
// ══════════════════════════════════════════════════════════════════
// 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);
await 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();
await 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);
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;
}
function buildVideoHiderPane(config) {
const videoHiderFeature = features.find(f => f.id === 'hideVideosFromHome');
const pane = document.createElement('section');
pane.id = 'ytkit-pane-Video-Hider';
pane.className = 'ytkit-pane ytkit-vh-pane';
// 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['eye-off'] || ICONS.settings;
paneIcon.appendChild(paneIconFn());
const paneTitleH2 = document.createElement('h2');
paneTitleH2.textContent = 'Video Hider';
paneTitle.appendChild(paneIcon);
paneTitle.appendChild(paneTitleH2);
// Enable toggle for Video Hider
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.hideVideosFromHome ? ' active' : '');
const toggleInput = document.createElement('input');
toggleInput.type = 'checkbox';
toggleInput.id = 'ytkit-toggle-hideVideosFromHome';
toggleInput.checked = appState.settings.hideVideosFromHome;
toggleInput.onchange = async () => {
appState.settings.hideVideosFromHome = toggleInput.checked;
toggleSwitch.classList.toggle('active', toggleInput.checked);
await settingsManager.save(appState.settings);
if (toggleInput.checked) {
videoHiderFeature?.init?.();
} else {
videoHiderFeature?.destroy?.();
}
updateAllToggleStates();
};
const toggleTrack = document.createElement('span');
toggleTrack.className = 'ytkit-switch-track';
toggleSwitch.appendChild(toggleInput);
toggleSwitch.appendChild(toggleTrack);
toggleLabel.appendChild(toggleText);
toggleLabel.appendChild(toggleSwitch);
paneHeader.appendChild(paneTitle);
paneHeader.appendChild(toggleLabel);
pane.appendChild(paneHeader);
// Tab navigation
const tabNav = document.createElement('div');
tabNav.className = 'ytkit-vh-tabs';
tabNav.style.cssText = 'display:flex;gap:0;border-bottom:1px solid var(--ytkit-border);margin-bottom:20px;';
const tabs = ['Videos', 'Channels', 'Keywords', 'Settings'];
tabs.forEach((tabName, i) => {
const tab = document.createElement('button');
tab.className = 'ytkit-vh-tab' + (i === 0 ? ' active' : '');
tab.dataset.tab = tabName.toLowerCase();
tab.textContent = tabName;
tab.style.cssText = `
flex:1;padding:12px 16px;background:transparent;border:none;
color:var(--ytkit-text-muted);font-size:13px;font-weight:500;
cursor:pointer;transition:all 0.2s;border-bottom:2px solid transparent;
`;
tab.onmouseenter = () => { if (!tab.classList.contains('active')) tab.style.color = 'var(--ytkit-text-secondary)'; };
tab.onmouseleave = () => { if (!tab.classList.contains('active')) tab.style.color = 'var(--ytkit-text-muted)'; };
tab.onclick = () => {
tabNav.querySelectorAll('.ytkit-vh-tab').forEach(t => {
t.classList.remove('active');
t.style.color = 'var(--ytkit-text-muted)';
t.style.borderBottomColor = 'transparent';
});
tab.classList.add('active');
tab.style.color = config.color;
tab.style.borderBottomColor = config.color;
renderTabContent(tabName.toLowerCase());
};
if (i === 0) {
tab.style.color = config.color;
tab.style.borderBottomColor = config.color;
}
tabNav.appendChild(tab);
});
pane.appendChild(tabNav);
// Tab content container
const tabContent = document.createElement('div');
tabContent.id = 'ytkit-vh-content';
pane.appendChild(tabContent);
function renderTabContent(tab) {
while (tabContent.firstChild) tabContent.removeChild(tabContent.firstChild);
if (tab === 'videos') {
const videos = videoHiderFeature?._getHiddenVideos() || [];
if (videos.length === 0) {
const empty = document.createElement('div');
empty.style.cssText = 'text-align:center;padding:60px 20px;color:var(--ytkit-text-muted);';
const emptyIcon = document.createElement('div');
emptyIcon.style.cssText = 'font-size:48px;margin-bottom:16px;opacity:0.5;';
emptyIcon.textContent = '📺';
const emptyTitle = document.createElement('div');
emptyTitle.style.cssText = 'font-size:15px;margin-bottom:8px;';
emptyTitle.textContent = 'No hidden videos yet';
const emptyDesc = document.createElement('div');
emptyDesc.style.cssText = 'font-size:13px;opacity:0.7;';
emptyDesc.textContent = 'Click the X button on video thumbnails to hide them';
empty.appendChild(emptyIcon);
empty.appendChild(emptyTitle);
empty.appendChild(emptyDesc);
tabContent.appendChild(empty);
} else {
const grid = document.createElement('div');
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;';
videos.forEach(vid => {
const item = document.createElement('div');
item.style.cssText = 'display:flex;align-items:center;gap:12px;padding:10px;background:var(--ytkit-bg-surface);border-radius:8px;border:1px solid var(--ytkit-border);';
const thumb = document.createElement('img');
thumb.src = `https://i.ytimg.com/vi/${vid}/mqdefault.jpg`;
thumb.style.cssText = 'width:100px;height:56px;object-fit:cover;border-radius:4px;flex-shrink:0;';
thumb.onerror = () => { thumb.style.background = 'var(--ytkit-bg-elevated)'; };
const info = document.createElement('div');
info.style.cssText = 'flex:1;min-width:0;';
const vidId = document.createElement('div');
vidId.style.cssText = 'font-size:12px;color:var(--ytkit-text-secondary);font-family:monospace;margin-bottom:4px;';
vidId.textContent = vid;
const link = document.createElement('a');
link.href = `https://youtube.com/watch?v=${vid}`;
link.target = '_blank';
link.style.cssText = 'font-size:12px;color:var(--ytkit-accent);text-decoration:none;';
link.textContent = 'View on YouTube →';
info.appendChild(vidId);
info.appendChild(link);
const removeBtn = document.createElement('button');
removeBtn.textContent = 'Unhide';
removeBtn.style.cssText = 'padding:6px 12px;background:var(--ytkit-bg-elevated);border:1px solid var(--ytkit-border);color:var(--ytkit-text-secondary);border-radius:6px;cursor:pointer;font-size:12px;transition:all 0.2s;';
removeBtn.onmouseenter = () => { removeBtn.style.background = '#dc2626'; removeBtn.style.color = '#fff'; removeBtn.style.borderColor = '#dc2626'; };
removeBtn.onmouseleave = () => { removeBtn.style.background = 'var(--ytkit-bg-elevated)'; removeBtn.style.color = 'var(--ytkit-text-secondary)'; removeBtn.style.borderColor = 'var(--ytkit-border)'; };
removeBtn.onclick = () => {
const h = videoHiderFeature._getHiddenVideos();
const idx = h.indexOf(vid);
if (idx > -1) { h.splice(idx, 1); videoHiderFeature._setHiddenVideos(h); }
item.remove();
videoHiderFeature._processAllVideos();
if (videoHiderFeature._getHiddenVideos().length === 0) renderTabContent('videos');
};
item.appendChild(thumb);
item.appendChild(info);
item.appendChild(removeBtn);
grid.appendChild(item);
});
tabContent.appendChild(grid);
// Clear all button
const clearBtn = document.createElement('button');
clearBtn.textContent = `Clear All Hidden Videos (${videos.length})`;
clearBtn.style.cssText = 'margin-top:20px;padding:12px 24px;width:100%;background:#dc2626;border:none;color:#fff;border-radius:8px;cursor:pointer;font-size:14px;font-weight:500;transition:background 0.2s;';
clearBtn.onmouseenter = () => { clearBtn.style.background = '#b91c1c'; };
clearBtn.onmouseleave = () => { clearBtn.style.background = '#dc2626'; };
clearBtn.onclick = () => {
if (!confirm(`Clear all ${videos.length} hidden videos?`)) return;
videoHiderFeature._setHiddenVideos([]);
videoHiderFeature._processAllVideos();
renderTabContent('videos');
};
tabContent.appendChild(clearBtn);
}
} else if (tab === 'channels') {
const channels = videoHiderFeature?._getBlockedChannels() || [];
if (channels.length === 0) {
const empty = document.createElement('div');
empty.style.cssText = 'text-align:center;padding:60px 20px;color:var(--ytkit-text-muted);';
const emptyIcon = document.createElement('div');
emptyIcon.style.cssText = 'font-size:48px;margin-bottom:16px;opacity:0.5;';
emptyIcon.textContent = '📢';
const emptyTitle = document.createElement('div');
emptyTitle.style.cssText = 'font-size:15px;margin-bottom:8px;';
emptyTitle.textContent = 'No blocked channels yet';
const emptyDesc = document.createElement('div');
emptyDesc.style.cssText = 'font-size:13px;opacity:0.7;';
emptyDesc.textContent = 'Right-click the X button on thumbnails to block channels';
empty.appendChild(emptyIcon);
empty.appendChild(emptyTitle);
empty.appendChild(emptyDesc);
tabContent.appendChild(empty);
} else {
const list = document.createElement('div');
list.style.cssText = 'display:flex;flex-direction:column;gap:8px;';
channels.forEach(ch => {
const item = document.createElement('div');
item.style.cssText = 'display:flex;align-items:center;gap:12px;padding:12px;background:var(--ytkit-bg-surface);border-radius:8px;border:1px solid var(--ytkit-border);';
const icon = document.createElement('div');
icon.style.cssText = 'width:40px;height:40px;border-radius:50%;background:var(--ytkit-bg-elevated);display:flex;align-items:center;justify-content:center;font-size:18px;';
icon.textContent = '📺';
const info = document.createElement('div');
info.style.cssText = 'flex:1;';
const name = document.createElement('div');
name.style.cssText = 'font-size:14px;color:var(--ytkit-text-primary);font-weight:500;';
name.textContent = ch.name || ch.id;
const handle = document.createElement('div');
handle.style.cssText = 'font-size:12px;color:var(--ytkit-text-muted);';
handle.textContent = ch.id;
info.appendChild(name);
info.appendChild(handle);
const removeBtn = document.createElement('button');
removeBtn.textContent = 'Unblock';
removeBtn.style.cssText = 'padding:6px 12px;background:var(--ytkit-bg-elevated);border:1px solid var(--ytkit-border);color:var(--ytkit-text-secondary);border-radius:6px;cursor:pointer;font-size:12px;transition:all 0.2s;';
removeBtn.onmouseenter = () => { removeBtn.style.background = '#22c55e'; removeBtn.style.color = '#fff'; removeBtn.style.borderColor = '#22c55e'; };
removeBtn.onmouseleave = () => { removeBtn.style.background = 'var(--ytkit-bg-elevated)'; removeBtn.style.color = 'var(--ytkit-text-secondary)'; removeBtn.style.borderColor = 'var(--ytkit-border)'; };
removeBtn.onclick = () => {
const c = videoHiderFeature._getBlockedChannels();
const idx = c.findIndex(x => x.id === ch.id);
if (idx > -1) { c.splice(idx, 1); videoHiderFeature._setBlockedChannels(c); }
item.remove();
videoHiderFeature._processAllVideos();
if (videoHiderFeature._getBlockedChannels().length === 0) renderTabContent('channels');
};
item.appendChild(icon);
item.appendChild(info);
item.appendChild(removeBtn);
list.appendChild(item);
});
tabContent.appendChild(list);
// Clear all button
const clearBtn = document.createElement('button');
clearBtn.textContent = `Unblock All Channels (${channels.length})`;
clearBtn.style.cssText = 'margin-top:20px;padding:12px 24px;width:100%;background:#dc2626;border:none;color:#fff;border-radius:8px;cursor:pointer;font-size:14px;font-weight:500;transition:background 0.2s;';
clearBtn.onmouseenter = () => { clearBtn.style.background = '#b91c1c'; };
clearBtn.onmouseleave = () => { clearBtn.style.background = '#dc2626'; };
clearBtn.onclick = () => {
if (!confirm(`Unblock all ${channels.length} channels?`)) return;
videoHiderFeature._setBlockedChannels([]);
videoHiderFeature._processAllVideos();
renderTabContent('channels');
};
tabContent.appendChild(clearBtn);
}
} else if (tab === 'keywords') {
const container = document.createElement('div');
container.style.cssText = 'padding:0;';
const desc = document.createElement('div');
desc.style.cssText = 'color:var(--ytkit-text-muted);font-size:13px;margin-bottom:16px;line-height:1.5;';
desc.textContent = 'Videos with titles containing these keywords will be automatically hidden. Separate multiple keywords with commas.';
container.appendChild(desc);
const textarea = document.createElement('textarea');
textarea.style.cssText = 'width:100%;min-height:150px;padding:12px;background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border);border-radius:8px;color:var(--ytkit-text-primary);font-size:13px;resize:vertical;font-family:inherit;';
textarea.placeholder = 'e.g., reaction, unboxing, prank, shorts';
textarea.value = appState.settings.hideVideosKeywordFilter || '';
textarea.onchange = async () => {
appState.settings.hideVideosKeywordFilter = textarea.value;
await settingsManager.save(appState.settings);
videoHiderFeature?._processAllVideos();
};
container.appendChild(textarea);
const hint = document.createElement('div');
hint.style.cssText = 'color:var(--ytkit-text-muted);font-size:11px;margin-top:8px;';
hint.textContent = 'Changes apply immediately. Keywords are case-insensitive.';
container.appendChild(hint);
tabContent.appendChild(container);
} else if (tab === 'settings') {
const container = document.createElement('div');
container.style.cssText = 'display:flex;flex-direction:column;gap:24px;';
// Duration filter
const durSection = document.createElement('div');
durSection.style.cssText = 'background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border);border-radius:12px;padding:20px;';
const durTitle = document.createElement('div');
durTitle.style.cssText = 'font-size:14px;font-weight:600;color:var(--ytkit-text-primary);margin-bottom:8px;';
durTitle.textContent = 'Duration Filter';
durSection.appendChild(durTitle);
const durDesc = document.createElement('div');
durDesc.style.cssText = 'font-size:12px;color:var(--ytkit-text-muted);margin-bottom:12px;';
durDesc.textContent = 'Automatically hide videos shorter than the specified duration.';
durSection.appendChild(durDesc);
const durRow = document.createElement('div');
durRow.style.cssText = 'display:flex;align-items:center;gap:12px;';
const durInput = document.createElement('input');
durInput.type = 'number';
durInput.min = '0';
durInput.max = '60';
durInput.value = appState.settings.hideVideosDurationFilter || 0;
durInput.style.cssText = 'width:80px;padding:8px 12px;background:var(--ytkit-bg-elevated);border:1px solid var(--ytkit-border);border-radius:6px;color:var(--ytkit-text-primary);font-size:14px;';
durInput.onchange = async () => {
appState.settings.hideVideosDurationFilter = parseInt(durInput.value) || 0;
await settingsManager.save(appState.settings);
videoHiderFeature?._processAllVideos();
};
const durLabel = document.createElement('span');
durLabel.style.cssText = 'color:var(--ytkit-text-secondary);font-size:13px;';
durLabel.textContent = 'minutes (0 = disabled)';
durRow.appendChild(durInput);
durRow.appendChild(durLabel);
durSection.appendChild(durRow);
container.appendChild(durSection);
// Subscription Load Limiter
const limiterSection = document.createElement('div');
limiterSection.style.cssText = 'background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border);border-radius:12px;padding:20px;';
const limiterTitle = document.createElement('div');
limiterTitle.style.cssText = 'font-size:14px;font-weight:600;color:var(--ytkit-text-primary);margin-bottom:8px;';
limiterTitle.textContent = 'Subscription Page Load Limiter';
limiterSection.appendChild(limiterTitle);
const limiterDesc = document.createElement('div');
limiterDesc.style.cssText = 'font-size:12px;color:var(--ytkit-text-muted);margin-bottom:16px;line-height:1.5;';
limiterDesc.textContent = 'Prevents infinite scrolling when many consecutive videos are hidden. Useful if you\'ve hidden years of subscription videos.';
limiterSection.appendChild(limiterDesc);
// Enable toggle
const limiterToggleRow = document.createElement('div');
limiterToggleRow.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;padding:12px;background:var(--ytkit-bg-elevated);border-radius:8px;';
const limiterToggleLabel = document.createElement('span');
limiterToggleLabel.style.cssText = 'color:var(--ytkit-text-secondary);font-size:13px;';
limiterToggleLabel.textContent = 'Enable load limiter';
const limiterSwitch = document.createElement('div');
limiterSwitch.className = 'ytkit-switch' + (appState.settings.hideVideosSubsLoadLimit !== false ? ' active' : '');
limiterSwitch.style.cssText = 'cursor:pointer;';
const limiterInput = document.createElement('input');
limiterInput.type = 'checkbox';
limiterInput.checked = appState.settings.hideVideosSubsLoadLimit !== false;
limiterInput.onchange = async () => {
appState.settings.hideVideosSubsLoadLimit = limiterInput.checked;
limiterSwitch.classList.toggle('active', limiterInput.checked);
await settingsManager.save(appState.settings);
};
const limiterTrack = document.createElement('span');
limiterTrack.className = 'ytkit-switch-track';
limiterSwitch.appendChild(limiterInput);
limiterSwitch.appendChild(limiterTrack);
limiterToggleRow.appendChild(limiterToggleLabel);
limiterToggleRow.appendChild(limiterSwitch);
limiterSection.appendChild(limiterToggleRow);
// Threshold setting
const thresholdRow = document.createElement('div');
thresholdRow.style.cssText = 'display:flex;align-items:center;gap:12px;';
const thresholdLabel = document.createElement('span');
thresholdLabel.style.cssText = 'color:var(--ytkit-text-secondary);font-size:13px;flex:1;';
thresholdLabel.textContent = 'Stop after consecutive hidden batches:';
const thresholdInput = document.createElement('input');
thresholdInput.type = 'number';
thresholdInput.min = '1';
thresholdInput.max = '20';
thresholdInput.value = appState.settings.hideVideosSubsLoadThreshold || 3;
thresholdInput.style.cssText = 'width:70px;padding:8px 12px;background:var(--ytkit-bg-elevated);border:1px solid var(--ytkit-border);border-radius:6px;color:var(--ytkit-text-primary);font-size:14px;text-align:center;';
thresholdInput.onchange = async () => {
appState.settings.hideVideosSubsLoadThreshold = Math.max(1, Math.min(20, parseInt(thresholdInput.value) || 3));
thresholdInput.value = appState.settings.hideVideosSubsLoadThreshold;
await settingsManager.save(appState.settings);
};
thresholdRow.appendChild(thresholdLabel);
thresholdRow.appendChild(thresholdInput);
limiterSection.appendChild(thresholdRow);
const thresholdHint = document.createElement('div');
thresholdHint.style.cssText = 'color:var(--ytkit-text-muted);font-size:11px;margin-top:8px;';
thresholdHint.textContent = 'Lower = stops faster, Higher = loads more before stopping (1-20)';
limiterSection.appendChild(thresholdHint);
container.appendChild(limiterSection);
// Stats section
const statsSection = document.createElement('div');
statsSection.style.cssText = 'background:var(--ytkit-bg-surface);border:1px solid var(--ytkit-border);border-radius:12px;padding:20px;';
const statsTitle = document.createElement('div');
statsTitle.style.cssText = 'font-size:14px;font-weight:600;color:var(--ytkit-text-primary);margin-bottom:12px;';
statsTitle.textContent = 'Statistics';
statsSection.appendChild(statsTitle);
const statsGrid = document.createElement('div');
statsGrid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:12px;';
const videoCount = videoHiderFeature?._getHiddenVideos()?.length || 0;
const channelCount = videoHiderFeature?._getBlockedChannels()?.length || 0;
const videoStat = document.createElement('div');
videoStat.style.cssText = 'background:var(--ytkit-bg-elevated);padding:16px;border-radius:8px;text-align:center;';
const videoStatNum = document.createElement('div');
videoStatNum.style.cssText = `font-size:24px;font-weight:700;color:${config.color};`;
videoStatNum.textContent = videoCount;
const videoStatLabel = document.createElement('div');
videoStatLabel.style.cssText = 'font-size:12px;color:var(--ytkit-text-muted);margin-top:4px;';
videoStatLabel.textContent = 'Hidden Videos';
videoStat.appendChild(videoStatNum);
videoStat.appendChild(videoStatLabel);
const channelStat = document.createElement('div');
channelStat.style.cssText = 'background:var(--ytkit-bg-elevated);padding:16px;border-radius:8px;text-align:center;';
const channelStatNum = document.createElement('div');
channelStatNum.style.cssText = `font-size:24px;font-weight:700;color:${config.color};`;
channelStatNum.textContent = channelCount;
const channelStatLabel = document.createElement('div');
channelStatLabel.style.cssText = 'font-size:12px;color:var(--ytkit-text-muted);margin-top:4px;';
channelStatLabel.textContent = 'Blocked Channels';
channelStat.appendChild(channelStatNum);
channelStat.appendChild(channelStatLabel);
statsGrid.appendChild(videoStat);
statsGrid.appendChild(channelStat);
statsSection.appendChild(statsGrid);
container.appendChild(statsSection);
tabContent.appendChild(container);
}
}
// Initial render
renderTabContent('videos');
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;
}
// Special handling for Video Hider
if (cat === 'Video Hider') {
const config = CATEGORY_CONFIG[cat];
content.appendChild(buildVideoHiderPane(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 = async () => {
if (!confirm(`Reset all "${cat}" settings to defaults?`)) return;
const categoryFeatures = featuresByCategory[cat];
categoryFeatures.forEach(f => {
const defaultValue = settingsManager.defaults[f.id];
if (defaultValue !== undefined) {
appState.settings[f.id] = defaultValue;
try { f.destroy?.(); } catch(e) {}
if (defaultValue) {
try { f.init?.(); } catch(e) {}
}
}
});
await 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');
};
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' || a.type === 'multiSelect';
const bIsDropdown = b.type === 'select' || b.type === 'multiSelect';
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.display = '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 = 'v15';
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;
// 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 card - no additional controls needed (installer is in footer)
} 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.id] || '';
// Make CSS textareas larger
if (f.id === 'customCssCode') {
textarea.style.cssText = 'min-height: 150px; font-family: monospace; font-size: 12px;';
}
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 = 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 === 'multiSelect') {
// Multi-select with checkboxes
const settingKey = f.settingKey || f.id;
const currentValues = appState.settings[settingKey] || [];
const wrapper = document.createElement('div');
wrapper.className = 'ytkit-multiselect';
wrapper.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;max-width:300px;';
// Create "Edit" button to expand options
const editBtn = document.createElement('button');
editBtn.className = 'ytkit-multiselect-btn';
editBtn.style.cssText = 'padding:6px 12px;border-radius:6px;background:var(--ytkit-bg-hover);color:#fff;border:1px solid rgba(255,255,255,0.1);cursor:pointer;font-size:12px;';
editBtn.textContent = `${currentValues.length} of ${f.options.length} selected`;
const dropdown = document.createElement('div');
dropdown.className = 'ytkit-multiselect-dropdown';
dropdown.style.cssText = 'display:none;position:absolute;right:0;top:100%;background:var(--ytkit-bg-elevated);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:8px;z-index:100;max-height:200px;overflow-y:auto;min-width:200px;';
f.options.forEach(opt => {
const label = document.createElement('label');
label.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer;font-size:12px;color:#e2e8f0;';
label.onmouseenter = () => { label.style.background = 'rgba(255,255,255,0.05)'; };
label.onmouseleave = () => { label.style.background = 'transparent'; };
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = opt.value;
cb.checked = currentValues.includes(opt.value);
cb.style.cssText = 'accent-color:#3b82f6;';
cb.dataset.featureId = f.id;
cb.dataset.settingKey = settingKey;
cb.className = 'ytkit-multiselect-cb';
label.appendChild(cb);
label.appendChild(document.createTextNode(opt.label));
dropdown.appendChild(label);
});
editBtn.onclick = (e) => {
e.stopPropagation();
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
};
wrapper.style.position = 'relative';
wrapper.appendChild(editBtn);
wrapper.appendChild(dropdown);
card.appendChild(wrapper);
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
dropdown.style.display = 'none';
}
});
} else {
const isEnabled = 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);
// Add "Manage" button for Video Hider feature
if (f.id === 'hideVideosFromHome') {
const manageBtn = document.createElement('button');
manageBtn.className = 'ytkit-manage-btn';
manageBtn.textContent = 'Manage';
manageBtn.title = 'Manage hidden videos and blocked channels';
manageBtn.style.cssText = `
padding: 6px 12px;
margin-left: 8px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.2);
background: rgba(255,255,255,0.1);
color: #fff;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
`;
manageBtn.onmouseenter = () => { manageBtn.style.background = 'rgba(255,255,255,0.2)'; manageBtn.style.borderColor = 'rgba(255,255,255,0.3)'; };
manageBtn.onmouseleave = () => { manageBtn.style.background = 'rgba(255,255,255,0.1)'; manageBtn.style.borderColor = 'rgba(255,255,255,0.2)'; };
manageBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
// Find the hideVideosFromHome feature and call its manager
const videoHiderFeature = features.find(feat => feat.id === 'hideVideosFromHome');
if (videoHiderFeature && videoHiderFeature._showManager) {
videoHiderFeature._showManager();
}
};
card.appendChild(manageBtn);
}
}
return card;
}
function createToast(message, type = 'success', duration = 3000) {
document.querySelector('.ytkit-toast')?.remove();
const toast = document.createElement('div');
toast.className = `ytkit-toast ytkit-toast-${type}`;
const span = document.createElement('span');
span.textContent = message;
toast.appendChild(span);
document.body.appendChild(toast);
requestAnimationFrame(() => {
requestAnimationFrame(() => toast.classList.add('show'));
});
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 400);
}, duration);
}
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 = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function attachUIEventListeners() {
const doc = document;
// 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');
doc.querySelector(`#ytkit-pane-${navBtn.dataset.tab}`)?.classList.add('active');
}
});
// 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');
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;
sub.style.display = appState.settings[parentId] ? '' : '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.display = '');
// Filter cards
let matchCount = 0;
allCards.forEach(card => {
const name = card.querySelector('.ytkit-feature-name')?.textContent.toLowerCase() || '';
const desc = card.querySelector('.ytkit-feature-desc')?.textContent.toLowerCase() || '';
const matches = name.includes(query) || desc.includes(query);
card.style.display = matches ? '' : 'none';
if (matches) matchCount++;
});
// 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', async (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);
appState.settings[featureId] = isEnabled;
await settingsManager.save(appState.settings);
const feature = features.find(f => f.id === featureId);
if (feature) {
isEnabled ? feature.init?.() : feature.destroy?.();
}
// Toggle sub-features visibility
const subContainer = doc.querySelector(`.ytkit-sub-features[data-parent-id="${featureId}"]`);
if (subContainer) {
subContainer.style.display = 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', async (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;
await settingsManager.save(appState.settings);
const feature = features.find(f => f.id === featureId);
if (feature) {
feature.destroy?.();
feature.init?.();
}
// Special case: if customCssCode changed, update the customCssEnabled feature
if (featureId === 'customCssCode' && appState.settings.customCssEnabled) {
const cssFeature = features.find(f => f.id === 'customCssEnabled');
if (cssFeature && typeof cssFeature.updateCss === 'function') {
cssFeature.updateCss(e.target.value);
}
}
}
// 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;
await settingsManager.save(appState.settings);
// Reinitialize the feature to apply changes immediately
if (feature) {
if (typeof feature.destroy === 'function') {
try { feature.destroy(); } catch (e) { /* ignore */ }
}
if (typeof feature.init === 'function') {
try { feature.init(); } 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');
}
// MultiSelect checkbox
if (e.target.matches('.ytkit-multiselect-cb')) {
const card = e.target.closest('[data-feature-id]');
const featureId = card.dataset.featureId;
const settingKey = e.target.dataset.settingKey;
const feature = features.find(f => f.id === featureId);
// Get current array and update it
let currentValues = appState.settings[settingKey] || [];
if (!Array.isArray(currentValues)) currentValues = [];
const value = e.target.value;
if (e.target.checked) {
if (!currentValues.includes(value)) {
currentValues.push(value);
}
} else {
currentValues = currentValues.filter(v => v !== value);
}
appState.settings[settingKey] = currentValues;
await settingsManager.save(appState.settings);
// Update button text
const btn = card.querySelector('.ytkit-multiselect-btn');
if (btn && feature) {
btn.textContent = `${currentValues.length} of ${feature.options.length} selected`;
}
// Reinitialize the feature to apply changes immediately
if (feature) {
if (typeof feature.destroy === 'function') {
try { feature.destroy(); } catch (err) { /* ignore */ }
}
if (typeof feature.init === 'function') {
try { feature.init(); } catch (err) { console.warn('[YTKit] Feature reinit error:', err); }
}
}
}
});
// Import/Export
doc.addEventListener('click', async (e) => {
if (e.target.closest('#ytkit-export')) {
const configString = await settingsManager.exportAllSettings();
handleFileExport('ytkit_settings.json', configString);
createToast('Settings exported successfully', 'success');
}
if (e.target.closest('#ytkit-import')) {
handleFileImport(async (content) => {
const success = await 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(`
/* ═══════════════════════════════════════════════════════════════════════════
YTKit Premium UI v6.0 - Professional Settings Panel
═══════════════════════════════════════════════════════════════════════════ */
@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 Download Buttons - Force Visibility */
.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;
}
/* Fallback button container */
.ytkit-button-container {
display: flex !important;
gap: 8px !important;
margin: 8px 0 !important;
flex-wrap: wrap !important;
visibility: visible !important;
}
/* Trigger Button */
.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);
}
/* Overlay */
#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;
}
/* Panel */
#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);
}
/* Header */
.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;
}
/* Body */
.ytkit-body {
display: flex;
flex: 1;
overflow: hidden;
}
/* Sidebar */
.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;
}
/* Search Box */
.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;
}
/* Sidebar Divider */
.ytkit-sidebar-divider {
height: 1px;
background: var(--ytkit-border);
margin: 8px 0 12px;
}
/* Search Active State */
.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;
}
/* Content */
.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); }
}
.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);
}
/* Reset Group Button */
.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;
}
/* Features Grid */
.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-radius: var(--ytkit-radius-md);
transition: all var(--ytkit-transition);
}
.ytkit-feature-card:hover {
background: var(--ytkit-bg-hover);
border-color: var(--ytkit-border);
}
.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);
}
/* Switch */
.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);
}
/* Footer */
.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);
}
/* Toast */
.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);
}
/* Watch page logo */
#yt-suite-watch-logo {
display: flex;
align-items: center;
margin-right: 12px;
}
#yt-suite-watch-logo a {
display: flex;
align-items: center;
}
#yt-suite-watch-logo ytd-logo {
width: 90px;
height: auto;
}
/* Layout fixes */
ytd-watch-metadata.watch-active-metadata {
margin-top: 180px !important;
}
ytd-live-chat-frame {
margin-top: -57px !important;
width: 402px !important;
}
/* Scrollbar */
.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);
}
/* ═══════════════════════════════════════════════════════════════════════════
Statistics Dashboard Styles
═══════════════════════════════════════════════════════════════════════════ */
.ytkit-stat-card {
background: linear-gradient(135deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02));
border: 1px solid var(--ytkit-border-subtle);
border-radius: var(--ytkit-radius-md);
padding: 16px;
text-align: center;
transition: all var(--ytkit-transition);
}
.ytkit-stat-card:hover {
background: linear-gradient(135deg, rgba(255,255,255,0.08), rgba(255,255,255,0.04));
border-color: var(--ytkit-border);
}
.ytkit-stat-value {
font-size: 24px;
font-weight: 700;
color: var(--ytkit-accent);
margin-bottom: 4px;
}
.ytkit-stat-label {
font-size: 12px;
color: var(--ytkit-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ═══════════════════════════════════════════════════════════════════════════
Profiles UI Styles
═══════════════════════════════════════════════════════════════════════════ */
.ytkit-profile-item:hover {
background: rgba(255,255,255,0.06) !important;
}
.ytkit-profile-item button:hover {
filter: brightness(1.1);
}
/* ═══════════════════════════════════════════════════════════════════════════
Custom CSS Editor Styles
═══════════════════════════════════════════════════════════════════════════ */
.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);
}
/* ═══════════════════════════════════════════════════════════════════════════
Bulk Operations Styles
═══════════════════════════════════════════════════════════════════════════ */
.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
// ══════════════════════════════════════════════════════════════════════════
async function main() {
appState.settings = await settingsManager.load();
appState.currentPage = getCurrentPage();
// Initialize keyboard manager with default settings shortcut
KeyboardManager.init();
KeyboardManager.register('ctrl+alt+y', () => {
document.body.classList.toggle('ytkit-panel-open');
}, 'Open YTKit Settings');
injectPanelStyles();
buildSettingsPanel();
injectSettingsButton();
attachUIEventListeners();
updateAllToggleStates();
// Initialize features with lazy-loading support
features.forEach(f => {
// For multiSelect features, check if the settingKey array has items
// For regular features, check if the feature is enabled
const isMultiSelect = f.type === 'multiSelect';
const settingKey = f.settingKey || f.id;
const isEnabled = isMultiSelect
? (appState.settings[settingKey] && appState.settings[settingKey].length > 0)
: appState.settings[f.id];
if (isEnabled) {
// Check if feature should run on this page (lazy-loading)
if (f.pages && !f.pages.includes(appState.currentPage)) {
return; // Skip this feature on this page
}
// Check feature dependencies
if (f.dependsOn && !appState.settings[f.dependsOn]) {
return; // Skip if dependency not enabled
}
try {
f.init?.();
DebugManager.log('Feature', `Initialized: ${f.id}`);
} catch (error) {
console.error(`[YTKit] Error initializing "${f.id}":`, error);
DebugManager.log('Error', `Failed to init ${f.id}`, error.message);
}
}
});
// 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 = await settingsManager.getFirstRunStatus();
if (!hasRun) {
document.body.classList.add('ytkit-panel-open');
await settingsManager.setFirstRunStatus(true);
}
// Track page changes for lazy loading
window.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 => {
const isMultiSelect = f.type === 'multiSelect';
const settingKey = f.settingKey || f.id;
const isEnabled = isMultiSelect
? (appState.settings[settingKey] && appState.settings[settingKey].length > 0)
: appState.settings[f.id];
if (isEnabled && f.pages) {
const wasActive = f.pages.includes(oldPage);
const shouldBeActive = f.pages.includes(newPage);
if (!wasActive && shouldBeActive) {
try { f.init?.(); } catch(e) {}
} else if (wasActive && !shouldBeActive) {
try { f.destroy?.(); } catch(e) {}
}
}
});
}
});
// Initialize statistics tracker
await StatsTracker.load();
console.log('[YTKit] v16 Initialized');
DebugManager.log('Init', 'YTKit v15 started', { page: appState.currentPage, features: Object.keys(appState.settings).filter(k => appState.settings[k]).length });
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
main();
} else {
window.addEventListener('DOMContentLoaded', main);
}
})();